• English
  • Example: HTTP Client Trust a Custom CA

    Go's HTTP client trusts certificates signed by the system's trusted CAs by default.

    If an HTTPS server uses a private CA, the client will reject its certificate unless that CA is trusted.

    Add the private CA to the client's RootCAs instead of disabling certificate verification.

    Quick Example

    Load the private CA certificate, add it to the system certificate pool, and use the pool in tls.Config:

    package main
    
    import (
    	"crypto/tls"
    	"crypto/x509"
    	"fmt"
    	"log"
    	"net/http"
    	"os"
    )
    
    func main() {
    	if err := run(); err != nil {
    		log.Fatal(err)
    	}
    }
    
    func run() error {
    	caPEM, err := os.ReadFile("server-ca.pem")
    	if err != nil {
    		return fmt.Errorf("read CA certificate: %w", err)
    	}
    
    	roots, err := x509.SystemCertPool()
    	if err != nil {
    		return fmt.Errorf("load system certificate pool: %w", err)
    	}
    
    	if !roots.AppendCertsFromPEM(caPEM) {
    		return fmt.Errorf("parse CA certificate")
    	}
    
    	client := &http.Client{
    		Transport: &http.Transport{
    			TLSClientConfig: &tls.Config{
    				RootCAs: roots,
    			},
    		},
    	}
    
    	resp, err := client.Get("https://api.internal.example.com")
    	if err != nil {
    		return fmt.Errorf("request: %w", err)
    	}
    	defer resp.Body.Close()
    
    	fmt.Println(resp.Status)
    	return nil
    }

    The important part is:

    roots, err := x509.SystemCertPool()
    if err != nil {
    	return err
    }
    
    if !roots.AppendCertsFromPEM(caPEM) {
    	return fmt.Errorf("parse CA certificate")
    }
    
    tlsConfig := &tls.Config{
    	RootCAs: roots,
    }

    RootCAs tells the TLS client which root CAs it can trust when verifying the server certificate.

    Keep the System CAs

    If the client needs to connect to both public and private HTTPS services, start with SystemCertPool:

    roots, err := x509.SystemCertPool()
    if err != nil {
    	return err
    }
    
    roots.AppendCertsFromPEM(caPEM)

    Don't start with an empty pool:

    roots := x509.NewCertPool()
    roots.AppendCertsFromPEM(caPEM)

    NewCertPool contains no certificates initially. Using it means the client trusts only the certificates you explicitly add.

    That can be correct for a client that intentionally trusts only a private CA. It is usually not what you want for an application that also calls normal public HTTPS services.

    Don't Use InsecureSkipVerify

    A private CA does not require disabling certificate verification.

    Avoid:

    tls.Config{
    	InsecureSkipVerify: true,
    }

    This disables normal server certificate verification.

    If you get:

    x509: certificate signed by unknown authority

    check whether the private root CA is missing from the trust pool.

    Also check the server's certificate chain. An incomplete chain, such as a missing intermediate certificate, can produce the same error.

    Use ServerName When the Connection Address Doesn't Match

    The TLS name being verified does not have to be the same as the address used to establish the TCP connection.

    For example, a service might be reached through:

    https://10.0.1.100

    while its certificate is issued for:

    api.internal.example.com

    Set ServerName when the certificate name needs to be different from the connection address:

    tlsConfig := &tls.Config{
    	RootCAs:    roots,
    	ServerName: "api.internal.example.com",
    }

    ServerName controls the name used for certificate verification. It does not change the destination address used by the HTTP client.

    If the certificate contains the IP address in its SAN, an IP-based URL can be verified directly. Otherwise, the name being verified must match a DNS SAN in the certificate.

    Load a CA Bundle Carefully

    AppendCertsFromPEM can parse multiple PEM-encoded certificates:

    caPEM, err := os.ReadFile("ca-bundle.pem")
    if err != nil {
    	return err
    }
    
    if !roots.AppendCertsFromPEM(caPEM) {
    	return fmt.Errorf("no certificates were added")
    }

    A CA bundle can therefore contain multiple certificates that the application intentionally trusts.

    That does not mean every certificate in a server's TLS chain belongs in RootCAs.

    Normally, the server sends its intermediate certificates during the TLS handshake, while the client trusts the appropriate root CA. Adding an intermediate certificate to the trust pool changes what the client treats as a trust anchor and should be done deliberately.

    Custom CA Is Not Client Authentication

    Trusting a private CA only changes how the client verifies the server.

    Client authentication is a separate configuration:

    tls.Config{
    	RootCAs:      roots,
    	Certificates: clientCertificates,
    }

    RootCAs is for trusting server certificates.

    Certificates provides client certificates when the server requests client authentication.

    So a private CA does not automatically mean the connection uses mutual TLS.

    Reuse the Client

    Create the http.Client and http.Transport once and reuse them:

    transport := &http.Transport{
    	TLSClientConfig: &tls.Config{
    		RootCAs: roots,
    	},
    }
    
    client := &http.Client{
    	Transport: transport,
    }

    Don't create a new transport for every request.

    http.Transport maintains connection state and can reuse connections. http.Client and http.Transport are safe for concurrent use.

    If different services require different trust policies, use separate clients or transports rather than changing a shared tls.Config between requests.

    Certificate Revocation

    Adding a CA to RootCAs establishes trust in certificates issued by that CA. It does not provide general certificate revocation checking.

    Go's standard crypto/x509 verification does not automatically perform CRL or OCSP revocation checking.

    If certificate revocation is required by your environment, handle it separately according to the service's security requirements.

    When to Use This

    Use a custom CA when an HTTPS server uses a private or internal certificate authority:

    • internal APIs
    • private service-to-service HTTPS
    • corporate PKI
    • development or test environments with their own CA

    Keep normal certificate and hostname verification enabled.

    The usual solution to:

    x509: certificate signed by unknown authority

    is to configure the correct trust chain—not to turn TLS verification off.