GitHub Actions "Bad credentials" / Token Expired From Runner Clock Skew
A self-hosted runner fails authentication or token-based API calls because its system clock is wrong. Short-lived/signed tokens validate against time, so skew makes them appear expired or not yet valid.
What this error means
Auth fails intermittently on a self-hosted runner with "Bad credentials" or a JWT "not before"/"expired" error, and the host's clock turns out to be off by minutes. Other runners with correct time work fine.
Error: Bad credentials
# or, for app/JWT auth:
'Expiration time' claim ('exp') is in the past / token used before issuedCommon causes
Runner system clock drifted
A runner whose clock is wrong (no NTP, VM paused/resumed) signs or validates time-bound tokens incorrectly, so they look expired or not-yet-valid.
GitHub App / OIDC tokens are time-sensitive
App installation JWTs and OIDC tokens carry iat/exp claims validated against the clock. A few minutes of skew is enough to reject them.
How to fix it
Sync the runner clock
Enable time synchronization on the host so token timestamps are valid.
# Linux: enable NTP
sudo timedatectl set-ntp true
timedatectl status # confirm 'System clock synchronized: yes'Keep time accurate on the fleet
- Run an NTP/chrony service on all self-hosted runner hosts.
- For VMs, ensure the host resyncs time after pause/resume.
- Re-run the failed job after fixing the clock; the auth failure is transient.
How to prevent it
- Run NTP/chrony on every self-hosted runner host.
- Resync VM clocks after pause/resume.
- Treat sudden auth failures with correct credentials as possible clock skew.