chrony_allowed_networks contained only 10.2.0.0/16, so the rendered chrony.conf permitted IPv4 clients and nothing else.
IPv6 is the primary address family here — ntp.srv.renner.uno resolves to fd40::123 — so an IPv4-only allow list refuses every client that uses the documented name.
This was live but invisible
The hosts carried allow fd00::/8 in chrony.conf for a long time without it ever being in this repository. Applying the role on 2026-08-07 rendered the template over the live config and dropped the line on all four NTP servers at once, taking internal NTP down over IPv6 until it was added here.
Same class of untracked host state as the anycast service address (#469, #470), and the same failure: config that exists only on the box survives right up until something re-renders it. Reading the drifted file was not enough — the rest of it needed diffing against what the template would produce, and I didn't do that.
install-ntp-ha.yml reported the v6 master as "holds the IPv6 service address ... it is the v6 MASTER and is serving ntp.srv.renner.uno" throughout the outage. Address ownership was correct; the service was refusing clients.
It was caught by querying the service instead:
chronyd -Q "server ntp.srv.renner.uno iburst"
-> No suitable source for synchronisation
and after the fix:
chronyd -Q "server ntp.srv.renner.uno iburst"
-> System clock wrong by 0.001999 seconds (ignored)
with a control query against ptbtime1.ptb.de first, to establish that the method rather than the target was sound.
fe80::/10
Included as well: a client on the same segment may source its query from a link-local address, and without it chrony drops the request — which presents as the server being unreachable rather than as a policy refusal.
State
Already applied to all four NTP servers. ntp.srv.renner.uno is serving; jupiter is v6 MASTER, saturn/neptune/edge-1 are BACKUP. This PR is the code catching up with what was done to restore service.
`chrony_allowed_networks` contained only `10.2.0.0/16`, so the rendered `chrony.conf` permitted IPv4 clients and nothing else.
IPv6 is the primary address family here — `ntp.srv.renner.uno` resolves to `fd40::123` — so an IPv4-only allow list refuses **every client that uses the documented name**.
## This was live but invisible
The hosts carried `allow fd00::/8` in `chrony.conf` for a long time without it ever being in this repository. Applying the role on 2026-08-07 rendered the template over the live config and **dropped the line on all four NTP servers at once**, taking internal NTP down over IPv6 until it was added here.
Same class of untracked host state as the anycast service address (#469, #470), and the same failure: config that exists only on the box survives right up until something re-renders it. Reading the drifted file was not enough — the rest of it needed diffing against what the template would produce, and I didn't do that.
Confirmed from the backup the role itself kept:
```
# /etc/chrony/chrony.conf.613631.2026-08-07@19:19:45~
16:allow 10.2.0.0/16
17:allow fd00::/8 <- present before, gone after
```
## The play did not catch it
`install-ntp-ha.yml` reported the v6 master as *"holds the IPv6 service address ... it is the v6 MASTER and is serving ntp.srv.renner.uno"* throughout the outage. Address ownership was correct; the service was refusing clients.
It was caught by querying the service instead:
```
chronyd -Q "server ntp.srv.renner.uno iburst"
-> No suitable source for synchronisation
```
and after the fix:
```
chronyd -Q "server ntp.srv.renner.uno iburst"
-> System clock wrong by 0.001999 seconds (ignored)
```
with a control query against `ptbtime1.ptb.de` first, to establish that the method rather than the target was sound.
## fe80::/10
Included as well: a client on the same segment may source its query from a link-local address, and without it chrony drops the request — which presents as the server being unreachable rather than as a policy refusal.
## State
Already applied to all four NTP servers. `ntp.srv.renner.uno` is serving; jupiter is v6 MASTER, saturn/neptune/edge-1 are BACKUP. This PR is the code catching up with what was done to restore service.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
chrony_allowed_networks contained only 10.2.0.0/16, so the rendered
chrony.conf permitted IPv4 clients and nothing else. IPv6 is the primary
address family here: ntp.srv.renner.uno resolves to fd40::123, so an
IPv4-only allow list refuses every client that uses the documented name.
This was live but invisible. The hosts carried `allow fd00::/8` in
chrony.conf for a long time without it ever being in this repository.
Applying the role on 2026-08-07 rendered the template over the live config
and dropped the line on all four NTP servers at once, taking internal NTP
down over IPv6 until it was added here.
Same class of untracked host state as the anycast service address, and the
same failure: config that exists only on the box survives right up until
something re-renders it. Reading the drifted file is not enough -- the rest
of it has to be diffed against what the template would produce.
The outage was not caught by the play, which reported the v6 master as
"serving ntp.srv.renner.uno" throughout. It was caught by querying the
service:
chronyd -Q "server ntp.srv.renner.uno iburst"
-> No suitable source for synchronisation
and after the fix:
chronyd -Q "server ntp.srv.renner.uno iburst"
-> System clock wrong by 0.001999 seconds (ignored)
with a control query against ptbtime1.ptb.de to confirm the method rather
than the target was sound.
fe80::/10 is included as well: a client on the same segment may source its
query from a link-local address, and without it chrony drops the request,
which presents as the server being unreachable rather than as a policy
refusal.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
🤖Automated review — model CLI default (complexity: complex), commit 24ed2485c9375da73e242dc2cbbb9b82c069a04e.
Advisory review by the review-bot pipeline. It never approves; a human merges. Push new commits to get a fresh review.
Summary
Adds fd00::/8 (ULA) and fe80::/10 (link-local) to chrony_allowed_networks in ansible/roles/chrony-ntp-server/defaults/main.yml, restoring IPv6 client access that was previously only present as untracked drift on the live hosts. The change is small, correctly scoped, and matches the one place this variable is consumed (templates/chrony.conf.j2:23-25, rendered via a plain {% for %} loop with no other transformation). No other file in the repo references chrony_allowed_networks or duplicates an IPv4-only assumption for NTP, and there's no separate firewall/nftables layer gating UDP/123 that would also need updating — the allow directive is the sole access control here. No blocking issues found.
🟡 Suggestions
fd00::/8 is broad — it permits any ULA-addressed host on any network, not just this fleet's addressing. The service VIP is fd40::123 (ansible/inventory/group_vars/ntp_cluster.yml:11) and clients are presumably numbered from a narrower ULA prefix (e.g. fd40::/16 or similar, based on the 10.2.0.0/16 IPv4 analog already in this list). If the actual client range is known and narrower, scoping to it would keep the allow list as tight as the IPv4 entry it sits next to, rather than opening to the entire ULA space (fc00::/7 minus fd00::/8's complement). Not a security issue in a homelab/private-network context, just an inconsistency in precision versus the existing IPv4 line.
The PR body's "fe80::/10" rationale (link-local queries from same-segment clients) is sound, but worth double-checking in the checkout: chrony_upstream_pools/chrony_upstream_servers sourcing and bindcmdaddress/cmdallow are IPv4/loopback-only and unaffected by this change — confirmed by reading templates/chrony.conf.j2:42-47, no action needed, just noting the blast radius is contained to the allow block as intended.
Tests
No test harness (molecule or otherwise) exists for Ansible roles anywhere in this repo, so the lack of tests here is consistent with repo convention, not a gap introduced by this PR. The change was validated operationally per the PR description (chronyd -Q against the service address before/after, on already-applied hosts), which is reasonable evidence for a config-only default change.
Recommendation
Looks reasonable — no blocking issues; comments above are optional.
🤖 **Automated review** — model `CLI default` (complexity: complex), commit `24ed2485c9375da73e242dc2cbbb9b82c069a04e`.
> Advisory review by the `review-bot` pipeline. It never approves; a human merges. Push new commits to get a fresh review.
---
## Summary
Adds `fd00::/8` (ULA) and `fe80::/10` (link-local) to `chrony_allowed_networks` in `ansible/roles/chrony-ntp-server/defaults/main.yml`, restoring IPv6 client access that was previously only present as untracked drift on the live hosts. The change is small, correctly scoped, and matches the one place this variable is consumed (`templates/chrony.conf.j2:23-25`, rendered via a plain `{% for %}` loop with no other transformation). No other file in the repo references `chrony_allowed_networks` or duplicates an IPv4-only assumption for NTP, and there's no separate firewall/nftables layer gating UDP/123 that would also need updating — the `allow` directive is the sole access control here. No blocking issues found.
## 🟡 Suggestions
- `fd00::/8` is broad — it permits any ULA-addressed host on any network, not just this fleet's addressing. The service VIP is `fd40::123` (`ansible/inventory/group_vars/ntp_cluster.yml:11`) and clients are presumably numbered from a narrower ULA prefix (e.g. `fd40::/16` or similar, based on the `10.2.0.0/16` IPv4 analog already in this list). If the actual client range is known and narrower, scoping to it would keep the allow list as tight as the IPv4 entry it sits next to, rather than opening to the entire ULA space (`fc00::/7` minus `fd00::/8`'s complement). Not a security issue in a homelab/private-network context, just an inconsistency in precision versus the existing IPv4 line.
- The PR body's "fe80::/10" rationale (link-local queries from same-segment clients) is sound, but worth double-checking in the checkout: `chrony_upstream_pools`/`chrony_upstream_servers` sourcing and `bindcmdaddress`/`cmdallow` are IPv4/loopback-only and unaffected by this change — confirmed by reading `templates/chrony.conf.j2:42-47`, no action needed, just noting the blast radius is contained to the `allow` block as intended.
## Tests
No test harness (molecule or otherwise) exists for Ansible roles anywhere in this repo, so the lack of tests here is consistent with repo convention, not a gap introduced by this PR. The change was validated operationally per the PR description (`chronyd -Q` against the service address before/after, on already-applied hosts), which is reasonable evidence for a config-only default change.
## Recommendation
Looks reasonable — no blocking issues; comments above are optional.
<!-- pr-review:sha=24ed2485c9375da73e242dc2cbbb9b82c069a04e -->
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
chrony_allowed_networkscontained only10.2.0.0/16, so the renderedchrony.confpermitted IPv4 clients and nothing else.IPv6 is the primary address family here —
ntp.srv.renner.unoresolves tofd40::123— so an IPv4-only allow list refuses every client that uses the documented name.This was live but invisible
The hosts carried
allow fd00::/8inchrony.conffor a long time without it ever being in this repository. Applying the role on 2026-08-07 rendered the template over the live config and dropped the line on all four NTP servers at once, taking internal NTP down over IPv6 until it was added here.Same class of untracked host state as the anycast service address (#469, #470), and the same failure: config that exists only on the box survives right up until something re-renders it. Reading the drifted file was not enough — the rest of it needed diffing against what the template would produce, and I didn't do that.
Confirmed from the backup the role itself kept:
The play did not catch it
install-ntp-ha.ymlreported the v6 master as "holds the IPv6 service address ... it is the v6 MASTER and is serving ntp.srv.renner.uno" throughout the outage. Address ownership was correct; the service was refusing clients.It was caught by querying the service instead:
and after the fix:
with a control query against
ptbtime1.ptb.defirst, to establish that the method rather than the target was sound.fe80::/10
Included as well: a client on the same segment may source its query from a link-local address, and without it chrony drops the request — which presents as the server being unreachable rather than as a policy refusal.
State
Already applied to all four NTP servers.
ntp.srv.renner.unois serving; jupiter is v6 MASTER, saturn/neptune/edge-1 are BACKUP. This PR is the code catching up with what was done to restore service.🤖 Generated with Claude Code
chrony_allowed_networks contained only 10.2.0.0/16, so the rendered chrony.conf permitted IPv4 clients and nothing else. IPv6 is the primary address family here: ntp.srv.renner.uno resolves to fd40::123, so an IPv4-only allow list refuses every client that uses the documented name. This was live but invisible. The hosts carried `allow fd00::/8` in chrony.conf for a long time without it ever being in this repository. Applying the role on 2026-08-07 rendered the template over the live config and dropped the line on all four NTP servers at once, taking internal NTP down over IPv6 until it was added here. Same class of untracked host state as the anycast service address, and the same failure: config that exists only on the box survives right up until something re-renders it. Reading the drifted file is not enough -- the rest of it has to be diffed against what the template would produce. The outage was not caught by the play, which reported the v6 master as "serving ntp.srv.renner.uno" throughout. It was caught by querying the service: chronyd -Q "server ntp.srv.renner.uno iburst" -> No suitable source for synchronisation and after the fix: chronyd -Q "server ntp.srv.renner.uno iburst" -> System clock wrong by 0.001999 seconds (ignored) with a control query against ptbtime1.ptb.de to confirm the method rather than the target was sound. fe80::/10 is included as well: a client on the same segment may source its query from a link-local address, and without it chrony drops the request, which presents as the server being unreachable rather than as a policy refusal. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>🤖 Automated review — model
CLI default(complexity: complex), commit24ed2485c9375da73e242dc2cbbb9b82c069a04e.Summary
Adds
fd00::/8(ULA) andfe80::/10(link-local) tochrony_allowed_networksinansible/roles/chrony-ntp-server/defaults/main.yml, restoring IPv6 client access that was previously only present as untracked drift on the live hosts. The change is small, correctly scoped, and matches the one place this variable is consumed (templates/chrony.conf.j2:23-25, rendered via a plain{% for %}loop with no other transformation). No other file in the repo referenceschrony_allowed_networksor duplicates an IPv4-only assumption for NTP, and there's no separate firewall/nftables layer gating UDP/123 that would also need updating — theallowdirective is the sole access control here. No blocking issues found.🟡 Suggestions
fd00::/8is broad — it permits any ULA-addressed host on any network, not just this fleet's addressing. The service VIP isfd40::123(ansible/inventory/group_vars/ntp_cluster.yml:11) and clients are presumably numbered from a narrower ULA prefix (e.g.fd40::/16or similar, based on the10.2.0.0/16IPv4 analog already in this list). If the actual client range is known and narrower, scoping to it would keep the allow list as tight as the IPv4 entry it sits next to, rather than opening to the entire ULA space (fc00::/7minusfd00::/8's complement). Not a security issue in a homelab/private-network context, just an inconsistency in precision versus the existing IPv4 line.chrony_upstream_pools/chrony_upstream_serverssourcing andbindcmdaddress/cmdalloware IPv4/loopback-only and unaffected by this change — confirmed by readingtemplates/chrony.conf.j2:42-47, no action needed, just noting the blast radius is contained to theallowblock as intended.Tests
No test harness (molecule or otherwise) exists for Ansible roles anywhere in this repo, so the lack of tests here is consistent with repo convention, not a gap introduced by this PR. The change was validated operationally per the PR description (
chronyd -Qagainst the service address before/after, on already-applied hosts), which is reasonable evidence for a config-only default change.Recommendation
Looks reasonable — no blocking issues; comments above are optional.