prole/etc/onboard_tenant.sh
chrisfu cf33342500 feat(prole): bootstrap knoe-auth on k3s; tenant onboarding; cluster stabilisation
knoe-auth (prole.org k3s):
- Fix CNPG manifest drift: remove spec.backup.pluginConfiguration (CNPG 1.28 only),
  switch spec.certificates from serverTLSSecret to serverAltDNSNames
- Apply knoe-auth Round 1 schema + GRANTs manually (postInitSQL had never run on live cluster)
- Fix OIDC signing key generator: base64(DER) not base64(PEM) — OidcTokenService
  does Base64.decode() → PKCS8EncodedKeySpec which requires raw DER bytes
- Add OIDC controllers: authorize, token, userinfo, jwks, discovery
- Add prole Spring profile: cookieDomain, emailDomain, Kerberos config
- Add secret example templates: knoe-db-user, knoe-auth-oidc-signing, knoe-auth-google-prole
- Kong configmap: scope knoe-auth route to /auth prefix only

Tenant onboarding:
- Add etc/onboard_tenant.sh: provision/apply/rotate/status workflow backed by 1Password
  vaults; types: 'enterprise' (own Kerberos + domain) and 'tenant' (hosted, initContainer KDC)
- Provision 'Knoe Tenant - prole.org' vault; apply all 7 k8s secrets to knoe-system
- init_knoe_auth.sh: add explicit GRANT + ALTER DEFAULT PRIVILEGES for knoe role

Cluster stabilisation:
- gitea: roll back 14-day stuck rollout (RWO PVC + maxSurge=100% deadlock);
  patch deployment strategy to Recreate
- supabase: create supabase_admin role, _supabase db, _analytics schema, _realtime schema
  in CNPG — analytics and realtime had never connected since Helm install day 1
- knoe-db barman ObjectStore: add GCS-backed objectstore manifest + scheduled backup

Infrastructure:
- gandalf host_vars: k3s registry config
- pi host_vars: clean up stale entries
- knoe-db schemas: ekosystem.sql, ekosystem_objects.sql
- init_prole_app.sql: prole app DB initialisation

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-26 00:50:37 -07:00

779 lines
33 KiB
Bash
Executable File

#!/usr/bin/env bash
# etc/onboard_tenant.sh — Tenant secret provisioning and Kubernetes wiring
#
# Usage:
# ./etc/onboard_tenant.sh provision <tenant-id> [OPTIONS]
# ./etc/onboard_tenant.sh apply <tenant-id> [OPTIONS]
# ./etc/onboard_tenant.sh rotate <tenant-id> [OPTIONS]
# ./etc/onboard_tenant.sh status <tenant-id>
#
# Subcommands:
# provision Create 1Password vault, generate all auto-generatable secrets,
# store PLACEHOLDERs for items that need external input (Google OAuth,
# Samba keytab). For 'tenant' type: fully automated, no PLACEHOLDERs.
#
# apply Read all secrets from the 1P vault and create/update Kubernetes
# secrets in the tenant namespace. Fails if any PLACEHOLDERs remain.
#
# rotate Post-install password rotation. For each user-rotatable secret,
# offers keep / enter custom / regenerate. Updates 1P + k8s + DB.
#
# status Print a table of what is provisioned vs. pending in the 1P vault.
#
# Options:
# --domain DOMAIN Tenant's primary domain (e.g. prole.org). Defaults to tenant-id.
# --type tenant|enterprise Tenant type. Default: enterprise.
# tenant Hosted tenant: uses knoe-auth at api.knoe.dev/auth, has data but no
# infrastructure. Keytab bootstrapped by initContainer. Fully automated.
# enterprise Brings own Kerberos infrastructure (Samba AD / MIT KDC) and a domain.
# knoe-auth integrates with their KDC. Requires Google OAuth + keytab
# from external AD. PLACEHOLDERs remain until human steps complete.
# --context CTX kubectl context to use. Default: current context.
# --namespace NS Kubernetes namespace for knoe-auth. Default: knoe-system.
# --db-host HOST PostgreSQL host for ALTER ROLE during rotate. Default: pg.<domain>.
# --db-context CTX kubectl context of the DB cluster. Default: same as --context.
# --db-namespace NS Namespace where the CNPG cluster lives. Default: knoe-db.
# --kdc-host HOST SSH host for Samba AD / KDC keytab extraction (enterprise only).
# Default: myrddin.<domain>.
# --ci-google-client ID Shared CI Google OAuth client ID (tenant type only).
# --ci-google-secret SEC Shared CI Google OAuth client secret (tenant type only).
# --yes Non-interactive: accept suggested values without prompting.
#
# 1Password vault layout: "Knoe Tenant - <tenant-id>"
# knoe-db Login — DB role 'knoe' password + superuser password + URL
# knoe-auth Password — session secret
# knoe-oidc Password — RS256 signing key (base64 PEM)
# knoe-google-tenant Login — per-tenant Google OAuth client (enrollment flow)
# knoe-kerberos Note — KDC master/admin passwords + HTTP keytab (base64)
#
# Kubernetes secrets created (namespace: knoe-system):
# knoe-db-user CNPG bootstrap secret (username + password for 'knoe' role)
# knoe-auth-db jdbc URL + user + password
# knoe-auth-secrets sessionSecret
# knoe-auth-oidc-signing signing-key
# knoe-auth-google-<tid> client_id + client_secret (enrollment Google OAuth)
# knoe-auth-http-keytab http.keytab file
# knoe-kdc-secrets master_password + admin_password (in-cluster KDC, tenant type)
#
# For enterprise tenants: after 'provision', the status command will show which items
# need external completion (Google Cloud Console, Samba AD keytab) before 'apply'.
#
# Rotate targets (prompted in order):
# 1. knoe DB role password — ALTER ROLE knoe + k8s secret + 1P
# 2. session secret — k8s secret + 1P (no DB op needed)
# 3. KDC master password — k8s secret + 1P (in-cluster KDC restart required)
# 4. KDC admin password — k8s secret + 1P
set -euo pipefail
# ── Constants ──────────────────────────────────────────────────────────────────
PLACEHOLDER="__PLACEHOLDER__"
VAULT_PREFIX="Knoe Tenant - "
ITEM_DB="knoe-db"
ITEM_AUTH="knoe-auth"
ITEM_OIDC="knoe-oidc"
ITEM_GOOGLE="knoe-google-tenant"
ITEM_KERBEROS="knoe-kerberos"
# ── Logging ────────────────────────────────────────────────────────────────────
_info() { echo " [onboard] $*"; }
_ok() { echo " [onboard] ✓ $*"; }
_warn() { echo " [onboard] ⚠ $*" >&2; }
_err() { echo " [onboard] ✗ $*" >&2; }
_die() { _err "$*"; exit 1; }
_sep() { echo " ──────────────────────────────────────────────────────────"; }
_head() { echo; echo " ══ $* ══"; }
# ── Argument parsing ──────────────────────────────────────────────────────────
SUBCOMMAND="${1:-}"
[[ -z "$SUBCOMMAND" ]] && { echo "Usage: $0 {provision|apply|rotate|status} <tenant-id> [OPTIONS]" >&2; exit 1; }
shift
TENANT_ID="${1:-}"
[[ -z "$TENANT_ID" ]] && _die "tenant-id required as second argument"
shift
# Defaults
TENANT_DOMAIN=""
TENANT_TYPE="enterprise"
KUBE_CONTEXT=""
KUBE_NAMESPACE="knoe-system"
DB_CONTEXT=""
DB_NAMESPACE="knoe-db"
DB_HOST=""
KDC_HOST=""
CI_GOOGLE_CLIENT_ID=""
CI_GOOGLE_CLIENT_SECRET=""
YES=false
while [[ $# -gt 0 ]]; do
case "$1" in
--domain) TENANT_DOMAIN="$2"; shift 2 ;;
--type) TENANT_TYPE="$2"; shift 2 ;;
--context) KUBE_CONTEXT="$2"; shift 2 ;;
--namespace) KUBE_NAMESPACE="$2"; shift 2 ;;
--db-context) DB_CONTEXT="$2"; shift 2 ;;
--db-namespace) DB_NAMESPACE="$2"; shift 2 ;;
--db-host) DB_HOST="$2"; shift 2 ;;
--kdc-host) KDC_HOST="$2"; shift 2 ;;
--ci-google-client) CI_GOOGLE_CLIENT_ID="$2"; shift 2 ;;
--ci-google-secret) CI_GOOGLE_CLIENT_SECRET="$2"; shift 2 ;;
--yes) YES=true; shift ;;
*) _die "Unknown option: $1" ;;
esac
done
# Apply defaults
[[ -z "$TENANT_DOMAIN" ]] && TENANT_DOMAIN="$TENANT_ID"
[[ -z "$DB_HOST" ]] && DB_HOST="pg.${TENANT_DOMAIN}"
[[ -z "$KDC_HOST" ]] && KDC_HOST="myrddin.${TENANT_DOMAIN}"
[[ -z "$DB_CONTEXT" ]] && DB_CONTEXT="${KUBE_CONTEXT}"
VAULT="${VAULT_PREFIX}${TENANT_ID}"
K8S_GOOGLE_SECRET="knoe-auth-google-${TENANT_ID//[^a-z0-9-]/-}"
# kubectl wrapper respecting --context
_kubectl() {
if [[ -n "$KUBE_CONTEXT" ]]; then
kubectl --context="$KUBE_CONTEXT" "$@"
else
kubectl "$@"
fi
}
_kubectl_db() {
if [[ -n "$DB_CONTEXT" ]]; then
kubectl --context="$DB_CONTEXT" "$@"
else
kubectl "$@"
fi
}
# ── Preflight ──────────────────────────────────────────────────────────────────
require_op() {
command -v op >/dev/null 2>&1 || _die "1Password CLI (op) not found. Install: brew install 1password-cli"
op whoami >/dev/null 2>&1 || _die "Not signed in to 1Password. Run: op signin"
}
require_openssl() {
command -v openssl >/dev/null 2>&1 || _die "openssl not found"
}
# ── Secret generation ──────────────────────────────────────────────────────────
gen_password() {
# 32-char alphanumeric+symbol password, URL-safe
openssl rand -base64 32 | tr -dc 'A-Za-z0-9!@#%^&*_+=' | head -c 32
}
gen_secret() {
# 48-byte random secret, base64-encoded (for session secrets etc.)
openssl rand -base64 48 | tr -d '\n'
}
gen_rsa_key_b64() {
# RSA-2048 private key in PKCS#8 DER format, base64-encoded (no newlines).
# Must be raw DER (not PEM): OidcTokenService does Base64.decode() → PKCS8EncodedKeySpec,
# which requires raw DER bytes. PEM headers cause a DER parse error ("extra data at the end").
openssl genrsa 2048 2>/dev/null \
| openssl pkcs8 -topk8 -nocrypt -outform DER 2>/dev/null \
| base64 | tr -d '\n'
}
# ── 1Password helpers ──────────────────────────────────────────────────────────
op_vault_exists() {
op vault get "$VAULT" >/dev/null 2>&1
}
op_item_exists() {
local title="$1"
op item get "$title" --vault "$VAULT" >/dev/null 2>&1
}
op_get_field() {
local title="$1" field="$2"
op item get "$title" --vault "$VAULT" --fields "label=$field" --reveal 2>/dev/null \
| tr -d '\n'
}
op_update_field() {
local title="$1" field="$2" value="$3"
op item edit "$title" --vault "$VAULT" "${field}[concealed]=${value}" >/dev/null
}
is_placeholder() {
[[ "${1:-}" == "$PLACEHOLDER" ]]
}
# ── Provision ─────────────────────────────────────────────────────────────────
cmd_provision() {
require_op
require_openssl
_head "Provisioning tenant: ${TENANT_ID} (type: ${TENANT_TYPE}, domain: ${TENANT_DOMAIN})"
# ── Create vault ──────────────────────────────────────────────────────────
if op_vault_exists; then
_warn "Vault '${VAULT}' already exists — skipping vault creation."
else
_info "Creating 1Password vault: ${VAULT}"
op vault create "$VAULT" >/dev/null
_ok "Vault created."
fi
# ── Item: knoe-db ──────────────────────────────────────────────────────────
_sep
_info "Item: knoe-db (database credentials)"
if op_item_exists "$ITEM_DB"; then
_warn "Item '${ITEM_DB}' already exists — skipping."
else
local knoe_pw superuser_pw
knoe_pw="$(gen_password)"
superuser_pw="$(gen_password)"
local db_url="jdbc:postgresql://knoe-db-rw.knoe-db.svc.cluster.local:5432/knoe-db"
op item create \
--vault "$VAULT" \
--category=Login \
--title="$ITEM_DB" \
--url "$db_url" \
"username[text]=knoe" \
"password[concealed]=${knoe_pw}" \
"superuser_password[concealed]=${superuser_pw}" \
"db_url[text]=${db_url}" \
"bootstrap_username[text]=knoe" \
>/dev/null
_ok "knoe-db: DB role password and superuser password generated."
fi
# ── Item: knoe-auth ────────────────────────────────────────────────────────
_sep
_info "Item: knoe-auth (session secret)"
if op_item_exists "$ITEM_AUTH"; then
_warn "Item '${ITEM_AUTH}' already exists — skipping."
else
local session_secret
session_secret="$(gen_secret)"
op item create \
--vault "$VAULT" \
--category=Password \
--title="$ITEM_AUTH" \
"password[concealed]=${session_secret}" \
>/dev/null
_ok "knoe-auth: session secret generated (${#session_secret} chars)."
fi
# ── Item: knoe-oidc ────────────────────────────────────────────────────────
_sep
_info "Item: knoe-oidc (OIDC RS256 signing key)"
if op_item_exists "$ITEM_OIDC"; then
_warn "Item '${ITEM_OIDC}' already exists — skipping."
else
_info " Generating RSA-2048 key (this takes a moment)..."
local signing_key
signing_key="$(gen_rsa_key_b64)"
op item create \
--vault "$VAULT" \
--category=Password \
--title="$ITEM_OIDC" \
"password[concealed]=${signing_key}" \
>/dev/null
_ok "knoe-oidc: RS256 signing key generated."
fi
# ── Item: knoe-google-tenant ───────────────────────────────────────────────
_sep
_info "Item: knoe-google-tenant (Google OAuth for enrollment flow)"
if op_item_exists "$ITEM_GOOGLE"; then
_warn "Item '${ITEM_GOOGLE}' already exists — skipping."
else
local g_client_id g_client_secret
if [[ "$TENANT_TYPE" == "tenant" ]]; then
# Hosted tenant: use shared CI client (points at api.knoe.dev/auth)
g_client_id="${CI_GOOGLE_CLIENT_ID:-$PLACEHOLDER}"
g_client_secret="${CI_GOOGLE_CLIENT_SECRET:-$PLACEHOLDER}"
if [[ "$g_client_id" == "$PLACEHOLDER" ]]; then
_warn "tenant: No CI Google client provided (--ci-google-client / --ci-google-secret)."
_warn " Set PLACEHOLDER now and fill before 'apply', OR pass flags to skip."
else
_ok "tenant: Using provided CI Google client."
fi
else
# Enterprise tenant: placeholder — customer must create OAuth app in their Cloud Console
g_client_id="$PLACEHOLDER"
g_client_secret="$PLACEHOLDER"
_warn "PLACEHOLDER set for Google OAuth client."
_warn " Complete in Google Cloud Console (enterprise's GCP project):"
_warn " App type: Web application"
_warn " Redirect URIs:"
_warn " https://api.${TENANT_DOMAIN}/auth/auth/enroll/google-callback"
_warn " http://localhost:8080/auth/auth/enroll/google-callback (k3d)"
_warn " Then run:"
_warn " op item edit '${ITEM_GOOGLE}' --vault '${VAULT}' \\"
_warn " 'client_id[text]=<ID>' 'password[concealed]=<SECRET>'"
fi
op item create \
--vault "$VAULT" \
--category=Login \
--title="$ITEM_GOOGLE" \
"username[text]=knoe-${TENANT_DOMAIN}" \
"password[concealed]=${g_client_secret}" \
"client_id[text]=${g_client_id}" \
"hosted_domain[text]=${TENANT_DOMAIN}" \
>/dev/null
_ok "knoe-google-tenant: item created."
fi
# ── Item: knoe-kerberos ────────────────────────────────────────────────────
_sep
_info "Item: knoe-kerberos (KDC passwords + HTTP service keytab)"
if op_item_exists "$ITEM_KERBEROS"; then
_warn "Item '${ITEM_KERBEROS}' already exists — skipping."
else
local master_pw admin_pw http_keytab_b64
master_pw="$(gen_password)"
admin_pw="$(gen_password)"
if [[ "$TENANT_TYPE" == "tenant" ]]; then
# Hosted tenant: uses an in-cluster KDC — keytab is bootstrapped at deploy time
# by the keytab-bootstrap initContainer; store a sentinel so apply knows
# to skip the keytab secret (initContainer handles it).
http_keytab_b64="__INITCONTAINER__"
_ok "tenant: Keytab will be bootstrapped by initContainer at deploy time."
else
# Enterprise tenant: keytab must be extracted from the enterprise's Samba AD / MIT KDC
http_keytab_b64="$PLACEHOLDER"
_warn "PLACEHOLDER set for HTTP service keytab (enterprise Kerberos infrastructure required)."
_warn " On ${KDC_HOST} (enterprise AD/KDC), run:"
_warn " sudo samba-tool user create knoe-auth-http --random-password"
_warn " sudo samba-tool spn add HTTP/api.${TENANT_DOMAIN} knoe-auth-http"
_warn " sudo samba-tool domain exportkeytab /tmp/http.keytab \\"
_warn " --principal=HTTP/api.${TENANT_DOMAIN}"
_warn " klist -k /tmp/http.keytab # verify"
_warn " Then store the keytab in 1Password:"
_warn " KEYTAB_B64=\$(ssh ${KDC_HOST} 'base64 -w0 /tmp/http.keytab')"
_warn " op item edit '${ITEM_KERBEROS}' --vault '${VAULT}' \\"
_warn " 'http_keytab_b64[concealed]='\"\${KEYTAB_B64}\""
fi
op item create \
--vault "$VAULT" \
--category=Password \
--title="$ITEM_KERBEROS" \
"password[concealed]=${master_pw}" \
"master_password[concealed]=${master_pw}" \
"admin_password[concealed]=${admin_pw}" \
"http_keytab_b64[concealed]=${http_keytab_b64}" \
"realm[text]=${TENANT_DOMAIN^^}" \
"service_principal[text]=HTTP/api.${TENANT_DOMAIN}@${TENANT_DOMAIN^^}" \
>/dev/null
_ok "knoe-kerberos: master + admin passwords generated."
fi
_sep
_head "Provision complete"
echo
echo " Vault: ${VAULT}"
echo
echo " Next steps:"
echo " 1. Run 'status ${TENANT_ID}' to see what still needs external input."
if [[ "$TENANT_TYPE" == "enterprise" ]]; then
echo " 2. Complete PLACEHOLDERs (Google OAuth + enterprise Kerberos keytab — see warnings above)."
echo " 3. Run 'apply ${TENANT_ID} --context <CTX>' to create Kubernetes secrets."
else
echo " 2. Run 'apply ${TENANT_ID} --context <CTX>' to create Kubernetes secrets."
fi
echo " 4. After install: run 'rotate ${TENANT_ID}' to set user-chosen passwords."
echo
}
# ── Status ────────────────────────────────────────────────────────────────────
cmd_status() {
require_op
_head "Secret status: ${TENANT_ID}"
echo " Vault: ${VAULT}"
echo
if ! op_vault_exists; then
_err "Vault '${VAULT}' not found. Run: $0 provision ${TENANT_ID}"
return 1
fi
_check_item() {
local item="$1" field="$2" label="$3"
local val
val="$(op_get_field "$item" "$field" 2>/dev/null || echo "$PLACEHOLDER")"
if [[ -z "$val" || "$val" == "$PLACEHOLDER" ]]; then
printf " %-40s %s\n" "$label" "⚠ PLACEHOLDER — needs external input"
elif [[ "$val" == "__INITCONTAINER__" ]]; then
printf " %-40s %s\n" "$label" "✓ (initContainer)"
else
printf " %-40s %s\n" "$label" "✓ set (${#val} chars)"
fi
}
printf " %-40s %s\n" "SECRET" "STATUS"
printf " %-40s %s\n" "──────────────────────────────────────" "──────────────────────────────────"
_check_item "$ITEM_DB" "password" "DB role 'knoe' password"
_check_item "$ITEM_DB" "superuser_password" "DB superuser password"
_check_item "$ITEM_AUTH" "password" "Session secret"
_check_item "$ITEM_OIDC" "password" "OIDC RS256 signing key"
_check_item "$ITEM_GOOGLE" "client_id" "Google OAuth client_id"
_check_item "$ITEM_GOOGLE" "password" "Google OAuth client_secret"
_check_item "$ITEM_KERBEROS" "master_password" "KDC master password"
_check_item "$ITEM_KERBEROS" "admin_password" "KDC admin password"
_check_item "$ITEM_KERBEROS" "http_keytab_b64" "HTTP service keytab"
echo
}
# ── Apply ─────────────────────────────────────────────────────────────────────
cmd_apply() {
require_op
_head "Applying secrets to Kubernetes: ${TENANT_ID}"
_info "Context: ${KUBE_CONTEXT:-<current>}"
_info "Namespace: ${KUBE_NAMESPACE}"
echo
if ! op_vault_exists; then
_die "Vault '${VAULT}' not found. Run: $0 provision ${TENANT_ID}"
fi
# ── Read all secrets ───────────────────────────────────────────────────────
local knoe_pw superuser_pw db_url session_secret signing_key
local g_client_id g_client_secret hosted_domain
local kdc_master kdc_admin http_keytab_b64
_info "Reading secrets from vault..."
knoe_pw=$(op_get_field "$ITEM_DB" "password")
superuser_pw=$(op_get_field "$ITEM_DB" "superuser_password")
db_url=$(op_get_field "$ITEM_DB" "db_url")
session_secret=$(op_get_field "$ITEM_AUTH" "password")
signing_key=$(op_get_field "$ITEM_OIDC" "password")
g_client_id=$(op_get_field "$ITEM_GOOGLE" "client_id")
g_client_secret=$(op_get_field "$ITEM_GOOGLE" "password")
hosted_domain=$(op_get_field "$ITEM_GOOGLE" "hosted_domain")
kdc_master=$(op_get_field "$ITEM_KERBEROS" "master_password")
kdc_admin=$(op_get_field "$ITEM_KERBEROS" "admin_password")
http_keytab_b64=$(op_get_field "$ITEM_KERBEROS" "http_keytab_b64")
# ── Validate: no PLACEHOLDERs ─────────────────────────────────────────────
local has_placeholder=false
_check_placeholder() {
local name="$1" val="$2"
if is_placeholder "$val"; then
_err "PLACEHOLDERs remain: ${name} — complete before running apply."
has_placeholder=true
fi
}
_check_placeholder "Google OAuth client_id" "$g_client_id"
_check_placeholder "Google OAuth client_secret" "$g_client_secret"
_check_placeholder "HTTP service keytab" "$http_keytab_b64"
if $has_placeholder; then
_die "Resolve all PLACEHOLDERs first. Use 'status ${TENANT_ID}' to check."
fi
_ok "All secrets validated — no PLACEHOLDERs."
_sep
# ── Create namespace if needed ─────────────────────────────────────────────
if ! _kubectl get namespace "$KUBE_NAMESPACE" >/dev/null 2>&1; then
_info "Creating namespace: ${KUBE_NAMESPACE}"
_kubectl create namespace "$KUBE_NAMESPACE"
fi
_apply_secret() {
local name="$1"; shift
if _kubectl -n "$KUBE_NAMESPACE" get secret "$name" >/dev/null 2>&1; then
_kubectl -n "$KUBE_NAMESPACE" delete secret "$name" >/dev/null
fi
_kubectl -n "$KUBE_NAMESPACE" create secret generic "$name" "$@" >/dev/null
_ok "Secret applied: ${name}"
}
# knoe-db-user (CNPG bootstrap — must match spec.bootstrap.initdb.owner)
_apply_secret knoe-db-user \
--from-literal=username="knoe" \
--from-literal=password="$knoe_pw"
# knoe-auth-db
_apply_secret knoe-auth-db \
--from-literal=db-url="$db_url" \
--from-literal=db-user="knoe" \
--from-literal=db-password="$knoe_pw"
# knoe-auth-secrets
_apply_secret knoe-auth-secrets \
--from-literal=sessionSecret="$session_secret"
# knoe-auth-oidc-signing
_apply_secret knoe-auth-oidc-signing \
--from-literal=signing-key="$signing_key"
# knoe-auth-google-<tenant> (enrollment flow)
_apply_secret "$K8S_GOOGLE_SECRET" \
--from-literal=client_id="$g_client_id" \
--from-literal=client_secret="$g_client_secret"
# knoe-auth-http-keytab (skip if initContainer-managed)
if [[ "$http_keytab_b64" == "__INITCONTAINER__" ]]; then
_info "Keytab: managed by initContainer — skipping knoe-auth-http-keytab secret."
else
local tmpkeytab
tmpkeytab="$(mktemp /tmp/http_keytab_XXXX)"
echo "$http_keytab_b64" | base64 -d > "$tmpkeytab"
_apply_secret knoe-auth-http-keytab \
--from-file=http.keytab="$tmpkeytab"
rm -f "$tmpkeytab"
fi
# knoe-kdc-secrets (always — initContainer uses these even when also generating keytab)
_apply_secret knoe-kdc-secrets \
--from-literal=master_password="$kdc_master" \
--from-literal=admin_password="$kdc_admin"
_sep
_head "Apply complete"
echo
echo " All Kubernetes secrets created in ${KUBE_NAMESPACE}."
echo
_warn "DB role sync required for existing clusters:"
_warn " The k8s secrets now hold the 1Password-generated password, but if the"
_warn " PostgreSQL 'knoe' role already exists with a different password, knoe-auth"
_warn " will fail to connect. Sync now:"
echo
echo " NEW_PW=\$(op item get 'knoe-db' --vault '${VAULT}' --fields 'label=password' --reveal)"
echo " kubectl --context=${KUBE_CONTEXT:-<CTX>} -n ${DB_NAMESPACE} \\"
echo " exec \$(kubectl --context=${KUBE_CONTEXT:-<CTX>} -n ${DB_NAMESPACE} get pod \\"
echo " -l 'cnpg.io/cluster=knoe-db,role=primary' -o jsonpath='{.items[0].metadata.name}') \\"
echo " -c postgres -- psql -U postgres -c \"ALTER ROLE knoe WITH PASSWORD '\${NEW_PW}';\""
echo
echo " Or use 'rotate' which handles this automatically:"
echo " $0 rotate ${TENANT_ID} --context ${KUBE_CONTEXT:-<CTX>}"
echo
echo " Then restart knoe-auth to pick up new secrets:"
echo " kubectl --context=${KUBE_CONTEXT:-<CTX>} -n ${KUBE_NAMESPACE} \\"
echo " rollout restart deployment/knoe-auth"
echo
}
# ── Rotate ────────────────────────────────────────────────────────────────────
cmd_rotate() {
require_op
require_openssl
_head "Post-install password rotation: ${TENANT_ID}"
_info "Cluster: ${KUBE_CONTEXT:-<current>}"
_info "Namespace: ${KUBE_NAMESPACE}"
_info "DB host: ${DB_HOST}"
echo
if ! op_vault_exists; then
_die "Vault '${VAULT}' not found."
fi
# Helper: prompt for new value or generate
_rotate_secret() {
local label="$1" current="$2"
local masked="${current:0:4}****"
if $YES; then
# Non-interactive: regenerate
gen_password
return
fi
echo
echo " ┌─ ${label}"
echo " │ Current: ${masked} (${#current} chars)"
echo " │"
echo " │ [1] Keep current"
echo " │ [2] Enter custom"
echo " │ [3] Regenerate (strong random)"
printf " └─ Choice [1/2/3]: "
read -r choice
case "$choice" in
2)
printf " Enter new value: "
read -rs new_val; echo
echo "$new_val"
;;
3)
local new; new="$(gen_password)"
echo " Generated: ${new:0:4}****" >&2
echo "$new"
;;
*)
echo "$current" # keep
;;
esac
}
# ── Rotate: DB role 'knoe' password ───────────────────────────────────────
_sep
_info "Rotating: DB role 'knoe' password"
local current_knoe_pw new_knoe_pw
current_knoe_pw="$(op_get_field "$ITEM_DB" "password")"
new_knoe_pw="$(_rotate_secret "DB role 'knoe' password" "$current_knoe_pw")"
if [[ "$new_knoe_pw" != "$current_knoe_pw" ]]; then
_info " Updating DB role 'knoe' via ALTER ROLE..."
local superuser_pw
superuser_pw="$(op_get_field "$ITEM_DB" "superuser_password")"
# Find CNPG primary pod
local primary_pod
primary_pod="$(_kubectl_db -n "$DB_NAMESPACE" get pod \
-l "cnpg.io/cluster=knoe-db,role=primary" \
-o jsonpath='{.items[0].metadata.name}' 2>/dev/null)"
[[ -z "$primary_pod" ]] && _die "Cannot find CNPG primary pod in ${DB_NAMESPACE}."
_kubectl_db -n "$DB_NAMESPACE" exec "$primary_pod" -c postgres -- \
psql -U postgres -c "ALTER ROLE knoe WITH PASSWORD '${new_knoe_pw}';" >/dev/null
_ok " DB role updated."
# Update 1P
op item edit "$ITEM_DB" --vault "$VAULT" \
"password[concealed]=${new_knoe_pw}" >/dev/null
_ok " 1Password updated."
# Update k8s secrets
local db_url
db_url="$(op_get_field "$ITEM_DB" "db_url")"
_kubectl -n "$KUBE_NAMESPACE" delete secret knoe-auth-db >/dev/null 2>&1 || true
_kubectl -n "$KUBE_NAMESPACE" create secret generic knoe-auth-db \
--from-literal=db-url="$db_url" \
--from-literal=db-user="knoe" \
--from-literal=db-password="$new_knoe_pw" >/dev/null
_kubectl -n "$KUBE_NAMESPACE" delete secret knoe-db-user >/dev/null 2>&1 || true
_kubectl -n "$KUBE_NAMESPACE" create secret generic knoe-db-user \
--from-literal=username="knoe" \
--from-literal=password="$new_knoe_pw" >/dev/null
_ok " Kubernetes secrets updated."
else
_ok " Keeping current DB password."
fi
# ── Rotate: session secret ─────────────────────────────────────────────────
_sep
_info "Rotating: session secret"
local current_session new_session
current_session="$(op_get_field "$ITEM_AUTH" "password")"
new_session="$(_rotate_secret "Session secret" "$current_session")"
if [[ "$new_session" != "$current_session" ]]; then
op item edit "$ITEM_AUTH" --vault "$VAULT" \
"password[concealed]=${new_session}" >/dev/null
_ok " 1Password updated."
_kubectl -n "$KUBE_NAMESPACE" delete secret knoe-auth-secrets >/dev/null 2>&1 || true
_kubectl -n "$KUBE_NAMESPACE" create secret generic knoe-auth-secrets \
--from-literal=sessionSecret="$new_session" >/dev/null
_ok " Kubernetes secret updated."
else
_ok " Keeping current session secret."
fi
# ── Rotate: KDC master password ────────────────────────────────────────────
_sep
_info "Rotating: KDC master password"
local current_master new_master
current_master="$(op_get_field "$ITEM_KERBEROS" "master_password")"
new_master="$(_rotate_secret "KDC master password" "$current_master")"
if [[ "$new_master" != "$current_master" ]]; then
op item edit "$ITEM_KERBEROS" --vault "$VAULT" \
"master_password[concealed]=${new_master}" \
"password[concealed]=${new_master}" >/dev/null
_ok " 1Password updated."
# Update the composite kdc-secrets secret
local current_admin
current_admin="$(op_get_field "$ITEM_KERBEROS" "admin_password")"
_kubectl -n "$KUBE_NAMESPACE" delete secret knoe-kdc-secrets >/dev/null 2>&1 || true
_kubectl -n "$KUBE_NAMESPACE" create secret generic knoe-kdc-secrets \
--from-literal=master_password="$new_master" \
--from-literal=admin_password="$current_admin" >/dev/null
_ok " Kubernetes secret updated."
_warn " KDC master password changed — the in-cluster KDC pod must be restarted."
_warn " Rolling restart will re-run the keytab-bootstrap initContainer."
else
_ok " Keeping current KDC master password."
fi
# ── Rotate: KDC admin password ─────────────────────────────────────────────
_sep
_info "Rotating: KDC admin password"
local current_kadmin new_kadmin
current_kadmin="$(op_get_field "$ITEM_KERBEROS" "admin_password")"
new_kadmin="$(_rotate_secret "KDC admin password" "$current_kadmin")"
if [[ "$new_kadmin" != "$current_kadmin" ]]; then
op item edit "$ITEM_KERBEROS" --vault "$VAULT" \
"admin_password[concealed]=${new_kadmin}" >/dev/null
_ok " 1Password updated."
local current_master_now
current_master_now="$(op_get_field "$ITEM_KERBEROS" "master_password")"
_kubectl -n "$KUBE_NAMESPACE" delete secret knoe-kdc-secrets >/dev/null 2>&1 || true
_kubectl -n "$KUBE_NAMESPACE" create secret generic knoe-kdc-secrets \
--from-literal=master_password="$current_master_now" \
--from-literal=admin_password="$new_kadmin" >/dev/null
_ok " Kubernetes secret updated."
else
_ok " Keeping current KDC admin password."
fi
# ── Roll deployment to pick up new secrets ─────────────────────────────────
_sep
_info "Rolling knoe-auth deployment to pick up updated secrets..."
_kubectl -n "$KUBE_NAMESPACE" rollout restart deployment/knoe-auth >/dev/null 2>&1 || \
_warn "Could not restart deployment — do it manually: kubectl rollout restart deployment/knoe-auth"
_ok "Rollout triggered."
_sep
_head "Rotation complete"
echo
echo " All selected secrets have been:"
echo " • Updated in 1Password vault '${VAULT}'"
echo " • Re-applied as Kubernetes secrets in ${KUBE_NAMESPACE}"
echo " • DB role updated via ALTER ROLE (if changed)"
echo
echo " knoe-auth deployment is rolling — run:"
echo " kubectl -n ${KUBE_NAMESPACE} rollout status deployment/knoe-auth"
echo
}
# ── Dispatch ──────────────────────────────────────────────────────────────────
case "$SUBCOMMAND" in
provision) cmd_provision ;;
apply) cmd_apply ;;
rotate) cmd_rotate ;;
status) cmd_status ;;
*)
echo "Usage: $0 {provision|apply|rotate|status} <tenant-id> [OPTIONS]" >&2
echo
echo " provision Create 1Password vault and pre-generate all secrets"
echo " apply Create Kubernetes secrets from 1Password vault"
echo " rotate Post-install: update passwords, re-apply secrets, roll deployment"
echo " status Show what is provisioned vs. pending in the vault"
echo
echo "Examples:"
echo " $0 provision prole.org --domain prole.org --type enterprise"
echo " $0 provision acme --type tenant --ci-google-client <ID> --ci-google-secret <S>"
echo " $0 status prole.org"
echo " $0 apply prole.org --context prole-service-cluster"
echo " $0 rotate prole.org --context prole-service-cluster --yes"
exit 1
;;
esac