Enforce LDAP signing and channel binding on DCs
Roll out LDAP signing and channel binding with evidence: collect events 2887, 2889 and 3039, fix clients, then set LdapEnforceChannelBinding safely.
Relaying NTLM authentication into LDAP on a domain controller is the most direct route from "I can make a machine authenticate to me" to domain compromise. An attacker who relays a computer account into LDAP can configure resource-based constrained delegation on that computer. A relayed privileged user can add accounts to groups or grant DCSync rights. Tools like ntlmrelayx and KrbRelayUp automate exactly this, which is why LDAP protection is usually the first thing a pentest report asks for.
The NTLM and legacy protocols pillar lists the two registry values that fix it. This guide covers the rollout: how to collect the evidence from every DC, how to read events 2886 to 2889 and 3039 to 3041, what to change on each type of client, and how to move from audit to enforcement without an outage on a Monday morning.
How the two controls differ
| Control | Protects | DC setting | Registry value (NTDS\Parameters) |
|---|---|---|---|
| LDAP signing | SASL binds (NTLM, Kerberos) on 389 without TLS; also rejects cleartext simple binds | Domain controller: LDAP server signing requirements | LDAPServerIntegrity: 1 = none, 2 = require |
| LDAP channel binding | Binds over TLS (636 or StartTLS) | Domain controller: LDAP server channel binding token requirements | LdapEnforceChannelBinding: 0 = never, 1 = when supported, 2 = always |
Both settings live under:
Computer Configuration > Policies > Windows Settings > Security Settings >
Local Policies > Security OptionsChannel binding only exists for sessions protected by TLS, so it assumes your DCs have a valid LDAPS certificate, usually issued from the Domain Controller Authentication or Kerberos Authentication template. If LDAPS has never worked in your domain, fix that first; the AD CS hardening guide covers the templates.
Defaults depend on version. Older DCs ship with signing not required and channel binding set to never. Windows Server 2025 introduced stricter defaults for new deployments and an additional enforcement setting in Security Options, but upgraded and in-place environments keep whatever was configured before. Always read the effective registry values instead of assuming.
Measure: turn on the telemetry
Every DC logs summary events in the Directory Service log without any change:
| Event | Meaning |
|---|---|
| 2886 | Signing is not required on this DC (logged at startup and every 24 hours) |
| 2887 | Number of unsigned SASL binds and cleartext simple binds in the last 24 hours |
| 2888 | Signing is enforced: number of such binds rejected in the last 24 hours |
| 2889 | One client made an unsigned SASL bind or cleartext simple bind (diagnostic logging required) |
| 3039 | One client bound over TLS without a valid channel binding token (diagnostic logging required) |
| 3040 | Number of TLS binds without a binding token in the last 24 hours |
| 3041 | Channel binding is not enforced on this DC |
The per-client events 2889 and 3039 are the ones you need, and they require LDAP interface diagnostic logging at level 2. Enable it on every DC. It is a small volume on most domains, but raise the Directory Service log size so a week of data fits.
$dcs = (Get-ADDomainController -Filter *).HostName
Invoke-Command -ComputerName $dcs -ScriptBlock {
Set-ItemProperty -Path 'HKLM:\SYSTEM\CurrentControlSet\Services\NTDS\Diagnostics' `
-Name '16 LDAP Interface Events' -Value 2 -Type DWord
Limit-EventLog -LogName 'Directory Service' -MaximumSize 512MB
}Audit: build the remediation list
Collect 2889 and 3039 from all DCs for at least two weeks, including a month-end if finance systems bind to AD. Event 2889 stores the client address, the identity and the bind type in fixed positions: bind type 0 is an unsigned SASL bind, 1 is a simple bind without TLS.
$results = foreach ($dc in $dcs) {
Get-WinEvent -ComputerName $dc -FilterHashtable @{ LogName = 'Directory Service'; Id = 2889 } -ErrorAction SilentlyContinue |
ForEach-Object {
[PSCustomObject]@{
DC = $dc
Client = ($_.Properties[0].Value -split ':')[0]
Identity = $_.Properties[1].Value
BindType = @{ '0' = 'Unsigned SASL'; '1' = 'Simple (cleartext)' }[[string]$_.Properties[2].Value]
}
}
}
$results | Group-Object Client, Identity, BindType | Sort-Object Count -Descending |
Select-Object Count, Name | Export-Csv .\ldap-unsigned-clients.csv -NoTypeInformation
# Channel binding: list the raw 3039 messages per DC
foreach ($dc in $dcs) {
Get-WinEvent -ComputerName $dc -FilterHashtable @{ LogName = 'Directory Service'; Id = 3039 } -MaxEvents 200 -ErrorAction SilentlyContinue |
Select-Object @{n='DC';e={$dc}}, TimeCreated, Message
}If you forward events centrally (see Windows Event Forwarding), add both IDs to the DC subscription instead and query the collector.
Resolve each client IP to an owner. From experience, the list almost always contains the same categories:
- Printers and multifunction devices doing address book lookups with a simple bind on 389.
- Linux and appliance integrations (VPN concentrators, firewalls, wiki and ticketing tools, NAS) configured with a bind DN and password over plain LDAP.
- Java applications using JNDI with simple authentication.
- Scripts using
System.DirectoryServiceswithAuthenticationTypes.Noneor explicit basic auth. - Windows clients are rarely a problem: they negotiate signing by default ("Network security: LDAP client signing requirements" = Negotiate signing) and have supported channel binding for years.
Fix the clients
For each source, pick one of these, in order of preference:
- Use Kerberos or Negotiate SASL if the library supports it. Signing is then handled by the protocol.
- Use LDAPS (636) or StartTLS with the enterprise CA chain trusted on the client. This satisfies the signing requirement. For channel binding, the client library must also send a binding token; recent Java releases support this through a JNDI property, and current OpenLDAP-based clients vary by version, so test each one.
- Retire or replace the device if it can do neither. An MFP that can only bind in cleartext also exposes the password of its bind account to anyone on the network.
Rotate the password of every account that appeared in a cleartext simple bind: it has been sent over the network unprotected for as long as the integration existed. Where possible, move these integrations to dedicated low-privilege accounts, not reused service accounts.
Also set the client-side policy on Windows machines to require signing once your DCs enforce it, so a rogue LDAP server cannot downgrade them:
Network security: LDAP client signing requirements = Require signing
HKLM\SYSTEM\CurrentControlSet\Services\LDAP\LDAPClientIntegrity (DWORD) = 2Enforce in stages
- Channel binding to 1 (when supported) on all DCs. This protects every client that already sends tokens, which covers modern Windows, with almost no risk.
- Signing to required (
LDAPServerIntegrity= 2) once 2889 is quiet or only shows accepted, scheduled-for-removal sources. Do one DC first if you can steer the remaining clients to it, then the rest. - Channel binding to 2 (always) once 3039 is quiet.
Deploy the settings through a GPO linked to the Domain Controllers OU so a newly promoted DC inherits them. Both take effect without a reboot on current Windows Server versions, but plan a rolling restart of the NTDS service or the DCs in a maintenance window if you want certainty.
Verify
Invoke-Command -ComputerName $dcs -ScriptBlock {
$p = Get-ItemProperty 'HKLM:\SYSTEM\CurrentControlSet\Services\NTDS\Parameters'
[PSCustomObject]@{
DC = $env:COMPUTERNAME
LDAPServerIntegrity = $p.LDAPServerIntegrity
ChannelBinding = $p.LdapEnforceChannelBinding
}
} | Format-Table DC, LDAPServerIntegrity, ChannelBindingEvery DC should report 2 and 2. Event 2886 and 3041 should stop appearing after the next 24-hour cycle, and 2888 should appear with the count of rejected binds, ideally zero. A non-zero 2888 count after enforcement means a client you missed is failing: the diagnostic events will identify it. Once enforcement is stable, drop the diagnostic level back to 0, or keep it at 2 if you want a permanent alert on regressions.
Finally, re-run your pentest tooling or ask your red team to repeat the relay-to-LDAP test. The relay should fail at the bind.
What it breaks
- Simple binds on 389 without TLS are rejected once signing is required: MFP address books, older VPN appliances, some SIEM and IAM connectors, and in-house scripts. They must move to LDAPS or StartTLS.
- LDAPS clients without channel binding support fail once the value is 2: older Java runtimes, some embedded LDAP stacks and certain third-party identity bridges. Value 1 leaves them working, which is why it is an intermediate step.
- TLS inspection or load balancers in front of DCs that terminate TLS break channel binding by design, because the client's channel is not the DC's channel. Do not put DCs behind TLS-terminating devices.
- Certificate problems become outages: once integrations depend on LDAPS, an expired DC certificate stops them. Monitor DC certificate expiry and auto-enrollment.
Related reading: the NTLM & Legacy Protocols topic, the LDAP channel binding glossary entry, blocking authentication coercion to remove the triggers attackers relay from, and constrained delegation and RBCD for the delegation write that relay-to-LDAP is usually after.
Frequently asked questions
If I require LDAP signing, do I still need channel binding?
Yes. Signing protects SASL binds on port 389. Binds made over TLS, on 636 or after StartTLS, already count as protected, so signing is not checked there. An attacker can relay NTLM into an LDAPS session, and only channel binding ties the NTLM authentication to that specific TLS channel. Enforce both to close relay into LDAP completely.
Event 2889 is logged for an account using a simple bind. How do I fix it?
A simple bind sends the distinguished name and password to the DC, and on port 389 without TLS that means cleartext on the wire. Reconfigure the application to use LDAPS on 636 or StartTLS on 389 with the DC's certificate trusted, or switch it to a Negotiate/Kerberos SASL bind if the library supports it. Many appliances only need a checkbox and a CA certificate import.
Is it safe to go straight to LdapEnforceChannelBinding = 2?
Only once you have evidence. Value 1 enforces channel binding for clients that advertise support and leaves older clients alone, so it is the safe intermediate step. Move to 2 after event 3039 has been quiet across all DCs for a full business cycle, or after every remaining source is decommissioned or moved to Kerberos. Value 2 rejects any TLS bind without a valid binding token.
Enforce LDAP signing and channel binding on DCs