diff --git a/.gitignore b/.gitignore index 36b13f1..8ba0b2d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,8 @@ +# ── Secrets (never commit) ───────────────────────────────────────────────────── +sso-mfa/bootstrap/secrets/ +*.age +*.kdbx + # ---> Python # Byte-compiled / optimized / DLL files __pycache__/ diff --git a/sso-mfa/bootstrap/README.md b/sso-mfa/bootstrap/README.md new file mode 100644 index 0000000..ed2e9e2 --- /dev/null +++ b/sso-mfa/bootstrap/README.md @@ -0,0 +1,117 @@ +# Phase 0a — Pre-cluster Secret Bootstrap + +This directory contains tooling for the KeePassXC bootstrap phase (T01 Phase 0a). +No secrets are stored here — only scripts and documentation. + +## Why KeePassXC first? + +The single-credential bootstrap principle (Decision D1): one master password +unlocks the KeePassXC vault; all other credentials are generated inside it. +KeePassXC is the pre-cluster source of truth. After the K3s cluster is running, +secrets migrate into HashiCorp Vault (T01 Phase 0b) and KeePassXC becomes the +break-glass / dev-local backup. + +## KeePassXC database structure + +Create a new `.kdbx` database. Use a strong master password stored in your +personal password manager (Bitwarden, 1Password, etc.). + +Recommended group structure: + +``` +net-kingdom/ +├── privacyIDEA/ +│ ├── pi-admin (username: pi-admin, password: PI_ADMIN_PASSWORD) +│ ├── database (username: privacyidea, password: PI_DB_PASSWORD) +│ ├── SECRET_KEY (password field only — PI_SECRET_KEY value) +│ ├── PI_PEPPER (password field only — PI_PEPPER value) +│ └── PI_ENCFILE (binary attachment: pi.enc — generate after deploy) +├── PostgreSQL/ +│ ├── postgres root (username: postgres, password: PG_ROOT_PASSWORD) +│ ├── keycloak user (username: keycloak, password: PG_KEYCLOAK_PASSWORD) +│ └── privacyidea user (username: privacyidea — same password as PI_DB_PASSWORD) +├── Keycloak/ +│ ├── admin (username: admin, password: KC_ADMIN_PASSWORD) +│ └── database (username: keycloak — same password as PG_KEYCLOAK_PASSWORD) +├── Break-glass/ +│ ├── break-glass (username: break-glass, password: BREAKGLASS_PASSWORD) +│ └── recovery-otp (TOTP seed — enroll manually after Keycloak is up) +└── Vault/ + └── (populated in Phase 0b after Vault is deployed) +``` + +## Workflow + +### Step 1 — Generate secrets + +```bash +chmod +x gen-secrets.sh +./gen-secrets.sh ./secrets +``` + +This writes `.env` files to `./secrets/` (gitignored). Inspect each file, +then copy each value into the appropriate KeePassXC entry. + +### Step 2 — Create the age encryption key (one-time) + +```bash +age-keygen -o ~/net-kingdom-ops-bundle.key +``` + +The public key prints to stdout. The private key is in the `.key` file. +Store the `.key` file somewhere safe (NOT in this repo; NOT in the secrets/ dir). + +### Step 3 — Create the encrypted ops bundle + +```bash +chmod +x pack-bundle.sh +./pack-bundle.sh ./secrets "age1..." ops-bundle.tar.age +``` + +Store `ops-bundle.tar.age` offsite (cloud storage, external drive, separate location). + +### Step 4 — Shred the generated files + +```bash +find ./secrets -type f -exec shred -u {} \; +rm -rf ./secrets +``` + +### Step 5 — PI_ENCFILE (after privacyIDEA container is running — T04) + +```bash +# Generate the encryption key inside the running container: +kubectl exec -n mfa -- pi-manage create_enckey + +# Extract it: +kubectl cp -n mfa :/etc/privacyidea/enckey ./pi.enc + +# Store as a binary attachment in KeePassXC → net-kingdom/privacyIDEA/PI_ENCFILE + +# Create the K8s Secret: +kubectl create secret generic privacyidea-enckey \ + --from-file=PI_ENCFILE=./pi.enc \ + --namespace mfa + +# Shred the local copy: +shred -u ./pi.enc +``` + +## Notes on secret reuse + +Some secrets appear in multiple components — this is intentional to avoid +drift. When adding to KeePassXC, note the cross-references rather than +duplicating the value: + +- `PI_DB_PASSWORD` == PostgreSQL `privacyidea` user password +- `KC_DB_PASSWORD` == PostgreSQL `keycloak` user password + +Use KeePassXC references (`{REF:P@T:UUID}`) to avoid maintaining two copies. + +## Phase 0b — HashiCorp Vault (after T02, once K3s is running) + +See `../vault/` (created in T01 Phase 0b) for: +- Vault Helm chart values +- ESO (External Secrets Operator) configuration +- Vault secret path layout +- Migration procedure: KeePassXC → Vault diff --git a/sso-mfa/bootstrap/gen-secrets.sh b/sso-mfa/bootstrap/gen-secrets.sh new file mode 100755 index 0000000..a3c3f1d --- /dev/null +++ b/sso-mfa/bootstrap/gen-secrets.sh @@ -0,0 +1,134 @@ +#!/usr/bin/env bash +# gen-secrets.sh — generate all bootstrap secrets for the net-kingdom SSO/MFA platform +# +# Usage: +# ./gen-secrets.sh [OUTPUT_DIR] +# +# Generates all pre-cluster secrets and writes them to OUTPUT_DIR (default: ./secrets). +# Output files are structured by component to mirror the KeePassXC entry layout. +# +# WARNING: The secrets/ directory must NEVER be committed to git. +# After entering values into KeePassXC, shred the generated files: +# find secrets/ -type f -exec shred -u {} \; +# +# PI_ENCFILE is NOT generated here — it must be produced inside the privacyIDEA +# container after deployment: +# kubectl exec -n mfa -- pi-manage create_enckey +# Extract the resulting file, store it in KeePassXC as a binary attachment on the +# privacyIDEA/PI_ENCFILE entry, then create the k8s secret from it. + +set -euo pipefail + +OUT_DIR="${1:-./secrets}" + +if [[ -e "$OUT_DIR" ]]; then + echo "ERROR: $OUT_DIR already exists. Delete it first or choose a different path." >&2 + exit 1 +fi + +# Helpers +rnd_hex() { openssl rand -hex "$1"; } +rnd_b64() { openssl rand -base64 "$1" | tr -d '\n/+=' | head -c "$2"; } + +mkdir -p \ + "$OUT_DIR/privacyidea" \ + "$OUT_DIR/postgres" \ + "$OUT_DIR/keycloak" \ + "$OUT_DIR/breakglass" + +# ── privacyIDEA ──────────────────────────────────────────────────────────────── +PI_SECRET_KEY="$(rnd_hex 32)" # 64 hex chars — Flask/PI app secret +PI_PEPPER="$(rnd_hex 16)" # 32 hex chars — password hashing pepper +PI_DB_PASS="$(rnd_b64 32 40)" # 40 printable chars — DB password +PI_ADMIN_PASS="$(rnd_b64 32 40)" + +cat > "$OUT_DIR/privacyidea/secrets.env" < -- pi-manage create_enckey +# kubectl cp -n mfa :/etc/privacyidea/enckey ./secrets/privacyidea/pi.enc +# Then store pi.enc as a binary attachment in KeePassXC → net-kingdom/privacyIDEA/PI_ENCFILE +EOF + +# ── PostgreSQL ───────────────────────────────────────────────────────────────── +PG_ROOT_PASS="$(rnd_b64 32 40)" +PG_KC_PASS="$(rnd_b64 32 40)" +# privacyIDEA DB user reuses PI_DB_PASS (single source of truth) + +cat > "$OUT_DIR/postgres/secrets.env" < "$OUT_DIR/keycloak/secrets.env" < "$OUT_DIR/breakglass/secrets.env" < /dev/null; echo "${PI_SECRET_KEY:0:16}…")" +echo " PI_PEPPER : ${PI_PEPPER:0:8}…" +echo " PI_DB_PASSWORD : ${PI_DB_PASS:0:8}…" +echo " PI_ADMIN_PASSWORD: ${PI_ADMIN_PASS:0:8}…" +echo " PI_ENCFILE : *** generate after container deploy (see comments) ***" +echo "" +echo " PostgreSQL:" +echo " PG_ROOT_PASSWORD : ${PG_ROOT_PASS:0:8}…" +echo " PG_KEYCLOAK_PASSWORD: ${PG_KC_PASS:0:8}…" +echo " PG_PI_PASSWORD : (same as PI_DB_PASSWORD)" +echo "" +echo " Keycloak:" +echo " KC_ADMIN_PASSWORD: ${KC_ADMIN_PASS:0:8}…" +echo " KC_DB_PASSWORD : (same as PG_KEYCLOAK_PASSWORD)" +echo "" +echo " Break-glass:" +echo " BREAKGLASS_PASSWORD: ${BG_PASS:0:8}…" +echo "" +echo "Next steps:" +echo " 1. Enter each value into KeePassXC (see comments in each secrets.env file)." +echo " 2. Run pack-bundle.sh to create an age-encrypted offsite backup." +echo " 3. Shred the generated files:" +echo " find $OUT_DIR -type f -exec shred -u {} \\;" +echo "" +echo "NEVER commit $OUT_DIR/ to git." diff --git a/sso-mfa/bootstrap/pack-bundle.sh b/sso-mfa/bootstrap/pack-bundle.sh new file mode 100755 index 0000000..e935555 --- /dev/null +++ b/sso-mfa/bootstrap/pack-bundle.sh @@ -0,0 +1,51 @@ +#!/usr/bin/env bash +# pack-bundle.sh — create an age-encrypted ops bundle from the secrets directory +# +# Usage: +# ./pack-bundle.sh SECRETS_DIR RECIPIENT_PUBKEY [OUTPUT_FILE] +# +# RECIPIENT_PUBKEY is the age public key to encrypt to. +# Generate one with: age-keygen -o ops-bundle.key (store key safely) +# The public key appears on stdout and in the .key file header. +# +# OUTPUT_FILE defaults to ops-bundle-.tar.age in the current directory. +# +# Example: +# age-keygen -o ~/ops-bundle.key +# ./pack-bundle.sh ./secrets "age1ql3z7hjy54pw3hyww5ayyfg7zqgvc7w3j2elw8zmrj2kg5sfn9aqmcac8p" + +set -euo pipefail + +if [[ $# -lt 2 ]]; then + echo "Usage: $0 SECRETS_DIR RECIPIENT_PUBKEY [OUTPUT_FILE]" >&2 + exit 1 +fi + +SECRETS_DIR="${1%/}" +RECIPIENT="$2" +TIMESTAMP="$(date +%Y%m%dT%H%M%S)" +OUTPUT="${3:-ops-bundle-${TIMESTAMP}.tar.age}" + +if [[ ! -d "$SECRETS_DIR" ]]; then + echo "ERROR: $SECRETS_DIR is not a directory." >&2 + exit 1 +fi + +if ! command -v age &>/dev/null; then + echo "ERROR: age is not installed. Install with: apt install age" >&2 + exit 1 +fi + +TMPTAR="$(mktemp --suffix=.tar)" +trap 'shred -u "$TMPTAR" 2>/dev/null || rm -f "$TMPTAR"' EXIT + +echo "Packing: $SECRETS_DIR → (tar) → (age encrypt) → $OUTPUT" +tar -C "$(dirname "$SECRETS_DIR")" -cf "$TMPTAR" "$(basename "$SECRETS_DIR")" +age -r "$RECIPIENT" -o "$OUTPUT" "$TMPTAR" + +echo "Bundle written: $OUTPUT" +echo "Recipient key : $RECIPIENT" +echo "" +echo "Store $OUTPUT offsite (cloud, external drive, second location)." +echo "Decrypt with:" +echo " age -d -i -o secrets.tar $OUTPUT && tar xf secrets.tar"