generated from coulomb/repo-seed
feat(sso-mfa): T04 privacyIDEA manifests (NK-WP-0001-T04)
Deploy privacyIDEA (MFA core) in the mfa namespace: - pvc.yaml: privacyidea-data (5Gi) and privacyidea-logs (2Gi) - configmap.yaml: pi.cfg reading secrets from env vars - deployment.yaml: Deployment + ClusterIP Service (port 8080) - middleware.yaml: Traefik RateLimit + admin IP AllowList - ingress.yaml: pink.coulomb.social (portal + admin), pink-account.coulomb.social (self-service) - create-secrets.sh: creates privacyidea-config Secret - enckey-bootstrap.sh: post-deploy key extraction + DR Secrets - bootstrap-admin.sh: pi-admin, trigger-admin, privacyidea-trigger-admin Secret - verify-t04.sh: 8-section done-criteria checker Config points CP-NK-002 (pink.coulomb.social) and CP-NK-003 (pink-account.coulomb.social) registered in CONFIG.md. pink = PrivacyIDEA Net Knights (project mnemonic). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
38
CONFIG.md
38
CONFIG.md
@@ -21,6 +21,44 @@ If yes to any of the above, don't add it here.
|
||||
| ID | Name | Value | Location(s) |
|
||||
|----|------|-------|-------------|
|
||||
| CP-NK-001 | ACME contact email | `bernd.worsch+netkingdom@gmail.com` | `sso-mfa/k8s/cert-manager/issuers.yaml:38` |
|
||||
| CP-NK-002 | privacyIDEA portal hostname | `pink.coulomb.social` | `sso-mfa/k8s/privacyidea/ingress.yaml` |
|
||||
| CP-NK-003 | privacyIDEA self-service hostname | `pink-account.coulomb.social` | `sso-mfa/k8s/privacyidea/ingress.yaml` |
|
||||
|
||||
---
|
||||
|
||||
## CP-NK-002 — privacyIDEA portal hostname
|
||||
|
||||
**Value:** `pink.coulomb.social`
|
||||
**Set:** 2026-03-19
|
||||
**Set by:** worsch
|
||||
|
||||
**Location(s):**
|
||||
- `sso-mfa/k8s/privacyidea/ingress.yaml` — all three Ingress `host` fields
|
||||
|
||||
**Why non-default:** Subdomain prefix must be chosen by the operator; no naming
|
||||
convention existed in the repo before T04. `pink` = **P**rivacy**I**DEA
|
||||
**N**et **K**nights (project-specific mnemonic).
|
||||
|
||||
**Scope:** TLS certificate, Traefik routing, and all references to the
|
||||
privacyIDEA public URL (including Keycloak Provider config in T05/T06).
|
||||
|
||||
---
|
||||
|
||||
## CP-NK-003 — privacyIDEA self-service portal hostname
|
||||
|
||||
**Value:** `pink-account.coulomb.social`
|
||||
**Set:** 2026-03-19
|
||||
**Set by:** worsch
|
||||
|
||||
**Location(s):**
|
||||
- `sso-mfa/k8s/privacyidea/ingress.yaml` — `privacyidea-account` Ingress `host` field
|
||||
|
||||
**Why non-default:** Separate hostname for the self-service portal allows
|
||||
different firewall/allowlist rules from the admin portal. Follows the
|
||||
`<service>-account` naming convention used in the workplan design.
|
||||
|
||||
**Scope:** TLS certificate and Traefik routing for the user-facing
|
||||
self-service token enrolment portal.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ spec:
|
||||
- Egress
|
||||
---
|
||||
# ── Allow ingress from Traefik ───────────────────────────────────────────────
|
||||
# pi.yourdomain.com and pi-account.yourdomain.com both terminate at Traefik.
|
||||
# pink.coulomb.social and pink-account.coulomb.social both terminate at Traefik.
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: NetworkPolicy
|
||||
metadata:
|
||||
|
||||
188
sso-mfa/k8s/privacyidea/README.md
Normal file
188
sso-mfa/k8s/privacyidea/README.md
Normal file
@@ -0,0 +1,188 @@
|
||||
# T04 — Phase 3: Deploy privacyIDEA
|
||||
|
||||
Phase 3 of NK-WP-0001: deploys the MFA core (privacyIDEA) in the `mfa` namespace.
|
||||
|
||||
**Hostnames (config points CP-NK-002 / CP-NK-003):**
|
||||
- `pink.coulomb.social` — main portal and API
|
||||
- `pink-account.coulomb.social` — self-service token portal
|
||||
|
||||
**Prerequisites:**
|
||||
- T02 complete: `mfa` namespace and NetworkPolicies applied, cert-manager running.
|
||||
- T03 complete: PostgreSQL cluster `net-kingdom-pg` in `databases` namespace is Ready.
|
||||
- T01 Phase 0a complete: `gen-secrets.sh` run, all secrets in KeePassXC.
|
||||
|
||||
---
|
||||
|
||||
## Apply order
|
||||
|
||||
### Step 1 — Create the config Secret
|
||||
|
||||
```bash
|
||||
cd sso-mfa/k8s/privacyidea
|
||||
chmod +x create-secrets.sh enckey-bootstrap.sh bootstrap-admin.sh
|
||||
./create-secrets.sh
|
||||
```
|
||||
|
||||
Creates `privacyidea-config` in the `mfa` namespace (PI_SECRET_KEY, PI_PEPPER,
|
||||
PI_SQLALCHEMY_DATABASE_URI).
|
||||
|
||||
---
|
||||
|
||||
### Step 2 — Apply manifests
|
||||
|
||||
```bash
|
||||
# From sso-mfa/k8s/privacyidea/
|
||||
kubectl apply -f pvc.yaml
|
||||
kubectl apply -f configmap.yaml
|
||||
kubectl apply -f middleware.yaml
|
||||
kubectl apply -f deployment.yaml
|
||||
kubectl apply -f ingress.yaml
|
||||
```
|
||||
|
||||
**Wait for the pod to reach Running state** (DB migrations run on first start —
|
||||
allow up to 3 minutes):
|
||||
|
||||
```bash
|
||||
kubectl get pods -n mfa -w
|
||||
# Expected: privacyidea-<hash> 1/1 Running
|
||||
```
|
||||
|
||||
If the pod is stuck in `Init` or `CrashLoopBackOff`, check logs:
|
||||
```bash
|
||||
kubectl logs -n mfa -l app.kubernetes.io/name=privacyidea --previous
|
||||
```
|
||||
|
||||
Common causes:
|
||||
- `privacyidea-config` Secret missing → run `create-secrets.sh`
|
||||
- PostgreSQL not reachable → verify T03, check NetworkPolicies
|
||||
- Wrong DB password → re-run `create-secrets.sh` with corrected secrets
|
||||
|
||||
---
|
||||
|
||||
### Step 3 — Extract key material and create DR Secrets
|
||||
|
||||
Run **once** after the pod reaches `Running`:
|
||||
|
||||
```bash
|
||||
./enckey-bootstrap.sh
|
||||
```
|
||||
|
||||
This generates RSA audit keys (if not already created), extracts the encryption
|
||||
key and audit keys from the pod, and creates two K8s Secrets as disaster-recovery
|
||||
copies:
|
||||
- `privacyidea-enckey`
|
||||
- `privacyidea-auditkeys`
|
||||
|
||||
Follow the printed instructions to store the key files in KeePassXC, then shred
|
||||
the local copies.
|
||||
|
||||
---
|
||||
|
||||
### Step 4 — Bootstrap admin accounts
|
||||
|
||||
```bash
|
||||
./bootstrap-admin.sh
|
||||
```
|
||||
|
||||
Creates `pi-admin` (full admin) and `trigger-admin` (triggerchallenge only).
|
||||
Also creates the `privacyidea-trigger-admin` K8s Secret used by Keycloak in T05.
|
||||
|
||||
**Immediately after the script completes:**
|
||||
1. Log in to `https://pink.coulomb.social` as `pi-admin`.
|
||||
2. Navigate to **Users → pi-admin → Enroll token** and enroll a TOTP or hardware token.
|
||||
3. Log out, log back in — the MFA challenge must appear.
|
||||
4. Verify the `trigger-admin-rights` policy at Config → Policies.
|
||||
|
||||
---
|
||||
|
||||
### Step 5 — Verify
|
||||
|
||||
```bash
|
||||
cd sso-mfa/k8s
|
||||
chmod +x verify-t04.sh
|
||||
./verify-t04.sh
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Container port note
|
||||
|
||||
The deployment uses `containerPort: 8080`. The official `privacyidea/privacyidea`
|
||||
image uses nginx internally; the default nginx port may be 80 depending on the
|
||||
image version.
|
||||
|
||||
**If the pod starts but requests return "connection refused":**
|
||||
```bash
|
||||
# Check what port the container actually listens on:
|
||||
kubectl exec -n mfa <pod> -- ss -tlnp | grep LISTEN
|
||||
```
|
||||
|
||||
If the container uses port 80, update:
|
||||
1. `deployment.yaml`: `containerPort: 80`, Service `targetPort: 80`
|
||||
2. `sso-mfa/k8s/network-policies/netpol-mfa.yaml`: `port: 80` in privacyIDEA rules
|
||||
3. Reapply both files.
|
||||
|
||||
---
|
||||
|
||||
## NetworkPolicy design
|
||||
|
||||
privacyIDEA sits entirely behind the NetworkPolicies applied in T02 (netpol-mfa.yaml):
|
||||
|
||||
| Source | Destination | Port | Purpose |
|
||||
|--------|-------------|------|---------|
|
||||
| Traefik (kube-system) | privacyIDEA (mfa) | 8080 | User-facing portal |
|
||||
| Keycloak (sso) | privacyIDEA (mfa) | 8080 | Provider API (triggerchallenge) |
|
||||
| privacyIDEA (mfa) | PostgreSQL (databases) | 5432 | Database |
|
||||
|
||||
Outbound to anything other than PostgreSQL and kube-dns is denied.
|
||||
|
||||
---
|
||||
|
||||
## Post-deploy steps (after verify-t04.sh passes)
|
||||
|
||||
### Rate limiting adjustment
|
||||
|
||||
The default rate limit (20 req/min, burst 5) is conservative. If Keycloak's
|
||||
triggerchallenge calls trigger false positives (HTTP 429), raise the average
|
||||
in `middleware.yaml` and reapply. Alternatively, the Keycloak-to-PI path is
|
||||
cluster-internal and not subject to the Ingress middleware.
|
||||
|
||||
### Admin WebUI IP restriction
|
||||
|
||||
Update `middleware.yaml` `privacyidea-admin-allowlist.spec.ipAllowList.sourceRange`
|
||||
to your actual VPN/office CIDRs and reapply:
|
||||
|
||||
```bash
|
||||
kubectl apply -f middleware.yaml
|
||||
```
|
||||
|
||||
### Self-service portal
|
||||
|
||||
Enable the self-service portal policy in privacyIDEA:
|
||||
Config → Policies → New policy:
|
||||
- Scope: `user`
|
||||
- Action: `enrollTOTP`, `enrollHOTP`, or token types you want users to manage
|
||||
- URL for self-service: `https://pink-account.coulomb.social`
|
||||
|
||||
---
|
||||
|
||||
## Disaster recovery
|
||||
|
||||
If the `privacyidea-data` PVC is lost:
|
||||
|
||||
1. Create a new PVC with `pvc.yaml`.
|
||||
2. Restore enckey and audit keys from KeePassXC:
|
||||
```bash
|
||||
# Copy pi.enc and private/public.pem from KeePassXC into a temporary pod
|
||||
kubectl run restore-helper --image=busybox -n mfa --restart=Never -- sleep 3600
|
||||
kubectl cp ./pi.enc mfa/restore-helper:/etc/privacyidea/enckey
|
||||
kubectl cp ./private.pem mfa/restore-helper:/etc/privacyidea/private.pem
|
||||
kubectl cp ./public.pem mfa/restore-helper:/etc/privacyidea/public.pem
|
||||
kubectl delete pod -n mfa restore-helper
|
||||
```
|
||||
Or, if the K8s Secrets survived (created by `enckey-bootstrap.sh`):
|
||||
```bash
|
||||
kubectl get secret privacyidea-enckey -n mfa -o jsonpath='{.data.enckey}' | base64 -d > pi.enc
|
||||
```
|
||||
3. Restart the privacyIDEA deployment — it will run DB migrations and use the
|
||||
restored key material.
|
||||
162
sso-mfa/k8s/privacyidea/bootstrap-admin.sh
Executable file
162
sso-mfa/k8s/privacyidea/bootstrap-admin.sh
Executable file
@@ -0,0 +1,162 @@
|
||||
#!/usr/bin/env bash
|
||||
# bootstrap-admin.sh — create pi-admin and trigger-admin in privacyIDEA
|
||||
#
|
||||
# Run AFTER enckey-bootstrap.sh (key material must exist before admin setup).
|
||||
#
|
||||
# What it does:
|
||||
# 1. Creates pi-admin — full admin (single-credential bootstrap moment).
|
||||
# 2. Creates trigger-admin — limited admin for Keycloak's triggerchallenge calls.
|
||||
# 3. Creates the trigger-admin policy (triggerchallenge right only, via REST API).
|
||||
# 4. Prints instructions to immediately enroll MFA for pi-admin.
|
||||
#
|
||||
# Usage:
|
||||
# ./bootstrap-admin.sh [secrets-dir] [pi-url]
|
||||
#
|
||||
# <secrets-dir> default: ../../bootstrap/secrets
|
||||
# <pi-url> default: https://pink.coulomb.social
|
||||
#
|
||||
# The script uses the in-cluster kubectl exec path for creating admin users
|
||||
# (avoids the need for a network route to pink.coulomb.social during bootstrap).
|
||||
# The trigger-admin policy is created via REST API, so the URL must be reachable.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
NAMESPACE="mfa"
|
||||
SECRETS_DIR="${1:-../../bootstrap/secrets}"
|
||||
PI_URL="${2:-https://pink.coulomb.social}"
|
||||
PI_ENV="$SECRETS_DIR/privacyidea/secrets.env"
|
||||
|
||||
if [[ ! -f "$PI_ENV" ]]; then
|
||||
echo "ERROR: $PI_ENV not found — run gen-secrets.sh first." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
PI_ADMIN_PASS=$(bash -c "source '$PI_ENV' 2>/dev/null; echo \$PI_ADMIN_PASSWORD")
|
||||
if [[ -z "$PI_ADMIN_PASS" ]]; then
|
||||
echo "ERROR: PI_ADMIN_PASSWORD not found in $PI_ENV" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# trigger-admin gets its own random password (generated here, stored in KeePassXC).
|
||||
TRIGGER_PASS=$(openssl rand -base64 32 | tr -d '\n/+=' | head -c 40)
|
||||
|
||||
# ── 1. Find running pod ───────────────────────────────────────────────────────
|
||||
PI_POD=$(kubectl get pod -n "$NAMESPACE" \
|
||||
-l app.kubernetes.io/name=privacyidea \
|
||||
--field-selector=status.phase=Running \
|
||||
-o jsonpath='{.items[0].metadata.name}' 2>/dev/null || echo "")
|
||||
|
||||
if [[ -z "$PI_POD" ]]; then
|
||||
echo "ERROR: no Running privacyIDEA pod found in namespace '$NAMESPACE'." >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "Using pod: $PI_POD"
|
||||
|
||||
# ── 2. Create pi-admin (full admin) ──────────────────────────────────────────
|
||||
echo ""
|
||||
echo "Creating admin: pi-admin"
|
||||
if kubectl exec -n "$NAMESPACE" "$PI_POD" -- \
|
||||
pi-manage admin add pi-admin --password "$PI_ADMIN_PASS" 2>&1 | grep -q "already exist"; then
|
||||
echo " pi-admin already exists — skipping."
|
||||
else
|
||||
echo " pi-admin created."
|
||||
fi
|
||||
|
||||
# ── 3. Create trigger-admin (limited admin) ───────────────────────────────────
|
||||
echo ""
|
||||
echo "Creating admin: trigger-admin"
|
||||
if kubectl exec -n "$NAMESPACE" "$PI_POD" -- \
|
||||
pi-manage admin add trigger-admin --password "$TRIGGER_PASS" 2>&1 | grep -q "already exist"; then
|
||||
echo " trigger-admin already exists — skipping password update."
|
||||
echo " WARNING: if you need to rotate trigger-admin's password, do so manually."
|
||||
else
|
||||
echo " trigger-admin created."
|
||||
fi
|
||||
|
||||
# ── 4. Create K8s Secret for trigger-admin credentials ───────────────────────
|
||||
# Keycloak's privacyIDEA Provider reads the trigger-admin credentials from
|
||||
# a K8s Secret that is referenced in the Keycloak realm configuration (T05/T06).
|
||||
echo ""
|
||||
echo "Creating K8s Secret: privacyidea-trigger-admin (namespace: mfa)"
|
||||
kubectl create secret generic privacyidea-trigger-admin \
|
||||
--namespace=mfa \
|
||||
--from-literal=username=trigger-admin \
|
||||
--from-literal=password="$TRIGGER_PASS" \
|
||||
--dry-run=client -o yaml | kubectl apply -f -
|
||||
|
||||
echo " Done."
|
||||
|
||||
# ── 5. Create trigger-admin policy via REST API ───────────────────────────────
|
||||
# This restricts trigger-admin to the triggerchallenge action only.
|
||||
# Requires pink.coulomb.social to be reachable.
|
||||
echo ""
|
||||
echo "Creating trigger-admin policy via REST API ($PI_URL)..."
|
||||
echo " (Skip this step if $PI_URL is not yet reachable — do it via the WebUI instead.)"
|
||||
echo ""
|
||||
|
||||
# Authenticate as pi-admin to get a bearer token.
|
||||
AUTH_RESPONSE=$(curl -sf -X POST "$PI_URL/auth" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{\"username\":\"pi-admin\",\"password\":\"$PI_ADMIN_PASS\"}" || echo "CURL_FAILED")
|
||||
|
||||
if [[ "$AUTH_RESPONSE" == "CURL_FAILED" ]]; then
|
||||
echo " Could not reach $PI_URL — skipping REST policy creation."
|
||||
echo " Create the trigger-admin policy manually (see README.md — Post-deploy steps)."
|
||||
else
|
||||
PI_TOKEN=$(echo "$AUTH_RESPONSE" | python3 -c \
|
||||
"import sys,json; print(json.load(sys.stdin)['result']['value']['token'])" 2>/dev/null || echo "")
|
||||
|
||||
if [[ -z "$PI_TOKEN" ]]; then
|
||||
echo " ERROR: could not extract auth token from response." >&2
|
||||
echo " Create the trigger-admin policy manually (see README.md)." >&2
|
||||
else
|
||||
# Create policy: trigger-admin can only call triggerchallenge.
|
||||
POLICY_RESP=$(curl -sf -X POST "$PI_URL/policy/trigger-admin-rights" \
|
||||
-H "Authorization: $PI_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"scope": "admin",
|
||||
"action": "triggerchallenge",
|
||||
"adminrealm": "*",
|
||||
"adminuser": "trigger-admin",
|
||||
"realm": "*",
|
||||
"priority": 1
|
||||
}' || echo "CURL_FAILED")
|
||||
|
||||
if [[ "$POLICY_RESP" == "CURL_FAILED" ]]; then
|
||||
echo " WARNING: policy creation request failed." >&2
|
||||
echo " Create the trigger-admin policy manually (see README.md)." >&2
|
||||
else
|
||||
echo " Policy 'trigger-admin-rights' created."
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
# ── 6. Summary ────────────────────────────────────────────────────────────────
|
||||
echo ""
|
||||
echo "════════════════════════════════════════════════════════════"
|
||||
echo " Admin bootstrap complete."
|
||||
echo "════════════════════════════════════════════════════════════"
|
||||
echo ""
|
||||
echo "CRITICAL — Do these steps NOW:"
|
||||
echo ""
|
||||
echo " 1. Store trigger-admin password in KeePassXC:"
|
||||
echo " KeePassXC group: net-kingdom/privacyIDEA"
|
||||
echo " Entry: trigger-admin → username=trigger-admin password=<below>"
|
||||
echo " trigger-admin password: $TRIGGER_PASS"
|
||||
echo " Then shred this terminal history."
|
||||
echo ""
|
||||
echo " 2. Log in to the privacyIDEA WebUI as pi-admin:"
|
||||
echo " $PI_URL"
|
||||
echo " Enroll MFA for pi-admin IMMEDIATELY (TOTP or hardware token)."
|
||||
echo " Until MFA is enrolled, pi-admin has only password authentication."
|
||||
echo ""
|
||||
echo " 3. Verify the trigger-admin policy:"
|
||||
echo " WebUI → Config → Policies → trigger-admin-rights"
|
||||
echo " Scope: admin Action: triggerchallenge AdminUser: trigger-admin"
|
||||
echo " If it was not created automatically, create it here."
|
||||
echo ""
|
||||
echo " 4. Test admin MFA:"
|
||||
echo " Log out, log back in as pi-admin — MFA challenge must appear."
|
||||
echo ""
|
||||
echo "Next step: run ../verify-t04.sh"
|
||||
45
sso-mfa/k8s/privacyidea/configmap.yaml
Normal file
45
sso-mfa/k8s/privacyidea/configmap.yaml
Normal file
@@ -0,0 +1,45 @@
|
||||
# ConfigMap: privacyidea-cfg
|
||||
#
|
||||
# Provides /etc/privacyidea/pi.cfg (the privacyIDEA Python config file).
|
||||
# Mounted as a subPath into the privacyidea-data PVC, so it overlays just
|
||||
# that one file while the rest of /etc/privacyidea/ remains on the PVC.
|
||||
#
|
||||
# Sensitive values (SECRET_KEY, PI_PEPPER, SQLALCHEMY_DATABASE_URI) are
|
||||
# injected as environment variables from the privacyidea-config Secret.
|
||||
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: privacyidea-cfg
|
||||
namespace: mfa
|
||||
labels:
|
||||
app.kubernetes.io/part-of: net-kingdom-sso-mfa
|
||||
net-kingdom/component: mfa
|
||||
data:
|
||||
pi.cfg: |
|
||||
# /etc/privacyidea/pi.cfg
|
||||
# Sensitive values injected via environment (privacyidea-config Secret).
|
||||
import os
|
||||
|
||||
# Flask session secret — never type this; comes from vault/KeePassXC.
|
||||
SECRET_KEY = os.environ["PI_SECRET_KEY"]
|
||||
|
||||
# Password hashing pepper — added to all hashed passwords.
|
||||
PI_PEPPER = os.environ["PI_PEPPER"]
|
||||
|
||||
# Encryption key for token secrets (auto-generated on first start if missing).
|
||||
PI_ENCFILE = "/etc/privacyidea/enckey"
|
||||
|
||||
# Audit log RSA signing keys (generated by pi-manage create_audit_keys).
|
||||
PI_AUDIT_KEY_PRIVATE = "/etc/privacyidea/private.pem"
|
||||
PI_AUDIT_KEY_PUBLIC = "/etc/privacyidea/public.pem"
|
||||
|
||||
# Database connection (full URI, password from vault/KeePassXC).
|
||||
SQLALCHEMY_DATABASE_URI = os.environ["PI_SQLALCHEMY_DATABASE_URI"]
|
||||
|
||||
# Application logging.
|
||||
PI_LOGFILE = "/var/log/privacyidea/privacyidea.log"
|
||||
PI_LOGLEVEL = 20 # 10=DEBUG 20=INFO 30=WARNING 40=ERROR
|
||||
|
||||
# Scripts folder for event handlers (leave default unless customising).
|
||||
PI_SCRIPT_FOLDER = "/etc/privacyidea/scripts"
|
||||
65
sso-mfa/k8s/privacyidea/create-secrets.sh
Executable file
65
sso-mfa/k8s/privacyidea/create-secrets.sh
Executable file
@@ -0,0 +1,65 @@
|
||||
#!/usr/bin/env bash
|
||||
# create-secrets.sh — create the privacyidea-config K8s Secret
|
||||
#
|
||||
# Usage:
|
||||
# ./create-secrets.sh [secrets-dir]
|
||||
#
|
||||
# <secrets-dir> is the output directory from sso-mfa/bootstrap/gen-secrets.sh
|
||||
# (default: ../../bootstrap/secrets).
|
||||
#
|
||||
# Creates ONE Secret in the mfa namespace:
|
||||
# privacyidea-config — PI_SECRET_KEY, PI_PEPPER, PI_SQLALCHEMY_DATABASE_URI
|
||||
#
|
||||
# This secret must exist before applying deployment.yaml.
|
||||
#
|
||||
# The enckey and auditkey Secrets (privacyidea-enckey, privacyidea-auditkeys)
|
||||
# are created separately by enckey-bootstrap.sh AFTER the first pod start,
|
||||
# because those keys are auto-generated by the container on first run.
|
||||
#
|
||||
# Re-run this script if you rotate PI_SECRET_KEY or PI_PEPPER in KeePassXC.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SECRETS_DIR="${1:-../../bootstrap/secrets}"
|
||||
PI_ENV="$SECRETS_DIR/privacyidea/secrets.env"
|
||||
|
||||
if [[ ! -d "$SECRETS_DIR" ]]; then
|
||||
echo "ERROR: secrets directory not found: $SECRETS_DIR" >&2
|
||||
echo "Run sso-mfa/bootstrap/gen-secrets.sh first." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [[ ! -f "$PI_ENV" ]]; then
|
||||
echo "ERROR: $PI_ENV not found" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Read values from the generated env file in a subshell to avoid polluting env.
|
||||
PI_SECRET_KEY=$(bash -c "source '$PI_ENV' 2>/dev/null; echo \$PI_SECRET_KEY")
|
||||
PI_PEPPER=$(bash -c "source '$PI_ENV' 2>/dev/null; echo \$PI_PEPPER")
|
||||
PI_DB_PASSWORD=$(bash -c "source '$PI_ENV' 2>/dev/null; echo \$PI_DB_PASSWORD")
|
||||
|
||||
if [[ -z "$PI_SECRET_KEY" || -z "$PI_PEPPER" || -z "$PI_DB_PASSWORD" ]]; then
|
||||
echo "ERROR: could not read PI_SECRET_KEY, PI_PEPPER, or PI_DB_PASSWORD from $PI_ENV" >&2
|
||||
echo "Check that gen-secrets.sh ran successfully." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Construct the SQLAlchemy database URI.
|
||||
# CloudNativePG read-write service: net-kingdom-pg-rw.databases.svc.cluster.local
|
||||
PI_DB_URI="postgresql://privacyidea:${PI_DB_PASSWORD}@net-kingdom-pg-rw.databases.svc.cluster.local:5432/privacyidea_db"
|
||||
|
||||
echo "Creating K8s Secret: privacyidea-config (namespace: mfa)"
|
||||
kubectl create secret generic privacyidea-config \
|
||||
--namespace=mfa \
|
||||
--from-literal=PI_SECRET_KEY="$PI_SECRET_KEY" \
|
||||
--from-literal=PI_PEPPER="$PI_PEPPER" \
|
||||
--from-literal=PI_SQLALCHEMY_DATABASE_URI="$PI_DB_URI" \
|
||||
--dry-run=client -o yaml | kubectl apply -f -
|
||||
|
||||
echo ""
|
||||
echo "Done. Secret privacyidea-config created in namespace: mfa"
|
||||
echo ""
|
||||
echo "Next:"
|
||||
echo " Apply manifests (see README.md apply order)."
|
||||
echo " After the pod is Running, run: ./enckey-bootstrap.sh"
|
||||
152
sso-mfa/k8s/privacyidea/deployment.yaml
Normal file
152
sso-mfa/k8s/privacyidea/deployment.yaml
Normal file
@@ -0,0 +1,152 @@
|
||||
# Deployment + Service — privacyIDEA (namespace: mfa)
|
||||
#
|
||||
# Prerequisites (apply in order):
|
||||
# 1. pvc.yaml — privacyidea-data and privacyidea-logs PVCs
|
||||
# 2. configmap.yaml — privacyidea-cfg (pi.cfg template)
|
||||
# 3. create-secrets.sh — privacyidea-config Secret (PI_SECRET_KEY, PI_PEPPER, DB URI)
|
||||
# 4. This file
|
||||
#
|
||||
# After first pod starts successfully:
|
||||
# 5. enckey-bootstrap.sh — extract enckey + audit keys, create DR Secrets
|
||||
# 6. bootstrap-admin.sh — create pi-admin (+ MFA enrolment) and trigger-admin
|
||||
#
|
||||
# Container port: 8080.
|
||||
# The official privacyidea/privacyidea image uses nginx internally.
|
||||
# If the image you pull listens on port 80 instead of 8080:
|
||||
# - Change containerPort below to 80
|
||||
# - Change the Service targetPort to 80
|
||||
# - Update sso-mfa/k8s/network-policies/netpol-mfa.yaml ports to 80
|
||||
# - Reapply both files
|
||||
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: privacyidea
|
||||
namespace: mfa
|
||||
labels:
|
||||
app.kubernetes.io/name: privacyidea
|
||||
app.kubernetes.io/part-of: net-kingdom-sso-mfa
|
||||
net-kingdom/component: mfa
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app.kubernetes.io/name: privacyidea
|
||||
strategy:
|
||||
type: Recreate # single-node — avoid split-brain on PVC
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app.kubernetes.io/name: privacyidea
|
||||
app.kubernetes.io/part-of: net-kingdom-sso-mfa
|
||||
net-kingdom/component: mfa
|
||||
spec:
|
||||
# ── Security context ───────────────────────────────────────────────────
|
||||
securityContext:
|
||||
runAsNonRoot: false # privacyIDEA nginx needs root to bind port; revisit
|
||||
fsGroup: 999 # privacyidea group inside container
|
||||
|
||||
# ── Init: ensure log dir exists and has correct permissions ───────────
|
||||
initContainers:
|
||||
- name: init-logdir
|
||||
image: busybox:1.36
|
||||
command: ["sh", "-c", "mkdir -p /var/log/privacyidea && chmod 777 /var/log/privacyidea"]
|
||||
volumeMounts:
|
||||
- name: logs
|
||||
mountPath: /var/log/privacyidea
|
||||
|
||||
containers:
|
||||
- name: privacyidea
|
||||
# Pin to a specific release; update via image update policy.
|
||||
# Check https://hub.docker.com/r/privacyidea/privacyidea for latest stable.
|
||||
image: privacyidea/privacyidea:3.12
|
||||
imagePullPolicy: IfNotPresent
|
||||
|
||||
ports:
|
||||
- name: http
|
||||
containerPort: 8080
|
||||
protocol: TCP
|
||||
|
||||
# ── Environment — sensitive values from Secret ──────────────────
|
||||
envFrom:
|
||||
- secretRef:
|
||||
name: privacyidea-config
|
||||
|
||||
# ── Volume mounts ───────────────────────────────────────────────
|
||||
volumeMounts:
|
||||
# pi.cfg overlaid into the data PVC as a single file (subPath).
|
||||
- name: config
|
||||
mountPath: /etc/privacyidea/pi.cfg
|
||||
subPath: pi.cfg
|
||||
readOnly: true
|
||||
# Data PVC: enckey, audit keys, scripts, and other PI runtime files.
|
||||
- name: data
|
||||
mountPath: /etc/privacyidea
|
||||
# Logs PVC: persistent application logs.
|
||||
- name: logs
|
||||
mountPath: /var/log/privacyidea
|
||||
|
||||
# ── Probes ──────────────────────────────────────────────────────
|
||||
# Startup probe: give PI up to 3 min to run DB migrations on first boot.
|
||||
startupProbe:
|
||||
tcpSocket:
|
||||
port: 8080
|
||||
initialDelaySeconds: 15
|
||||
periodSeconds: 10
|
||||
failureThreshold: 18 # 18 × 10s = 3 min
|
||||
livenessProbe:
|
||||
tcpSocket:
|
||||
port: 8080
|
||||
initialDelaySeconds: 0
|
||||
periodSeconds: 15
|
||||
failureThreshold: 3
|
||||
readinessProbe:
|
||||
httpGet:
|
||||
path: /token/
|
||||
port: 8080
|
||||
initialDelaySeconds: 0
|
||||
periodSeconds: 10
|
||||
failureThreshold: 3
|
||||
|
||||
# ── Resources ───────────────────────────────────────────────────
|
||||
# Raise limits for production; privacyIDEA handles crypto and DB queries.
|
||||
resources:
|
||||
requests:
|
||||
cpu: "100m"
|
||||
memory: "256Mi"
|
||||
limits:
|
||||
cpu: "500m"
|
||||
memory: "512Mi"
|
||||
|
||||
# ── Volumes ─────────────────────────────────────────────────────────
|
||||
volumes:
|
||||
- name: config
|
||||
configMap:
|
||||
name: privacyidea-cfg
|
||||
- name: data
|
||||
persistentVolumeClaim:
|
||||
claimName: privacyidea-data
|
||||
- name: logs
|
||||
persistentVolumeClaim:
|
||||
claimName: privacyidea-logs
|
||||
|
||||
---
|
||||
# Service — ClusterIP; Traefik and Keycloak reach privacyIDEA via this.
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: privacyidea
|
||||
namespace: mfa
|
||||
labels:
|
||||
app.kubernetes.io/name: privacyidea
|
||||
app.kubernetes.io/part-of: net-kingdom-sso-mfa
|
||||
net-kingdom/component: mfa
|
||||
spec:
|
||||
type: ClusterIP
|
||||
selector:
|
||||
app.kubernetes.io/name: privacyidea
|
||||
ports:
|
||||
- name: http
|
||||
port: 8080
|
||||
targetPort: 8080
|
||||
protocol: TCP
|
||||
121
sso-mfa/k8s/privacyidea/enckey-bootstrap.sh
Executable file
121
sso-mfa/k8s/privacyidea/enckey-bootstrap.sh
Executable file
@@ -0,0 +1,121 @@
|
||||
#!/usr/bin/env bash
|
||||
# enckey-bootstrap.sh — extract privacyIDEA key material and create DR Secrets
|
||||
#
|
||||
# Run ONCE after the first successful pod start (deployment.yaml applied and Running).
|
||||
#
|
||||
# What it does:
|
||||
# 1. Finds the running privacyIDEA pod.
|
||||
# 2. Generates RSA audit keys inside the pod if they do not exist yet.
|
||||
# 3. Extracts: enckey, private.pem, public.pem → local files.
|
||||
# 4. Creates K8s Secrets privacyidea-enckey and privacyidea-auditkeys.
|
||||
# These are disaster-recovery copies — the authoritative key material
|
||||
# lives in the privacyidea-data PVC.
|
||||
# 5. Prints instructions to copy the extracted files into KeePassXC.
|
||||
#
|
||||
# Usage:
|
||||
# ./enckey-bootstrap.sh [secrets-dir]
|
||||
#
|
||||
# <secrets-dir> is where local copies are written (default: ../../bootstrap/secrets).
|
||||
# Files in secrets-dir are sensitive — shred them after entering into KeePassXC.
|
||||
#
|
||||
# Re-running is safe: kubectl apply is idempotent; pi-manage create_audit_keys
|
||||
# does not overwrite existing keys.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
NAMESPACE="mfa"
|
||||
SECRETS_DIR="${1:-../../bootstrap/secrets}"
|
||||
PI_DIR="$SECRETS_DIR/privacyidea"
|
||||
|
||||
mkdir -p "$PI_DIR"
|
||||
|
||||
# ── 1. Find the running pod ───────────────────────────────────────────────────
|
||||
echo "Looking for running privacyIDEA pod in namespace: $NAMESPACE"
|
||||
PI_POD=$(kubectl get pod -n "$NAMESPACE" \
|
||||
-l app.kubernetes.io/name=privacyidea \
|
||||
--field-selector=status.phase=Running \
|
||||
-o jsonpath='{.items[0].metadata.name}' 2>/dev/null || echo "")
|
||||
|
||||
if [[ -z "$PI_POD" ]]; then
|
||||
echo "ERROR: no Running privacyIDEA pod found in namespace '$NAMESPACE'." >&2
|
||||
echo " Apply the manifests first and wait for the pod to reach Running state:" >&2
|
||||
echo " kubectl get pods -n $NAMESPACE -w" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Found pod: $PI_POD"
|
||||
|
||||
# ── 2. Generate audit keys if missing ────────────────────────────────────────
|
||||
echo ""
|
||||
echo "Checking for audit keys..."
|
||||
if kubectl exec -n "$NAMESPACE" "$PI_POD" -- test -f /etc/privacyidea/private.pem 2>/dev/null; then
|
||||
echo " Audit keys already exist — skipping generation."
|
||||
else
|
||||
echo " Generating audit keys (pi-manage create_audit_keys)..."
|
||||
kubectl exec -n "$NAMESPACE" "$PI_POD" -- pi-manage create_audit_keys
|
||||
echo " Done."
|
||||
fi
|
||||
|
||||
# ── 3. Check enckey exists (auto-generated by PI on first DB init) ────────────
|
||||
echo ""
|
||||
echo "Checking for encryption key..."
|
||||
if ! kubectl exec -n "$NAMESPACE" "$PI_POD" -- test -f /etc/privacyidea/enckey 2>/dev/null; then
|
||||
echo " enckey not found — generating (pi-manage create_enckey)..."
|
||||
kubectl exec -n "$NAMESPACE" "$PI_POD" -- pi-manage create_enckey
|
||||
echo " Done."
|
||||
else
|
||||
echo " enckey exists."
|
||||
fi
|
||||
|
||||
# ── 4. Extract key material to local files ────────────────────────────────────
|
||||
echo ""
|
||||
echo "Extracting key material..."
|
||||
|
||||
kubectl exec -n "$NAMESPACE" "$PI_POD" -- \
|
||||
cat /etc/privacyidea/enckey > "$PI_DIR/pi.enc"
|
||||
echo " Wrote: $PI_DIR/pi.enc"
|
||||
|
||||
kubectl exec -n "$NAMESPACE" "$PI_POD" -- \
|
||||
cat /etc/privacyidea/private.pem > "$PI_DIR/private.pem"
|
||||
echo " Wrote: $PI_DIR/private.pem"
|
||||
|
||||
kubectl exec -n "$NAMESPACE" "$PI_POD" -- \
|
||||
cat /etc/privacyidea/public.pem > "$PI_DIR/public.pem"
|
||||
echo " Wrote: $PI_DIR/public.pem"
|
||||
|
||||
# ── 5. Create K8s Secrets (DR copies) ────────────────────────────────────────
|
||||
echo ""
|
||||
echo "Creating K8s Secret: privacyidea-enckey (namespace: $NAMESPACE)"
|
||||
kubectl create secret generic privacyidea-enckey \
|
||||
--namespace="$NAMESPACE" \
|
||||
--from-file=enckey="$PI_DIR/pi.enc" \
|
||||
--dry-run=client -o yaml | kubectl apply -f -
|
||||
|
||||
echo "Creating K8s Secret: privacyidea-auditkeys (namespace: $NAMESPACE)"
|
||||
kubectl create secret generic privacyidea-auditkeys \
|
||||
--namespace="$NAMESPACE" \
|
||||
--from-file=private.pem="$PI_DIR/private.pem" \
|
||||
--from-file=public.pem="$PI_DIR/public.pem" \
|
||||
--dry-run=client -o yaml | kubectl apply -f -
|
||||
|
||||
# ── 6. Summary and KeePassXC instructions ─────────────────────────────────────
|
||||
echo ""
|
||||
echo "════════════════════════════════════════════════════════════"
|
||||
echo " Key material extracted and K8s Secrets created."
|
||||
echo "════════════════════════════════════════════════════════════"
|
||||
echo ""
|
||||
echo "IMPORTANT — Store the extracted files in KeePassXC NOW:"
|
||||
echo ""
|
||||
echo " KeePassXC group: net-kingdom/privacyIDEA"
|
||||
echo " Entry: PI_ENCFILE"
|
||||
echo " → Attach $PI_DIR/pi.enc as a binary attachment."
|
||||
echo " Entry: Audit Keys"
|
||||
echo " → Attach $PI_DIR/private.pem and $PI_DIR/public.pem"
|
||||
echo ""
|
||||
echo " Then shred the local copies:"
|
||||
echo " find '$PI_DIR' -name '*.enc' -o -name '*.pem' | xargs shred -u"
|
||||
echo ""
|
||||
echo "DR restore: if the privacyidea-data PVC is lost, copy these files"
|
||||
echo " back into a fresh PVC before starting the pod."
|
||||
echo ""
|
||||
echo "Next step: run ./bootstrap-admin.sh"
|
||||
121
sso-mfa/k8s/privacyidea/ingress.yaml
Normal file
121
sso-mfa/k8s/privacyidea/ingress.yaml
Normal file
@@ -0,0 +1,121 @@
|
||||
# Ingress — privacyIDEA (namespace: mfa)
|
||||
#
|
||||
# pink.coulomb.social — main portal (login, self-service, API)
|
||||
# pink-account.coulomb.social — self-service token portal
|
||||
#
|
||||
# Both hostnames resolve to the same privacyIDEA Service.
|
||||
# privacyIDEA serves the self-service portal at /account/ when the
|
||||
# "privacyideaserver" policy for self-service is enabled (configured
|
||||
# in bootstrap-admin.sh / T04 README).
|
||||
#
|
||||
# TLS: cert-manager issues certificates via the letsencrypt-prod ClusterIssuer
|
||||
# (T02). Public DNS for both hostnames must resolve to the cluster's external IP
|
||||
# before cert-manager can complete the ACME HTTP-01 challenge.
|
||||
#
|
||||
# Rate limiting: the privacyidea-rate-limit middleware (middleware.yaml) is
|
||||
# applied to pink.coulomb.social. Admin paths are further restricted by
|
||||
# privacyidea-admin-allowlist applied in the separate /admin Ingress below.
|
||||
#
|
||||
# Config points (see CONFIG.md):
|
||||
# CP-NK-002 pink.coulomb.social
|
||||
# CP-NK-003 pink-account.coulomb.social
|
||||
|
||||
# ── Main portal — pink.coulomb.social ────────────────────────────────────────
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: privacyidea
|
||||
namespace: mfa
|
||||
labels:
|
||||
app.kubernetes.io/name: privacyidea
|
||||
app.kubernetes.io/part-of: net-kingdom-sso-mfa
|
||||
net-kingdom/component: mfa
|
||||
annotations:
|
||||
cert-manager.io/cluster-issuer: letsencrypt-prod
|
||||
# Rate-limit middleware (Traefik v3 format; see middleware.yaml for v2 note).
|
||||
traefik.ingress.kubernetes.io/router.middlewares: "mfa-privacyidea-rate-limit@kubernetescrd"
|
||||
spec:
|
||||
ingressClassName: traefik
|
||||
rules:
|
||||
- host: pink.coulomb.social
|
||||
http:
|
||||
paths:
|
||||
- path: /
|
||||
pathType: Prefix
|
||||
backend:
|
||||
service:
|
||||
name: privacyidea
|
||||
port:
|
||||
number: 8080
|
||||
tls:
|
||||
- secretName: pink-tls
|
||||
hosts:
|
||||
- pink.coulomb.social
|
||||
---
|
||||
# ── Admin WebUI — pink.coulomb.social/admin — restricted to VPN/office IPs ──
|
||||
# Separate Ingress so the admin-allowlist middleware applies only to /admin/*.
|
||||
# The main Ingress above already handles / (which includes /admin/ by prefix);
|
||||
# this Ingress's more-specific /admin path takes precedence in Traefik routing.
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: privacyidea-admin
|
||||
namespace: mfa
|
||||
labels:
|
||||
app.kubernetes.io/name: privacyidea
|
||||
app.kubernetes.io/part-of: net-kingdom-sso-mfa
|
||||
net-kingdom/component: mfa
|
||||
annotations:
|
||||
cert-manager.io/cluster-issuer: letsencrypt-prod
|
||||
# Both rate-limit AND IP allowlist for admin paths.
|
||||
traefik.ingress.kubernetes.io/router.middlewares: >-
|
||||
mfa-privacyidea-rate-limit@kubernetescrd,
|
||||
mfa-privacyidea-admin-allowlist@kubernetescrd
|
||||
spec:
|
||||
ingressClassName: traefik
|
||||
rules:
|
||||
- host: pink.coulomb.social
|
||||
http:
|
||||
paths:
|
||||
- path: /admin
|
||||
pathType: Prefix
|
||||
backend:
|
||||
service:
|
||||
name: privacyidea
|
||||
port:
|
||||
number: 8080
|
||||
tls:
|
||||
- secretName: pink-tls
|
||||
hosts:
|
||||
- pink.coulomb.social
|
||||
---
|
||||
# ── Self-service portal — pink-account.coulomb.social ────────────────────────
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: privacyidea-account
|
||||
namespace: mfa
|
||||
labels:
|
||||
app.kubernetes.io/name: privacyidea
|
||||
app.kubernetes.io/part-of: net-kingdom-sso-mfa
|
||||
net-kingdom/component: mfa
|
||||
annotations:
|
||||
cert-manager.io/cluster-issuer: letsencrypt-prod
|
||||
traefik.ingress.kubernetes.io/router.middlewares: "mfa-privacyidea-rate-limit@kubernetescrd"
|
||||
spec:
|
||||
ingressClassName: traefik
|
||||
rules:
|
||||
- host: pink-account.coulomb.social
|
||||
http:
|
||||
paths:
|
||||
- path: /
|
||||
pathType: Prefix
|
||||
backend:
|
||||
service:
|
||||
name: privacyidea
|
||||
port:
|
||||
number: 8080
|
||||
tls:
|
||||
- secretName: pink-account-tls
|
||||
hosts:
|
||||
- pink-account.coulomb.social
|
||||
56
sso-mfa/k8s/privacyidea/middleware.yaml
Normal file
56
sso-mfa/k8s/privacyidea/middleware.yaml
Normal file
@@ -0,0 +1,56 @@
|
||||
# Traefik Middlewares for privacyIDEA (namespace: mfa)
|
||||
#
|
||||
# Middleware names follow the pattern referenced in ingress.yaml annotations:
|
||||
# mfa-privacyidea-rate-limit@kubernetescrd
|
||||
# mfa-privacyidea-admin-allowlist@kubernetescrd
|
||||
#
|
||||
# Traefik API version:
|
||||
# Traefik v3 (K3s >= 1.30): traefik.io/v1alpha1
|
||||
# Traefik v2 (K3s < 1.30): traefik.containo.us/v1alpha1
|
||||
# Check: kubectl get middleware -n mfa -o yaml | grep apiVersion
|
||||
# Update both documents below if you need the v2 apiVersion.
|
||||
|
||||
# ── Rate limit — all PI endpoints ────────────────────────────────────────────
|
||||
# Applies globally to pink.coulomb.social.
|
||||
# Primary protection for /validate/check (OTP verification) and /auth.
|
||||
# 20 requests/minute per client IP; burst of 5 allowed.
|
||||
# Adjust average/burst upward if legitimate automation (e.g. Keycloak Provider)
|
||||
# triggers false positives — or exclude the Keycloak source IP at network level.
|
||||
apiVersion: traefik.io/v1alpha1
|
||||
kind: Middleware
|
||||
metadata:
|
||||
name: privacyidea-rate-limit
|
||||
namespace: mfa
|
||||
labels:
|
||||
app.kubernetes.io/part-of: net-kingdom-sso-mfa
|
||||
net-kingdom/component: mfa
|
||||
spec:
|
||||
rateLimit:
|
||||
average: 20
|
||||
period: 1m
|
||||
burst: 5
|
||||
---
|
||||
# ── Admin path allowlist — restrict WebUI to internal/VPN IPs ────────────────
|
||||
# Applied to the /admin/* Ingress (see ingress.yaml — separate Ingress for /admin/).
|
||||
#
|
||||
# ADJUST sourceRange to your actual VPN / office CIDR(s) before going live.
|
||||
# Leaving RFC-1918 ranges here is only a dev/staging default.
|
||||
#
|
||||
# Traefik v3 uses ipAllowList; Traefik v2 uses ipWhiteList.
|
||||
# Check your Traefik version and update accordingly.
|
||||
apiVersion: traefik.io/v1alpha1
|
||||
kind: Middleware
|
||||
metadata:
|
||||
name: privacyidea-admin-allowlist
|
||||
namespace: mfa
|
||||
labels:
|
||||
app.kubernetes.io/part-of: net-kingdom-sso-mfa
|
||||
net-kingdom/component: mfa
|
||||
spec:
|
||||
ipAllowList:
|
||||
# EDIT: replace with your VPN/office CIDRs (see CONFIG.md for the pattern).
|
||||
# Example VPN: "10.8.0.0/24"
|
||||
sourceRange:
|
||||
- "10.0.0.0/8"
|
||||
- "172.16.0.0/12"
|
||||
- "192.168.0.0/16"
|
||||
40
sso-mfa/k8s/privacyidea/pvc.yaml
Normal file
40
sso-mfa/k8s/privacyidea/pvc.yaml
Normal file
@@ -0,0 +1,40 @@
|
||||
# PersistentVolumeClaims for privacyIDEA (namespace: mfa)
|
||||
#
|
||||
# privacyidea-data — /etc/privacyidea/
|
||||
# Holds: enckey, audit signing keys, and any runtime PI config.
|
||||
# PI auto-generates missing key material here on first start.
|
||||
# Run enckey-bootstrap.sh after first deploy to extract keys into
|
||||
# KeePassXC and K8s Secrets (disaster recovery copies).
|
||||
#
|
||||
# privacyidea-logs — /var/log/privacyidea/
|
||||
# Application log files; separate PVC keeps data PVC clean.
|
||||
#
|
||||
# Adjust storage sizes before production deployment.
|
||||
|
||||
apiVersion: v1
|
||||
kind: PersistentVolumeClaim
|
||||
metadata:
|
||||
name: privacyidea-data
|
||||
namespace: mfa
|
||||
labels:
|
||||
app.kubernetes.io/part-of: net-kingdom-sso-mfa
|
||||
net-kingdom/component: mfa
|
||||
spec:
|
||||
accessModes: [ReadWriteOnce]
|
||||
resources:
|
||||
requests:
|
||||
storage: 5Gi
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: PersistentVolumeClaim
|
||||
metadata:
|
||||
name: privacyidea-logs
|
||||
namespace: mfa
|
||||
labels:
|
||||
app.kubernetes.io/part-of: net-kingdom-sso-mfa
|
||||
net-kingdom/component: mfa
|
||||
spec:
|
||||
accessModes: [ReadWriteOnce]
|
||||
resources:
|
||||
requests:
|
||||
storage: 2Gi
|
||||
217
sso-mfa/k8s/verify-t04.sh
Executable file
217
sso-mfa/k8s/verify-t04.sh
Executable file
@@ -0,0 +1,217 @@
|
||||
#!/usr/bin/env bash
|
||||
# verify-t04.sh — verify NK-WP-0001-T04 done-criteria
|
||||
#
|
||||
# Checks:
|
||||
# 1. privacyIDEA pod is Running in the mfa namespace
|
||||
# 2. privacyidea Service exists
|
||||
# 3. Traefik Middlewares exist
|
||||
# 4. Ingress resources exist with correct hostnames
|
||||
# 5. TLS certificates issued by cert-manager
|
||||
# 6. Required K8s Secrets are present
|
||||
# 7. PVCs are Bound
|
||||
# 8. Enckey and auditkey Secrets present (from enckey-bootstrap.sh)
|
||||
#
|
||||
# Usage:
|
||||
# chmod +x verify-t04.sh
|
||||
# ./verify-t04.sh
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
NAMESPACE="mfa"
|
||||
PASS=0
|
||||
FAIL=0
|
||||
WARN=0
|
||||
|
||||
pass() { echo " [PASS] $1"; ((PASS++)); }
|
||||
fail() { echo " [FAIL] $1"; ((FAIL++)); }
|
||||
warn() { echo " [WARN] $1"; ((WARN++)); }
|
||||
|
||||
section() { echo ""; echo "── $1 ──────────────────────────────────────"; }
|
||||
|
||||
# ── 1. privacyIDEA pod ────────────────────────────────────────────────────────
|
||||
section "1. privacyIDEA pod (namespace: $NAMESPACE)"
|
||||
|
||||
PI_POD=$(kubectl get pod -n "$NAMESPACE" \
|
||||
-l app.kubernetes.io/name=privacyidea \
|
||||
--field-selector=status.phase=Running \
|
||||
-o jsonpath='{.items[0].metadata.name}' 2>/dev/null || echo "")
|
||||
|
||||
if [[ -n "$PI_POD" ]]; then
|
||||
pass "Pod Running: $PI_POD"
|
||||
|
||||
READY=$(kubectl get pod -n "$NAMESPACE" "$PI_POD" \
|
||||
-o jsonpath='{.status.containerStatuses[0].ready}' 2>/dev/null || echo "false")
|
||||
if [[ "$READY" == "true" ]]; then
|
||||
pass "Pod readiness probe passing"
|
||||
else
|
||||
fail "Pod is Running but not Ready (probe failing — check logs)"
|
||||
fi
|
||||
else
|
||||
PENDING=$(kubectl get pod -n "$NAMESPACE" \
|
||||
-l app.kubernetes.io/name=privacyidea \
|
||||
-o name 2>/dev/null | wc -l || echo 0)
|
||||
if [[ "$PENDING" -gt 0 ]]; then
|
||||
fail "privacyIDEA pod(s) exist but none are Running (check kubectl describe pod -n $NAMESPACE)"
|
||||
else
|
||||
fail "No privacyIDEA pods found in namespace $NAMESPACE — apply deployment.yaml"
|
||||
fi
|
||||
fi
|
||||
|
||||
# ── 2. Service ────────────────────────────────────────────────────────────────
|
||||
section "2. Service"
|
||||
|
||||
if kubectl get service privacyidea -n "$NAMESPACE" &>/dev/null; then
|
||||
pass "Service privacyidea exists"
|
||||
PORT=$(kubectl get service privacyidea -n "$NAMESPACE" \
|
||||
-o jsonpath='{.spec.ports[0].port}' 2>/dev/null || echo "")
|
||||
if [[ "$PORT" == "8080" ]]; then
|
||||
pass "Service port: 8080"
|
||||
else
|
||||
warn "Service port is $PORT (expected 8080 — check container port and netpol)"
|
||||
fi
|
||||
else
|
||||
fail "Service privacyidea not found in namespace $NAMESPACE"
|
||||
fi
|
||||
|
||||
# ── 3. Traefik Middlewares ────────────────────────────────────────────────────
|
||||
section "3. Traefik Middlewares"
|
||||
|
||||
for mw in privacyidea-rate-limit privacyidea-admin-allowlist; do
|
||||
if kubectl get middleware "$mw" -n "$NAMESPACE" &>/dev/null; then
|
||||
pass "Middleware $mw exists"
|
||||
else
|
||||
fail "Middleware $mw not found — apply middleware.yaml"
|
||||
fi
|
||||
done
|
||||
|
||||
# ── 4. Ingress resources ──────────────────────────────────────────────────────
|
||||
section "4. Ingress resources"
|
||||
|
||||
for ing in privacyidea privacyidea-admin privacyidea-account; do
|
||||
if kubectl get ingress "$ing" -n "$NAMESPACE" &>/dev/null; then
|
||||
pass "Ingress $ing exists"
|
||||
else
|
||||
fail "Ingress $ing not found — apply ingress.yaml"
|
||||
fi
|
||||
done
|
||||
|
||||
# Verify hostnames in the main Ingress
|
||||
PI_HOST=$(kubectl get ingress privacyidea -n "$NAMESPACE" \
|
||||
-o jsonpath='{.spec.rules[0].host}' 2>/dev/null || echo "")
|
||||
if [[ "$PI_HOST" == "pink.coulomb.social" ]]; then
|
||||
pass "Ingress host: pink.coulomb.social"
|
||||
else
|
||||
fail "Ingress host is '$PI_HOST' (expected pink.coulomb.social)"
|
||||
fi
|
||||
|
||||
ACCOUNT_HOST=$(kubectl get ingress privacyidea-account -n "$NAMESPACE" \
|
||||
-o jsonpath='{.spec.rules[0].host}' 2>/dev/null || echo "")
|
||||
if [[ "$ACCOUNT_HOST" == "pink-account.coulomb.social" ]]; then
|
||||
pass "Self-service host: pink-account.coulomb.social"
|
||||
else
|
||||
fail "Self-service host is '$ACCOUNT_HOST' (expected pink-account.coulomb.social)"
|
||||
fi
|
||||
|
||||
# ── 5. TLS certificates ───────────────────────────────────────────────────────
|
||||
section "5. TLS certificates"
|
||||
|
||||
for cert_secret in pink-tls pink-account-tls; do
|
||||
if kubectl get secret "$cert_secret" -n "$NAMESPACE" &>/dev/null; then
|
||||
# Check cert-manager Ready condition
|
||||
CERT_NAME="${cert_secret%-tls}"
|
||||
CERT_READY=$(kubectl get certificate "$CERT_NAME" -n "$NAMESPACE" \
|
||||
-o jsonpath='{.status.conditions[?(@.type=="Ready")].status}' 2>/dev/null || echo "")
|
||||
if [[ "$CERT_READY" == "True" ]]; then
|
||||
pass "Certificate $cert_name is Ready (TLS secret $cert_secret exists)"
|
||||
else
|
||||
warn "TLS secret $cert_secret exists but certificate status is not Ready (DNS propagation pending?)"
|
||||
fi
|
||||
else
|
||||
warn "TLS secret $cert_secret not yet issued (cert-manager pending — check DNS and ACME)"
|
||||
fi
|
||||
done
|
||||
|
||||
# ── 6. K8s Secrets ────────────────────────────────────────────────────────────
|
||||
section "6. K8s Secrets (namespace: $NAMESPACE)"
|
||||
|
||||
for secret in privacyidea-config; do
|
||||
if kubectl get secret "$secret" -n "$NAMESPACE" &>/dev/null; then
|
||||
pass "Secret $secret exists"
|
||||
else
|
||||
fail "Secret $secret not found — run create-secrets.sh"
|
||||
fi
|
||||
done
|
||||
|
||||
# DR secrets — created by enckey-bootstrap.sh (may not exist yet on first run)
|
||||
for secret in privacyidea-enckey privacyidea-auditkeys privacyidea-trigger-admin; do
|
||||
if kubectl get secret "$secret" -n "$NAMESPACE" &>/dev/null; then
|
||||
pass "Secret $secret exists"
|
||||
else
|
||||
warn "Secret $secret not found — run enckey-bootstrap.sh / bootstrap-admin.sh"
|
||||
fi
|
||||
done
|
||||
|
||||
# ── 7. PVCs ───────────────────────────────────────────────────────────────────
|
||||
section "7. PersistentVolumeClaims"
|
||||
|
||||
for pvc in privacyidea-data privacyidea-logs; do
|
||||
STATUS=$(kubectl get pvc "$pvc" -n "$NAMESPACE" \
|
||||
-o jsonpath='{.status.phase}' 2>/dev/null || echo "not found")
|
||||
if [[ "$STATUS" == "Bound" ]]; then
|
||||
pass "PVC $pvc: Bound"
|
||||
elif [[ "$STATUS" == "not found" ]]; then
|
||||
fail "PVC $pvc not found — apply pvc.yaml"
|
||||
else
|
||||
fail "PVC $pvc status: $STATUS (expected Bound)"
|
||||
fi
|
||||
done
|
||||
|
||||
# ── 8. Key material check ─────────────────────────────────────────────────────
|
||||
section "8. Key material (inside pod)"
|
||||
|
||||
if [[ -n "$PI_POD" ]]; then
|
||||
if kubectl exec -n "$NAMESPACE" "$PI_POD" -- test -f /etc/privacyidea/enckey 2>/dev/null; then
|
||||
pass "enckey present in pod"
|
||||
else
|
||||
warn "enckey not found in pod — run enckey-bootstrap.sh (or wait for PI to generate on first DB init)"
|
||||
fi
|
||||
|
||||
if kubectl exec -n "$NAMESPACE" "$PI_POD" -- test -f /etc/privacyidea/private.pem 2>/dev/null; then
|
||||
pass "audit private.pem present in pod"
|
||||
else
|
||||
warn "audit private.pem not found — run enckey-bootstrap.sh"
|
||||
fi
|
||||
|
||||
if kubectl exec -n "$NAMESPACE" "$PI_POD" -- \
|
||||
pi-manage admin list 2>/dev/null | grep -q "pi-admin"; then
|
||||
pass "Admin user pi-admin exists"
|
||||
else
|
||||
warn "pi-admin not found — run bootstrap-admin.sh"
|
||||
fi
|
||||
|
||||
if kubectl exec -n "$NAMESPACE" "$PI_POD" -- \
|
||||
pi-manage admin list 2>/dev/null | grep -q "trigger-admin"; then
|
||||
pass "Admin user trigger-admin exists"
|
||||
else
|
||||
warn "trigger-admin not found — run bootstrap-admin.sh"
|
||||
fi
|
||||
else
|
||||
warn "Skipping key material checks — no running pod"
|
||||
fi
|
||||
|
||||
# ── Summary ───────────────────────────────────────────────────────────────────
|
||||
echo ""
|
||||
echo "════════════════════════════════════════════════════════════"
|
||||
echo " T04 verification: PASS=$PASS WARN=$WARN FAIL=$FAIL"
|
||||
echo "════════════════════════════════════════════════════════════"
|
||||
|
||||
if [[ "$FAIL" -gt 0 ]]; then
|
||||
echo " Result: INCOMPLETE — resolve FAIL items before proceeding to T05"
|
||||
exit 1
|
||||
elif [[ "$WARN" -gt 0 ]]; then
|
||||
echo " Result: PARTIAL — T04 core is up; WARN items should be resolved before T05"
|
||||
exit 0
|
||||
else
|
||||
echo " Result: COMPLETE — T04 done-criteria met; proceed to T05 (Keycloak)"
|
||||
exit 0
|
||||
fi
|
||||
@@ -196,8 +196,9 @@ restore drill passed.
|
||||
```task
|
||||
id: NK-WP-0001-T04
|
||||
state_hub_task_id: 6ad1296a-a488-4031-b665-f77030e971ed
|
||||
status: todo
|
||||
status: in_progress
|
||||
priority: high
|
||||
note: Manifests committed (pvc, configmap, deployment, middleware, ingress). Scripts: create-secrets.sh, enckey-bootstrap.sh, bootstrap-admin.sh. verify-t04.sh. Domain pink.coulomb.social (CP-NK-002/003). Pending: apply to live cluster, run enckey-bootstrap.sh, bootstrap-admin.sh.
|
||||
```
|
||||
|
||||
Deploy privacyIDEA via `gpappsoft/privacyidea` Helm chart (Artifact Hub) or
|
||||
@@ -217,7 +218,7 @@ privacyidea:
|
||||
key: PI_ENCFILE
|
||||
ingress:
|
||||
enabled: true
|
||||
hostname: pi.yourdomain.com
|
||||
hostname: pink.coulomb.social
|
||||
tls: true
|
||||
```
|
||||
|
||||
@@ -233,7 +234,7 @@ WAF rules at Traefik level.
|
||||
4. Apply policies: WebUI restricted to VPN/office IPs; MFA required for
|
||||
all admin actions.
|
||||
|
||||
**Done when:** privacyIDEA reachable at pi.yourdomain.com with valid TLS,
|
||||
**Done when:** privacyIDEA reachable at pink.coulomb.social with valid TLS,
|
||||
pi-admin enrolled with MFA, trigger-admin created, rate-limiting active.
|
||||
|
||||
---
|
||||
@@ -287,7 +288,7 @@ In Keycloak:
|
||||
the enterprise tier (not in scope for this workplan phase).
|
||||
2. Create Authentication Flow "privacyIDEA Browser":
|
||||
- Add privacyIDEA execution step (REQUIRED)
|
||||
- Config: privacyIDEA URL = `https://pi.yourdomain.com`, service account
|
||||
- Config: privacyIDEA URL = `https://pink.coulomb.social`, service account
|
||||
= `trigger-admin` (secret from K8s Secret)
|
||||
- Optional: bypass group (break-glass) with strict restrictions + alerts
|
||||
3. Set this flow as the default browser flow.
|
||||
@@ -326,7 +327,7 @@ Define policies in privacyIDEA:
|
||||
- Enrollment rules (who can self-enroll, which token types)
|
||||
- Admin rights separation: super-admin vs. helpdesk-admin
|
||||
|
||||
Enable self-service portal at `pi-account.yourdomain.com` for user token
|
||||
Enable self-service portal at `pink-account.coulomb.social` for user token
|
||||
enrollment/replacement.
|
||||
|
||||
Configure auditing and log shipping: privacyIDEA audit logs + Keycloak
|
||||
@@ -395,7 +396,7 @@ documented and tested, HSTS and NetworkPolicies verified.
|
||||
- [ ] `sso`, `mfa`, `databases` namespaces + NetworkPolicies deployed
|
||||
- [ ] TLS everywhere via cert-manager (Traefik ingress)
|
||||
- [ ] PostgreSQL live; both DBs created; backup + restore tested
|
||||
- [ ] privacyIDEA running at `pi.yourdomain.com`; pi-admin MFA enrolled;
|
||||
- [ ] privacyIDEA running at `pink.coulomb.social`; pi-admin MFA enrolled;
|
||||
trigger-admin created with least-privilege rights
|
||||
- [ ] Keycloak running from custom image including privacyIDEA Provider JAR
|
||||
- [ ] Keycloak "privacyIDEA Browser" flow enforced as default
|
||||
|
||||
Reference in New Issue
Block a user