Kerberoasting: From TTP to a Rule You Can Actually Deploy
A full walkthrough - the protocol behaviour, the naive rule everyone writes first, why it drowns you, and the tuned version with its test cases.
Kerberoasting is the detection interview question for a reason. It is simple enough to explain in a paragraph, and hard enough that the obvious rule is unusable in a real domain.
The behaviour
Any authenticated domain user can request a service ticket for any account with a Service Principal Name. That is not a bug - it is how Kerberos works. The ticket is encrypted with a key derived from the service account’s password.
If the account is a machine account, the password is 120 random characters rotated every 30 days and cracking is hopeless. If it is a user account with an SPN - the SQL service account someone created in 2014 - the password may be Summer2019! and crack in seconds.
So the attack is: enumerate user accounts with SPNs, request tickets, take them offline, crack.
The detection hook is encryption type. Modern domains negotiate AES. RC4-HMAC (0x17) is weaker and faster to crack, so tooling explicitly requests it. A TGS-REQ downgraded to RC4 is the signal.
This does not require exploitation, elevation, or malware. The only thing that distinguishes the attacker from a normal user is pattern.
The naive rule
Everybody writes this first:
detection:
selection:
EventID: 4769
TicketEncryptionType: '0x17'
condition: selection
level: high
Deploy it in a 5,000-seat domain and you get roughly 40,000 events a day. It is technically correct and operationally worthless.
Three reasons:
Machine accounts dominate. Every computer requesting tickets for itself generates 4769s, and plenty negotiate RC4 depending on domain functional level. These end in $ and are noise.
krbtgt appears constantly. Normal TGT activity, nothing to do with roasting.
Legacy applications genuinely use RC4. That 2011 appliance, that Java service with an old JDK, that NAS. They will use RC4 forever and you cannot fix them.
Filtering to the signal
Machine accounts and krbtgt
filter_machine_accounts:
ServiceName|endswith: '$'
filter_krbtgt:
ServiceName: 'krbtgt'
Roughly a 95% volume reduction. Note the filter is on ServiceName - the account whose ticket was requested - not TargetUserName. Getting this backwards is the single most common mistake in this rule, and it silently inverts the logic: you filter out the requesting user instead of the machine target, keeping exactly the events you wanted to drop.
Ticket options
Rubeus and Impacket’s GetUserSPNs.py both set TicketOptions: 0x40810000 (forwardable, renewable, canonicalize). Normal Windows clients vary more.
TicketOptions: '0x40810000'
Worth a caveat: this is a tool artefact, not a protocol requirement. It works today against unmodified tooling. Anyone who edits one constant in Rubeus evades it. Treat it as a volume reducer that buys you fidelity against commodity tooling, not as the load-bearing part of the detection. If you are hunting a competent operator, drop this condition and accept the volume.
The legacy allowlist
Here is where teams go wrong. The instinct is to exclude the noisy service accounts. Do not - those are exactly the accounts an attacker wants, and an exclusion list becomes a target list for anyone who reads your repository.
Instead, allowlist the source hosts that legitimately talk to those services:
filter_known_legacy_pairs:
ServiceName:
- 'svc_legacy_nas'
- 'svc_jenkins_2011'
IpAddress:
- '10.20.4.15' # nas-gateway
- '10.20.4.16' # jenkins-primary
Now the SQL service account is still monitored from every host except the two that should be asking. An attacker roasting svc_legacy_nas from a workstation still fires.
This is more work to maintain and it is the right trade. Review it quarterly, and when a pair goes unused for two quarters, delete it.
The finished rule
title: Kerberoasting via RC4 Service Ticket Request
id: 8c1d4b0e-6f2a-4a71-9a3d-2f0b5c7e1a44
status: stable
description: >
Detects Kerberos TGS-REQ activity downgraded to RC4-HMAC, the encryption
type attackers force so service ticket hashes crack faster offline.
references:
- https://attack.mitre.org/techniques/T1558/003/
author: DetectionOps
date: 2026/08/19
logsource:
product: windows
service: security
detection:
selection:
EventID: 4769
TicketEncryptionType: '0x17'
TicketOptions: '0x40810000'
filter_machine_accounts:
ServiceName|endswith: '$'
filter_krbtgt:
ServiceName: 'krbtgt'
condition: selection and not 1 of filter_*
fields:
- ServiceName
- TargetUserName
- IpAddress
falsepositives:
- Legacy applications that genuinely negotiate RC4
- Domain functional level below 2008
- Authenticated vulnerability scanners
level: high
tags:
- attack.credential_access
- attack.t1558.003
The aggregation layer
The rule above catches single requests. Bulk roasting - the common case - is better caught by counting.
An attacker enumerating SPNs requests tickets for many distinct services from one host in a short window. Normal users touch two or three services an hour.
SigninLogs is the wrong source here; this is Windows Security 4769.
In Splunk:
index=wineventlog EventCode=4769 Ticket_Encryption_Type=0x17
| where NOT match(Service_Name, "\$$") AND Service_Name!="krbtgt"
| bin _time span=10m
| stats dc(Service_Name) as distinct_spns
values(Service_Name) as spns
by _time, Account_Name, Client_Address
| where distinct_spns >= 6
| sort - distinct_spns
Six distinct SPNs in ten minutes from one account. Tune the threshold against your own baseline - run it at >= 2 for a fortnight, look at the distribution, then set the threshold at roughly the 99.5th percentile of normal.
The aggregate rule catches loud tooling. The single-event rule catches the careful operator taking one ticket a day. Deploy both; they fail differently, which is the point.
Test cases
Three, minimum.
True positive - Rubeus detonation in the lab:
{
"_case_name": "rubeus-kerberoast-single-spn",
"EventID": 4769,
"ServiceName": "svc_sql_reporting",
"TargetUserName": "j.okafor@corp.local",
"TicketEncryptionType": "0x17",
"TicketOptions": "0x40810000",
"IpAddress": "10.40.12.88"
}
False positive - machine account:
{
"_case_name": "fp-machine-account-rc4",
"EventID": 4769,
"ServiceName": "WKSTN-4471$",
"TicketEncryptionType": "0x17",
"TicketOptions": "0x40810000"
}
False positive - AES is normal:
{
"_case_name": "fp-aes-service-ticket",
"EventID": 4769,
"ServiceName": "svc_sql_reporting",
"TicketEncryptionType": "0x12",
"TicketOptions": "0x40810000"
}
That third case matters more than it looks. It is the one that catches a future edit widening TicketEncryptionType into a wildcard.
Validate it
# Lab DC, isolated. Never in production.
Invoke-AtomicTest T1558.003 -TestNumbers 1 -GetPrereqs
Invoke-AtomicTest T1558.003 -TestNumbers 1
Invoke-AtomicTest T1558.003 -TestNumbers 1 -Cleanup
Then confirm three things, in order:
- The 4769 event reached the SIEM. If Kerberos auditing is not enabled on the DCs, nothing else matters. Check this first - it is the failure mode that silently invalidates the whole exercise.
- The rule fired.
- The alert reached a queue a human reads.
Re-run quarterly. This rule depends on domain functional level, DC audit policy, and the tooling’s choice of ticket options - all three drift.
Fix the underlying problem too
Detection is the backstop, not the fix. In parallel:
- Find the SPNs.
Get-ADUser -Filter {ServicePrincipalName -like "*"}- every user account returned is roastable. - Migrate to Group Managed Service Accounts. gMSAs have 240-character machine-managed passwords. Cracking is off the table.
- For accounts that cannot migrate, set a 30+ character password and disable RC4 on the account (
msDS-SupportedEncryptionTypes = 0x18, AES only).
Every account you migrate is one your detection no longer has to catch. A detection that never has to fire because the technique no longer works is the best outcome available, and it is worth saying out loud in a discipline that measures itself by alerts.