Skip to content

Certificates with acme.sh (webroot) + permissions ​

In this section, we obtain TLS certificates for www.curvature.blog using acme.sh and webroot mode.

Goal: we can issue a cert successfully, and install the cert/key to a stable path for nginx (e.g. /etc/nginx/cert/).

Key idea: webroot mode requires nginx web root to be writable (at least temporarily) so ACME challenge files can be created.


3.1 Install acme.sh under a normal user ​

We run acme.sh under our non-root user (e.g. vpsadmin).

  • install acme.sh

    shell
    wget -O - https://get.acme.sh | sh
  • reload shell environment (so acme.sh command is available)

    shell
    source ~/.bashrc
  • (optional) upgrade acme.sh and enable auto-upgrade

    shell
    acme.sh --upgrade --auto-upgrade

3.2 Choose CA (ZeroSSL vs Let's Encrypt) ​

At some point, acme.sh may default to ZeroSSL and ask for account email / EAB credentials.
We switched to Let's Encrypt for simplicity.

  • set default CA to Let's Encrypt
    shell
    acme.sh --set-default-ca --server letsencrypt

3.3 Prepare Webroot Permissions (Crucial Step) ​

If /var/www/html is owned by root, acme.sh (running as vpsadmin) will fail to create the .well-known challenge directory, resulting in a Permission denied error.

  • change ownership of webroot to allow vpsadmin to write
  • ensure nginx (www-data) can still read it by setting permissions to 755
    shell
    sudo chown -R vpsadmin:vpsadmin /var/www/html
    sudo chmod -R 755 /var/www/html

3.4 Issue certificate using webroot mode ​

webroot mode works like this:

  • ACME server asks you to prove domain ownership
  • acme.sh writes a challenge file into <webroot>/.well-known/acme-challenge/...
  • ACME server fetches that file from your domain via HTTP (port 80).

This requires:

  • www.curvature.blog must already point to your VPS IP.
  • nginx must already serve HTTP on port 80.

Issue the cert:

shell
acme.sh --issue \
  -d www.curvature.blog \
  --webroot /var/www/html

After success, the cert and key are temporarily stored under the acme.sh home directory (e.g., ~/.acme.sh/www.curvature.blog_ecc/).


3.5 Prepare Cert Directory ​

We want nginx to read certs from a stable system path: /etc/nginx/cert/

  • create the directory if missing
    shell
    sudo mkdir -p /etc/nginx/cert
  • grant vpsadmin ownership of the directory so acme.sh can install files there (otherwise you will get touch: cannot touch... Permission denied).
    shell
    sudo chown vpsadmin:vpsadmin /etc/nginx/cert

3.6 Install cert/key and setup Auto-Renew ​

Certificates renew automatically because acme.sh runs on a cron schedule. But nginx must be reloaded to pick up the updated cert files. We use --reloadcmd so that every renewal automatically reloads nginx.

  • install the cert and define the reload command:
    shell
    acme.sh --install-cert -d www.curvature.blog \
    --key-file       /etc/nginx/cert/www.curvature.blog.key \
    --fullchain-file /etc/nginx/cert/www.curvature.blog.fullchain.cer \
    --reloadcmd      "sudo systemctl reload nginx"

⚠️ Edge Case (Re-running the command): If you ever need to re-run this install command after you have locked down the file permissions (Step 3.8), acme.sh will fail with cp: cannot open ... for reading: Permission denied. To fix this, you must temporarily give vpsadmin ownership of the files before installing again:

shell
sudo chown vpsadmin:vpsadmin /etc/nginx/cert/www.curvature.blog.key /etc/nginx/cert/www.curvature.blog.fullchain.cer

3.7 Allow passwordless nginx reload (Required for Cron) ​

Because --reloadcmd runs automatically in the background via cron under the vpsadmin user, it cannot prompt for a sudo password. We must explicitly allow vpsadmin to reload nginx without a password.

  • edit the sudoers file
    shell
    sudo visudo
  • add this exact line to the bottom of the file:
    vpsadmin ALL=(root) NOPASSWD: /usr/bin/systemctl reload nginx

3.8 Restrict permissions again (Crucial for Security) ​

After issuing the cert and installing it to the nginx path, it is critical to restrict permissions so your private key isn't exposed.

Our final goal: Let vpsadmin own the directory (so acme.sh can overwrite files on renewal), but lock down the files so only root (and Nginx) can read the private key.

  • apply the final security permissions:
    shell
    sudo chown -R vpsadmin:vpsadmin /etc/nginx/cert
    sudo chmod 700 /etc/nginx/cert
    sudo chmod 600 /etc/nginx/cert/www.curvature.blog.key
    sudo chmod 644 /etc/nginx/cert/www.curvature.blog.fullchain.cer

3.9 Quick checklist for this stage ​

After finishing this section, you should have:

  • acme.sh installed and configured for Let's Encrypt.
  • webroot permissions cleanly configured for ACME challenges.
  • A successfully issued certificate for www.curvature.blog
  • Cert/key installed securely under /etc/nginx/cert/
  • Passwordless sudo configured so acme.sh can automatically run systemctl reload nginx upon renewal
  • File and directory permissions strictly tightened.