From c1d2a9199197275d435c25545bc0d9b0d352c361 Mon Sep 17 00:00:00 2001 From: chrisfu Date: Wed, 27 May 2026 20:06:36 -0700 Subject: [PATCH] fix(spnego): Subject.callAs, AES keytab, and krb5.conf sync for JDK 21 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - KerberosSpnegoService: replace Subject.doAs with Subject.callAs (JDK 18+) so Subject.current() is set correctly for JGSS credential lookup in JDK 21. Pass null GSSCredential to createContext so the mechanism auto-selects rather than failing with "No credential found for 1.3.6.1.5.5.2". - knoe-auth-deployment: fix PROLE_KERBEROS_KEYTAB_PATH to /etc/knoe-auth/http.keytab (matches the knoe-auth-http-keytab secret volume mount). - knoe-kdc-configmap: full rewrite from stale KNOE.DEV to KNOE.LOCAL; add PROLE.ORG realm (Samba AD at 10.0.0.3) and [capaths] for future cross-realm trust. Drop arcfour-hmac/des3 from permitted_enctypes — RC4 hard-removed in JDK 21 and service account now AES-only (msDS-SupportedEncryptionTypes=24). Fix hardcoded secret values in entrypoint.sh to use ${PROLE_KDC_MASTER_PASSWORD} variable references. Co-Authored-By: Claude Sonnet 4.6 --- .../auth/kerberos/KerberosSpnegoService.java | 16 +- .../manifests/knoe/knoe-auth-deployment.yaml | 2 +- .../manifests/knoe/knoe-kdc-configmap.yaml | 170 ++++++++++++++---- 3 files changed, 148 insertions(+), 40 deletions(-) diff --git a/authority/src/main/java/dev/knoe/auth/kerberos/KerberosSpnegoService.java b/authority/src/main/java/dev/knoe/auth/kerberos/KerberosSpnegoService.java index ade0b1e..5047a14 100644 --- a/authority/src/main/java/dev/knoe/auth/kerberos/KerberosSpnegoService.java +++ b/authority/src/main/java/dev/knoe/auth/kerberos/KerberosSpnegoService.java @@ -1,7 +1,7 @@ package dev.knoe.auth.kerberos; -import java.security.PrivilegedExceptionAction; import java.util.Map; +import java.util.concurrent.Callable; import javax.security.auth.Subject; import javax.security.auth.login.AppConfigurationEntry; import javax.security.auth.login.Configuration; @@ -16,11 +16,18 @@ import org.springframework.stereotype.Component; @Component public class KerberosSpnegoService { + /** + * See knoe-auth/src/main/java copy for full history comment. + * Short version: Subject.callAs (not doAs) is required in JDK 21 so that + * Subject.current() is set correctly for the JGSS credential lookup. + */ + private static final Oid SPNEGO_OID; private static final Oid KRB5_OID; static { try { - KRB5_OID = new Oid("1.2.840.113554.1.2.2"); + SPNEGO_OID = new Oid("1.3.6.1.5.5.2"); + KRB5_OID = new Oid("1.2.840.113554.1.2.2"); } catch (Exception e) { throw new RuntimeException("Failed to init Kerberos OID", e); } @@ -42,10 +49,9 @@ public class KerberosSpnegoService { loginContext.login(); Subject subject = loginContext.getSubject(); - return Subject.doAs(subject, (PrivilegedExceptionAction) () -> { + return Subject.callAs(subject, (Callable) () -> { GSSManager manager = GSSManager.getInstance(); - GSSCredential creds = manager.createCredential(null, GSSCredential.DEFAULT_LIFETIME, KRB5_OID, GSSCredential.ACCEPT_ONLY); - GSSContext ctx = manager.createContext(creds); + GSSContext ctx = manager.createContext((GSSCredential) null); byte[] outToken = ctx.acceptSecContext(token, 0, token.length); boolean established = ctx.isEstablished(); diff --git a/deploy/opentofu/k3s/manifests/knoe/knoe-auth-deployment.yaml b/deploy/opentofu/k3s/manifests/knoe/knoe-auth-deployment.yaml index 96bbb82..cc36768 100644 --- a/deploy/opentofu/k3s/manifests/knoe/knoe-auth-deployment.yaml +++ b/deploy/opentofu/k3s/manifests/knoe/knoe-auth-deployment.yaml @@ -167,7 +167,7 @@ spec: name: knoe-auth-kerberos key: servicePrincipal - name: PROLE_KERBEROS_KEYTAB_PATH - value: "/etc/knoe/keytabs/http.keytab" + value: "/etc/knoe-auth/http.keytab" - name: PROLE_KERBEROS_REALM valueFrom: configMapKeyRef: diff --git a/deploy/opentofu/k3s/manifests/knoe/knoe-kdc-configmap.yaml b/deploy/opentofu/k3s/manifests/knoe/knoe-kdc-configmap.yaml index 2c0632c..e70dc27 100644 --- a/deploy/opentofu/k3s/manifests/knoe/knoe-kdc-configmap.yaml +++ b/deploy/opentofu/k3s/manifests/knoe/knoe-kdc-configmap.yaml @@ -2,29 +2,46 @@ apiVersion: v1 kind: ConfigMap metadata: name: knoe-kdc-config + namespace: knoe-system data: krb5.conf: | [libdefaults] - default_realm = KNOE.DEV + # AES-only: RC4 (arcfour-hmac) hard-removed from JDK 21, not supported by + # the service account (msDS-SupportedEncryptionTypes=24 = AES128+AES256). + # Note: if cross-realm trust (KNOE.LOCAL ↔ PROLE.ORG) is implemented via + # the KDC sidecar, these will need to include arcfour-hmac for krbtgt + # principals (RC4 avoids the MIT/Samba salt mismatch). See entrypoint.sh. + permitted_enctypes = aes256-cts-hmac-sha1-96 aes128-cts-hmac-sha1-96 + default_tkt_enctypes = aes256-cts-hmac-sha1-96 aes128-cts-hmac-sha1-96 + default_tgs_enctypes = aes256-cts-hmac-sha1-96 aes128-cts-hmac-sha1-96 + default_realm = KNOE.LOCAL dns_lookup_realm = false dns_lookup_kdc = false - ticket_lifetime = 10h - renew_lifetime = 7d - forwardable = true + [realms] - KNOE.DEV = { + KNOE.LOCAL = { kdc = 127.0.0.1 admin_server = 127.0.0.1 } - [domain_realm] - .knoe.dev = KNOE.DEV - knoe.dev = KNOE.DEV + PROLE.ORG = { + kdc = 10.0.0.3 + admin_server = 10.0.0.3 + } + + [capaths] + KNOE.LOCAL = { + PROLE.ORG = . + } + PROLE.ORG = { + KNOE.LOCAL = . + } + kdc.conf: | [kdcdefaults] kdc_ports = 88 kdc_tcp_ports = 88 [realms] - KNOE.DEV = { + KNOE.LOCAL = { database_name = /var/lib/krb5kdc/principal admin_keytab = FILE:/etc/krb5kdc/kadm5.keytab acl_file = /etc/krb5kdc/kadm5.acl @@ -33,26 +50,31 @@ data: max_renewable_life = 7d 0h 0m 0s default_principal_flags = +preauth } + kadm5.acl: | - admin/admin@KNOE.DEV * + admin/admin@KNOE.LOCAL * + entrypoint.sh: | #!/usr/bin/env bash set -euo pipefail - export DEBIAN_FRONTEND=noninteractive - realm="${KNOE_KDC_REALM:-KNOE.DEV}" - admin_principal="${KNOE_KDC_ADMIN_PRINCIPAL:-admin/admin}" - if [[ "${admin_principal}" != *"@"* ]]; then - admin_principal="${admin_principal}@${realm}" + # Enable verbose debug if requested + if [[ "${KNOE_DEBUG:-}" == "1" ]]; then + set -x + echo "[DEBUG] Environment snapshot:" >&2 + env | sed -E 's/(PASSWORD|TOKEN|SECRET)=.*/\1=****/g' >&2 || true fi + export DEBIAN_FRONTEND=noninteractive + if ! command -v krb5kdc >/dev/null 2>&1; then echo "Installing Kerberos packages..." - echo "krb5-config krb5-config/default_realm string ${realm}" | debconf-set-selections || true + echo "krb5-config krb5-config/default_realm string KNOE.LOCAL" | debconf-set-selections || true echo "krb5-config krb5-config/kerberos_servers string 127.0.0.1" | debconf-set-selections || true echo "krb5-config krb5-config/admin_server string 127.0.0.1" | debconf-set-selections || true - apt-get update -qq + apt-get update apt-get install -y --no-install-recommends krb5-kdc krb5-admin-server krb5-user dnsutils ca-certificates rm -rf /var/lib/apt/lists/* fi + mkdir -p /etc/krb5kdc /var/lib/krb5kdc if [[ -f /opt/knoe-kdc/krb5.conf ]]; then cp /opt/knoe-kdc/krb5.conf /etc/krb5.conf @@ -63,37 +85,117 @@ data: if [[ -f /opt/knoe-kdc/kadm5.acl ]]; then cp /opt/knoe-kdc/kadm5.acl /etc/krb5kdc/kadm5.acl fi - if [[ -z "${KNOE_KDC_MASTER_PASSWORD:-}" ]]; then - echo "ERROR: Missing required env KNOE_KDC_MASTER_PASSWORD" >&2 + + # Validate required secrets early to avoid silent crashes + if [[ -z "${PROLE_KDC_MASTER_PASSWORD:-}" ]]; then + echo "ERROR: Missing required env PROLE_KDC_MASTER_PASSWORD (secret 'knoe-kdc-secrets/master_password')." >&2 exit 1 fi - if [[ -z "${KNOE_KDC_ADMIN_PASSWORD:-}" ]]; then - echo "ERROR: Missing required env KNOE_KDC_ADMIN_PASSWORD" >&2 + if [[ -z "${PROLE_KDC_ADMIN_PASSWORD:-}" ]]; then + echo "ERROR: Missing required env PROLE_KDC_ADMIN_PASSWORD (secret 'knoe-kdc-secrets/admin_password')." >&2 exit 1 fi + + # Optionally generate a minimal Samba configuration if a child realm is provided + realm="${PROLE_CHILD_REALM:-}" + if [[ -z "$realm" ]]; then + realm="${PROLE_KDC_REALM}" + fi + if [[ -n "$realm" ]]; then + workgroup="${PROLE_CHILD_WORKGROUP:-}" + if [[ -z "$workgroup" ]]; then + workgroup="${realm%%.*}" + fi + netbios="${PROLE_CHILD_NETBIOS_NAME:-}" + if [[ -z "$netbios" ]]; then + netbios="$workgroup" + fi + server_string="${PROLE_CHILD_SERVER_STRING:-}" + if [[ -z "$server_string" ]]; then + server_string="${realm} AD DC" + fi + server_role="${PROLE_SAMBA_SERVER_ROLE:-}" + if [[ -z "$server_role" ]]; then + server_role='active directory domain controller' + fi + mkdir -p /etc/samba + # Write minimal Samba config without using a here-doc to avoid YAML indentation issues + # when this script is embedded in a ConfigMap. Variables are expanded at container runtime. + { + printf '%s\n' "[global]" + printf '%s\n' " workgroup = ${workgroup}" + printf '%s\n' " realm = ${realm}" + printf '%s\n' " netbios name = ${netbios}" + printf '%s\n' " server string = ${server_string}" + printf '%s\n' " server role = ${server_role}" + } > /etc/samba/smb.conf + fi + + realm="${PROLE_KDC_REALM}" + admin_principal="${PROLE_KDC_ADMIN_PRINCIPAL}" + if [[ "${admin_principal}" != *"@"* ]]; then + admin_principal="${admin_principal}@${PROLE_KDC_REALM}" + fi + if [[ ! -f /var/lib/krb5kdc/principal ]]; then - echo "Initializing realm database for ${realm}..." - kdb5_util create -s -r "${realm}" -P "${KNOE_KDC_MASTER_PASSWORD}" + echo "Initializing realm database for ${PROLE_KDC_REALM}..." + kdb5_util create -s -r "${realm}" -P "${PROLE_KDC_MASTER_PASSWORD}" fi + if ! kadmin.local -q "get_principal ${admin_principal}" >/dev/null 2>&1; then echo "Creating admin principal ${admin_principal}..." - kadmin.local -q "addprinc -pw ${KNOE_KDC_ADMIN_PASSWORD} ${admin_principal}" + kadmin.local -q "addprinc -pw ${PROLE_KDC_ADMIN_PASSWORD} ${admin_principal}" fi - # system admin user principal - 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 ${KNOE_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 ${KNOE_KDC_GUEST_PASSWORD:-changeme} guest@${realm}" + + if [[ -n "${PROLE_KDC_TRUST_REALM:-}" && "${PROLE_KDC_TRUST_REALM}" != "${PROLE_KDC_REALM}" ]]; then + shared_pw="${PROLE_KDC_TRUST_SHARED_PASSWORD:-${PROLE_KDC_MASTER_PASSWORD}}" + # ------------------------------------------------------------------ + # Cross-realm krbtgt principals — RC4 only. + # + # Both directions of the trust live as their own krbtgt principal, + # each keyed to the same shared password. We pin RC4 (arcfour-hmac) + # because AES key derivation requires a salt, and Samba's salt + # convention ( + UPN) does not match MIT's + # ( + ). RC4 derives keys from + # the password alone, so both sides converge with no salt fight. + # + # NOTE: if this path is activated, also re-add arcfour-hmac to + # permitted_enctypes in krb5.conf so the KDC can issue RC4 tickets. + # ------------------------------------------------------------------ + + # Outbound: KNOE.LOCAL → PROLE.ORG (issued here, decrypted by Samba) + if ! kadmin.local -q "get_principal krbtgt/${PROLE_KDC_TRUST_REALM}@${PROLE_KDC_REALM}" >/dev/null 2>&1; then + echo "Creating outbound trust principal krbtgt/${PROLE_KDC_TRUST_REALM}@${PROLE_KDC_REALM}..." + kadmin.local -q "addprinc -pw ${shared_pw} -e arcfour-hmac:normal krbtgt/${PROLE_KDC_TRUST_REALM}@${PROLE_KDC_REALM}" + fi + + # Inbound: PROLE.ORG → KNOE.LOCAL (issued by Samba, decrypted here) + if ! kadmin.local -q "get_principal krbtgt/${PROLE_KDC_REALM}@${PROLE_KDC_TRUST_REALM}" >/dev/null 2>&1; then + echo "Creating inbound trust principal krbtgt/${PROLE_KDC_REALM}@${PROLE_KDC_TRUST_REALM}..." + kadmin.local -q "addprinc -pw ${shared_pw} -e arcfour-hmac:normal krbtgt/${PROLE_KDC_REALM}@${PROLE_KDC_TRUST_REALM}" + fi + + # NOTE: The Samba-side trust account (user "krbtgt_${PROLE_KDC_REALM}" + # in PROLE.ORG with UPN/SPN krbtgt/${PROLE_KDC_REALM}) is provisioned + # OUT-OF-BAND by this repo's Ansible playbook: + # infrastructure/playbooks/kerberos_trust_setup.yml + # Earlier versions of this script tried to use a remote "kadmin" + # client to write that principal into Samba, but Samba AD does not + # accept additions over MIT's kadmin protocol — it always failed + # with "Missing parameters in krb5.conf required for kadmin client". + # Run the playbook once after this KDC comes up: + # ANSIBLE_VAULT_PASSWORD_FILE=$PWD/.vault_pass \ + # ansible-playbook infrastructure/playbooks/kerberos_trust_setup.yml + echo "Note: Samba-side trust account is provisioned out-of-band by" + echo " infrastructure/playbooks/kerberos_trust_setup.yml" fi + + # Start daemons. Keep kadmind in PID 1; run krb5kdc in background and verify it binds. echo "Starting krb5kdc and kadmind ..." krb5kdc -n & sleep 0.5 if ! pgrep -x krb5kdc >/dev/null 2>&1; then - echo "ERROR: krb5kdc failed to start." >&2 + echo "ERROR: krb5kdc failed to start. Check /var/log/ (syslog) for details." >&2 exit 1 fi exec kadmind -nofork