Skip to content
04 · NTLM & Legacy ProtocolsPart 2 of 5Intermediate

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.

Florian Amette7 min read

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

ControlProtectsDC settingRegistry value (NTDS\Parameters)
LDAP signingSASL binds (NTLM, Kerberos) on 389 without TLS; also rejects cleartext simple bindsDomain controller: LDAP server signing requirementsLDAPServerIntegrity: 1 = none, 2 = require
LDAP channel bindingBinds over TLS (636 or StartTLS)Domain controller: LDAP server channel binding token requirementsLdapEnforceChannelBinding: 0 = never, 1 = when supported, 2 = always

Both settings live under:

Text
Computer Configuration > Policies > Windows Settings > Security Settings >
  Local Policies > Security Options

Channel 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:

EventMeaning
2886Signing is not required on this DC (logged at startup and every 24 hours)
2887Number of unsigned SASL binds and cleartext simple binds in the last 24 hours
2888Signing is enforced: number of such binds rejected in the last 24 hours
2889One client made an unsigned SASL bind or cleartext simple bind (diagnostic logging required)
3039One client bound over TLS without a valid channel binding token (diagnostic logging required)
3040Number of TLS binds without a binding token in the last 24 hours
3041Channel 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.

PowerShell
$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.

PowerShell
$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.DirectoryServices with AuthenticationTypes.None or 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:

  1. Use Kerberos or Negotiate SASL if the library supports it. Signing is then handled by the protocol.
  2. 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.
  3. 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:

Text
Network security: LDAP client signing requirements = Require signing
HKLM\SYSTEM\CurrentControlSet\Services\LDAP\LDAPClientIntegrity (DWORD) = 2

Enforce in stages

  1. 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.
  2. 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.
  3. 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

PowerShell
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, ChannelBinding

Every 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

Related guides

NTLM & Legacy Protocols

Require SMB signing and disable SMBv1 domain-wide

Enforce SMB signing on clients and servers, understand Windows 11 24H2 and Server 2025 defaults, audit SMBv1 use, and remove it without breaking file access.

Foundation