feat: deploy Authentik with age-encrypted secrets
This commit is contained in:
parent
9f8f619f3e
commit
e966ec09ae
9 changed files with 240 additions and 260 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
|
@ -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
341
README.md
|
|
@ -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)
|
|
||||||
```
|
|
||||||
|
|
|
||||||
0
data/authentik/custom-templates/.gitkeep
Normal file
0
data/authentik/custom-templates/.gitkeep
Normal 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
|
||||||
|
|
|
||||||
|
|
@ -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
31
env/common.env
vendored
Normal 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
29
komodo.toml
Normal 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
BIN
secrets/prod.env.age
Normal file
Binary file not shown.
47
tests/compose-secrets.sh
Normal file
47
tests/compose-secrets.sh
Normal 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
|
||||||
Loading…
Add table
Add a link
Reference in a new issue