certflow

package
v0.9.2 Latest Latest
Warning

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

Go to latest
Published: Nov 6, 2026 License: Apache-2.0 Imports: 13 Imported by: 0

Documentation

Overview

Package certflow decides whether a domain is ready for a public certificate, tells operators the truth about the certificate it has, and retries stuck renewals with backoff. It holds no ACME client: issuance and renewal stay with the embedded ingress.

Index

Constants

View Source
const (
	EnvFailedPerHour    = "APP_ACME_FAILED_PER_HOUR"
	EnvDuplicatePerWeek = "APP_ACME_DUPLICATE_PER_WEEK"

	// LedgerKeep is how long stored attempts matter.
	LedgerKeep = ledgerKeep
)

Env names for the Let's Encrypt limits the ledger reasons about.

View Source
const (
	KindFailed  = "failed"
	KindIssued  = "issued"
	KindStarted = "started"
)

Entry kinds written to a Backing.

View Source
const (
	ReasonFailedValidations = "failed_validations"
	ReasonDuplicates        = "duplicate_certificates"
	ReasonCARateLimited     = "ca_rate_limited"
)

Rate limit reasons.

View Source
const (
	CodeIssuable  = "issuable"
	CodeUpstream  = "upstream_proxy"
	CodeDNS       = "dns"
	CodePorts     = "ports"
	CodeCAA       = "caa"
	CodeEmail     = "email"
	CodeRateLimit = "rate_limit"
	CodeStaging   = "staging"
)

Preflight item codes.

View Source
const (
	EnvWatchdogInterval = "APP_CERT_WATCHDOG_INTERVAL"
	EnvWatchdogBackoff  = "APP_CERT_WATCHDOG_BACKOFF"
)

Env names for the renewal watchdog.

View Source
const BudgetWindow = duplicateWindow

BudgetWindow is the span the CA's duplicate and new-order limits are counted over.

View Source
const CodeReplaces = "replaces_certificate"

CodeReplaces is the preflight item and API error code for a staging request that would replace a working certificate.

View Source
const (
	EnvFailDays = "APP_CERT_FAIL_DAYS"
)

Env names and defaults for the thresholds this package reads.

View Source
const EnvProofTTL = "APP_REACH_PROOF_TTL"

EnvProofTTL is how long a reachability answer is reused before the CA is asked again.

Variables

This section is empty.

Functions

func DaysLeft

func DaysLeft(notAfter, now time.Time) int

DaysLeft is whole days until notAfter, negative once expired.

func FailDays

func FailDays() int

FailDays is how close to expiry an automatically renewed certificate turns the doctor red, from APP_CERT_FAIL_DAYS (default 14).

func IsRateLimitError

func IsRateLimitError(msg string) bool

IsRateLimitError reports whether a CA error text is a rate limit answer.

func ProofTTLFromEnv

func ProofTTLFromEnv() time.Duration

ProofTTLFromEnv reads the cache lifetime of an answer.

Types

type Backing

type Backing interface {
	Save(kind, domain string, at time.Time)
}

Backing persists ledger entries. Save is best effort: a failed write must not block issuance.

type Badge

type Badge struct {
	Kind        Kind     `json:"kind"`
	DaysLeft    int      `json:"days_left"`
	LastRenewal *Renewal `json:"last_renewal,omitempty"`
	// NextRetryAt is when the watchdog tries again, set only while a renewal
	// is stuck or failing.
	NextRetryAt *time.Time `json:"next_retry_at,omitempty"`
}

Badge is the per-domain certificate truth shown on lists and the overview.

type Candidate

type Candidate struct {
	Domain string
	Reason string
	// Expired certificates are cleared before re-issuing; valid ones are left in place.
	Expired bool
	// Blocked is set while a CA rate limit makes a try pointless.
	Blocked bool
}

Candidate is a certificate whose renewal failed or is stuck.

type Entry

type Entry struct {
	Kind, Domain string
	At           time.Time
}

Entry is one stored attempt.

type Existing

type Existing struct {
	Domain  string
	Kind    Kind
	Expired bool
	Stalled bool
}

Existing is a stored certificate as the staging guard sees it.

type Input

type Input struct {
	Domain          string
	Email           string
	DefaultEmail    string
	Staging         bool
	TLSUpstream     bool
	Report          domaindoctor.Report
	Existing        []Existing
	History         Snapshot
	Limits          Limits
	LastCAError     string
	LastCAErrorTime time.Time
	Now             time.Time
}

Input is everything Evaluate decides from. The caller fills Report from the domain doctor with ACME forced on, so CAA and port checks really run.

type Item

type Item struct {
	Code   string    `json:"code"`
	State  ItemState `json:"state"`
	Detail string    `json:"detail"`
	Fix    string    `json:"fix,omitempty"`
}

Item is one preflight finding with the next step in plain words.

type ItemState

type ItemState string

ItemState is the outcome of one preflight item.

const (
	StatePass ItemState = "pass"
	StateWarn ItemState = "warn"
	StateFail ItemState = "fail"
	StateSkip ItemState = "skipped"
)

ItemState values. Only fail blocks the primary action.

type Kind

type Kind string

Kind says who signed the certificate a domain serves.

const (
	KindNone        Kind = "none"
	KindInternal    Kind = "internal"
	KindStaging     Kind = "staging"
	KindLetsEncrypt Kind = "letsencrypt"
	KindCustom      Kind = "custom"
	KindProxy       Kind = "proxy"
	KindOther       Kind = "other"
)

Kind values, from least to most trusted by browsers.

func Classify

func Classify(issuer, source, owner string) Kind

Classify names the signer from the leaf's issuer common name, the row's source and who owns TLS for the host.

func (Kind) AutoRenewed

func (k Kind) AutoRenewed() bool

AutoRenewed reports whether the platform renews this kind itself.

func (Kind) Trusted

func (k Kind) Trusted() bool

Trusted reports whether browsers accept certificates of this kind.

type Ledger

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

Ledger remembers recent production attempts per hostname so the next safe time can be shown before another try burns a limit. Attach a Backing to keep it across restarts; without one it lives in memory and the CA's answer wins.

func (*Ledger) IssuedSince

func (l *Ledger) IssuedSince(now time.Time, window time.Duration) int

IssuedSince counts production certificates issued in the last window across every hostname, which is what a shared weekly budget is spent on.

func (*Ledger) MarkStarted

func (l *Ledger) MarkStarted(domain string, at time.Time)

MarkStarted notes when a guided issuance began for domain.

func (*Ledger) RecordFailure

func (l *Ledger) RecordFailure(domain string, at, now time.Time)

RecordFailure notes a failed production validation; repeats of one timestamp are ignored.

func (*Ledger) RecordIssued

func (l *Ledger) RecordIssued(domain string, at, now time.Time)

RecordIssued notes a production certificate issued for domain.

func (*Ledger) Restore

func (l *Ledger) Restore(b Backing, entries []Entry, now time.Time)

Restore replays stored entries into the ledger without writing them back, then sends later changes to b.

func (*Ledger) Snapshot

func (l *Ledger) Snapshot(domain string) Snapshot

Snapshot returns a copy of domain's recent history.

func (*Ledger) StartedAt

func (l *Ledger) StartedAt(domain string) time.Time

StartedAt is when the last guided issuance for domain began, or the zero time.

type Limits

type Limits struct {
	FailedPerHour    int
	DuplicatePerWeek int
}

Limits are the CA limits that matter for one hostname.

func LimitsFromEnv

func LimitsFromEnv() Limits

LimitsFromEnv reads the limits, defaulting to Let's Encrypt's published ones.

type Orderer

type Orderer interface {
	ProveHTTP01(ctx context.Context, directory, domain string) error
}

Orderer places one staging order for domain and answers the HTTP-01 challenge on port 80. A nil error means the CA validated it. A *ValidationError means the CA ran the check and it failed; any other error means the CA could not be asked.

type Outcome

type Outcome string

Outcome is what the staging order proved about port 80 from the outside.

const (
	OutcomeOpen    Outcome = "open"
	OutcomeClosed  Outcome = "closed"
	OutcomeUnknown Outcome = "unknown"
	OutcomeLimited Outcome = "limited"
)

Outcome values. Limited means no order was placed, so nothing was proven.

type Proof

type Proof struct {
	Domain     string     `json:"domain"`
	Outcome    Outcome    `json:"outcome"`
	Detail     string     `json:"detail"`
	CAReason   string     `json:"ca_reason,omitempty"`
	NextSafeAt *time.Time `json:"next_safe_at,omitempty"`
	At         time.Time  `json:"at"`
}

Proof is the verdict shown on the ports step.

type Prover

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

Prover asks a staging CA to reach this server, within the ledger and attempt limits.

func NewProver

func NewProver(o Orderer, directory string, led *Ledger, lim Limits, ttl time.Duration) (*Prover, error)

NewProver refuses any directory that is not a staging one, so a proof can never spend production certificates.

func (*Prover) Prove

func (p *Prover) Prove(ctx context.Context, domain string, now time.Time, perHour int, lastCAErr string, lastCAErrAt time.Time) Proof

Prove returns a recent answer when there is one, else places a staging order. perHour is the domains.https_attempts_per_hour guard.

type RateLimit

type RateLimit struct {
	Blocked bool
	// NextSafeAt is the earliest time another production try is safe.
	NextSafeAt time.Time
	// Reason is "failed_validations", "duplicate_certificates" or "ca_rate_limited".
	Reason string
}

RateLimit is the verdict on trying again now.

type Renewal

type Renewal struct {
	At     time.Time     `json:"at"`
	Result RenewalResult `json:"result"`
	Error  string        `json:"error,omitempty"`
}

Renewal is the last issue or renewal attempt as an operator reads it.

func BuildRenewal

func BuildRenewal(kind Kind, notBefore time.Time, failAt time.Time, failErr string, failed bool) *Renewal

BuildRenewal reports the last attempt: the CA's recorded failure when there is one, else the leaf's own issue time for certificates a CA signed.

type RenewalResult

type RenewalResult string

RenewalResult is the outcome of the last automatic issue or renewal.

const (
	RenewalOK     RenewalResult = "ok"
	RenewalFailed RenewalResult = "failed"
)

RenewalResult values.

type ReplaceError

type ReplaceError struct {
	Domain string
	Kind   Kind
}

ReplaceError says a staging request would swap a working certificate for an untrusted one.

func StagingConflict

func StagingConflict(domain string, existing []Existing) *ReplaceError

StagingConflict returns the first working certificate a staging request would put at risk, or nil. Switching the CA drops every stored ACME certificate server-wide, so a working Let's Encrypt or public CA certificate on any domain counts; an uploaded or proxy certificate counts only for the requested domain.

func (*ReplaceError) Error

func (e *ReplaceError) Error() string

Error is the plain-language message shown by the API, CLI and dashboard.

type Result

type Result struct {
	Domain  string `json:"domain"`
	Staging bool   `json:"staging"`
	// Email is the contact that will be used: the typed one or the inferred default.
	Email string `json:"email"`
	// Ready is true when no item failed.
	Ready bool   `json:"ready"`
	Items []Item `json:"items"`
	// NextSafeAt is set when a rate limit is in the way.
	NextSafeAt *time.Time `json:"next_safe_at,omitempty"`
}

Result is the whole preflight for one domain.

func Evaluate

func Evaluate(in Input) Result

Evaluate turns the inputs into a verdict. It performs no network calls.

type RetryState

type RetryState struct {
	Attempts int
	NextAt   time.Time
	LastErr  string
}

RetryState is one stuck domain's retry bookkeeping.

type Snapshot

type Snapshot struct {
	Failed []time.Time
	Issued []time.Time
}

Snapshot is the recent history of one hostname.

func (Snapshot) Check

func (s Snapshot) Check(lim Limits, now time.Time, lastCAErr string, lastCAErrAt time.Time) RateLimit

Check decides whether a production attempt is safe at now. lastCAErr is the CA's latest error text and lastCAErrAt its time, so a "rate limited" answer pauses even when this process never counted the attempts.

type ValidationError

type ValidationError struct{ Detail string }

ValidationError means the CA was reached and its HTTP-01 check failed.

func (*ValidationError) Error

func (e *ValidationError) Error() string

type Watchdog

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

Watchdog retries stuck renewals with exponential backoff per domain.

func NewWatchdog

func NewWatchdog(deps WatchdogDeps) *Watchdog

NewWatchdog builds a watchdog, reading its timings from the environment.

func (*Watchdog) Run

func (w *Watchdog) Run(ctx context.Context)

Run ticks until ctx ends.

func (*Watchdog) State

func (w *Watchdog) State(domain string) (RetryState, bool)

State returns the retry bookkeeping for domain, if it is being retried.

func (*Watchdog) Tick

func (w *Watchdog) Tick(ctx context.Context)

Tick runs one pass: it retries every due candidate and forgets domains that recovered.

type WatchdogDeps

type WatchdogDeps struct {
	Scan  func(ctx context.Context) ([]Candidate, error)
	Renew func(ctx context.Context, c Candidate) error
	Now   func() time.Time
	Log   *slog.Logger
}

WatchdogDeps are the seams the watchdog drives; it has no ACME code.

Jump to

Keyboard shortcuts

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