Tracked in the compliance matrix as CLOCK-SKEW-CLUSTER-CHECKS.
Backlog — Cluster clock-skew checks (bootstrap + cert-load)
Filed: 2026-05-20 (from the compliance-matrix implementer-
perspective gap-probing session — Q11 on time synchronization
across cores).
Problem
Pryv core processes use machine wall-clock for every timestamp
(audit row time, access expiry, LE cert renewal trigger, TLS
handshake validity-window checks). Clock synchronization between
machines is the operator's job — chronyd / ntpd on the host —
and iso-27001.A.8.17 (Clock synchronization) is correctly
classified out-of-scope in the matrix because Pryv does no
synchronization itself.
But two failure modes today are silent:
-
A core boots / joins a cluster with a badly skewed clock.
The bootstrap bundle flow (bin/bootstrap.js init →
bin/master.js --bootstrap) brings up a new core that joins the
Raft cluster + immediately starts serving traffic with its
inherited config. If the joining machine's clock is wrong (NTP
not yet converged, hardware-clock-only boot, container drift),
nothing notices. Audit rows from the new core land with skewed
timestamps + the new core's TLS stack may reject the materialized
cert (forward skew past notAfter) or refuse to load a freshly
issued one (backward skew before notBefore).
-
A core hot-swaps a freshly-rotated cert without validating
its own clock against the cert's window. The acme:rotate
IPC fanout pushes the new cert to every worker; each calls
https.Server.setSecureContext. If the local clock is backward-
skewed before the new cert's notBefore, the TLS stack starts
serving a cert it considers not-yet-valid → every handshake on
that core fails. If forward-skewed past notAfter of the
previous cert, the previous cert was already broken anyway
(this is the worse pre-existing state). Either way, an explicit
pre-load validity check would surface the misalignment in logs
- skip the swap rather than break in production.
Direction
Two small additions, both in components/business/src/:
1. Bootstrap-join clock-skew check
In applyBundle.ts (or a separate step in bin/bootstrap.js --bootstrap), after the joining core has Raft connectivity but
before announcing readiness to the orchestrator (ack to
POST /system/admin/cores/ack):
GET <issuer-core>/system/admin/now # returns { serverTime }
# Note: requires a new admin endpoint
# or reuse of the existing /service/info
# which already carries serverTime.
compare to Date.now()
if |delta| > skewThresholdSec (default 30s):
log error + refuse to ack + exit 1
Operator runs the boot only after fixing the clock. Threshold is
operator-configurable: tight (10s) for strict-audit deployments,
loose (300s) for development.
2. Pre-cert-load validity check
In the worker-side acme:rotate IPC handler (or in
CertRenewer's materialization path on non-renewer cores) — wrap
the setSecureContext call with:
const cert = x509.X509Certificate(newPem);
const now = Date.now();
if (cert.validFromDate.getTime() > now + skewThresholdMs) {
logger.error(`[acme:rotate] refusing to load cert: notBefore is ${cert.validFromDate} > now ${new Date(now)}; clock probably backward-skewed`);
return; // keep previous cert loaded
}
if (cert.validToDate.getTime() < now) {
logger.error(`[acme:rotate] refusing to load cert: notAfter is ${cert.validToDate} < now ${new Date(now)}; clock probably forward-skewed past renewal`);
return;
}
setSecureContext(...);
Same skewThresholdMs operator config (default 30s).
Constraints
- Per-core / intra-core only. No cross-core clock agreement
required (Q11 confirmed: cores are independent — user is
core-affine; PlatformDB is index, not router; cores never need
to agree on cert validity).
- Operator setting. Both checks gated by
cluster.clockSkewThresholdSec (default 30). Setting to 0
disables.
- No NTP enforcement. Pryv does not run an NTP client. The
checks detect misalignment; the operator fixes it.
- Composable with
serverTime API helper. Pryv already
returns meta.serverTime in every API response (Unix timestamp
seconds; see
components/api-server/src/methods/helpers/setCommonMeta.ts)
for client-side skew detection. The bootstrap check uses the
same primitive on the server side, peer-to-peer.
Status
Not started. Backlog.
Matrix impact when shipped
| Scope |
Row |
Today |
After shipping |
| iso-27001 |
A.8.17 Clock synchronization |
Out of scope |
F: Awareness | Low (Pryv contributes bootstrap-time + cert-load skew detection + serverTime client helper; operator still runs NTP) |
| hipaa-security |
164.312(c)(1) Integrity |
F: Primitive | Med |
unchanged tier; cert-load check tightens the integrity story |
| iso-27001 |
A.8.15 Logging |
Implemented | High |
unchanged; audit timestamps still depend on operator NTP |
Related
- Q11 in
compliance-matrix/docs/implementer-faq.md.
- Sibling backlog:
AUDIT-LOG-CHAINING.md — the chained-audit-log
proposal explicitly depends on per-core monotonic time
(cross-core ordering is not meaningful per Q11). Notes added
to that backlog + matrix proposal to surface the dependency.
pryv-primitives.md audit + account-backup-tool entries cite
serverTime as the client-side skew-detection helper.
- Architecture note:
compliance-matrix/context/core-affinity-architecture.md.
Tracked in the compliance matrix as
CLOCK-SKEW-CLUSTER-CHECKS.Backlog — Cluster clock-skew checks (bootstrap + cert-load)
Filed: 2026-05-20 (from the compliance-matrix implementer-
perspective gap-probing session — Q11 on time synchronization
across cores).
Problem
Pryv core processes use machine wall-clock for every timestamp
(audit row time, access expiry, LE cert renewal trigger, TLS
handshake validity-window checks). Clock synchronization between
machines is the operator's job —
chronyd/ntpdon the host —and
iso-27001.A.8.17(Clock synchronization) is correctlyclassified
out-of-scopein the matrix because Pryv does nosynchronization itself.
But two failure modes today are silent:
A core boots / joins a cluster with a badly skewed clock.
The bootstrap bundle flow (
bin/bootstrap.js init→bin/master.js --bootstrap) brings up a new core that joins theRaft cluster + immediately starts serving traffic with its
inherited config. If the joining machine's clock is wrong (NTP
not yet converged, hardware-clock-only boot, container drift),
nothing notices. Audit rows from the new core land with skewed
timestamps + the new core's TLS stack may reject the materialized
cert (forward skew past
notAfter) or refuse to load a freshlyissued one (backward skew before
notBefore).A core hot-swaps a freshly-rotated cert without validating
its own clock against the cert's window. The
acme:rotateIPC fanout pushes the new cert to every worker; each calls
https.Server.setSecureContext. If the local clock is backward-skewed before the new cert's
notBefore, the TLS stack startsserving a cert it considers not-yet-valid → every handshake on
that core fails. If forward-skewed past
notAfterof theprevious cert, the previous cert was already broken anyway
(this is the worse pre-existing state). Either way, an explicit
pre-load validity check would surface the misalignment in logs
Direction
Two small additions, both in
components/business/src/:1. Bootstrap-join clock-skew check
In
applyBundle.ts(or a separate step inbin/bootstrap.js --bootstrap), after the joining core has Raft connectivity butbefore announcing readiness to the orchestrator (
acktoPOST /system/admin/cores/ack):Operator runs the boot only after fixing the clock. Threshold is
operator-configurable: tight (10s) for strict-audit deployments,
loose (300s) for development.
2. Pre-cert-load validity check
In the worker-side
acme:rotateIPC handler (or inCertRenewer's materialization path on non-renewer cores) — wrapthe
setSecureContextcall with:Same
skewThresholdMsoperator config (default 30s).Constraints
required (Q11 confirmed: cores are independent — user is
core-affine; PlatformDB is index, not router; cores never need
to agree on cert validity).
cluster.clockSkewThresholdSec(default30). Setting to0disables.
checks detect misalignment; the operator fixes it.
serverTimeAPI helper. Pryv alreadyreturns
meta.serverTimein every API response (Unix timestampseconds; see
components/api-server/src/methods/helpers/setCommonMeta.ts)for client-side skew detection. The bootstrap check uses the
same primitive on the server side, peer-to-peer.
Status
Not started. Backlog.
Matrix impact when shipped
serverTimeclient helper; operator still runs NTP)Related
compliance-matrix/docs/implementer-faq.md.AUDIT-LOG-CHAINING.md— the chained-audit-logproposal explicitly depends on per-core monotonic time
(cross-core ordering is not meaningful per Q11). Notes added
to that backlog + matrix proposal to surface the dependency.
pryv-primitives.mdaudit +account-backup-toolentries citeserverTimeas the client-side skew-detection helper.compliance-matrix/context/core-affinity-architecture.md.