| |

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.

Similar Posts