mirror of
https://github.com/dredx/prole.git
synced 2026-09-23 11:03:59 +00:00
The matching knoe-db change (commit `ff7546d`) re-keys the MIT-side cross-realm krbtgts with `-e arcfour-hmac:normal` to dodge the AES-salt mismatch between Samba and MIT. For the trust to actually carry traffic, the Samba side must also offer only RC4 on the trust account — otherwise the client picks AES, the two sides derive different keys from the same password (different salt conventions), and TGT decryption fails. Change `msDS-SupportedEncryptionTypes` from 28 (RC4+AES128+AES256) to 4 (RC4 only). RC4 has no salt, so keys derive from the password alone and both sides converge. Updated docstring + summary debug print to reflect the new value and the why. Tracked alongside the Junie brief at ~/dev/knoe-db/docs/plans/junie/kdc-trust-reset-repeatable.md. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
414 lines
18 KiB
YAML
414 lines
18 KiB
YAML
---
|
|
# kerberos_trust_setup.yml — Register the KNOE.LOCAL ↔ PROLE.ORG cross-realm
|
|
# Kerberos trust in Samba AD on myrddin.prole.org.
|
|
#
|
|
# This unblocks SSO from PROLE.ORG (the on-prem Samba AD realm) into KNOE.LOCAL
|
|
# (the in-cluster MIT KDC realm), which is required for chrisfu@PROLE.ORG →
|
|
# service@KNOE.LOCAL ticket flows targeting db.prole.org / pg_oauth and other
|
|
# cluster-hosted services that authenticate against the KNOE.LOCAL KDC.
|
|
#
|
|
# Why not `samba-tool domain trust create`?
|
|
# -----------------------------------------
|
|
# That command requires the remote side to be another writeable AD-style DC,
|
|
# and tries to discover it via DNS-SRV lookup. Our peer is the in-cluster MIT
|
|
# KDC at svc/auth.knoe-system — a plain Kerberos KDC, not AD — so the
|
|
# command bails with "Failed to find a writeable DC for domain KNOE.LOCAL".
|
|
#
|
|
# The supported Samba-AD ↔ MIT-KDC approach is to create the inter-realm TGT
|
|
# principal as an ordinary user account in Samba whose sAMAccountName equals
|
|
# `krbtgt/KNOE.LOCAL`, and to share that account's password with the MIT side.
|
|
# The MIT side already has the matching principal (created by init_kdc.sh in
|
|
# the knoe-db repo), keyed to the same trust_shared_password we pull from the
|
|
# in-cluster Secret knoe-system/knoe-kdc-secrets.
|
|
#
|
|
# PREREQUISITES
|
|
# -------------
|
|
# 1. Run AFTER init_knoe_users.sh / init_kdc.sh has run on the k3s cluster.
|
|
# The trust_shared_password used here must match the value stored in the
|
|
# in-cluster Secret knoe-system/knoe-kdc-secrets (key: trust_shared_password).
|
|
# The in-cluster MIT KDC must already have created the principal
|
|
# krbtgt/PROLE.ORG@KNOE.LOCAL
|
|
# and krbtgt/KNOE.LOCAL@PROLE.ORG
|
|
# before this playbook runs. (init_kdc.sh handles this.)
|
|
#
|
|
# 2. trust_kdc_ip must be the ClusterIP of the 'auth' Service in the knoe-system
|
|
# namespace. myrddin (the Samba DC) must be able to reach it — either because
|
|
# it is the k3s server node (and is therefore on the pod/service CIDR network)
|
|
# or because a static route has been added.
|
|
#
|
|
# 3. The Samba AD administrator password lives in the Ansible vault as
|
|
# vault_samba_dns_admin_pass
|
|
# (group_vars/ad_dc/vault.yml). Decrypt with --vault-password-file .vault_pass
|
|
# or `ANSIBLE_VAULT_PASSWORD_FILE=$PWD/.vault_pass`.
|
|
#
|
|
# DRY-RUN
|
|
# -------
|
|
# ansible-playbook infrastructure/playbooks/kerberos_trust_setup.yml \
|
|
# --check --diff \
|
|
# --vault-password-file .vault_pass \
|
|
# -e trust_shared_password=dummy
|
|
#
|
|
# FULL RUN (auto-resolves trust_shared_password from the cluster Secret)
|
|
# ---------------------------------------------------------------------
|
|
# ansible-playbook infrastructure/playbooks/kerberos_trust_setup.yml \
|
|
# --vault-password-file .vault_pass
|
|
#
|
|
# OVERRIDES
|
|
# ---------
|
|
# -e trust_kdc_ip=10.43.x.y # skip ClusterIP autoresolve
|
|
# -e trust_shared_password=... # skip Secret autoresolve
|
|
# -e samba_admin_password=... # override vault (e.g. CI without vault)
|
|
|
|
- name: Register KNOE.LOCAL cross-realm trust in Samba AD on myrddin
|
|
hosts: myrddin.prole.org
|
|
gather_facts: false
|
|
become: true
|
|
|
|
vars:
|
|
trust_realm: "KNOE.LOCAL"
|
|
samba_local_realm: "PROLE.ORG"
|
|
# sAMAccountName cannot contain a slash, so we store the inter-realm TGT
|
|
# account under an underscore-form name and explicitly set its
|
|
# userPrincipalName to the canonical Kerberos form below.
|
|
samba_account_name: "krbtgt_KNOE.LOCAL"
|
|
trust_principal: "krbtgt/KNOE.LOCAL@PROLE.ORG"
|
|
trust_kdc_ip: "" # auto-resolved below if empty
|
|
# Pull the trust password from the cluster Secret unless caller overrides.
|
|
trust_shared_password: "{{ lookup('env', 'PROLE_TRUST_SHARED_PASSWORD') | default('', true) }}"
|
|
# Admin password resolution order:
|
|
# 1. -e samba_admin_password=... (explicit override)
|
|
# 2. SAMBA_ADMIN_PASSWORD env var (legacy / CI)
|
|
# 3. vault_samba_dns_admin_pass (Ansible vault — preferred)
|
|
samba_admin_password: >-
|
|
{{ lookup('env', 'SAMBA_ADMIN_PASSWORD')
|
|
| default(hostvars[inventory_hostname].vault_samba_dns_admin_pass
|
|
| default(vault_samba_dns_admin_pass | default('')), true) }}
|
|
|
|
tasks:
|
|
# ------------------------------------------------------------------
|
|
# 0. Ensure required Debian packages are present
|
|
# (ldb-tools provides ldbmodify; we use it later to set UPN/SPN
|
|
# on the trust account.)
|
|
# ------------------------------------------------------------------
|
|
- name: Ensure ldb-tools is installed (provides ldbmodify)
|
|
ansible.builtin.apt:
|
|
name: ldb-tools
|
|
state: present
|
|
update_cache: true
|
|
cache_valid_time: 3600
|
|
|
|
# ------------------------------------------------------------------
|
|
# 0a. Resolve the ClusterIP of the in-cluster MIT KDC if not provided
|
|
# ------------------------------------------------------------------
|
|
- name: Resolve trust_kdc_ip from cluster if not provided
|
|
ansible.builtin.command:
|
|
cmd: kubectl -n knoe-system get svc auth -o jsonpath='{.spec.clusterIP}'
|
|
delegate_to: localhost
|
|
become: false
|
|
register: _kdc_clusterip
|
|
when: trust_kdc_ip == ""
|
|
changed_when: false
|
|
check_mode: false
|
|
|
|
- name: Set trust_kdc_ip fact from cluster lookup
|
|
ansible.builtin.set_fact:
|
|
trust_kdc_ip: "{{ _kdc_clusterip.stdout | trim }}"
|
|
when: trust_kdc_ip == "" and _kdc_clusterip is defined and _kdc_clusterip.stdout is defined
|
|
|
|
- name: Assert trust_kdc_ip is set
|
|
ansible.builtin.assert:
|
|
that:
|
|
- trust_kdc_ip != ""
|
|
fail_msg: >
|
|
trust_kdc_ip is empty. Provide it via --extra-vars trust_kdc_ip=...
|
|
or ensure 'kubectl -n knoe-system get svc auth' succeeds from the
|
|
control node.
|
|
|
|
# ------------------------------------------------------------------
|
|
# 0b. Resolve trust_shared_password from the cluster Secret if not provided
|
|
# ------------------------------------------------------------------
|
|
- name: Resolve trust_shared_password from knoe-system/knoe-kdc-secrets
|
|
ansible.builtin.shell:
|
|
cmd: >
|
|
set -o pipefail;
|
|
kubectl -n knoe-system get secret knoe-kdc-secrets
|
|
-o jsonpath='{.data.trust_shared_password}' | base64 -d
|
|
executable: /bin/bash
|
|
delegate_to: localhost
|
|
become: false
|
|
register: _trust_pw
|
|
when: trust_shared_password == ""
|
|
changed_when: false
|
|
check_mode: false
|
|
no_log: true
|
|
|
|
- name: Set trust_shared_password fact from cluster Secret
|
|
ansible.builtin.set_fact:
|
|
trust_shared_password: "{{ _trust_pw.stdout | trim }}"
|
|
when: trust_shared_password == "" and _trust_pw is defined and _trust_pw.stdout is defined
|
|
no_log: true
|
|
|
|
- name: Assert trust_shared_password is set
|
|
ansible.builtin.assert:
|
|
that:
|
|
- trust_shared_password | length > 0
|
|
fail_msg: >
|
|
trust_shared_password is empty. Provide it via -e trust_shared_password=...
|
|
or ensure the Secret knoe-system/knoe-kdc-secrets has key
|
|
'trust_shared_password' populated by init_kdc.sh.
|
|
|
|
- name: Assert samba_admin_password is set
|
|
ansible.builtin.assert:
|
|
that:
|
|
- samba_admin_password | length > 0
|
|
fail_msg: >
|
|
samba_admin_password is empty. Decrypt with --vault-password-file .vault_pass
|
|
or set SAMBA_ADMIN_PASSWORD / -e samba_admin_password=...
|
|
|
|
# ------------------------------------------------------------------
|
|
# 1. Idempotency probe — does the inter-realm krbtgt user exist?
|
|
# ------------------------------------------------------------------
|
|
# NOTE: `samba-tool domain trust create --type=external` only works
|
|
# against another AD-style writeable DC. For an MIT KDC peer
|
|
# (which KNOE.LOCAL is — it's the in-cluster Heimdal/MIT KDC, not AD),
|
|
# the documented approach is to create the inter-realm TGT
|
|
# principal as an ordinary user account in Samba and share its
|
|
# password with the MIT KDC. The MIT side already has
|
|
# krbtgt/KNOE.LOCAL@PROLE.ORG (created by init_kdc.sh) using the
|
|
# same trust_shared_password we just fetched from the cluster Secret.
|
|
- name: Check whether {{ samba_account_name }} user already exists in Samba
|
|
ansible.builtin.command:
|
|
cmd: >
|
|
samba-tool user list
|
|
register: _samba_users
|
|
changed_when: false
|
|
failed_when: false
|
|
check_mode: false
|
|
|
|
- name: Set fact — inter-realm krbtgt user already present
|
|
ansible.builtin.set_fact:
|
|
_trust_exists: "{{ samba_account_name in _samba_users.stdout }}"
|
|
|
|
- name: Report trust pre-existence
|
|
ansible.builtin.debug:
|
|
msg: >-
|
|
{{ samba_account_name }} user
|
|
{{ 'already present in Samba — will reset password to match cluster Secret.'
|
|
if _trust_exists else 'is missing — will create.' }}
|
|
|
|
# ------------------------------------------------------------------
|
|
# 2. Add KNOE.LOCAL realm block to /etc/krb5.conf on myrddin
|
|
# ------------------------------------------------------------------
|
|
- name: Ensure {{ trust_realm }} realm block is present in /etc/krb5.conf
|
|
ansible.builtin.blockinfile:
|
|
path: /etc/krb5.conf
|
|
marker: "# {mark} ANSIBLE MANAGED BLOCK — {{ trust_realm }}"
|
|
insertafter: '^\[realms\]'
|
|
block: |
|
|
{{ trust_realm }} = {
|
|
kdc = {{ trust_kdc_ip }}
|
|
admin_server = {{ trust_kdc_ip }}
|
|
}
|
|
|
|
- name: Ensure [domain_realm] mapping prole-cluster → {{ trust_realm }}
|
|
ansible.builtin.blockinfile:
|
|
path: /etc/krb5.conf
|
|
marker: "# {mark} ANSIBLE MANAGED BLOCK — domain_realm {{ trust_realm }}"
|
|
insertafter: '^\[domain_realm\]'
|
|
block: |
|
|
.knoe.local = {{ trust_realm }}
|
|
knoe.local = {{ trust_realm }}
|
|
|
|
# ------------------------------------------------------------------
|
|
# 3. Create / re-sync the inter-realm krbtgt user
|
|
# ------------------------------------------------------------------
|
|
# Samba's default domain password policy rejects the cluster-generated
|
|
# trust_shared_password (it's high-entropy random bytes and may not
|
|
# satisfy the "3 of 4 character categories" complexity rule).
|
|
# We work around that by temporarily disabling complexity, doing the
|
|
# create + setpassword, then restoring the previous complexity state.
|
|
# The restore lives in an `always:` block so even a hard failure in
|
|
# the middle leaves Samba's policy intact.
|
|
#
|
|
# The "no_log" wrapper protects the password but also hides the
|
|
# error message on failure; if a real run dies inside the block,
|
|
# re-run the failing command by hand to see stderr (see playbook
|
|
# header).
|
|
- name: Read current password complexity setting
|
|
ansible.builtin.command:
|
|
cmd: samba-tool domain passwordsettings show
|
|
register: _pw_policy
|
|
changed_when: false
|
|
check_mode: false
|
|
|
|
- name: Set fact — password complexity currently on?
|
|
ansible.builtin.set_fact:
|
|
_pw_complexity_was_on: "{{ 'Password complexity: on' in _pw_policy.stdout }}"
|
|
|
|
- name: Create / re-sync krbtgt user (with complexity temporarily off)
|
|
block:
|
|
- name: Disable password complexity for trust-account write
|
|
ansible.builtin.command:
|
|
cmd: samba-tool domain passwordsettings set --complexity=off
|
|
when: _pw_complexity_was_on
|
|
changed_when: true
|
|
|
|
- name: Create inter-realm krbtgt user {{ samba_account_name }}
|
|
ansible.builtin.command:
|
|
argv:
|
|
- samba-tool
|
|
- user
|
|
- create
|
|
- "{{ samba_account_name }}"
|
|
- "{{ trust_shared_password }}"
|
|
- "--description=Inter-realm TGT for {{ trust_realm }} (MIT KDC) cross-realm trust"
|
|
- "--use-username-as-cn"
|
|
- "-U"
|
|
- "administrator%{{ samba_admin_password }}"
|
|
no_log: true
|
|
register: _krbtgt_create
|
|
changed_when: _krbtgt_create.rc == 0
|
|
when: not _trust_exists
|
|
|
|
- name: Re-sync {{ samba_account_name }} password with cluster Secret
|
|
ansible.builtin.command:
|
|
argv:
|
|
- samba-tool
|
|
- user
|
|
- setpassword
|
|
- "{{ samba_account_name }}"
|
|
- "--newpassword={{ trust_shared_password }}"
|
|
- "-U"
|
|
- "administrator%{{ samba_admin_password }}"
|
|
no_log: true
|
|
register: _krbtgt_setpw
|
|
changed_when: _krbtgt_setpw.rc == 0
|
|
when: _trust_exists
|
|
always:
|
|
- name: Restore password complexity to its prior state
|
|
ansible.builtin.command:
|
|
cmd: samba-tool domain passwordsettings set --complexity=on
|
|
when: _pw_complexity_was_on
|
|
changed_when: true
|
|
|
|
# ------------------------------------------------------------------
|
|
# 5. Disable password expiry on the krbtgt account
|
|
# ------------------------------------------------------------------
|
|
- name: Disable password expiry on {{ samba_account_name }}
|
|
ansible.builtin.command:
|
|
argv:
|
|
- samba-tool
|
|
- user
|
|
- setexpiry
|
|
- "{{ samba_account_name }}"
|
|
- "--noexpiry"
|
|
- "-U"
|
|
- "administrator%{{ samba_admin_password }}"
|
|
no_log: true
|
|
register: _krbtgt_noexpiry
|
|
changed_when: _krbtgt_noexpiry.rc == 0
|
|
failed_when: _krbtgt_noexpiry.rc != 0
|
|
|
|
# ------------------------------------------------------------------
|
|
# 6. Set the Kerberos principal name (UPN + SPN) and pin the
|
|
# supported encryption types to RC4 only.
|
|
#
|
|
# sAMAccountName cannot contain "/" so we stored the account
|
|
# under {{ samba_account_name }} (krbtgt_KNOE.LOCAL). For the
|
|
# Samba KDC to issue tickets for the canonical Kerberos name
|
|
# krbtgt/KNOE.LOCAL@PROLE.ORG, we set both:
|
|
#
|
|
# userPrincipalName = krbtgt/KNOE.LOCAL@PROLE.ORG
|
|
# servicePrincipalName = krbtgt/KNOE.LOCAL
|
|
#
|
|
# msDS-SupportedEncryptionTypes = 4 = RC4 only.
|
|
# AES uses a salt as part of key derivation; Samba salts as
|
|
# <remote_realm>+UPN, MIT salts as <local_realm>+<principal>.
|
|
# The salts do not match across vendors, so AES keys derived
|
|
# from the same shared password come out different on each
|
|
# side and TGT decryption fails. RC4 has no salt — keys depend
|
|
# only on the password — so both sides converge. The matching
|
|
# MIT-side cross-realm krbtgts are created with
|
|
# `-e arcfour-hmac:normal` in knoe-db/etc/init_kdc.sh.
|
|
# ------------------------------------------------------------------
|
|
- name: Locate ldbmodify binary
|
|
ansible.builtin.shell:
|
|
cmd: |
|
|
for p in /usr/bin/ldbmodify /usr/sbin/ldbmodify /usr/local/bin/ldbmodify /opt/samba/bin/ldbmodify; do
|
|
[ -x "$p" ] && { echo "$p"; exit 0; }
|
|
done
|
|
# Fallback: filesystem scan limited to common roots
|
|
found=$(find /usr /opt -maxdepth 4 -type f -name ldbmodify 2>/dev/null | head -1)
|
|
[ -n "$found" ] && { echo "$found"; exit 0; }
|
|
echo "ldbmodify not found" >&2
|
|
exit 1
|
|
register: _ldbmodify_path
|
|
changed_when: false
|
|
check_mode: false
|
|
|
|
- name: Set UPN, SPN, and supported encryption types on {{ samba_account_name }}
|
|
ansible.builtin.shell:
|
|
cmd: |
|
|
set -euo pipefail
|
|
ldif=$(mktemp)
|
|
DN=$(samba-tool user show "{{ samba_account_name }}" \
|
|
-U "administrator%{{ samba_admin_password }}" \
|
|
| awk -F': ' '/^dn:/ {print $2; exit}')
|
|
if [ -z "$DN" ]; then
|
|
echo "Could not resolve DN for {{ samba_account_name }}" >&2
|
|
exit 1
|
|
fi
|
|
cat > "$ldif" <<EOF
|
|
dn: $DN
|
|
changetype: modify
|
|
replace: userPrincipalName
|
|
userPrincipalName: {{ trust_principal }}
|
|
-
|
|
replace: servicePrincipalName
|
|
servicePrincipalName: krbtgt/{{ trust_realm }}
|
|
-
|
|
replace: msDS-SupportedEncryptionTypes
|
|
msDS-SupportedEncryptionTypes: 4
|
|
EOF
|
|
"{{ _ldbmodify_path.stdout | trim }}" -H /var/lib/samba/private/sam.ldb "$ldif"
|
|
rm -f "$ldif"
|
|
no_log: true
|
|
register: _krbtgt_enctype
|
|
changed_when: "'Modified 1 records' in (_krbtgt_enctype.stdout | default(''))"
|
|
failed_when: _krbtgt_enctype.rc != 0
|
|
|
|
# ------------------------------------------------------------------
|
|
# 7. Smoke probe — confirm the user is visible to samba-tool
|
|
# ------------------------------------------------------------------
|
|
- name: Smoke probe — samba-tool user show {{ samba_account_name }}
|
|
ansible.builtin.command:
|
|
argv:
|
|
- samba-tool
|
|
- user
|
|
- show
|
|
- "{{ samba_account_name }}"
|
|
- "-U"
|
|
- "administrator%{{ samba_admin_password }}"
|
|
no_log: true
|
|
register: _krbtgt_show
|
|
changed_when: false
|
|
check_mode: false
|
|
# In --check mode the create step is skipped, so on a first dry
|
|
# run the user doesn't exist yet — don't fail the play in that
|
|
# case. Real runs still fail hard on rc!=0.
|
|
failed_when:
|
|
- _krbtgt_show.rc != 0
|
|
- not (ansible_check_mode and not _trust_exists)
|
|
|
|
- name: Print summary
|
|
ansible.builtin.debug:
|
|
msg:
|
|
- "{{ samba_account_name }} present in Samba (PROLE.ORG realm)."
|
|
- "Password is now in sync with knoe-system/knoe-kdc-secrets/trust_shared_password."
|
|
- "msDS-SupportedEncryptionTypes set to 4 (RC4 only — matches MIT cross-realm krbtgt)."
|
|
- "From a PROLE.ORG client try:"
|
|
- " kdestroy && kinit chrisfu@PROLE.ORG"
|
|
- " kvno krbtgt/{{ trust_realm }}@PROLE.ORG # cross-realm TGT only"
|
|
- " kvno HTTP/auth.knoe.dev@{{ trust_realm }} # full service ticket"
|
|
- " klist # expect krbtgt/{{ trust_realm }}@PROLE.ORG (slash form)"
|