- What happened: the Let's Encrypt certificate for
*.actions.githubusercontent.comexpired at 23:05:54 UTC on July 19, 2026. That wildcard frontspipelines.actions.githubusercontent.comandvstoken.actions.githubusercontent.com, the endpoints every GitHub Actions runner uses to take jobs. - Impact: self-hosted runners fail with
The SSL connection could not be established/certificate has expiredand drop offline. Fleets die simultaneously, which looks like your problem. It is not. - It is not your CA store, your clock, or your proxy. Verify in one command:
echo | openssl s_client -connect pipelines.actions.githubusercontent.com:443 2>/dev/null | openssl x509 -noout -enddate. If the date is in the past, stop changing things locally. - GitHub opened "Incident with GitHub Actions" at 23:34 UTC, 29 minutes after the expiry. Runners under a supervisor that retries (systemd
Restart=) will reconnect on their own once the certificate is rotated. Runners without one stay dead until you restart them.
GitHub fully restored Actions at 04:43 UTC and marked the incident resolved at 04:44 UTC on July 20, five hours and thirty-eight minutes after the certificate expired. The detail that reframes the entire outage: the replacement certificate now on the wire was issued June 18, 2026 (valid to September 16), a full month before the old one died. This was never a missed renewal. The renewed certificate existed the whole time and simply was not deployed to pipelines and run-service.actions.githubusercontent.com until after the expired one had already taken runners offline worldwide. Our 23:26 UTC root cause held start to finish.
What happened
GitHub serves its Actions runner control plane from *.actions.githubusercontent.com, behind Microsoft's Azure Front Door. The certificate on that wildcard is a 90-day Let's Encrypt issue: valid from April 20, 2026 at 23:05:55 UTC, expiring July 19, 2026 at 23:05:54 UTC. Nobody rotated it. At 23:05:54 the edge kept serving the same certificate, now expired, and every TLS client that validates dates, which is all of them, started refusing the handshake.
The blast radius is precise: anything that talks to the Actions service endpoints. Self-hosted runners long-poll pipelines.actions.githubusercontent.com for jobs and hit vstoken.actions.githubusercontent.com for tokens, so they disconnect within one poll cycle. The rest of GitHub keeps working, api.github.com and the web UI sit on a different chain and were unaffected, which makes the failure look partial and local instead of what it is: a single expired certificate upstream.
What it looks like from your side
If you run self-hosted runners, tonight looked like this in the runner log:
2026-07-19 23:23:12Z: Runner connect error: The SSL connection could not be established, see inner exception.. Retrying until reconnected.
And like this from curl:
$ curl -v https://pipelines.actions.githubusercontent.com * Connected to pipelines.actions.githubusercontent.com (13.107.42.16) port 443 * SSL certificate problem: certificate has expired
Every runner in the fleet shows it at the same time. In our case that was three runners on three different machines, all failing within the same minute. When independent machines fail identically at the same moment, the cause is a shared dependency, not the machines.
The diagnosis, minute by minute
- 23:23 UTCA runner will not connect
Bringing a runner back into service, it starts, then loops on
SSL connection could not be established. First instinct: something is wrong with this box. - 23:24Split the failure domain
curl https://api.github.comworks.curl https://pipelines.actions.githubusercontent.comfails, and so doesvstoken. GitHub-the-API is fine; Actions-the-service is not reachable. Different hostnames, different CDN chains. - 23:25Suspect the local trust store
The reflex from the 2021 DST Root X3 era: an old CA bundle chokes on a chain the rest of the world accepts. But
ca-certificatesis current, the clock is NTP-synced, and updating changed nothing. - 23:26Read the server certificate itself
openssl s_clientprints the answer:notAfter=Jul 19 23:05:54 2026 GMT, Let's Encrypt R12, expired minutes ago. Not our store. Their certificate. - 23:27Confirm from a second network
The same check from an AWS box in us-east-1 returns the identical expired certificate. Two networks, two trust stores, one answer: this is GitHub's outage. We stopped touching our infrastructure and started writing this up.
- 23:34GitHub acknowledges
The status page opens "Incident with GitHub Actions," status investigating, Actions degraded. Twenty-nine minutes after the expiry, eleven minutes after we had the root cause.
This is not GitHub's first expired certificate
In March 2023, the certificate for objects.githubusercontent.com expired and broke release downloads across the ecosystem until it was rotated. Same failure class, different subdomain: short-lived automated certificates are excellent right up until the automation quietly stops, and a 90-day lifetime means a rotation gap becomes a hard outage on a schedule you can predict to the second. Tonight's certificate was issued April 20. The renewal window came and went sometime in June or July with nobody, human or machine, completing the rotation.
What to do right now
- FirstVerify before you change anything
One command tells you whether this is you or them:
echo | openssl s_client -connect pipelines.actions.githubusercontent.com:443 \ -servername pipelines.actions.githubusercontent.com 2>/dev/null \ | openssl x509 -noout -enddate
If
notAfteris in the past, do not touch your CA bundle, your clock, or your proxy. There is nothing to fix on your side. - ThenCheck how your runners are supervised
A runner installed as a systemd service sits in a retry loop and reconnects on its own the moment GitHub rotates the certificate. A runner launched by hand, or under a supervisor that gives up, exits and stays dead. Know which kind you have before the fix lands, not after.
- After recoveryConfirm the fleet actually came back
List your runners in the repo or org settings and check every one reports online. In our fleet, the runner under systemd recovered without intervention and the two under lighter supervision needed a manual restart. Partial recovery is easy to miss.
The monitoring lesson
A CI fleet that cannot take jobs is an outage, and tonight the entire failure lived in someone else's certificate. If your infrastructure long-polls a third-party endpoint, that endpoint's certificate expiry is one of your availability metrics. Monitor notAfter on the handful of external hosts your operations actually depend on and alert at 14, 7, and 1 days out. It is one cron job and it would have predicted tonight to the second.
The second lesson is about diagnosis speed. The instinct when TLS breaks is to fix your own end: update the CA bundle, check the clock, blame the proxy. The faster path is to read the certificate the server actually presented, then confirm from a second vantage point. Two commands, two minutes, and it prevented an evening of changing local settings that were never broken.
Timeline (all times UTC, July 19, 2026)
- 23:05:54 - the
*.actions.githubusercontent.comcertificate expires. Runner long-polls begin failing worldwide. - 23:23 - we hit it live while returning a runner to service; diagnosis begins.
- 23:26 - root cause identified: the server certificate itself is expired.
- 23:27 - confirmed identical from a second network (AWS us-east-1). Local causes eliminated.
- 23:34:03 - GitHub opens "Incident with GitHub Actions," status investigating, Actions component degraded.
- 23:37 - GitHub: Actions degraded on github.com and in GHEC DR stamps; new workflows may delay or fail to start, ongoing runs may fail.
- 23:57 - GitHub: "We have identified the cause of failures in GitHub Actions and are working to restore service." That is 52 minutes after expiry, and 31 minutes after our public root cause.
- 00:07 (Jul 20) - the incident spreads: API Requests now reported as degraded.
- 00:20 - GitHub: Actions still degraded, "continuing to investigate."
- 00:26 - cascade widens to Git LFS operations and loading files through the API.
- 00:34 (7:34pm CT), ~90 minutes in - the served certificate is still
notAfter Jul 19 23:05:54and the endpoints still fail TLS validation. Unresolved. - 00:48-00:52 - the blast radius widens again: Pages, then Issues, reported degraded.
- 01:11 - GitHub raises the Actions incident to critical: "the extended downtime has started to cause knock-on effects to other services."
- 01:17 (8:17pm CT), ~2h11m in -
pipelinesandrun-service.actions.githubusercontent.comstill serve the expired certificate (curlrejects it: "certificate has expired"), whilebroker.actions.githubusercontent.comanswers with a separate, still-valid cert (Jul 10 to Oct 8). The renewal never reached the runner control-plane hosts. Still unresolved. - 02:43 - GitHub: Issues, API Requests, and Pages have recovered; work continues on the Actions runners themselves.
- 03:27 - GitHub: "We're seeing recovery across all impacted Actions runners." Fleets on a retry supervisor start reconnecting on their own.
- 03:34 - incident moved to monitoring: the Actions degradation is mitigated.
- 04:43 - Actions reported fully recovered.
- 04:44 - incident resolved. The expired certificate was live in production for 5 hours 38 minutes.
- The reframe: the certificate now served was issued June 18, 2026 (valid to September 16), predating the outage by a month. The renewal was never missed. The renewed certificate simply was not rolled out to the runner control-plane hosts until after the old one expired.
After this post went up, Patrick Duggan covered the same outage and reached the same conclusion, citing this write-up as "the first public diagnosis" and "correct end to end": GitHub Let a Free Certificate Expire and Killed Every Self-Hosted Runner on Earth. His angle sharpens ours: the real failure was choosing to hand-manage certificate rotation on infrastructure that could have automated it.
Sources
Live observation from two independent networks, presented-certificate details via openssl s_client. GitHub status: "Incident with GitHub Actions," opened 23:34:03 UTC July 19, cause identified 23:57 UTC, escalated to critical (01:11) and cascading to API Requests (00:07), Git LFS (00:26), Pages and Issues (00:48-00:52); mitigated 03:34, fully recovered 04:43, resolved 04:44 UTC July 20 after 5h 38m (githubstatus.com). Replacement certificate serial 05977C13… was issued June 18, 2026 and is valid to September 16, predating the outage by a month. Independent corroboration: Patrick Duggan, dugganusa.com. Precedent: GitHub community discussion #50963 and the March 2023 objects.githubusercontent.com expiry coverage.
Would your monitoring have caught this?
RedEye Security builds detection and monitoring that watches the dependencies your uptime actually rests on, certificates included, with Caver turning the telemetry into alerts your team sees in time to act.
Talk to RedEye Security