Categories
Linux Samba Windows

Samba AD, chrony and Windows 11: why signed NTP silently fails, and how to fix it

A Windows 11 domain member refuses to take its time from a Samba AD DC running chrony. Plain NTP works, the configuration is fine, yet the DC never answers. The cause: above functional level 2012, Windows uses the MS-SNTP extended authenticator, which neither chrony nor Samba support. I patched both, and after one more trap in the byte order, Windows now accepts the signed replies.

In an Active Directory domain, Windows clients take their time from a domain controller, and they insist on signed NTP replies. On a Samba AD DC, the NTP daemon (chrony or ntpd) does not sign anything itself: it passes each request through a Unix socket to Samba’s ntp_signd, which signs it with the client’s computer account key. That’s the theory. In my domain, a Windows 11 client simply refused to sync from the Samba DC. This post describes how I narrowed it down, what triggers the problem, how I patched Samba and chrony, and the byte-order trap in the specification that cost me the most time.

The general Windows side (w32tm commands, the debug log, time zone pitfalls, chrony as a time source) is in a separate article: Fixing time synchronisation on Windows (with chrony as the time source). Here I only cover what’s specific to Samba and MS-SNTP.

The short version

  • If the DC runs at functional level 2012 or higher, Windows clients send MS-SNTP requests with the extended authenticator (120 bytes of NTP instead of 68).
  • chrony recognises these requests and silently drops them (“Not supported yet”). No reply, no log line.
  • Samba’s ntp_signd can only produce the classic signature anyway, so switching to ntpd or NTPsec doesn’t help. This is the open Samba bug #14757, reported in 2021.
  • I asked Claude if there’s enough information/specifications available to patch Samba and chrony to sign extended requests, and the answer was “Yes”. And after some fine-tuning Claude really managed to create patches for both, so my Windows 11 25H2 client syncs from the Samba DC and logs the reply as authenticated successfully.
  • The hard part was not the cryptography but four bytes: the spec’s packet diagram shows the flag fields in the opposite order from what Windows sends and expects. Get them wrong and Windows fails with 0x80070032 before it even looks at the checksum.
  • The patches are not upstream yet. The series prepared for submission is in my forks: Samba and chrony, branch mssntp-ext (see status of the patches).

The setup

  • Samba AD DC in a container, chrony 4.8 as NTP server, domain and forest at functional level 2016
  • Windows 11 25H2 (build 26200) domain member in the default NT5DS mode (time from the domain hierarchy)

On top of the usual allow for the client network, the Samba-specific part of the chrony configuration is a single line:

ntpsigndsocket /var/lib/samba/ntp_signd

Step 1: what does the Windows client say?

The w32time debug log (how to enable it) is far more useful than the event log here. The interesting lines after a w32tm /resync /rediscover:

Domain member syncing from \\dc1.
Association: (Local) 0.0.0.0:123 => 192.168.1.33:123 (Remote)
Sending packet to dc1 (ntp.d|0.0.0.0:123->192.168.1.33:123) in Win2K detect mode, stage 1.
...
** NTP sample vector is empty.
No new NTP sample is available.
PeerPollingThread: waiting 1023.986s

So the client configuration is fine: it finds the DC through the domain hierarchy and sends a request. What’s missing is any sign of a reply. If the DC had answered with an unsigned or badly signed packet, w32time would log that it received and rejected it. Here there is just silence, and the next attempt is 1024 seconds later.

Step 2: plain NTP works

w32tm /stripchart /computer:dc1 sends ordinary, unauthenticated NTP requests (details in the general article). Offsets came back, so firewall, routing and chrony’s allow are fine. The DC answers plain NTP, but not the signed requests.

Step 3: the usual suspects on the DC

Most guides about this problem end with one of these, so I checked them all:

# ls -ld /var/lib/samba/ntp_signd; ls -l /var/lib/samba/ntp_signd/
drwxr-x--- 2 root _chrony 3 Oct 24 08:20 /var/lib/samba/ntp_signd
srwxrwxrwx 1 root root    0 Oct 24 08:20 socket

# ps -o user= -C chronyd
_chrony

# chronyd -v
chronyd (chrony) version 4.8 (+CMDMON +REFCLOCK +RTC +PRIVDROP +SCFILTER +SIGND +NTS +SECHASH +IPV6 -DEBUG)
  • The socket exists, so Samba’s ntp_signd service is running.
  • The directory belongs to the group chrony runs as (_chrony, mode 0750).
  • chrony is built with +SIGND and was started after the last config change.
  • Running in a container, AppArmor’s chronyd profile doesn’t apply.

Everything as it should be.

Step 4: look at the packet

# tcpdump -ni any -vv udp port 123 and host 192.168.1.104
13:45:31.612340 eth1  In  IP (tos 0x0, ttl 128, id 13974, offset 0, flags [none], proto UDP (17), length 148)
    192.168.1.104.123 > 192.168.1.33.123: [udp sum ok] NTPv3, Client, length 120
        Leap indicator: clock unsynchronized (192), Stratum 0 (unspecified), poll 17 (131072s), precision -23
        ...
        (72 more bytes after the header)

That’s the clue: 120 bytes of NTP, i.e. the 48-byte header plus 72 bytes of authenticator. And no reply.

The MS-SNTP specification defines two authenticator formats for NTPv3 packets:

AuthenticatorExtended authenticator
Size after the header20 bytes72 bytes
ContentsKey identifier (the computer account’s RID) + 16-byte checksumKey identifier, flags and hash hints + 64-byte checksum
ChecksumMD5 over NT hash and packetHMAC-SHA512 with a key derived from the NT hash
NTP length on the wire68 bytes120 bytes

My Windows 11 client uses the extended one.

What chrony does with it

chrony’s source is quite explicit. ntp_core.c classifies NTPv3 requests by the length of the trailing data:

if (info->version == 3 && info->auth.mac.key_id != 0) {
  if (remainder == 20 && is_zero_data(data + parsed + 4, remainder - 4))
    info->auth.mode = NTP_AUTH_MSSNTP;
  else if (remainder == 72 && is_zero_data(data + parsed + 8, remainder - 8))
    info->auth.mode = NTP_AUTH_MSSNTP_EXT;
}

and ntp_auth.c decides what happens to the request:

case NTP_AUTH_MSSNTP:
  /* MS-SNTP requests are not authenticated */
  break;
case NTP_AUTH_MSSNTP_EXT:
  /* Not supported yet */
  return 0;

Classic MS-SNTP requests are passed to ntp_signd for signing. Extended ones are dropped without a reply and without a log message. That matches exactly what I saw: the socket is never used, Samba logs nothing, chrony logs nothing.

Would ntpd or NTPsec help?

No, and the reason is on Samba’s side. Every Linux NTP daemon depends on ntp_signd, because only Samba has access to the computer account keys. In source4/ntp_signd/ntp_signd.c, Samba builds the reply as the packet, followed by the key identifier, followed by MD5(NT hash of the computer account ‖ packet): 20 bytes, the classic format. There is no code for the extended one.

NTPsec doesn’t drop the request, but it doesn’t sign it either. Its ntp_proto.c recognises the 72 bytes and strips them (“We don’t deal with the MS-SNTP fields, so just strip them”), so the request is treated as plain, unauthenticated NTP. An unsigned reply is no more acceptable to the Windows client than no reply at all.

The upstream bug

This is Samba bug 14757: “Support for MS-SNTP extended authenticator in ntp_signd”, opened in July 2021 and still in status NEW. The original report already names the core problem: chrony can detect the new packets, but has no way to pass the extra data to ntp_signd. Fixing it needs a new command or protocol version between the NTP daemon and Samba, so both projects have to move.

A recent comment on the bug (September 2026) describes the same situation with Samba 4.24 and chrony 4.8: a Windows Server 2025 client in NT5DS mode sends 120-byte requests, the DC never replies, and after eight attempts the client gives up and runs on its local CMOS clock. A thread on the samba mailing list from 2024 shows the same symptom with Windows 10/11 clients: only 120-byte requests in tcpdump, no replies, “Local CMOS Clock” as source.

What makes Windows use the extended format

It’s not the Windows version. Section 3.1.9 of the spec says:

If the selected DC has a DC functional level of DS_BEHAVIOR_WIN2012 or greater, then the ExtendedAuthenticatorSupported ADM element MUST be set to true; otherwise it MUST be set to false.

So it’s the DC functional level. The client learns it from the DC locator: in the Netlogon debug log of my client, the DC flags are 0xe001d37d, which include DS_DS_8_FLAG (0x4000), DS_DS_9_FLAG (0x8000) and DS_DS_10_FLAG (0x10000). Samba DCs at the classic default level 2008 R2 get 68-byte requests and sign them without problems. Recent Samba versions can raise the level to 2012 R2 or 2016 (ad dc functional level), and from then on Windows switches to the extended format.

Going back is only possible as long as the domain itself is still at 2008 R2, because a DC can’t run below the domain level. My domain is at 2016, so that door was closed.

Patching Samba and chrony

The extended format is documented, so I implemented it. Sections 3.2.5.1.1 and 3.1.5.5 describe the checksum. The spec leaves a few details open: which PRF the KDF uses, the output length, how the label is terminated and how the context is encoded. KerbWolf, a security tool that computes these checksums and says it was verified against Windows Server 2022 and 2025, fills the gaps. Windows accepting my signatures now confirms them:

K        = KDF_SP800-108 in counter mode, PRF = HMAC-SHA512, 512-bit output
             Key     = NT hash of the computer account
             Label   = "sntp-ms" including its terminating NUL
             Context = Key Identifier (the RID, 4 bytes little-endian)
Checksum = HMAC-SHA512(K, first 48 bytes of the response)

Conveniently, Samba already has an SP800-108 helper for SMB key derivation that produces exactly this layout. The changes are small:

  • chrony: instead of dropping NTP_AUTH_MSSNTP_EXT, pass the request to ntp_signd with a new signd operation and send the 120-byte reply. With an unpatched Samba, chrony drops the request as before.
  • Samba: a new operation in the ntp_signd socket protocol for extended requests. Samba checks that ClientHashIDHints announces NTLM_PWD_HASH, looks up the account by RID, derives K from the current password, computes the HMAC and returns a 120-byte reply with SignatureHashID = NTLM_PWD_HASH. Like Windows DCs, it always signs with the current password, even if the request asks for the previous one: the client checks the reply against both its current and previous password anyway, and answering unauthenticated requests with the previous password would only expose it. The classic path is unchanged, and an old ntp_signd simply refuses the new operation.

To test the DC without Windows, I used a small Python script that sends a 120-byte request and verifies the reply with the computer account’s NT hash. Against the patched DC:

Sent 120-byte ExtendedAuthenticator request to dc1:123
Got 120-byte reply
RESULT: MATCH - the DC signed with the password you supplied.

Looked good. Windows disagreed.

Where Windows stopped: 0x80070032

With the patches in place, tcpdump showed a 120-byte reply to every 120-byte request, and the w32time log confirmed the client received and parsed it. Then:

ListeningThread -- response heard from 192.168.1.33:123
| LeapIndicator: 0 - no warning;  VersionNumber: 3;  Mode: 4 - Server
| LocalClockOffset: -53933721800ns - 0:53.933721800s
Logging warning: NtpClient encountered an error while validating the computer account
for this machine, so NtpClient cannot determine whether the response received from dc1
has a valid signature. The response will be ignored. The error was: The request is not
supported. (0x80070032)
Can't verify packet because compute digest failed. Ignoring packet.

The client fails to compute its own expected checksum, before it even looks at ours. 0x80070032 is ERROR_NOT_SUPPORTED. I chased a few things that turned out to be irrelevant. They’re worth checking anyway, so here they are:

  • Clock skew. On the first attempts the client was exactly nine hours off because of a wrong time zone (and PowerShell’s per-process time zone cache made the fix look like it hadn’t worked; see the general article). After fixing it, the error stayed the same.
  • A broken secure channel. nltest /sc_verify succeeds.
  • Credential Guard. It’s not running on this client.
  • A Netlogon call that Samba refuses. The client’s Netlogon debug log (nltest /dbflag:0x2080ffff) shows no call to the DC during the resync, only the DC lookup. The error comes from inside the client.
  • Netlogon over Kerberos. The Netlogon log also showed that the client first tries the new Kerberos-based secure channel setup that came with Windows Server 2025, and falls back to the classic one when the DC refuses. Samba supports it since 4.22 with server support krb5 netlogon = yes (disabled by default). With that enabled, the secure channel came up via Kerberos, but w32time failed exactly as before.

The real cause: byte order

The extended authenticator starts with the 4-byte Key Identifier, followed by four 1-byte fields. Sections 2.2.3 and 2.2.4 of the spec draw them in this order:

Key Identifier (4) | Reserved (1) | Flags (1) | ClientHashIDHints (1) | SignatureHashID (1) | Checksum (64)

But the text describes each of these fields as “little-endian” with 32-bit constant values such as 0x00000001, which only makes sense if the four bytes are read together as one little-endian word. And that’s what Windows does on the wire: the order is reversed.

NTP byteSpec diagramWhat Windows sends and expects
52ReservedSignatureHashID
53FlagsClientHashIDHints
54ClientHashIDHintsFlags
55SignatureHashIDReserved

Read that way, everything falls into place. The real request carries 00 01 00 00 in bytes 52–55: ClientHashIDHints = NTLM_PWD_HASH, Flags = 0. Exactly what the spec requires. In an earlier version of this post I wrote that Windows sets USE_OLDKEY_VERSION and leaves ClientHashIDHints at 0, in violation of the spec. That was a misreading, mine and KerbWolf’s: we read the bytes in diagram order.

My first reply had 00 00 00 01, i.e. SignatureHashID at the position of the diagram. Windows read that as SignatureHashID = 0 and Reserved = 1, didn’t know which hash to use and gave up with ERROR_NOT_SUPPORTED, before comparing any checksum. With 01 00 00 00, it works:

Response received from domain controller dc1.example.com authenticated successfully (using signature format)
NTP Sample 553756864 chosen. ... NtpClient returned 1 samples.
Sample 0 offset:-54.1747816s delay:+00.0062486s

and w32tm /query /source shows the DC as the time source.

The lesson: these four bytes are not part of the HMAC input, which covers only the first 48 bytes. My test script only compared checksums, so it said MATCH while Windows rejected the reply. A self-made test tool shares your assumptions; only the real client could reveal this one.

A note on security

MS-SNTP requests are not authenticated. Anyone who is allowed to query the DC can ask for a signed reply for any RID. With the classic format, that reply allows offline cracking of the machine account’s password (“timeroasting”). The extended format uses HMAC-SHA512 instead of MD5, but the principle of the exposure is the same. So limit allow in chrony to the networks your clients are actually in. Machine account passwords are long random strings, so in practice this mostly matters for accounts with weak, manually set passwords.

Status of the patches

The patches work in my lab with Windows 11 25H2, but they’re not upstream yet. The cleaned-up series, prepared for submission to Samba (bug 14757) and chrony, is available as branches in my forks:

You need both: the chrony patch passes the extended requests to ntp_signd, and the Samba patch signs them. Until they’re merged, a stock Samba DC at level 2012 or higher can’t serve signed time to Windows clients.

Workarounds until the patches land upstream

  • Keep the DC at functional level 2008 R2, if your domain is still there. Then Windows uses the classic format, which stock Samba signs. This is the only option that keeps signed time without patches.
  • Unsigned NTP from the DC. Switch the clients from NT5DS to Type=NTP with the DC as manual server (dc1.example.com,0x8), e.g. via the Configure Windows NTP Client GPO. chrony happily answers plain requests, as the stripchart test showed. The settings and their traps are in the general article.
  • Replace the Windows time client, e.g. with Meinberg’s NTP for Windows, as suggested on the mailing list. If you want authentication, NTP symmetric keys or NTS between chrony and the clients are an option, but that means managing them yourself.
  • A Windows DC as time source, if you happen to have one in the domain.

The price of the unsigned options: anyone who can spoof NTP packets on the network can shift the clients’ clocks, and Kerberos stops working at five minutes of skew. On a trusted LAN that’s an acceptable risk for many setups. It’s still better than clients drifting on their CMOS clock until Kerberos breaks on its own.

Checklist for your own DC

  1. Rule out the basics first (network, allow, time zone, server synchronised) with the general checklist. If w32tm /stripchart gets offsets from the DC but the domain sync fails, continue here.
  2. Socket directory group and mode, ntpsigndsocket in the config, +SIGND in chronyd -v, AppArmor denials in the kernel log.
  3. samba-tool domain level show. At 2012 or higher, expect the extended format.
  4. tcpdump -vv udp port 123 on the DC while the client runs w32tm /resync /rediscover. 68 bytes of NTP: classic MS-SNTP, which chrony and Samba can sign, so look at the client’s w32time debug log and step 2. 120 bytes: the extended authenticator, and you’ve hit bug 14757.
  5. If you run patched versions, check the w32time debug log for authenticated successfully (using signature format). If you see 0x80070032 / “compute digest failed” instead, look at bytes 52–55 of the reply: SignatureHashID belongs in byte 52.

If you’re affected, consider adding your client versions, your domain level and a capture to Samba bug 14757. More real-world reports make it easier to get the fix merged.

Leave a Reply

Your email address will not be published. Required fields are marked *

Are you human? Please solve:Captcha