Incident Resolved · GitHub Actions

GitHub Let Its Actions Certificate Expire and Self-Hosted CI Died Worldwide

At 23:05:54 UTC tonight the TLS certificate for *.actions.githubusercontent.com reached the end of its 90-day life with no replacement in place. Every self-hosted runner that long-polls those endpoints lost its connection at the same moment. We had it root-caused before GitHub's status page said a word.

Matt Lucas  |  July 19, 2026  |  6 min
Editorial hero illustration
23:05:54
UTC cert expiry, Jul 19
5h 38m
expired cert live in prod
3 of 3
our runners down at once
31 min
ahead of GitHub's own root cause
TL;DR
  • What happened: the Let's Encrypt certificate for *.actions.githubusercontent.com expired at 23:05:54 UTC on July 19, 2026. That wildcard fronts pipelines.actions.githubusercontent.com and vstoken.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 expired and 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.
Resolved · 04:44 UTC Jul 20, after 5h 38m

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

  1. 23:23 UTC
    A 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.

  2. 23:24
    Split the failure domain

    curl https://api.github.com works. curl https://pipelines.actions.githubusercontent.com fails, and so does vstoken. GitHub-the-API is fine; Actions-the-service is not reachable. Different hostnames, different CDN chains.

  3. 23:25
    Suspect 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-certificates is current, the clock is NTP-synced, and updating changed nothing.

  4. 23:26
    Read the server certificate itself

    openssl s_client prints the answer: notAfter=Jul 19 23:05:54 2026 GMT, Let's Encrypt R12, expired minutes ago. Not our store. Their certificate.

  5. 23:27
    Confirm 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.

  6. 23:34
    GitHub 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

  1. First
    Verify 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 notAfter is in the past, do not touch your CA bundle, your clock, or your proxy. There is nothing to fix on your side.

  2. Then
    Check 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.

  3. After recovery
    Confirm 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

Your availability includes their certificates

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.com certificate 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:54 and 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 - pipelines and run-service.actions.githubusercontent.com still serve the expired certificate (curl rejects it: "certificate has expired"), while broker.actions.githubusercontent.com answers 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.
Independently corroborated

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