--- # 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: # ------------------------------------------------------------------ # 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: "{{ ('krbtgt/' + trust_realm) 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 force supported # encryption types to AES128+AES256+RC4. # # 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 = 28 = 0x04 (RC4) | 0x08 (AES128) | 0x10 (AES256) # must match what the MIT KDC offers (init_kdc.sh keys the # cross-realm krbtgt with AES256). # ------------------------------------------------------------------ - 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" <