feat: deploy Authentik with age-encrypted secrets

This commit is contained in:
Philip Henning 2026-08-21 14:04:26 +02:00
parent 9f8f619f3e
commit e966ec09ae
9 changed files with 240 additions and 260 deletions

3
.gitignore vendored
View file

@ -1,9 +1,12 @@
# Project # Project
.env .env
lego.env lego.env
.komodo/
data/* data/*
data/nginx/* data/nginx/*
!data !data
!data/authentik
!data/authentik/custom-templates
!data/nginx !data/nginx
!data/nginx/default.conf.template !data/nginx/default.conf.template

341
README.md
View file

@ -1,269 +1,120 @@
# `sso.s1q.dev` - Base23 SSO for all services # Authentik deployment
[Authentik](https://goauthentik.io/) based SSO for our sevices. 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.
## Table of Contents ## Layout
- [`sso.s1q.dev` - Base23 SSO for all services](#ssos1qdev---base23-sso-for-all-services) ```text
- [Table of Contents](#table-of-contents) .
- [Prerequisites](#prerequisites) ├── docker-compose.yml
- [Tailscale](#tailscale) ├── docker-compose.override.yml
- [CrowdSec](#crowdsec) ├── komodo.toml
- [Setup CrowdSec Repo](#setup-crowdsec-repo) ├── env/
- [Install CrowdSec](#install-crowdsec) │ └── common.env
- [Configure CrowdSec](#configure-crowdsec) ├── secrets/
- [Installation](#installation) │ └── prod.env.age
- [Clone \& configure initially](#clone--configure-initially) ├── scripts/
- [Fist run](#fist-run) │ ├── compose.sh
- [Upgrade](#upgrade) │ └── init.sh
- [Test](#test) └── tests/
- [Prod](#prod) └── compose-secrets.sh
- [Disaster recovery / restore](#disaster-recovery--restore)
- [Rebuild containers locally](#rebuild-containers-locally)
## Prerequisites
- dokploy
### Tailscale
```shell
printf "Enter preauthkey for Tailscale: " \
&& read -rs TAILSCALE_PREAUTHKEY \
&& curl -fsSL https://pkgs.tailscale.com/stable/debian/bookworm.noarmor.gpg | sudo tee /usr/share/keyrings/tailscale-archive-keyring.gpg >/dev/null \
&& curl -fsSL https://pkgs.tailscale.com/stable/debian/bookworm.tailscale-keyring.list | sudo tee /etc/apt/sources.list.d/tailscale.list \
&& apt-get update \
&& apt-get install tailscale \
&& tailscale up --login-server https://vpn.s1q.dev --authkey ${TAILSCALE_PREAUTHKEY} \
&& sleep 2 \
&& tailscale status \
&& unset TAILSCALE_PREAUTHKEY
``` ```
### CrowdSec `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.
#### Setup CrowdSec Repo 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`.
```shell Komodo passes `docker-compose.yml` first and `docker-compose.override.yml`
apt update \ second. The base and deployment-specific override remain separate by design.
&& apt upgrade -y \
&& apt install -y debian-archive-keyring \ ## Secrets
&& apt install -y curl gnupg apt-transport-https \
&& mkdir -p /etc/apt/keyrings/ \ The encrypted production bundle contains:
&& curl -fsSL https://packagecloud.io/crowdsec/crowdsec/gpgkey | gpg --dearmor > /etc/apt/keyrings/crowdsec_crowdsec-archive-keyring.gpg \
&& cat << EOF > /etc/apt/sources.list.d/crowdsec_crowdsec.list \ ```text
&& apt update PG_PASS
deb [signed-by=/etc/apt/keyrings/crowdsec_crowdsec-archive-keyring.gpg] https://packagecloud.io/crowdsec/crowdsec/any any main AUTHENTIK_SECRET_KEY
deb-src [signed-by=/etc/apt/keyrings/crowdsec_crowdsec-archive-keyring.gpg] https://packagecloud.io/crowdsec/crowdsec/any any main AUTHENTIK_EMAIL__PASSWORD
EOF GEOIPUPDATE_LICENSE_KEY
``` ```
#### Install CrowdSec To edit it locally, decrypt to a mode-`0600` temporary file, edit it, then
encrypt it to both recipients:
Install CrowdSec: ```bash
tmp=$(mktemp)
chmod 0600 "$tmp"
trap 'rm -f -- "$tmp"' EXIT HUP INT TERM
```shell age --decrypt \
printf "Enter CrowdSec context: " \ --identity ~/.ssh/identities/agenix-phg \
&& read -rs CROWDSEC_CONTEXT \ --output "$tmp" \
&& apt install -y crowdsec crowdsec-firewall-bouncer-iptables \ secrets/prod.env.age
&& cscli completion bash | tee /etc/bash_completion.d/cscli \
&& source ~/.bashrc \ ${EDITOR:-vi} "$tmp"
&& cscli console enroll -e context ${CROWDSEC_CONTEXT} \
&& unset CROWDSEC_CONTEXT age --encrypt \
--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
``` ```
Restart CordSec Service, after accepting the enrollment on the [CrowdSec Console](https://app.crowdsec.net/): 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
track the temporary `.komodo/prod.env` file.
```shell The decrypted values are Compose secret sources rather than service
systemctl restart crowdsec; systemctl status crowdsec.service environment values. Containers receive only their declared files under
``` `/run/secrets`; Authentik reads them through `file://` configuration values,
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.
#### Configure CrowdSec ## Compose wrapper
Whitelist Tailscale IPs: Komodo wraps `config`, `pull`, `up`, and `run` with
the NixOS-provided `with-age-env` command. The wrapper:
```shell - obtains an exclusive per-bundle lock;
cat << EOF > /etc/crowdsec/parsers/s02-enrich/01-s1q-dev-tailscale.yaml \ - removes stale plaintext and temporary files;
&& systemctl restart crowdsec; journalctl -xef -u crowdsec.service - decrypts through the NixOS-provided systemd `age-identity` credential;
name: s1q-dev/tailscale ## Must be unqiue - creates plaintext with mode `0600`; and
description: "Whitelist events from Tailscale Subnet" - removes plaintext on success, failure, or a handled signal.
whitelist:
reason: "Tailscale clients"
cidr:
- "100.64.0.0/10"
EOF
```
Whitelist my current Public IPs: 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.
```shell NixOS owns the wrapper, its runtime dependencies, the host-key systemd
mkdir -p /etc/crowdsec/postoverflows/s01-whitelist/ \ credential, Periphery's Podman permissions, and the external
&& cat << EOF > /etc/crowdsec/postoverflows/s01-whitelist/01-s1q-dev-public-ips.yaml \ `dokploy-network`. Workload containers do not receive the age identity, but
&& crowdsec -t && systemctl restart crowdsec; systemctl status crowdsec.service commands launched by Periphery can access it. Revisit that trust boundary if
name: s1q-dev/public-ips ## Must be unqiue deployment authority is ever granted to another operator.
description: "Whitelist events from s1q-dev public IPs"
whitelist:
reason: "s1q-dev Public IPs"
expression:
- evt.Overflow.Alert.Source.IP in LookupHost("r3w.de")
EOF
```
Add Authentik integration: 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.
```shell ## Optional test environment
cscli collections install firix/authentik \
&& cat << EOF > /etc/crowdsec/acquis.d/authentik.yaml \
&& crowdsec -t && systemctl restart crowdsec
---
source: docker
container_name_regexp:
- sso-s1q-dev-de-server-*
- sso-s1q-dev-de-worker-*
labels:
type: authentik
EOF
```
Enable increasing ban time: Do not create a test Stack or dummy bundle until there is a real long-lived
`test` branch. When it is needed:
```shell 1. add `secrets/test.env.age`, encrypted to the test host and personal keys;
sed -i -e 's/^#duration_expr/duration_expr/g' /etc/crowdsec/profiles.yaml \ 2. keep both Stack declarations in `komodo.toml` on `main`;
&& crowdsec -t && systemctl restart crowdsec 3. point `authentik-test` at branch `test` and its own encrypted/decrypted
``` paths; and
4. add `authentik-test` to the Procedure's deployment stage in the central
bootstrap repository.
Setup notifications: The repository webhook should use the Procedure's `__ANY__` endpoint so pushes
to either protected environment branch enter the same ordered flow.
## Installation
### Clone & configure initially
1. [Create a Storage Box sub account](https://confluence.base23.de/pages/viewpage.action?pageId=27820074).
2. Enter the username to `env.template`.
3. Run the initial configuration script:
```shell
cd /root/apps \
&& git clone ssh://git@git.base23.de:222/base23/sso.s1q.dev.git \
&& cd sso.s1q.dev \
&& ./scripts/init.sh
```
4. Use the generated SSH key and copy it to the Hetzner Storage box for backups:
```shell
TARGET_DOMAIN=cloud.backup.base23.de \
TARGET_KEY_TYPES="ecdsa-sha2-nistp521,ed25519,ed25519-sk,rsa,dsa,ecdsa,ecdsa-sk" \
TARGET_IPV4=$(dig +short "${TARGET_DOMAIN}" A | grep -E '^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$') \
TARGET_IPV6=$(dig +short "${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]))$') \
&& cat ./data/restic/ssh/id_ed25519.pub | ssh -p23 u291924-sub4@${TARGET_DOMAIN} install-ssh-key \
&& ssh-keyscan -p 23 -t ${TARGET_KEY_TYPES} ${TARGET_DOMAIN} > ./data/restic/ssh/known_hosts \
&& ssh-keyscan -p 23 -t ${TARGET_KEY_TYPES} ${TARGET_IPV4} >> ./data/restic/ssh/known_hosts \
&& ssh-keyscan -p 23 -t ${TARGET_KEY_TYPES} ${TARGET_IPV6} >> ./data/restic/ssh/known_hosts \
&& ssh-keyscan -p 22 -t ${TARGET_KEY_TYPES} ${TARGET_DOMAIN} >> ./data/restic/ssh/known_hosts \
&& ssh-keyscan -p 22 -t ${TARGET_KEY_TYPES} ${TARGET_IPV4} >> ./data/restic/ssh/known_hosts \
&& ssh-keyscan -p 22 -t ${TARGET_KEY_TYPES} ${TARGET_IPV6} >> ./data/restic/ssh/known_hosts
```
### Fist run
```shell
./scripts/compose.sh build --no-cache \
--build-arg BUILD_DATE=$(date -u +'%Y-%m-%dT%H:%M:%SZ') \
--build-arg SRC_REV=$(git rev-parse --short HEAD) \
&& ./scripts/compose.sh up -d; ./scripts/compose.sh logs -f
```
## Upgrade
### Test
This is intended for testing the upgrades before rollout on prod.
1. Check if the backups are up to date: `./scripts/compose.sh run --rm restore-cli /usr/local/bin/restic snapshots`
2. Create a new branch `git checkout -b <version>`.
3. Download the the docker compose for the version you want to update:
`curl -fsSL -o docker-compose.yml https://goauthentik.io/version/<version>/docker-compose.yml`
4. Update `AUTHENTIK_TAG` to the desired tag in `env.test.template`.
5. Check the upstream `docker-compose.yml` file against ours for changes made in the configuration. Check the Postgresql and Redis docker tags. Minor revisions of Postgresql *should be fine*, check the changelogs for any issues and if none are present raise to the latest Minor version (e.g. 16.6 -> 16.9). Redis should be less problematic for upgrades, check nonetheless.
6. Run `diff --color='auto' env.test.template .env` to display the diff between
`env.test.template` and `.env`.
7. Port the changes made to `.env`.
8. `./scripts/compose.sh pull`
9. `./scripts/compose.sh down`
10. `./scripts/compose.sh up -d; ./scripts/compose.sh logs -f`
9. Check the logs for any issues during during startup. Check if https://sso.test.base23.de is available and test the sso login (e.g https://whoami.test.base23.de)
10. Apply changes for test to prod files (`docker-compose.<stage>.yml`, `env.<stage>.template`), commit & push changes to the Repo in a new branch and create a merge request in preparation for the prod upgrade
### Prod
It is expected that the Upgrade has already been performed and tested on https://sso.test.base23.de, and the changes have been merged into main
1. Check if the backups are up to date: `./scripts/compose.sh run --rm restore-cli /usr/local/bin/restic snapshots`
2. Create a new branch `git checkout -b <version>`.
3. Download the the docker compose for the version you want to update:
`curl -fsSL -o docker-compose.yml https://goauthentik.io/version/<version>/docker-compose.yml`
4. Update `AUTHENTIK_TAG` to the desired tag in `env.prod.template`.
5. Commit & push changes to the Repo.
6. Run `diff --color='auto' env.prod.template .env` to display the diff between
`env.prod.template` and `.env`.
7. Port the made changes to `.env`.
8. `./scripts/compose.sh pull`
9. `./scripts/compose.sh down`
10. `./scripts/compose.sh up -d; ./scripts/compose.sh logs -f`
## Disaster recovery / restore
**IMPORTANT:**
You've to use different docker cli clients on prod/test.
- Prod
- Docker: `docker`
- Docker compose: `docker compose`
- Test
- Docker: `docker`
- Docker compose: `docker-compose-2.32.4`
For the ease of readability I'll use `docker` and `docker compose` in the documentation below,
please repalce it for restores on test!
1. Run the restore cli
```shell
docker compose run --rm restore-cli
```
2. Run the restore command and follow it's instructions
```shell
restore
```
3. If the restore was successfull, exit the restore container.
**DO NOT START THE APPLICATION YET!**
4. Run the PostgreSQL container without starting the main application
```shell
docker compose run --rm postgresql
```
5. Open another shell in the sso git directory.
6. Execute a shell in the running PostgreSQL container (replace `<containerid>` with the actual container id)
```shell
docker exec -it sso-base23-de-postgresql-run-<containerid> bash
```
7. If the database already contains data, delete an recreate it:
```shell
dropdb -U ${PG_USER:-authentik} ${PG_DB:-authentik}
createdb -U ${PG_USER:-authentik} ${PG_DB:-authentik}
```
8. Restore the database
```shell
psql ${PG_USER:-authentik} -d ${PG_DB:-authentik} -f /var/lib/postgresql/backups/authentik.sql
````
9. After the database is restored, exit the container
10. Now it's safe to start the complete application stack again
```shell
docker compose up -d; docker compose logs -f
```
## Rebuild containers locally
```shell
docker compose build --no-cache \
--build-arg BUILD_DATE=$(date -u +'%Y-%m-%dT%H:%M:%SZ') \
--build-arg SRC_REV=$(git rev-parse --short HEAD)
```

View file

View file

@ -2,7 +2,7 @@
services: services:
geoipupdate: geoipupdate:
image: "maxmindinc/geoipupdate:latest" image: "maxmindinc/geoipupdate:${GEOIPUPDATE_TAG:?GEOIPUPDATE_TAG is not configured}"
restart: unless-stopped restart: unless-stopped
volumes: volumes:
- "geoip:/usr/share/GeoIP" - "geoip:/usr/share/GeoIP"
@ -10,7 +10,9 @@ services:
GEOIPUPDATE_EDITION_IDS: "GeoLite2-City GeoLite2-ASN" GEOIPUPDATE_EDITION_IDS: "GeoLite2-City GeoLite2-ASN"
GEOIPUPDATE_FREQUENCY: "8" GEOIPUPDATE_FREQUENCY: "8"
GEOIPUPDATE_ACCOUNT_ID: "${GEOIPUPDATE_ACCOUNT_ID:?MaxMind GeoIP account ID required}" GEOIPUPDATE_ACCOUNT_ID: "${GEOIPUPDATE_ACCOUNT_ID:?MaxMind GeoIP account ID required}"
GEOIPUPDATE_LICENSE_KEY: "${GEOIPUPDATE_LICENSE_KEY:?MaxMind GeoIP license key required}" GEOIPUPDATE_LICENSE_KEY_FILE: /run/secrets/geoip_license_key
secrets:
- geoip_license_key
postgresql: postgresql:
image: docker.io/library/postgres:${POSTGRES_TAG:?POSTGRES_TAG is not configured} image: docker.io/library/postgres:${POSTGRES_TAG:?POSTGRES_TAG is not configured}
@ -24,8 +26,6 @@ services:
server: server:
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:?AUTHENTIK_TAG is not configured} image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:?AUTHENTIK_TAG is not configured}
environment:
AUTHENTIK_POSTGRESQL__PASSWORD: ${PG_PASS:?PG_PASS is required. - Password for authentik's postgresql database}
ports: [] ports: []
volumes: volumes:
- media:/media - media:/media
@ -58,8 +58,6 @@ services:
worker: worker:
image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:?AUTHENTIK_TAG is not configured} image: ${AUTHENTIK_IMAGE:-ghcr.io/goauthentik/server}:${AUTHENTIK_TAG:?AUTHENTIK_TAG is not configured}
environment:
AUTHENTIK_POSTGRESQL__PASSWORD: ${PG_PASS:?PG_PASS is required. - Password for authentik's postgresql database}
volumes: volumes:
- /var/run/docker.sock:/var/run/docker.sock - /var/run/docker.sock:/var/run/docker.sock
- media:/media - media:/media
@ -80,4 +78,4 @@ volumes:
networks: networks:
backend: backend:
dokploy-network: dokploy-network:
external: true external: true

View file

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

31
env/common.env vendored Normal file
View file

@ -0,0 +1,31 @@
# 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
# Email
AUTHENTIK_EMAIL__HOST=mail.s1q.dev
AUTHENTIK_EMAIL__PORT=25
AUTHENTIK_EMAIL__USERNAME=smtp@s1q.dev
AUTHENTIK_EMAIL__USE_TLS=true
AUTHENTIK_EMAIL__USE_SSL=false
AUTHENTIK_EMAIL__TIMEOUT=10
AUTHENTIK_EMAIL__FROM=sso@s1q.dev
# MaxMind GeoIP
GEOIPUPDATE_ACCOUNT_ID=765001
# PostgreSQL
PG_USER=authentik
PG_DB=authentik

29
komodo.toml Normal file
View file

@ -0,0 +1,29 @@
[[stack]]
name = "authentik-prod"
description = "Production Authentik identity provider"
tags = ["sync:authentik", "application:authentik", "environment:production"]
deploy = false
[stack.config]
server = "sbx0docker01"
project_name = "sso-s1q-dev"
git_provider = "git.s1q.dev"
git_https = true
repo = "docker-compose/authentik"
branch = "main"
run_directory = ""
file_paths = ["docker-compose.yml", "docker-compose.override.yml"]
additional_env_files = [
{ path = "env/common.env", track = true },
{ path = ".komodo/prod.env", track = false },
]
config_files = [
{ path = "secrets/prod.env.age", requires = "Redeploy" },
]
compose_cmd_wrapper = "with-age-env secrets/prod.env.age .komodo/prod.env -- [[COMPOSE_COMMAND]]"
compose_cmd_wrapper_include = ["config", "pull", "up", "run"]
webhook_enabled = false
webhook_force_deploy = false
auto_update = false
poll_for_updates = false
send_alerts = true

BIN
secrets/prod.env.age Normal file

Binary file not shown.

47
tests/compose-secrets.sh Normal file
View file

@ -0,0 +1,47 @@
#!/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