Skip to content

Instantly share code, notes, and snippets.

@Jcpetrucci
Forked from Greelan/letsencrypt_notes.sh
Last active November 9, 2020 19:43
Show Gist options
  • Save Jcpetrucci/044898448c01d682ed532c084279a9b2 to your computer and use it in GitHub Desktop.
Save Jcpetrucci/044898448c01d682ed532c084279a9b2 to your computer and use it in GitHub Desktop.
Provision certificate via ACME using acme.sh as non-root user

Get started

Simply run:

./acme-nonroot.sh

You do not need to be root, but you do need to be able to sudo. You are the one running as sudo, not acme.sh, and this is only necessary during this one-time setup.

Variables

You can pre-define the variables which begin with CFG_ by uncommenting them for a non-interactive experience. If the variables are commented out, you will be prompted when they are needed.

Opt-out logging

Everything is logged to .acme-nonroot.sh.log automatically, so that you can refer back to things later. If you don't want logging, use the -r flag.

Troubleshooting

For debugging purposes, run ./acme-nonroot.sh -v[v[v]] - the more Vs, the more verbose the output will be.

About

acme.sh is fantastic, but it expects to be run as the root user. This script helps you set up an environment where acme.sh runs as a permission-limited user.

Core principals of acme-nonroot.sh:

  • Easy to read code, so you can review and understand everything we do
    • Long arguments where possible ("--verbose", not "-v")
    • Succinct comments
  • Easy to undo
  • Tolerates multiple executions
    • Things may not work perfectly the first time. If you run acme-nonroot.sh again, it won't make a mess of your configurations.
  • Logging by default
  • Separate and modular configuration files rather than inline edits to monolithic configuration (e.g. /etc/httpd/conf.d/wellknown-acmechallenge.conf, /etc/sudoers.d/99_acme)

To do

  • Support for other web server software (e.g. nginx)
  • Handle errors, such as when
    • curl acme.sh is blocked by firewall
    • multiple httpd/conf.d files complicate things
  • Support multiple --domain arguments
#!/bin/bash
# Run this script once to set up ACME.sh as a non-root user. After this script is done, it can be removed if you want. The script also can be run multiple times safely such that it will not create multiples of anything.
#CFG_ACME_USERNAME='acme' # What do you want to call the user who will fetch certificates?
#CFG_CERT_DOMAIN='tst-server.virtual.example.com' # What fully qualified domain name should the certificate be for?
#CFG_ACME_SERVER='https://acme-v02.api.letsencrypt.org/directory' # What is the ACME server we should get certificates from?
verbosity=2 # Start counting at 2 so that any increase to this will result in a minimum of file descriptor 3. You should leave this alone.
maxverbosity=5 # The highest verbosity we use / allow to be displayed. Feel free to adjust.
while getopts ":vr" opt; do
case $opt in
v) (( verbosity=verbosity+1 ));;
r) run="true" ;;
esac
done
# Use `script` utility to log output in case you want to review it later.
[[ "$run" == "true" ]] || exec script --return --quiet --command "$0 -r $*" --append ${0}.log
printf "%s %d. %s\n" "Verbosity level set to:" "$verbosity" "Increase with '-v[v]'."
for v in $(seq 3 $verbosity) # Start counting from 3 since 1 and 2 are standards (stdout/stderr).
do
(( "$v" <= "$maxverbosity" )) && eval exec "$v>&2" # Don't change anything higher than the maximum verbosity allowed.
done
for v in $(seq $(( verbosity+1 )) $maxverbosity ) # From the verbosity level one higher than requested, through the maximum;
do
(( "$v" > "2" )) && eval exec "$v>/dev/null" # Redirect these to bitbucket, provided that they don't match stdout and stderr.
done
function prePrompts(){
printf '\tdebug: %s%s%s\n' "$(tput setab 0)" "$BASH_COMMAND" "$(tput sgr0)" >&3
}
trap 'prePrompts' DEBUG
printf '%s running at %s\n' "$(basename $0)" "$(date)"
# Ensure a username has been chosen
until [[ -n "$CFG_ACME_USERNAME" ]]; do read -r -p "What do you want to call the user who will fetch certificates?: " CFG_ACME_USERNAME; done;
# Create a system user account and group for acme.sh to run as
sudo useradd --create-home --shell $(which nologin) "$CFG_ACME_USERNAME" 2>&4
# Temporarily allow shell use
shell_was=$(getent passwd "$CFG_ACME_USERNAME" | awk 'BEGIN{FS=":"} {print $NF}')
sudo usermod --shell /bin/bash "$CFG_ACME_USERNAME" 2>&4
# Permanently allow crontab use
if ! sudo grep -E "^${CFG_ACME_USERNAME}$" /etc/cron.allow >&4; then
printf 'Allowing %s to use crontab...\n' "$CFG_ACME_USERNAME" >&3
echo "$CFG_ACME_USERNAME" | sudo tee -a /etc/cron.allow
else
printf '%s already allowed to use crontab...\n' "$CFG_ACME_USERNAME" >&3
fi
# Permanently allow sudo use to restart the web server (to detect new certificates)
my_systemctl=$(which systemctl)
printf '%s ALL = NOPASSWD: %s restart httpd.service\n' "$CFG_ACME_USERNAME" "$my_systemctl" | sudo tee /etc/sudoers.d/99_acme >&3
# Permanently allow the user to write to /var/www/html/.well-known/acme-challenge/
sudo mkdir --verbose --parents /var/www/html/.well-known/acme-challenge 2>&3
sudo chgrp --verbose "$CFG_ACME_USERNAME" /var/www/html/.well-known/acme-challenge/ 1>&3
sudo chmod --verbose g+rwx /var/www/html/.well-known/acme-challenge/ 1>&3
# Permanently make location for our user to store key and certificates
for file in /etc/pki/tls/certs/acme.crt /etc/pki/tls/private/acme.key /etc/pki/tls/certs/acme-chain.crt; do
sudo touch $file;
sudo chown --verbose "$CFG_ACME_USERNAME" $file 1>&3;
done
sudo chmod --verbose o-rwx /etc/pki/tls/private/acme.key 1>&3
# Permanently expose /var/www/html/.well-known/acme-challenge/ for HTTP
sudo cat <<-EOF | sudo dd of=/etc/httpd/conf.d/wellknown-acmechallenge.conf 2>&4
<Directory "/var/www/html/.well-known/acme-challenge/" >
#Options Indexes FollowSymLinks
Require all granted
</Directory>
EOF
sudo systemctl restart httpd.service
# Download acme.sh as the user
sudo --user="$CFG_ACME_USERNAME" --set-home bash -c "cd ~; curl https://get.acme.sh | sh" 2>&3
# Issue certificate as the user
until [[ -n "$CFG_CERT_DOMAIN" ]]; do read -r -p "What domain should the certificate be for?: " CFG_CERT_DOMAIN; done;
until [[ -n "$CFG_ACME_SERVER" ]]; do read -r -p "What is the ACME server we should get certificates from?: " CFG_ACME_SERVER; done;
sudo CFG_CERT_DOMAIN="$CFG_CERT_DOMAIN" CFG_ACME_SERVER="$CFG_ACME_SERVER" --user="$CFG_ACME_USERNAME" --set-home bash -c 'cd ~; .acme.sh/acme.sh --force --issue --domain "$CFG_CERT_DOMAIN" --server $CFG_ACME_SERVER --webroot /var/www/html' 2>&3
# Put the certificate where the web server can use it
sudo CFG_CERT_DOMAIN="$CFG_CERT_DOMAIN" --user="$CFG_ACME_USERNAME" --set-home bash -c 'cd ~; .acme.sh/acme.sh --force --install-cert --domain "$CFG_CERT_DOMAIN" --cert-file /etc/pki/tls/certs/acme.crt --key-file /etc/pki/tls/private/acme.key --fullchain-file /etc/pki/tls/certs/acme-chain.crt --reloadcmd "sudo systemctl restart httpd.service"' 2>&3
# Permanently configure web server to use the new certificate
sudo sed --in-place --regexp-extended 's|^SSLCertificateFile .+|SSLCertificateFile /etc/pki/tls/certs/acme.crt|' /etc/httpd/conf.d/ssl.conf
sudo sed --in-place --regexp-extended 's|^SSLCertificateKeyFile .+|SSLCertificateKeyFile /etc/pki/tls/private/acme.key|' /etc/httpd/conf.d/ssl.conf
sudo sed --in-place --regexp-extended 's|^#?SSLCertificateChainFile .+|SSLCertificateChainFile /etc/pki/tls/certs/acme-chain.crt|' /etc/httpd/conf.d/ssl.conf
sudo systemctl restart httpd.service
# Re-disable shell use
sudo usermod --shell "$shell_was" "$CFG_ACME_USERNAME" 2>&3
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment