aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMitchell Riedstra <mitch@riedstra.dev>2023-01-09 23:01:36 -0500
committerMitchell Riedstra <mitch@riedstra.dev>2023-01-09 23:01:36 -0500
commit7e8d29755135a4384d8c2aa8cfd24c5ddfeb7c97 (patch)
tree951be2c46639267f229c3fd4496c0049e0ca7127
downloadacme-warehouse-master.tar.gz
acme-warehouse-master.tar.xz
InitialHEADmaster
-rw-r--r--.gitignore3
-rw-r--r--Dockerfile16
-rwxr-xr-xentrypoint.sh125
-rw-r--r--fetch.sh161
-rw-r--r--readme.md100
-rwxr-xr-xrenewal.sh13
-rwxr-xr-xsetup.sh88
7 files changed, 506 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..e32a521
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,3 @@
+env*
+.env
+*home*
diff --git a/Dockerfile b/Dockerfile
new file mode 100644
index 0000000..8c5833a
--- /dev/null
+++ b/Dockerfile
@@ -0,0 +1,16 @@
+FROM docker.io/alpine:3.17
+
+RUN echo "http://dl-cdn.alpinelinux.org/alpine/v3.17/main" > /etc/apk/repositories
+RUN echo "http://dl-cdn.alpinelinux.org/alpine/v3.17/community" >> /etc/apk/repositories
+
+RUN apk update
+RUN apk add nginx
+RUN apk add age signify acme.sh
+
+RUN mkdir -p /var/acme /var/www/acme
+
+COPY setup.sh /setup.sh
+COPY entrypoint.sh /
+COPY renewal.sh /
+
+ENTRYPOINT /entrypoint.sh
diff --git a/entrypoint.sh b/entrypoint.sh
new file mode 100755
index 0000000..8acd56a
--- /dev/null
+++ b/entrypoint.sh
@@ -0,0 +1,125 @@
+#!/bin/sh
+_username="acme"
+printf "\033[1;31m"
+set -e
+set -x
+
+SERVICES="nginx renewal"
+NGINX_LISTEN="${NGINX_LISTEN:-8080}"
+FULL_NAME="${FULL_NAME:-Acme User}"
+
+ACME_USER_SHELL="${ACME_USER_SHELL:-/bin/ash}"
+ACME_USER_UID="${ACME_USER_UID:-3500}"
+ACME_USER_GID="${ACME_USER_GID:-3500}"
+
+NGINX_WORKER_PROCESSES="${NGINX_WORKER_PROCESSES:-1}"
+NGINX_WORKER_CONNECTIONS="${NGINX_WORKER_CONNECTIONS:-1024}"
+NGINX_AUTOINDEX="${NGINX_AUTOINDEX:-on}"
+
+AWS_DEFAULT_REGION="${AWS_DEFAULT_REGION:-us-east-2}"
+
+set +x
+
+echo "Checking required variables..."
+
+err=0
+for var in ACME_DELEGATION_DOMAIN ACME_EMAIL DOMAINS ACMESH_FLAGS; do
+ eval val="\$$var"
+ #shellcheck disable=SC2154
+ echo "$var=$val"
+ if [ -z "$val" ] && ! [ "$var" = "ACMESH_FLAGS" ] ; then
+ err=1
+ fi
+done
+
+if [ $err -ne 0 ] ; then
+ echo "Please set environment variables"
+ printf '\033[0m'
+ exit 3;
+fi
+
+printf '\033[1;32m'
+
+echo "all good"
+
+printf "\033[0m"
+
+# This is only run once in the container's lifetime unless /setup is removed
+setup() {
+if [ -e /setup ] ; then return ; fi
+
+addgroup -g "${ACME_USER_GID}" "$_username"
+adduser -h /var/acme --gecos "$FULL_NAME" -D -s "${ACME_USER_SHELL}" \
+ -u "${ACME_USER_UID}" -G "$_username" "$_username"
+# passwd -u "$_username"
+
+
+touch /setup
+}
+
+run_nginx() {
+autoindex=on
+if [ "$NGINX_AUTOINDEX" = "OFF" ] ; then
+ autoindex="off"
+fi
+
+cat > /etc/nginx/nginx.conf <<NGINX
+worker_processes $NGINX_WORKER_PROCESSES;
+error_log /dev/fd/2;
+events {
+ worker_connections $NGINX_WORKER_CONNECTIONS;
+}
+http {
+ access_log /dev/fd/1;
+ include mime.types;
+ default_type application/octet-stream;
+ sendfile on;
+ keepalive_timeout 65;
+ gzip on;
+ server_tokens off;
+ server {
+ listen $NGINX_LISTEN; listen [::]:$NGINX_LISTEN;
+ root /var/www/acme;
+
+ location / {
+ autoindex $autoindex;
+ }
+ }
+}
+NGINX
+
+mkdir -p /run/nginx
+nginx -g 'daemon off;' &
+}
+
+run_renewal() {
+ /renewal.sh &
+}
+
+watchServices() {
+interval="$1"; shift
+while true ; do
+ for service in $SERVICES ; do
+ if ! pgrep "$service" >/dev/null ; then
+ echo "Service $service has stopped... quitting!"
+ exit 1
+ fi
+ done
+ sleep "$interval"
+done
+}
+
+set -x
+# MAIN / Actual entrypoint start
+setup
+chown -R acme:acme /var/acme /var/www/acme
+for service in $SERVICES ; do
+ eval "run_$service"
+done
+su acme /setup.sh
+
+# Bail out if a service stops, poll it every 30 seconds
+set +x
+watchServices 30
+# or if you comment out the above, drop into a shell
+# exec /bin/ash "$@"
diff --git a/fetch.sh b/fetch.sh
new file mode 100644
index 0000000..fc15aa1
--- /dev/null
+++ b/fetch.sh
@@ -0,0 +1,161 @@
+#!/bin/sh
+# fetch.sh fetch and verify certificates from an HTTP endpoint
+# This is meant to be used with the ACME
+set -e
+
+verbose() {
+ if [ $_verbose -ne 0 ] ; then
+ return 0
+ fi
+ return 1
+}
+
+cleanup() {
+ verbose && echo "Removing $_tmpdir"
+ rm -rf "$_tmpdir" || echo
+ exit 0
+}
+
+_curl='curl -sS'
+
+fullchainf=
+certfile=
+keyfile=
+fetchurl=
+domain=
+encryptionKeyFile=
+signatureKeyFile=
+_verbose=0
+_tmpdir="$(mktemp -d)"
+
+help() {
+cat <<EOF
+$0 -f <fullchain> -c <certfile> -k <keyfile> -u <fetchURL> -d <domain>
+ -K <encryptionKeyFile> -S <signatureKeyFile> [-v]
+
+Optionally, AGE_ENCRYPTION_KEY and SIGNIFY_KEY can be set and
+the contents of the environment variables will be used as key files
+instead.
+
+-v is verbose.
+
+EOF
+exit 1
+}
+
+#shellcheck disable=SC2034
+while [ $# -gt 0 ] ; do case $1 in
+ -f) fullchainf="$2"; shift ; shift ;;
+ -c) certfile="$2"; shift ; shift ;;
+ -k) keyfile="$2"; shift ; shift ;;
+ -u) fetchurl="$2"; shift ; shift ;;
+ -d) domain="$2"; shift ; shift ;;
+ -K) encryptionKeyFile="$2"; shift ; shift ;;
+ -S) signatureKeyFile="$2"; shift ; shift ;;
+ -v) _verbose=1; shift ;;
+ *) help ;;
+esac ; done
+
+cd "$_tmpdir"
+
+if [ -n "$AGE_ENCRYPTION_KEY" ] ; then
+ encryptionKeyFile="age.key"
+ chmod 600 "$encryptionKeyFile"
+ echo "$AGE_ENCRYPTION_KEY" > "$encryptionKeyFile"
+fi
+
+if [ -n "$SIGNIFY_KEY" ] ; then
+ signatureKeyFile="signify.sig"
+ chmod 600 "$signatureKeyFile"
+ echo "$SIGNIFY_KEY" > "$signatureKeyFile"
+fi
+
+cd -
+
+err=0
+for opt in fullchainf certfile keyfile fetchurl domain \
+ encryptionKeyFile signatureKeyFile ; do
+ eval val="\$$opt"
+ #shellcheck disable=SC2154
+ if [ -z "$val" ] ; then
+ echo "Missing $opt"
+ err=1
+ fi
+done
+if [ $err -ne 0 ] ; then
+ echo "missing required arguments"
+ exit 1;
+fi
+
+encryptionKeyFile="$(realpath "$encryptionKeyFile")"
+signatureKeyFile="$(realpath "$signatureKeyFile")"
+
+[ -e "$keyfile" ] && keyfile="$(realpath "$keyfile")"
+[ -e "$fullchainf" ] && fullchainf="$(realpath "$fullchainf")"
+[ -e "$certfile" ] && certfile="$(realpath "$certfile")"
+
+if ! [ -e "$keyfile" ] ; then
+ echo "Warning: no key file found" >&2
+fi
+
+if ! [ -e "$fullchainf" ] ; then
+ echo "Warning: no fullchain file found" >&2
+fi
+
+if ! [ -e "$certfile" ] ; then
+ echo "Warning: no cert file found" >&2
+fi
+
+cd "$_tmpdir"
+
+trap cleanup EXIT INT
+
+verbose && echo Fetching checkums...
+
+$_curl "${fetchurl}/${domain}.sha256sum" > "${domain}.sha256sum"
+$_curl "${fetchurl}/${domain}.sha256sum.sig" > "${domain}.sha256sum.sig"
+
+signify -V -p "$signatureKeyFile" -m "${domain}.sha256sum"
+
+verbose && echo "Checksums OK"
+
+ok=0
+checksum="$(sha256sum "$keyfile" | awk '{print $1}')"
+if grep -q "$checksum" "${domain}".sha256sum ; then
+ verbose && echo "Current key file \"$keyfile\" OK"
+ ok=$((ok + 1))
+fi
+
+checksum="$(sha256sum "$fullchainf" | awk '{print $1}')"
+if grep -q "$checksum" "${domain}".sha256sum ; then
+ verbose && echo "Current fullchain file \"$fullchainf\" ok"
+ ok=$((ok + 1))
+fi
+
+checksum="$(sha256sum "$certfile" | awk '{print $1}')"
+if grep -q "$checksum" "${domain}".sha256sum ; then
+ verbose && echo "Current cert file \"$certfile\" ok"
+ ok=$((ok + 1))
+fi
+
+if [ $ok -eq 3 ] ; then
+ verbose && echo "All files appear okay, exiting"
+ exit 0
+fi
+
+for _f in fullchain cer key.enc ; do
+ verbose && echo "Fetching ${_f}..."
+ $_curl "${fetchurl}/${domain}.${_f}" > "${domain}.$_f"
+done
+
+verbose && echo "Decrypting key"
+age -i "$encryptionKeyFile" -d "${domain}.key.enc" > "${domain}".key
+
+verbose && echo "Validating checksum for downloaded files"
+sha256sum -c "${domain}.sha256sum"
+
+verbose && echo "Updating files"
+cat < "${domain}".fullchain > "$fullchainf"
+cat < "${domain}".cer > "$certfile"
+cat < "${domain}.key" > "$keyfile"
+
diff --git a/readme.md b/readme.md
new file mode 100644
index 0000000..6d9c8a9
--- /dev/null
+++ b/readme.md
@@ -0,0 +1,100 @@
+# ACME certificate warehouse
+
+A pretty bare-bones docker container for storing and distributing ACME
+certificates to your infrastructure.
+
+It will fetch certificates, encrypt the key file, generate checksums
+then sign the checksums for each domain provided.
+
+This allows plain HTTP to serve as the transport for what is otherwise
+sensitive information ( the key file, mostly )
+
+It will generate both an encryption and signing key upon first start.
+
+To simplify usage there's an example `fetch.sh` script that can be used
+to fetch the certificates from this service taking care of checksum validation,
+signature checking and decryption for you if given a URL.
+
+It's currently setup *only* to use acme.sh and do DNS validation to route53.
+
+That may or may not suit your needs. This is not meant to be a generalized
+solution for everyone's certificate needs, but rather a shell you can copy
+and expand upon for your needs.
+
+It's only a few hundred lines of shell, so it's easily modified and hacked to
+suit your needs.
+
+The general flow here is:
+
+```
+entrypoint.sh ->
+ Nginx -> serve up the certificates and keys
+ renewal.sh -> renew certificates if needed, every 24 hours
+ setup.sh -> runs `acme.sh`, then encrypts, signs and places them in the webroot
+```
+
+### Why?
+
+Ephemeral services across large swaths of disparate infrastructure where
+tools like Ansible break down and give way too large of an attack surface if
+automated.
+
+Solutions like S3 require esoteric rune-casting, and probably more code than
+what you see here just to handle assuming fetching temporary credentials from
+an assumed IAM role... Then lock you into a specific setup anyway.
+
+This also splits the access to DNS validation and certificate access even
+further, if a server does managed to get compromised, you don't
+
+## Building
+
+```
+$ buildah build -t acme-warehouse .
+```
+
+## Running
+
+You may wish to bind mount `/var/acme` or create a volume for persistence.
+
+```
+$ podman run --env-file acme-env-file acme-warehouse
+```
+
+Where the `acme-env-file` contains the required environment variables.
+They are:
+
+```
+ACME_DELEGATION_DOMAIN
+ACME_EMAIL
+DOMAINS
+```
+
+Additionally you may wish to pass in `ACMESH_FLAGS=--staging` until you've
+verified that it pulls down and generates certificates as the DNS validation
+limits are fairly low.
+
+`DOMAINS` are space or newline delimited list of domains to fetch certificates
+for. That, and the subdomain wildcard will be fetched.
+
+You may wish to also supply AWS specific environment variables such as:
+
+```
+AWS_SECRET_ACCESS_KEY
+AWS_ACCESS_KEY_ID
+AWS_DEFAULT_REGION
+```
+
+If you want to encrypt with additional keys simply set:
+
+```
+AGE_RECIPIENTS
+```
+
+Generating a new key is easy with [`age-keygen`](https://github.com/FiloSottile/age)
+
+If you want to retrieve the key the container generated run:
+
+```
+podman exec <running_container_name> cat /var/acme/age.key
+```
+
diff --git a/renewal.sh b/renewal.sh
new file mode 100755
index 0000000..04b3450
--- /dev/null
+++ b/renewal.sh
@@ -0,0 +1,13 @@
+#!/bin/sh
+set -ex
+timer=86400
+
+
+while true ; do
+ sleep $timer
+
+ su acme /bin/sh \
+ -c '/var/acme/acme_home/acme.sh --cron --home /var/acme/acme_home/'
+
+ su acme /setup.sh
+done
diff --git a/setup.sh b/setup.sh
new file mode 100755
index 0000000..1bb9b03
--- /dev/null
+++ b/setup.sh
@@ -0,0 +1,88 @@
+#!/bin/sh
+# acme user's setup script
+set -ex
+cd "$HOME"
+
+if ! [ -e "sign.sec" ] ; then
+ rm -f sign.pub || echo ""
+ signify -G -n -p sign.pub -s sign.sec
+fi
+cat sign.pub
+
+if ! [ -e "age.key" ] ; then
+ age-keygen -o age.key
+fi
+
+if [ -n "$AGE_RECIPIENTS" ] ; then
+ echo "$AGE_RECIPIENTS" > recipients.txt
+fi
+
+awk '/public key/{print $4}' age.key >> recipients.txt
+
+if [ -z "$ACME_EMAIL" ] ; then
+ echo "ACME_EMAIL must be set"
+ exit 1
+fi
+
+if [ -z "$ACME_DELEGATION_DOMAIN" ] ; then
+ echo "ACME_DELEGATION_DOMAIN must be set"
+ exit 1
+fi
+
+if [ -z "$DOMAINS" ] ; then
+ echo "DOMAINS must be set"
+ exit 1
+fi
+
+cp /usr/bin/acme.sh ./
+
+sh ./acme.sh --install \
+ --home "$HOME/acme_home" \
+ --config-home "$HOME/acme_conf" \
+ --cert-home "$HOME/certs" \
+ --accountemail "$ACME_EMAIL" \
+ --accountkey "$HOME/acme_account.key" \
+ --accountconf "$HOME/acme_account.conf" \
+ --no-cron
+
+
+#shellcheck disable=SC1091
+. "$HOME/acme_home/acme.sh.env"
+
+acme.sh --upgrade
+
+for domain in $DOMAINS ; do
+ #shellcheck disable=SC2086
+ if ! [ -f "certs/$domain/$domain.cer" ] ; then
+ acme.sh $ACMESH_FLAGS \
+ --issue \
+ --dns dns_aws \
+ --challenge-alias "$ACME_DELEGATION_DOMAIN" \
+ -d "$domain" -d "*.${domain}"
+ fi
+
+ cd "certs/$domain"
+ sha256sum "${domain}.cer" "${domain}.key" \
+ > "/var/www/acme/${domain}.sha256sum"
+
+ age -e -a -R "$HOME"/recipients.txt "${domain}.key" \
+ > "/var/www/acme/${domain}.key.enc"
+
+ cp "${domain}.cer" /var/www/acme/
+ cp "fullchain.cer" /var/www/acme/"${domain}".fullchain
+
+ cd /var/www/acme
+
+ sha256sum "${domain}.fullchain" >> "${domain}.sha256sum"
+
+ sha256sum "${domain}.key.enc" >> "${domain}.sha256sum"
+
+ rm -f "${domain}.sha256sum.sig" || echo ""
+
+ signify -S -m "${domain}.sha256sum" -s "$HOME/sign.sec" \
+ -x "${domain}.sha256sum.sig"
+
+ cd "$HOME"
+done
+
+cp sign.pub /var/www/acme/