Sửa lỗi 'x509: certificate signed by unknown authority' trong Go khi gọi HTTPS

intermediate🔷 Go2026-05-15| Go 1.16+, Linux / macOS / Windows, bất kỳ endpoint HTTPS nào dùng self-signed hoặc chứng chỉ CA nội bộ

Error Message

Get "https://example.com": x509: certificate signed by unknown authority
#tls#https#x509#certificate#net/http

Lỗi Gặp Phải

Bạn thực hiện một HTTPS request bằng package net/http của Go và nhận được:

Get "https://example.com": x509: certificate signed by unknown authority

Chứng chỉ TLS của máy chủ hoàn toàn hợp lệ — nhưng Go không tin tưởng CA đã cấp phát nó. Request của bạn bị hủy trước khi bất kỳ dữ liệu nào được trao đổi.

Nguyên Nhân

Go xác thực chứng chỉ TLS dựa trên kho CA (Certificate Authority) của hệ thống. Quá trình xác thực thất bại khi:

  • Máy chủ sử dụng chứng chỉ tự ký (self-signed certificate) — thường gặp trong môi trường dev và staging
  • Chứng chỉ được ký bởi một CA nội bộ/riêng tư chưa được đăng ký trong kho tin cậy của hệ điều hành
  • Một corporate proxy hoặc VPN đang thực hiện TLS inspection và ký lại traffic bằng CA của riêng nó (Zscaler, Palo Alto NGFW, Cisco Umbrella)
  • Bạn đang chạy trong một Docker container tối giản (scratch hoặc alpine) mà không có CA cert nào được cài đặt
  • Bộ CA bundle của hệ thống bị lỗi thời hoặc đơn giản là không tồn tại

Cách Sửa 1: Thêm CA Certificate vào System Trust Store

Đối với môi trường production và máy chủ dùng chung, hãy thêm CA cert vào kho tin cậy của hệ điều hành một lần duy nhất. Mọi binary Go trên máy đó sẽ tự động nhận diện nó — không cần thay đổi code.

Linux (Debian/Ubuntu)

# Copy CA cert của bạn (định dạng PEM) vào 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

Sau khi thêm cert, hãy khởi động lại ứng dụng Go của bạn. Nó đọc kho hệ thống lúc khởi động, không phải mỗi lần request.

Cách Sửa 2: Tải CA Certificate Trực Tiếp Trong Code

Không thể chỉnh sửa system trust store? Hãy tải CA cert trực tiếp trong code Go của bạn. Cách này hoạt động tốt trên các máy chủ dùng chung, container chỉ đọc, hoặc bất kỳ nơi nào bạn không có quyền truy cập vào hệ điều hành.

package main

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

func main() {
    // Đọc file CA certificate của bạn (định dạng PEM)
    caCert, err := os.ReadFile("your-ca.crt")
    if err != nil {
        panic(err)
    }

    // Thêm vào system pool
    caCertPool, err := x509.SystemCertPool()
    if err != nil {
        // Fallback: tạo một pool rỗng
        caCertPool = x509.NewCertPool()
    }
    caCertPool.AppendCertsFromPEM(caCert)

    // Khởi tạo HTTP client sử dụng pool mở rộng
    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))
}

CA cert của bạn đi kèm với binary. Không cần thay đổi hệ điều hành, không ảnh hưởng toàn hệ thống.

Cách Sửa 3: Khắc Phục CA Cert Bị Thiếu Trong Docker Container

Các base image tối giản — scratch, alpine, distroless — không đi kèm CA certificate nào. Binary biên dịch bình thường. Nhưng khi chạy, HTTPS call đầu tiên sẽ bị crash.

Tùy chọn A — Cài ca-certificates trong Alpine

FROM alpine:3.19
RUN apk add --no-cache ca-certificates
COPY your-app /app
ENTRYPOINT ["/app"]

Tùy chọn B — Copy CA cert từ builder stage (dùng cho scratch image)

FROM golang:1.22 AS builder
WORKDIR /src
COPY . .
RUN CGO_ENABLED=0 go build -o app .

FROM scratch
# Copy system CA bundle từ builder
COPY --from=builder /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/
COPY --from=builder /src/app /app
ENTRYPOINT ["/app"]

Cách Sửa 4: Bỏ Qua Xác Thực TLS (Chỉ Dùng Khi Phát Triển)

Tùy chọn này tồn tại. Chỉ dùng cho kiểm thử local. Trong môi trường production, InsecureSkipVerify: true có nghĩa là bất kỳ máy chủ nào — kể cả kẻ tấn công man-in-the-middle — đều có thể đưa ra chứng chỉ bất kỳ và code của bạn chấp nhận mà không cần kiểm tra.

client := &http.Client{
    Transport: &http.Transport{
        TLSClientConfig: &tls.Config{
            InsecureSkipVerify: true, // ⚠️ CHỈ DÙNG KHI DEV
        },
    },
}

Tìm thấy đoạn này trong code production? Hãy xử lý nguyên nhân gốc rễ bằng Cách Sửa 1 hoặc Cách Sửa 2.

Cách Sửa 5: Corporate Proxy / VPN TLS Inspection

Dấu hiệu nhận biết: đồng nghiệp của bạn trên cùng mạng không gặp lỗi này, nhưng bạn thì có. Rất có thể một proxy đang chặn traffic TLS và ký lại bằng CA của riêng nó. Hãy hỏi đội IT hoặc bảo mật để lấy root CA certificate của proxy, sau đó áp dụng Cách Sửa 1 hoặc Cách Sửa 2 với cert đó.

Để xem chính xác chứng chỉ nào mà máy chủ đang trình bày, hãy chạy:

openssl s_client -connect example.com:443 < /dev/null 2>&1 | openssl x509 -noout -issuer -subject

Nếu issuer hiển thị "Zscaler Root CA" hoặc tên công ty bạn thay vì DigiCert hay Let's Encrypt, đó chính là proxy cert bạn cần tin tưởng.

Xác Nhận Đã Sửa Xong

Chạy lại chương trình Go của bạn. Không còn lỗi TLS nghĩa là đã sửa thành công. Để kiểm tra thủ công, dùng OpenSSL trực tiếp:

# Chạy chương trình với output chi tiết
go run -v your_main.go

# Xác thực chuỗi cert với CA file của bạn
openssl s_client -connect example.com:443 -CAfile your-ca.crt

Khi OpenSSL báo Verify return code: 0 (ok), chuỗi chứng chỉ đã hoàn toàn hợp lệ. Go sẽ chấp nhận nó.

Phòng Ngừa

  • Lưu CA nội bộ trong version control: Đặt CA cert của công ty vào một repo dùng chung hoặc công cụ quản lý cấu hình (Ansible, Chef, Puppet). Mỗi máy chủ mới sẽ tự động nhận chúng khi được khởi tạo — không cần thao tác thủ công.
  • Luôn đóng gói ca-certificates trong Docker image có thực hiện HTTPS call ra ngoài. Đừng để đến khi xảy ra sự cố production mới phát hiện ra nó đang bị thiếu.
  • Cấm InsecureSkipVerify trong CI: Một lệnh grep đơn giản có thể phát hiện trước khi merge — grep -r InsecureSkipVerify ./ trong pipeline PR của bạn.
  • Tải đường dẫn CA từ biến môi trường: Cùng một binary có thể hoạt động ở cả môi trường dev (self-signed cert) và production (public CA) mà không cần thay đổi code.

Related Error Notes