This is an allow-list map, not an S4U cookbook. Target is a fake lab forest LAB.INTERNAL with a two-tier app: WEB01$ may present delegated credentials to HTTP/api.lab.internal and CIFS/fs01.lab.internal. Goal: setspn the accounts, klist a real user TGT plus the HTTP ticket from a browser hit, and draw who may hop where. I do not run Rubeus / Kekeo, I do not request S4U2Self, I do not forge a ticket, I do not /ptt.

1Figure 1. Constrained delegation is an allow-list on the front-end. Protocol transition is a separate bit.
2labuser --Kerberos--> HTTP/web01
3                         WEB01$  msDS-AllowedToDelegateTo:
4                           HTTP/api.lab.internal
5                           CIFS/fs01.lab.internal
6                         (TrustedToAuthForDelegation = False)

Lab layout

1labs/kcd_lab/
2  setspn.txt
3  klist-labuser.txt
4  getad-web01.txt
5  4769.txt

Domain LAB.INTERNAL, DC dc01.lab.internal. Person labuser. Front-end computer WEB01$. Back-ends API01$ (HTTP/api.lab.internal) and FS01$ (CIFS/fs01.lab.internal). Isolated VMs. SIDs [REDACTED].

Artifact: setspn

 1C:\lab> setspn -L WEB01
 2Registered ServicePrincipalNames for CN=WEB01,OU=servers,DC=lab,DC=internal:
 3    HTTP/web01.lab.internal
 4    HTTP/web01
 5    HOST/web01.lab.internal
 6    HOST/web01
 7
 8C:\lab> setspn -L API01
 9Registered ServicePrincipalNames for CN=API01,OU=servers,DC=lab,DC=internal:
10    HTTP/api.lab.internal
11    HTTP/api
12    HOST/api01.lab.internal
13    HOST/api01
14
15C:\lab> setspn -L FS01
16Registered ServicePrincipalNames for CN=FS01,OU=servers,DC=lab,DC=internal:
17    CIFS/fs01.lab.internal
18    CIFS/fs01
19    HOST/fs01.lab.internal
20    HOST/fs01
21
22C:\lab> setspn -Q HTTP/api.lab.internal
23Checking domain DC=lab,DC=internal
24CN=API01,OU=servers,DC=lab,DC=internal
25    HTTP/api.lab.internal
26    HTTP/api
27Existing SPN found!

One object per HTTP SPN. A duplicate setspn -Q hit would be a map error, not a KCD feature.

Artifact: the allow-list on WEB01$

1PS C:\lab> Get-ADComputer WEB01 -Properties TrustedForDelegation,
2            TrustedToAuthForDelegation, msDS-AllowedToDelegateTo,
3            userAccountControl |
4            fl Name, TrustedForDelegation, TrustedToAuthForDelegation,
5               msDS-AllowedToDelegateTo, userAccountControl
1Name                       : WEB01
2TrustedForDelegation       : False
3TrustedToAuthForDelegation : False
4msDS-AllowedToDelegateTo   : {HTTP/api.lab.internal, CIFS/fs01.lab.internal}
5userAccountControl         : 4096

Decode I write on the ticket:

FieldValueMeaning
TrustedForDelegationFalsenot unconstrained
TrustedToAuthForDelegationFalseno protocol transition (no S4U2Self for arbitrary users from this bit)
msDS-AllowedToDelegateToHTTP/api, CIFS/fs01classic constrained allow-list
UAC4096WORKSTATION_TRUST_ACCOUNT

RBCD on the targets, for the same hop counted from the other end:

1PS C:\lab> Get-ADComputer API01,FS01 -Properties msDS-AllowedToActOnBehalfOfOtherIdentity |
2            select Name, msDS-AllowedToActOnBehalfOfOtherIdentity
3Name  msDS-AllowedToActOnBehalfOfOtherIdentity
4----  ----------------------------------------
5API01                                         # empty in this lab
6FS01                                          # empty in this lab

Classic constrained only. The other note in this series covers RBCD leftovers. This map is one plane.

Artifact: klist as the user (no forged tickets)

Logon as labuser on LABPC01, browse https://web01.lab.internal/. Cache:

 1C:\lab> klist
 2Current LogonId is 0:0x[REDACTED]
 3
 4Cached Tickets: (2)
 5
 6#0>     Client: labuser @ LAB.INTERNAL
 7        Server: krbtgt/LAB.INTERNAL @ LAB.INTERNAL
 8        KerbTicket Encryption Type: AES-256-CTS-HMAC-SHA1-96
 9        Ticket Flags 0x40e10000 -> forwardable renewable initial pre_authent
10        Start Time: 3/17/2020 10:41:02 (local)
11        End Time:   3/17/2020 20:41:02 (local)
12        Renew Time: 3/24/2020 10:41:02 (local)
13        Session Key Type: AES-256-CTS-HMAC-SHA1-96
14        Cache Flags: 0x1 -> PRIMARY
15        Kdc Called: dc01.lab.internal
16
17#1>     Client: labuser @ LAB.INTERNAL
18        Server: HTTP/web01.lab.internal @ LAB.INTERNAL
19        KerbTicket Encryption Type: AES-256-CTS-HMAC-SHA1-96
20        Ticket Flags 0x40a10000 -> forwardable renewable pre_authent
21        Start Time: 3/17/2020 10:41:18 (local)
22        End Time:   3/17/2020 20:41:02 (local)
23        Session Key Type: AES-256-CTS-HMAC-SHA1-96
24        Cache Flags: 0
25        Kdc Called: dc01.lab.internal

#0 is the TGT (initial). #1 is the front-end service ticket. There is no HTTP/api ticket on the workstation. The hop, if the app is working, happens on WEB01 with the constrained allow-list. I do not dump klist as WEB01$ after an S4U. I dump 4769 on the DC instead.

Linux klist from the same user (SSSD lab join), same times:

1$ klist
2Ticket cache: FILE:/tmp/krb5cc_[REDACTED]
3Default principal: labuser@LAB.INTERNAL
4
5Valid starting       Expires              Service principal
603/17/20 10:41:02    03/17/20 20:41:02    krbtgt/LAB.INTERNAL@LAB.INTERNAL
7        renew until 03/24/20 10:41:02
803/17/20 10:41:18    03/17/20 20:41:02    HTTP/web01.lab.internal@LAB.INTERNAL

4769: the hop as the DC saw it

A healthy app request, one user, one front-end, then one back-end. Redacted:

 1Event 4769  A Kerberos service ticket was requested.
 2Account Information:
 3  Account Name:        labuser@LAB.INTERNAL
 4  Account Domain:      LAB.INTERNAL
 5  Logon ID:            0x[REDACTED]
 6Service Information:
 7  Service Name:        HTTP/web01.lab.internal
 8  Service ID:          S-1-5-21-[REDACTED]-1112
 9Network Information:
10  Client Address:      ::ffff:10.0.10.50
11  Client Port:         [REDACTED]
12Additional Information:
13  Ticket Options:      0x40810000
14  Ticket Encryption Type: 0x12
15  Failure Code:        0x0
16  Transited Services:  -

Second 4769, a few milliseconds later, from WEB01’s address, still Account Name: labuser@LAB.INTERNAL, Service Name: HTTP/api.lab.internal. That pair is the constrained hop: same user, different client address, service on the allow-list. I do not mint the second ticket by hand.

1Event 4769
2  Account Name:        labuser@LAB.INTERNAL
3  Service Name:        HTTP/api.lab.internal
4  Client Address:      ::ffff:10.0.10.20      # WEB01, not LABPC01
5  Ticket Encryption Type: 0x12
6  Failure Code:        0x0
7  Transited Services:  HTTP/web01.lab.internal   # present on some builds

If Transited Services is populated, it names the front-end. If it is -, I still have client address = WEB01 plus service = allow-list member.

A failed hop is also useful. I pointed the lab app at CIFS/dc01.lab.internal, which is not on msDS-AllowedToDelegateTo. The DC answered:

1Event 4769
2  Account Name:        labuser@LAB.INTERNAL
3  Service Name:        cifs/dc01.lab.internal
4  Client Address:      ::ffff:10.0.10.20
5  Failure Code:        0xC          # KDC_ERR_BADOPTION  (constrained deny)
6  Ticket Encryption Type: 0xFFFFFFFF

0xC here is the allow-list working. I do not then widen the list to make the call succeed.

What I do not file as success: a 4769 for krbtgt with odd flags, a 4769 for CIFS/dc01 from WEB01$ with result 0, a ticket I created with a forged PAC.

Map I actually keep

1principal          edges
2-----------------  --------------------------------------------------
3labuser            AS -> krbtgt/LAB ; TGS -> HTTP/web01
4WEB01$             AllowedToDelegateTo -> HTTP/api , CIFS/fs01
5                   TrustedToAuthForDelegation = False
6API01$             SPN HTTP/api.lab.internal ; RBCD empty
7FS01$              SPN CIFS/fs01.lab.internal ; RBCD empty
8who can write      Domain Admins on these attributes in this lab
9                   (if Account Operators can write them, that is a finding)

S4U2Self / S4U2Proxy as ideas, not as commands:

  • S4U2Self: front-end asks a ticket to itself for a given user. Needed when the front door was not Kerberos. Requires TrustedToAuthForDelegation in the classic model. False here, so this lab app must receive Kerberos from the user (it did — klist #1).
  • S4U2Proxy: front-end asks a ticket to a different SPN on the allow-list. That is the second 4769.

Analysts who can fill the table do not need a tool screenshot to argue with an app owner.

Mitigation

  • Prefer an allow-list of two SPNs with owners over a dump of thirty.
  • Keep TrustedToAuthForDelegation off unless the front door is documented non-Kerberos. This lab’s False is the desired default.
  • 5136 on msDS-AllowedToDelegateTo and on userAccountControl bits 0x80000 / 0x1000000.
  • 4769 from a mid-tier host for many distinct Account Name values toward a new SPN: investigate. I do not generate that pattern.
  • Protect write ACL on those attributes like privileged-group membership.
  • Unconstrained (TrustedForDelegation) is a different plane — see the delegation-edge-cases note. This map assumes it is False on WEB01 (it is).
1PS C:\lab> Get-ADComputer -Filter { msDS-AllowedToDelegateTo -like '*' } |
2            select Name, msDS-AllowedToDelegateTo
3Name  msDS-AllowedToDelegateTo
4----  ------------------------
5WEB01 {HTTP/api.lab.internal, CIFS/fs01.lab.internal}

One row in a tiny lab. Production: rank rows by whether the target SPN is a DC, SQL, or a file server with secrets.

What I file after this lab

  • setspn -L WEB01 / API01 / FS01; -Q HTTP/api one hit
  • WEB01: constrained to HTTP/api, CIFS/fs01; no unconstrained; no protocol transition
  • klist labuser: TGT + HTTP/web01, AES-256, no forged tickets
  • 4769 pair: HTTP/web01 from 10.0.10.50, then HTTP/api from 10.0.10.20
  • Negative: 4769 cifs/dc01 from WEB01 → Failure Code 0xC (not on allow-list)
  • Out of scope: S4U2Self/S4U2Proxy tooling, PAC forge, /ptt, RBCD write

Commands appendix

1setspn -L WEB01
2setspn -Q HTTP/api.lab.internal
3klist
4Get-ADComputer WEB01 -Properties TrustedForDelegation, TrustedToAuthForDelegation,
5  msDS-AllowedToDelegateTo
6Get-ADComputer -Filter { msDS-AllowedToDelegateTo -like '*' } |
7  select Name, msDS-AllowedToDelegateTo
8wevtutil qe Security /q:"*[System[(EventID=4769)]]" /c:8 /f:text