caddykms

package module
v0.0.0-...-76cf538 Latest Latest
Warning

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

Go to latest
Published: Sep 9, 2026 License: Apache-2.0 Imports: 28 Imported by: 0

README

caddy-kms

A Caddy module that serves TLS from a certificate whose private key never leaves a key management system — a TPM, a PKCS #11 token, or a cloud KMS — using the KMS abstraction from smallstep/crypto.

The key and certificate are provisioned and rotated outside Caddy. Caddy is a consumer only: no ACME, no issuance, no renewal.

Install

xcaddy build --with github.com/hslatman/caddy-kms

The default build is pure Go and needs no cgo. It registers two KMS backends, tpmkms and softkms.

Quick start

example.com {
    kms_certificate tpmkms:name=caddy-tls {
        certificate       /etc/caddy/tls.crt
        storage_directory /etc/step/tpm
    }
}

That is enough to stop Caddy managing certificates for example.com. Caddy skips automatic management for any name it already holds a certificate for, so you do not need auto_https off or an automation policy to prevent ACME.

Configuration

Field Caddyfile Required Meaning
key first argument yes KMS URI; the scheme selects the backend
certificate certificate no Path to a PEM file. Omit to load the chain from the KMS.
kms.pin pin no Passed to the backend as its PIN
kms.storage_directory storage_directory no Where the TPM KMS keeps its serialized objects
tags tags no Caddy certificate tags, for explicit selection. Loader only.
ttl ttl no Cache lifetime before re-reading from the KMS. Manager only, default 5m.

All string fields go through Caddy's replacer, so {env.*} placeholders work.

The key URI does double duty, exactly as step's ca.json does it: its scheme selects the backend, any backend parameters it carries are honoured, and its name= identifies the key. Backends ignore URI parameters they do not recognise, which is what makes one field sufficient.

JSON
{"apps": {"tls": {"certificates": {"load_kms": [
  {
    "key": "tpmkms:name=caddy-tls",
    "certificate": "/etc/caddy/tls.crt",
    "kms": {"storage_directory": "/etc/step/tpm"},
    "tags": ["internal"]
  }
]}}}}

Loader or manager?

Two modules are available, and choosing wrong fails quietly, so it is worth a moment.

kms_certificate / tls.certificates.load_kms resolves the certificate once, at config load, and hands it to Caddy's cache. Picking up a certificate rotated outside Caddy needs a caddy reload. This is the right default.

get_certificate kms / tls.get_certificate.kms is consulted during the handshake and re-reads the KMS at most once per ttl, so an externally rotated certificate appears without a reload.

example.com {
    tls {
        get_certificate kms tpmkms:name=caddy-tls {
            certificate /etc/caddy/tls.crt
            ttl         5m
        }
    }
}

They are alternatives for a given hostname, not layers. CertMagic resolves certificates in the order: exact cache match, wildcard cache match, managers, storage, issuers. A name already covered by a loaded certificate therefore never reaches a manager. Configuring both for the same name silently exercises only the loader.

Operational characteristics

These are inherent to keeping the key in hardware, not defects. Read them before deploying.

Every handshake costs one KMS signature. With a TPM that is an open, a key load, a sign, and a close — serialized across the whole process by a mutex, and taking tens to hundreds of milliseconds. This caps handshake throughput, and no module structure can avoid it. Enable TLS session resumption. Measure your device before sizing anything; see the hardware checklist.

caddy validate needs KMS access. Validation happens at config load, so validate and every reload open the KMS and perform one test signature per certificate. A CI machine without a TPM cannot validate a TPM config.

A hostless, non-standard-port site needs TLS enabled explicitly. On something like :8443, kms_certificate alone does not turn on TLS, because it emits no connection policy. Use https:// or add a tls block. This is the same caveat that already applies to tls <cert> <key>.

A catch-all site using get_certificate kms logs a warning saying certificates can only come from the configured external managers. That is the intended state, not a misconfiguration.

Key requirements

ECDSA P-256 is the safe choice.

An RSA key must be able to produce PSS signatures, because TLS 1.3 requires them. A TPM RSA key created with the RSASSA scheme cannot, and would fail every TLS 1.3 handshake. The module signs once at config load to check, and rejects such a key with an error saying so rather than letting it fail at handshake time.

A key identified by path= — a TSS2 PEM file — can be used for signing but cannot supply its own certificate, because LoadCertificateChain requires name=. Pair it with certificate.

Adding a KMS backend

Add one blank import to backends.go. No other code is backend-specific; certificate loading is selected by interface assertion, never by backend name.

Note that pkcs11, yubikey and mackms need cgo. go.step.sm/crypto ships nopkcs11, noyubikey, nomackms, noawskms and noazurekms build tags for trimming a wider backend set back down.

Development

make test       # cgo-free, no hardware
make test-race
make test-tpm   # TPM simulator; needs cgo and libssl-dev
make lint
make build      # xcaddy build, checks the plugin registers

License

Apache-2.0

Documentation

Overview

Package caddykms serves TLS certificates whose private keys live in a key management system, using the KMS abstraction from go.step.sm/crypto.

The private key and certificate are managed outside Caddy. Loading a certificate through this package suppresses Caddy's automatic certificate management for the names it covers, so no ACME issuance is attempted.

Index

Constants

This section is empty.

Variables

This section is empty.

Functions

This section is empty.

Types

type Entry

type Entry struct {
	// Key is a KMS URI identifying the private key. Its scheme selects the
	// backend, for example "tpmkms:name=caddy-tls" or
	// "softkms:/etc/caddy/tls.key". Backend parameters carried by the URI,
	// such as "device=" for the TPM, are honoured.
	Key string `json:"key"`

	// Certificate is the path to a PEM file holding the leaf certificate,
	// optionally followed by intermediates in issuer order. When empty, the
	// chain is loaded from the KMS, which only some backends support.
	Certificate string `json:"certificate,omitempty"`

	// KMS holds backend options that do not belong in the key URI.
	KMS *KMSOptions `json:"kms,omitempty"`

	// Tags are arbitrary values associated with the certificate, so that a
	// connection policy can select it explicitly. Only the certificate loader
	// uses them; the certificate manager ignores them.
	Tags []string `json:"tags,omitempty"`
	// contains filtered or unexported fields
}

Entry describes one certificate together with the KMS-resident private key that belongs to it.

func (*Entry) UnmarshalCaddyfile

func (e *Entry) UnmarshalCaddyfile(d *caddyfile.Dispenser) error

UnmarshalCaddyfile implements caddyfile.Unmarshaler, parsing:

kms_certificate <key-uri> {
    certificate       <path>
    pin               <pin>
    storage_directory <path>
    tags              <tags...>
}

type KMSOptions

type KMSOptions struct {
	// Pin unlocks the KMS, where the backend needs one.
	Pin string `json:"pin,omitempty"`

	// StorageDirectory is where the TPM KMS keeps its serialized objects. It
	// must match the directory used by whichever tool created the key. When
	// unset, the backend picks a default relative to the working directory,
	// which is rarely what a service wants.
	StorageDirectory string `json:"storage_directory,omitempty"`
}

KMSOptions holds KMS backend options that are awkward to express in a key URI. Anything the backend accepts as a URI parameter can go in the URI instead.

type Loader

type Loader []Entry

Loader loads certificates whose private keys live in a KMS.

Each certificate is resolved once, when the config is loaded, and cached by Caddy as an unmanaged certificate. Caddy skips automatic certificate management for any name it already holds a certificate for, so no ACME issuance is attempted and no extra configuration is needed to prevent it. A certificate rotated outside Caddy is picked up on the next reload; use tls.get_certificate.kms instead to pick one up without reloading.

Loader must remain a slice type: the Caddyfile adapter groups certificate loaders using reflection and silently discards any loader whose kind is not a slice.

func (Loader) CaddyModule

func (Loader) CaddyModule() caddy.ModuleInfo

CaddyModule returns the Caddy module information.

func (*Loader) Cleanup

func (l *Loader) Cleanup() error

Cleanup releases every KMS handle the loader holds.

func (Loader) LoadCertificates

func (l Loader) LoadCertificates() ([]caddytls.Certificate, error)

LoadCertificates implements caddytls.CertificateLoader.

func (*Loader) Provision

func (l *Loader) Provision(ctx caddy.Context) error

Provision opens the KMS for every entry and validates its configuration, so that problems abort the config load rather than surfacing per handshake.

func (*Loader) UnmarshalCaddyfile

func (l *Loader) UnmarshalCaddyfile(d *caddyfile.Dispenser) error

UnmarshalCaddyfile implements caddyfile.Unmarshaler, appending one entry.

type Manager

type Manager struct {
	Entry

	// TTL is how long a resolved certificate is served before the KMS is read
	// again. Defaults to 5m. Tags are ignored by the manager.
	TTL caddy.Duration `json:"ttl,omitempty"`
	// contains filtered or unexported fields
}

Manager serves a certificate whose private key lives in a KMS, re-reading it from the KMS at most once per TTL.

Use this instead of tls.certificates.load_kms when a certificate rotated outside Caddy has to be picked up without a reload. Note that certmagic consults managers only after both in-memory cache lookups miss, so a name already covered by a loaded certificate never reaches a manager: the two are alternatives for a given name rather than layers.

certmagic does not cache what a manager returns, so this is called on every handshake for the names it serves. Refreshes are collapsed so that a burst of connections causes one KMS read, and a failed refresh keeps serving the last known good certificate rather than failing handshakes.

func (*Manager) CaddyModule

func (*Manager) CaddyModule() caddy.ModuleInfo

CaddyModule returns the Caddy module information.

func (*Manager) Cleanup

func (m *Manager) Cleanup() error

Cleanup releases the KMS handle.

func (*Manager) GetCertificate

func (m *Manager) GetCertificate(_ context.Context, hello *tls.ClientHelloInfo) (*tls.Certificate, error)

GetCertificate implements certmagic.Manager. It returns (nil, nil) for names it has no certificate for, so that other managers and issuers still get a chance.

func (*Manager) Provision

func (m *Manager) Provision(ctx caddy.Context) error

Provision opens the KMS and validates the configuration, so that problems abort the config load rather than surfacing per handshake.

func (*Manager) UnmarshalCaddyfile

func (m *Manager) UnmarshalCaddyfile(d *caddyfile.Dispenser) error

UnmarshalCaddyfile implements caddyfile.Unmarshaler, parsing:

get_certificate kms <key-uri> {
    certificate       <path>
    pin               <pin>
    storage_directory <path>
    ttl               <duration>
}

type Source

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

Source turns one Entry into a tls.Certificate whose private key stays in the KMS. It is the only type in this package that talks to go.step.sm/crypto.

The lifecycle is newSource, Open, Resolve any number of times, Close.

func (*Source) Close

func (s *Source) Close() error

Close releases the shared KeyManager.

func (*Source) Open

func (s *Source) Open(ctx context.Context) error

Open acquires the shared KeyManager. Every successful Open must be paired with exactly one Close.

func (*Source) Resolve

func (s *Source) Resolve() (*tls.Certificate, error)

Resolve builds a tls.Certificate for the entry. The returned certificate's PrivateKey is a crypto.Signer backed by the KMS; the key material is never extracted.

Directories

Path Synopsis
internal
fakekms
Package fakekms provides an in-memory apiv1.KeyManager for tests, so that the rest of the module can be exercised without a TPM or any other hardware.
Package fakekms provides an in-memory apiv1.KeyManager for tests, so that the rest of the module can be exercised without a TPM or any other hardware.

Jump to

Keyboard shortcuts

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