Help us shape the future of Go! Take the 2026 Go Developer Survey

credential

package
v0.5.1 Latest Latest
Warning

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

Go to latest
Published: Oct 29, 2026 License: Apache-2.0 Imports: 20 Imported by: 0

Documentation

Overview

Package credential is the agent's side of enrollment and renewal: it generates the workload keypair, builds certificate signing requests, calls the control plane, and keeps the resulting credential in a state directory the daemon reads (ADR-0016).

The private key is generated here and never leaves the host. The control plane sees only a CSR, and the CSR contributes only its public key; identity is granted by the control plane.

Index

Constants

View Source
const (
	// KeyFile holds the workload private key (PKCS#8 PEM).
	KeyFile = "workload.key"
	// CertFile holds the current workload certificate (PEM).
	CertFile = "workload.crt"
	// BundleFile holds the authority bundle the control plane is verified with.
	BundleFile = "ca-bundle.crt"
)
View Source
const ExpiredRetryCap = 5 * time.Minute

ExpiredRetryCap bounds the backoff between renewal attempts once the credential has expired, whatever RetryCap says. Renewal is then the daemon's only way back to the control plane (the sync stream and flow reports are refused until it succeeds), so a control plane that returns from an outage hears from each expired agent within this long; it is the sync stream's reconnect cap, a load the control plane already carries.

View Source
const ReenrollRequired = "re-enrollment with a new provisioning token is required"

ReenrollRequired is the remedy for a credential expired beyond the control plane's renewal grace, in the words the control plane's refusal uses, so the agent's log and the refusal it carries agree.

View Source
const RenewFraction = 2.0 / 3.0

RenewFraction is the point in a credential's lifetime by which the daemon begins renewal: the two-thirds mark. Jitter moves the first attempt earlier, never later, so at least a third of the lifetime always remains for retries.

View Source
const RenewJitterFraction = 1.0 / 12.0

RenewJitterFraction is the width of the jitter window before the renewal point, as a fraction of lifetime, so that a fleet enrolled together does not renew together.

Variables

View Source
var ErrHoldsCredential = errors.New("credential: the state directory already holds a valid credential; remove it to enroll again")

ErrHoldsCredential is returned by Enrollable when the state directory holds a credential that is still valid: a live identity, which a new enrollment would orphan.

View Source
var ErrNotEnrolled = errors.New("credential: not enrolled")

ErrNotEnrolled is returned by Load when the state directory holds no credential.

View Source
var ErrReenrolled = errors.New("credential: the state directory holds a different key (the host was enrolled again); restart the daemon to run as the new workload")

ErrReenrolled is returned by the renewer when the state directory no longer holds this daemon's key: an operator enrolled the host again underneath it. The daemon cannot run as two workloads, and writing a renewed certificate beside the new key would pair it with the wrong one, so it stops and the restarted daemon runs as the new workload.

Functions

func GenerateKey

func GenerateKey() (*ecdsa.PrivateKey, error)

GenerateKey creates the workload keypair.

func NewCSR

func NewCSR(key *ecdsa.PrivateKey) ([]byte, error)

NewCSR builds a PEM-encoded PKCS#10 request over key. The request names nothing: no subject, no SANs. The control plane ignores those fields by rule, and an empty request makes that rule visible from the agent side.

func RenewAt

func RenewAt(notBefore, notAfter time.Time, rng *rand.Rand) time.Time

RenewAt returns when a credential valid from notBefore to notAfter should be renewed: two thirds of the way through its lifetime, pulled earlier by a random amount up to RenewJitterFraction of the lifetime.

func TLSConfig

func TLSConfig(cred *tls.Certificate, trustPEM []byte) (*tls.Config, error)

TLSConfig builds the agent's client configuration: the workload credential (nil for enrollment, which has none yet) and the PEM trust anchor for the control plane (empty means the host's trust store).

Types

type EnrollOptions

type EnrollOptions struct {
	// Server is the control-plane address, host:port.
	Server string
	// Token is the provisioning token plaintext.
	Token string
	// BootstrapCA is the PEM trust anchor for the control plane, delivered
	// out of band together with the token. At enrollment the agent holds
	// nothing that could verify the control plane: no credential and no
	// bundle. The token authenticates the agent to the server; this anchor
	// authenticates the server to the agent. Without it the token would be
	// sent to whatever answers at Server. Empty means the host's system
	// trust store, which is right when the control plane presents a
	// certificate the host already trusts.
	BootstrapCA []byte
	// Facts are descriptive host facts sent alongside; they never influence
	// identity or authorization.
	Facts *innerwallv1.HostFacts
}

EnrollOptions configure one enrollment call.

type EnrollResult

type EnrollResult struct {
	WorkloadID     identity.WorkloadID
	CertificatePEM []byte
	BundlePEM      []byte
	Labels         []*innerwallv1.Label
}

EnrollResult is what the control plane granted.

func Enroll

func Enroll(ctx context.Context, key *ecdsa.PrivateKey, opts EnrollOptions) (*EnrollResult, error)

Enroll generates nothing and persists nothing: it exchanges the token and a CSR over key for a credential and returns it for the caller to store.

type Holder

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

Holder is the credential a running daemon presents, swappable in place. Connections opened while it holds one certificate keep using it; the client TLS configuration it builds asks the holder for the certificate at each handshake, so a renewal takes effect on the next connection without dropping the current stream.

func LoadHolder

func LoadHolder(store Store) (*Holder, error)

LoadHolder reads the stored credential into a holder.

func (*Holder) Current

func (h *Holder) Current() (tls.Certificate, []byte)

Current returns the current credential and trust bundle.

func (*Holder) Key

func (h *Holder) Key() *ecdsa.PrivateKey

Key returns the workload private key.

func (*Holder) Leaf

func (h *Holder) Leaf() *x509.Certificate

Leaf returns the current certificate.

func (*Holder) Replace

func (h *Holder) Replace(certPEM, bundlePEM []byte) error

Replace installs a renewed certificate, on disk first and then in memory, so a crash between the two leaves the newer one on disk. It returns ErrReenrolled, and writes nothing, when the state directory no longer holds this holder's key.

func (*Holder) TLSConfig

func (h *Holder) TLSConfig() (*tls.Config, error)

TLSConfig builds a client configuration that presents whatever certificate the holder has at handshake time and trusts the bundle it has now.

type RenewOptions

type RenewOptions struct {
	Server string
	// Credential is the current workload credential, presented over mTLS.
	Credential tls.Certificate
	// Trust is the authority bundle stored at enrollment.
	Trust []byte
}

RenewOptions configure one renewal call.

type RenewResult

type RenewResult struct {
	WorkloadID     identity.WorkloadID
	CertificatePEM []byte
	BundlePEM      []byte
}

RenewResult is a renewed credential.

func Renew

func Renew(ctx context.Context, key *ecdsa.PrivateKey, opts RenewOptions) (*RenewResult, error)

Renew asks for a fresh certificate over the current key, authenticated by the current credential. The control plane binds the same identity; the caller checks nothing about which workload because it cannot choose.

type Renewer

type Renewer struct {
	Holder *Holder
	// Server is the control-plane address for RenewCredential.
	Server string
	Log    *slog.Logger
	// RetryBase and RetryCap bound the backoff after a failed renewal;
	// one minute and one hour when zero. ExpiredRetryCap bounds it
	// further once the credential has expired.
	RetryBase time.Duration
	RetryCap  time.Duration
	// Now is the wall clock; time.Now if nil.
	Now func() time.Time
	// Elapsed is a monotonic clock: the time since an arbitrary fixed
	// origin, unaffected by changes to the wall clock. Measured from the
	// renewer's start when nil. Tests substitute it.
	Elapsed func() time.Duration
	// After is the timer; time.After if nil. Tests substitute it to run
	// the schedule without waiting.
	After func(d time.Duration) <-chan time.Time
	// Renew performs one renewal; the package's Renew when nil. Tests
	// substitute it.
	Renew func(ctx context.Context, key *ecdsa.PrivateKey, opts RenewOptions) (*RenewResult, error)
	// Renewed, when set, is called after each successful renewal.
	Renewed func(leaf *x509.Certificate)
	// contains filtered or unexported fields
}

Renewer renews the holder's credential on a timer.

Expiry is never fatal. Renewal begins by the two-thirds mark of a credential's lifetime, leaving at least the last third for retries; if every attempt fails until it expires (the control plane was unreachable through the whole window), the renewer keeps retrying with backoff, because the control plane accepts an expired credential for renewal, and nothing else, within its renewal grace (ADR-0016 as amended). Enforcement meanwhile continues from the persisted policy, and everything else resumes on the fresh credential. Beyond the grace the refusals say re-enrollment is required; the renewer keeps trying until the operator intervenes, and the reason rides every heartbeat that gets through.

The renewal point is taken from two clocks, whichever says renew first: the wall clock against the certificate's validity, and the monotonic time elapsed since the credential was obtained. A host whose wall clock runs behind would otherwise sleep through its renewal window.

func (*Renewer) LastError

func (r *Renewer) LastError() string

LastError returns the reason the most recent renewal attempt failed, or empty when the last attempt succeeded or none has been due. Once the credential has expired the reason says so, with the expiry. The daemon carries it on every heartbeat so the control plane sees a workload heading for expiry before it gets there.

func (*Renewer) Run

func (r *Renewer) Run(ctx context.Context) error

Run renews until ctx ends, and returns nil then. It returns early only with ErrReenrolled, when the host was enrolled again underneath it. Failed attempts retry with jittered exponential backoff and are logged at error level every time, because each one brings the workload closer to expiry, or further past it.

type Store

type Store struct {
	Dir string
}

Store is the on-disk credential state of one agent.

func (Store) Enrollable added in v0.2.0

func (s Store) Enrollable(now time.Time) (expired *x509.Certificate, err error)

Enrollable reports whether a new enrollment may be written to the state directory at instant now. An empty directory may take one, and so may one whose stored credential has expired: the refusal exists to protect a live identity, and an expired credential no longer is one (ADR-0016 as amended). The expired leaf is returned so the caller can say what it replaces. A valid credential, or a key whose credential cannot be read and so cannot be shown to have expired, is refused.

func (Store) Load

func (s Store) Load() (cred tls.Certificate, bundlePEM []byte, err error)

Load reads the stored credential and bundle.

func (Store) LoadKey

func (s Store) LoadKey() (*ecdsa.PrivateKey, error)

LoadKey reads only the private key.

func (Store) ReplaceCertificate

func (s Store) ReplaceCertificate(certPEM, bundlePEM []byte) error

ReplaceCertificate swaps in a renewed certificate (and refreshed bundle) atomically: a reader sees either the old file or the new one, never a partial write. The key is untouched.

func (Store) Save

func (s Store) Save(key *ecdsa.PrivateKey, certPEM, bundlePEM []byte) error

Save writes a freshly enrolled credential: key, certificate, and bundle. The directory is created with mode 0700 and every file with mode 0600.

Jump to

Keyboard shortcuts

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