The Error
You make an HTTPS request with Go's net/http package and get:
Get "https://example.com": x509: certificate signed by unknown authority
The server's TLS certificate is real and valid โ Go just doesn't trust the CA that issued it. Your request dies before any data is exchanged.
Why This Happens
Go validates TLS certificates against the system's CA (Certificate Authority) store. That validation fails when:
- The server uses a self-signed certificate โ common in dev and staging environments
- The cert was signed by a private/internal CA not registered in your OS trust store
- A corporate proxy or VPN is doing TLS inspection and re-signing traffic with its own CA (Zscaler, Palo Alto NGFW, Cisco Umbrella)
- You're in a minimal Docker container (
scratchoralpine) with no CA certs installed at all - The system CA bundle is outdated or simply missing
Fix 1: Add the CA Certificate to the System Trust Store
For production and shared hosts, add the CA cert to the OS trust store once. Every Go binary on that machine picks it up automatically โ no code changes needed.
Linux (Debian/Ubuntu)
# Copy your CA cert (PEM format) into the trust store
sudo cp your-ca.crt /usr/local/share/ca-certificates/your-ca.crt
sudo update-ca-certificates
Linux (RHEL/CentOS/Fedora)
sudo cp your-ca.crt /etc/pki/ca-trust/source/anchors/your-ca.crt
sudo update-ca-trust
macOS
sudo security add-trusted-cert -d -r trustRoot -k /Library/Keychains/System.keychain your-ca.crt
After adding the cert, restart your Go application. It reads the system store at startup, not at every request.
Fix 2: Load the CA Certificate Programmatically
Can't touch the system trust store? Load the CA cert directly in your Go code instead. This works well on shared servers, read-only containers, or anywhere the OS is off-limits.
package main
import (
"crypto/tls"
"crypto/x509"
"fmt"
"io"
"net/http"
"os"
)
func main() {
// Read your CA certificate file (PEM format)
caCert, err := os.ReadFile("your-ca.crt")
if err != nil {
panic(err)
}
// Add it to the system pool
caCertPool, err := x509.SystemCertPool()
if err != nil {
// Fallback: create an empty pool
caCertPool = x509.NewCertPool()
}
caCertPool.AppendCertsFromPEM(caCert)
// Build an HTTP client that uses the extended pool
client := &http.Client{
Transport: &http.Transport{
TLSClientConfig: &tls.Config{
RootCAs: caCertPool,
},
},
}
resp, err := client.Get("https://example.com")
if err != nil {
panic(err)
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
fmt.Println(string(body))
}
Your CA cert travels with the binary. No OS changes, no system-wide side effects.
Fix 3: Fix Missing CA Certs in Docker Containers
Minimal base images โ scratch, alpine, distroless โ ship with zero CA certificates. The binary compiles fine. At runtime, the first HTTPS call crashes.
Option A โ Install ca-certificates in Alpine
FROM alpine:3.19
RUN apk add --no-cache ca-certificates
COPY your-app /app
ENTRYPOINT ["/app"]
Option B โ Copy CA certs from a builder stage (scratch images)
FROM golang:1.22 AS builder
WORKDIR /src
COPY . .
RUN CGO_ENABLED=0 go build -o app .
FROM scratch
# Copy system CA bundle from the builder
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
COPY --from=builder /src/app /app
ENTRYPOINT ["/app"]
Fix 4: Skip TLS Verification (Development Only)
This option exists. Use it only for local testing. In production, InsecureSkipVerify: true means any server โ including a man-in-the-middle โ can present any certificate and your code accepts it without question.
client := &http.Client{
Transport: &http.Transport{
TLSClientConfig: &tls.Config{
InsecureSkipVerify: true, // โ ๏ธ DEV ONLY
},
},
}
Found this in production code? Fix the root cause with Fix 1 or Fix 2.
Fix 5: Corporate Proxy / VPN TLS Inspection
A tell-tale sign: your colleagues on the same network aren't hitting this error, but you are. A proxy is likely intercepting TLS traffic and re-signing it with its own CA. Ask your IT or security team for the proxy's root CA certificate, then apply Fix 1 or Fix 2 with that cert.
To see exactly which certificate the server is presenting, run:
openssl s_client -connect example.com:443 < /dev/null 2>&1 | openssl x509 -noout -issuer -subject
If the issuer shows "Zscaler Root CA" or your company name instead of DigiCert or Let's Encrypt, that's the proxy cert you need to trust.
Verify the Fix
Run your Go program again. No TLS error means the fix worked. For a manual check, use OpenSSL directly:
# Run your program with verbose output
go run -v your_main.go
# Verify the cert chain against your CA file
openssl s_client -connect example.com:443 -CAfile your-ca.crt
When OpenSSL reports Verify return code: 0 (ok), the chain is clean. Go will accept it.
Prevention
- Store internal CAs in version control: Put company CA certs in a shared repo or config management tool (Ansible, Chef, Puppet). Every new host gets them on provisioning โ no manual steps.
- Always bundle
ca-certificatesin Docker images that make outbound HTTPS calls. Don't wait for a production incident to discover it was missing. - Ban
InsecureSkipVerifyin CI: A one-line grep catches it before it merges โgrep -r InsecureSkipVerify ./in your PR pipeline. - Load CA paths from environment variables: The same binary then works in dev (self-signed cert) and prod (public CA) without any code changes.

