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
- Variables
- func GenerateKey() (*ecdsa.PrivateKey, error)
- func NewCSR(key *ecdsa.PrivateKey) ([]byte, error)
- func RenewAt(notBefore, notAfter time.Time, rng *rand.Rand) time.Time
- func TLSConfig(cred *tls.Certificate, trustPEM []byte) (*tls.Config, error)
- type EnrollOptions
- type EnrollResult
- type Holder
- type RenewOptions
- type RenewResult
- type Renewer
- type Store
- func (s Store) Enrollable(now time.Time) (expired *x509.Certificate, err error)
- func (s Store) Load() (cred tls.Certificate, bundlePEM []byte, err error)
- func (s Store) LoadKey() (*ecdsa.PrivateKey, error)
- func (s Store) ReplaceCertificate(certPEM, bundlePEM []byte) error
- func (s Store) Save(key *ecdsa.PrivateKey, certPEM, bundlePEM []byte) error
Constants ¶
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" )
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.
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.
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.
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 ¶
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.
var ErrNotEnrolled = errors.New("credential: not enrolled")
ErrNotEnrolled is returned by Load when the state directory holds no credential.
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.
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 ¶
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) Leaf ¶
func (h *Holder) Leaf() *x509.Certificate
Leaf returns the current certificate.
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 ¶
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 ¶
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
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 ¶
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.