generated from coulomb/repo-seed
feat(sso-mfa): T02/T03 live apply — age-encrypted secrets, CNPG cluster (NK-WP-0001-T02/T03)
- Add encrypt-secrets.sh / decrypt-secrets.sh: age-based secrets workflow replaces KeePassXC dependency; encrypted .env.age files committed to repo - Add bootstrap/secrets.enc/: all component secrets encrypted to age pubkey - Fix .gitignore: allow secrets.enc/**/*.age while blocking plaintext - Fix verify-t02.sh: update netpol names for Authelia+LLDAP+KeyCape stack - Fix verify-t03.sh: remove keycloak_db/role checks; fix ((PASS++)) set-e bug - Update postgresql/cluster.yaml: drop keycloak_db, bootstrap privacyidea_db only - Update postgresql/create-secrets.sh: remove keycloak secret - Fix netpol-databases.yaml: add port 8000 for CNPG instance manager HTTP API - T02 COMPLETE: namespaces, network policies, cert-manager issuers applied - T03 COMPLETE: CNPG operator installed, net-kingdom-pg cluster healthy Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1,6 +1,7 @@
|
||||
# ── Secrets (never commit) ─────────────────────────────────────────────────────
|
||||
sso-mfa/bootstrap/secrets/
|
||||
*.age
|
||||
!sso-mfa/bootstrap/secrets.enc/**/*.age
|
||||
*.kdbx
|
||||
|
||||
# ---> Python
|
||||
|
||||
62
sso-mfa/bootstrap/decrypt-secrets.sh
Executable file
62
sso-mfa/bootstrap/decrypt-secrets.sh
Executable file
@@ -0,0 +1,62 @@
|
||||
#!/usr/bin/env bash
|
||||
# decrypt-secrets.sh — decrypt secrets.enc/ to secrets/ using age
|
||||
#
|
||||
# Usage:
|
||||
# ./decrypt-secrets.sh [OUTPUT_DIR] [AGE_KEY_FILE]
|
||||
#
|
||||
# OUTPUT_DIR where to write plaintext secrets (default: ./secrets)
|
||||
# AGE_KEY_FILE age private key file (default: ~/.config/net-kingdom/age.key)
|
||||
#
|
||||
# Decrypts all *.age files in secrets.enc/ to OUTPUT_DIR for use by
|
||||
# create-secrets.sh scripts. Shred OUTPUT_DIR when done:
|
||||
# find secrets/ -type f -exec shred -u {} \; && rm -rf secrets/
|
||||
#
|
||||
# The age key must be present on the machine. Keep it outside the repo:
|
||||
# ~/.config/net-kingdom/age.key
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
OUTPUT_DIR="${1:-./secrets}"
|
||||
AGE_KEY="${2:-$HOME/.config/net-kingdom/age.key}"
|
||||
|
||||
ENC_DIR="$(dirname "$OUTPUT_DIR")/secrets.enc"
|
||||
|
||||
if [[ ! -d "$ENC_DIR" ]]; then
|
||||
echo "ERROR: encrypted secrets directory not found: $ENC_DIR" >&2
|
||||
echo "Expected secrets.enc/ next to the output directory." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ ! -f "$AGE_KEY" ]]; then
|
||||
echo "ERROR: age key not found: $AGE_KEY" >&2
|
||||
echo "Copy your age key to $AGE_KEY or pass the path as the second argument." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ -e "$OUTPUT_DIR" ]]; then
|
||||
echo "ERROR: $OUTPUT_DIR already exists. Remove it first or choose a different path." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Decrypting $ENC_DIR → $OUTPUT_DIR/"
|
||||
echo ""
|
||||
|
||||
count=0
|
||||
for component_dir in "$ENC_DIR"/*/; do
|
||||
component=$(basename "$component_dir")
|
||||
mkdir -p "$OUTPUT_DIR/$component"
|
||||
for f in "$component_dir"*.age; do
|
||||
[[ -f "$f" ]] || continue
|
||||
fname=$(basename "${f%.age}")
|
||||
out="$OUTPUT_DIR/$component/$fname"
|
||||
age -d -i "$AGE_KEY" -o "$out" "$f"
|
||||
echo " decrypted: secrets.enc/$component/$(basename "$f") → $component/$fname"
|
||||
count=$((count + 1))
|
||||
done
|
||||
done
|
||||
|
||||
echo ""
|
||||
echo "$count file(s) decrypted to $OUTPUT_DIR/"
|
||||
echo ""
|
||||
echo "Use create-secrets.sh scripts, then shred:"
|
||||
echo " find $OUTPUT_DIR -type f -exec shred -u {} \\; && rm -rf $OUTPUT_DIR"
|
||||
79
sso-mfa/bootstrap/encrypt-secrets.sh
Executable file
79
sso-mfa/bootstrap/encrypt-secrets.sh
Executable file
@@ -0,0 +1,79 @@
|
||||
#!/usr/bin/env bash
|
||||
# encrypt-secrets.sh — encrypt secrets/ directory to secrets.enc/ using age
|
||||
#
|
||||
# Usage:
|
||||
# ./encrypt-secrets.sh [SECRETS_DIR] [AGE_KEY_FILE]
|
||||
#
|
||||
# SECRETS_DIR plaintext secrets directory (default: ./secrets)
|
||||
# AGE_KEY_FILE age private key file (default: ~/.config/net-kingdom/age.key)
|
||||
#
|
||||
# Reads the public key from the age key file and encrypts each *.env file
|
||||
# (and pi.enc if present) to secrets.enc/<component>/<filename>.age.
|
||||
#
|
||||
# After a successful encrypt, shreds the plaintext secrets directory unless
|
||||
# --no-shred is passed.
|
||||
#
|
||||
# Run after gen-secrets.sh to store secrets safely in the repo.
|
||||
# Commit secrets.enc/ to git.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SECRETS_DIR="${1:-./secrets}"
|
||||
AGE_KEY="${2:-$HOME/.config/net-kingdom/age.key}"
|
||||
NO_SHRED=false
|
||||
for arg in "$@"; do [[ "$arg" == "--no-shred" ]] && NO_SHRED=true; done
|
||||
|
||||
if [[ ! -d "$SECRETS_DIR" ]]; then
|
||||
echo "ERROR: secrets directory not found: $SECRETS_DIR" >&2
|
||||
echo "Run gen-secrets.sh first." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ ! -f "$AGE_KEY" ]]; then
|
||||
echo "ERROR: age key not found: $AGE_KEY" >&2
|
||||
echo "Generate with: age-keygen -o $AGE_KEY" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Extract public key from the private key file
|
||||
PUBKEY=$(grep 'public key:' "$AGE_KEY" | awk '{print $NF}')
|
||||
if [[ -z "$PUBKEY" ]]; then
|
||||
echo "ERROR: could not read public key from $AGE_KEY" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
ENC_DIR="$(dirname "$SECRETS_DIR")/secrets.enc"
|
||||
mkdir -p "$ENC_DIR"
|
||||
|
||||
echo "Encrypting secrets → $ENC_DIR/"
|
||||
echo "Recipient: $PUBKEY"
|
||||
echo ""
|
||||
|
||||
count=0
|
||||
for component_dir in "$SECRETS_DIR"/*/; do
|
||||
component=$(basename "$component_dir")
|
||||
mkdir -p "$ENC_DIR/$component"
|
||||
for f in "$component_dir"*; do
|
||||
[[ -f "$f" ]] || continue
|
||||
fname=$(basename "$f")
|
||||
out="$ENC_DIR/$component/$fname.age"
|
||||
age -r "$PUBKEY" -o "$out" "$f"
|
||||
echo " encrypted: $component/$fname → secrets.enc/$component/$fname.age"
|
||||
count=$((count + 1))
|
||||
done
|
||||
done
|
||||
|
||||
echo ""
|
||||
echo "$count file(s) encrypted to $ENC_DIR/"
|
||||
echo ""
|
||||
|
||||
if [[ "$NO_SHRED" == false ]]; then
|
||||
echo "Shredding plaintext secrets..."
|
||||
find "$SECRETS_DIR" -type f -exec shred -u {} \;
|
||||
rm -rf "$SECRETS_DIR"
|
||||
echo "Done. Plaintext secrets shredded."
|
||||
else
|
||||
echo "(--no-shred: plaintext kept at $SECRETS_DIR)"
|
||||
fi
|
||||
echo ""
|
||||
echo "Next: commit secrets.enc/ to git."
|
||||
BIN
sso-mfa/bootstrap/secrets.enc/authelia/secrets.env.age
Normal file
BIN
sso-mfa/bootstrap/secrets.enc/authelia/secrets.env.age
Normal file
Binary file not shown.
BIN
sso-mfa/bootstrap/secrets.enc/breakglass/secrets.env.age
Normal file
BIN
sso-mfa/bootstrap/secrets.enc/breakglass/secrets.env.age
Normal file
Binary file not shown.
BIN
sso-mfa/bootstrap/secrets.enc/keycape/secrets.env.age
Normal file
BIN
sso-mfa/bootstrap/secrets.enc/keycape/secrets.env.age
Normal file
Binary file not shown.
7
sso-mfa/bootstrap/secrets.enc/lldap/secrets.env.age
Normal file
7
sso-mfa/bootstrap/secrets.enc/lldap/secrets.env.age
Normal file
@@ -0,0 +1,7 @@
|
||||
age-encryption.org/v1
|
||||
-> X25519 yR2D3J78/vw1ohcdXCLy5IOoIuG+FtRs7Eiswk3gKyo
|
||||
c9axBYTsFS4Gqb3Zdv5Gtk+/yEtKNH21iFLU1U3mxNs
|
||||
--- Kc/0n9icRSyEEcAHJJdx2Vcv5CgjLucU8FdZArV3C2U
|
||||
ìÏ9ÍôeY<EFBFBD>œ·ŒdT-GÄëÊiΑ%½0xžày=„0úOñî—Ö«ü豃־Qÿ"ú-[gß‹ÁóÐ3eýœV3”<33>wt1½º<>“Cä$rj2\zû=IW ï7>=ŽKü<4B>ª8JUT¡G†læ"bv{g3@þ-¡â:Ƚ™2£;ÖÍPrÕUH<55>Aö-Æë<C386>°ZØÌx¦„«.ïÑx}@EMž“+©ÚHÐ
|
||||
€Óš´$¤Î;”¤<ɶ>iûáÕe˜ò1xtCÌU¡4¹àÜÒ‚‘O®¦zÃ
|
||||
Žý<EFBFBD>O{qãÔ<C3A3>qE¬Ù¡?àS<ÂsµÎg©XL<58>¬ÎÂþy«í'‚¶Ùñ«f[넪Òü6<C3BC>°W£@C{‡¢#ö<>xÐñƒÅ9<C385>÷Τ%ò2³~ªyQ™(–¥c ;¿ìùÄ͆’«#l`}uNÖ»Ž
|
||||
BIN
sso-mfa/bootstrap/secrets.enc/postgres/secrets.env.age
Normal file
BIN
sso-mfa/bootstrap/secrets.enc/postgres/secrets.env.age
Normal file
Binary file not shown.
BIN
sso-mfa/bootstrap/secrets.enc/privacyidea/secrets.env.age
Normal file
BIN
sso-mfa/bootstrap/secrets.enc/privacyidea/secrets.env.age
Normal file
Binary file not shown.
@@ -93,6 +93,8 @@ spec:
|
||||
ports:
|
||||
- port: 5432
|
||||
protocol: TCP
|
||||
- port: 8000 # CloudNativePG instance manager HTTP API (used for status extraction)
|
||||
protocol: TCP
|
||||
- port: 9187 # CloudNativePG metrics exporter
|
||||
protocol: TCP
|
||||
---
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
# CloudNativePG Cluster — net-kingdom-pg
|
||||
#
|
||||
# Creates a PostgreSQL 16 cluster with two application databases:
|
||||
# keycloak_db (owner: keycloak)
|
||||
# Creates a PostgreSQL 16 cluster with one application database:
|
||||
# privacyidea_db (owner: privacyidea)
|
||||
#
|
||||
# Note: keycloak_db removed — Keycloak replaced by Authelia+LLDAP+KeyCape (T05).
|
||||
#
|
||||
# Prerequisites:
|
||||
# - CloudNativePG operator installed (see README.md)
|
||||
# - K8s Secrets created (see create-secrets.sh)
|
||||
@@ -27,33 +28,19 @@ spec:
|
||||
imageName: ghcr.io/cloudnative-pg/postgresql:16
|
||||
|
||||
# ── Bootstrap ────────────────────────────────────────────────────────────────
|
||||
# Creates keycloak_db with owner keycloak. privacyidea_db and the
|
||||
# privacyidea role are created in postInitSQL (runs as superuser).
|
||||
# managed.roles below reconciles passwords for both users continuously.
|
||||
# Creates privacyidea_db with owner privacyidea.
|
||||
# managed.roles below reconciles the password continuously from K8s Secret.
|
||||
bootstrap:
|
||||
initdb:
|
||||
database: keycloak_db
|
||||
owner: keycloak
|
||||
database: privacyidea_db
|
||||
owner: privacyidea
|
||||
secret:
|
||||
name: net-kingdom-pg-keycloak-app
|
||||
postInitSQL:
|
||||
- "CREATE ROLE privacyidea WITH LOGIN;"
|
||||
- "CREATE DATABASE privacyidea_db OWNER privacyidea;"
|
||||
- "REVOKE CONNECT ON DATABASE privacyidea_db FROM PUBLIC;"
|
||||
- "REVOKE CONNECT ON DATABASE keycloak_db FROM PUBLIC;"
|
||||
- "GRANT CONNECT ON DATABASE keycloak_db TO keycloak;"
|
||||
- "GRANT CONNECT ON DATABASE privacyidea_db TO privacyidea;"
|
||||
name: net-kingdom-pg-privacyidea-app
|
||||
|
||||
# ── Managed roles ────────────────────────────────────────────────────────────
|
||||
# Operator reconciles these passwords continuously from K8s Secrets.
|
||||
# This ensures password rotation in KeePassXC/Vault propagates to PG.
|
||||
# Operator reconciles the password continuously from K8s Secret.
|
||||
managed:
|
||||
roles:
|
||||
- name: keycloak
|
||||
ensure: present
|
||||
login: true
|
||||
passwordSecret:
|
||||
name: net-kingdom-pg-keycloak-app
|
||||
- name: privacyidea
|
||||
ensure: present
|
||||
login: true
|
||||
|
||||
@@ -7,10 +7,11 @@
|
||||
# <secrets-dir> is the output directory produced by sso-mfa/bootstrap/gen-secrets.sh
|
||||
# (default: ../../bootstrap/secrets).
|
||||
#
|
||||
# Creates two K8s Secrets in the databases namespace:
|
||||
# net-kingdom-pg-keycloak-app — keycloak DB credentials
|
||||
# Creates one K8s Secret in the databases namespace:
|
||||
# net-kingdom-pg-privacyidea-app — privacyIDEA DB credentials
|
||||
#
|
||||
# Note: net-kingdom-pg-keycloak-app removed — Keycloak replaced by Authelia+LLDAP+KeyCape (T05).
|
||||
#
|
||||
# These secrets must exist before applying cluster.yaml.
|
||||
# Re-run this script whenever you rotate passwords in KeePassXC / gen-secrets.sh.
|
||||
|
||||
@@ -24,36 +25,23 @@ if [[ ! -d "$SECRETS_DIR" ]]; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
PG_SECRETS="$SECRETS_DIR/postgres/secrets.env"
|
||||
PI_SECRETS="$SECRETS_DIR/privacyidea/secrets.env"
|
||||
|
||||
if [[ ! -f "$PG_SECRETS" ]]; then
|
||||
echo "ERROR: $PG_SECRETS not found" >&2
|
||||
exit 1
|
||||
fi
|
||||
if [[ ! -f "$PI_SECRETS" ]]; then
|
||||
echo "ERROR: $PI_SECRETS not found" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Source the generated env files (they contain KEY=VALUE pairs, no export)
|
||||
# Source the generated env file (KEY=VALUE pairs, no export)
|
||||
# Use a subshell to avoid polluting the current environment.
|
||||
PG_KC_PASS=$(bash -c "source $PG_SECRETS 2>/dev/null; echo \$PG_KEYCLOAK_PASSWORD")
|
||||
PI_DB_PASS=$(bash -c "source $PI_SECRETS 2>/dev/null; echo \$PI_DB_PASSWORD")
|
||||
|
||||
if [[ -z "$PG_KC_PASS" || -z "$PI_DB_PASS" ]]; then
|
||||
echo "ERROR: could not read passwords from secrets files." >&2
|
||||
echo "Check that gen-secrets.sh ran successfully and the files are intact." >&2
|
||||
if [[ -z "$PI_DB_PASS" ]]; then
|
||||
echo "ERROR: could not read PI_DB_PASSWORD from $PI_SECRETS" >&2
|
||||
echo "Check that gen-secrets.sh ran successfully and the file is intact." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Creating K8s Secret: net-kingdom-pg-keycloak-app"
|
||||
kubectl create secret generic net-kingdom-pg-keycloak-app \
|
||||
--namespace=databases \
|
||||
--from-literal=username=keycloak \
|
||||
--from-literal=password="$PG_KC_PASS" \
|
||||
--dry-run=client -o yaml | kubectl apply -f -
|
||||
|
||||
echo "Creating K8s Secret: net-kingdom-pg-privacyidea-app"
|
||||
kubectl create secret generic net-kingdom-pg-privacyidea-app \
|
||||
--namespace=databases \
|
||||
@@ -62,8 +50,8 @@ kubectl create secret generic net-kingdom-pg-privacyidea-app \
|
||||
--dry-run=client -o yaml | kubectl apply -f -
|
||||
|
||||
echo ""
|
||||
echo "Done. Secrets created in namespace: databases"
|
||||
echo "Done. Secret created in namespace: databases"
|
||||
echo ""
|
||||
echo "Verify:"
|
||||
echo " kubectl get secrets -n databases"
|
||||
echo " kubectl describe secret net-kingdom-pg-keycloak-app -n databases"
|
||||
echo " kubectl describe secret net-kingdom-pg-privacyidea-app -n databases"
|
||||
|
||||
@@ -62,9 +62,8 @@ for ns in sso mfa databases; do
|
||||
check "allow-egress-dns in $ns" \
|
||||
$KUBECTL get networkpolicy allow-egress-dns -n "$ns"
|
||||
done
|
||||
check "allow-ingress-from-traefik in sso" $KUBECTL get networkpolicy allow-ingress-from-traefik -n sso
|
||||
check "allow-egress-to-postgres in sso" $KUBECTL get networkpolicy allow-egress-to-postgres -n sso
|
||||
check "allow-egress-to-privacyidea in sso" $KUBECTL get networkpolicy allow-egress-to-privacyidea -n sso
|
||||
check "allow-traefik-to-keycape in sso" $KUBECTL get networkpolicy allow-traefik-to-keycape -n sso
|
||||
check "allow-keycape-egress-to-privacyidea in sso" $KUBECTL get networkpolicy allow-keycape-egress-to-privacyidea -n sso
|
||||
check "allow-ingress-from-traefik in mfa" $KUBECTL get networkpolicy allow-ingress-from-traefik -n mfa
|
||||
check "allow-ingress-from-keycloak in mfa" $KUBECTL get networkpolicy allow-ingress-from-keycloak -n mfa
|
||||
check "allow-egress-to-postgres in mfa" $KUBECTL get networkpolicy allow-egress-to-postgres -n mfa
|
||||
|
||||
@@ -4,11 +4,13 @@
|
||||
# Checks:
|
||||
# 1. CloudNativePG operator is installed and running
|
||||
# 2. Cluster net-kingdom-pg is Ready
|
||||
# 3. Both application databases exist (keycloak_db, privacyidea_db)
|
||||
# 4. Both application roles exist (keycloak, privacyidea)
|
||||
# 3. privacyidea_db database exists
|
||||
# 4. privacyidea role exists
|
||||
# 5. K8s Secrets are present in the databases namespace
|
||||
# 6. (Optional) Scheduled backup CR is present when backup is configured
|
||||
#
|
||||
# Note: keycloak_db removed — Keycloak replaced by Authelia+LLDAP+KeyCape (T05).
|
||||
#
|
||||
# Usage:
|
||||
# chmod +x verify-t03.sh
|
||||
# ./verify-t03.sh
|
||||
@@ -19,9 +21,9 @@ PASS=0
|
||||
FAIL=0
|
||||
WARN=0
|
||||
|
||||
pass() { echo " [PASS] $1"; ((PASS++)); }
|
||||
fail() { echo " [FAIL] $1"; ((FAIL++)); }
|
||||
warn() { echo " [WARN] $1"; ((WARN++)); }
|
||||
pass() { echo " [PASS] $1"; PASS=$((PASS + 1)); }
|
||||
fail() { echo " [FAIL] $1"; FAIL=$((FAIL + 1)); }
|
||||
warn() { echo " [WARN] $1"; WARN=$((WARN + 1)); }
|
||||
|
||||
section() { echo ""; echo "── $1 ──────────────────────────────────────"; }
|
||||
|
||||
@@ -76,15 +78,9 @@ section "3. Databases"
|
||||
|
||||
if [[ -n "$PRIMARY_POD" ]]; then
|
||||
DB_LIST=$(kubectl exec -n databases "$PRIMARY_POD" -- \
|
||||
psql -U postgres -tAc "SELECT datname FROM pg_database WHERE datname IN ('keycloak_db','privacyidea_db') ORDER BY datname;" \
|
||||
psql -U postgres -tAc "SELECT datname FROM pg_database WHERE datname = 'privacyidea_db';" \
|
||||
2>/dev/null || echo "")
|
||||
|
||||
if echo "$DB_LIST" | grep -q "keycloak_db"; then
|
||||
pass "keycloak_db exists"
|
||||
else
|
||||
fail "keycloak_db not found"
|
||||
fi
|
||||
|
||||
if echo "$DB_LIST" | grep -q "privacyidea_db"; then
|
||||
pass "privacyidea_db exists"
|
||||
else
|
||||
@@ -99,15 +95,9 @@ section "4. Database roles"
|
||||
|
||||
if [[ -n "$PRIMARY_POD" ]]; then
|
||||
ROLE_LIST=$(kubectl exec -n databases "$PRIMARY_POD" -- \
|
||||
psql -U postgres -tAc "SELECT rolname FROM pg_roles WHERE rolname IN ('keycloak','privacyidea') ORDER BY rolname;" \
|
||||
psql -U postgres -tAc "SELECT rolname FROM pg_roles WHERE rolname = 'privacyidea';" \
|
||||
2>/dev/null || echo "")
|
||||
|
||||
if echo "$ROLE_LIST" | grep -q "keycloak"; then
|
||||
pass "role keycloak exists"
|
||||
else
|
||||
fail "role keycloak not found"
|
||||
fi
|
||||
|
||||
if echo "$ROLE_LIST" | grep -q "privacyidea"; then
|
||||
pass "role privacyidea exists"
|
||||
else
|
||||
@@ -120,7 +110,7 @@ fi
|
||||
# ── 5. K8s Secrets ────────────────────────────────────────────────────────────
|
||||
section "5. K8s Secrets (databases namespace)"
|
||||
|
||||
for secret in net-kingdom-pg-keycloak-app net-kingdom-pg-privacyidea-app; do
|
||||
for secret in net-kingdom-pg-privacyidea-app; do
|
||||
if kubectl get secret "$secret" -n databases &>/dev/null; then
|
||||
pass "Secret $secret exists"
|
||||
else
|
||||
|
||||
Reference in New Issue
Block a user