tls

package
v2.57.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Sep 25, 2026 License: MIT Imports: 16 Imported by: 0

README

tls

Package tls builds the TLS configuration a server serves with, and rereads the files behind it on an interval so that a rotated certificate is served without a restart. A Manager implements app.Component, so the app.App lifecycle starts and stops it alongside everything else.

Consumers do not import crypto/tls. Paths are strings, and the client authentication mode, the protocol versions and the cipher suites are string enums.

Quick start

manager, err := tls.NewWithConfig(&tls.Config{
    Name:     "api",
    Enabled:  true,
    CertFile: "/secrets/tls/tls.crt",
    KeyFile:  "/secrets/tls/tls.key",
    Logger:   a.Logger(),
})
if err != nil {
    return err
}

srv := httpserver.NewWithConfig(&httpserver.Config{
    Addr:       ":8443",
    Handler:    mux,
    TLSManager: manager,
})

a.Register(manager)
a.Register(srv)

The server does not own the manager. Register both, and register the manager first: components shut down in reverse registration order, so the server stops serving before the manager stops rereading.

One manager can be handed to several servers, which then serve the same material:

srv := httpserver.NewWithConfig(&httpserver.Config{
    Addr:            ":8443",
    Handler:         mux,
    TLSManager:      manager,
    ProbeTLSManager: manager,
})

Serving plaintext

The zero Config serves plaintext, and so does any config with Enabled: false. Manager.TLSConfig returns nil for such a manager, which is what leaves a listener unencrypted. A consumer can therefore wire TLS unconditionally and leave the decision to one configuration value.

Enabled is never inferred from the presence of certificate material. A configuration that names files without being enabled serves plaintext, and says so in a log line.

Configuration

manager, err := tls.NewWithConfig(&tls.Config{
    Name:           "api",                          // names this configuration in logs, errors and metrics (default: "tls")
    Enabled:        true,                           // default: false, which serves plaintext
    CertFile:       "/secrets/tls/tls.crt",         // PEM serving certificate
    KeyFile:        "/secrets/tls/tls.key",         // PEM private key for that certificate
    ClientCAFiles:  []string{"/secrets/ca/ca.crt"}, // authorities client certificates are verified against
    ClientAuth:     tls.ClientAuthOptional,         // default: resolved from ClientCAFiles; see below
    MinVersion:     tls.VersionTLS12,               // default: tls1.2
    MaxVersion:     tls.VersionTLS13,               // default: no maximum
    CipherSuites:   []tls.CipherSuite{"TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256"}, // default: left to crypto/tls
    ReloadInterval: time.Minute,                    // default: 1m
    Logger:         a.Logger(),                     // default: nothing is logged
    Registerer:     a.Metrics().Registerer(),       // default: no metrics
})

CipherSuites applies to TLS 1.2 only. TLS 1.3 cipher suites are not configurable in crypto/tls, so naming one here does not select it. Suites that crypto/tls reports as insecure are refused rather than served.

Verifying client certificates

ClientAuth decides what the server asks of a client, and what it does with the answer. The values are nginx's ssl_verify_client vocabulary, matched case-insensitively after trimming.

ClientAuth Server requests a certificate Client presents none Client presents one that does not verify
off no admitted n/a
optional_no_ca yes admitted admitted, unverified
optional yes admitted rejected
on yes rejected rejected

optional_no_ca requests a certificate and admits whatever arrives without checking it against anything. optional and on verify the chain against ClientCAFiles.

manager, err := tls.NewWithConfig(&tls.Config{
    Name:          "api",
    Enabled:       true,
    CertFile:      "/secrets/tls/tls.crt",
    KeyFile:       "/secrets/tls/tls.key",
    ClientCAFiles: []string{"/secrets/ca/ca.crt"},
    ClientAuth:    tls.ClientAuthOn,
})
An unset mode resolves from the authorities

Leaving ClientAuth unset resolves it from whether ClientCAFiles names an authority:

  • No authority resolves to off.
  • An authority resolves to optional, and says so at info level.

It resolves to optional rather than on so that adding an authority cannot by itself start rejecting clients. Mounting a CA and enforcing mutual TLS are then two separate changes, in that order, and the second is deliberate.

What is refused and what is only reported

A verifying mode with no authority to verify against is refused with ErrMissingClientCA, and so is one whose files yield no certificate between them. An empty pool is not the same as a permissive one: crypto/tls falls back to the host's trust store, which would admit a client certificate issued by any public authority.

Everything short of that is reported and skipped, so one bad entry does not stop a service starting:

  • A ClientCAFiles entry that is blank, a file that cannot be read, and a PEM block that is not a certificate or does not parse are each logged and ignored.
  • Authorities configured alongside off or optional_no_ca are logged as ignored, because neither mode reads them.

A file may hold a bundle, and every certificate in every file is trusted.

Reloading material

Registering the manager with the app.App starts the rereads. Every ReloadInterval, which defaults to one minute, the configured files are read again and what they hold replaces what the manager is serving. A manager serving plaintext starts nothing, and one that has already been started returns ErrAlreadyStarted rather than running a second loop.

A reread that cannot read the key pair changes nothing. The material already in use stays in place and the server goes on serving what it last read, so a secret caught half written does not interrupt service. The failure is logged at error level: construction refuses material it cannot read, so a service whose rereads are failing is one restart away from not starting at all, even while it serves clients normally.

The certificate authorities follow the rule given above instead. An unreadable file or an unusable entry is logged and skipped, and only a set that yields no certificate between them fails the reread, so a reread that succeeds commits whatever pool the readable files produced.

Shutdown ends the rereads and waits for a reread in flight to finish, or for its context to expire.

What a reload reaches

A rotated serving certificate is picked up without a restart. The certificate is chosen as each handshake arrives, rather than fixed into the configuration the server holds, so a connection that is already open keeps the certificate it handshook with and the next connection gets the new one. Nothing is closed to make the change take effect.

The client certificate authorities do not yet follow it. They are the ones loaded when Manager.TLSConfig was called, so changing trust roots currently takes a restart.

Metrics

Setting Registerer publishes two families, each labelled with Config.Name. A manager serving plaintext reads no material and publishes nothing.

manager, err := tls.NewWithConfig(&tls.Config{
    Name:       "api",
    Enabled:    true,
    CertFile:   "/secrets/tls/tls.crt",
    KeyFile:    "/secrets/tls/tls.key",
    Registerer: a.Metrics().Registerer(),
})

Scraped from a server wired that way:

# HELP gitlab_labkit_tls_certificate_expiry_timestamp_seconds Unix time at which the serving certificate stops being valid.
# TYPE gitlab_labkit_tls_certificate_expiry_timestamp_seconds gauge
gitlab_labkit_tls_certificate_expiry_timestamp_seconds{name="api"} 1.821738922e+09
# HELP gitlab_labkit_tls_reloads_total Total number of rereads of the configured TLS material, by outcome.
# TYPE gitlab_labkit_tls_reloads_total counter
gitlab_labkit_tls_reloads_total{name="api",outcome="failure"} 0
gitlab_labkit_tls_reloads_total{name="api",outcome="success"} 3

The expiry is an absolute timestamp rather than a remaining duration, read off the certificate at collection time.

Metric names are fixed, so one alert can cover every service. Config.Name is what tells one configuration's series from another's, which is why each configuration registered against the same registerer needs a distinct name; two that collide fail construction rather than reporting into one series.

Documentation

Overview

Package tls builds the TLS configuration a server serves with, and rereads the files behind it on an interval so that a rotated certificate is served without a restart.

A consumer does not import crypto/tls. Paths are strings, and the client authentication mode, the protocol versions and the cipher suites are string enums.

The zero Config serves plaintext, which lets a consumer wire TLS unconditionally and leave the decision to configuration. Config.Enabled is never inferred from the presence of certificate material: a configuration that carries material without being enabled is reported, and serves plaintext.

Manager implements app.Component, so an app.App starts and shuts it down alongside everything else. One Manager can be handed to several servers, which then serve the same material.

Basic usage

mux := http.NewServeMux()

a, err := app.New(ctx)
if err != nil { log.Fatal(err) }

manager, err := tls.NewWithConfig(&tls.Config{
	Name:     "api",
	Enabled:  true,
	CertFile: "/secrets/tls/tls.crt",
	KeyFile:  "/secrets/tls/tls.key",
	Logger:   a.Logger(),
})
if err != nil { log.Fatal(err) }

srv := httpserver.NewWithConfig(&httpserver.Config{
	Addr:       ":8443",
	Handler:    mux,
	TLSManager: manager,
})

a.Register(manager)
a.Register(srv)

if err := a.Start(ctx); err != nil { log.Fatal(err) }
defer a.Shutdown(ctx) //nolint:errcheck

Verifying client certificates

Config.ClientAuth selects what the server asks of a client, from least to most strict: ClientAuthOff neither requests nor accepts a certificate, ClientAuthOptionalNoCA requests one and admits whatever is presented without verifying it, ClientAuthOptional verifies a certificate when one is presented and admits a client that presents none, and ClientAuthOn requires every client to present one that verifies.

An unset mode is resolved from whether Config.ClientCAFiles names an authority. With none it resolves to ClientAuthOff. With one it resolves to ClientAuthOptional rather than ClientAuthOn, so that adding an authority cannot by itself start rejecting clients; moving to ClientAuthOn is a separate, deliberate change. A mode that verifies without an authority to verify against is refused with ErrMissingClientCA, because crypto/tls would otherwise fall back to the host's trust store and admit a client certificate from any public authority.

Reloading material

Manager.Start begins rereading the configured files every Config.ReloadInterval, which defaults to one minute. A reread that cannot read the key pair changes nothing: the material already in use stays in place and the server goes on serving what it last read, so a secret caught half written does not interrupt service.

The certificate authorities are treated more leniently. An unreadable file or an unusable entry is skipped, and only a set that yields no certificate between them fails the reread.

A rotated serving certificate reaches a configuration a server already holds, because the certificate is read as each handshake arrives.

The client certificate authorities do not yet follow it: they are the ones loaded when Manager.TLSConfig was called, so changing trust roots currently takes a restart.

Metrics

Setting Config.Registerer publishes the expiry of the serving certificate and a count of rereads by outcome, each labelled with Config.Name:

manager, err := tls.NewWithConfig(&tls.Config{
	Name:       "api",
	Enabled:    true,
	CertFile:   "/secrets/tls/tls.crt",
	KeyFile:    "/secrets/tls/tls.key",
	Registerer: a.Metrics().Registerer(),
})

Each configuration registered against one registerer needs a distinct Config.Name, since the name is what tells the series apart.

Index

Constants

This section is empty.

Variables

View Source
var ErrAlreadyStarted = errors.New("tls manager already started")

ErrAlreadyStarted is returned by Manager.Start for a Manager that is already rereading its files. A Manager is started once and is not restarted.

View Source
var ErrIncompleteKeyPair = errors.New("incomplete tls key pair")

ErrIncompleteKeyPair is returned when a serving certificate is configured without its private key, or a private key without its certificate.

View Source
var ErrInvalidCipherSuite = errors.New("invalid tls cipher suite")

ErrInvalidCipherSuite is returned when a cipher suite is configured that this package cannot serve.

View Source
var ErrInvalidClientAuth = errors.New("invalid tls client authentication")

ErrInvalidClientAuth is returned when client authentication is configured in a way this package cannot serve.

View Source
var ErrInvalidPEM = errors.New("invalid tls pem material")

ErrInvalidPEM is returned when a file that should hold PEM-encoded TLS material cannot be parsed, holds the wrong kind of material, or holds a certificate and a private key that do not belong together.

View Source
var ErrInvalidReloadInterval = errors.New("invalid tls reload interval")

ErrInvalidReloadInterval is returned when the interval between rereads of the configured files is negative, which names no schedule.

View Source
var ErrInvalidVersion = errors.New("invalid tls version")

ErrInvalidVersion is returned when the configured TLS protocol versions cannot be served.

View Source
var ErrMissingClientCA = errors.New("no client certificate authority")

ErrMissingClientCA is returned when a client authentication mode that verifies certificates has no authority to verify against, whether because none was configured or because none of the configured files yielded one. A nil crypto/tls ClientCAs falls back to the host's trust store, which would admit any clientAuth certificate from any public authority.

View Source
var ErrNoCertificate = errors.New("no tls serving certificate configured")

ErrNoCertificate is returned when TLS is enabled but no serving certificate is configured.

View Source
var ErrNoCipherSuite = errors.New("no usable tls cipher suite configured")

ErrNoCipherSuite is returned when no configured cipher suite can serve a protocol version the server negotiates, which would fail every handshake at that version.

Functions

This section is empty.

Types

type CipherSuite

type CipherSuite string

CipherSuite names a TLS cipher suite, spelled as crypto/tls spells it.

func (*CipherSuite) UnmarshalText

func (s *CipherSuite) UnmarshalText(text []byte) error

UnmarshalText implements encoding.TextUnmarshaler, so a CipherSuite can be decoded straight out of JSON, YAML or an environment variable without the consumer writing an adapter. The receiver is left unchanged when the input does not parse.

type ClientAuthMode

type ClientAuthMode string

ClientAuthMode selects how a server treats a client certificate. The values are nginx's ssl_verify_client vocabulary, so a Helm chart value and a LabKit value are the same string.

const (
	// ClientAuthUnset leaves the mode to be resolved from the rest of the
	// configuration. It is the zero value.
	ClientAuthUnset ClientAuthMode = ""

	// ClientAuthOff neither requests nor accepts a client certificate.
	ClientAuthOff ClientAuthMode = "off"

	// ClientAuthOptionalNoCA requests a client certificate and admits the
	// client whatever it presents, without verifying it. The certificate is
	// available to the application, which takes on the whole of the
	// verification burden.
	ClientAuthOptionalNoCA ClientAuthMode = "optional_no_ca"

	// ClientAuthOptional verifies a client certificate against the configured
	// certificate authorities when one is presented, and admits a client that
	// presents none.
	ClientAuthOptional ClientAuthMode = "optional"

	// ClientAuthOn requires every client to present a certificate that
	// verifies against the configured certificate authorities.
	ClientAuthOn ClientAuthMode = "on"
)

func (ClientAuthMode) String

func (m ClientAuthMode) String() string

String implements fmt.Stringer. The zero value renders as "unset" so it is distinguishable in a log line.

func (*ClientAuthMode) UnmarshalText

func (m *ClientAuthMode) UnmarshalText(text []byte) error

UnmarshalText implements encoding.TextUnmarshaler, so a ClientAuthMode can be decoded straight out of JSON, YAML or an environment variable without the consumer writing an adapter. The receiver is left unchanged when the input does not parse.

type Config

type Config struct {
	// Name identifies this configuration in errors and log lines. Defaults to
	// "tls". Give distinct names to distinct listeners.
	Name string

	// Enabled turns TLS on. It defaults to false and is never inferred from
	// the presence of certificate material, mirroring tls.enabled in the
	// GitLab Helm charts. A disabled configuration that carries material is
	// reported, and serves plaintext.
	Enabled bool

	// CertFile is the path to the PEM-encoded serving certificate. It may hold
	// a chain, leaf first, when clients need intermediates to reach a trusted
	// root. In a Kubernetes deployment it is the tls.crt entry of a
	// kubernetes.io/tls secret, mounted as a file.
	//
	// The certificate must carry the names its clients dial as subject
	// alternative names; a common name alone is not accepted.
	//
	// The path is trimmed, and a blank value counts as absent.
	CertFile string

	// KeyFile is the path to the PEM-encoded private key for CertFile. In a
	// Kubernetes deployment it is the tls.key entry of the same secret.
	//
	// The path is trimmed, and a blank value counts as absent.
	KeyFile string

	// ClientCAFiles are paths to PEM-encoded certificate authorities that
	// client certificates are verified against. A file may hold a bundle, and
	// every certificate in every file is trusted. In a Kubernetes deployment
	// this is the ca.crt entry of a trust-manager Bundle or an Opaque secret,
	// mounted as a file.
	//
	// Paths are trimmed, and a blank entry is ignored.
	//
	// Configuring authorities does not by itself require a client to present a
	// certificate. See ClientAuth.
	ClientCAFiles []string

	// ClientAuth selects how a client certificate is treated. The zero value
	// resolves against ClientCAFiles: ClientAuthOptional when authorities are
	// configured, ClientAuthOff when they are not.
	ClientAuth ClientAuthMode

	// MinVersion is the oldest protocol version the server will negotiate. The
	// zero value resolves to VersionTLS12.
	MinVersion Version

	// MaxVersion is the newest protocol version the server will negotiate. The
	// zero value imposes no maximum.
	MaxVersion Version

	// CipherSuites are the cipher suites the server will negotiate for TLS 1.2,
	// spelled as crypto/tls spells them and matched case-insensitively. The
	// zero value leaves the choice to crypto/tls. TLS 1.3 cipher suites are not
	// configurable, so naming one here does not select it.
	CipherSuites []CipherSuite

	// ReloadInterval is how often the configured files are reread, so that a
	// rotated certificate is served without a restart. The zero value resolves
	// to one minute.
	ReloadInterval time.Duration

	// Logger receives the log lines this configuration emits as it resolves.
	// When nil, nothing is logged.
	Logger *slog.Logger

	// Registerer publishes the Prometheus metrics describing the material
	// served. When nil, no metrics are collected, as is the case for a
	// configuration that is not enabled and so reads no material.
	//
	// Each configuration registered against the same Registerer must have a
	// distinct Name, which is the constant label on every metric.
	Registerer prometheus.Registerer
}

Config describes how a server serves TLS. The zero value serves plaintext, so a consumer can wire TLS unconditionally and leave the decision to configuration.

type Manager added in v2.50.0

type Manager struct {
	// contains filtered or unexported fields
}

Manager holds the material a server serves TLS with. Build one with NewWithConfig.

Start and Shutdown must not be called concurrently. The expected usage is to call Start once during application startup and Shutdown once during graceful shutdown, consistent with the app.Component lifecycle.

func NewWithConfig added in v2.50.0

func NewWithConfig(cfg *Config) (*Manager, error)

NewWithConfig returns a Manager for cfg with its material loaded. A nil cfg, and any configuration that is not enabled, yields a Manager that serves plaintext and reads no files.

cfg is resolved once, here, so the log lines describing it are emitted once.

func (*Manager) Name added in v2.53.0

func (m *Manager) Name() string

Name returns the component name for use in logs and error messages.

func (*Manager) Shutdown added in v2.53.0

func (m *Manager) Shutdown(ctx context.Context) error

Shutdown ends the rereads and waits for the reload in flight to finish, or for ctx to expire. A Manager that was never started shuts down at once, and shutting one down twice is safe.

func (*Manager) Start added in v2.53.0

func (m *Manager) Start(context.Context) error

Start begins rereading the configured files on an interval, so that material replaced on disk is served without a restart. It returns immediately, and a Manager serving plaintext starts nothing.

A Manager that has been started returns ErrAlreadyStarted, whether or not it has since been shut down.

func (*Manager) TLSConfig added in v2.50.0

func (m *Manager) TLSConfig() *stdtls.Config

TLSConfig returns the crypto/tls configuration a server serves m with, or nil for a Manager that serves plaintext. Each call returns a new value.

The serving certificate is read from m as each handshake arrives, so a reload reaches a configuration a server already holds. The client certificate authorities are the ones m had loaded when this was called.

type Version

type Version string

Version names a TLS protocol version.

const (
	// VersionUnset leaves the version to be defaulted. It is the zero value.
	VersionUnset Version = ""

	// VersionTLS12 is TLS 1.2.
	VersionTLS12 Version = "tls1.2"

	// VersionTLS13 is TLS 1.3.
	VersionTLS13 Version = "tls1.3"
)

func (Version) String

func (v Version) String() string

String implements fmt.Stringer. The zero value renders as "unset" so it is distinguishable in a log line.

func (*Version) UnmarshalText

func (v *Version) UnmarshalText(text []byte) error

UnmarshalText implements encoding.TextUnmarshaler, so a Version can be decoded straight out of JSON, YAML or an environment variable without the consumer writing an adapter. The receiver is left unchanged when the input does not parse.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL