Docker image allowing to generate, renew, revoke RSA and/or ECDSA SSL certificates from LetsEncrypt CA using certbot and acme.sh clients in automated fashion.
See also my blog post RSA and ECDSA hybrid Nginx setup with LetsEncrypt certificates that shows a primer for this docker image.
This project might be one you're looking for, if:
- you need to obtain new SSL certificate for your new shiny domain/website
- you need simple domain validated (DV) certificate, and you don't want to pay money to Certificate Authorities (CA), like DigiCert or Symantec for humble DV certificates.
- you've heard about LetsEncrypt CA, which allows to automate issueance of free DV certificates, but you're lazy enough to learn in-depth and don't want to spent much time there.
- you need to have both RSA and ECDSA certificates
- you're learning LetsEncrypt and want to check out certbot and acme.sh clients usage primers
- you're using Docker to deploy/run your app and services, and you're going to automate process of certificate issueance and/or renewal (e.g as a part of CI/CD process).
So, this Docker image provides a simple single entrypoint to obtain and manage SSL certificates from LetsEncrypt CA. It encapsulates two popular ACME clients: certbot and acme.sh, which are used to obtain RSA and/or ECDSA certificates respectively.
Following single responsibilty principle, this image cares only about how to talk to LetsEncrypt CA to provide you with a certificate, and it's completely unaware and not coupled with web server software or any other infrastructure service. This approach makes it a more versatile tool and unlocks greater number of use cases.
You can use it ad-hoc at a build time, at a run-time prior to Nginx/Apache startup, or by running it from cron job to renew certificates on regular basis. LetsEncrypt stuff stays within a single container, and you don't need to pollute your Nginx/Apache container.
Here is a list of notable features:
- automate issueance and managing LetsEncrypt SSL certificates
- generate DV certificates for 1..N domains
- support multi-domain SAN (Subject alternative names) certificates
- generate RSA and/or ECDSA certificate with configurable key params: RSA key length (2048, 3072, 4096) and elliptic curve for EC key (prime256v1, secp384r1)
- choose DV challenge verification method: standalone or webroot
- renew certificates when they're about to expire or force renewal
- revoke certificates by contacting LetsEncrypt CA
- use either LetsEncrypt staging or production server
It's assumed you already have a domain name, a server, and a working DNS configuration with at least "A" record mapping name to your server's IP address.
In a standalone mode, you need to run this image on that server, with 80 port opened by firewall, so ACME http-01 challenge verification succeeds.
Let's say I have foobbz.site
domain, DigitalOcean droplet with running Docker Engine (188.166.168.213), and DNS "A" record "foobbz.site"->"188.166.168.213".
Let's issue new RSA (2048 bit length) and ECDSA (prime256v1 curve) certificate for single domain "foobbz.site":
docker run \
-v /var/ssl:/var/ssl \
-p 80:80 \
-e DOMAINS=foobbz.site \
--rm \
asamoshkin/letsencrypt-certgen issue
Once done, container stops and is automatically removed (--rm). Certificates, keys and related files are stored in /var/ssl/foobbz.site
:
# tree /var/ssl
/var/ssl
└── foobbz.site
├── certs
│ ├── cert.ecc.pem
│ ├── cert.rsa.pem
│ ├── chain.ecc.pem
│ ├── chain.rsa.pem
│ ├── fullchain.ecc.pem
│ └── fullchain.rsa.pem
└── private
├── privkey.ecc.pem
└── privkey.rsa.pem
All files are encoded in PEM format.
cert.rsa.pem
,cert.ecc.pem
- generated certificate (RSA or ECDSA)chain.[type].pem
- chain of intermediate CA certificates (e.g. Fake LE Intermediate X1)fullchain.[type].pem
- generated certificate bundled with intermediate CA certificates. Suitable for Nginx configuration directivessl_certificate
, which should point to a bundle, instead of individual certificate.privkey.[type].pem
- private key file
Given that, you can then mount /var/ssl:/etc/nginx/ssl
into Nginx container and configure it to use RSA or ECDSA key or even both.
# RSA certificates
ssl_certificate /etc/nginx/ssl/foobbz.site/certs/fullchain.rsa.pem;
ssl_certificate_key /etc/nginx/ssl/foobbz.site/private/privkey.rsa.pem;
# ECDSA certificates
ssl_certificate /etc/nginx/ssl/foobbz.site/certs/fullchain.ecc.pem;
ssl_certificate_key /etc/nginx/ssl/foobbz.site/private/privkey.ecc.pem;
You're not limited to certificate with single domain only. You can generate several individual certificates for different domains. Or you can have single multi-domain SAN (Subject Alternate Names) certificate. Or both.
Prepare domains.txt
file. Each line represents individual certificate to be issued. First name within each line is a common name, subsequent comma-separated names are certificate alternative names.
# cat /root/domains.txt
foobbz.site,www.foobbz.site,web.foobbz.site
foobbz2.site,www.foobbz.site
Tell container to pick up domains list from domains.txt
. $DOMAINS
variable is double-purpose: it indicates either domains list as a string or points to a file with a domains list.
docker run \
-v /var/ssl:/var/ssl \
-v /root/domains.txt:/etc/domains.txt \
-p 80:80 \
-e DOMAINS=/etc/domains.txt \
--rm \
asamoshkin/letsencrypt-certgen issue
As a result, we have 2 individual certificates generated:
ls /var/ssl
foobbz.site
foobbz2.site
And let's check out how multiple domains are stored in the certificate in X.509 SAN extension.
docker run -v /var/ssl:/var/ssl --entrypoint sh --rm -it alpine
/ # apk --update add openssl
/ # openssl x509 -in /var/ssl/foobbz.site/certs/cert.rsa.pem -noout -text
Issuer: CN=Fake LE Intermediate X1
Subject: CN=foobbz.site
...
X509v3 extensions:
X509v3 Subject Alternative Name:
DNS:foobbz.site, DNS:web.foobbz.site, DNS:www.foobbz.site
Each LetsEncrypt client (certbot, acme.sh) manages its own place to store certificates, keys, account keys and various settings. You need to ensure this location is stored outside of the container for persistency.
docker volume create --name ssl
docker volume create --name acme
docker volume create --name letsencrypt
docker run \
-v ssl:/var/ssl \
-v acme:/etc/acme \
-v letsencrypt:/etc/letsencrypt \
-p 80:80 \
-e DOMAINS=foobbz.site \
--rm \
asamoshkin/letsencrypt-certgen issue
/etc/acme
and /etc/letsencrypt
are just internal storages of acme.sh
and certbot
clients, which are used under the hood. They contain certificates, keys, various settings, but we don't use them directly as their structure varies and is a subject to change. Therefore, /var/ssl
volume serves as a target drop location for certificates and keys. You should mount /var/ssl
into any container, that needs certificates (e.g. Nginx).
Once you enabled persistency for "certbot" and "acme.sh" clients internal storage, you can perform management actions, like renewing, revoking or deleting a certificate.
LetsEncrypt CA issues short-lived certificates which are only valid for 90 days. While renewing, it will check certificate validity period. If it's not due to expire (more than 1 month before expiration date), existing certificate will be kept. You can force renewal:
docker run \
-v ssl:/var/ssl \
-v acme:/etc/acme \
-v letsencrypt:/etc/letsencrypt \
-p 80:80 \
-e DOMAINS=foobbz.site \
-e FORCE_RENEWAL=1 \
--rm \
asamoshkin/letsencrypt-certgen renew
Use revoke
or delete
commands to trigger respective actions. Use $DOMAINS
variable to specify particular domain to revoke or delete. It's ok to tell just common name, no need to specify all alternative names, as you did for issue
command.
When revoking certificate, it will not remove files neither from internal storages, nor from /var/ssl
volume. On the other hand, deleting certificate removes from both locations, but do not revoke certificate by contacting LetsEncrypt CA.
When issuing certificate, CA needs to verify domain ownership. This project uses simple http-01
method.
Here is how it works. LetsEncrypt client creates a special file. CA contacts foobbz.site
domain on port 80 with GET /.well-known/acme-challenge
request for that file. If request succeeds, it proves the domain ownership.
In standalone mode, during challenge verification certbot
or acme.sh
spin up an embedded web server, which listens on port 80 and is capable of serving that file. This is a default setting.
If you already have a running web server on port 80, you can opt for webroot
mode. acme.sh
or certbot
will just store the file at predefined location, and your web server will handle serving it from that location at particular url.
Create a dedicated volume:
docker volume create --name acme_challenge_webroot
When running Nginx container, make sure to mount it:
docker run \
-v ssl:/etc/nginx/ssl
-v acme_challenge_webroot:/var/www/acme_challenge_webroot
-p 80:80 \
-p 443:443 \
--name web
my-nginx-container
Configure Nginx to serve /.well-known/acme-challenge
requests from that volume:
server {
listen 80;
server_name foobbz.site www.foobbz.site;
location ^~ /.well-known/acme-challenge {
allow all;
root /var/www/acme_challenge_webroot;
default_type text/plain;
}
}
Finally, run this image in webroot
mode to issue/renew certificates. Tip: you can do this from cron job to renew on regular basis. Note, when using webroot
method, there is no need to expose 80 port on this container any more.
docker run \
-v ssl:/var/ssl \
-v acme:/etc/acme \
-v letsencrypt:/etc/letsencrypt \
-v acme_challenge_webroot:/var/acme_challenge_webroot \
-e DOMAINS=foobbz.site \
-e CHALLENGE_MODE=webroot \
--rm \
asamoshkin/letsencrypt-certgen renew
Main use case for webroot
method, is the ability to renew certificates, without a need to stop you existing web server and running applications.
certbot
is not capable of generating ECDSA yet (except from custom CSR). So, cerbot
is used for RSA, whereas acme.sh
is for ECDSA.
Default is to generate both. But you can disable one or another using $RSA_ENABLED
and $ECDSA_ENABLED
environment variables.
Also, you can configure RSA key length: 2048, 3072 or 4096. For ECDSA key, you can tell elliptic curve: prime256v1 (ec-256), secp384r1 (ec-384), secp521r1 (ec-521, not yet supported by LetsEncrypt CA).
For example:
docker run \
-v ssl:/var/ssl \
-p 80:80 \
-e DOMAINS=foobbz.site \
-e RSA_ENABLED=0
-e ECDSA_KEY_LENGTH=ec-384
--rm \
asamoshkin/letsencrypt-certgen renew
Note, that ECDSA certificates are still signed by LetsEncrypt's RSA certificate chain (Fake LE Intermediate X1, Fake LE Root X1). LetsEncrypt does not use dedicated EC certificates to sign for complete EC chain.
You might also request certificate with OCSP must-staple extension, by passing MUST_STAPLE=1
environment variable. Yes, LetsEncrypt supports issuing certificates with OCSP must-staple flag.
Be aware, that LetsEncrypt CA production servers put strict rate limits:
- certificates per Registered Domain (20 per week)
- up to 100 alternative names per certificate
- duplicate certificate limit of 5 certificates per week
While you're trying and experimenting, it's better to use LetsEncrypt staging environment with much relaxed limits:
- The Certificates per Registered Domain limit is 30,000 per week.
- The Duplicate Certificate limit is 30,000 per week.
- The Failed Validations limit is 60 per hour.
- The Accounts per IP Address limit is 50 accounts per three 3 hour period per IP.
Using staging server is a default option here. To switch to production servers, set STAGING=0
environment variable.
When using sharing volumes, permissions and ownership issue needs to be resolved.
It's a good practice to restrict permissions for private key files, so it's not world accessible (umask 007
), or even group accessible (umask 077
). On the other hand, we need to make sure that SSL certificates/keys can be read when mounted into another container, which runs as a less-priviledged non-root user (Nginx running as nginx:nginx).
The solution is to set group ownership to a dedicated GID, and let less-priviledged user in other containers join that group to access files. When running container, you can override GID (default is 1337
):
docker run \
-v ssl:/var/ssl \
-p 80:80 \
-e DOMAINS=foobbz.site \
-e SSL_GROUP_ID=1561
--rm \
asamoshkin/letsencrypt-certgen renew
Check out permissions and ownership of created certificates:
tree -pug /var/ssl
/var/ssl
└── [drwxr-xr-x root 1561] foobbz.site
├── [drwxr-xr-x root 1561] certs
│ ├── [-rw-r--r-- root 1561] cert.ecc.pem
│ ├── [-rw-r--r-- root 1561] cert.rsa.pem
│ ├── [-rw-r--r-- root 1561] chain.ecc.pem
│ ├── [-rw-r--r-- root 1561] chain.rsa.pem
│ ├── [-rw-r--r-- root 1561] fullchain.ecc.pem
│ └── [-rw-r--r-- root 1561] fullchain.rsa.pem
└── [drwxr-x--- root 1561] private
├── [-rw-r----- root 1561] privkey.ecc.pem
└── [-rw-r----- root 1561] privkey.rsa.pem
You can see that all files has root:1561
ownership. Note, it's not required to create a real group in /etc/group
, it's enough to just assign numeric GIDs.
/var/ssl/foobbz.site/private
directory has 750
perm mode, and key files inside it are 640
. So it's not world accessible, and only user with a dedicated group membership can read those files.
When trying or experimenting with this image, it becomes tough to type long docker run
commands. Use docker-compose
instead:
docker-compose build && docker-compose run --rm -p 80:80 certgen issue
And docker-compose.yml
file looks like:
version: '2'
services:
certgen:
image: samoshkin/letsencrypt-certgen
environment:
- DOMAINS=foobbz.site,www.foobbz.site,web.foobbz.site
- VERBOSE=1
volumes:
- letsencrypt:/etc/letsencrypt
- acme:/etc/acme
- ssl:/var/ssl
- acme_challenge_webroot:/var/acme_challenge_webroot
volumes:
letsencrypt:
acme:
ssl:
acme_challenge_webroot: