Documentation
¶
Overview ¶
Package analysis finds the oddities of a PKI that no alarm covers: a certificate nobody serves, two sources issuing for the same names, a renewal loop, a wave of expiries, a weak key, a CA that ends before the certificates it issues (docs/PHASE3.md section 14).
The rules are pure Go functions over a Snapshot, a plain copy of what the server knows at one moment (Load builds it through the existing services). Each rule answers in one of three states: ok, findings, or unknown when its input is missing (D9: a rule never claims more than the snapshot shows). Nothing is stored, nothing is dismissed and nothing leaves the server: the package opens no connection (a test checks the import list), and findings never become alerts, e-mails or webhooks (D8).
Untrusted text. Certificate, agent and CA names come from CSRs, ACME clients and agents. Findings carry them as raw strings, with control and bidi characters removed and clipped to a fixed length, inside fixed text templates; they never hold markup. Escaping is the job of the template and the JSON encoder that render them.
Index ¶
- Constants
- func CleanText(s string) string
- func SafeHref(href string) string
- type Account
- type Agent
- type Agents
- type Analysis
- type CA
- type Certificate
- type Counts
- type Deployment
- type Deps
- type Endpoint
- type Endpoints
- type Engine
- type Finding
- type Generation
- type Missing
- type Result
- func CACliff(s Snapshot) Result
- func DuplicateCertificates(s Snapshot) Result
- func ExpiryWave(s Snapshot) Result
- func OrphanedCertificate(s Snapshot) Result
- func OutdatedAgent(s Snapshot) Result
- func RenewalLoop(s Snapshot) Result
- func Run(s Snapshot) []Result
- func UnusedACMEAccount(s Snapshot) Result
- func UnusedDomain(s Snapshot) Result
- func WeakKey(s Snapshot) Result
- func WeakSignature(s Snapshot) Result
- type Rule
- type Service
- type Snapshot
- type Summary
Constants ¶
const ( SeverityInfo = "info" SeverityWarning = "warning" SeverityCritical = "critical" )
Finding severities (D10). "info" is never a call to act.
const ( StateOK = "ok" StateFindings = "findings" StateUnknown = "unknown" )
Rule states (D9).
const ( RuleOrphanedCertificate = "orphaned_certificate" RuleDuplicateCertificates = "duplicate_certificates" RuleRenewalLoop = "renewal_loop" RuleExpiryWave = "expiry_wave" RuleWeakKey = "weak_key" RuleWeakSignature = "weak_signature" RuleCACliff = "ca_cliff" RuleOutdatedAgent = "outdated_agent" RuleUnusedACMEAccount = "unused_acme_account" RuleUnusedDomain = "unused_domain" )
Rule IDs. They are stable and appear in the API (docs/PHASE3.md section 14.2).
const CacheTTL = time.Minute
CacheTTL is how long a finished analysis is reused: the monitor's period.
const FailTTL = 5 * time.Second
FailTTL is how long a failed run is remembered: for this long every caller gets the same error at once, instead of each one repeating the full read of the tenant against a database that is failing.
const MaxFindingsPerRule = 200
MaxFindingsPerRule bounds the findings a Result keeps; the rest is counted.
const RecentWindow = 7 * 24 * time.Hour
RecentWindow is how far back Snapshot.Recent reaches.
Variables ¶
This section is empty.
Functions ¶
func CleanText ¶
CleanText drops control characters (line breaks, TAB, ESC), format characters (bidi overrides and isolates, zero-width marks) and invalid UTF-8 from text that came from a certificate, a CSR or an agent. A name that reorders the text around it or hides a character must not reach a page, a log or an API consumer. The result is still data: the template escapes it.
func SafeHref ¶
SafeHref returns href if it is a path inside the UI: one leading slash, then no second slash, no backslash and no byte below 0x20 or 0x7f. Browsers strip TAB, CR and LF from a URL, so "/<TAB>/host" would become "//host", and some treat a backslash like a slash. Anything else gives "": the text of a finding stays, its link goes.
Types ¶
type Account ¶
type Account struct {
ID, Status string
AgentLinked bool
CreatedAt time.Time
// LastOrderAt is zero when the account never ordered.
LastOrderAt time.Time
}
Account is an ACME account with its last order.
type Agent ¶
type Agent struct {
ID, Hostname, Version string
// Status is active, disabled or revoked.
Status string
Deployments []Deployment
}
Agent is an enrolled agent with its deployments.
type Agents ¶
type Agents interface {
ListAgents(ctx context.Context, who keys.Who) ([]agents.Agent, error)
GetAgent(ctx context.Context, who keys.Who, id string) (*agents.Agent, error)
}
Agents is what Load reads about agents (*agents.Service).
type Analysis ¶
type Analysis struct {
GeneratedAt time.Time
// Results has one entry per rule, in the order of Rules.
Results []Result
// Findings are all findings of all rules, most severe first. Like
// Results it holds at most MaxFindingsPerRule per rule; Summary counts
// every finding.
Findings []Finding
Summary Summary
}
Analysis is the outcome of one run.
type CA ¶
type CA struct {
ID, Name string
// Issuing: active, with a key that is not offline, so it signs
// end-entity certificates.
Issuing bool
// MaxValidityDays caps the validity of the leaves the CA issues; the
// engine shortens a longer default to it. 0 = not known, no cap assumed.
MaxValidityDays int
Generations []Generation
}
CA is a certificate authority of the tenant.
type Certificate ¶
type Certificate struct {
ID, Serial, Fingerprint string
// Source is the certificates.source column: issued, imported or external.
Source string
CAID string
CAName string
// DNSNames and IPs are the names; CommonName is used when there are none.
DNSNames, IPs []string
CommonName string
// Class and IssuerKey say who issued it (ca.SourceUI, ca.SourceACME,
// ca.SourceAgent; "ui", "acme:<account>", "agent:<id>"). Both are "" for
// an imported or external certificate, and for an issued one whose origin
// could not be read.
Class, IssuerKey string
NotBefore, NotAfter, CreatedAt time.Time
// Traits is nil when the DER could not be parsed.
Traits *ca.Traits
}
Certificate is an end-entity certificate of the tenant.
type Deployment ¶
Deployment is a certificate an agent keeps on its host.
type Deps ¶
type Deps struct {
Tenant string // for the permission check of Service.Get
Engine Engine
Agents Agents // nil = the agent listener is off
Scan Endpoints // nil = no scanning
// ScanOff: scanning is switched off (monitor.scan_interval: off); the
// endpoints that are entered then keep their last result, which no rule
// should lean on.
ScanOff bool
ServerVersion string
Clock clock.Clock
// Who reads the data: a system principal that may read the whole tenant.
// The zero value means authz.System("analysis").
Who keys.Who
// Log gets the debug line with the duration of a run; nil = no logging.
Log *slog.Logger
}
Deps are the inputs of one tenant's analysis.
type Endpoint ¶
type Endpoint struct {
ID, Host string
Port int
// Name is the name the certificate is expected for (SNI, else the host).
Name string
// LastOKAt is the last successful scan; zero = never. Fingerprint is the
// leaf it served then (it stays while later scans fail) and
// FingerprintSince the first successful scan that saw it.
LastOKAt time.Time
Fingerprint string
FingerprintSince time.Time
}
Endpoint is a TLS endpoint the scan visits, with its last scan.
type Endpoints ¶
Endpoints is the list of TLS endpoints (*tlsscan.Service). Only the stored result of the last scan is read; the analysis never connects anywhere.
type Engine ¶
type Engine interface {
Certificates(ctx context.Context, who keys.Who, f ca.CertificateFilter) (*ca.CertificatePage, error)
CAs(ctx context.Context, who keys.Who) ([]ca.CAInfo, error)
CertificateTraits(ctx context.Context, who keys.Who) (map[string]ca.Traits, error)
CertificateOrigins(ctx context.Context, who keys.Who) (map[string]ca.Origin, error)
CertificateNames(ctx context.Context, who keys.Who) ([]ca.CertName, error)
ACMEAccountActivity(ctx context.Context, who keys.Who) (accounts []ca.AccountActivity, truncated bool, err error)
ListDomains(ctx context.Context, who keys.Who) ([]ca.Domain, error)
}
Engine is what Load reads from the CA engine (*ca.Engine).
type Finding ¶
type Finding struct {
// Key is "rule:subject id", stable between runs.
Key string
Rule string
// Severity is SeverityInfo, SeverityWarning or SeverityCritical.
Severity string
// Subject names what it is about (a certificate, an agent, a week).
Subject string
// Seen is what the snapshot shows; Why says why it matters; Recommendation
// says what to do. A finding never states more than Seen.
Seen, Why, Recommendation string
// Href is a path inside the UI ("/certificates/<id>"), or "".
Href string
}
Finding is one thing a rule saw. Every field is text, none is markup.
type Generation ¶
type Generation struct {
Number int64
Current bool
// Signed: the certificate exists (a pending generation has none).
Signed bool
Revoked bool
// Intermediate: the CA certificate has an issuer; a root signs itself.
Intermediate bool
NotAfter time.Time
Signature x509.SignatureAlgorithm
}
Generation is one key and certificate of a CA.
type Missing ¶
type Missing struct {
// Agents: the agent listener is off, so there is no agent data.
Agents string
// Scan: the TLS scan is off, so no endpoint was scanned.
Scan string
}
Missing names the input a rule cannot do without, when Load could not get it. A rule that depends on it answers "unknown" with this reason.
type Result ¶
type Result struct {
ID, Title string
// State is StateOK, StateFindings or StateUnknown.
State string
// Counts are all findings of the rule by severity; Findings holds at most
// MaxFindingsPerRule of them, most severe first.
Counts Counts
Findings []Finding
// Unassessed is how many things the rule could not judge for lack of data;
// UnknownReason says why. With State unknown the reason is the whole
// answer; with State findings it qualifies them ("and N could not be
// assessed").
Unassessed int
UnknownReason string
}
Result is what one rule found.
func CACliff ¶
CACliff flags an issuing CA whose current certificate ends sooner than the default validity of the certificates it issues. The engine shortens a default validity to the end of the CA and refuses a longer explicit one, so every certificate issued from now on ends with the CA. The default is the ACME default: the longest validity_days of the domains of the CA, 90 days without a domain, and never more than the CA's maximum validity. The monitor's ca_expiry alert stays the alarm.
func DuplicateCertificates ¶
DuplicateCertificates flags valid certificates with identical names from two sources (warning) or three or more from one source (info). A certificate counts only while more than a third of its lifetime is left, so the overlap of a renewal does not.
func ExpiryWave ¶
ExpiryWave flags a calendar week (ISO, UTC) in which at least 10 valid certificates and at least 30% of all valid certificates end. A tenant with fewer than 20 valid certificates cannot show a wave: unknown.
func OrphanedCertificate ¶
OrphanedCertificate flags a current certificate that no scanned endpoint and no agent has served for a week although endpoints that cover its names were scanned. It says "not seen", never "unused": the scan reaches only the endpoints somebody entered, and only their last answer is stored.
Evidence needed: an endpoint that covers one of the names, was scanned successfully within 7 days, serves another certificate, and has done so since at least 7 days (FingerprintSince), so no scan of the window can have seen this certificate. A certificate without such an endpoint is not judged; the rule then reports one summary count, never a finding per certificate.
func OutdatedAgent ¶
OutdatedAgent flags an active agent older than the server: a minor version or more behind is a warning, a patch only info. With a server that is a development build, or an agent version that is not strict SemVer, nothing can be compared: unknown.
func RenewalLoop ¶
RenewalLoop flags a name set for which a client got three or more certificates within 24 hours (critical at six) or eight within 7 days. It counts certificates of any status: a loop that revokes its predecessors is still a loop.
func Run ¶
Run evaluates every rule against the snapshot. The results come in the order of Rules; the findings of each are sorted by severity (most severe first), subject and key.
func UnusedACMEAccount ¶
UnusedACMEAccount flags a valid account without agent link that is at least 30 days old and has no order for 90 days or ever. Contacts are not shown.
func UnusedDomain ¶
UnusedDomain flags a domain at least 30 days old for which no certificate of the tenant, whatever its status, has a name the domain covers.
func WeakKey ¶
WeakKey flags valid certificates with an RSA key under 2048 bits. RSA 2048 is not flagged. A certificate whose key size could not be read is not judged.
func WeakSignature ¶
WeakSignature flags a valid end-entity or intermediate CA certificate that is signed with SHA-1, MD5 or MD2. The signature of a root certificate is its own and irrelevant. An algorithm Go does not know is not judged.
type Service ¶
type Service struct {
// contains filtered or unexported fields
}
Service runs the analysis of one tenant and keeps the result for CacheTTL. It is the way in for the API and the pages.
func NewService ¶
NewService returns the analysis service of one tenant.
func (*Service) Get ¶
Get returns the analysis to who. The tenant-wide authz.ReadPKI check comes first and nothing is read for a caller who fails it: findings name the CAs, hosts, accounts and domains of the whole tenant. The cache belongs to the tenant, not to the caller. The Analysis is shared: do not change it.
One run at a time: a caller that finds a run in progress waits for it, but gives up when its own context ends. A failed run is remembered for FailTTL (except when the caller of that run gave up: that says nothing about the data).
type Snapshot ¶
type Snapshot struct {
Now time.Time
// ServerVersion is the running version; "dev" for a development build.
ServerVersion string
// Certificates are the valid end-entity certificates, whatever their
// source (agent identity certificates are left out).
Certificates []Certificate
// Recent are the certificates created within RecentWindow of Now, valid
// or not: a revoked or expired one still counts as something a client got.
Recent []Certificate
// Names are the DNS names and IPs of every certificate of the tenant,
// whatever its status.
Names []ca.CertName
CAs []CA
Domains []ca.Domain
// Accounts are the ACME accounts that were read, newest first. If there
// are more than the engine reads, AccountsLimit is how many it did read and
// the older accounts are not in the list; 0 = every account is.
Accounts []Account
AccountsLimit int
Agents []Agent
Endpoints []Endpoint
// Missing says why a section is not there ("" = it is).
Missing Missing
}
Snapshot is what the server knows at one moment, as plain values. The rules read nothing else, and never the clock: Now is the time of the snapshot.
Every name in it (certificate names, host names, versions) is attacker input and stays a raw string; see the package comment.