When Let's Encrypt is valid but GitHub Actions still reject it

jicea2 pts0 comments

When Let's Encrypt Is Valid but GitHub Actions Still Rejects It

A few days ago I ran into a surprisingly tricky TLS issue while setting up automated tests for a service running behind Let’s Encrypt certificates.

The symptoms were confusing at first:

Browsers accepted the certificate.

curl --insecure worked.

The certificate looked perfectly valid.

Yet GitHub Actions consistently failed with:

SSL certificate problem: unable to get local issuer certificate

At first glance this looked like a classic missing intermediate certificate problem.

It wasn’t.

The Error

The failing test was executed using Hurl inside GitHub Actions:

error: HTTP connection

GET https://ueo.ventures/

(60) SSL certificate problem:<br>unable to get local issuer certificate

Running OpenSSL against the endpoint produced a similar result:

verify error:num=20:<br>unable to get local issuer certificate

The certificate itself looked completely normal:

subject=CN = ueo.ventures

issuer=C = US,<br>O = Let's Encrypt,<br>CN = YE2

The server also provided an intermediate certificate:

Let's Encrypt YE2

which was issued by:

ISRG Root YE

Nothing appeared to be missing.

First Suspect: Broken Certificate Chain

Whenever OpenSSL reports:

unable to get local issuer certificate

the first thing to check is whether the server sends the full certificate chain.

The output confirmed that both the leaf certificate and the intermediate were present:

Certificate chain

0: ueo.ventures<br>1: Let's Encrypt YE2

So the server configuration was not the problem.

Second Suspect: An Outdated CA Store

The tests were running on GitHub Actions using the ubuntu-latest runner.

At the time of writing, ubuntu-latest resolves to Ubuntu 24.04 LTS (Noble Numbat), which is generally considered a modern and well-maintained platform.

Checking the installed CA package showed:

ca-certificates 20240203

At first glance, this made the trust-store theory seem unlikely. Ubuntu 24.04 is the current LTS release, and GitHub keeps the runner images regularly updated.

Reinstalling the package changed nothing:

sudo apt install --reinstall ca-certificates<br>sudo update-ca-certificates

The verification error remained.

This was the first clue that the issue was not caused by an outdated operating system or a stale package installation.

Instead, we were dealing with a certificate chain that had moved faster than the trust stores available in common CI environments.

The New Let&rsquo;s Encrypt Generation Y Hierarchy

The certificate had been issued from Let&rsquo;s Encrypt&rsquo;s newer Generation Y hierarchy:

ueo.ventures<br>└─ Let's Encrypt YE2<br>└─ ISRG Root YE

The GitHub Actions runner image we tested did not yet trust ISRG Root YE.

The chain itself was perfectly valid.

The trust store simply did not contain the root certificate required to complete verification.

From the client&rsquo;s perspective the chain effectively ended in mid-air:

ueo.ventures<br>└─ YE2<br>└─ Root YE

Root YE: unknown

Result:

unable to get local issuer certificate

Why Browsers Worked

Modern browsers often maintain their own trust infrastructure and update independently from the operating system.

CI runners, containers and server operating systems usually rely on the OS certificate bundle.

This means a certificate can be accepted in Chrome while failing in automated build pipelines.

A frustrating difference that is easy to overlook.

How We Verified the Root Cause

The key clue came from OpenSSL:

depth=1<br>C = US,<br>O = Let's Encrypt,<br>CN = YE2

verify error:num=20:<br>unable to get local issuer certificate

Notice that verification fails above the leaf certificate.

OpenSSL was able to validate:

ueo.ventures -> YE2

but could not find a trusted issuer for:

YE2 -> Root YE

That narrowed the problem down immediately.

Our Solution

After confirming that the certificate chain itself was valid, we shifted our focus to the GitHub Actions runner.

The root cause was that the ubuntu-latest runner, which currently resolves to Ubuntu 24.04 LTS, did not yet trust the new ISRG Root YE certificate used by Let&rsquo;s Encrypt&rsquo;s Generation Y hierarchy.

Reinstalling the CA bundle did not help because the required root certificate simply was not part of the installed trust store.

The solution was to explicitly install the missing root certificate during the CI run and update the local trust store before executing the tests.

In our GitHub Actions workflow the fix looked like this:

- name: Install ISRG Root YE<br>run: |<br>curl -fsSL https://letsencrypt.org/certs/gen-y/root-ye.pem \<br>-o root-ye.pem

sudo cp root-ye.pem \<br>/usr/local/share/ca-certificates/root-ye.crt

sudo update-ca-certificates

- name: Verify certificate chain<br>run: |<br>openssl s_client \<br>-connect ueo.ventures:443 \<br>-servername ueo.ventures \<br>-verify_return_error \

- name: Run Hurl tests<br>run: |<br>hurl --test tests/container-registry/test.hurl

Once the runner trusted ISRG Root YE, certificate verification succeeded and the Hurl tests passed...

certificate root encrypt github actions rsquo

Related Articles