#!/bin/sh # Creates the installation-private CA (once) and the two server certificates the reference # deployment needs (ADR 0044): `postgres` for the database and `object-storage` for SeaweedFS. # # sudo sh deploy/scripts/init-tls.sh /etc/ekklesia/tls # # Idempotent. An existing key or certificate is never overwritten: delete a file deliberately to # reissue it. Ownership and modes are re-applied on every run, which needs root; a non-root run # creates what is missing and then stops before that step, saying so. No private key is ever # printed. # # Output (in the directory given): # ca.key the CA's private key. Root only. Needed only to issue certificates; keep a # copy offline and consider removing it from the server afterwards. # ca.crt the CA certificate. Public. Mounted into api, migrate, and proxy. # postgres.crt/.key for the name `postgres`; the key is owned by PostgreSQL's uid (70). # object-storage.crt/.key for the name `object-storage`; the key is owned by SeaweedFS's uid (1000). set -eu umask 077 usage() { echo "Usage: sh init-tls.sh " >&2 exit 64 } [ "$#" -eq 1 ] || usage out=$1 case "$out" in -*) usage ;; esac # The uids the two images run their servers as: postgres:18.4-alpine3.24 (postgres) and # chrislusf/seaweedfs:4.28 (seaweed). Each server refuses or fails to read a key it cannot own. POSTGRES_UID=${EKKLESIA_POSTGRES_UID:-70} SEAWEED_UID=${EKKLESIA_SEAWEED_UID:-1000} CA_DAYS=3650 SERVER_DAYS=825 mkdir -p "$out" chmod 700 "$out" created="" # Only generating needs openssl. A later root-only pass that applies ownership to files that already # exist runs without it, in any image that has a POSIX shell. require_openssl() { command -v openssl >/dev/null 2>&1 || { echo "init-tls: openssl is required to create keys and certificates." >&2 exit 69 } } note_created() { created="$created $1" } # A file this script would create already exists: keep it, and say so. exists() { if [ -e "$out/$1" ]; then echo "init-tls: keeping existing $out/$1" return 0 fi return 1 } if ! exists ca.key; then require_openssl openssl ecparam -name prime256v1 -genkey -noout -out "$out/ca.key" note_created ca.key fi if ! exists ca.crt; then require_openssl openssl req -x509 -new -sha256 -days "$CA_DAYS" -key "$out/ca.key" -out "$out/ca.crt" \ -subj "/CN=Ekklesia installation CA" \ -addext "basicConstraints=critical,CA:TRUE,pathlen:0" \ -addext "keyUsage=critical,keyCertSign,cRLSign" \ -addext "subjectKeyIdentifier=hash" note_created ca.crt fi # issue : a key and a certificate whose only subject alternative name is , the Compose # service name the client verifies against. issue() { name=$1 if ! exists "$name.key"; then require_openssl openssl ecparam -name prime256v1 -genkey -noout -out "$out/$name.key" note_created "$name.key" fi if ! exists "$name.crt"; then require_openssl extensions=$(mktemp) request=$(mktemp) trap 'rm -f "$extensions" "$request"' EXIT cat >"$extensions" </dev/null rm -f "$extensions" "$request" trap - EXIT note_created "$name.crt" fi } issue postgres issue object-storage # Public material is world-readable; private keys are readable only by the process that needs them. chmod 644 "$out/ca.crt" "$out/postgres.crt" "$out/object-storage.crt" chmod 600 "$out/ca.key" "$out/postgres.key" "$out/object-storage.key" [ -e "$out/ca.srl" ] && chmod 600 "$out/ca.srl" if [ -n "$created" ]; then echo "init-tls: created$created in $out" else echo "init-tls: nothing to create in $out" fi if [ "$(id -u)" -ne 0 ]; then echo "init-tls: ownership not applied: re-run as root to give postgres.key to uid $POSTGRES_UID and object-storage.key to uid $SEAWEED_UID." >&2 exit 77 fi chown root:root "$out" "$out/ca.key" "$out/ca.crt" "$out/postgres.crt" "$out/object-storage.crt" [ -e "$out/ca.srl" ] && chown root:root "$out/ca.srl" chown "$POSTGRES_UID:$POSTGRES_UID" "$out/postgres.key" chown "$SEAWEED_UID:$SEAWEED_UID" "$out/object-storage.key" # The directory itself must be traversable by the two server uids for their bind mounts. chmod 755 "$out" echo "init-tls: postgres.key is owned by uid $POSTGRES_UID and object-storage.key by uid $SEAWEED_UID (mode 600)." echo echo "Set EKKLESIA_TLS_DIR=$out in deploy/.env, and put this line in ekklesia.env (the CA" echo "certificate is public; this is not a secret):" echo printf 'EKKLESIA_DATABASE_CA="%s"\n' "$(awk 'BEGIN { ORS = "\\n" } { print }' "$out/ca.crt")"