Secure

Time sync

Why tokens need a correct clock, how much skew the relay tolerates, what fails when a clock is wrong, and how to keep time on devices.

Tokens carry times, and every check against them uses a clock. A token has an issue time (iat) and an expiry (exp). The tokens we mint carry those two and no not-before (nbf) claim. The relay compares them with its own clock, and refuses everything while that clock is not trustworthy.

Whose clock matters

ClockWhat it decidesIf it is wrong
The API host (it mints)iat and exp of every tokenno token is minted while the kernel says the clock is unsynchronized: 503 clock_unsynchronized
The relay host (it checks)whether a token is expired or not yet issuedevery token is refused while the kernel says the clock is unsynchronized; a clock that is behind by more than the skew refuses tokens as "issued in the future"
Your backendnothing: it calls the API and passes the answer onnothing
A browserwhen the player renews (timed from the token's own iat and exp, not the page's clock)see below
A device that publishes with a token from your backendnothing in the tokenits TLS connection to the relay can fail if the clock is far out (a certificate is valid between two dates)
A host that mints its own tokens (nearglass token mint, a self-hosted relay)iat and exp of its tokenssee the table of runs below

With the managed service the two clocks that matter are ours. If you publish or watch with tokens from our API, a wrong clock on a device or a browser does not make a token invalid. It matters for self-hosted relays and for any machine of yours that mints tokens.

Two things in the player depend on the page's clock:

  • A descriptor looks stale to the player when its expiry is less than 10 s away on the page's clock. A browser whose clock is ahead by more than the token's life minus 10 s (110 s for the default 120 s) treats every token it gets as stale, so it asks for a second token before each connect. That costs tokens against the limit of 6 a minute for a seat, not a failure.
  • Renewal itself does not: it is timed from exp minus iat, counted from when the token arrived (Token renewal).

What the relay tolerates

The relay's check, for each token, at setup and again on each request:

CheckRuleRefused as
Expiredthe token is good until exp plus the skewan expired token
Issued in the futurerefused when iat is later than the relay's now plus the skewunauthorized; the relay logs why
Too longrefused when exp minus iat is more than 7 daysunauthorized
No iatbounded by exp aloneaccepted by the other rules
Clock unsynchronizedno token is accepted, at setup or on any requestclock not synchronized

The skew is the relay scope's clock_skew_secs. For every customer scope in the managed service it is 2 s. The sample scope file for a self-hosted relay (relay/scope.example.json) sets 60. A longer skew tolerates a worse clock and lets an expired token live that much longer; a publishing session (a driver's turn too) ends at exp plus the skew.

The reason for the issue-time rule: a relay whose clock is behind would accept tokens that have expired. A token issued after that relay's now means the relay's clock is behind its issuer's, so it refuses and logs it:

ERROR moq_relay_ietf::clock_sync: refusing a token issued in the future: this relay's
clock is behind its issuer's by more than the clock skew, so it would also accept
expired tokens; fix the time sync  subject="device:skew" issued_at=1793560367 now=1793560362 behind_secs=5

The relay writes it at most every 10 s and counts every refusal in moq_relay_token_issued_in_future_total.

A real run

A test stack (relay and API as packaged, skew 2 s) and a publisher using tokens minted with a clock offset. nearglass token mint always stamps the current time, so these came from our test script (demo/sales/cat.mjs, which takes a now), signed with the tenant's key:

The minting clockTokenWhat the relay did
1 s aheadttl 60accepted, published
5 s aheadttl 60refused, unauthorized, with the log line above (behind_secs=5)
3600 s aheadttl 60refused, unauthorized
30 s behindttl 60accepted (it lives 30 s of real time)
30 s behindttl 20refused: already expired when minted
3600 s behindttl 7200accepted (it lives 3600 s of real time)

A clock that is behind makes tokens shorter in real time, so they fail closed. A clock that is ahead by more than the skew is refused.

Also run: a token with ttl 1, used 5 s later, was refused the same way (the relay closes with code 2, unauthorized); a connect with no token the same.

The refused publisher in these runs, nearglass publish, did not back off: it reconnected hundreds of times a second for as long as it ran. A device that is refused should wait before it asks again.

What "synchronized" means

On Linux the relay and the API ask the kernel (adjtimex, read only, no privilege needed): the clock is synchronized when the kernel's time state is not an error and its STA_UNSYNC flag is clear. That is what timedatectl shows as System clock synchronized: yes. A time service (chrony, ntpd, systemd-timesyncd) clears the flag once it has set the clock, and the kernel sets it again when the service has not updated the clock for a long time. Anything other than Linux, or a failed call, counts as not synchronized.

Check a host:

timedatectl                # System clock synchronized: yes
chronyc tracking           # with chrony

A relay started before its time service has synced logs system clock is not synchronized: token scopes refuse every token until it is and starts serving by itself once the clock syncs. The relay's gauge moq_relay_clock_synchronized is 1 or 0, and moq_relay_clock_unsynced_refusals_total counts what it refused.

Keep time on devices and hosts

  • Every relay and API host: run chrony or systemd-timesyncd, and alert on timedatectl saying no, or on the gauge above being 0.
  • Devices that mint nothing and take tokens from your backend do not need tight time for the tokens. Give them a time service anyway: certificate checks, logs and any frame timestamps you compare need it.
  • Boards without a battery-backed clock start at a wrong time (often the date they were built). Start the time service at boot and wait for it before connecting to the relay (timedatectl shows yes, or chronyc waitsync).
  • A device that mints its own tokens, or a self-hosted relay on its own network: give it an NTP source you control, and keep the skew small (a few seconds) so a turn ends close to its token.

On this page