mirror of
https://github.com/dredx/prole.git
synced 2026-09-24 15:44:33 +00:00
feat: Kerberos identity management — PROLE.LOCAL realm, cross-realm trust, user provisioning
Kerberos is now the single source of truth for all knoe-system accounts. In-cluster KDC (PROLE.LOCAL): - Rename in-cluster KDC realm PROLE.ORG → PROLE.LOCAL to avoid collision with the myrddin.prole.org Samba AD DC which owns PROLE.ORG - Add [capaths] cross-realm trust block: PROLE.ORG (Samba AD) ↔ PROLE.LOCAL - Create admin@PROLE.LOCAL on KDC startup (master password) for UI login - Create guest@PROLE.LOCAL on KDC startup (read-only, PROLE_KDC_GUEST_PASSWORD) - Update prole-kdc-secrets.example.yaml with trust_shared_password and guest_password - Update prole-auth-kerberos ConfigMap realm/servicePrincipal to PROLE.LOCAL - Add PROLE_AUTH_ADMIN_PRINCIPALS and PROLE_KDC_GUEST_PASSWORD to prole-auth Deployment Database (knoe-db — k3s and GKE): - Add spec.managed.roles: admin (superuser), guest (login), developer (group) - Enable GSS pg_hba rules for both PROLE.ORG and PROLE.LOCAL realms - Add krb_server_keyfile parameter for postgres service principal keytab - Mount knoe-db-pg-keytab Secret via spec.additionalVolumes - Add demo schema in postInitSQL with guest read-only grants prole-auth (Spring Boot): - SessionUser: add List<String> groups field - AuthProperties: add adminPrincipals list (env: PROLE_AUTH_ADMIN_PRINCIPALS) - LoginController: stamp groups=["admin"] for configured admin principals at login - SessionTokenService: carry groups through TokenPayload; @JsonIgnoreProperties for backward compat with existing 2-field session cookies - VerifyController: emit X-Prole-Groups header on /auth/verify - grafana-proxy nginx: strip inbound X-Prole-Groups; capture and forward from auth Services: - ArgoCD (k3s + GKE): add g, admin, role:admin to argocd-rbac-cm policy.csv etc/init_knoe_users.sh (new): - Orchestrates full user provisioning: KDC principals, postgres keytab export, CNPG managed.roles patch, demo schema SQL, ArgoCD RBAC patch, Gitea and GitLab admin promotion via API - Actions: initialize | status | cleanup - Idempotent; sources prole_cfg.sh; follows existing init_*.sh style Cross-realm trust activation (myrddin side): samba-tool user add krbtgt_PROLELOCAL --random-password samba-tool user setpassword krbtgt_PROLELOCAL --newpassword=<trust_shared_password> samba-tool spn add "krbtgt/PROLE.LOCAL" krbtgt_PROLELOCAL -U administrator samba-tool user setexpiry krbtgt_PROLELOCAL --noexpiry Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
bb92ec3576
commit
872d715864
@ -41,6 +41,7 @@ data:
|
|||||||
policy.default: role:readonly
|
policy.default: role:readonly
|
||||||
policy.csv: |
|
policy.csv: |
|
||||||
g, ${BOOTSTRAP_ADMIN_EMAIL}, role:admin
|
g, ${BOOTSTRAP_ADMIN_EMAIL}, role:admin
|
||||||
|
g, admin, role:admin
|
||||||
|
|
||||||
---
|
---
|
||||||
# Secret referenced by oidc.config above ($oidc.argocd.clientSecret).
|
# Secret referenced by oidc.config above ($oidc.argocd.clientSecret).
|
||||||
|
|||||||
@ -61,6 +61,28 @@ spec:
|
|||||||
- GRANT USAGE ON SCHEMA storage TO anon;
|
- GRANT USAGE ON SCHEMA storage TO anon;
|
||||||
- GRANT USAGE ON SCHEMA graphql_public TO anon;
|
- GRANT USAGE ON SCHEMA graphql_public TO anon;
|
||||||
- GRANT anon TO authenticator;
|
- GRANT anon TO authenticator;
|
||||||
|
# demo schema for guest read-only access (evolves over time)
|
||||||
|
- CREATE SCHEMA IF NOT EXISTS demo;
|
||||||
|
- GRANT USAGE ON SCHEMA demo TO guest;
|
||||||
|
- ALTER DEFAULT PRIVILEGES IN SCHEMA demo GRANT SELECT ON TABLES TO guest;
|
||||||
|
|
||||||
|
managed:
|
||||||
|
roles:
|
||||||
|
- name: admin
|
||||||
|
ensure: present
|
||||||
|
login: true
|
||||||
|
superuser: true
|
||||||
|
comment: "Admin principal — full cluster database access"
|
||||||
|
- name: guest
|
||||||
|
ensure: present
|
||||||
|
login: true
|
||||||
|
superuser: false
|
||||||
|
comment: "Guest principal — read-only access to demo schema"
|
||||||
|
- name: developer
|
||||||
|
ensure: present
|
||||||
|
login: false
|
||||||
|
superuser: false
|
||||||
|
comment: "Developer group role — granted to knoe-system user accounts"
|
||||||
|
|
||||||
certificates:
|
certificates:
|
||||||
serverTLSSecret: knoe-db-tls
|
serverTLSSecret: knoe-db-tls
|
||||||
|
|||||||
@ -18,6 +18,7 @@ data:
|
|||||||
|
|
||||||
# Never trust inbound auth headers from clients.
|
# Never trust inbound auth headers from clients.
|
||||||
proxy_set_header X-WEBAUTH-USER "";
|
proxy_set_header X-WEBAUTH-USER "";
|
||||||
|
proxy_set_header X-Prole-Groups "";
|
||||||
|
|
||||||
location = /_auth_verify {
|
location = /_auth_verify {
|
||||||
internal;
|
internal;
|
||||||
@ -32,11 +33,13 @@ data:
|
|||||||
location / {
|
location / {
|
||||||
auth_request /_auth_verify;
|
auth_request /_auth_verify;
|
||||||
auth_request_set $prole_user $upstream_http_x_prole_user;
|
auth_request_set $prole_user $upstream_http_x_prole_user;
|
||||||
|
auth_request_set $prole_groups $upstream_http_x_prole_groups;
|
||||||
|
|
||||||
error_page 401 = @login;
|
error_page 401 = @login;
|
||||||
error_page 403 = @login;
|
error_page 403 = @login;
|
||||||
|
|
||||||
proxy_set_header X-WEBAUTH-USER $prole_user;
|
proxy_set_header X-WEBAUTH-USER $prole_user;
|
||||||
|
proxy_set_header X-Prole-Groups $prole_groups;
|
||||||
proxy_set_header Host $host;
|
proxy_set_header Host $host;
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||||
proxy_set_header X-Forwarded-Proto $scheme;
|
proxy_set_header X-Forwarded-Proto $scheme;
|
||||||
|
|||||||
@ -34,18 +34,22 @@ spec:
|
|||||||
shared_buffers: 256MB
|
shared_buffers: 256MB
|
||||||
pg_stat_statements.max: '10000'
|
pg_stat_statements.max: '10000'
|
||||||
pg_stat_statements.track: all
|
pg_stat_statements.track: all
|
||||||
|
krb_server_keyfile: '/etc/krb5keytab/pg.keytab'
|
||||||
shared_preload_libraries:
|
shared_preload_libraries:
|
||||||
- pg_stat_statements
|
- pg_stat_statements
|
||||||
- pg_tde
|
- pg_tde
|
||||||
pg_hba:
|
pg_hba:
|
||||||
# allow password access from remote hosts if environment is "#dev"
|
|
||||||
- local all postgres trust
|
- local all postgres trust
|
||||||
- local all prole scram-sha-256
|
- local all prole scram-sha-256
|
||||||
|
# GSS auth for cross-realm PROLE.ORG users (myrddin.prole.org Samba AD)
|
||||||
|
- hostgssenc all all all gss include_realm=1 krb_realm=PROLE.ORG
|
||||||
|
# GSS auth for in-cluster PROLE.LOCAL principals (admin, guest, service accounts)
|
||||||
|
- hostgssenc all all all gss include_realm=1 krb_realm=PROLE.LOCAL
|
||||||
|
# SCRAM fallback for non-Kerberos clients
|
||||||
- host all postgres all scram-sha-256
|
- host all postgres all scram-sha-256
|
||||||
- host prole knoe-db all scram-sha-256
|
- host prole knoe-db all scram-sha-256
|
||||||
- host all all all scram-sha-256
|
- host all all all scram-sha-256
|
||||||
- hostssl prole knoe-db all scram-sha-256
|
- hostssl prole knoe-db all scram-sha-256
|
||||||
# - host all all all gss include_realm=1 krb_realm=EXAMPLE.COM
|
|
||||||
|
|
||||||
bootstrap:
|
bootstrap:
|
||||||
initdb:
|
initdb:
|
||||||
@ -75,6 +79,37 @@ spec:
|
|||||||
- GRANT USAGE ON SCHEMA storage TO anon;
|
- GRANT USAGE ON SCHEMA storage TO anon;
|
||||||
- GRANT USAGE ON SCHEMA graphql_public TO anon;
|
- GRANT USAGE ON SCHEMA graphql_public TO anon;
|
||||||
- GRANT anon TO authenticator;
|
- GRANT anon TO authenticator;
|
||||||
|
# demo schema for guest read-only access (evolves over time)
|
||||||
|
- CREATE SCHEMA IF NOT EXISTS demo;
|
||||||
|
- GRANT USAGE ON SCHEMA demo TO guest;
|
||||||
|
- ALTER DEFAULT PRIVILEGES IN SCHEMA demo GRANT SELECT ON TABLES TO guest;
|
||||||
|
|
||||||
|
managed:
|
||||||
|
roles:
|
||||||
|
- name: admin
|
||||||
|
ensure: present
|
||||||
|
login: true
|
||||||
|
superuser: true
|
||||||
|
comment: "Kerberos admin principal — full cluster access"
|
||||||
|
- name: guest
|
||||||
|
ensure: present
|
||||||
|
login: true
|
||||||
|
superuser: false
|
||||||
|
comment: "Kerberos guest principal — read-only access to demo schema"
|
||||||
|
- name: developer
|
||||||
|
ensure: present
|
||||||
|
login: false
|
||||||
|
superuser: false
|
||||||
|
comment: "Developer group role — granted to knoe-system user accounts"
|
||||||
|
|
||||||
|
additionalVolumes:
|
||||||
|
- name: krb5keytab
|
||||||
|
secret:
|
||||||
|
secretName: knoe-db-pg-keytab
|
||||||
|
items:
|
||||||
|
- key: pg.keytab
|
||||||
|
path: pg.keytab
|
||||||
|
defaultMode: 0400
|
||||||
|
|
||||||
certificates:
|
certificates:
|
||||||
serverTLSSecret: knoe-db-tls
|
serverTLSSecret: knoe-db-tls
|
||||||
|
|||||||
@ -186,6 +186,9 @@ spec:
|
|||||||
name: prole-platform-config
|
name: prole-platform-config
|
||||||
key: frontdoorHost
|
key: frontdoorHost
|
||||||
optional: true
|
optional: true
|
||||||
|
# Comma-separated list of bare usernames granted admin group in OIDC tokens
|
||||||
|
- name: PROLE_AUTH_ADMIN_PRINCIPALS
|
||||||
|
value: "admin"
|
||||||
volumeMounts:
|
volumeMounts:
|
||||||
- name: keytab
|
- name: keytab
|
||||||
mountPath: /etc/prole/keytabs
|
mountPath: /etc/prole/keytabs
|
||||||
@ -216,6 +219,12 @@ spec:
|
|||||||
secretKeyRef:
|
secretKeyRef:
|
||||||
name: prole-kdc-secrets
|
name: prole-kdc-secrets
|
||||||
key: admin_password
|
key: admin_password
|
||||||
|
- name: PROLE_KDC_GUEST_PASSWORD
|
||||||
|
valueFrom:
|
||||||
|
secretKeyRef:
|
||||||
|
name: prole-kdc-secrets
|
||||||
|
key: guest_password
|
||||||
|
optional: true # auto-generated by init_knoe_users.sh if absent
|
||||||
ports:
|
ports:
|
||||||
- name: krb5-udp
|
- name: krb5-udp
|
||||||
containerPort: 88
|
containerPort: 88
|
||||||
|
|||||||
@ -4,5 +4,5 @@ metadata:
|
|||||||
name: prole-auth-kerberos
|
name: prole-auth-kerberos
|
||||||
data:
|
data:
|
||||||
# Kerberos HTTP service principal for SPNEGO (must match keytab)
|
# Kerberos HTTP service principal for SPNEGO (must match keytab)
|
||||||
servicePrincipal: "HTTP/api.prole.org@PROLE.ORG"
|
servicePrincipal: "HTTP/api.prole.org@PROLE.LOCAL"
|
||||||
realm: "PROLE.ORG"
|
realm: "PROLE.LOCAL"
|
||||||
|
|||||||
@ -5,21 +5,33 @@ metadata:
|
|||||||
data:
|
data:
|
||||||
krb5.conf: |
|
krb5.conf: |
|
||||||
[libdefaults]
|
[libdefaults]
|
||||||
default_realm = PROLE.ORG
|
default_realm = PROLE.LOCAL
|
||||||
dns_lookup_realm = false
|
dns_lookup_realm = false
|
||||||
dns_lookup_kdc = false
|
dns_lookup_kdc = false
|
||||||
|
|
||||||
[realms]
|
[realms]
|
||||||
PROLE.ORG = {
|
PROLE.LOCAL = {
|
||||||
kdc = 127.0.0.1
|
kdc = 127.0.0.1
|
||||||
admin_server = 127.0.0.1
|
admin_server = 127.0.0.1
|
||||||
}
|
}
|
||||||
|
PROLE.ORG = {
|
||||||
|
kdc = myrddin.prole.org
|
||||||
|
admin_server = myrddin.prole.org
|
||||||
|
}
|
||||||
|
|
||||||
|
[capaths]
|
||||||
|
PROLE.LOCAL = {
|
||||||
|
PROLE.ORG = .
|
||||||
|
}
|
||||||
|
PROLE.ORG = {
|
||||||
|
PROLE.LOCAL = .
|
||||||
|
}
|
||||||
kdc.conf: |
|
kdc.conf: |
|
||||||
[kdcdefaults]
|
[kdcdefaults]
|
||||||
kdc_ports = 88
|
kdc_ports = 88
|
||||||
kdc_tcp_ports = 88
|
kdc_tcp_ports = 88
|
||||||
[realms]
|
[realms]
|
||||||
PROLE.ORG = {
|
PROLE.LOCAL = {
|
||||||
database_name = /var/lib/krb5kdc/principal
|
database_name = /var/lib/krb5kdc/principal
|
||||||
admin_keytab = FILE:/etc/krb5kdc/kadm5.keytab
|
admin_keytab = FILE:/etc/krb5kdc/kadm5.keytab
|
||||||
acl_file = /etc/krb5kdc/kadm5.acl
|
acl_file = /etc/krb5kdc/kadm5.acl
|
||||||
@ -29,7 +41,7 @@ data:
|
|||||||
default_principal_flags = +preauth
|
default_principal_flags = +preauth
|
||||||
}
|
}
|
||||||
kadm5.acl: |
|
kadm5.acl: |
|
||||||
admin/admin@PROLE.ORG *
|
admin/admin@PROLE.LOCAL *
|
||||||
entrypoint.sh: |
|
entrypoint.sh: |
|
||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
@ -81,6 +93,18 @@ data:
|
|||||||
kadmin.local -q "addprinc -pw ${PROLE_KDC_ADMIN_PASSWORD} ${admin_principal}"
|
kadmin.local -q "addprinc -pw ${PROLE_KDC_ADMIN_PASSWORD} ${admin_principal}"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# admin user principal for UI login (uses master password)
|
||||||
|
if ! kadmin.local -q "get_principal admin@${realm}" >/dev/null 2>&1; then
|
||||||
|
echo "Creating admin user principal admin@${realm}..."
|
||||||
|
kadmin.local -q "addprinc -pw ${PROLE_KDC_MASTER_PASSWORD} admin@${realm}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# guest user principal (read-only)
|
||||||
|
if ! kadmin.local -q "get_principal guest@${realm}" >/dev/null 2>&1; then
|
||||||
|
echo "Creating guest user principal guest@${realm}..."
|
||||||
|
kadmin.local -q "addprinc -pw ${PROLE_KDC_GUEST_PASSWORD:-changeme} guest@${realm}"
|
||||||
|
fi
|
||||||
|
|
||||||
echo "Starting krb5kdc and kadmind ..."
|
echo "Starting krb5kdc and kadmind ..."
|
||||||
krb5kdc -n &
|
krb5kdc -n &
|
||||||
sleep 0.5
|
sleep 0.5
|
||||||
|
|||||||
@ -7,3 +7,9 @@ stringData:
|
|||||||
# Strong random values (do not commit real secrets)
|
# Strong random values (do not commit real secrets)
|
||||||
master_password: "CHANGE_ME"
|
master_password: "CHANGE_ME"
|
||||||
admin_password: "CHANGE_ME"
|
admin_password: "CHANGE_ME"
|
||||||
|
# Shared secret used for cross-realm trust with myrddin.prole.org PROLE.ORG realm
|
||||||
|
trust_shared_password: "CHANGE_ME"
|
||||||
|
# myrddin.prole.org Samba AD admin password (for reciprocal trust principal creation)
|
||||||
|
trust_password: "CHANGE_ME"
|
||||||
|
# Password for guest@PROLE.LOCAL read-only principal (auto-generated by init_knoe_users.sh if absent)
|
||||||
|
guest_password: "CHANGE_ME"
|
||||||
|
|||||||
535
etc/init_knoe_users.sh
Executable file
535
etc/init_knoe_users.sh
Executable file
@ -0,0 +1,535 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
# init_knoe_users.sh
|
||||||
|
# Purpose:
|
||||||
|
# - Provision Kerberos principals and database accounts for knoe-system users
|
||||||
|
# - Creates: admin@PROLE.LOCAL (master password), guest@PROLE.LOCAL (read-only),
|
||||||
|
# postgres service principal (keytab for GSS auth), developer group role
|
||||||
|
# - Cross-realm trust with myrddin.prole.org PROLE.ORG is activated via
|
||||||
|
# PROLE_KDC_TRUST_REALM=PROLE.ORG in init_kdc.sh / init_knoe_auth.sh
|
||||||
|
# - Sets up service admin access: ArgoCD RBAC, Gitea, GitLab
|
||||||
|
#
|
||||||
|
# Prerequisites:
|
||||||
|
# init_kdc.sh initialize (with PROLE_KDC_REALM=PROLE.LOCAL,
|
||||||
|
# PROLE_KDC_TRUST_REALM=PROLE.ORG,
|
||||||
|
# KRB5_KDC=myrddin.prole.org)
|
||||||
|
# init_cnpg_backup.sh (CNPG cluster must exist)
|
||||||
|
# init_argocd.sh (ArgoCD must be running)
|
||||||
|
#
|
||||||
|
# Usage:
|
||||||
|
# ./init_knoe_users.sh initialize # create/update all user resources
|
||||||
|
# ./init_knoe_users.sh status # show current state
|
||||||
|
# ./init_knoe_users.sh cleanup # remove managed secrets/patches
|
||||||
|
|
||||||
|
SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
|
||||||
|
|
||||||
|
# shellcheck disable=SC1090
|
||||||
|
source "$SCRIPT_DIR/prole_cfg.sh"
|
||||||
|
|
||||||
|
if [[ "${1:-}" == "--mode" || "${1:-}" == "-m" ]]; then
|
||||||
|
prole_set_mode "${2:-}"
|
||||||
|
shift 2
|
||||||
|
elif [[ "${1:-}" == --mode=* || "${1:-}" == -m=* ]]; then
|
||||||
|
prole_set_mode "${1#*=}"
|
||||||
|
shift
|
||||||
|
fi
|
||||||
|
|
||||||
|
prole_ensure_kubeconfig >/dev/null 2>&1 || true
|
||||||
|
ensure_kube_context || exit 1
|
||||||
|
|
||||||
|
ACTION="${1:-initialize}"
|
||||||
|
|
||||||
|
# ── Configurable variables ─────────────────────────────────────────────────
|
||||||
|
KNOE_USERS_NAMESPACE="${KNOE_USERS_NAMESPACE:-${SERVICE_NAMESPACE:-knoe-system}}"
|
||||||
|
KNOE_KDC_NAMESPACE="${KNOE_KDC_NAMESPACE:-${KNOE_USERS_NAMESPACE}}"
|
||||||
|
KNOE_DB_NAMESPACE="${KNOE_DB_NAMESPACE:-${DATABASE_NAMESPACE:-knoe-db}}"
|
||||||
|
KNOE_DB_CLUSTER="${KNOE_DB_CLUSTER:-knoe-db}"
|
||||||
|
KNOE_ADMIN_PRINCIPAL="${KNOE_ADMIN_PRINCIPAL:-admin}"
|
||||||
|
PROLE_KDC_REALM="${PROLE_KDC_REALM:-PROLE.LOCAL}"
|
||||||
|
GITEA_HOST="${GITEA_HOST:-git.prole.org}"
|
||||||
|
GITLAB_HOST="${GITLAB_HOST:-gitlab.prole.org}"
|
||||||
|
GITEA_NAMESPACE="${GITEA_NAMESPACE:-gitea}"
|
||||||
|
GITLAB_NAMESPACE="${GITLAB_NAMESPACE:-gitlab}"
|
||||||
|
ARGOCD_NAMESPACE="${ARGOCD_NAMESPACE:-argocd}"
|
||||||
|
# Provided externally or resolved from prole-kdc-secrets
|
||||||
|
PROLE_KDC_MASTER_PASSWORD="${PROLE_KDC_MASTER_PASSWORD:-}"
|
||||||
|
KNOE_GUEST_PASSWORD="${KNOE_GUEST_PASSWORD:-}"
|
||||||
|
# Optional: personal access tokens for service admin promotion
|
||||||
|
GITEA_ADMIN_TOKEN="${GITEA_ADMIN_TOKEN:-}"
|
||||||
|
GITLAB_ADMIN_TOKEN="${GITLAB_ADMIN_TOKEN:-}"
|
||||||
|
|
||||||
|
log() { printf '[INFO] %s\n' "$*"; }
|
||||||
|
err() { printf '[ERROR] %s\n' "$*" >&2; }
|
||||||
|
warn() { printf '[WARN] %s\n' "$*" >&2; }
|
||||||
|
die() { err "$*"; exit 1; }
|
||||||
|
|
||||||
|
ensure_tools() {
|
||||||
|
for t in kubectl curl python3; do
|
||||||
|
command -v "$t" >/dev/null || die "Missing required tool: $t"
|
||||||
|
done
|
||||||
|
}
|
||||||
|
|
||||||
|
b64_decode() {
|
||||||
|
if base64 --decode </dev/null >/dev/null 2>&1; then
|
||||||
|
base64 --decode
|
||||||
|
elif base64 -d </dev/null >/dev/null 2>&1; then
|
||||||
|
base64 -d
|
||||||
|
else
|
||||||
|
base64 -D
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
gen_password() {
|
||||||
|
if command -v openssl >/dev/null 2>&1; then
|
||||||
|
openssl rand -base64 18
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
python3 - <<'PY'
|
||||||
|
import secrets, string
|
||||||
|
alphabet = string.ascii_letters + string.digits
|
||||||
|
print(''.join(secrets.choice(alphabet) for _ in range(24)))
|
||||||
|
PY
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── KDC helpers ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
get_kdc_pod() {
|
||||||
|
# The kdc container lives inside the prole-auth deployment pod
|
||||||
|
kubectl -n "$KNOE_KDC_NAMESPACE" get pods \
|
||||||
|
-l "app=prole-auth" \
|
||||||
|
-o jsonpath='{.items[0].metadata.name}' 2>/dev/null || true
|
||||||
|
}
|
||||||
|
|
||||||
|
kdc_principal_exists() {
|
||||||
|
local pod="$1" principal="$2"
|
||||||
|
kubectl -n "$KNOE_KDC_NAMESPACE" exec "$pod" -c kdc -- \
|
||||||
|
kadmin.local -q "get_principal ${principal}" >/dev/null 2>&1
|
||||||
|
}
|
||||||
|
|
||||||
|
kdc_addprinc() {
|
||||||
|
local pod="$1" principal="$2" password="$3"
|
||||||
|
kubectl -n "$KNOE_KDC_NAMESPACE" exec "$pod" -c kdc -- \
|
||||||
|
kadmin.local -q "addprinc -pw ${password} ${principal}"
|
||||||
|
}
|
||||||
|
|
||||||
|
kdc_addprinc_randkey() {
|
||||||
|
local pod="$1" principal="$2"
|
||||||
|
kubectl -n "$KNOE_KDC_NAMESPACE" exec "$pod" -c kdc -- \
|
||||||
|
kadmin.local -q "addprinc -randkey ${principal}"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── CNPG helpers ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
get_cnpg_primary() {
|
||||||
|
local pod=""
|
||||||
|
# CNPG 1.20+: instanceRole label
|
||||||
|
pod=$(kubectl -n "$KNOE_DB_NAMESPACE" get pods \
|
||||||
|
-l "cnpg.io/cluster=${KNOE_DB_CLUSTER},cnpg.io/instanceRole=primary" \
|
||||||
|
-o jsonpath='{.items[0].metadata.name}' 2>/dev/null || true)
|
||||||
|
if [[ -z "$pod" ]]; then
|
||||||
|
# Older label set
|
||||||
|
pod=$(kubectl -n "$KNOE_DB_NAMESPACE" get pods \
|
||||||
|
-l "cnpg.io/cluster=${KNOE_DB_CLUSTER},role=primary" \
|
||||||
|
-o jsonpath='{.items[0].metadata.name}' 2>/dev/null || true)
|
||||||
|
fi
|
||||||
|
printf '%s' "$pod"
|
||||||
|
}
|
||||||
|
|
||||||
|
cnpg_psql() {
|
||||||
|
local pod="$1"; shift
|
||||||
|
kubectl -n "$KNOE_DB_NAMESPACE" exec "$pod" -- \
|
||||||
|
psql -U postgres -d postgres "$@"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Main: initialize ───────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
initialize() {
|
||||||
|
ensure_tools
|
||||||
|
|
||||||
|
log "=== knoe-system user provisioning ==="
|
||||||
|
|
||||||
|
# 1. Resolve passwords
|
||||||
|
if [[ -z "$PROLE_KDC_MASTER_PASSWORD" ]]; then
|
||||||
|
PROLE_KDC_MASTER_PASSWORD=$(
|
||||||
|
kubectl -n "$KNOE_KDC_NAMESPACE" get secret prole-kdc-secrets \
|
||||||
|
-o jsonpath='{.data.master_password}' 2>/dev/null | b64_decode || true
|
||||||
|
)
|
||||||
|
fi
|
||||||
|
[[ -z "$PROLE_KDC_MASTER_PASSWORD" ]] && die "PROLE_KDC_MASTER_PASSWORD is required (set env or store in prole-kdc-secrets)"
|
||||||
|
|
||||||
|
if [[ -z "$KNOE_GUEST_PASSWORD" ]]; then
|
||||||
|
KNOE_GUEST_PASSWORD=$(
|
||||||
|
kubectl -n "$KNOE_KDC_NAMESPACE" get secret prole-kdc-secrets \
|
||||||
|
-o jsonpath='{.data.guest_password}' 2>/dev/null | b64_decode || true
|
||||||
|
)
|
||||||
|
fi
|
||||||
|
if [[ -z "$KNOE_GUEST_PASSWORD" ]]; then
|
||||||
|
KNOE_GUEST_PASSWORD=$(gen_password)
|
||||||
|
log "Auto-generated guest password — storing in prole-kdc-secrets ..."
|
||||||
|
kubectl -n "$KNOE_KDC_NAMESPACE" create secret generic prole-kdc-secrets \
|
||||||
|
--from-literal=guest_password="$KNOE_GUEST_PASSWORD" \
|
||||||
|
--dry-run=client -o yaml | kubectl apply -f - >/dev/null
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 2. Locate KDC pod
|
||||||
|
local kdc_pod
|
||||||
|
kdc_pod=$(get_kdc_pod)
|
||||||
|
[[ -z "$kdc_pod" ]] && die "No KDC pod found in namespace ${KNOE_KDC_NAMESPACE} (label app=prole-auth). Is the cluster up?"
|
||||||
|
log "KDC pod: $kdc_pod"
|
||||||
|
|
||||||
|
# 3. Create admin@PROLE.LOCAL (UI login with master password)
|
||||||
|
local admin_princ="${KNOE_ADMIN_PRINCIPAL}@${PROLE_KDC_REALM}"
|
||||||
|
if kdc_principal_exists "$kdc_pod" "$admin_princ"; then
|
||||||
|
log "${admin_princ} already exists"
|
||||||
|
else
|
||||||
|
log "Creating ${admin_princ} ..."
|
||||||
|
kdc_addprinc "$kdc_pod" "$admin_princ" "$PROLE_KDC_MASTER_PASSWORD"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 4. Create guest@PROLE.LOCAL (read-only)
|
||||||
|
local guest_princ="guest@${PROLE_KDC_REALM}"
|
||||||
|
if kdc_principal_exists "$kdc_pod" "$guest_princ"; then
|
||||||
|
log "${guest_princ} already exists"
|
||||||
|
else
|
||||||
|
log "Creating ${guest_princ} ..."
|
||||||
|
kdc_addprinc "$kdc_pod" "$guest_princ" "$KNOE_GUEST_PASSWORD"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 5. Ensure BOTH cross-realm trust principals exist in the MIT KDC
|
||||||
|
# Direction A: krbtgt/PROLE.ORG@PROLE.LOCAL — cluster users → AD services
|
||||||
|
# (created by init_kdc.sh PROLE_KDC_TRUST_REALM mechanism)
|
||||||
|
# Direction B: krbtgt/PROLE.LOCAL@PROLE.ORG — AD users → cluster services ← THIS IS WHAT WE NEED
|
||||||
|
local trust_princ_in="krbtgt/PROLE.ORG@${PROLE_KDC_REALM}"
|
||||||
|
local trust_princ_out="krbtgt/${PROLE_KDC_REALM}@PROLE.ORG"
|
||||||
|
local trust_shared_pw
|
||||||
|
trust_shared_pw=$(kubectl -n "$KNOE_KDC_NAMESPACE" get secret prole-kdc-secrets \
|
||||||
|
-o jsonpath='{.data.trust_shared_password}' 2>/dev/null | b64_decode || true)
|
||||||
|
if [[ -z "$trust_shared_pw" ]]; then
|
||||||
|
warn "trust_shared_password not set in prole-kdc-secrets; cross-realm trust principals will not be created"
|
||||||
|
warn "Add trust_shared_password to prole-kdc-secrets and re-run"
|
||||||
|
else
|
||||||
|
# Direction A (init_kdc.sh may already have created this)
|
||||||
|
if kdc_principal_exists "$kdc_pod" "$trust_princ_in"; then
|
||||||
|
log "${trust_princ_in} already exists"
|
||||||
|
else
|
||||||
|
log "Creating cross-realm principal ${trust_princ_in} ..."
|
||||||
|
kdc_addprinc "$kdc_pod" "$trust_princ_in" "$trust_shared_pw"
|
||||||
|
fi
|
||||||
|
# Direction B — the principal that allows AD users to reach cluster services
|
||||||
|
if kdc_principal_exists "$kdc_pod" "$trust_princ_out"; then
|
||||||
|
log "${trust_princ_out} already exists"
|
||||||
|
else
|
||||||
|
log "Creating cross-realm principal ${trust_princ_out} ..."
|
||||||
|
kdc_addprinc "$kdc_pod" "$trust_princ_out" "$trust_shared_pw"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 6. Create postgres service principal + export keytab
|
||||||
|
local pg_spn="postgres/knoe-db-rw.${KNOE_DB_NAMESPACE}.svc.cluster.local@${PROLE_KDC_REALM}"
|
||||||
|
if kdc_principal_exists "$kdc_pod" "$pg_spn"; then
|
||||||
|
log "${pg_spn} already exists"
|
||||||
|
else
|
||||||
|
log "Creating service principal ${pg_spn} ..."
|
||||||
|
kdc_addprinc_randkey "$kdc_pod" "$pg_spn"
|
||||||
|
fi
|
||||||
|
|
||||||
|
log "Exporting keytab for ${pg_spn} ..."
|
||||||
|
kubectl -n "$KNOE_KDC_NAMESPACE" exec "$kdc_pod" -c kdc -- \
|
||||||
|
kadmin.local -q "ktadd -k /tmp/pg.keytab -norandkey ${pg_spn}" >/dev/null
|
||||||
|
|
||||||
|
log "Storing keytab in Secret knoe-db-pg-keytab ..."
|
||||||
|
local keytab_b64
|
||||||
|
keytab_b64=$(kubectl -n "$KNOE_KDC_NAMESPACE" exec "$kdc_pod" -c kdc -- \
|
||||||
|
sh -c 'base64 /tmp/pg.keytab | tr -d "\\n"')
|
||||||
|
# Write secret into the DB namespace where CNPG mounts it
|
||||||
|
kubectl -n "$KNOE_DB_NAMESPACE" create secret generic knoe-db-pg-keytab \
|
||||||
|
--from-literal=pg.keytab="$(printf '%s' "$keytab_b64" | b64_decode)" \
|
||||||
|
--dry-run=client -o yaml | kubectl apply -f - >/dev/null
|
||||||
|
log "knoe-db-pg-keytab stored in namespace ${KNOE_DB_NAMESPACE}"
|
||||||
|
|
||||||
|
# 6. Patch CNPG cluster with managed.roles (idempotent merge)
|
||||||
|
log "Patching CNPG cluster ${KNOE_DB_CLUSTER} managed.roles ..."
|
||||||
|
kubectl -n "$KNOE_DB_NAMESPACE" patch cluster "$KNOE_DB_CLUSTER" \
|
||||||
|
--type=merge --patch '{
|
||||||
|
"spec": {
|
||||||
|
"managed": {
|
||||||
|
"roles": [
|
||||||
|
{
|
||||||
|
"name": "admin",
|
||||||
|
"ensure": "present",
|
||||||
|
"login": true,
|
||||||
|
"superuser": true,
|
||||||
|
"comment": "Kerberos admin principal — full cluster access"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "guest",
|
||||||
|
"ensure": "present",
|
||||||
|
"login": true,
|
||||||
|
"superuser": false,
|
||||||
|
"comment": "Kerberos guest principal — read-only access to demo schema"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "developer",
|
||||||
|
"ensure": "present",
|
||||||
|
"login": false,
|
||||||
|
"superuser": false,
|
||||||
|
"comment": "Developer group role — granted to knoe-system user accounts"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}'
|
||||||
|
|
||||||
|
# 7. Locate CNPG primary and run SQL
|
||||||
|
local primary
|
||||||
|
primary=$(get_cnpg_primary)
|
||||||
|
if [[ -z "$primary" ]]; then
|
||||||
|
warn "CNPG primary pod not found — skipping SQL setup (run again once cluster is ready)"
|
||||||
|
else
|
||||||
|
log "CNPG primary pod: $primary"
|
||||||
|
log "Setting up demo schema and role grants ..."
|
||||||
|
cnpg_psql "$primary" <<'SQL'
|
||||||
|
-- demo schema for guest read-only access (evolves over time)
|
||||||
|
CREATE SCHEMA IF NOT EXISTS demo;
|
||||||
|
|
||||||
|
-- Ensure roles exist (CNPG managed.roles may not have reconciled yet)
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
IF NOT EXISTS (SELECT FROM pg_roles WHERE rolname = 'admin') THEN
|
||||||
|
CREATE ROLE admin LOGIN SUPERUSER;
|
||||||
|
ELSE
|
||||||
|
ALTER ROLE admin SUPERUSER LOGIN;
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
|
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
IF NOT EXISTS (SELECT FROM pg_roles WHERE rolname = 'guest') THEN
|
||||||
|
CREATE ROLE guest LOGIN;
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
|
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
IF NOT EXISTS (SELECT FROM pg_roles WHERE rolname = 'developer') THEN
|
||||||
|
CREATE ROLE developer NOLOGIN;
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
|
|
||||||
|
-- guest: read-only on demo schema
|
||||||
|
GRANT USAGE ON SCHEMA demo TO guest;
|
||||||
|
GRANT SELECT ON ALL TABLES IN SCHEMA demo TO guest;
|
||||||
|
ALTER DEFAULT PRIVILEGES IN SCHEMA demo GRANT SELECT ON TABLES TO guest;
|
||||||
|
|
||||||
|
-- developer: read/write access on public and demo schemas
|
||||||
|
GRANT USAGE ON SCHEMA public TO developer;
|
||||||
|
GRANT USAGE ON SCHEMA demo TO developer;
|
||||||
|
GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO developer;
|
||||||
|
GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA demo TO developer;
|
||||||
|
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO developer;
|
||||||
|
ALTER DEFAULT PRIVILEGES IN SCHEMA demo GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO developer;
|
||||||
|
SQL
|
||||||
|
log "SQL setup complete"
|
||||||
|
fi
|
||||||
|
|
||||||
|
# 8. ArgoCD RBAC patch
|
||||||
|
patch_argocd_rbac
|
||||||
|
|
||||||
|
# 9. Gitea admin promotion
|
||||||
|
promote_gitea_admin
|
||||||
|
|
||||||
|
# 10. GitLab admin promotion
|
||||||
|
promote_gitlab_admin
|
||||||
|
|
||||||
|
log "=== Initialization complete ==="
|
||||||
|
log ""
|
||||||
|
log "Next steps:"
|
||||||
|
log " 1. On myrddin.prole.org: add krbtgt/PROLE.LOCAL@PROLE.ORG trust principal"
|
||||||
|
log " (samba-tool domain trust or equivalent, using trust_shared_password from prole-kdc-secrets)"
|
||||||
|
log " 2. Confirm Grafana auth.proxy configured with: headers = Role:X-Prole-Groups"
|
||||||
|
log " 3. After admin logs in to Gitea/GitLab for the first time, re-run: $0 initialize"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── ArgoCD RBAC ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
patch_argocd_rbac() {
|
||||||
|
if ! kubectl -n "$ARGOCD_NAMESPACE" get configmap argocd-rbac-cm >/dev/null 2>&1; then
|
||||||
|
warn "argocd-rbac-cm not found in namespace ${ARGOCD_NAMESPACE}; skipping ArgoCD RBAC patch"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
local admin_line="g, ${KNOE_ADMIN_PRINCIPAL}, role:admin"
|
||||||
|
local current_csv
|
||||||
|
current_csv=$(kubectl -n "$ARGOCD_NAMESPACE" get configmap argocd-rbac-cm \
|
||||||
|
-o jsonpath='{.data.policy\.csv}' 2>/dev/null || true)
|
||||||
|
|
||||||
|
if printf '%s' "$current_csv" | grep -qF "$admin_line"; then
|
||||||
|
log "ArgoCD RBAC already contains admin entry"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
local new_csv="${current_csv}"$'\n'"${admin_line}"
|
||||||
|
local escaped_csv
|
||||||
|
escaped_csv=$(python3 -c 'import sys,json; print(json.dumps(sys.stdin.read()))' <<< "$new_csv")
|
||||||
|
|
||||||
|
kubectl -n "$ARGOCD_NAMESPACE" patch configmap argocd-rbac-cm \
|
||||||
|
--type=merge \
|
||||||
|
--patch "{\"data\":{\"policy.csv\":${escaped_csv}}}" >/dev/null
|
||||||
|
log "ArgoCD RBAC: added '${admin_line}'"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Gitea admin promotion ───────────────────────────────────────────────────
|
||||||
|
|
||||||
|
promote_gitea_admin() {
|
||||||
|
local token="$GITEA_ADMIN_TOKEN"
|
||||||
|
if [[ -z "$token" ]]; then
|
||||||
|
token=$(kubectl -n "$GITEA_NAMESPACE" get secret gitea-admin-token \
|
||||||
|
-o jsonpath='{.data.token}' 2>/dev/null | b64_decode || true)
|
||||||
|
fi
|
||||||
|
if [[ -z "$token" ]]; then
|
||||||
|
warn "No GITEA_ADMIN_TOKEN available — skipping Gitea admin promotion"
|
||||||
|
warn " Manually: curl -X PATCH https://${GITEA_HOST}/api/v1/admin/users/${KNOE_ADMIN_PRINCIPAL} \\"
|
||||||
|
warn " -H 'Authorization: token <token>' -H 'Content-Type: application/json' \\"
|
||||||
|
warn " -d '{\"admin\":true,\"login_name\":\"${KNOE_ADMIN_PRINCIPAL}\",\"source_id\":0}'"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
local http_code
|
||||||
|
http_code=$(curl -s -o /dev/null -w '%{http_code}' \
|
||||||
|
-H "Authorization: token ${token}" \
|
||||||
|
"https://${GITEA_HOST}/api/v1/users/${KNOE_ADMIN_PRINCIPAL}" 2>/dev/null || true)
|
||||||
|
|
||||||
|
if [[ "$http_code" == "404" ]]; then
|
||||||
|
warn "Gitea: user '${KNOE_ADMIN_PRINCIPAL}' not found (must log in first)"
|
||||||
|
warn " Re-run '$0 initialize' after first login"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
http_code=$(curl -s -o /dev/null -w '%{http_code}' \
|
||||||
|
-X PATCH \
|
||||||
|
-H "Authorization: token ${token}" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d "{\"admin\":true,\"login_name\":\"${KNOE_ADMIN_PRINCIPAL}\",\"source_id\":0}" \
|
||||||
|
"https://${GITEA_HOST}/api/v1/admin/users/${KNOE_ADMIN_PRINCIPAL}" 2>/dev/null || true)
|
||||||
|
|
||||||
|
if [[ "$http_code" =~ ^2 ]]; then
|
||||||
|
log "Gitea: '${KNOE_ADMIN_PRINCIPAL}' promoted to admin"
|
||||||
|
else
|
||||||
|
warn "Gitea admin promotion returned HTTP ${http_code}"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── GitLab admin promotion ─────────────────────────────────────────────────
|
||||||
|
|
||||||
|
promote_gitlab_admin() {
|
||||||
|
local token="$GITLAB_ADMIN_TOKEN"
|
||||||
|
if [[ -z "$token" ]]; then
|
||||||
|
warn "No GITLAB_ADMIN_TOKEN (personal access token) available — skipping GitLab admin promotion"
|
||||||
|
warn " Set GITLAB_ADMIN_TOKEN and re-run, or use:"
|
||||||
|
warn " kubectl exec -n ${GITLAB_NAMESPACE} <rails-pod> -- gitlab-rails runner \\"
|
||||||
|
warn " \"User.find_by_username('${KNOE_ADMIN_PRINCIPAL}')&.update(admin: true)\""
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Look up user ID by username
|
||||||
|
local user_json user_id
|
||||||
|
user_json=$(curl -s \
|
||||||
|
-H "PRIVATE-TOKEN: ${token}" \
|
||||||
|
"https://${GITLAB_HOST}/api/v4/users?username=${KNOE_ADMIN_PRINCIPAL}" 2>/dev/null || true)
|
||||||
|
user_id=$(python3 -c \
|
||||||
|
'import sys,json; u=json.load(sys.stdin); print(u[0]["id"] if u else "")' \
|
||||||
|
<<< "$user_json" 2>/dev/null || true)
|
||||||
|
|
||||||
|
if [[ -z "$user_id" ]]; then
|
||||||
|
warn "GitLab: user '${KNOE_ADMIN_PRINCIPAL}' not found (must log in first)"
|
||||||
|
warn " Re-run '$0 initialize' after first login"
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
local http_code
|
||||||
|
http_code=$(curl -s -o /dev/null -w '%{http_code}' \
|
||||||
|
-X PUT \
|
||||||
|
-H "PRIVATE-TOKEN: ${token}" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"admin":true}' \
|
||||||
|
"https://${GITLAB_HOST}/api/v4/users/${user_id}" 2>/dev/null || true)
|
||||||
|
|
||||||
|
if [[ "$http_code" =~ ^2 ]]; then
|
||||||
|
log "GitLab: '${KNOE_ADMIN_PRINCIPAL}' promoted to admin"
|
||||||
|
else
|
||||||
|
warn "GitLab admin promotion returned HTTP ${http_code}"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Status ─────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
show_status() {
|
||||||
|
log "=== KDC Principals ==="
|
||||||
|
local kdc_pod
|
||||||
|
kdc_pod=$(get_kdc_pod)
|
||||||
|
if [[ -n "$kdc_pod" ]]; then
|
||||||
|
kubectl -n "$KNOE_KDC_NAMESPACE" exec "$kdc_pod" -c kdc -- \
|
||||||
|
kadmin.local -q "list_principals" 2>/dev/null \
|
||||||
|
| grep -E "^(admin|guest|developer|postgres/|krbtgt/)" || true
|
||||||
|
else
|
||||||
|
warn "No KDC pod found"
|
||||||
|
fi
|
||||||
|
|
||||||
|
log ""
|
||||||
|
log "=== CNPG Managed Roles ==="
|
||||||
|
kubectl -n "$KNOE_DB_NAMESPACE" get cluster "$KNOE_DB_CLUSTER" \
|
||||||
|
-o jsonpath='{.spec.managed.roles}' 2>/dev/null \
|
||||||
|
| python3 -m json.tool 2>/dev/null || warn "Could not read managed.roles"
|
||||||
|
|
||||||
|
log ""
|
||||||
|
log "=== PostgreSQL Roles ==="
|
||||||
|
local primary
|
||||||
|
primary=$(get_cnpg_primary)
|
||||||
|
if [[ -n "$primary" ]]; then
|
||||||
|
cnpg_psql "$primary" -c "\\du admin guest developer" 2>/dev/null || true
|
||||||
|
else
|
||||||
|
warn "No CNPG primary pod found"
|
||||||
|
fi
|
||||||
|
|
||||||
|
log ""
|
||||||
|
log "=== Keytab Secret ==="
|
||||||
|
if kubectl -n "$KNOE_DB_NAMESPACE" get secret knoe-db-pg-keytab >/dev/null 2>&1; then
|
||||||
|
log "knoe-db-pg-keytab exists in namespace ${KNOE_DB_NAMESPACE}"
|
||||||
|
else
|
||||||
|
warn "knoe-db-pg-keytab NOT FOUND in namespace ${KNOE_DB_NAMESPACE}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
log ""
|
||||||
|
log "=== ArgoCD RBAC ==="
|
||||||
|
kubectl -n "$ARGOCD_NAMESPACE" get configmap argocd-rbac-cm \
|
||||||
|
-o jsonpath='{.data.policy\.csv}' 2>/dev/null || warn "argocd-rbac-cm not found"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Cleanup ────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
cleanup() {
|
||||||
|
warn "cleanup removes the pg keytab secret and clears managed.roles from CNPG."
|
||||||
|
warn "KDC principals are NOT deleted (use kadmin.local to remove them manually)."
|
||||||
|
|
||||||
|
kubectl -n "$KNOE_DB_NAMESPACE" delete secret knoe-db-pg-keytab \
|
||||||
|
--ignore-not-found >/dev/null && log "Deleted knoe-db-pg-keytab"
|
||||||
|
|
||||||
|
kubectl -n "$KNOE_DB_NAMESPACE" patch cluster "$KNOE_DB_CLUSTER" \
|
||||||
|
--type=merge --patch '{"spec":{"managed":null}}' >/dev/null 2>&1 \
|
||||||
|
&& log "Cleared managed.roles from cluster ${KNOE_DB_CLUSTER}" || true
|
||||||
|
}
|
||||||
|
|
||||||
|
# ── Dispatch ───────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
case "$ACTION" in
|
||||||
|
initialize|init) initialize ;;
|
||||||
|
status) show_status ;;
|
||||||
|
cleanup) cleanup ;;
|
||||||
|
*)
|
||||||
|
err "Unknown action: ${ACTION}"
|
||||||
|
err "Usage: $(basename "$0") [initialize|status|cleanup]"
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
@ -1,6 +1,8 @@
|
|||||||
package org.prole.auth.config;
|
package org.prole.auth.config;
|
||||||
|
|
||||||
import java.time.Duration;
|
import java.time.Duration;
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||||
|
|
||||||
@ -13,6 +15,7 @@ public class AuthProperties {
|
|||||||
private String sessionSecret = "";
|
private String sessionSecret = "";
|
||||||
private String emailDomain = "prole.org";
|
private String emailDomain = "prole.org";
|
||||||
private boolean formEnabled = false;
|
private boolean formEnabled = false;
|
||||||
|
private List<String> adminPrincipals = new ArrayList<>();
|
||||||
|
|
||||||
public boolean isEnabled() {
|
public boolean isEnabled() {
|
||||||
return enabled;
|
return enabled;
|
||||||
@ -69,4 +72,12 @@ public class AuthProperties {
|
|||||||
public void setFormEnabled(boolean formEnabled) {
|
public void setFormEnabled(boolean formEnabled) {
|
||||||
this.formEnabled = formEnabled;
|
this.formEnabled = formEnabled;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public List<String> getAdminPrincipals() {
|
||||||
|
return adminPrincipals;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setAdminPrincipals(List<String> adminPrincipals) {
|
||||||
|
this.adminPrincipals = adminPrincipals;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
package org.prole.auth.session;
|
package org.prole.auth.session;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.security.MessageDigest;
|
import java.security.MessageDigest;
|
||||||
@ -7,6 +8,7 @@ import java.time.Clock;
|
|||||||
import java.time.Duration;
|
import java.time.Duration;
|
||||||
import java.time.Instant;
|
import java.time.Instant;
|
||||||
import java.util.Base64;
|
import java.util.Base64;
|
||||||
|
import java.util.List;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import javax.crypto.Mac;
|
import javax.crypto.Mac;
|
||||||
import javax.crypto.spec.SecretKeySpec;
|
import javax.crypto.spec.SecretKeySpec;
|
||||||
@ -47,7 +49,7 @@ public class SessionTokenService {
|
|||||||
}
|
}
|
||||||
Instant now = clock.instant();
|
Instant now = clock.instant();
|
||||||
Instant exp = now.plus(ttl);
|
Instant exp = now.plus(ttl);
|
||||||
TokenPayload payload = new TokenPayload(user.username(), user.email(), now.getEpochSecond(), exp.getEpochSecond());
|
TokenPayload payload = new TokenPayload(user.username(), user.email(), now.getEpochSecond(), exp.getEpochSecond(), user.groups());
|
||||||
try {
|
try {
|
||||||
String payloadJson = objectMapper.writeValueAsString(payload);
|
String payloadJson = objectMapper.writeValueAsString(payload);
|
||||||
String payloadB64 = B64_URL.encodeToString(payloadJson.getBytes(StandardCharsets.UTF_8));
|
String payloadB64 = B64_URL.encodeToString(payloadJson.getBytes(StandardCharsets.UTF_8));
|
||||||
@ -105,7 +107,8 @@ public class SessionTokenService {
|
|||||||
if (payload.sub() == null || payload.sub().isBlank()) {
|
if (payload.sub() == null || payload.sub().isBlank()) {
|
||||||
return Optional.empty();
|
return Optional.empty();
|
||||||
}
|
}
|
||||||
return Optional.of(new SessionUser(payload.sub(), payload.email()));
|
List<String> groups = payload.groups() != null ? payload.groups() : List.of();
|
||||||
|
return Optional.of(new SessionUser(payload.sub(), payload.email(), groups));
|
||||||
}
|
}
|
||||||
|
|
||||||
private static byte[] hmacSha256(String secret, String signingInput) {
|
private static byte[] hmacSha256(String secret, String signingInput) {
|
||||||
@ -118,6 +121,7 @@ public class SessionTokenService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
record TokenPayload(String sub, String email, long iat, long exp) {
|
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||||
|
record TokenPayload(String sub, String email, long iat, long exp, List<String> groups) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,4 +1,6 @@
|
|||||||
package org.prole.auth.session;
|
package org.prole.auth.session;
|
||||||
|
|
||||||
public record SessionUser(String username, String email) {
|
import java.util.List;
|
||||||
|
|
||||||
|
public record SessionUser(String username, String email, List<String> groups) {
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,7 +1,9 @@
|
|||||||
package org.prole.auth.web;
|
package org.prole.auth.web;
|
||||||
|
|
||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.Base64;
|
import java.util.Base64;
|
||||||
|
import java.util.List;
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
|
|
||||||
import jakarta.annotation.PostConstruct;
|
import jakarta.annotation.PostConstruct;
|
||||||
@ -143,7 +145,7 @@ public class LoginController {
|
|||||||
.orElseThrow(() -> new IllegalStateException("Failed to normalize Kerberos principal"));
|
.orElseThrow(() -> new IllegalStateException("Failed to normalize Kerberos principal"));
|
||||||
String email = username + "@" + auth.getEmailDomain();
|
String email = username + "@" + auth.getEmailDomain();
|
||||||
|
|
||||||
setSessionCookie(response, new SessionUser(username, email));
|
setSessionCookie(response, new SessionUser(username, email, resolveGroups(username)));
|
||||||
return builder.location(safeNext(next)).build();
|
return builder.location(safeNext(next)).build();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -173,7 +175,7 @@ public class LoginController {
|
|||||||
String norm = normalizer.normalizeUsernameFromKerberosPrincipal(principal)
|
String norm = normalizer.normalizeUsernameFromKerberosPrincipal(principal)
|
||||||
.orElseThrow(() -> new IllegalStateException("Failed to normalize username"));
|
.orElseThrow(() -> new IllegalStateException("Failed to normalize username"));
|
||||||
String email = norm + "@" + auth.getEmailDomain();
|
String email = norm + "@" + auth.getEmailDomain();
|
||||||
setSessionCookie(response, new SessionUser(norm, email));
|
setSessionCookie(response, new SessionUser(norm, email, resolveGroups(norm)));
|
||||||
return ResponseEntity.status(HttpStatus.FOUND).location(safeNext(next)).build();
|
return ResponseEntity.status(HttpStatus.FOUND).location(safeNext(next)).build();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -183,6 +185,14 @@ public class LoginController {
|
|||||||
return ResponseEntity.status(HttpStatus.FOUND).location(safeNext(next)).build();
|
return ResponseEntity.status(HttpStatus.FOUND).location(safeNext(next)).build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private List<String> resolveGroups(String username) {
|
||||||
|
List<String> groups = new ArrayList<>();
|
||||||
|
if (auth.getAdminPrincipals().contains(username)) {
|
||||||
|
groups.add("admin");
|
||||||
|
}
|
||||||
|
return groups;
|
||||||
|
}
|
||||||
|
|
||||||
private void setSessionCookie(HttpServletResponse response, SessionUser user) {
|
private void setSessionCookie(HttpServletResponse response, SessionUser user) {
|
||||||
String token = sessionTokenService.issue(auth.getSessionSecret(), user, auth.getSessionTtl());
|
String token = sessionTokenService.issue(auth.getSessionSecret(), user, auth.getSessionTtl());
|
||||||
|
|
||||||
|
|||||||
@ -20,6 +20,7 @@ import org.springframework.web.bind.annotation.RestController;
|
|||||||
public class VerifyController {
|
public class VerifyController {
|
||||||
public static final String USER_HEADER = "X-Prole-User";
|
public static final String USER_HEADER = "X-Prole-User";
|
||||||
public static final String EMAIL_HEADER = "X-Prole-Email";
|
public static final String EMAIL_HEADER = "X-Prole-Email";
|
||||||
|
public static final String GROUPS_HEADER = "X-Prole-Groups";
|
||||||
|
|
||||||
private final AuthProperties auth;
|
private final AuthProperties auth;
|
||||||
private final SessionTokenService tokens;
|
private final SessionTokenService tokens;
|
||||||
@ -42,6 +43,7 @@ public class VerifyController {
|
|||||||
return ResponseEntity.ok()
|
return ResponseEntity.ok()
|
||||||
.header(USER_HEADER, user.get().username())
|
.header(USER_HEADER, user.get().username())
|
||||||
.header(EMAIL_HEADER, user.get().email() == null ? "" : user.get().email())
|
.header(EMAIL_HEADER, user.get().email() == null ? "" : user.get().email())
|
||||||
|
.header(GROUPS_HEADER, user.get().groups() == null ? "" : String.join(",", user.get().groups()))
|
||||||
// harden: don't let intermediates cache auth decisions
|
// harden: don't let intermediates cache auth decisions
|
||||||
.header(HttpHeaders.CACHE_CONTROL, "no-store")
|
.header(HttpHeaders.CACHE_CONTROL, "no-store")
|
||||||
.build();
|
.build();
|
||||||
|
|||||||
@ -17,6 +17,9 @@ prole:
|
|||||||
sessionSecret: ""
|
sessionSecret: ""
|
||||||
emailDomain: prole.org
|
emailDomain: prole.org
|
||||||
formEnabled: false
|
formEnabled: false
|
||||||
|
# Comma-separated list of bare usernames granted admin group membership.
|
||||||
|
# Override via env: PROLE_AUTH_ADMIN_PRINCIPALS=admin
|
||||||
|
adminPrincipals: []
|
||||||
kerberos:
|
kerberos:
|
||||||
# REQUIRED for SPNEGO when enabled. Provide via env.
|
# REQUIRED for SPNEGO when enabled. Provide via env.
|
||||||
servicePrincipal: ""
|
servicePrincipal: ""
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user