July 23, 2026 · devlog
A node with no clock mints an expired certificate
Rasputin’s control plane mints its own HTTPS certificate on first boot. On a Raspberry Pi 5 — no battery-backed real-time clock — that certificate came out already expired and the browser refused the connection. The image was correct; the clock it signed against was thirteen months slow.
Symptom (2026-07-12):
- Opening a recently-flashed Pi 5 control plane in a browser: the certificate reads as expired.
- The served leaf was an ordinary one-year cert —
notBefore Jun 25 2025,notAfter Jun 25 2026— but real “now” was mid-July 2026, past its window. - On the node itself:
timedatectlread2025-06-25,System clock synchronized: no, RTC at1970. The clock, not the cert, was the problem.
Root cause:
- The Pi 5 has no RTC. Before NTP, systemd advances the wall clock to a fixed
build-time epoch baked into the image (
2025-06-25here) and no further. systemd-timesyncdwas pointed at hostname NTP servers (time*.google.com). The bench DHCP handed out a dead DNS server, so those names never resolved — timesyncd never reached a time server, and the clock stayed at the floor.- The control plane minted its leaf against that frozen clock: a validity window anchored in mid-2025, already closed by mid-2026. A client with a correct clock rejects it.
- The trap underneath:
systemd-resolvedwon’t fall back to its public DNS while a per-link server is configured, even a broken one.timesyncd’s fallback NTP has the same rule. One dead DHCP-supplied server blocks the fallback that would otherwise cover it.
The fix, part one — correct time without DNS (node OS):
- A numeric
FallbackNTPdrop-in at/usr/lib/systemd/timesyncd.conf.d/10-rasputin-fallback.conf— anycast IPs (Cloudflare162.159.200.123,162.159.200.1; Google216.239.35.0). Numeric addresses sync over UDP/123 with no DNS at all. FallbackNTP, notNTP=, so a DHCP-advertised (option 42) or operator-set server still wins — it’s the floor, not an override. Added an optionalRASPUTIN_NTP_SERVERseed field for a homelab-local time source.
The fix, part two — don’t mint against a bad clock (control plane):
waitForTrustworthyClockinapi/cmd/rasputin-api/main.goholds the leaf mint untilsystemd-timesyncdreports a synchronized clock — the/run/systemd/timesync/synchronizedmarker — bounded to 90 seconds so an offline node still comes up.- The mint moved into the HTTPS goroutine so the wait can’t delay
sd_notifyreadiness (the unit isType=notifywith a 90-second start timeout), and the plain-HTTP bootstrap page stays reachable while the clock settles.
Validation:
- Reproduced the original condition on the bench Pi — deleted the persisted
leaf, rebooted. The clock came up at the baked floor,
timesyncdsynced against162.159.200.1:123about five seconds into boot, and the control plane minted a fresh, valid leaf against the corrected time. No hands on the keyboard. The served cert is good for ~364 days. - Shipped in
2026.07.2across control plane, node OS, and firewall.
Takeaway: if your appliance mints its own TLS identity, treat the clock as untrusted until NTP confirms it — a board with no RTC will sign a certificate dated to whenever it thinks it booted.
Rasputin is an open-source (AGPL) homelab cluster system — a control plane, node OS, and firewall image that make a few Raspberry Pis or N100 boxes behave like one appliance. It’s pre-alpha, on GitHub, and I’m looking for a handful of design partners to run it and tell me what’s broken.
Want the next one in your inbox? Subscribe, or grab the RSS feed. Found something broken? We're recruiting design partners.