From 1bd19d6dee1799e0ed472941a6ff3f47dc01e4d6 Mon Sep 17 00:00:00 2001 From: Evilham Date: Mon, 23 Mar 2020 12:26:59 +0100 Subject: [PATCH] [__letsencrypt_acmetiny] Simpler alternative to certbot. This is inspired heavily by `debops.pki` in the https://debops.org project. However there are several simplifications to their way of doing it. --- .../__letsencrypt_acmetiny/gencode-remote | 109 ++++++++++ .../conf/type/__letsencrypt_acmetiny/manifest | 1 + .../type/__letsencrypt_acmetiny/nonparallel | 0 .../parameter/optional_multiple | 1 + .../gencode-remote | 12 ++ .../type/__letsencrypt_acmetiny_base/manifest | 202 ++++++++++++++++++ .../parameter/optional | 1 + .../__letsencrypt_acmetiny_base/singleton | 0 8 files changed, 326 insertions(+) create mode 100644 cdist/conf/type/__letsencrypt_acmetiny/gencode-remote create mode 100644 cdist/conf/type/__letsencrypt_acmetiny/manifest create mode 100644 cdist/conf/type/__letsencrypt_acmetiny/nonparallel create mode 100644 cdist/conf/type/__letsencrypt_acmetiny/parameter/optional_multiple create mode 100644 cdist/conf/type/__letsencrypt_acmetiny_base/gencode-remote create mode 100644 cdist/conf/type/__letsencrypt_acmetiny_base/manifest create mode 100644 cdist/conf/type/__letsencrypt_acmetiny_base/parameter/optional create mode 100644 cdist/conf/type/__letsencrypt_acmetiny_base/singleton diff --git a/cdist/conf/type/__letsencrypt_acmetiny/gencode-remote b/cdist/conf/type/__letsencrypt_acmetiny/gencode-remote new file mode 100644 index 00000000..466b889d --- /dev/null +++ b/cdist/conf/type/__letsencrypt_acmetiny/gencode-remote @@ -0,0 +1,109 @@ +#!/bin/sh -e + +ACME_TINY_CERT_REQUEST_DIR="/var/acme-tiny/cert-requests" +ACME_TINY_ACCOUNT_KEY="/var/acme-tiny/account.key" +ACME_CHALLENGE_DIR="/srv/www/sites/acme/public/.well-known/acme-challenge" + +REALM="${__object_id}" +EXTRA_DOMAINS="" +if [ -f "${__object}/parameter/extra-domain" ]; then + EXTRA_DOMAINS="$(cat "${__object}/parameter/extra-domain")" +fi + +#TODO: support linux too +REALM_DIR="/usr/local/etc/pki/realms/${REALM}" +REALM_CERT="${REALM_DIR}/default.crt" +REALM_KEY="${REALM_DIR}/default.key" +REALM_CERT_REQUEST="${ACME_TINY_CERT_REQUEST_DIR}/${REALM}.csr" +REALM_CERT_REQUEST_CNF="${ACME_TINY_CERT_REQUEST_DIR}/${REALM}.cnf" + +CSR_ALT_NAMES="" +REALM_CERT_REQUEST_CNF_LINE="" +if [ -n "${EXTRA_DOMAINS}" ]; then + CSR_ALT_NAMES="DNS:${REALM}" + for domain in ${EXTRA_DOMAINS}; do + CSR_ALT_NAMES="${CSR_ALT_NAMES},DNS:${domain}" + done + # CSR requests are executed always against .new, only after succeeding .new replaces the .cnf + REALM_CERT_REQUEST_CNF_LINE="-reqexts SAN -config '${REALM_CERT_REQUEST_CNF}.new'" +fi + +cat << EOF +if [ ! -d '${REALM_DIR}' ]; then + mkdir -p '${REALM_DIR}' +fi +if [ ! -f '${REALM_KEY}' ]; then + openssl genrsa 4096 > '${REALM_KEY}' +fi + +if [ ! -d '${ACME_TINY_CERT_REQUEST_DIR}' ]; then + mkdir '${ACME_TINY_CERT_REQUEST_DIR}' +fi + +FORCE_CSR_REGEN="" +if [ -n '${CSR_ALT_NAMES}' ]; then + # Generate new config + cat /etc/ssl/openssl.cnf > '${REALM_CERT_REQUEST_CNF}.new' + printf '[SAN]\nsubjectAltName=${CSR_ALT_NAMES}' >> '${REALM_CERT_REQUEST_CNF}.new' + # Compare to previous config if necessary + if [ -f '${REALM_CERT_REQUEST_CNF}' ]; then + CNF_DIFF=\$(diff -q '${REALM_CERT_REQUEST_CNF}' '${REALM_CERT_REQUEST_CNF}.new' || true) + if [ -n "\${CNF_DIFF}" ]; then + # Options have changed + FORCE_CSR_REGEN="YES" + else + # Since they match, we won't be using this, clean it + rm '${REALM_CERT_REQUEST_CNF}.new' + fi + else + # We never used SAN here, CSR regen needed. + FORCE_CSR_REGEN="YES" + fi +else + # We used SAN at some point, not any more + if [ -f '${REALM_CERT_REQUEST_CNF}' ]; then + rm '${REALM_CERT_REQUEST_CNF}' + FORCE_CSR_REGEN="YES" + fi +fi + +# Create or re-create when params have changed +if [ ! -f '${REALM_CERT_REQUEST}' -o -n "\${FORCE_CSR_REGEN}" ]; then + openssl req -new -sha256 -key '${REALM_KEY}' -subj '/CN=${REALM}' -out '${REALM_CERT_REQUEST}' ${REALM_CERT_REQUEST_CNF_LINE} +fi + +# Check if cert exists, and if so whether or not it's older than a month +if [ -f '${REALM_CERT}' ]; then + MODIFIED_IN_30d="\$(find '${REALM_CERT}' -mtime -30d)" + if [ -z "\${MODIFIED_IN_30d}" ]; then + # Cert is over a month old, it's fine to regenerate + FORCE_CRT_REGEN="YES" + fi +else + # This cert doesn't exist + FORCE_CRT_REGEN="YES" +fi + + +# Only request certificate when needed +# TODO: support linux too +if [ -n "\${FORCE_CSR_REGEN}" -o -n "\${FORCE_CRT_REGEN}" ]; then + doas -u acme-tiny -- acme_tiny \ + --account '${ACME_TINY_ACCOUNT_KEY}' \ + --csr '${REALM_CERT_REQUEST}' \ + --acme-dir '${ACME_CHALLENGE_DIR}' > '${REALM_CERT}.new' + + if [ -s '${REALM_CERT}.new' ]; then + mv '${REALM_CERT}.new' '${REALM_CERT}' + else + echo "Failed to generate cert for realm '${REALM}'." + exit 1 + fi +fi + +if [ -n '${REALM_CERT_REQUEST_CNF_LINE}' -a -f '${REALM_CERT_REQUEST_CNF}.new' ]; then + # CSR and cert generation succeded with a new config, put new config in-place. + # This is the last thing we do, so we try again next time if sth fails. + mv '${REALM_CERT_REQUEST_CNF}.new' '${REALM_CERT_REQUEST_CNF}' +fi +EOF diff --git a/cdist/conf/type/__letsencrypt_acmetiny/manifest b/cdist/conf/type/__letsencrypt_acmetiny/manifest new file mode 100644 index 00000000..48438abb --- /dev/null +++ b/cdist/conf/type/__letsencrypt_acmetiny/manifest @@ -0,0 +1 @@ +#__letsencrypt_acmetiny_base diff --git a/cdist/conf/type/__letsencrypt_acmetiny/nonparallel b/cdist/conf/type/__letsencrypt_acmetiny/nonparallel new file mode 100644 index 00000000..e69de29b diff --git a/cdist/conf/type/__letsencrypt_acmetiny/parameter/optional_multiple b/cdist/conf/type/__letsencrypt_acmetiny/parameter/optional_multiple new file mode 100644 index 00000000..7bfb11da --- /dev/null +++ b/cdist/conf/type/__letsencrypt_acmetiny/parameter/optional_multiple @@ -0,0 +1 @@ +extra-domain diff --git a/cdist/conf/type/__letsencrypt_acmetiny_base/gencode-remote b/cdist/conf/type/__letsencrypt_acmetiny_base/gencode-remote new file mode 100644 index 00000000..1e4174a4 --- /dev/null +++ b/cdist/conf/type/__letsencrypt_acmetiny_base/gencode-remote @@ -0,0 +1,12 @@ +#!/bin/sh -e + +ACME_HOME="/var/acme-tiny" +ACME_ACCOUNT_KEY="${ACME_HOME}/account.key" + +cat << EOF +if [ ! -f '${ACME_ACCOUNT_KEY}' ]; then + openssl genrsa 4096 > '${ACME_ACCOUNT_KEY}' + chown acme-tiny:acme-tiny '${ACME_ACCOUNT_KEY}' + chmod 640 '${ACME_ACCOUNT_KEY}' +fi +EOF diff --git a/cdist/conf/type/__letsencrypt_acmetiny_base/manifest b/cdist/conf/type/__letsencrypt_acmetiny_base/manifest new file mode 100644 index 00000000..fd6961fa --- /dev/null +++ b/cdist/conf/type/__letsencrypt_acmetiny_base/manifest @@ -0,0 +1,202 @@ +# Arguments +ACME_DOMAIN="$(cat $__object/parameter/acme_domain || true)" + +if [ -z "${ACME_DOMAIN}" ]; then + ACME_DOMAIN="${__target_host}" +fi + + +# Install needed stuffz + +## TODO: consider not depending on nginx? It is... practical though. +## TODO: Maybe just move this out to a sepecial type? +__package "nginx" + +NGINX_ETC="/usr/local/etc/nginx" + +# Setup the acme-challenge snippet +require="__package/nginx" __directory "${NGINX_ETC}/snippets" --state present +require="__directory${NGINX_ETC}/snippets" __file "${NGINX_ETC}/snippets/acme-challenge.conf" \ + --mode 644 \ + --source - << EOF +# This file is managed remotely, all changes will be lost + +# This was heavily inspired by debops.org. + +# Automatic Certificate Management Environment (ACME) support. +# https://tools.ietf.org/html/draft-ietf-acme-acme-01 +# https://en.wikipedia.org/wiki/Automated_Certificate_Management_Environment + + +# Return the ACME challenge present in the server public root. +# If not found, switch to global web server root. +location ^~ /.well-known/acme-challenge/ { + default_type "text/plain"; + try_files \$uri @well-known-acme-challenge; +} + +# Return the ACME challenge present in the global server public root. +# If not present, redirect request to a specified domain. +location @well-known-acme-challenge { + root /srv/www/sites/acme/public; + default_type "text/plain"; + try_files \$uri @redirect-acme-challenge; +} + +# Redirect the ACME challenge to a different host. If a redirect loop is +# detected, return 404. +location @redirect-acme-challenge { + if (\$arg_redirect) { + return 404; + } + return 307 \$scheme://${ACME_DOMAIN}\$request_uri?redirect=yes; +} + +# Return 404 if ACME challenge well known path is accessed directly. +location = /.well-known/acme-challenge/ { + return 404; +} +EOF + +require="__package/nginx" __directory "${NGINX_ETC}/sites-enabled" --state present +require="__directory${NGINX_ETC}/sites-enabled" __file "${NGINX_ETC}/nginx.conf" \ + --mode 644 \ + --source - << EOF +# This file is managed remotely, all changes will be lost + +worker_processes 1; + +# This default error log path is compiled-in to make sure configuration parsing +# errors are logged somewhere, especially during unattended boot when stderr +# isn't normally logged anywhere. This path will be touched on every nginx +# start regardless of error log location configured here. See +# https://trac.nginx.org/nginx/ticket/147 for more info. +# +#error_log /var/log/nginx/error.log; +# + +#pid logs/nginx.pid; + + +events { + worker_connections 1024; +} + + +http { + + include mime.types; + default_type application/octet-stream; + + server_tokens off; + + ssl_session_cache shared:SSL:10m; + ssl_session_timeout 5m; + sendfile on; + tcp_nopush on; + tcp_nodelay on; + types_hash_max_size 2048; + gzip on; + gzip_disable "msie6"; + gzip_comp_level 5; + gzip_min_length 256; + gzip_proxied any; + gzip_vary on; + gzip_types + application/atom+xml + application/javascript + application/json + application/ld+json + application/manifest+json + application/rss+xml + application/vnd.geo+json + application/vnd.ms-fontobject + application/x-font-ttf + application/x-web-app-manifest+json + application/xhtml+xml + application/xml + font/opentype + image/bmp + image/svg+xml + image/x-icon + text/cache-manifest + text/css + text/plain + text/vcard + text/vnd.rim.location.xloc + text/vtt + text/x-component + text/x-cross-domain-policy; + + # Logging + access_log /var/log/nginx/access.log; + error_log /var/log/nginx/error.log; + + #add_header X-Clacks-Overhead "GNU Terry Pratchett"; + + # Virtual Hosts Configs + include ${NGINX_ETC}/sites-enabled/*.conf; +} +EOF + +require="__directory${NGINX_ETC}/sites-enabled" __file "${NGINX_ETC}/sites-enabled/welcome.conf" \ + --mode 644 \ + --source - << EOF +# This file is managed remotely, all changes will be lost + +# nginx server configuration for: +# - https://welcome/ + +server { + + listen [::]:80; + + server_name welcome; + + root /srv/www/sites/welcome/public; + + include snippets/acme-challenge.conf; + + location / { + return 301 https://$host$request_uri; + } +} +EOF + +## TODO: this is kinda bad, don't restart every time. +## Otherwise this isn't idempotent. +require="__package/nginx" __service nginx --action onerestart +require="__package/nginx" __start_on_boot nginx + + +__package "acme-tiny" + +# Create acme-tiny user and secure home dir +ACME_TINY_HOME="/var/acme-tiny" +require="__package/acme-tiny" __user acme-tiny --system --home ${ACME_TINY_HOME} --comment "acme-tiny client" +require="__user/acme-tiny" __directory "${ACME_TINY_HOME}" --state present --mode 0750 --owner acme-tiny --group acme-tiny + +# Create ACME challenge dirs to be served by nginx +ACME_PUBLIC_DIR="/srv/www/sites/acme/public" +ACME_WELLKNOWN_DIR="${ACME_PUBLIC_DIR}/.well-known" +ACME_CHALLENGE_DIR="${ACME_WELLKNOWN_DIR}/acme-challenge" +__directory "${ACME_PUBLIC_DIR}" \ + --parents \ + --state present \ + --owner acme-tiny --group www \ + --mode 2750 # TODO: check whether this does require gid? +require="__directory${ACME_PUBLIC_DIR}" __directory "${ACME_WELLKNOWN_DIR}" \ + --state present \ + --owner acme-tiny --group www \ + --mode 0750 +require="__directory${ACME_WELLKNOWN_DIR}" __directory "${ACME_CHALLENGE_DIR}" \ + --state present \ + --owner acme-tiny --group www \ + --mode 0750 + +__package doas +DOAS_CONF="/usr/local/etc/doas.conf" +require="__package/doas" __file "${DOAS_CONF}" --mode 0640 +require="__file${DOAS_CONF}" __line "${DOAS_CONF}" \ + --regex 'root as acme-tiny' \ + --line 'permit nopass root as acme-tiny' diff --git a/cdist/conf/type/__letsencrypt_acmetiny_base/parameter/optional b/cdist/conf/type/__letsencrypt_acmetiny_base/parameter/optional new file mode 100644 index 00000000..fb20814d --- /dev/null +++ b/cdist/conf/type/__letsencrypt_acmetiny_base/parameter/optional @@ -0,0 +1 @@ +acme_domain diff --git a/cdist/conf/type/__letsencrypt_acmetiny_base/singleton b/cdist/conf/type/__letsencrypt_acmetiny_base/singleton new file mode 100644 index 00000000..e69de29b