prole/etc/init_knoe_auth.sh
chrisfu 3728889e25 Phase 1: OIDC provider integration and GKE auth deployment
- Implement Google OIDC support in Authority module via GoogleOAuthService

- Update AuthProperties and application.yml with OIDC configuration

- Add oidc-setup.md documentation for GKE/Google Cloud setup

- Update etc/init_knoe_auth.sh to handle OIDC secrets and path-B configuration

- Configure knoe-auth-deployment.yaml and gke.cfg for production auth

Co-authored-by: Junie <junie@jetbrains.com>
2026-04-28 12:22:45 -07:00

397 lines
15 KiB
Bash
Executable File

#!/usr/bin/env bash
# init_knoe_auth.sh
# Provision knoe-auth (Kerberos KDC + Spring Boot enrollment service) on GKE.
#
# Usage:
# ./etc/init_knoe_auth.sh [--context KUBECONTEXT] [--namespace NAMESPACE] [--project PROJECT_ID]
# ./etc/init_knoe_auth.sh initialize # full setup
# ./etc/init_knoe_auth.sh schema # schema only (idempotent)
# ./etc/init_knoe_auth.sh invite EMAIL # create first admin invite
# ./etc/init_knoe_auth.sh status # check pod + principal state
#
# Prerequisites:
# kubectl, op (1Password CLI), psql (or kubectl exec fallback)
#
# Env vars (override args):
# APP_CLUSTER_KUBECONTEXT, KNOE_NAMESPACE, GCP_PROJECT_ID,
# KNOE_DB_HOST, KNOE_DB_PORT, KNOE_DB_NAME, KNOE_DB_SUPERUSER
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
GKE_DIR="$REPO_ROOT/deploy/gcp/gke"
# ── Defaults ────────────────────────────────────────────────────────────────
APP_CTX="${APP_CLUSTER_KUBECONTEXT:-gke_plenary-truck-485623-p7_us-west3_knoe-dev-0}"
NAMESPACE="${KNOE_NAMESPACE:-knoe-system}"
GCP_PROJECT="${GCP_PROJECT_ID:-plenary-truck-485623-p7}"
DB_HOST="${KNOE_DB_HOST:-}" # resolved from CNPG svc if blank
DB_PORT="${KNOE_DB_PORT:-5432}"
DB_NAME="${KNOE_DB_NAME:-knoe}"
DB_SUPERUSER="${KNOE_DB_SUPERUSER:-postgres}"
REALM="KNOE.DEV"
AUTH_HOST="${KNOE_AUTH_HOST:-https://auth.knoe.dev}"
log() { echo "[init_knoe_auth] $*"; }
info() { log "INFO $*"; }
warn() { log "WARN $*" >&2; }
die() { log "ERROR $*" >&2; exit 1; }
kube() { kubectl --context="$APP_CTX" "$@"; }
# ── Argument parsing ─────────────────────────────────────────────────────────
COMMAND="${1:-initialize}"
shift || true
while [[ $# -gt 0 ]]; do
case "$1" in
--context) APP_CTX="$2"; shift 2 ;;
--namespace) NAMESPACE="$2"; shift 2 ;;
--project) GCP_PROJECT="$2"; shift 2 ;;
--db-host) DB_HOST="$2"; shift 2 ;;
*) break ;;
esac
done
# ── Helpers ──────────────────────────────────────────────────────────────────
require_tool() {
command -v "$1" >/dev/null 2>&1 || die "Required tool not found: $1 — install it and retry."
}
wait_for_pods() {
local label="$1"
local timeout="${2:-180}"
info "Waiting up to ${timeout}s for pods with label ${label} in ${NAMESPACE}..."
kube -n "$NAMESPACE" wait pod \
-l "$label" \
--for=condition=Ready \
--timeout="${timeout}s"
}
op_secret() {
# Retrieve a 1Password secret; fall back to prompting if op isn't authed.
local item="$1" field="${2:-password}"
if command -v op >/dev/null 2>&1; then
op item get "$item" --fields "$field" 2>/dev/null || {
warn "1Password: could not read $item/$field — prompting."
read -rsp "Enter value for $item/$field: " val; echo
printf '%s' "$val"
}
else
read -rsp "Enter value for $item/$field: " val; echo
printf '%s' "$val"
fi
}
resolve_db_host() {
if [[ -n "$DB_HOST" ]]; then return; fi
# Try to resolve CNPG primary service from the DB cluster context
DB_CTX="${DB_CLUSTER_KUBECONTEXT:-gke_plenary-truck-485623-p7_us-west3_knoe-cnpg-0}"
DB_HOST=$(kubectl --context="$DB_CTX" -n knoe-db-0 \
get svc knoe-db-rw -o jsonpath='{.spec.clusterIP}' 2>/dev/null || echo "")
[[ -z "$DB_HOST" ]] && die "Cannot resolve KNOE DB host. Set KNOE_DB_HOST or ensure knoe-db-rw svc exists."
info "Resolved DB host: $DB_HOST"
}
psql_file() {
local file="$1"
if command -v psql >/dev/null 2>&1 && [[ -n "${PGPASSWORD:-}" ]]; then
psql -h "$DB_HOST" -p "$DB_PORT" -U "$DB_SUPERUSER" -d "$DB_NAME" -f "$file"
else
# Fallback: exec into CNPG primary pod
DB_CTX="${DB_CLUSTER_KUBECONTEXT:-gke_plenary-truck-485623-p7_us-west3_knoe-cnpg-0}"
local pod
pod=$(kubectl --context="$DB_CTX" -n knoe-db-0 \
get pod -l cnpg.io/cluster=knoe-db,role=primary \
-o jsonpath='{.items[0].metadata.name}' 2>/dev/null)
[[ -z "$pod" ]] && die "Cannot find CNPG primary pod. Set PGPASSWORD and KNOE_DB_HOST for direct psql."
kubectl --context="$DB_CTX" -n knoe-db-0 cp "$file" "${pod}:/tmp/knoe_auth_schema.sql"
kubectl --context="$DB_CTX" -n knoe-db-0 exec "$pod" -- \
psql -U "$DB_SUPERUSER" -d "$DB_NAME" -f /tmp/knoe_auth_schema.sql
fi
}
# ── Schema ───────────────────────────────────────────────────────────────────
run_schema() {
info "Applying knoe-auth schema additions..."
resolve_db_host
local tmpfile
tmpfile=$(mktemp /tmp/knoe_auth_schema_XXXX.sql)
cat > "$tmpfile" <<'ENDSQL'
-- ── knoe-auth Round 1 schema additions ───────────────────────────────────────
-- Idempotent: all CREATE TABLE ... IF NOT EXISTS
-- Invite tokens (admin creates, single-use)
-- contact is the email/phone the invite was sent to — the OTP trust anchor.
-- knoe.dev starts with ZERO pre-knowledge of the developer's home org.
CREATE TABLE IF NOT EXISTS knoe.invitation (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
token TEXT NOT NULL UNIQUE,
contact TEXT NOT NULL,
contact_type TEXT NOT NULL DEFAULT 'email',
name_hint TEXT,
otp_hash TEXT NOT NULL,
otp_expires_at TIMESTAMPTZ NOT NULL,
otp_attempts INT NOT NULL DEFAULT 0,
otp_verified_at TIMESTAMPTZ,
created_by TEXT NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
expires_at TIMESTAMPTZ NOT NULL,
used_at TIMESTAMPTZ,
used_by TEXT
);
CREATE INDEX IF NOT EXISTS idx_invitation_token ON knoe.invitation(token);
CREATE INDEX IF NOT EXISTS idx_invitation_contact ON knoe.invitation(contact);
-- External identity corroborations (Google sub → knoe user)
-- provider_hd records the developer's home domain (prole.org, gmail.com, etc.)
-- for audit purposes only — it is NOT used for access control.
CREATE TABLE IF NOT EXISTS knoe.identity (
id SERIAL PRIMARY KEY,
user_id INT NOT NULL REFERENCES knoe.user(id) ON DELETE CASCADE,
provider TEXT NOT NULL,
provider_sub TEXT NOT NULL,
provider_email TEXT,
provider_hd TEXT,
verified_at TIMESTAMPTZ NOT NULL,
UNIQUE(provider, provider_sub)
);
CREATE INDEX IF NOT EXISTS idx_identity_user ON knoe.identity(user_id);
-- TOTP 2FA credentials (encrypted secret, backup codes)
CREATE TABLE IF NOT EXISTS knoe.totp_credential (
user_id INT PRIMARY KEY REFERENCES knoe.user(id) ON DELETE CASCADE,
secret TEXT NOT NULL,
verified_at TIMESTAMPTZ,
backup_codes TEXT[],
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
-- Platform-managed resources (repos, db roles, policies, etc.)
CREATE TABLE IF NOT EXISTS knoe.knobject (
id SERIAL PRIMARY KEY,
type TEXT NOT NULL,
name TEXT NOT NULL,
platform_id TEXT,
metadata JSONB,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
UNIQUE(type, name)
);
-- Access grants (user → knobject with role)
CREATE TABLE IF NOT EXISTS knoe.access_grant (
id SERIAL PRIMARY KEY,
user_id INT NOT NULL REFERENCES knoe.user(id),
knobject_id INT NOT NULL REFERENCES knoe.knobject(id),
role TEXT NOT NULL,
granted_by TEXT NOT NULL,
granted_at TIMESTAMPTZ NOT NULL DEFAULT now(),
revoked_at TIMESTAMPTZ,
UNIQUE(user_id, knobject_id)
);
-- Async provisioning job queue (GitLab user, Gitea user, CNPG role, etc.)
CREATE TABLE IF NOT EXISTS knoe.provisioning_job (
id SERIAL PRIMARY KEY,
user_id INT NOT NULL REFERENCES knoe.user(id),
job_type TEXT NOT NULL,
status TEXT NOT NULL DEFAULT 'pending',
payload JSONB,
result JSONB,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE INDEX IF NOT EXISTS idx_provisioning_job_status
ON knoe.provisioning_job(status, created_at);
-- Seed well-known knobjects
INSERT INTO knoe.knobject (type, name, metadata) VALUES
('gitea_org', 'knoe.dev', '{"description": "Knoe.DEV Gitea organisation"}'),
('gitlab_group','knoe.dev', '{"description": "Knoe.DEV GitLab group"}')
ON CONFLICT (type, name) DO NOTHING;
SELECT 'knoe-auth schema v1 applied.' AS status;
ENDSQL
psql_file "$tmpfile"
rm -f "$tmpfile"
info "Schema applied."
}
# ── KDC secrets ──────────────────────────────────────────────────────────────
create_kdc_secrets() {
if kube -n "$NAMESPACE" get secret knoe-kdc-secrets >/dev/null 2>&1; then
info "knoe-kdc-secrets already exists — skipping."
return
fi
info "Creating knoe-kdc-secrets from 1Password..."
local master admin
master=$(op_secret "knoe-kdc-master" "password")
admin=$(op_secret "knoe-kdc-admin" "password")
kube -n "$NAMESPACE" create secret generic knoe-kdc-secrets \
--from-literal=master_password="$master" \
--from-literal=admin_password="$admin"
info "knoe-kdc-secrets created."
}
create_google_oidc_secret() {
if kube -n "$NAMESPACE" get secret knoe-auth-google-oidc >/dev/null 2>&1; then
info "knoe-auth-google-oidc already exists — skipping."
return
fi
info "Creating knoe-auth-google-oidc secret..."
local client_id client_secret
client_id=$(op_secret "knoe-google-oidc" "client_id")
client_secret=$(op_secret "knoe-google-oidc" "client_secret")
kube -n "$NAMESPACE" create secret generic knoe-auth-google-oidc \
--from-literal=client_id="$client_id" \
--from-literal=client_secret="$client_secret"
info "knoe-auth-google-oidc created."
}
create_session_secret() {
if kube -n "$NAMESPACE" get secret knoe-auth-secrets >/dev/null 2>&1; then
info "knoe-auth-secrets already exists — skipping."
return
fi
info "Creating knoe-auth-secrets (session HMAC key)..."
local session_secret
session_secret=$(op_secret "knoe-auth-session" "password")
kube -n "$NAMESPACE" create secret generic knoe-auth-secrets \
--from-literal=sessionSecret="$session_secret"
info "knoe-auth-secrets created."
}
create_oidc_path_b_secret() {
if kube -n "$NAMESPACE" get secret knoe-auth-oidc >/dev/null 2>&1; then
info "knoe-auth-oidc already exists — skipping."
return
fi
info "Creating knoe-auth-oidc secret for Path B..."
local client_id client_secret signing_key
client_id=$(op_secret "knoe-auth-oidc-gitlab" "client_id")
client_secret=$(op_secret "knoe-auth-oidc-gitlab" "client_secret")
signing_key=$(op_secret "knoe-auth-oidc-signing" "private_key")
kube -n "$NAMESPACE" create secret generic knoe-auth-oidc \
--from-literal=client-id="$client_id" \
--from-literal=client-secret="$client_secret" \
--from-literal=signing-key="$signing_key"
info "knoe-auth-oidc created."
}
# ── Manifests ────────────────────────────────────────────────────────────────
apply_manifests() {
info "Applying KDC ConfigMap..."
kube apply -f "$GKE_DIR/knoe-kdc-configmap.yaml"
info "Applying knoe-auth Deployment..."
kube apply -f "$GKE_DIR/knoe-auth-deployment.yaml"
}
# ── Invite helper ─────────────────────────────────────────────────────────────
create_first_invite() {
local contact="${1:-}"
[[ -z "$contact" ]] && { read -rp "Invite contact (email or phone): " contact; }
local name_hint=""
read -rp "Display name hint (optional, press Enter to skip): " name_hint || true
info "Creating invite for: $contact"
local admin_token
admin_token=$(op_secret "knoe-admin-token" "credential" 2>/dev/null || \
{ read -rsp "Admin token: " t; echo; printf '%s' "$t"; })
local response
response=$(curl -sf -X POST \
-H "Content-Type: application/json" \
-H "Authorization: Bearer $admin_token" \
-d "{\"contact\":\"$contact\",\"contactType\":\"email\",\"nameHint\":\"$name_hint\"}" \
"${AUTH_HOST}/auth/admin/invites") || {
warn "Admin API call failed. knoe-auth may not be ready yet."
warn "Retry: POST ${AUTH_HOST}/auth/admin/invites"
return 1
}
local invite_url
invite_url=$(printf '%s' "$response" | grep -o '"enrollUrl":"[^"]*"' | sed 's/"enrollUrl":"//;s/"//')
printf '\n\033[1;32mInvite URL:\033[0m %s\n\n' "$invite_url"
info "Send the above URL to: $contact"
}
# ── Status ────────────────────────────────────────────────────────────────────
show_status() {
info "=== Pod status ==="
kube -n "$NAMESPACE" get pods -l app=knoe-auth 2>/dev/null || true
info "=== Secrets ==="
kube -n "$NAMESPACE" get secret \
knoe-kdc-secrets knoe-auth-google-oidc knoe-auth-secrets 2>/dev/null || true
info "=== Services ==="
kube -n "$NAMESPACE" get svc knoe-auth 2>/dev/null || true
info "=== Enrollment endpoint ==="
info "${AUTH_HOST}/auth/enroll"
}
# ── Full initialization ───────────────────────────────────────────────────────
cmd_initialize() {
require_tool kubectl
info "=== knoe-auth initialization ==="
info " Realm: $REALM"
info " Cluster: $APP_CTX"
info " NS: $NAMESPACE"
info ""
# 1. Ensure namespace exists
kube get namespace "$NAMESPACE" >/dev/null 2>&1 || \
kube create namespace "$NAMESPACE"
# 2. Secrets
create_kdc_secrets
create_google_oidc_secret
create_session_secret
create_oidc_path_b_secret
# 3. Apply ConfigMap + Deployment
apply_manifests
# 4. Wait for pods
wait_for_pods "app=knoe-auth" 240
# 5. Schema
run_schema
# 6. Done
info ""
info "=== knoe-auth is ready ==="
info "Enrollment URL: ${AUTH_HOST}/auth/enroll?token=<invite_token>"
info ""
info "Next: create first admin invite:"
info " $0 invite chrisfu@prole.org"
info ""
show_status
}
# ── Dispatch ──────────────────────────────────────────────────────────────────
case "$COMMAND" in
initialize) cmd_initialize ;;
schema) run_schema ;;
invite) create_first_invite "${1:-}" ;;
status) show_status ;;
*)
echo "Usage: $0 {initialize|schema|invite EMAIL|status} [--context CTX] [--namespace NS]" >&2
exit 1
;;
esac