# x509: certificate signed by unknown authority (Go): fixes

> Go programs print x509: certificate signed by unknown authority when no trusted root matches the server chain. Add the root to the system store or RootCAs.

Source: https://ssl.sh/errors/x509-certificate-signed-by-unknown-authority/
Last updated: 2026-11-10

**`x509: certificate signed by unknown authority` is Go's way of saying the server's certificate chain does not end at any root certificate the program trusts.** Usually the server is missing an intermediate, the machine or container lacks a CA bundle, or the server uses a private CA (or a TLS-inspecting proxy) whose root was never installed.

## Quick fix

What the server sends decides the fix: repair its chain, install a CA bundle, or trust the private root.

First check what the server sends and who signed it:

```bash
openssl s_client -connect example.com:443 -servername example.com -showcerts </dev/null | grep -E 'depth|s:|i:|Verify return'
```

- **Only a leaf, `Verify return code: 21`:** the server chain is incomplete. Fix the server (see [missing intermediate certificate](https://ssl.sh/errors/missing-intermediate-certificate/)).
- **Full chain to a public root, but your program still fails:** the client has no or an old CA bundle. Install `ca-certificates`.
- **Chain ends at a private or proxy root:** trust that root on the client:

```bash
export SSL_CERT_FILE=/etc/ssl/certs/company-bundle.pem   # Linux, per process
```

Better, add the root to the system store (commands below) so every Go tool picks it up.

## Causes

### 1. No CA bundle in the container

Minimal images (`scratch`, some `distroless` and slim variants) ship without CA certificates. Every HTTPS call from a Go binary then fails with this error.

- **Confirm:** `ls /etc/ssl/certs/` inside the container is empty or missing.
- **Fix (multi-stage build):**

```dockerfile
FROM golang:1 AS build
WORKDIR /src
COPY . .
RUN CGO_ENABLED=0 go build -o /app .

FROM scratch
COPY --from=build /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
COPY --from=build /app /app
ENTRYPOINT ["/app"]
```

Alternatively, `import _ "golang.org/x/crypto/x509roots/fallback"` embeds a Mozilla-derived root bundle that Go uses only when no system roots are found.

### 2. The server omits its intermediate certificate

Go does not fetch missing intermediates. A server that sends only the leaf works in Chrome and fails in Go.

- **Fix:** configure the full chain on the server; check it with the [chain builder](https://ssl.sh/tools/chain-builder/).

### 3. Private CA, self-signed certificate or proxy root

Internal registries, Vault, Consul, Kubernetes API servers and corporate proxies all use roots that are not in public bundles.

- **Fix in code**, keeping the system roots and adding yours:

```go
pool, err := x509.SystemCertPool()
if err != nil {
    pool = x509.NewCertPool()
}
pem, err := os.ReadFile("/etc/myapp/company-root.pem")
if err != nil {
    log.Fatal(err)
}
if !pool.AppendCertsFromPEM(pem) {
    log.Fatal("no certificates parsed from company-root.pem")
}
client := &http.Client{Transport: &http.Transport{
    TLSClientConfig: &tls.Config{RootCAs: pool},
}}
```

### 4. Wrong root after a CA change

If a CA moved to a new root and your bundle predates it, chains to the new root fail. Updating the `ca-certificates` package fixes it.

## Fixes by environment

Every fix installs the missing root where the Go program looks: the system store, the container image, or `RootCAs` in code.

**Debian / Ubuntu:**

```bash
sudo apt-get install -y ca-certificates
sudo cp company-root.crt /usr/local/share/ca-certificates/
sudo update-ca-certificates
```

**RHEL / Rocky / Fedora:** copy to `/etc/pki/ca-trust/source/anchors/` and run `sudo update-ca-trust`.

**Alpine:** `apk add --no-cache ca-certificates`, then copy the root into `/usr/local/share/ca-certificates/` and run `update-ca-certificates`.

**macOS and Windows:** Go uses the platform verifier, so add the root to the System keychain ("Always Trust") or the Windows "Trusted Root Certification Authorities" store.

**Tool-specific pages:** Docker pulls are verified by the daemon, not the CLI, and kubectl uses the CA from kubeconfig; see [Docker x509 error](https://ssl.sh/errors/docker-x509-certificate-signed-by-unknown-authority/) and [kubectl x509 error](https://ssl.sh/errors/kubectl-x509-certificate-signed-by-unknown-authority/).

**Go module downloads:** `go mod download` behind a TLS-inspecting proxy fails the same way. Trust the proxy root system-wide rather than setting `GOINSECURE`.

## Don't do this

- **`InsecureSkipVerify: true`.** It disables both chain and hostname checks. Linters such as gosec flag it.
- **Replacing `RootCAs` with only your private root** when the program also talks to public services. Start from `x509.SystemCertPool()` and append.
- **`GOINSECURE` to get past module download errors.** It skips HTTPS certificate checks for every matching module path, so a tampered download goes unnoticed unless the checksum database catches it.
- **Copying a random `ca-certificates.crt` from the internet** into your image. Use the distribution package or the Go fallback package.
