Documentation
¶
Overview ¶
Package acme implements the asynchronous certificate issuance orchestrator for the ingress controller (RUNE-066).
The orchestrator is intentionally split from any concrete ACME client implementation so the state machine is fully unit-testable against a fake Issuer, and so that a Pebble-backed integration test can exercise the production HTTP-01 path without touching state-machine code.
Lifecycle of a single (service, host) request:
Pending --(Issuer.Issue ok)--> Issued | | | +-- (renewal cron, 30 days | before expiry) --> Pending v Failed --(NextRetry elapsed)-----> Pending
Default-deny does not apply here; missing IngressCertStatus means no ACME work is requested. The orchestrator is single-writer per (namespace, name, host) tuple; concurrent requests are coalesced.
**Multi-node note:** in v1 the orchestrator runs unconditionally because single-node = trivially the only node. Multi-node leader election is RUNE-066b; the LeaderProvider hook below makes the transition mechanical.
Package acme: helpers that touch crypto/x509 + encoding/pem. Kept in a separate file so acme.go stays focused on the state machine.
Index ¶
- Constants
- func NeedsRenewal(certPEM []byte, now time.Time, renewBefore time.Duration) bool
- type AlwaysLeader
- type BadgerCertStore
- type CertStore
- type ChallengeStore
- type Config
- type HTTP01Issuer
- type Issuer
- type LeaderProvider
- type MemCertStore
- type Orchestrator
- type Request
- type RetryPolicy
- type StatusSink
Constants ¶
const CertNamespace = "system"
CertNamespace is the namespace used by BadgerCertStore. Built-in (seeded by SeedBuiltinNamespaces); operators see one Secret per host here when they `rune get secrets -n system`.
Variables ¶
This section is empty.
Functions ¶
func NeedsRenewal ¶
NeedsRenewal reports whether certPEM should be renewed now, applied as (notAfter <= now + renewBefore). A parse failure is treated as "yes, renew" so a malformed persisted cert doesn't pin the orchestrator forever.
Types ¶
type AlwaysLeader ¶
type AlwaysLeader struct{}
AlwaysLeader is a LeaderProvider that always returns true. Used in single-node and in tests.
type BadgerCertStore ¶
type BadgerCertStore struct {
// contains filtered or unexported fields
}
BadgerCertStore persists ACME-issued certs through the existing SecretRepo (BadgerDB + the runed KEK encryption pipeline). One Secret per host under the `system` namespace.
Wired in cmd/runed in place of MemCertStore so that runed restarts reuse the cert that's already on disk instead of re-issuing against the ACME provider — the production outage the Propeller team hit when our in-memory MemCertStore meant every restart counted as a fresh LE issuance against the per-identifier-set 168h rate limit.
func NewBadgerCertStore ¶
func NewBadgerCertStore(st store.Store, opts ...repos.SecretOption) *BadgerCertStore
NewBadgerCertStore returns a CertStore backed by the given store. The caller is responsible for ensuring the SecretRepo's KEK is already configured (same KEK as the rest of the runed store).
SecretRepo's defaults reject zero-length key names, which would otherwise drop our tls.crt / tls.key writes on the floor. We inject our own limits at the cert-store layer so the operator's runefile [secret.limits] section can't accidentally shrink the envelope below what an ACME cert + key need to be persisted.
func (*BadgerCertStore) Delete ¶
func (s *BadgerCertStore) Delete(ctx context.Context, host string) error
Delete removes the persisted cert for host. Idempotent on a missing host.
type CertStore ¶
type CertStore interface {
// Set persists cert + key for host atomically.
Set(ctx context.Context, host string, cert, key []byte) error
// Get returns the most recent (cert, key) for host, or
// (nil, nil, nil) if no cert exists yet.
Get(ctx context.Context, host string) (cert, key []byte, err error)
// Delete removes any cert for host.
Delete(ctx context.Context, host string) error
}
CertStore is where issued certificates land. The ingress listener reads here on every TLS handshake (or on hot-reload signal).
Set MUST be atomic: a partial cert + missing key is a TLS handshake failure that the orchestrator should never produce.
type ChallengeStore ¶
type ChallengeStore interface {
// Put stores the keyAuth value for the given token. The token
// path served by the ingress listener is
// /.well-known/acme-challenge/<token>.
Put(token, keyAuth string)
// Delete removes the token after the challenge completes.
Delete(token string)
}
ChallengeStore is the seam through which the orchestrator publishes HTTP-01 challenge tokens. Edge ingress listeners read from the same store. The Issuer typically wraps this with the keyAuth value for the token; the listener just serves whatever is stored.
type Config ¶
type Config struct {
Issuer Issuer
Certs CertStore
Status StatusSink
Leader LeaderProvider
Logger log.Logger
Retry RetryPolicy
// RenewBefore is how long before ExpiresAt to renew. Default 30 days.
RenewBefore time.Duration
// Now overrides the clock for tests.
Now func() time.Time
}
Config bundles the orchestrator's dependencies.
type HTTP01Issuer ¶
type HTTP01Issuer struct {
// Directory is the ACME directory URL. Defaults to Let's Encrypt
// production. Use xacme.LetsEncryptURL or a Pebble URL in tests.
Directory string
// Email is the ACME account contact email. Optional but
// recommended for renewal notifications.
Email string
// Challenges is where the issuer stores HTTP-01 keyAuth values.
// The ingress listener reads from the same store.
Challenges ChallengeStore
// AcceptTOS is called on registration; default accepts.
AcceptTOS func(tosURL string) bool
// contains filtered or unexported fields
}
HTTP01Issuer is a production Issuer that talks to an ACME v2 server (Let's Encrypt by default) and completes the HTTP-01 challenge by publishing the keyAuth into the configured ChallengeStore. The edge ingress listener serves the token.
The issuer registers an account on first use and caches the account key in memory; persistent account-key storage is a follow-up (RUNE-066b).
type Issuer ¶
type Issuer interface {
// Issue obtains a certificate for host. It returns the encoded
// PEM cert chain and PEM private key on success.
Issue(ctx context.Context, host string) (cert []byte, key []byte, err error)
}
Issuer is the abstract certificate issuer. Production binds this to an HTTP-01 ACME client; tests bind it to a fake.
Issue MUST be cancellable via ctx and SHOULD return promptly when it is. Implementations are responsible for performing the challenge dance (publishing the token via ChallengeStore where appropriate).
type LeaderProvider ¶
type LeaderProvider interface {
IsLeader() bool
}
LeaderProvider gates whether this orchestrator instance should drive issuance. Single-node returns true unconditionally; the multi-node wrapper (RUNE-066b) plugs the Raft leader check here.
type MemCertStore ¶
type MemCertStore struct {
// contains filtered or unexported fields
}
MemCertStore is an in-memory CertStore. Suitable for tests and as a thin wrapper while the SecretRepo-backed store is being built.
func NewMemCertStore ¶
func NewMemCertStore() *MemCertStore
NewMemCertStore returns an empty MemCertStore.
func (*MemCertStore) Delete ¶
func (s *MemCertStore) Delete(_ context.Context, host string) error
Delete removes the cert for host.
type Orchestrator ¶
type Orchestrator struct {
// contains filtered or unexported fields
}
Orchestrator drives issuance, renewal, and retry for a set of Requests. Concurrency-safe.
func New ¶
func New(cfg Config) *Orchestrator
New constructs an Orchestrator. Issuer, Certs, and Status are required; New panics if any is nil — these are programming errors, not runtime conditions.
func (*Orchestrator) Forget ¶
func (o *Orchestrator) Forget(req Request)
Forget removes a request. Existing cert in the CertStore is left alone (deletion is a separate concern owned by the controller that drives orchestrator lifecycle).
func (*Orchestrator) Run ¶
func (o *Orchestrator) Run(ctx context.Context) error
Run drives the state machine until ctx is done. Safe to call only once; the orchestrator is not designed to restart in the same process.
func (*Orchestrator) Status ¶
func (o *Orchestrator) Status(req Request) (types.IngressCertStatus, bool)
Status returns a snapshot of the current status for req, or (zero, false) if not tracked.
func (*Orchestrator) Submit ¶
func (o *Orchestrator) Submit(req Request)
Submit registers (or refreshes) a request. Idempotent — calling Submit with the same key on an already-issued cert is a no-op until renewal time.
On first registration of a host, Submit consults the CertStore for an already-persisted cert and, when one exists and is still well away from expiry, transitions the request directly to Issued (next action = renewal point). This is the load-bearing fix for the production outage where a runed restart re-issued every cert from scratch and tripped the Let's Encrypt per-identifier-set rate limit. See RUNE-BUG-ACME-REISSUE-ON-EVERY-RESTART.
type Request ¶
Request describes one (service, host) pair the orchestrator should keep alive — issue, renew, retry on failure.
type RetryPolicy ¶
type RetryPolicy struct {
// Initial is the first retry delay after a failure. Default 30s.
Initial time.Duration
// Max caps the retry delay. Default 1h per design doc.
Max time.Duration
// Multiplier between successive retries. Default 2.0.
Multiplier float64
}
RetryPolicy controls exponential backoff after issuance failure.
type StatusSink ¶
type StatusSink interface {
UpdateIngressCert(ctx context.Context, namespace, name string, status types.IngressCertStatus) error
}
StatusSink is how the orchestrator surfaces IngressCertStatus back to the operator. The orchestrator does not own the Service object; the sink writes to whatever store the control plane uses.