Fixing gRPC Error: x509: certificate signed by unknown authority

intermediate🔒 SSL/TLS2026-07-12| Go (Golang) gRPC services, Docker containers, Kubernetes, Linux/macOS/Windows, using self-signed SSL/TLS certificates.

Error Message

rpc error: code = Unavailable desc = connection error: desc = "transport: authentication handshake failed: x509: certificate signed by unknown authority"
#grpc#go#tls#x509#handshake#security

The Error Message

You’ve set up your gRPC server, enabled TLS, and everything looks ready. Then, you run your client and hit this wall:

rpc error: code = Unavailable desc = connection error: desc = "transport: authentication handshake failed: x509: certificate signed by unknown authority"

Why This Error Occurs

This error pops up during the TLS handshake. Your gRPC client is trying to verify the server's identity, but it doesn't recognize the Certificate Authority (CA) that signed the server's cert. If the CA isn't in your system's trust store or explicitly defined in your code, the connection drops immediately for safety.

You’ll usually run into this in these three situations:

  • Local Development: You’re using self-signed certificates generated via OpenSSL.
  • Private CAs: Your organization uses an internal CA that hasn't been added to your Docker container or OS trust store.
  • Broken Chains: The server provides its certificate but fails to send the intermediate certificates required to reach the Root CA.

Step-by-Step Fix

Method 1: Passing the Root CA to the gRPC Client (Recommended)

The most reliable fix for internal microservices is to hand the CA certificate directly to the client. Rather than relying on the operating system's global store, you tell the client exactly which certificate to trust.

1. Locate your CA certificate. You need the ca.crt file (the public key of the CA) available to your client code.

2. Load the file in your Go client:

import (
    "google.golang.org/grpc"
    "google.golang.org/grpc/credentials"
    "log"
)

func main() {
    // Path to the CA certificate that signed the server's certificate
    caFile := "certs/ca.crt"

    // Initialize client TLS credentials from the file
    creds, err := credentials.NewClientTLSFromFile(caFile, "")
    if err != nil {
        log.Fatalf("Failed to load credentials: %v", err)
    }

    // Connect using the specific credentials
    conn, err := grpc.Dial("localhost:50051", grpc.WithTransportCredentials(creds))
    if err != nil {
        log.Fatalf("Connection failed: %v", err)
    }
    defer conn.Close()

    // Proceed with gRPC calls...
}

Method 2: Handling Hostname Mismatches

Even with a trusted CA, the handshake fails if the hostname doesn't match the certificate. If your cert was issued for api.internal.com but you are connecting to localhost, Go will reject it. You can override this expectation in the client:

// The second argument overrides the expected server name (ServerNameOverride)
creds, err := credentials.NewClientTLSFromFile(caFile, "api.internal.com")

Method 3: Bypassing Verification (Strictly for Testing)

If you are in a pinch and need to debug a connection issue, you can tell the client to ignore all certificate errors. Never use this in production as it leaves you vulnerable to Man-in-the-Middle (MITM) attacks.

import (
    "crypto/tls"
    "google.golang.org/grpc"
    "google.golang.org/grpc/credentials"
)

config := &tls.Config{
    InsecureSkipVerify: true,
}

creds := credentials.NewTLS(config)
conn, err := grpc.Dial("localhost:50051", grpc.WithTransportCredentials(creds))

Verifying the Fix

After updating your code, test the connection using grpcurl. It’s a great way to confirm the server is behaving before you dive back into Go code:

grpcurl -cacert ca.crt localhost:50051 list

A successful command returns a list of services. If you still see the x509 error, the issue is likely with the certificate file itself.

Pro-Tips for Production

Small errors in certificate files can lead to hours of frustration. If a certificate file is truncated or corrupted during a CI/CD deployment, the error message won't tell you why. I always verify the SHA-256 checksum of my ca.crt on both the local machine and the server to ensure they are identical.

For a quick check, I use ToolCraft's Hash Generator. It runs locally in your browser, so your certificate data stays private. Comparing hashes is a 10-second task that prevents a lot of headache.

Finally, avoid hardcoding file paths like /Users/dev/certs. Use environment variables such as SSL_CERT_PATH. This makes your service portable, whether it's running in a local Docker container or a production Kubernetes cluster.

Related Error Notes