177 lines
7.4 KiB
Bash
Executable File
177 lines
7.4 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# infra/authentik/backup/backup.sh
|
|
#
|
|
# Daily backup of the self-hosted Authentik stack. Triggered by crond at 03:30
|
|
# UTC (see crontab). Idempotent — re-running on the same day overwrites the
|
|
# same S3 prefix.
|
|
#
|
|
# Steps:
|
|
# 1. pg_dump --format=custom of the Authentik Postgres
|
|
# 2. tar of /media (Authentik media volume, mounted into this container)
|
|
# 3. SECRETS-MANIFEST.txt listing the env-var names that MUST be backed up
|
|
# out-of-band (Coolify-Vault export)
|
|
# 4. GPG-encrypt all three artifacts (recipient OR AES256 fallback)
|
|
# 5. Upload to S3 under s3://<bucket>/authentik/YYYY-MM-DD/
|
|
#
|
|
# Env (sourced from the Compose service `environment` block, which itself
|
|
# reads from infra/authentik/.env):
|
|
# AUTHENTIK_S3_BACKUP_ENDPOINT S3 endpoint URL
|
|
# AUTHENTIK_S3_BACKUP_BUCKET target bucket name
|
|
# AUTHENTIK_S3_BACKUP_ACCESS_KEY S3 access key
|
|
# AUTHENTIK_S3_BACKUP_SECRET_KEY S3 secret key
|
|
# AUTHENTIK_SECRET_KEY fallback symmetric passphrase for GPG
|
|
# AUTHENTIK_BACKUP_GPG_RECIPIENT optional GPG recipient fingerprint
|
|
#
|
|
# AUTHENTIK_PG_HOST default "postgresql" (compose service name)
|
|
# AUTHENTIK_PG_PORT default 5432
|
|
# AUTHENTIK_POSTGRESQL__USER default "authentik"
|
|
# AUTHENTIK_POSTGRESQL__NAME default "authentik"
|
|
# AUTHENTIK_POSTGRESQL__PASSWORD required (read via PGPASSWORD env)
|
|
|
|
set -Eeuo pipefail
|
|
shopt -s inherit_errexit 2>/dev/null || true
|
|
|
|
# -----------------------------------------------------------------------------
|
|
# Defaults + required-env guard
|
|
# -----------------------------------------------------------------------------
|
|
: "${AUTHENTIK_PG_HOST:=postgresql}"
|
|
: "${AUTHENTIK_PG_PORT:=5432}"
|
|
: "${AUTHENTIK_POSTGRESQL__USER:=authentik}"
|
|
: "${AUTHENTIK_POSTGRESQL__NAME:=authentik}"
|
|
|
|
: "${AUTHENTIK_S3_BACKUP_ENDPOINT:?AUTHENTIK_S3_BACKUP_ENDPOINT is required}"
|
|
: "${AUTHENTIK_S3_BACKUP_BUCKET:?AUTHENTIK_S3_BACKUP_BUCKET is required}"
|
|
: "${AUTHENTIK_S3_BACKUP_ACCESS_KEY:?AUTHENTIK_S3_BACKUP_ACCESS_KEY is required}"
|
|
: "${AUTHENTIK_S3_BACKUP_SECRET_KEY:?AUTHENTIK_S3_BACKUP_SECRET_KEY is required}"
|
|
: "${AUTHENTIK_POSTGRESQL__PASSWORD:?AUTHENTIK_POSTGRESQL__PASSWORD is required}"
|
|
|
|
TODAY="$(date -u +%Y-%m-%d)"
|
|
NOW="$(date -u +%Y-%m-%dT%H-%M-%SZ)"
|
|
WORK="${BACKUP_HOME:-/srv/backup}/work/${TODAY}"
|
|
mkdir -p "${WORK}"
|
|
|
|
log() { printf "[authentik-backup %s] %s\n" "${NOW}" "$*"; }
|
|
|
|
cleanup() { rm -rf "${WORK}"; }
|
|
trap cleanup EXIT
|
|
|
|
# -----------------------------------------------------------------------------
|
|
# Step 1: Postgres dump
|
|
# -----------------------------------------------------------------------------
|
|
log "dumping Postgres (${AUTHENTIK_PG_HOST}/${AUTHENTIK_POSTGRESQL__NAME})"
|
|
PG_DUMP_FILE="${WORK}/authentik-postgres-${NOW}.dump"
|
|
export PGPASSWORD="${AUTHENTIK_POSTGRESQL__PASSWORD}"
|
|
pg_dump \
|
|
--host "${AUTHENTIK_PG_HOST}" \
|
|
--port "${AUTHENTIK_PG_PORT}" \
|
|
--username "${AUTHENTIK_POSTGRESQL__USER}" \
|
|
--dbname "${AUTHENTIK_POSTGRESQL__NAME}" \
|
|
--format=custom \
|
|
--compress=9 \
|
|
--no-owner \
|
|
--no-privileges \
|
|
--file "${PG_DUMP_FILE}"
|
|
log "postgres dump size: $(du -h "${PG_DUMP_FILE}" | cut -f1)"
|
|
|
|
# -----------------------------------------------------------------------------
|
|
# Step 2: media volume tar
|
|
# -----------------------------------------------------------------------------
|
|
MEDIA_TAR="${WORK}/authentik-media-${NOW}.tar.gz"
|
|
if [[ -d /media ]]; then
|
|
log "archiving /media"
|
|
tar -czf "${MEDIA_TAR}" -C / media || log "media tar exited non-zero (continuing)"
|
|
log "media archive size: $(du -h "${MEDIA_TAR}" | cut -f1)"
|
|
else
|
|
log "/media not mounted in backup container — skipping media tar"
|
|
MEDIA_TAR=""
|
|
fi
|
|
|
|
# -----------------------------------------------------------------------------
|
|
# Step 3: SECRETS-MANIFEST.txt
|
|
# -----------------------------------------------------------------------------
|
|
# CRITICAL: without these secrets the Postgres dump is useless — Authentik
|
|
# encrypts OIDC provider client_secrets, password-stage data, etc. with
|
|
# AUTHENTIK_SECRET_KEY. Restoring a dump against a fresh instance with a
|
|
# DIFFERENT key will leave you with unreadable encrypted fields.
|
|
SECRETS_MANIFEST="${WORK}/SECRETS-MANIFEST-${NOW}.txt"
|
|
cat > "${SECRETS_MANIFEST}" <<EOF
|
|
Authentik Backup — SECRETS MANIFEST (${NOW})
|
|
|
|
The following secrets are REQUIRED to restore this backup. They are NOT
|
|
included in the Postgres dump. Back them up separately from Coolify-Vault
|
|
(or your equivalent secret-store) to an offline password vault.
|
|
|
|
REQUIRED for restore:
|
|
- AUTHENTIK_SECRET_KEY
|
|
Decrypts all Authentik-encrypted fields. CANNOT BE ROTATED without
|
|
destroying all encrypted-field data (sessions, OIDC client secrets,
|
|
password-stage data). Loss = full re-onboarding of all admin users.
|
|
|
|
- AUTHENTIK_POSTGRESQL__PASSWORD
|
|
Used by the new Authentik instance to connect to the restored Postgres.
|
|
|
|
- AUTHENTIK_BOOTSTRAP_TOKEN
|
|
Optional but useful for re-bootstrapping blueprints via API.
|
|
|
|
OPTIONAL but recommended:
|
|
- TOOLCTRL_ADMIN_OIDC_CLIENT_SECRET
|
|
The OIDC provider's client_secret is stored encrypted in Postgres;
|
|
the plaintext lives in Coolify-Vault and in the boilerplate app's
|
|
.env (consumed by Phase 09 #82).
|
|
|
|
- AUTHENTIK_SENTRY__DSN
|
|
If you ship error-reporting to the self-hosted Sentry.
|
|
|
|
- AUTHENTIK_EMAIL__PASSWORD
|
|
SMTP credentials for recovery mail.
|
|
|
|
Postgres dump artifact: $(basename "${PG_DUMP_FILE}")
|
|
Media archive: $([ -n "${MEDIA_TAR}" ] && basename "${MEDIA_TAR}" || echo "n/a")
|
|
EOF
|
|
|
|
# -----------------------------------------------------------------------------
|
|
# Step 4: encrypt
|
|
# -----------------------------------------------------------------------------
|
|
encrypt() {
|
|
local src="$1"
|
|
local dst="${src}.gpg"
|
|
if [[ -n "${AUTHENTIK_BACKUP_GPG_RECIPIENT:-}" ]]; then
|
|
log "encrypting ${src##*/} to GPG recipient ${AUTHENTIK_BACKUP_GPG_RECIPIENT}"
|
|
gpg --batch --yes --trust-model always --recipient "${AUTHENTIK_BACKUP_GPG_RECIPIENT}" \
|
|
--output "${dst}" --encrypt "${src}"
|
|
else
|
|
log "encrypting ${src##*/} symmetrically (AES256) with AUTHENTIK_SECRET_KEY"
|
|
gpg --batch --yes --symmetric --cipher-algo AES256 \
|
|
--passphrase "${AUTHENTIK_SECRET_KEY:?AUTHENTIK_SECRET_KEY missing for symmetric fallback}" \
|
|
--output "${dst}" "${src}"
|
|
fi
|
|
rm -f "${src}"
|
|
printf "%s" "${dst}"
|
|
}
|
|
|
|
PG_ENCRYPTED="$(encrypt "${PG_DUMP_FILE}")"
|
|
SECRETS_ENCRYPTED="$(encrypt "${SECRETS_MANIFEST}")"
|
|
if [[ -n "${MEDIA_TAR:-}" && -f "${MEDIA_TAR}" ]]; then
|
|
MEDIA_ENCRYPTED="$(encrypt "${MEDIA_TAR}")"
|
|
else
|
|
MEDIA_ENCRYPTED=""
|
|
fi
|
|
|
|
# -----------------------------------------------------------------------------
|
|
# Step 5: upload to S3
|
|
# -----------------------------------------------------------------------------
|
|
export AWS_ACCESS_KEY_ID="${AUTHENTIK_S3_BACKUP_ACCESS_KEY}"
|
|
export AWS_SECRET_ACCESS_KEY="${AUTHENTIK_S3_BACKUP_SECRET_KEY}"
|
|
export AWS_EC2_METADATA_DISABLED=true
|
|
|
|
S3_PREFIX="s3://${AUTHENTIK_S3_BACKUP_BUCKET}/authentik/${TODAY}"
|
|
|
|
log "uploading to ${S3_PREFIX}/"
|
|
aws s3 cp --endpoint-url "${AUTHENTIK_S3_BACKUP_ENDPOINT}" "${PG_ENCRYPTED}" "${S3_PREFIX}/$(basename "${PG_ENCRYPTED}")"
|
|
aws s3 cp --endpoint-url "${AUTHENTIK_S3_BACKUP_ENDPOINT}" "${SECRETS_ENCRYPTED}" "${S3_PREFIX}/$(basename "${SECRETS_ENCRYPTED}")"
|
|
if [[ -n "${MEDIA_ENCRYPTED}" ]]; then
|
|
aws s3 cp --endpoint-url "${AUTHENTIK_S3_BACKUP_ENDPOINT}" "${MEDIA_ENCRYPTED}" "${S3_PREFIX}/$(basename "${MEDIA_ENCRYPTED}")"
|
|
fi
|
|
|
|
log "authentik backup complete for ${TODAY}"
|