[__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.
This commit is contained in:
parent
f7d5f5bc97
commit
1bd19d6dee
8 changed files with 326 additions and 0 deletions
cdist/conf/type
__letsencrypt_acmetiny
__letsencrypt_acmetiny_base
109
cdist/conf/type/__letsencrypt_acmetiny/gencode-remote
Normal file
109
cdist/conf/type/__letsencrypt_acmetiny/gencode-remote
Normal file
|
@ -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
|
1
cdist/conf/type/__letsencrypt_acmetiny/manifest
Normal file
1
cdist/conf/type/__letsencrypt_acmetiny/manifest
Normal file
|
@ -0,0 +1 @@
|
||||||
|
#__letsencrypt_acmetiny_base
|
0
cdist/conf/type/__letsencrypt_acmetiny/nonparallel
Normal file
0
cdist/conf/type/__letsencrypt_acmetiny/nonparallel
Normal file
|
@ -0,0 +1 @@
|
||||||
|
extra-domain
|
12
cdist/conf/type/__letsencrypt_acmetiny_base/gencode-remote
Normal file
12
cdist/conf/type/__letsencrypt_acmetiny_base/gencode-remote
Normal file
|
@ -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
|
202
cdist/conf/type/__letsencrypt_acmetiny_base/manifest
Normal file
202
cdist/conf/type/__letsencrypt_acmetiny_base/manifest
Normal file
|
@ -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'
|
|
@ -0,0 +1 @@
|
||||||
|
acme_domain
|
0
cdist/conf/type/__letsencrypt_acmetiny_base/singleton
Normal file
0
cdist/conf/type/__letsencrypt_acmetiny_base/singleton
Normal file
Loading…
Add table
Reference in a new issue