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.Clienthttp.Transporttls.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 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.
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(, resp.Status)
log.Printf(, body)
}
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:
-
os.ReadFile("ca.pem")- Go reads the certificate file from disk.
- This usually contains a CA certificate or self-signed certificate in PEM format.
-
x509.NewCertPool()- A new pool of trusted certificates is created.
-
roots.AppendCertsFromPEM(certPEM)- The certificate from the file is added to the trusted pool.
- Now Go can trust certificates signed by that CA.
-
http.Clientwithhttp.Transport- A custom HTTP client is created.
- The transport controls low-level network behavior, including TLS.
-
tls.Config{RootCAs: roots}
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.
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.
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)
}
Comparisons
| Approach | What it does | Hostname checked? | Safer for production? | Typical use |
|---|---|---|---|---|
Default http.Get | Uses system trust store and normal TLS checks | Yes | Yes | Normal public HTTPS requests |
Custom RootCAs | Adds trusted certificates or CAs | Yes | Yes | Internal CA, self-signed certs in controlled environments |
InsecureSkipVerify: true | Disables built-in certificate checks | No | No | Temporary local testing only |
| Custom verification callback | Lets you verify certs manually | Only if you implement it | Sometimes |
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.
RootCAshelps with trust, not hostname mismatch.InsecureSkipVerifydisables important security checks.- Prefer fixing the certificate on the server if possible.
- Use a custom
http.Clientwhen TLS settings need to change. - Always close
resp.Body.
Key packages
net/http— HTTP clientcrypto/tls— TLS configurationcrypto/x509— certificate pools and parsingos— reading certificate files
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?
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.Clientwith a customtls.Config. - Make an HTTPS GET request using that client.
- Print the response status or a useful error message.
Keep learning
Related questions
Automatic Build Versioning in Go: Embed Incrementing Build Numbers
Learn how to add automatic build versioning in Go using linker flags, build metadata, CI counters, and Git-based version values.
Blank Identifier Imports in Go: What `_` Means in an Import Statement
Learn what `_` means in a Go import, why blank identifier imports run package init code, and when to use them safely.
Calling Functions Across Files in the Same Go Package
Learn how Go uses packages across multiple files, why functions may appear undefined, and how to organize code correctly.