Question

How to Handle HTTPS Requests with Invalid Certificates in Go

gosslhttps

Question

I want to make an HTTPS request to https://golang.org/ programmatically in Go. At the moment, the server presents a certificate that does not match the hostname, so this code fails:

package main

import (
    "log"
    "net/http"
)

func main() {
    _, err := http.Get("https://golang.org/")
    if err != nil {
        log.Fatal(err)
    }
}

The error is:

Get https://golang.org/: certificate is valid for *.appspot.com, *.*.appspot.com, appspot.com, not golang.org

Suppose I intentionally want to trust this certificate myself, such as in a private environment or when working with a self-signed certificate that I have verified separately. How can I make an HTTPS request in Go and configure certificate validation or trust manually?

Do I need to download the certificate with a tool such as OpenSSL, load it from a file, and configure a tls.Config value?

Short Answer

By the end of this page, you will understand why Go rejects invalid HTTPS certificates, how TLS verification works, and how to make secure custom HTTPS requests using http.Transport and tls.Config. You will also learn the difference between trusting a custom certificate authority and disabling verification entirely, and when each approach is appropriate.

Concept

HTTPS in Go uses TLS certificate verification to confirm two important things:

  • The server's certificate is signed by a trusted certificate authority (CA) or another CA you explicitly trust.
  • The certificate matches the hostname you are connecting to.

In the example, the request fails because the certificate says it is valid for *.appspot.com, but the code connects to golang.org. Even if you personally know the server is safe, Go correctly rejects it because hostname validation is part of HTTPS security.

This matters because without certificate and hostname checks, an attacker could impersonate a server and intercept traffic.

In Go, HTTPS behavior is usually controlled through:

  • http.Client
  • http.Transport
  • tls.Config

The key idea is:

  • If you want to trust a custom CA or self-signed certificate, add it to a certificate pool.
  • If the problem is a hostname mismatch, adding the certificate alone is not enough. The name still must match unless you disable verification or implement your own verification logic.

That distinction is very important:

  • Untrusted issuer → can often be fixed by adding a CA or certificate.
  • Wrong hostname → cannot be fixed just by trusting the cert; hostname verification is separate.

So yes, you may load certificates from files and use tls.Config, but the exact solution depends on why verification is failing.

Mental Model

Think of HTTPS like entering a secure office building.

  • The certificate authority is like the company that issued the employee badge.
  • The hostname is the employee name printed on the badge.
  • Go checks both:
    • Is the badge issued by someone I trust?
    • Does the badge belong to the person I expected to meet?

In your example, the badge may be real, but it has the wrong name on it. Trusting the badge issuer does not magically change the name on the badge.

So there are two different problems:

  • "I don't recognize who issued this badge" → add that issuer to your trusted list.
  • "This badge belongs to someone else" → reject it, unless you intentionally take over verification yourself.
Take Quiz

Syntax and Examples

The usual way to customize HTTPS in Go is to create your own http.Client with a custom Transport.

1. Default request

resp, err := http.Get("https://example.com")

This uses Go's default TLS verification.

2. Custom client with TLS config

package main

import (
    "crypto/tls"
    "crypto/x509"
    "io"
    "log"
    "net/http"
    "os"
)

func main() {
    certPEM, err := os.ReadFile("server-or-ca.pem")
    if err != nil {
        log.Fatal(err)
    }

    roots := x509.NewCertPool()
    if !roots.AppendCertsFromPEM(certPEM) {
        log.Fatal("failed to append certificate")
    }

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

    resp, err := client.Get("https://example.com")
    if err != nil {
        log.Fatal(err)
    }
    defer resp.Body.Close()

    body, err := io.ReadAll(resp.Body)
    if err != nil {
        log.Fatal(err)
    }

    log.Printf("status: %s", resp.Status)
    log.Printf("body: %s", body)
}

This works when the certificate chain is valid and the hostname matches.

3. Insecure skip verify

client := &http.Client{
    Transport: &http.Transport{
        TLSClientConfig: &tls.Config{
            InsecureSkipVerify: true,
        },
    },
}

This disables certificate verification entirely. It is useful for quick tests, but it is unsafe for production.

4. Custom verification callback

If you need to verify a certificate manually, Go allows custom verification logic.

package main

import (
    "crypto/sha256"
    "crypto/tls"
    "crypto/x509"
    "encoding/hex"
    "fmt"
    "io"
    "log"
    "net/http"
)

func main() {
    expectedFingerprint := "replace_with_expected_sha256_fingerprint"

    client := &http.Client{
        Transport: &http.Transport{
            TLSClientConfig: &tls.Config{
                InsecureSkipVerify: true,
                VerifyConnection: func(cs tls.ConnectionState) error {
                    if len(cs.PeerCertificates) == 0 {
                        return fmt.Errorf("no server certificate presented")
                    }

                    cert := cs.PeerCertificates[0]
                    sum := sha256.Sum256(cert.Raw)
                    actual := hex.EncodeToString(sum[:])

                    if actual != expectedFingerprint {
                        return fmt.Errorf("unexpected certificate fingerprint: %s", actual)
                    }

                    return nil
                },
            },
        },
    }

    resp, err := client.Get("https://example.com")
    if err != nil {
        log.Fatal(err)
    }
    defer resp.Body.Close()

    body, _ := io.ReadAll(resp.Body)
    fmt.Println(string(body))
}

This pattern is closer to certificate pinning. If you disable built-in verification, you must replace it with your own checks carefully.

Step by Step Execution

Consider this example using a custom CA pool:

certPEM, err := os.ReadFile("ca.pem")
if err != nil {
    log.Fatal(err)
}

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

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

resp, err := client.Get("https://my-service.local")

Here is what happens step by step:

  1. os.ReadFile("ca.pem")

    • Go reads the certificate file from disk.
    • This usually contains a CA certificate or self-signed certificate in PEM format.
  2. x509.NewCertPool()

    • A new pool of trusted certificates is created.
  3. roots.AppendCertsFromPEM(certPEM)

    • The certificate from the file is added to the trusted pool.
    • Now Go can trust certificates signed by that CA.
  4. http.Client with http.Transport

    • A custom HTTP client is created.
    • The transport controls low-level network behavior, including TLS.
  5. tls.Config{RootCAs: roots}

    • The TLS configuration tells Go to use your custom trusted roots when validating the server certificate.
  6. client.Get(...)

    • Go opens a TCP connection.
    • TLS handshake begins.
    • The server presents its certificate chain.
    • Go checks whether the certificate chains back to one of the trusted roots.
    • Go checks whether the certificate matches the requested hostname.
  7. If both checks pass

    • The request succeeds.
    • You receive the response.
  8. If either check fails

    • The request returns an error.

Important: if the hostname is wrong, adding the certificate to RootCAs does not fix that. Hostname verification still fails.

Real World Use Cases

Custom HTTPS verification appears in real projects in situations like these:

  • Internal company services

    • A private API may use an internal CA not trusted by the operating system.
    • The app loads the internal CA certificate and adds it to RootCAs.
  • Development environments

    • Local services may use self-signed certificates.
    • Developers often trust a local CA for testing.
  • IoT or embedded systems

    • Devices may connect only to a known backend certificate.
    • Certificate pinning or custom trust logic can be used.
  • Private Kubernetes clusters

    • Internal dashboards or service endpoints may use cluster-specific certificates.
    • Tools often need custom trust bundles.
  • Security-sensitive clients

    • Applications may pin server certificates or public keys to reduce CA-based attack risks.

In most real systems, the preferred fix is to issue a correct certificate for the correct hostname. Manual trust is usually a fallback for controlled environments.

Take Quiz

Real Codebase Usage

In real Go codebases, developers usually avoid changing global behavior and instead build a dedicated http.Client for the specific service.

Common patterns include:

Guard clauses for certificate loading

certPEM, err := os.ReadFile("ca.pem")
if err != nil {
    return fmt.Errorf("read CA file: %w", err)
}

This keeps failures clear and early.

Configuration-driven trust

Many apps load a CA path from configuration:

caPath := os.Getenv("CUSTOM_CA_FILE")

This makes environments flexible without changing code.

Reusable client factory

func newHTTPClient(rootPEM []byte) (*http.Client, error) {
    roots := x509.NewCertPool()
    if !roots.AppendCertsFromPEM(rootPEM) {
        return nil, fmt.Errorf("invalid root certificate")
    }

    tr := &http.Transport{
        TLSClientConfig: &tls.Config{RootCAs: roots},
    }

    return &http.Client{Transport: tr}, nil
}

This keeps TLS setup in one place.

Avoiding InsecureSkipVerify in production

Teams often allow it only in local development via config flags, and reject it elsewhere.

Validation plus pinning

Some clients do normal certificate validation and also check an expected fingerprint or public key. That gives stronger control without fully turning off built-in checks.

Error wrapping

resp, err := client.Get(url)
if err != nil {
    return fmt.Errorf("request %s failed: %w", url, err)
}

This makes TLS failures easier to debug in logs.

Common Mistakes

Here are common beginner mistakes when dealing with HTTPS certificates in Go.

1. Assuming trust fixes hostname mismatch

Broken idea:

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

This only adds trust. It does not make *.appspot.com valid for golang.org.

How to avoid it:

  • Make sure the server certificate matches the hostname.
  • If it does not, understand that you are bypassing a core HTTPS check.

2. Using InsecureSkipVerify: true without replacement checks

Broken code:

TLSClientConfig: &tls.Config{
    InsecureSkipVerify: true,
}

Problem:

  • This disables certificate and hostname verification.
  • It leaves you open to man-in-the-middle attacks.

How to avoid it:

  • Use a custom CA when possible.
  • If you must disable verification, add strict custom verification like fingerprint pinning.

3. Forgetting to close the response body

Broken code:

resp, err := client.Get(url)
if err != nil {
    log.Fatal(err)
}

// forgot: defer resp.Body.Close()

How to avoid it:

defer resp.Body.Close()

4. Loading the wrong certificate file

Beginners sometimes load the server certificate when they really need the CA certificate, or vice versa.

How to avoid it:

  • If a server cert is self-signed, that cert may be the trust anchor.
  • If a CA signed the server cert, usually trust the CA cert instead.

5. Reusing the default client when custom TLS is needed

Broken idea:

http.Get(url)

This uses the default client and default trust settings.

How to avoid it:

  • Build a custom http.Client when you need TLS customization.

6. Comparing fingerprints in the wrong format

If you do manual fingerprint checks, make sure the expected and actual values use the same format:

  • lowercase vs uppercase
  • hex with or without colons
  • SHA-256 vs SHA-1

Consistency matters.

Comparisons

ApproachWhat it doesHostname checked?Safer for production?Typical use
Default http.GetUses system trust store and normal TLS checksYesYesNormal public HTTPS requests
Custom RootCAsAdds trusted certificates or CAsYesYesInternal CA, self-signed certs in controlled environments
InsecureSkipVerify: trueDisables built-in certificate checksNoNoTemporary local testing only
Custom verification callbackLets you verify certs manuallyOnly if you implement itSometimesPinning, special trust rules

Custom CA vs disabling verification

  • Custom CA is the preferred approach when the issue is trust.
  • Disabling verification is a last resort when you understand the risks.

Certificate trust vs hostname validation

ProblemExampleFix
Unknown issuerSelf-signed cert or private CAAdd cert or CA to RootCAs
Hostname mismatchCert says api.internal, request goes to example.comUse the correct hostname or implement deliberate custom verification
Expired certificateCert is past valid dateReplace the certificate
Wrong chainMissing intermediate CAFix server chain or add correct certs
Take Quiz

Cheat Sheet

// Default HTTPS request
resp, err := http.Get("https://example.com")
// Custom trusted CA
roots := x509.NewCertPool()
roots.AppendCertsFromPEM(certPEM)

client := &http.Client{
    Transport: &http.Transport{
        TLSClientConfig: &tls.Config{
            RootCAs: roots,
        },
    },
}
// Unsafe: disables TLS verification
TLSClientConfig: &tls.Config{
    InsecureSkipVerify: true,
}

Rules to remember

  • HTTPS validation checks both trust chain and hostname.
  • RootCAs helps with trust, not hostname mismatch.
  • InsecureSkipVerify disables important security checks.
  • Prefer fixing the certificate on the server if possible.
  • Use a custom http.Client when TLS settings need to change.
  • Always close resp.Body.

Key packages

  • net/http — HTTP client
  • crypto/tls — TLS configuration
  • crypto/x509 — certificate pools and parsing
  • os — reading certificate files

Edge case

If the cert is valid but for the wrong hostname, adding it to RootCAs still will not make the request pass.

FAQ

Why does Go reject a certificate even if I trust it manually?

Because HTTPS checks more than issuer trust. It also checks whether the certificate matches the hostname you requested.

Can I bypass certificate verification in Go?

Yes, using InsecureSkipVerify: true, but this is unsafe unless you replace it with your own strict verification logic.

How do I trust a self-signed certificate in Go?

Load the certificate into an x509.CertPool, then assign that pool to tls.Config.RootCAs in a custom http.Client.

Does adding a certificate to RootCAs fix hostname mismatch?

No. RootCAs only affects trust of the issuer. Hostname validation is separate.

Should I use OpenSSL to download the certificate first?

You can, if you need to inspect or save the certificate. But the important part in Go is loading the correct PEM certificate and configuring tls.Config properly.

What is the safest solution if the certificate name is wrong?

Fix the server certificate so it includes the correct hostname. That is the standard and safest solution.

Can I verify a certificate fingerprint manually in Go?

Yes. You can inspect the peer certificate and compare its fingerprint in a custom verification callback.

Is certificate pinning better than trusting a CA?

It depends. Pinning gives tighter control but is harder to rotate and maintain. Trusting a CA is more flexible and common.

Take Quiz

Mini Project

Description

Build a small Go program that connects to a private HTTPS service using a custom certificate authority. This demonstrates how to replace the default system trust with your own trusted certificate bundle, which is common in internal APIs, staging environments, and local development setups.

Goal

Create a Go HTTP client that loads a PEM certificate from disk, trusts it, performs an HTTPS request, and prints the response status.

Requirements

  • Read a PEM certificate file from disk.
  • Add the certificate to a custom x509.CertPool.
  • Create an http.Client with a custom tls.Config.
  • Make an HTTPS GET request using that client.
  • Print the response status or a useful error message.
Take Quiz

Keep learning

Related questions

View more