acme

package
v0.0.1-dev.137 Latest Latest
Warning

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

Go to latest
Published: Jul 11, 2026 License: MIT Imports: 19 Imported by: 0

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

View Source
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

func NeedsRenewal(certPEM []byte, now time.Time, renewBefore time.Duration) bool

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.

func (AlwaysLeader) IsLeader

func (AlwaysLeader) IsLeader() bool

IsLeader returns true.

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.

func (*BadgerCertStore) Get

func (s *BadgerCertStore) Get(ctx context.Context, host string) ([]byte, []byte, error)

Get returns (nil, nil, nil) when no cert exists for host. Callers (the orchestrator's check-before-issue gate) rely on the (nil, nil) sentinel rather than an error to keep the happy path clean.

func (*BadgerCertStore) Set

func (s *BadgerCertStore) Set(ctx context.Context, host string, cert, key []byte) error

Set atomically writes cert + key for host. Idempotent — repeated Set calls for the same host overwrite the previous value (the SecretRepo bumps the internal version each time).

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).

func (*HTTP01Issuer) Issue

func (i *HTTP01Issuer) Issue(ctx context.Context, host string) ([]byte, []byte, error)

Issue obtains a certificate for host using the HTTP-01 challenge. Returns PEM-encoded cert chain and PEM PKCS#8 private key.

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.

func (*MemCertStore) Get

func (s *MemCertStore) Get(_ context.Context, host string) ([]byte, []byte, error)

Get returns cert + key for host. Missing host returns (nil, nil, nil).

func (*MemCertStore) Set

func (s *MemCertStore) Set(_ context.Context, host string, cert, key []byte) error

Set stores cert + key for host atomically.

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

type Request struct {
	Namespace string
	Name      string
	Host      string
}

Request describes one (service, host) pair the orchestrator should keep alive — issue, renew, retry on failure.

func (Request) Key

func (r Request) Key() string

Key uniquely identifies a request inside the orchestrator's in-memory map. Includes namespace+name+host so two services in different namespaces can request the same host without collision (the operator's responsibility to make sure DNS only points one way at a time).

func (Request) String

func (r Request) String() string

String satisfies the fmt.Stringer interface for log fields.

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.

Jump to

Keyboard shortcuts

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