feat(deploy): add the reference deployment Compose project
CI / verify (pull_request) Successful in 54s
CI / infrastructure-smoke (pull_request) Successful in 21s
CI / migration-smoke (pull_request) Successful in 1m48s
CI / image-build (pull_request) Successful in 27s
CI / deploy-smoke (pull_request) Successful in 42s
CI / verify (pull_request) Successful in 54s
CI / infrastructure-smoke (pull_request) Successful in 21s
CI / migration-smoke (pull_request) Successful in 1m48s
CI / image-build (pull_request) Successful in 27s
CI / deploy-smoke (pull_request) Successful in 42s
Implement ADR 0044's reference deployment as a base Compose file plus overlays, so an installation that already runs a proxy can use the base alone. - compose.yaml: api, postgres, object-storage, and the operator-only migrate (profile ops, plan by default). No published port. api and migrate run init, read-only, tmpfs /tmp, cap_drop ALL, no-new-privileges. postgres is only on the internal data network. - TLS everywhere from one installation-private CA: PostgreSQL TLS 1.3 only, with pg_hba rejecting every non-TLS connection, and the API on verify-full via EKKLESIA_DATABASE_CA (PEM text). SeaweedFS serves S3 over HTTPS (ADR 0030 requires https in production), trusted by the API through NODE_EXTRA_CA_CERTS. No Core change. - compose.proxy.yaml + Caddyfile.example: Caddy pinned by digest, ACME HTTP-01 (DNS-01 documented), routes /, /api/*, /display/*, /assets/* to api and refuses everything else; the storage hostname preserves Host. The API trusts exactly the proxy's fixed /32 on an edge network whose dynamic range excludes the fixed addresses; Caddy overwrites X-Forwarded-For and strips caller-supplied forwarding headers. - compose.tunnel.yaml: cloudflared pinned by digest, token from its own env file; Caddy honors CF-Connecting-IP only from its fixed address. - Four per-service root-owned env files, examples with no values; scripts/init-tls.sh (idempotent, never overwrites a key) and scripts/gen-secrets.sh (prints only). - CI: pnpm deploy:config in ci:verify for all three combinations, and a deploy-smoke job (scripts/smoke-deploy.sh) proving readiness, the Dashboard, signed storage delivery, the client-address chain, initial owner sign-in/out, verify-full database TLS, and Doctor through the proxy, against disposable services. tests/deploy.test.ts pins the structural guarantees. - ADR 0044 (still Proposed) amended to match: API-to-storage HTTPS, DNS-01 needs a custom Caddy build and tunnel-only needs DNS-01, the tunnel's origin TLS settings live in the Cloudflare dashboard, port 80 serves HTTP-01, and secrets are one env file per service. Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
b593abe7a5
commit
167b4c4f14
Executable
+148
@@ -0,0 +1,148 @@
|
||||
#!/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 <output directory>" >&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 <name>: a key and a certificate whose only subject alternative name is <name>, 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" <<EOF
|
||||
basicConstraints=critical,CA:FALSE
|
||||
keyUsage=critical,digitalSignature
|
||||
extendedKeyUsage=serverAuth
|
||||
subjectAltName=DNS:$name
|
||||
subjectKeyIdentifier=hash
|
||||
authorityKeyIdentifier=keyid
|
||||
EOF
|
||||
openssl req -new -sha256 -key "$out/$name.key" -subj "/CN=$name" -out "$request"
|
||||
openssl x509 -req -sha256 -days "$SERVER_DAYS" -in "$request" \
|
||||
-CA "$out/ca.crt" -CAkey "$out/ca.key" -CAcreateserial -CAserial "$out/ca.srl" \
|
||||
-extfile "$extensions" -out "$out/$name.crt" 2>/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")"
|
||||
Reference in New Issue
Block a user