tls

package
v2.61.0 Latest Latest
Warning

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

Go to latest
Published: Sep 30, 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 rotated material 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 configured with no authority at all is refused with ErrMissingClientCA. 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.

Every configured file must be usable. One that cannot be read fails with the underlying filesystem error, so a secret that has not been mounted is recognisable with errors.Is against fs.ErrNotExist; one holding no certificate, or a certificate that does not parse, fails with ErrInvalidPEM. A pool built from whichever files happened to read would narrow what the server trusts without saying so.

Two things are reported rather than refused, because neither changes what is trusted: a blank ClientCAFiles entry, and authorities configured alongside off or optional_no_ca, which never read them.

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

Application protocols

The application protocols offered during the handshake, as ALPN, belong to the server rather than to the material: a manager cannot know what the server it is given to speaks. httpserver names them for its listeners with WithNextProtos, so a consumer serving through it has nothing to do here. A server that names none negotiates none, which leaves an HTTP client on HTTP/1.1.

The configuration Manager.TLSConfig returns replaces itself as each handshake arrives, so a field set on it afterwards is not the one the handshake reads. An Option carries anything that has to reach the handshake.

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 authorities follow the same rule. Any configured file that cannot be read, or that holds something unusable, fails the reread, so the pool a server is verifying against is never narrowed by a bundle that was briefly unreadable.

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

What a reload reaches

A reread is picked up without a restart. Both the serving certificate and the client certificate authorities are chosen as each handshake arrives, rather than fixed into the configuration the server holds, so a connection that is already open keeps what it handshook with and the next connection gets what was last read. Nothing is closed to make the change take effect.

A client whose authority the server has stopped trusting is refused even when it resumes a cached session.

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 rotated material 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.

Application protocols

The application protocols offered during the handshake, as ALPN, belong to the server rather than to the material: a Manager cannot know what the server it is given to speaks. The httpserver package names them for its listeners with WithNextProtos, and a server that names none negotiates none, which leaves an HTTP client on HTTP/1.1.

The configuration Manager.TLSConfig returns replaces itself as each handshake arrives, so a field set on it afterwards is not the one the handshake reads. An Option carries anything that has to reach the handshake.

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 authorities follow the same rule. Any configured file that cannot be read, or that holds something unusable, fails the reread rather than narrowing the pool to whatever could be read.

A reread reaches a server that is already listening. Both the serving certificate and the client certificate authorities are read as each handshake arrives, so a connection already open keeps what it handshook with and the next one gets what was last read. Nothing is closed to make the change take effect.

A client whose authority the server has stopped trusting is refused even when it resumes a cached session.

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 is configured with no authority to verify against. 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(opts ...Option) *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.

Both the serving certificate and the client certificate authorities are read from m as each handshake arrives, so a reload reaches a server that is already listening.

A handshake is answered by a configuration m builds as it arrives, not by the value returned here, so setting a field on the returned value changes nothing. A server passes what it needs through an Option.

type Option added in v2.61.0

type Option func(*options)

Option supplies part of the crypto/tls configuration that belongs to the server rather than to the material a Manager holds.

func WithNextProtos added in v2.61.0

func WithNextProtos(protos ...string) Option

WithNextProtos names the application protocols a server offers during the handshake, in preference order, as the ALPN extension. A Manager holds material rather than a server, so it cannot know which protocols the server speaks. A server that names none negotiates none, which leaves an HTTP client on HTTP/1.1.

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