Files
HiveGrid-Wireframes/hivegrid-cloud/infra/authentik/backup/backup.sh
T

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}"