generated from coulomb/repo-seed
feat(sso-mfa): Phase 0a bootstrap tooling (NK-WP-0001-T01)
- sso-mfa/bootstrap/gen-secrets.sh: generates all pre-cluster secrets (PI_SECRET_KEY, PI_PEPPER, DB passwords, Keycloak admin, break-glass) into a structured secrets/ directory; prints summary with truncated values. PI_ENCFILE deferred — must be generated inside the privacyIDEA container. - sso-mfa/bootstrap/pack-bundle.sh: age-encrypts the secrets directory into an offsite ops bundle. - sso-mfa/bootstrap/README.md: KeePassXC group/entry structure, full workflow (generate → KeePassXC → bundle → shred → PI_ENCFILE post-deploy). - .gitignore: add sso-mfa/bootstrap/secrets/, *.age, *.kdbx. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
5
.gitignore
vendored
5
.gitignore
vendored
@@ -1,3 +1,8 @@
|
|||||||
|
# ── Secrets (never commit) ─────────────────────────────────────────────────────
|
||||||
|
sso-mfa/bootstrap/secrets/
|
||||||
|
*.age
|
||||||
|
*.kdbx
|
||||||
|
|
||||||
# ---> Python
|
# ---> Python
|
||||||
# Byte-compiled / optimized / DLL files
|
# Byte-compiled / optimized / DLL files
|
||||||
__pycache__/
|
__pycache__/
|
||||||
|
|||||||
117
sso-mfa/bootstrap/README.md
Normal file
117
sso-mfa/bootstrap/README.md
Normal file
@@ -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-pod-name> -- pi-manage create_enckey
|
||||||
|
|
||||||
|
# Extract it:
|
||||||
|
kubectl cp -n mfa <pi-pod-name>:/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
|
||||||
134
sso-mfa/bootstrap/gen-secrets.sh
Executable file
134
sso-mfa/bootstrap/gen-secrets.sh
Executable file
@@ -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-pod> -- 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" <<EOF
|
||||||
|
# privacyIDEA secrets — KeePassXC group: net-kingdom/privacyIDEA
|
||||||
|
# Entry: pi-admin → username=pi-admin password=PI_ADMIN_PASSWORD
|
||||||
|
# Entry: database → username=privacyidea password=PI_DB_PASSWORD
|
||||||
|
# Entry: SECRET_KEY → password=PI_SECRET_KEY (no username)
|
||||||
|
# Entry: PI_PEPPER → password=PI_PEPPER (no username)
|
||||||
|
# Entry: PI_ENCFILE → binary attachment (generate AFTER container deploy)
|
||||||
|
|
||||||
|
PI_SECRET_KEY=$PI_SECRET_KEY
|
||||||
|
PI_PEPPER=$PI_PEPPER
|
||||||
|
PI_DB_PASSWORD=$PI_DB_PASS
|
||||||
|
PI_ADMIN_PASSWORD=$PI_ADMIN_PASS
|
||||||
|
|
||||||
|
# PI_ENCFILE: generate with:
|
||||||
|
# kubectl exec -n mfa <pi-pod> -- pi-manage create_enckey
|
||||||
|
# kubectl cp -n mfa <pi-pod>:/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" <<EOF
|
||||||
|
# PostgreSQL secrets — KeePassXC group: net-kingdom/PostgreSQL
|
||||||
|
# Entry: postgres root → username=postgres password=PG_ROOT_PASSWORD
|
||||||
|
# Entry: keycloak user → username=keycloak password=PG_KEYCLOAK_PASSWORD
|
||||||
|
# Entry: privacyidea user → username=privacyidea password=PI_DB_PASSWORD (copy from privacyIDEA entry)
|
||||||
|
|
||||||
|
PG_ROOT_PASSWORD=$PG_ROOT_PASS
|
||||||
|
PG_KEYCLOAK_PASSWORD=$PG_KC_PASS
|
||||||
|
# PI_DB_PASSWORD is in privacyidea/secrets.env — do NOT copy here; link in KeePassXC
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# ── Keycloak ───────────────────────────────────────────────────────────────────
|
||||||
|
KC_ADMIN_PASS="$(rnd_b64 32 40)"
|
||||||
|
# Keycloak DB password == PG_KEYCLOAK_PASSWORD (single source of truth)
|
||||||
|
|
||||||
|
cat > "$OUT_DIR/keycloak/secrets.env" <<EOF
|
||||||
|
# Keycloak secrets — KeePassXC group: net-kingdom/Keycloak
|
||||||
|
# Entry: admin → username=admin password=KC_ADMIN_PASSWORD
|
||||||
|
# Entry: database → username=keycloak password=KC_DB_PASSWORD (copy from postgres/keycloak user)
|
||||||
|
|
||||||
|
KC_ADMIN_PASSWORD=$KC_ADMIN_PASS
|
||||||
|
KC_DB_PASSWORD=$PG_KC_PASS
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# ── Break-glass ────────────────────────────────────────────────────────────────
|
||||||
|
BG_PASS="$(rnd_b64 32 40)"
|
||||||
|
|
||||||
|
cat > "$OUT_DIR/breakglass/secrets.env" <<EOF
|
||||||
|
# Break-glass secrets — KeePassXC group: net-kingdom/Break-glass
|
||||||
|
# Entry: break-glass → username=break-glass password=BREAKGLASS_PASSWORD
|
||||||
|
# Entry: recovery-otp → TOTP seed (enroll manually after Keycloak is up)
|
||||||
|
|
||||||
|
BREAKGLASS_PASSWORD=$BG_PASS
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# ── Summary ────────────────────────────────────────────────────────────────────
|
||||||
|
echo "=== net-kingdom SSO/MFA bootstrap secrets ==="
|
||||||
|
echo "Generated: $(date -Iseconds)"
|
||||||
|
echo "Output : $OUT_DIR/"
|
||||||
|
echo ""
|
||||||
|
echo " privacyIDEA:"
|
||||||
|
echo " PI_SECRET_KEY : $(wc -c < "$OUT_DIR/privacyidea/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."
|
||||||
51
sso-mfa/bootstrap/pack-bundle.sh
Executable file
51
sso-mfa/bootstrap/pack-bundle.sh
Executable file
@@ -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-<timestamp>.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 <private-key-file> -o secrets.tar $OUTPUT && tar xf secrets.tar"
|
||||||
Reference in New Issue
Block a user