From 71736f8dd93f9ee386f2a2af48fbd6a98d44ce6c Mon Sep 17 00:00:00 2001 From: phg Date: Tue, 1 Sep 2026 09:22:37 +0000 Subject: [PATCH] feat: enhance Authentik deployment with age-encrypted secrets; change deployment to komodo --- .age-docker.toml | 12 ++ README.md | 216 ++++++++++++++++++++++++++---------- docker-compose.override.yml | 65 ++++++++--- docker-compose.yml | 41 ++----- env/common.env | 17 ++- scripts/compose.sh | 32 ------ scripts/init.sh | 167 ---------------------------- secrets/prod.env.age | 26 ++--- tests/compose-secrets.sh | 47 -------- 9 files changed, 254 insertions(+), 369 deletions(-) create mode 100644 .age-docker.toml delete mode 100755 scripts/compose.sh delete mode 100755 scripts/init.sh delete mode 100644 tests/compose-secrets.sh diff --git a/.age-docker.toml b/.age-docker.toml new file mode 100644 index 0000000..a583111 --- /dev/null +++ b/.age-docker.toml @@ -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"] diff --git a/README.md b/README.md index 40cd241..ebcd3aa 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/docker-compose.override.yml b/docker-compose.override.yml index 7ff8673..502d697 100644 --- a/docker-compose.override.yml +++ b/docker-compose.override.yml @@ -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 diff --git a/docker-compose.yml b/docker-compose.yml index c7ec86f..fda03b7 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -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 diff --git a/env/common.env b/env/common.env index a18a86d..7a8320e 100644 --- a/env/common.env +++ b/env/common.env @@ -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 diff --git a/scripts/compose.sh b/scripts/compose.sh deleted file mode 100755 index 9ddaea3..0000000 --- a/scripts/compose.sh +++ /dev/null @@ -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 " - 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- ${@:-} diff --git a/scripts/init.sh b/scripts/init.sh deleted file mode 100755 index 43be8ef..0000000 --- a/scripts/init.sh +++ /dev/null @@ -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 < /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 < /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 diff --git a/secrets/prod.env.age b/secrets/prod.env.age index 37f269b..4b81fa9 100644 --- a/secrets/prod.env.age +++ b/secrets/prod.env.age @@ -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----- diff --git a/tests/compose-secrets.sh b/tests/compose-secrets.sh deleted file mode 100644 index f4ff4e4..0000000 --- a/tests/compose-secrets.sh +++ /dev/null @@ -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