certflow

package
v0.8.8 Latest Latest
Warning

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

Go to latest
Published: Oct 30, 2026 License: Apache-2.0 Imports: 12 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"
)

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

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

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.

Types

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 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. It lives in memory: a restart forgets it, and the CA's own answer still wins.

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