Generate SSL certificates with acme.sh on Nginx.
In this article, we will see how to install and configure “acme.sh” to generate SSL certificates for domains and how to implement it with Nginx to secure the connection to corresponding websites hosted on our web server via “HTTPS”.
To optimize the security of connections to the web server and comply with all applicable guidelines, it is necessary to modify the configuration of Nginx, which we will also implement in this guide.
This tutorial requires you to be logged in as root, so switch to root user if you are not already.
sudo -i
Download “acme.sh” using the git repository and save it in the “/usr/local/src/” directory.
git clone https://github.com/Neilpang/acme.sh.git /usr/local/src/acme.sh -q
Create the necessary directories.
mkdir -p /etc/acme/{config,live,certs}
Switch to the directory where we saved “acme.sh”.
cd /usr/local/src/acme.sh
During the installation of “acme.sh” you will have to provide an email address to create an account that will also be used to send certificate renewal notifications.
For convenience, we put the e-mail address in a variable “ACME_EMAIL”. Replace “email@your_domain.tld” here with your own email address.
export ACME_EMAIL="email@your_domain.tld"
Before proceeding, it is advisable to check that the variable has the correct value.
echo $ACME_EMAIL
Which in this example should give you the following result.
email@your_domain.tld
Install “acme.sh” with the appropriate settings.
./acme.sh --install -m $ACME_EMAIL \ --home /etc/acme \ --config-home /etc/acme/config \ --cert-home /etc/acme/certs
To ensure that we have the latest version of “acme.sh”, it is advisable to upgrade now and adjust the configuration so that new versions in the future are also upgraded automatically.
/etc/acme/acme.sh --config-home '/etc/acme/config' --upgrade --auto-upgrade
Log out as root and then log in for the settings to take effect.
/etc/acme/acme.sh info
You should see an output similar to this:
LE_WORKING_DIR=/etc/acme LE_CONFIG_HOME=/etc/acme/config #LOG_FILE="/etc/acme/config/acme.sh.log" #LOG_LEVEL=1 AUTO_UPGRADE='1' #NO_TIMESTAMP=1 CERT_HOME='/etc/acme/certs' ACCOUNT_EMAIL='email@your_domain.tld' UPGRADE_HASH='cf3dd4c136f80aa56613c289b0da69243b4d264a' USER_PATH='/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/games:/snap/bin'
If you followed the previous article, you can skip the next two steps.
If not, we need to create an additional global configuration file “acme.conf” that provides an alias for the “.well-known/acme-challenge/” needed to enable http-01 validation over HTTP to request SSL certificates.
cat > /etc/nginx/global/acme.conf << EOF location /.well-known/acme-challenge/ { alias /var/www/acme/.well-known/acme-challenge/; } EOF
And create the corresponding directory.
mkdir -p -m 750 /var/www/acme/.well-known/acme-challenge && chown -R www-data. /var/www/acme
The easiest way to ensure that our web server supports secure parameters for Diffie-Hellman key exchange is to use a predefined key that can be downloaded from the Mozilla website.
curl https://ssl-config.mozilla.org/ffdhe4096.txt > /etc/ssl/private/ffdhe4096.pem
Verify that the certificate has been downloaded correctly.
cat /etc/ssl/private/ffdhe4096.pem
Which should give you the following output.
-----BEGIN DH PARAMETERS----- MIICCAKCAgEA//////////+t+FRYortKmq/cViAnPTzx2LnFg84tNpWp4TZBFGQz +8yTnc4kmz75fS/jY2MMddj2gbICrsRhetPfHtXV/WVhJDP1H18GbtCFY2VVPe0a 87VXE15/V8k1mE8McODmi3fipona8+/och3xWKE2rec1MKzKT0g6eXq8CrGCsyT7 YdEIqUuyyOP7uWrat2DX9GgdT0Kj3jlN9K5W7edjcrsZCwenyO4KbXCeAvzhzffi 7MA0BM0oNC9hkXL+nOmFg/+OTxIy7vKBg8P+OxtMb61zO7X8vC7CIAXFjvGDfRaD ssbzSibBsu/6iGtCOGEfz9zeNVs7ZRkDW7w09N75nAI4YbRvydbmyQd62R0mkff3 7lmMsPrBhtkcrv4TCYUTknC0EwyTvEN5RPT9RFLi103TZPLiHnH1S/9croKrnJ32 nuhtK8UiNjoNq8Uhl5sN6todv5pC1cRITgq80Gv6U93vPBsg7j/VnXwl5B0rZp4e 8W5vUsMWTfT7eTDp5OWIV7asfV9C1p9tGHdjzx1VA0AEh/VbpX4xzHpxNciG77Qx iu1qHgEtnmgyqQdgCpGBMMRtx3j5ca0AOAkpmaMzy4t6Gh25PXFAADwqTs6p+Y0K zAqCkc3OyX3Pjsm1Wn+IpGtNtahR9EGC4caKAH5eZV9q//////////8CAQI= -----END DH PARAMETERS-----
All other parameters needed to set up a secure connection that complies with applicable guidelines we bundle in a separate global configuration file “ssl.conf” that we can easily include in an Nginx server block.
cat > /etc/nginx/global/ssl.conf << EOF ssl_protocols TLSv1.2 TLSv1.3; ssl_prefer_server_ciphers on; ssl_dhparam /etc/ssl/private/ffdhe4096.pem; ssl_ecdh_curve secp384r1; ssl_ciphers EECDH+AESGCM:EDH+AESGCM; ssl_session_timeout 60m; ssl_session_cache shared:SSL:50m; ssl_buffer_size 8k; ssl_session_tickets off; ssl_stapling on; ssl_stapling_verify on; resolver 8.8.8.8 8.8.4.4 valid=300s; resolver_timeout 10s; EOF
In the next step, we will assume that, as described in a previous article, you have already created a virtual host for the domain “example.com” that we use here as an example and that it can be accessed via “HTTP” in a browser.
To avoid repeating the domain in the next steps and to minimize the chance of errors, we are going to define a variable “DOMAIN”.
export DOMAIN="example.com"
We are going to do the same for any alternate domains for which the certificate should be valid, which will be defined as “ALT_DOMAINS” variable.
By default, the certificate is requested for the domain (example.com) itself and not for “www” (www.example.com). Usually it is desirable that the certificate should also be valid for “www” so “www.example.com” should also be added to the “ALT_DOMAINS” variable.
If you have no other domains for which the certificate should be valid, just leave everything inside the quotes blank.
To make sure that the variable “ALT_DOMAINS” does not contain any records from previous processes we are going to empty it first. This is especially important if there are no further domains to be added to the certificate.
export ALT_DOMAINS="www.example.com example.net www.example.net example.eu www.example.eu"
So if you have no other domains for which the certificate should be valid, leave everything inside the quotes blank. Do not skip this step, otherwise the variable “ALT_DOMAINS” might contain records from previous processes.
export ALT_DOMAINS=""
Before proceeding, it is advisable to check that the variables have the correct value.
echo $DOMAIN \ && echo $ALT_DOMAINS
Which in this example should give you the following result.
example.com www.example.com example.net www.example.net example.eu www.example.eu
Make sure that all the domains in the “ALT_DOMAINS” variable are also included in the Nginx “server_name” directive and thus available via “HTTP” otherwise the certificate request procedure will not be performed.
We merge the the two variables into a new variable “DOMAINS”.
if [ -z "$ALT_DOMAINS" ]; then DOMAINS=$DOMAIN; else export DOMAINS="$DOMAIN " && export DOMAINS+=$ALT_DOMAINS; fi
If you are not sure, you should now re-create the Nginx server block for this domain.
cat > /etc/nginx/sites-available/$DOMAIN <<EOF server { listen 80; listen [::]:80; server_name $DOMAINS; root /var/www/virtual/$DOMAIN/htdocs/; index index.php index.html index.htm; include global/acme.conf; access_log /var/log/nginx/$DOMAIN-access.log; error_log /var/log/nginx/$DOMAIN-error.log; location / { try_files \$uri \$uri/ =404; } location ~ \.php$ { fastcgi_pass unix:/run/php/php8.2-fpm.sock; include global/fastcgi-php.conf; include global/security-headers.conf; } location ~ /\.ht { deny all; } } EOF
Once you have created a new server block, you do need to restart Nginx for the new configuration to take effect.
Check the configuration.
nginx -t
Which should give you the following result.
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok nginx: configuration file /etc/nginx/nginx.conf test is successful
If no errors occur, restart Nginx to implement the new configuration.
systemctl restart nginx
Now that Nginx is configured, the following command will put all the domain names in the correct order and ensure that each individual domain name is prefixed with the “acme.sh” option “-d” after which it is put into a new variable “ACME_DOMAINS”.
export ALT_DOMAIMS_ARRAY=($ALT_DOMAINS) && export ACME_ALT_DONAIMS=$(for i in ${ALT_DOMAIMS_ARRAY[@]}; do echo -n "-d $i "; done) && export ACME_DOMAINS="-d $DOMAIN $ACME_ALT_DONAIMS"
echo $ACME_DOMAINS
-d example.com -d www.example.com -d example.net -d www.example.net -d example.eu -d www.example.eu
Next, the actual certificate is requested.
/etc/acme/acme.sh --issue --home /etc/acme --config-home /etc/acme/config --cert-home /etc/acme/certs $ACME_DOMAINS -w /var/www/acme --ocsp-must-staple
If you experience problems and get errors when creating the certificate, you can run an online test on the Let’s Debug website that can identify the problem, if any.
By default, “acme.sh” uses ZeroSSL to issue certificates, but although this is a very good alternative to Let’s Encrypt it still sometimes wants to falter and a timeout occurs. You can easily switch to Let’s Encrypt in that case by adding “–server letsencrypt” to the following command.
/etc/acme/acme.sh --issue --home /etc/acme --config-home /etc/acme/config --cert-home /etc/acme/certs $ACME_DOMAINS -w /var/www/acme --ocsp-must-staple --server letsencrypt
Note: In particular, Let’s Encrypt often requires that an additional “CAA” record be created and thus may return an error message if not created.
Create a separate directory for the specific domain where the certificates will be stored.
mkdir -p /etc/acme/live/$DOMAIN
Install the certificate and also provide the command to be used after renewal in our case “systemctl reload nginx”.
/etc/acme/acme.sh --install-cert \ --home /etc/acme -d $DOMAIN \ --keypath /etc/acme/live/$DOMAIN/privkey.pem \ --fullchainpath /etc/acme/live/$DOMAIN/fullchain.pem \ --reloadcmd "systemctl reload nginx"
The script should also create a cronjob for renewal stored in “/var/spool/cron/crontabs/root”. To be sure, we are going to check that this has been done effectively.
cat /var/spool/cron/crontabs/root
Which should give you the following result.
# DO NOT EDIT THIS FILE - edit the master and reinstall. # (- installed on Sat Mar 25 11:57:24 2023) # (Cron version -- $Id: crontab.c,v 2.13 1994/01/17 03:20:37 vixie Exp $) 24 0 * * * "/etc/acme"/acme.sh --cron --home "/etc/acme" --config-home "/etc/acme/config" > /dev/null
For Nginx to use the new certificate and establish a secure connection via “HTTPS,” the server block must be modified.
If you have alternative domains (e.g. www.example.com), create a new server block with the command below:
cat > /etc/nginx/sites-available/$DOMAIN <<EOF server { listen 80; listen [::]:80; server_name $DOMAINS; include global/acme.conf; location / { return 301 https://$DOMAIN\$request_uri; } } server { listen 443 ssl http2; listen [::]:443 ssl http2; server_name $ALT_DOMAINS; ssl_certificate /etc/acme/live/$DOMAIN/fullchain.pem; ssl_certificate_key /etc/acme/live/$DOMAIN/privkey.pem; include global/ssl.conf; location / { return 301 https://$DOMAIN\$request_uri; } } server { listen 443 ssl http2; listen [::]:443 ssl http2; server_name $DOMAIN; ssl_certificate /etc/acme/live/$DOMAIN/fullchain.pem; ssl_certificate_key /etc/acme/live/$DOMAIN/privkey.pem; include global/ssl.conf; root /var/www/virtual/$DOMAIN/htdocs; index index.php index.html index.htm; access_log /var/log/nginx/$DOMAIN-access.log; error_log /var/log/nginx/$DOMAIN-error.log; location / { try_files \$uri \$uri/ =404; } location ~ \.php$ { fastcgi_pass unix:/run/php/php8.2-fpm.sock; include global/fastcgi-php.conf; } location ~ /\.ht { deny all; } } EOF
If you have no alternative domains create a new server block with the command below:
cat > /etc/nginx/sites-available/$DOMAIN <<EOF server { listen 80; listen [::]:80; server_name $DOMAINS; include global/acme.conf; location / { return 301 https://$DOMAIN\$request_uri; } } server { listen 443 ssl http2; listen [::]:443 ssl http2; server_name $DOMAIN; ssl_certificate /etc/acme/live/$DOMAIN/fullchain.pem; ssl_certificate_key /etc/acme/live/$DOMAIN/privkey.pem; include global/ssl.conf; root /var/www/virtual/$DOMAIN/htdocs; index index.php index.html index.htm; access_log /var/log/nginx/$DOMAIN-access.log; error_log /var/log/nginx/$DOMAIN-error.log; location / { try_files \$uri \$uri/ =404; } location ~ \.php$ { fastcgi_pass unix:/run/php/php8.2-fpm.sock; include global/fastcgi-php.conf; } location ~ /\.ht { deny all; } } EOF
Check the configuration.
nginx -t
Which should give you the following result.
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok nginx: configuration file /etc/nginx/nginx.conf test is successful
If no errors occur, restart Nginx to implement the new configuration.
systemctl restart nginx
Check the list of created certificates.
/etc/acme/acme.sh list
Which in our case should give the following result.
Main_Domain KeyLength SAN_Domains CA Created Renew example.com "ec-256" www.example.com ZeroSSL.com 2023-03-24T16:10:03Z 2023-05-22T16:10:03Z
You can also request detailed info on a specific domain.
/etc/acme/acme.sh info example.com
[Sun Mar 26 17:08:45 CEST 2023] The domain 'example.com' seems to have a ECC cert already, lets use ecc cert. DOMAIN_CONF=/etc/acme/certs/example.com_ecc/example.com.conf Le_Domain=example.com Le_Alt=www.example.com Le_Webroot=/var/www/acme Le_PreHook= Le_PostHook= Le_RenewHook= Le_API=https://acme.zerossl.com/v2/DV90 Le_Keylength=ec-256 Le_OCSP_Staple=1 Le_OrderFinalize=https://acme.zerossl.com/v2/DV90/order/6cWOgGe4XYldivPhWhJ1gQ/finalize Le_LinkOrder=https://acme.zerossl.com/v2/DV90/order/8c5OgHe4XYldiHPhWhJ1gQ Le_LinkCert=https://acme.zerossl.com/v2/DV90/cert/GhRE-Y7hgrJjmDFGb6KYhT Le_CertCreateTime=1679674203 Le_CertCreateTimeStr=2023-03-24T16:10:03Z Le_NextRenewTimeStr=2023-05-22T16:10:03Z Le_NextRenewTime=1684771803 Le_RealCertPath= Le_RealCACertPath= Le_RealKeyPath=/etc/acme/live/example.com/privkey.pem Le_ReloadCmd=systemctl reload nginx Le_RealFullChainPath=/etc/acme/live/example.com/fullchain.pem
You can view and validate the certificate chain after the deployment of the certificate by using:
openssl s_client -connect example.com:443 -showcerts
To check your SSL score, you can take an online test at ssllabs.com. If you have fully implemented all the steps in this tutorial, you should now get an A+.
Of course, we also strongly recommend enabling DNSSEC for the domain and generating a TLSA record based on the SSL certificate just created, as discussed in a previous article.
Although not strictly necessary, a “security.txt” can also be created and added to the web server.