Chronos is designed for environments that permit only HTTPS egress. Its security posture centers on the gateway's backend transport policy, strict sample rejection, and TLS validation.
Configured under security in the gateway config and enforced in
chronos-core (SecurityPolicy) before any request is issued:
security:
allow_plain_http_backends: false # remote plain-HTTP backends
allow_plain_http_loopback: true # plain-HTTP to 127.0.0.1 / ::1 / localhost
pinned_spki: [] # optional SPKI pin set (see below)Rules:
| Backend transport | Decision |
|---|---|
https://… |
Always allowed; certificate and hostname are validated. |
http:// loopback (127.0.0.1, ::1, localhost) |
Allowed only when allow_plain_http_loopback is true. |
http:// remote host |
Rejected by default; allowed only when allow_plain_http_backends is true. |
In addition, a backend with require_tls: true is rejected at construction if
its URL is not https. A rejected backend fails fast at startup rather than
silently sampling over an insecure transport.
HTTPS backends are validated by rustls (certificate chain + hostname). Setting
require_valid_cert: false is a lab-only escape hatch that disables
verification (accepts any presented certificate); never use it in production.
security.pinned_spki accepts base64-encoded SHA-256 hashes of the backend's
Subject Public Key Info (the same value used by HPKP / openssl … | openssl dgst -sha256 -binary | base64). When the list is non-empty, the gateway additionally
requires the server's leaf-certificate SPKI hash to be in the set, on top of
normal chain validation. Pin entries are validated for format at startup; a
malformed pin aborts startup. An empty list disables pinning.
Compute a pin from a server certificate:
openssl x509 -in server.crt -pubkey -noout \
| openssl pkey -pubin -outform der \
| openssl dgst -sha256 -binary \
| openssl base64A sample is rejected (and never written to chrony) if:
- the HTTP request failed;
- TLS validation failed;
- the response JSON was invalid;
- the backend reported
sync != synchronized; - the round-trip time exceeded
max_rtt_ms; - the offset was an outlier beyond
outlier_threshold_ms; - fewer than
min_good_samplessurvived the round.
A round with no usable sample leaves the previous good sample in place and moves
the gateway to degraded / unsynchronized. During an outage the gateway feeds
no time at all rather than bad time.
The shipped server block
(packaging/nginx/chronos-server.conf)
should:
- terminate TLS;
- forward only
/time,/healthz,/status(under the server'sapi.base_pathprefix when one is configured, e.g./chronos/time); - disable caching and set
Cache-Control: no-storefor/time; - optionally apply a source-IP allowlist (
allow/deny); - optionally require client certificates (mTLS) for
/time.
- mTLS and response signing are design-level notes in v1, not implemented.
- The gateway never calls
clock_settime/adjtimex/settimeofday; clock discipline is entirelychronyd's responsibility.