Skip to content

mTLS Configuration

CYPT71 edited this page Aug 9, 2026 · 1 revision

mTLS Configuration: internal/mtls

A small, dependency-free package (crypto/tls, crypto/x509 only) that builds conservative TLS configurations for services you package with this project's builder. It is not wired into the builder or into any consumer automatically — the security.tls.minimum/security.mtls OCI labels mentioned in CLI Reference are pure documentation metadata; if you want real mTLS enforcement, your service imports and calls this package itself.

API

type Options struct {
    CAPEM        []byte            // PEM-encoded CA bundle
    Certificates []tls.Certificate // from tls.LoadX509KeyPair or tls.X509KeyPair
    ServerName   string            // client-side: expected server name
    MutualTLS    bool              // server-side: require + verify client certs
}

func ClientConfig(options Options) (*tls.Config, error)
func ServerConfig(options Options) (*tls.Config, error)

ClientConfig

Returns a *tls.Config with:

  • MinVersion: tls.VersionTLS12 - TLS 1.2 is the floor.
  • No MaxVersion set - deliberately left at Go's default (unset), which means TLS 1.3 is automatically enabled whenever both sides support it. This is a common mistake to get backwards: pinning MaxVersion to 1.2 "to be safe" actually prevents the connection from ever using the newer, stronger protocol version.
  • RootCAs built from Options.CAPEM (see certificatePool below).
  • Certificates copied (not aliased) from Options.Certificates, for client-certificate authentication if the server requires it.
  • ServerName passed through for SNI / hostname verification.

ServerConfig

  • Requires at least one certificate - returns an error immediately if Options.Certificates is empty. A TLS server with no certificate can't serve anything, so this fails fast instead of producing a config that would only break at the first handshake.
  • Same MinVersion: tls.VersionTLS12, no MaxVersion cap, same reasoning as ClientConfig.
  • If Options.MutualTLS is true:
    • requires a non-empty CA bundle - returns an error if CAPEM is empty, since mutual TLS with no way to verify client certificates would be a false sense of security.
    • sets ClientAuth: tls.RequireAndVerifyClientCert and ClientCAs to the parsed pool - every connecting client must present a certificate the server can verify against that bundle. There is no "request but don't require" mode exposed here; this package only offers the strict form.

certificatePool (internal)

  • An empty/nil CAPEM returns (nil, nil) - not an error. This lets ClientConfig be called with no custom CA bundle (falling back to the Go runtime's system trust store) while still surfacing a real error if a non-empty CAPEM contains no valid PEM certificates (pool.AppendCertsFromPEM returning false).

Example: a client trusting a private CA

caPEM, err := os.ReadFile("ca.pem")
if err != nil {
    log.Fatal(err)
}
cfg, err := mtls.ClientConfig(mtls.Options{
    CAPEM:      caPEM,
    ServerName: "internal-service.example",
})
if err != nil {
    log.Fatal(err)
}
client := &http.Client{Transport: &http.Transport{TLSClientConfig: cfg}}

Example: a server requiring mutual TLS

cert, err := tls.LoadX509KeyPair("server.pem", "server-key.pem")
if err != nil {
    log.Fatal(err)
}
caPEM, err := os.ReadFile("client-ca.pem")
if err != nil {
    log.Fatal(err)
}
cfg, err := mtls.ServerConfig(mtls.Options{
    Certificates: []tls.Certificate{cert},
    CAPEM:        caPEM,
    MutualTLS:    true,
})
if err != nil {
    log.Fatal(err)
}
srv := &http.Server{TLSConfig: cfg, Addr: ":8443"}

Testing

internal/mtls/config_test.go covers: minimum-version enforcement, missing-certificate rejection in ServerConfig, missing-CA rejection when MutualTLS is requested, and invalid-PEM rejection in the CA pool builder. Run just this package's tests with:

go test ./internal/mtls -count=1

(This exact invocation is also one of the explicit regression checks in ci-quality.yml - see Testing and CI/CD.)

Clone this wiki locally