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; package dev.knoe.auth.kerberos;
import java.security.PrivilegedExceptionAction;
import java.util.Map; import java.util.Map;
import java.util.concurrent.Callable;
import javax.security.auth.Subject; import javax.security.auth.Subject;
import javax.security.auth.login.AppConfigurationEntry; import javax.security.auth.login.AppConfigurationEntry;
import javax.security.auth.login.Configuration; import javax.security.auth.login.Configuration;
@ -16,10 +16,17 @@ import org.springframework.stereotype.Component;
@Component @Component
public class KerberosSpnegoService { 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; private static final Oid KRB5_OID;
static { static {
try { try {
SPNEGO_OID = new Oid("1.3.6.1.5.5.2");
KRB5_OID = new Oid("1.2.840.113554.1.2.2"); KRB5_OID = new Oid("1.2.840.113554.1.2.2");
} catch (Exception e) { } catch (Exception e) {
throw new RuntimeException("Failed to init Kerberos OID", e); throw new RuntimeException("Failed to init Kerberos OID", e);
@ -42,10 +49,9 @@ public class KerberosSpnegoService {
loginContext.login(); loginContext.login();
Subject subject = loginContext.getSubject(); Subject subject = loginContext.getSubject();
return Subject.doAs(subject, (PrivilegedExceptionAction<Result>) () -> { return Subject.callAs(subject, (Callable<Result>) () -> {
GSSManager manager = GSSManager.getInstance(); GSSManager manager = GSSManager.getInstance();
GSSCredential creds = manager.createCredential(null, GSSCredential.DEFAULT_LIFETIME, KRB5_OID, GSSCredential.ACCEPT_ONLY); GSSContext ctx = manager.createContext((GSSCredential) null);
GSSContext ctx = manager.createContext(creds);
byte[] outToken = ctx.acceptSecContext(token, 0, token.length); byte[] outToken = ctx.acceptSecContext(token, 0, token.length);
boolean established = ctx.isEstablished(); boolean established = ctx.isEstablished();

View File

@ -167,7 +167,7 @@ spec:
name: knoe-auth-kerberos name: knoe-auth-kerberos
key: servicePrincipal key: servicePrincipal
- name: PROLE_KERBEROS_KEYTAB_PATH - name: PROLE_KERBEROS_KEYTAB_PATH
value: "/etc/knoe/keytabs/http.keytab" value: "/etc/knoe-auth/http.keytab"
- name: PROLE_KERBEROS_REALM - name: PROLE_KERBEROS_REALM
valueFrom: valueFrom:
configMapKeyRef: configMapKeyRef:

View File

@ -2,29 +2,46 @@ apiVersion: v1
kind: ConfigMap kind: ConfigMap
metadata: metadata:
name: knoe-kdc-config name: knoe-kdc-config
namespace: knoe-system
data: data:
krb5.conf: | krb5.conf: |
[libdefaults] [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_realm = false
dns_lookup_kdc = false dns_lookup_kdc = false
ticket_lifetime = 10h
renew_lifetime = 7d
forwardable = true
[realms] [realms]
KNOE.DEV = { KNOE.LOCAL = {
kdc = 127.0.0.1 kdc = 127.0.0.1
admin_server = 127.0.0.1 admin_server = 127.0.0.1
} }
[domain_realm] PROLE.ORG = {
.knoe.dev = KNOE.DEV kdc = 10.0.0.3
knoe.dev = KNOE.DEV admin_server = 10.0.0.3
}
[capaths]
KNOE.LOCAL = {
PROLE.ORG = .
}
PROLE.ORG = {
KNOE.LOCAL = .
}
kdc.conf: | kdc.conf: |
[kdcdefaults] [kdcdefaults]
kdc_ports = 88 kdc_ports = 88
kdc_tcp_ports = 88 kdc_tcp_ports = 88
[realms] [realms]
KNOE.DEV = { KNOE.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
@ -33,26 +50,31 @@ data:
max_renewable_life = 7d 0h 0m 0s max_renewable_life = 7d 0h 0m 0s
default_principal_flags = +preauth default_principal_flags = +preauth
} }
kadm5.acl: | kadm5.acl: |
admin/admin@KNOE.DEV * admin/admin@KNOE.LOCAL *
entrypoint.sh: | entrypoint.sh: |
#!/usr/bin/env bash #!/usr/bin/env bash
set -euo pipefail set -euo pipefail
export DEBIAN_FRONTEND=noninteractive # Enable verbose debug if requested
realm="${KNOE_KDC_REALM:-KNOE.DEV}" if [[ "${KNOE_DEBUG:-}" == "1" ]]; then
admin_principal="${KNOE_KDC_ADMIN_PRINCIPAL:-admin/admin}" set -x
if [[ "${admin_principal}" != *"@"* ]]; then echo "[DEBUG] Environment snapshot:" >&2
admin_principal="${admin_principal}@${realm}" env | sed -E 's/(PASSWORD|TOKEN|SECRET)=.*/\1=****/g' >&2 || true
fi fi
export DEBIAN_FRONTEND=noninteractive
if ! command -v krb5kdc >/dev/null 2>&1; then if ! command -v krb5kdc >/dev/null 2>&1; then
echo "Installing Kerberos packages..." 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/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 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 apt-get install -y --no-install-recommends krb5-kdc krb5-admin-server krb5-user dnsutils ca-certificates
rm -rf /var/lib/apt/lists/* rm -rf /var/lib/apt/lists/*
fi fi
mkdir -p /etc/krb5kdc /var/lib/krb5kdc mkdir -p /etc/krb5kdc /var/lib/krb5kdc
if [[ -f /opt/knoe-kdc/krb5.conf ]]; then if [[ -f /opt/knoe-kdc/krb5.conf ]]; then
cp /opt/knoe-kdc/krb5.conf /etc/krb5.conf cp /opt/knoe-kdc/krb5.conf /etc/krb5.conf
@ -63,37 +85,117 @@ data:
if [[ -f /opt/knoe-kdc/kadm5.acl ]]; then if [[ -f /opt/knoe-kdc/kadm5.acl ]]; then
cp /opt/knoe-kdc/kadm5.acl /etc/krb5kdc/kadm5.acl cp /opt/knoe-kdc/kadm5.acl /etc/krb5kdc/kadm5.acl
fi 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 exit 1
fi fi
if [[ -z "${KNOE_KDC_ADMIN_PASSWORD:-}" ]]; then if [[ -z "${PROLE_KDC_ADMIN_PASSWORD:-}" ]]; then
echo "ERROR: Missing required env KNOE_KDC_ADMIN_PASSWORD" >&2 echo "ERROR: Missing required env PROLE_KDC_ADMIN_PASSWORD (secret 'knoe-kdc-secrets/admin_password')." >&2
exit 1 exit 1
fi 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 if [[ ! -f /var/lib/krb5kdc/principal ]]; then
echo "Initializing realm database for ${realm}..." echo "Initializing realm database for ${PROLE_KDC_REALM}..."
kdb5_util create -s -r "${realm}" -P "${KNOE_KDC_MASTER_PASSWORD}" kdb5_util create -s -r "${realm}" -P "${PROLE_KDC_MASTER_PASSWORD}"
fi fi
if ! kadmin.local -q "get_principal ${admin_principal}" >/dev/null 2>&1; then if ! kadmin.local -q "get_principal ${admin_principal}" >/dev/null 2>&1; then
echo "Creating admin principal ${admin_principal}..." 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 fi
# system admin user principal
if ! kadmin.local -q "get_principal admin@${realm}" >/dev/null 2>&1; then if [[ -n "${PROLE_KDC_TRUST_REALM:-}" && "${PROLE_KDC_TRUST_REALM}" != "${PROLE_KDC_REALM}" ]]; then
echo "Creating admin user principal admin@${realm}..." shared_pw="${PROLE_KDC_TRUST_SHARED_PASSWORD:-${PROLE_KDC_MASTER_PASSWORD}}"
kadmin.local -q "addprinc -pw ${KNOE_KDC_MASTER_PASSWORD} admin@${realm}" # ------------------------------------------------------------------
# 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 fi
# guest user principal (read-only)
if ! kadmin.local -q "get_principal guest@${realm}" >/dev/null 2>&1; then # Inbound: PROLE.ORG → KNOE.LOCAL (issued by Samba, decrypted here)
echo "Creating guest user principal guest@${realm}..." if ! kadmin.local -q "get_principal krbtgt/${PROLE_KDC_REALM}@${PROLE_KDC_TRUST_REALM}" >/dev/null 2>&1; then
kadmin.local -q "addprinc -pw ${KNOE_KDC_GUEST_PASSWORD:-changeme} guest@${realm}" 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 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 ..." echo "Starting krb5kdc and kadmind ..."
krb5kdc -n & krb5kdc -n &
sleep 0.5 sleep 0.5
if ! pgrep -x krb5kdc >/dev/null 2>&1; then 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 exit 1
fi fi
exec kadmind -nofork exec kadmind -nofork