feat: enhance Authentik deployment with age-encrypted secrets; change deployment to komodo

This commit is contained in:
Philip Henning 2026-09-01 09:22:37 +00:00
parent c3689404bd
commit 71736f8dd9
9 changed files with 254 additions and 369 deletions

12
.age-docker.toml Normal file
View file

@ -0,0 +1,12 @@
version = 1
[keys]
phg = "age1uyag8gskstp590dlsw5wej3wdzu0turr9pjn0ln734nn23csu36q9dt0fw"
sbx0docker01 = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIH4umkUY5CG5aNJBhUjVcU8TWbh453N0pGHZhYDTOGQa"
[groups]
users = ["phg"]
production = ["sbx0docker01"]
[secrets]
"secrets/prod.env.age" = ["users", "production"]

216
README.md
View file

@ -1,37 +1,68 @@
# Authentik deployment
This repository owns the production Authentik Compose deployment and its
Komodo Stack declaration. The central `komodo-bootstrap` repository owns the
child Resource Sync and the ordered deployment Procedure.
This repository owns the production deployment of
[Authentik](https://goauthentik.io/) for `sso.s1q.dev`, including its Compose
configuration, encrypted secrets, and Komodo Stack declaration.
## Layout
## Deployment architecture
The `authentik-prod` Stack follows the protected `main` branch. Its direct
webhook is disabled. A Forgejo webhook calls the central `authentik-deploy`
Procedure, which first runs the child Resource Sync and then runs
`DeployStackIfChanged`. The central `docker-compose/komodo-bootstrap`
repository owns that child Sync and Procedure.
Komodo passes `docker-compose.yml` first and `docker-compose.override.yml`
second. The upstream-derived base and deployment-specific override remain
separate by design. A regular deployment therefore follows this path:
1. merge a reviewed change into `main`;
2. Forgejo calls the `authentik-deploy` Procedure webhook;
3. the Procedure syncs `komodo.toml`; and
4. Komodo deploys `authentik-prod` if a tracked file changed.
For a first deployment or a manual retry, run `authentik-deploy` in Komodo
instead of deploying the Stack directly. This keeps resource synchronization
and deployment in the intended order.
## Prerequisites
Before the first deployment, ensure that:
- NixOS has provisioned the `sbx0docker01` Komodo Server, Periphery's container
permissions, the `with-age-env` wrapper, and its `age-identity` credential;
- the external `dokploy-network` exists on the deployment host;
- the root `bootstrap` Resource Sync has registered the `authentik` child Sync
and the `authentik-deploy` Procedure; and
- the Forgejo repository webhook uses the Procedure's `__ANY__` webhook URL.
Host provisioning, Tailscale, CrowdSec, and registry setup belong to NixOS or
their respective infrastructure repositories and are intentionally not
duplicated here.
## Layout and ownership
```text
.
├── docker-compose.yml
├── docker-compose.override.yml
├── komodo.toml
├── .age-docker.toml # Secret recipients and policy
├── docker-compose.yml # Upstream-derived Compose base
├── docker-compose.override.yml # Production networks, routing, and images
├── env/
│ └── common.env
│ └── common.env # Tracked non-secret configuration and versions
├── komodo.toml # authentik-prod Stack declaration
├── secrets/
│ └── prod.env.age
├── scripts/
│ ├── compose.sh
│ └── init.sh
└── tests/
└── compose-secrets.sh
│ └── prod.env.age # Encrypted production secrets
└── data/authentik/custom-templates/
```
`env/common.env` contains tracked, non-secret configuration and explicit image
versions. `secrets/prod.env.age` contains only secrets and is encrypted to the
`sbx0docker01` SSH host key and the personal `agenix-phg` SSH key.
NixOS owns Komodo, Periphery, host credentials, Podman permissions, and the
external `dokploy-network`. This repository owns the application definition.
Persistent database, Redis, media, certificate, and GeoIP data live in named
container volumes on the deployment host and are not stored in Git.
The `authentik-prod` Stack follows protected `main`. Its direct webhook is
disabled. A Forgejo webhook calls the central `authentik-deploy` Procedure,
which runs the child Sync before `DeployStackIfChanged`.
Komodo passes `docker-compose.yml` first and `docker-compose.override.yml`
second. The base and deployment-specific override remain separate by design.
`env/common.env` is the authoritative source for tracked runtime settings and
explicit image versions. The old local `.env`/template and init-script
workflow is no longer part of deployment.
## Secrets
@ -44,33 +75,33 @@ AUTHENTIK_EMAIL__PASSWORD
GEOIPUPDATE_LICENSE_KEY
```
To edit it locally, decrypt to a mode-`0600` temporary file, edit it, then
encrypt it to both recipients:
Local secret management uses only `age-docker`. Recipient keys, groups, and
the rule for the encrypted bundle live in `.age-docker.toml`. The bundle is
currently encrypted for the personal `phg` age key and the `sbx0docker01` SSH
host key.
Edit the bundle with:
```bash
tmp=$(mktemp)
chmod 0600 "$tmp"
trap 'rm -f -- "$tmp"' EXIT HUP INT TERM
age --decrypt \
--identity ~/.ssh/identities/agenix-phg \
--output "$tmp" \
secrets/prod.env.age
${EDITOR:-vi} "$tmp"
age --encrypt \
--armor \
--recipient 'ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIH4umkUY5CG5aNJBhUjVcU8TWbh453N0pGHZhYDTOGQa' \
--recipient 'ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIIr4Dji0wWzwSXyqbxdGa8LdWkhP+0T7kDKrLbyCoyos' \
--output secrets/prod.env.age.new \
"$tmp"
mv secrets/prod.env.age.new secrets/prod.env.age
age-docker edit secrets/prod.env.age
```
Do not add a plaintext secret file to Git. The Stack config tracks the
encrypted bundle so a rotation counts as a deployment change, but does not
When a configured recipient changes, re-encrypt the bundle and validate the
complete policy:
```bash
age-docker rekey secrets/prod.env.age
age-docker check
```
`age-docker` creates mode-`0600` temporary plaintext, removes it after the
editor exits or a handled signal occurs, produces armored ciphertext, and
verifies the new ciphertext with the personal identity before atomically
replacing the tracked file. It does not add implicit recipients: every key
with access is declared in `.age-docker.toml`.
Never add a plaintext secret file to Git. The Stack tracks the encrypted
bundle so that secret rotation counts as a deployment change, but it does not
track the temporary `.komodo/prod.env` file.
The decrypted values are Compose secret sources rather than service
@ -80,10 +111,10 @@ while PostgreSQL and GeoIP Update use their `_FILE` variables. Consequently,
`docker compose config` renders secret source names and file paths without
persisting the values in Komodo's merged configuration.
## Compose wrapper
## Komodo Compose wrapper
Komodo wraps `config`, `pull`, `up`, and `run` with
the NixOS-provided `with-age-env` command. The wrapper:
Komodo wraps `config`, `pull`, `up`, and `run` with the NixOS-provided
`with-age-env` command. The wrapper:
- obtains an exclusive per-bundle lock;
- removes stale plaintext and temporary files;
@ -95,15 +126,86 @@ The plaintext target and matching `.lock` and `.tmp.*` paths are exclusively
owned by the wrapper. A `SIGKILL` or power loss can leave plaintext behind;
the next invocation removes it before decrypting again.
NixOS owns the wrapper, its runtime dependencies, the host-key systemd
credential, Periphery's Podman permissions, and the external
`dokploy-network`. Workload containers do not receive the age identity, but
commands launched by Periphery can access it. Revisit that trust boundary if
deployment authority is ever granted to another operator.
Workload containers do not receive the age identity, but commands launched by
Periphery can access it. Revisit that trust boundary if deployment authority
is ever granted to another operator.
Run `bash tests/compose-secrets.sh` to verify that sentinel secret values do
not appear in the rendered Compose model and that every service receives only
its intended secret files.
## Upgrade
There is currently no long-lived test environment. Prepare and review every
upgrade on a branch before merging it into production. If a test Stack is
added later, perform the upgrade there first.
1. Confirm that the host backups are current and restorable.
2. Read the target version's [Authentik release notes](https://docs.goauthentik.io/releases/)
and upgrading section. Note all manual migrations, breaking configuration
changes, and outpost compatibility requirements before changing files.
3. Create an upgrade branch.
4. Download the target version's upstream Compose file alongside the tracked
file. Replace the example version with the intended target:
```bash
authentik_version="2025.8"
curl -fsSL \
"https://goauthentik.io/version/${authentik_version}/lifecycle/container/compose.yml" \
-o docker-compose.yml
```
5. Compare `docker-compose.yml` with the previously tracked version.
6. Review `docker-compose.override.yml` against the new base structure. Keep
the production networks, Traefik routing, volumes, and explicit image
declarations working with the new release.
7. Update `AUTHENTIK_TAG` in `env/common.env`. Review the supported PostgreSQL
and Redis versions and update `POSTGRES_TAG`, `REDIS_TAG`, and
`GEOIPUPDATE_TAG` only after checking their respective release and migration
notes. Plan to update all Authentik outposts to the same Authentik version.
8. Remove the downloaded comparison file and validate the resulting policy
and Compose model. Dummy values satisfy Compose's secret-source checks
without decrypting production secrets:
```bash
rm docker-compose.upstream.yml
age-docker check
PG_PASS=dummy \
AUTHENTIK_SECRET_KEY=dummy \
AUTHENTIK_EMAIL__PASSWORD=dummy \
GEOIPUPDATE_LICENSE_KEY=dummy \
docker compose \
--env-file env/common.env \
--file docker-compose.yml \
--file docker-compose.override.yml \
config --quiet
```
9. Review the complete diff, commit it, push the branch, and merge it through a
merge request. Do not run `docker compose pull`, `down`, or `up` manually on
the host; the merge triggers the ordered Komodo deployment flow.
10. Follow the `authentik-deploy` Procedure and `authentik-prod` Stack logs in
Komodo. Verify that all services become healthy, `https://sso.s1q.dev` is
reachable, and an SSO login succeeds. Update outposts to the matching
version and verify their connectivity.
Record any repository-specific manual migration steps in this README before
the rollout. None are currently documented.
### Rollback
For a configuration or image regression, revert the merge on `main`; the
webhook will run the same ordered deployment flow with the previous tracked
configuration. Do not assume that an application or database migration can be
downgraded. Follow the target release's rollback guidance and restore a tested
backup when a migration is not reversible.
## Backup and disaster recovery
This Compose project does not define a backup or `restore-cli` service. The
old container-based restore commands therefore no longer apply. Backups and
volume recovery must be provided and documented by the host infrastructure.
Before relying on the deployment in production, ensure that the PostgreSQL
data and all named volumes required by Authentik are covered by a tested
backup and recovery runbook. Keep the runbook with the infrastructure that
owns those backups and link it here once it exists.
## Optional test environment

View file

@ -1,8 +1,7 @@
---
services:
geoipupdate:
image: "maxmindinc/geoipupdate:${GEOIPUPDATE_TAG:?GEOIPUPDATE_TAG is not configured}"
image: "maxmindinc/geoipupdate:v7.1.1"
restart: unless-stopped
volumes:
- "geoip:/usr/share/GeoIP"
@ -14,29 +13,47 @@ services:
secrets:
- geoip_license_key
postgresql:
image: docker.io/library/postgres:${POSTGRES_TAG:?POSTGRES_TAG is not configured}
networks:
- backend
postgresql: !reset []
# env_file: !override []
# environment:
# POSTGRES_PASSWORD_FILE: /run/secrets/postgres_password
# secrets:
# - postgres_password
# networks:
# - backend
# ports: !override []
redis:
image: docker.io/library/redis:${REDIS_TAG:?REDIS_TAG is not configured}
networks:
- backend
server:
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:?AUTHENTIK_TAG is not configured}
ports: []
depends_on:
postgresql: !reset []
env_file: !reset []
environment:
AUTHENTIK_EMAIL__PASSWORD: file:///run/secrets/authentik_email_password
AUTHENTIK_POSTGRESQL__HOST: ${AUTHENTIK_POSTGRESQL__HOST:?Authentik PostgreSQL host required}
AUTHENTIK_POSTGRESQL__PORT: ${AUTHENTIK_POSTGRESQL__PORT:?Authentik PostgreSQL port required}
AUTHENTIK_POSTGRESQL__PASSWORD: file:///run/secrets/postgres_password
AUTHENTIK_POSTGRESQL__SSLMODE: ${AUTHENTIK_POSTGRESQL__SSLMODE:-disable}
AUTHENTIK_POSTGRESQL__SSLROOTCERT: ${AUTHENTIK_POSTGRESQL__SSLROOTCERT:-/etc/ssl/certs/ca-certificates.crt}
AUTHENTIK_SECRET_KEY: file:///run/secrets/authentik_secret_key
ports: !reset []
secrets:
- authentik_email_password
- authentik_secret_key
- postgres_password
volumes:
- media:/media
- ./data/authentik/custom-templates:/templates
- geoip:/geoip
networks:
- backend
- dokploy-network
- frontend
labels:
- "traefik.enable=true"
- "traefik.docker.network=dokploy-network"
- "traefik.docker.network=frontend"
- "traefik.http.services.sso-server.loadbalancer.server.port=9443" # set port the container listenes to
- "traefik.http.services.sso-server.loadbalancer.server.scheme=https"
@ -57,7 +74,19 @@ services:
worker:
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:?AUTHENTIK_TAG is not configured}
env_file: !reset []
environment:
AUTHENTIK_EMAIL__PASSWORD: file:///run/secrets/authentik_email_password
AUTHENTIK_POSTGRESQL__HOST: ${AUTHENTIK_POSTGRESQL__HOST:?Authentik PostgreSQL host required}
AUTHENTIK_POSTGRESQL__PORT: ${AUTHENTIK_POSTGRESQL__PORT:?Authentik PostgreSQL port required}
AUTHENTIK_POSTGRESQL__PASSWORD: file:///run/secrets/postgres_password
AUTHENTIK_POSTGRESQL__SSLMODE: ${AUTHENTIK_POSTGRESQL__SSLMODE:-disable}
AUTHENTIK_POSTGRESQL__SSLROOTCERT: ${AUTHENTIK_POSTGRESQL__SSLROOTCERT:-/etc/ssl/certs/ca-certificates.crt}
AUTHENTIK_SECRET_KEY: file:///run/secrets/authentik_secret_key
secrets:
- authentik_email_password
- authentik_secret_key
- postgres_password
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- media:/media
@ -67,6 +96,16 @@ services:
networks:
- backend
secrets:
authentik_email_password:
environment: AUTHENTIK_EMAIL__PASSWORD
authentik_secret_key:
environment: AUTHENTIK_SECRET_KEY
geoip_license_key:
environment: GEOIPUPDATE_LICENSE_KEY
postgres_password:
environment: PG_PASS
volumes:
geoip:
driver: local
@ -77,5 +116,5 @@ volumes:
networks:
backend:
dokploy-network:
frontend:
external: true

View file

@ -1,10 +1,10 @@
services:
postgresql:
env_file:
- env/common.env
- .env
environment:
POSTGRES_DB: ${PG_DB:-authentik}
POSTGRES_PASSWORD_FILE: /run/secrets/postgres_password
POSTGRES_PASSWORD: ${PG_PASS:?database password required}
POSTGRES_USER: ${PG_USER:-authentik}
healthcheck:
interval: 30s
@ -16,8 +16,6 @@ services:
timeout: 5s
image: docker.io/library/postgres:16-alpine
restart: unless-stopped
secrets:
- postgres_password
volumes:
- database:/var/lib/postgresql/data
redis:
@ -42,24 +40,19 @@ services:
redis:
condition: service_healthy
env_file:
- env/common.env
- .env
environment:
AUTHENTIK_EMAIL__PASSWORD: file:///run/secrets/authentik_email_password
AUTHENTIK_POSTGRESQL__HOST: postgresql
AUTHENTIK_POSTGRESQL__NAME: ${PG_DB:-authentik}
AUTHENTIK_POSTGRESQL__PASSWORD: file:///run/secrets/postgres_password
AUTHENTIK_POSTGRESQL__PASSWORD: ${PG_PASS}
AUTHENTIK_POSTGRESQL__USER: ${PG_USER:-authentik}
AUTHENTIK_REDIS__HOST: redis
AUTHENTIK_SECRET_KEY: file:///run/secrets/authentik_secret_key
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2025.8.1}
AUTHENTIK_SECRET_KEY: ${AUTHENTIK_SECRET_KEY:?secret key required}
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2025.8.6}
ports:
- ${COMPOSE_PORT_HTTP:-9000}:9000
- ${COMPOSE_PORT_HTTPS:-9443}:9443
restart: unless-stopped
secrets:
- authentik_email_password
- authentik_secret_key
- postgres_password
volumes:
- ./media:/media
- ./custom-templates:/templates
@ -71,36 +64,22 @@ services:
redis:
condition: service_healthy
env_file:
- env/common.env
- .env
environment:
AUTHENTIK_EMAIL__PASSWORD: file:///run/secrets/authentik_email_password
AUTHENTIK_POSTGRESQL__HOST: postgresql
AUTHENTIK_POSTGRESQL__NAME: ${PG_DB:-authentik}
AUTHENTIK_POSTGRESQL__PASSWORD: file:///run/secrets/postgres_password
AUTHENTIK_POSTGRESQL__PASSWORD: ${PG_PASS}
AUTHENTIK_POSTGRESQL__USER: ${PG_USER:-authentik}
AUTHENTIK_REDIS__HOST: redis
AUTHENTIK_SECRET_KEY: file:///run/secrets/authentik_secret_key
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2025.8.1}
AUTHENTIK_SECRET_KEY: ${AUTHENTIK_SECRET_KEY:?secret key required}
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:-2025.8.6}
restart: unless-stopped
secrets:
- authentik_email_password
- authentik_secret_key
- postgres_password
user: root
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- ./media:/media
- ./certs:/certs
- ./custom-templates:/templates
secrets:
authentik_email_password:
environment: AUTHENTIK_EMAIL__PASSWORD
authentik_secret_key:
environment: AUTHENTIK_SECRET_KEY
geoip_license_key:
environment: GEOIPUPDATE_LICENSE_KEY
postgres_password:
environment: PG_PASS
volumes:
database:
driver: local

17
env/common.env vendored
View file

@ -1,18 +1,17 @@
# Deployment identity
DEPLOYMENT_ENVIRONMENT=prod
COMPOSE_PROJECT_NAME=sso-s1q-dev
PUBLIC_DOMAIN=sso.s1q.dev
TLS_DOMAIN=*.s1q.dev
# Explicit image versions. Update these values through Git.
AUTHENTIK_TAG=2025.8.1
POSTGRES_TAG=17.6-alpine
REDIS_TAG=8.0-alpine
GEOIPUPDATE_TAG=v7.1.1
# Authentik logging and error reporting
AUTHENTIK_ERROR_REPORTING__ENABLED=true
AUTHENTIK_LOG_LEVEL=warning
AUTHENTIK_ERROR_REPORTING__ENABLED=false
AUTHENTIK_LOG_LEVEL=info
# Authentik database configuration
AUTHENTIK_POSTGRESQL__HOST=sbx0db00.int.s1q.dev
AUTHENTIK_POSTGRESQL__PORT=5432
AUTHENTIK_POSTGRESQL__SSLMODE=verify-full
AUTHENTIK_POSTGRESQL__SSLROOTCERT=/etc/ssl/certs/ca-certificates.crt
# Email
AUTHENTIK_EMAIL__HOST=mail.s1q.dev

View file

@ -1,32 +0,0 @@
#!/usr/bin/env bash
set -euf -o pipefail
# Check if yq is installed
if ! command -v yq &>/dev/null; then
echo "yq is required but not installed. Please install it manually."
exit 1
fi
STAGE="${1:-}"
shift || true
if [[ -z "${STAGE,,}" ]]; then
echo "Usage: $0 <stage>"
echo "Example: $0 test"
exit 1
fi
cd "$(dirname "$(realpath "$0")")/../"
AUTHENTIK_DOCKER_COMPOSE_PATH="$(realpath "$(pwd)")"
# Merge docker-compose files using yq
# 1st merger is docker-compose.override.yml on top of the base docker-compose.yml
# 2nd merger is the stage-specific docker-compose file on top of the result of the first merger
# The final result is piped to docker compose command
yq eval-all 'select(fileIndex == 0) * select(fileIndex == 1)' \
docker-compose.yml \
docker-compose.override.yml \
| yq eval-all 'select(fileIndex == 0) * select(fileIndex == 1)' \
- \
docker-compose.${STAGE,,}.yml \
| ${DOCKER_COMPOSE_CLI} -f- ${@:-}

View file

@ -1,167 +0,0 @@
#!/usr/bin/env bash
set -euf -o pipefail
# Ask if initialized for production or test
while true; do
read -p "Do you want to init a [P]roduction or [T]est environment? (P/T): " DEPLOYMENT_ENVIRONMENT
case "$DEPLOYMENT_ENVIRONMENT" in
[Pp]* )
DEPLOYMENT_ENVIRONMENT="PRODUCTION"
ENV_TEMPLATE="env.prod.template"
break
;;
[Tt]* )
DEPLOYMENT_ENVIRONMENT="TEST"
ENV_TEMPLATE="env.test.template"
break
;;
* )
echo "Please answer with P or T."
;;
esac
done
source $(dirname "$(readlink -f "$0")")/../${ENV_TEMPLATE}
SERVICE_DOMAIN="${RESTIC_TAG:?Restic backup tag is missing -- RESTIC_TAG}"
BACKUP_TARGET_DOMAIN="${RESTIC_REPO_ADDRESS:?Restic backup target domain is missing -- RESTIC_REPO_ADDRESS}"
BACKUP_TARGET_USER="${RESTIC_REPO_USER:?Restic backup target user is missing -- RESTIC_REPO_USER}"
HOSTNAME=$(hostname -f)
# Function to securely query user for a password, verify it, and return it for further use
prompt_password() {
local purpose="$1"
local password password_confirm
while true; do
printf "Enter password for %s: " "$purpose"
read -rs password
printf "\nConfirm password for %s: " "$purpose"
read -rs password_confirm
printf "\n"
# Check if passwords match
if [[ "$password" == "$password_confirm" ]]; then
RETURNED_PASSWORD="$password"
printf "Password verified for %s.\n" "$purpose"
return 0
else
printf "Error: Passwords do not match. Please try again.\n" >&2
fi
done
}
# Trap SIGINT to exit gracefully if the user aborts with CTRL+C
trap 'printf "\nOperation aborted by user.\n" >&2; exit 1' SIGINT
cd "$(dirname "$(realpath "$0")")/../"
AUTHENTIK_DOCKER_COMPOSE_PATH="$(realpath "$(pwd)")"
if [[ ! -f ./docker-compose.yml ]]; then
[[ "${DEPLOYMENT_ENVIRONMENT}" == "PRODUCTION" ]] && ln -s ./docker-compose.prod.yml ./docker-compose.yml
[[ "${DEPLOYMENT_ENVIRONMENT}" == "TEST" ]] && ln -s ./docker-compose.test.yml ./docker-compose.yml
fi
# Check if .env exists and exit if it is
if [[ ! -f ./.env ]]; then
cat ./${ENV_TEMPLATE} >> ./.env
echo "# SECRETS" >> ./.env
prompt_password "PG_PASS (leave empty to generate a password)"; echo "PG_PASS=$([[ -n ${RETURNED_PASSWORD} ]] && echo -n "${RETURNED_PASSWORD}" || openssl rand -base64 36 | tr -d '\n')" >> ./.env; unset RETURNED_PASSWORD
prompt_password "AUTHENTIK_SECRET_KEY (leave empty to generate a password)"; echo "AUTHENTIK_SECRET_KEY=$([[ -n ${RETURNED_PASSWORD} ]] && echo -n "${RETURNED_PASSWORD}" || openssl rand -base64 60 | tr -d '\n')" >> ./.env; unset RETURNED_PASSWORD
prompt_password "AUTHENTIK_EMAIL__PASSWORD"; echo "AUTHENTIK_EMAIL__PASSWORD=${RETURNED_PASSWORD}" >> ./.env; unset RETURNED_PASSWORD
prompt_password "GEOIPUPDATE_LICENSE_KEY"; echo "GEOIPUPDATE_LICENSE_KEY=${RETURNED_PASSWORD}" >> ./.env; unset RETURNED_PASSWORD
prompt_password "RESTIC_REPO_PASSWORD (leave empty to generate a password)"; echo "RESTIC_REPO_PASSWORD=$([[ -n ${RETURNED_PASSWORD} ]] && echo -n "${RETURNED_PASSWORD}" || openssl rand -base64 60 | tr -d '\n')" >> ./.env; unset RETURNED_PASSWORD
echo "" >> ./.env
fi
# Check if lego.env exists and exit if it is
if [[ ! -f ./lego.env && "${DEPLOYMENT_ENVIRONMENT}" == "PRODUCTION" ]]; then
echo "# Lego - Let's Encrypt certificate tool" >> ./lego.env
prompt_password HETZNER_API_KEY; echo "HETZNER_API_KEY=${RETURNED_PASSWORD}" >> ./lego.env; unset RETURNED_PASSWORD
echo "" >> ./.env
fi
BACKUP_TARGET_KEY_TYPES="ed25519,rsa"
BACKUP_TARGET_IPV4=$(dig +short "${BACKUP_TARGET_DOMAIN}" A | grep -E '^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$')
BACKUP_TARGET_IPV6=$(dig +short "${BACKUP_TARGET_DOMAIN}" AAAA | grep -E '^(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))$')
# Check if ssh key already exists, otherwise generate one
[[ ! -d ./data/restic/ssh/ ]] && mkdir -p ./data/restic/ssh/ && chmod 700 ./data/restic/ssh/
if [[ ! -f ./data/restic/ssh/id_ed25519 ]]; then
ssh-keygen -t ed25519 -C "${SERVICE_DOMAIN}" -f ./data/restic/ssh/id_ed25519 && chmod 600 ./data/restic/ssh/id_ed25519
# Copy SSH key to backup target
cat ./data/restic/ssh/id_ed25519.pub | ssh -p23 ${BACKUP_TARGET_USER}@${BACKUP_TARGET_DOMAIN} install-ssh-key
fi
# Setup known_hosts for backup container
if [[ ! -f ./data/restic/ssh/known_hosts ]]; then
ssh-keyscan -p 23 -t ${BACKUP_TARGET_KEY_TYPES} ${BACKUP_TARGET_DOMAIN} > ./data/restic/ssh/known_hosts
ssh-keyscan -p 23 -t ${BACKUP_TARGET_KEY_TYPES} ${BACKUP_TARGET_IPV4} >> ./data/restic/ssh/known_hosts
ssh-keyscan -p 23 -t ${BACKUP_TARGET_KEY_TYPES} ${BACKUP_TARGET_IPV6} >> ./data/restic/ssh/known_hosts
ssh-keyscan -p 22 -t ${BACKUP_TARGET_KEY_TYPES} ${BACKUP_TARGET_DOMAIN} >> ./data/restic/ssh/known_hosts
ssh-keyscan -p 22 -t ${BACKUP_TARGET_KEY_TYPES} ${BACKUP_TARGET_IPV4} >> ./data/restic/ssh/known_hosts
ssh-keyscan -p 22 -t ${BACKUP_TARGET_KEY_TYPES} ${BACKUP_TARGET_IPV6} >> ./data/restic/ssh/known_hosts
chmod 600 ./data/restic/ssh/known_hosts
fi
if [[ "${DEPLOYMENT_ENVIRONMENT}" == "PRODUCTION" ]]; then
# Generate dhparam, if not existing
[[ ! -d ./data/nginx/certs ]] && mkdir -p ./data/nginx/certs && chmod 700 ./data/nginx/certs && chown 101:101 ./data/nginx/certs || true
[[ ! -f ./data/nginx/dhparams.pem ]] && echo "" && openssl dhparam -out ./data/nginx/dhparams.pem 4096 && chown 101:101 ./data/nginx/dhparams.pem \
&& echo "" && echo "Checking generated dhparams" && openssl dhparam -check -in ./data/nginx/dhparams.pem || true
# Create certificate
if [[ ! -d ./data/.lego ]]; then
echo ""
echo "Create certificate"
lego \
--path ./data/.lego \
--accept-tos \
--email="acme@base23.de" \
--domains="*.base23.de" \
--dns hetzner \
run \
&& install -m 400 -o 101 -g 101 "./data/.lego/certificates"/{_.base23.de.crt,_.base23.de.issuer.crt,_.base23.de.key} "./data/nginx/certs"
fi
# Setup directory for acme cheallenges
[[ ! -d ./data/nginx/acme ]] && mkdir -p ./data/nginx/acme
# Setup cronjob to automatically renew certificates
[[ ! -f /etc/systemd/system/lego-renew-wildcard-base23-de.service ]] && cat <<EOF > /etc/systemd/system/lego-renew-wildcard-base23-de.service && systemctl daemon-reload
[Unit]
Description=SSL Certificate renewal for *.base23.de with LEGO
Documentation=https://go-acme.github.io/lego/
Wants=network-online.target
After=network-online.target
[Service]
Type=oneshot
EnvironmentFile=${AUTHENTIK_DOCKER_COMPOSE_PATH}/lego.env
ExecStart=${AUTHENTIK_DOCKER_COMPOSE_PATH}/scripts/cert_renew.sh
WorkingDirectory=${AUTHENTIK_DOCKER_COMPOSE_PATH}/
User=root
Group=root
RemainAfterExit=no
[Install]
WantedBy=multi-user.target
EOF
[[ ! -f /etc/systemd/system/lego-renew-wildcard-base23-de.timer ]] && cat <<EOF > /etc/systemd/system/lego-renew-wildcard-base23-de.timer && systemctl daemon-reload && systemctl enable --now lego-renew-wildcard-base23-de.timer
[Unit]
Description=SSL Certificate renewal for *.base23.de with LEGO Timer
[Timer]
OnCalendar=*-*-* 01:32:00
# add extra delay, here up to 1 hour:
RandomizedDelaySec=1h
Persistent=true
[Install]
WantedBy=timers.target
EOF
fi

View file

@ -1,15 +1,15 @@
-----BEGIN AGE ENCRYPTED FILE-----
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IHNzaC1lZDI1NTE5IE0yRy94ZyBQRnc4
V1d0c1RoU1Baa1VKcmcvRlJmNDVRcko0MC95Y25YU0FkZVNHTHpVCmlCc28yWGp5
Tk1lKzFSUDdDSFdOc1JnM3lIUWx6ZUkxeUtFMDF0TldVRlkKLT4gc3NoLWVkMjU1
MTkgUzVoZU1RIHRTejhlTmlHMGpEU0lCWnBJbDd1clVLcU9NZUZ0VXhqbWxtZGxM
RFZqWGMKbFQ3b1RwRXBvZllYVEpyeHZTdkFQVTJCaVQ0bmYxRmRtWjlBS2dSRXpn
dwotLS0gN0RpUFZIeGZkQUxZMnN6aHpPcmVqdWpXSlBjTmhHeXJmQjJsK3NtNVZw
ZwqUQnen/0lpSkPthpgteS3q0WoNYbUgz4oQo8rGJffyLIezLlwC1s20QboMEr1p
oPuwJ8nPymQWoSjasydDTqtAl+Ke1cDPx1Qo/FLI1bB4jcFO/ck/g2g5AG+X0SJa
F6UgInwGdoMqThKg3i3eJI/0EjBfHBmp5zjzZbART4Sr54Cb8AaEpKV8S28NJGec
+Z00zS/7MnEPKhTSiNrIWNp8dKFqXS2GR6sSoEHhThXUVoXz7VwK6HWLbbWCUgMh
LoEYOXBu3AFE8+7bS57MQH2uGx/+7Rt3MhcXXpOz5e8//2NhWla63R8T+I0GHsVk
ObLnyoHu7nE7OuIVdMHD0kZnKBpZy1GE6Zj4JqTRcUtcVXr/SHyz5r3GbvbU37LI
IjGoP4QukixeqbEIag==
YWdlLWVuY3J5cHRpb24ub3JnL3YxCi0+IFgyNTUxOSBpRVlEMXB3b1ZsTDlqZ00z
djZQaXU5dG9BTGhNT2VDcEdqKytoalZ3M2dBCjZEQjJLZHBrbytzdXN1YitMLy9V
bGxKOXpvSjl2eWZFSEwxVGU3dnZYU2cKLT4gc3NoLWVkMjU1MTkgTTJHL3hnIEtY
WGd5b3dqQ3NsZ0JkbS83SGJPQyt3cHAzT1F3M0l3bklPMmgvbXBneEEKaTMxUlBS
dk1JNC9kMGNVN25UVUdiREZ5VXVlRTFnK3RITUJhRGI4WTVScwotLS0gYmFrRC9z
UmVBZUVEdWozTXEzakVhWTlQczRQTmZNdnVITy94NThacmFxTQqjb3dg++cz4TFP
ozngALxv/9V3A5ps+lL+p2zGUkiMX6eWS97cX7q9klwUP91yRuhP2MnON5BsFRSj
DPyt70lKwHON04wT2VwqHGE7oL0/Qmi1U+E4+Eo68lJaKc8f+S9FP5WryQhwTik0
HAQA+iafcNvHolXoQrPBsgngpyTHUV9ywg9Df0Da5kSoHSYX+tZ0oUi3R9L7JWs0
Pf64dxntZkuM1i5C8LRnilDuV9Ys2wAQ0jObsUEnV4kpLsMvJbSF6DpQMJdINa7i
0m250w9V4GSFnVKo7OSYR/BREVUBEuME0rlKlJVdLaC1SrTltPOq67xAdsaneNiV
1vYZ/O6SsglDQYodtjg1bnGjpybRmVIQ1mWNsIdjb43UFFDlfw47IDyGbwh9bkW6
ekhDBHca7MRBL5+c+KGZUW8=
-----END AGE ENCRYPTED FILE-----

View file

@ -1,47 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
repo_root=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)
test_dir=$(mktemp -d)
trap 'rm -rf -- "$test_dir"' EXIT
install -d "$test_dir/env" "$test_dir/.komodo"
cp -- "$repo_root/docker-compose.yml" "$test_dir/docker-compose.yml"
cp -- "$repo_root/docker-compose.override.yml" "$test_dir/docker-compose.override.yml"
cp -- "$repo_root/env/common.env" "$test_dir/env/common.env"
cat > "$test_dir/.komodo/prod.env" <<'EOF'
PG_PASS=SENTINEL_POSTGRES_PASSWORD
AUTHENTIK_SECRET_KEY=SENTINEL_AUTHENTIK_SECRET_KEY
AUTHENTIK_EMAIL__PASSWORD=SENTINEL_EMAIL_PASSWORD
GEOIPUPDATE_LICENSE_KEY=SENTINEL_GEOIP_LICENSE_KEY
EOF
(
cd "$test_dir"
docker compose \
--env-file env/common.env \
--env-file .komodo/prod.env \
--file docker-compose.yml \
--file docker-compose.override.yml \
config --format json
) > "$test_dir/rendered.json"
if grep -F 'SENTINEL_' "$test_dir/rendered.json"; then
printf 'Rendered Compose configuration contains plaintext secrets\n' >&2
exit 1
fi
jq -e '
.services.postgresql.environment.POSTGRES_PASSWORD_FILE == "/run/secrets/postgres_password" and
(.services.postgresql.environment | has("POSTGRES_PASSWORD") | not) and
.services.server.environment.AUTHENTIK_POSTGRESQL__PASSWORD == "file:///run/secrets/postgres_password" and
.services.server.environment.AUTHENTIK_SECRET_KEY == "file:///run/secrets/authentik_secret_key" and
.services.server.environment.AUTHENTIK_EMAIL__PASSWORD == "file:///run/secrets/authentik_email_password" and
.services.geoipupdate.environment.GEOIPUPDATE_LICENSE_KEY_FILE == "/run/secrets/geoip_license_key" and
([.services.postgresql.secrets[].source] | sort) == ["postgres_password"] and
([.services.server.secrets[].source] | sort) == ["authentik_email_password", "authentik_secret_key", "postgres_password"] and
([.services.worker.secrets[].source] | sort) == ["authentik_email_password", "authentik_secret_key", "postgres_password"] and
([.services.geoipupdate.secrets[].source] | sort) == ["geoip_license_key"] and
(.services.redis | has("secrets") | not)
' "$test_dir/rendered.json" >/dev/null