fix(spnego): Subject.callAs, AES keytab, and krb5.conf sync for JDK 21

- 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 <noreply@anthropic.com>
This commit is contained in:
chrisfu 2026-05-27 20:06:36 -07:00
parent 9523045add
commit c1d2a91991
3 changed files with 148 additions and 40 deletions

View File

@ -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<Result>) () -> {
return Subject.callAs(subject, (Callable<Result>) () -> {
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();

View File

@ -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:

View File

@ -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 (<remote_realm> + UPN) does not match MIT's
# (<local_realm> + <principal-no-realm>). 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