GitLab CI Error Guide: 'x509: certificate signed by unknown authority' Runner & Registry TLS
Fix GitLab Runner's 'x509: certificate signed by unknown authority' on self-hosted GitLab/registry: add the CA to config.toml tls-ca-file, mount it into containers, and trust it.
- #gitlab-cicd
- #troubleshooting
- #errors
- #tls
Exact Error Message
A job — or the runner itself during registration — fails with a TLS trust error while talking to a self-hosted GitLab or registry over HTTPS:
fatal: unable to access 'https://gitlab.internal/acme/app.git/': SSL certificate problem: unable to get local issuer certificate
ERROR: Job failed (system failure): Get "https://gitlab.internal/api/v4/jobs/request":
x509: certificate signed by unknown authority
Error response from daemon: Get "https://registry.internal/v2/": x509: certificate signed by unknown authority
The exact phrasing varies — Git says unable to get local issuer certificate, Go (the runner and Docker) says x509: certificate signed by unknown authority — but the meaning is identical: the certificate’s issuing CA is not in the trust store of the process making the request.
What the Error Means
Your self-hosted GitLab, registry, or proxy presents a certificate issued by a private or self-signed CA. The component reaching out — the runner process, the build container, the Docker helper, or git inside the job — doesn’t have that CA in its trust store, so it can’t verify the chain and aborts the connection. It is a trust problem, not an expiry or hostname problem (those produce different messages).
The tricky part: the CA may be trusted on the runner host but not inside the container the job runs in, or vice versa. You must place the CA wherever the failing process actually looks.
Common Causes
- Private / self-signed CA not trusted by the runner. The host’s system trust store lacks the internal CA.
- Missing
tls-ca-fileinconfig.toml. The runner can’t validate the GitLab endpoint duringjobs/request. - CA not mounted into the build container or helper. The job runs in a clean image with no custom CA.
- Expired certificate. A genuinely expired cert (different message, but worth ruling out).
- MITM / TLS-inspecting proxy. A corporate proxy re-signs traffic with its own CA that the runner doesn’t trust.
How to Reproduce the Error
Register a runner against a self-hosted GitLab whose cert is issued by an internal CA, without supplying that CA:
gitlab-runner register \
--url https://gitlab.internal/ \
--token "$RUNNER_TOKEN" \
--executor docker --docker-image alpine:3.20
The runner registers but jobs immediately fail:
ERROR: Job failed (system failure): Get "https://gitlab.internal/api/v4/jobs/request":
x509: certificate signed by unknown authority
A clone inside a job fails the same way:
test:
image: alpine/git
script:
- git clone https://gitlab.internal/acme/app.git
fatal: unable to access 'https://gitlab.internal/acme/app.git/': SSL certificate problem: unable to get local issuer certificate
Diagnostic Commands
Inspect the served chain and confirm where the CA is (or isn’t) trusted:
# Show the certificate chain the server presents
openssl s_client -connect gitlap.internal:443 -servername gitlab.internal -showcerts </dev/null
# Verify against a candidate CA bundle
openssl verify -CAfile /etc/gitlab-runner/certs/internal-ca.crt server.crt
# Check the runner config for a tls-ca-file entry
grep -n tls-ca-file /etc/gitlab-runner/config.toml
# What's in the runner's cert directory?
ls -l /etc/gitlab-runner/certs/
verify error:num=20:unable to get local issuer certificate
That num=20 confirms a missing-issuer (trust) problem rather than expiry (num=10) or hostname mismatch (num=62). The job log line that matters:
x509: certificate signed by unknown authority
Step-by-Step Resolution
1. Add the CA to the runner via config.toml
Place the PEM CA file on the runner host and point tls-ca-file at it per runner:
[[runners]]
url = "https://gitlab.internal/"
executor = "docker"
tls-ca-file = "/etc/gitlab-runner/certs/internal-ca.crt"
[runners.docker]
image = "alpine:3.20"
The conventional location is /etc/gitlab-runner/certs/<hostname>.crt — the runner auto-loads a file named after the GitLab hostname there.
2. Mount the CA into build containers
tls-ca-file covers the runner→GitLab connection, but the job container (and any git/docker inside it) has its own trust store. Mount and install the CA:
[runners.docker]
volumes = ["/etc/gitlab-runner/certs:/etc/gitlab-runner/certs:ro"]
before_script:
- cp /etc/gitlab-runner/certs/internal-ca.crt /usr/local/share/ca-certificates/
- update-ca-certificates # apt-based; use update-ca-trust on RHEL/Alpine differs
On Alpine: apk add --no-cache ca-certificates && cp ... && update-ca-certificates.
3. Trust the CA on the host
So the runner process and the Docker daemon both trust it:
sudo cp internal-ca.crt /usr/local/share/ca-certificates/internal-ca.crt
sudo update-ca-certificates
sudo systemctl restart gitlab-runner docker
For the Docker daemon talking to a registry, also place it at /etc/docker/certs.d/registry.internal/ca.crt.
4. Pass the CA at registration time
Avoid the failed-registration loop by supplying the CA up front:
gitlab-runner register \
--url https://gitlab.internal/ \
--token "$RUNNER_TOKEN" \
--tls-ca-file /etc/gitlab-runner/certs/internal-ca.crt \
--executor docker --docker-image alpine:3.20
5. Rule out expiry and proxies
If openssl verify reports certificate has expired (num=10), renew the cert — no CA change fixes that. If a corporate proxy is re-signing TLS, add the proxy’s CA to every trust store above.
Prevention and Best Practices
- Distribute the internal CA to every layer that makes HTTPS calls: the runner host,
config.tomltls-ca-file, the build container, and the Docker daemon’scerts.d. - Name the runner cert file after the GitLab hostname in
/etc/gitlab-runner/certs/so it auto-loads, and bake the CA into custom base images to avoid per-jobupdate-ca-certificates. - Monitor certificate expiry with
openssl s_clientin a scheduled pipeline so renewals never surprise you (an expired cert gives a different x509 error). - For TLS-inspecting proxies, manage the proxy CA in your image build, not ad-hoc in
before_script. - The free incident assistant can distinguish a trust failure from expiry/hostname errors in a log. More patterns live in the GitLab CI/CD guides.
Related Errors
certificate has expired/certificate is valid for X, not Y— sibling x509 errors. The first is expiry (renew the cert), the second is a hostname mismatch (fix SANs); neither is solved by adding a CA.- denied: requested access to the resource is denied — once TLS trust is fixed, registry pushes can still fail on authorization; that guide covers the
deniedcase. Invalid CI config— unrelated, but a brokenbefore_scriptCA install can cascade into config or job errors; see the Invalid CI config guide.
Frequently Asked Questions
Why does the runner register but every job fail?
Registration may hit an endpoint that succeeded before trust was needed, or you supplied the token but not the CA. The per-job jobs/request call then fails TLS verification with x509: certificate signed by unknown authority. Supply --tls-ca-file (or tls-ca-file in config.toml) and restart the runner.
I added tls-ca-file but clones inside the job still fail. Why?
tls-ca-file only covers the runner→GitLab connection. The job runs in a separate container with its own trust store. Mount the CA in and run update-ca-certificates (or bake it into the image) so git and docker inside the job trust it too.
How do I tell a trust error from an expired certificate?
Run openssl verify -CAfile ca.crt server.crt. num=20 unable to get local issuer certificate is a trust problem (add the CA); num=10 certificate has expired is expiry (renew the cert). The job log wording can look similar, so verify with openssl.
Where exactly does the Docker daemon expect the registry CA?
At /etc/docker/certs.d/<registry-host>:<port>/ca.crt on the host running the daemon, after which a daemon restart picks it up. This is separate from the system trust store used by git.
Download the Free 500-Prompt DevOps AI Toolkit
500 battle-tested, copy-paste AI prompts engineered by a senior systems engineer — every one with fill-in placeholders and safety/back-out notes. Drop your email and it's yours.
- 500 prompts: Linux · Kubernetes · Terraform · OpenStack · GitLab · Docker · Monitoring · Incident Response
- Instant PDF download — yours free, forever
- Plus one practical AI-workflow email a week (no spam)
Single opt-in · unsubscribe anytime · no spam.