authentik/README.md

120 lines
4.2 KiB
Markdown

# 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.
## Layout
```text
.
├── docker-compose.yml
├── docker-compose.override.yml
├── komodo.toml
├── env/
│ └── common.env
├── secrets/
│ └── prod.env.age
├── scripts/
│ ├── compose.sh
│ └── init.sh
└── tests/
└── compose-secrets.sh
```
`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.
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.
## Secrets
The encrypted production bundle contains:
```text
PG_PASS
AUTHENTIK_SECRET_KEY
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:
```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 \
--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
```
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.
The decrypted values are Compose secret sources rather than 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.
## Compose 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;
- decrypts through the NixOS-provided systemd `age-identity` credential;
- creates plaintext with mode `0600`; and
- removes plaintext on success, failure, or a handled signal.
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.
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.
## Optional test environment
Do not create a test Stack or dummy bundle until there is a real long-lived
`test` branch. When it is needed:
1. add `secrets/test.env.age`, encrypted to the test host and personal keys;
2. keep both Stack declarations in `komodo.toml` on `main`;
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.
The repository webhook should use the Procedure's `__ANY__` endpoint so pushes
to either protected environment branch enter the same ordered flow.