Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
Set up Let’s Encrypt certificate using acme.sh as non-root user
# How to use acme.sh to set up Let's Encrypt, with the script being run
# mostly without root permissions
# See https://github.com/Neilpang/acme.sh for more
# These instructions use the domain "EXAMPLE.COM" as an example
# These instructions:
# - work on Ubuntu 18.04 and 20.04 with nginx
# - use CloudFlare DNS validation
# - set up a wildcard certificate for the "EXAMPLE.COM" domain
# - use a systemd service, rather than cron job, to renew the certificate
# When this is done, there will be an "acme" user that handles issuing,
# updating, and installing certificates. This user will have the following
# (fairly minimal) permissions:
# - Copy certificates and key to /etc/letsencrypt/EXAMPLE.COM
# - Reload your nginx server
# First things first - create a system user account and group for acme
sudo useradd -m -d /var/lib/acme -s /usr/sbin/nologin -r -U acme
sudo chmod 700 /var/lib/acme
# Create a directory for the acme account to save certs in
MYDOMAIN="EXAMPLE.COM"
sudo mkdir -m 710 /etc/letsencrypt/"$MYDOMAIN"
sudo chown acme.www-data /etc/letsencrypt/"$MYDOMAIN"
# Edit your sudoers file to allow the acme user to reload (not restart) nginx
sudo visudo
# Add the following line at the end:
acme ALL=(ALL) NOPASSWD: /bin/systemctl reload nginx.service
# Now change to the "acme" user - you'll do most of the rest of this guide as them
sudo MYDOMAIN="$MYDOMAIN" -s -u acme bash
export HOME=/var/lib/acme
cd ~
# Install acme.sh
git clone https://github.com/Neilpang/acme.sh.git
cd acme.sh
./acme.sh --install
# Export your CloudFlare API token and account ID so that acme.sh can use them
# See https://github.com/Neilpang/acme.sh/wiki/dnsapi for more about API tokens
# You can find your account ID in the URL of any page within the Cloudflare Dashboard
# after selecting the appropriate account
export CF_Token="[insert]"
export CF_Account_ID="[insert]"
# Create your certificate
# Note that the "force" flag is needed for the below commands as otherwise
# the acme.sh script complains about being run as sudo
cd ~
.acme.sh/acme.sh --issue -d "$MYDOMAIN" -d *."$MYDOMAIN" --dns dns_cf --force
# If everything went well, install your certificate
.acme.sh/acme.sh --install-cert --domain "$MYDOMAIN" \
--ca-file /etc/letsencrypt/"$MYDOMAIN"/chain.pem \
--key-file /etc/letsencrypt/"$MYDOMAIN"/key.pem \
--fullchain-file /etc/letsencrypt/"$MYDOMAIN"/fullchain.pem \
--reloadcmd "sudo systemctl reload nginx.service" --force
# Disable default cron job as a systemd service will be created instead for renewal
.acme.sh/acme.sh --uninstall-cronjob --force
# Drop back to your own user
exit
# Now modify your nginx config to work with the new certs
sudo nano /etc/nginx/sites-enabled/"$MYDOMAIN"
# Example SSL config section
#server {
# ...
# ssl_certificate /etc/letsencrypt/EXAMPLE.COM/fullchain.pem;
# ssl_certificate_key /etc/letsencrypt/EXAMPLE.COM/key.pem;
# ssl_trusted_certificate /etc/letsencrypt/EXAMPLE.COM/chain.pem;
# include /etc/nginx/ssl/ssl_params; # Change to whatever SSL settings you use - see further below
# ...
#}
# acme.sh does not create its own suggested SSL settings for you to use with nginx,
# so you will need to create your own (if you haven't already)
# The following commands set up SSL parameters of a reasonable level of security -
# relax or harden as you see fit (eg to include OCSP stapling), or skip if you
# already have your own. See https://ssl-config.mozilla.org/ for suggestions
sudo mkdir /etc/nginx/ssl
sudo tee /etc/nginx/ssl/ssl_params >/dev/null <<EOF
ssl_session_cache shared:SSL:10m;
ssl_session_timeout 1d;
ssl_protocols TLSv1.2 TLSv1.3;
ssl_prefer_server_ciphers off;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:\
ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:\
ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:\
DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384;
ssl_ecdh_curve X25519:secp384r1;
ssl_session_tickets off;
ssl_dhparam /etc/nginx/ssl/dhparam.pem;
EOF
sudo openssl dhparam -out /etc/nginx/ssl/dhparam.pem 2048
# Now test nginx to see if everything is working
sudo nginx -t
# And reload if it worked
sudo systemctl reload nginx.service
# Create a systemd service and timer to renew the certificate and reload nginx
sudo tee /etc/systemd/system/acme_letsencrypt.service >/dev/null <<EOF
[Unit]
Description=Renew Let's Encrypt certificates using acme.sh
After=network-online.target
[Service]
Type=oneshot
User=acme
Group=acme
Environment="HOME=/var/lib/acme"
ExecStart=/var/lib/acme/.acme.sh/acme.sh --cron
SuccessExitStatus=0 2
EOF
sudo tee /etc/systemd/system/acme_letsencrypt.timer >/dev/null <<EOF
[Unit]
Description=Daily renewal of Let's Encrypt's certificates
[Timer]
OnCalendar=daily
RandomizedDelaySec=1h
Persistent=true
[Install]
WantedBy=timers.target
EOF
# Now start and enable
sudo systemctl start acme_letsencrypt.timer
sudo systemctl enable acme_letsencrypt.timer
# Congrats, you have a Let's Encrypt wildcard certificate set up on your box
# and it is configured to automatically renew, all by running the acme.sh script mostly
# without root permissions (other than to reload nginx on renewal).
@PaulWebster

This comment has been minimized.

Copy link

@PaulWebster PaulWebster commented Jan 16, 2020

Thanks for this.
I came across a problem when trying it in my environment. Here is what I found and how I solved it.
I do not know if this is a general problem - but have included a way to test for it.

The problem that I hit was that nginx was happily serving up https but some clients were reporting issues with certificate chain validation.
To show the problem I used:
openssl s_client -connect example.com:443 -servername example.com
If that does not report a problem then things are probably fine.
In my case it was reporting an issue.

My solution was to change the way that acme.sh was making the exported certs/key.
Instead of creating .cer files, I changed it to make .pem

.acme.sh/acme.sh --install-cert --domain EXAMPLE.COM
--key-file /etc/letsencrypt/EXAMPLE.COM/EXAMPLE.COM.pem
--fullchain-file /etc/letsencrypt/EXAMPLE.COM/fullchain.pem
--reloadcmd "sudo systemctl reload nginx.service" --force

and then configured nginx to use those 2 files rather than the 3 .cer files

@Greelan

This comment has been minimized.

Copy link
Owner Author

@Greelan Greelan commented Jan 16, 2020

Thanks for the feedback. So is this issue effectively what is the problem you found, ie you need a key file that is the combined domain cert and key? acme.sh doesn't appear to generate that in the first instance (ie, what is stores in /var/lib/acme/.acme.sh/EXAMPLE.COM is ca.cer, fullchain.cer, EXAMPLE.COM.cer, and EXAMPLE.COM.key), but is the effect of the modified --install-cert command you ran that it does that work for you, ie concatenates the cert and key and installs them as a pem?

@Greelan

This comment has been minimized.

Copy link
Owner Author

@Greelan Greelan commented Jan 16, 2020

Ah, I see I should have followed Neil's instructions more closely on this one. Will update the gist (and my installation!).

@Greelan

This comment has been minimized.

Copy link
Owner Author

@Greelan Greelan commented Jan 19, 2020

Gist has been updated, including to add the cert-file parameter, which will enable OCSP stapling to be used. Thanks again for the input.

@PaulWebster

This comment has been minimized.

Copy link

@PaulWebster PaulWebster commented Jan 19, 2020

Excellent

@Greelan

This comment has been minimized.

Copy link
Owner Author

@Greelan Greelan commented Feb 14, 2020

Correction: should use ca-file parameter for OCSP stapling, although given ssl_certificate already uses fullchain.pem it is probably not even necessary now to use ssl_trusted_certificate. Gist updated to reflect modified parameter, as well as to update the SSL parameters to reflect that support for TLSv1.1 will be dropped by most browsers in March 2020.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.