101 lines
3.6 KiB
Markdown
101 lines
3.6 KiB
Markdown
# checker-caa
|
|
|
|
CAA posture checker for happyDomain.
|
|
|
|
Validates that certificates observed by `checker-tls` were issued by a
|
|
CA actually authorized by the domain's `CAA` records. This checker
|
|
runs no network probes of its own: it reads the `svcs.CAAPolicy`
|
|
service body (already parsed by happyDomain from the zone's `CAA`
|
|
resource records) and the `tls_probes` observations published by
|
|
`checker-tls`, and cross-references them via the CCADB "CAA
|
|
Identifiers" mapping.
|
|
|
|
## How it works
|
|
|
|
1. The host runs this checker on a `svcs.CAAPolicy` service.
|
|
2. `Collect` unmarshals the service body into a list of
|
|
`(flag, tag, value)` entries. No network.
|
|
3. The `caa_compliance` rule:
|
|
- calls `obs.Get("caa_policy", …)` to load its own payload;
|
|
- calls `obs.GetRelated("tls_probes")` to pick up every TLS probe
|
|
produced on the target;
|
|
- resolves each observed issuer (keyed by `IssuerAKI` with an
|
|
`IssuerDN` fallback) against the embedded CCADB CSV to find the
|
|
CA's published CAA identifier domain(s);
|
|
- compares the observed identifiers against the `issue` /
|
|
`issuewild` allow list (or flags a `DisallowIssue` violation).
|
|
|
|
## Observation payload
|
|
|
|
This checker does not publish endpoints or add a new observation
|
|
schema. Under its own observation key `caa_policy` it returns a
|
|
pass-through view of the zone-side CAA records:
|
|
|
|
```json
|
|
{
|
|
"domain": "example.net",
|
|
"records": [
|
|
{ "flag": 0, "tag": "issue", "value": "letsencrypt.org" },
|
|
{ "flag": 0, "tag": "issuewild", "value": ";" }
|
|
],
|
|
"run_at": "2026-04-22T12:34:56Z"
|
|
}
|
|
```
|
|
|
|
## Rule outcomes
|
|
|
|
- `caa_ok`: every observed issuer is authorized by the zone's CAA
|
|
policy.
|
|
- `caa_no_tls`: no TLS probes related to this target have been
|
|
published yet. Reported as `UNKNOWN` (the same "eventual
|
|
consistency" steady state used by `checker-tls` when it has no
|
|
endpoints yet).
|
|
- `caa_not_authorized`: CCADB mapped an observed issuer to a domain
|
|
the CAA policy does not list. Reported `CRIT`.
|
|
- `caa_issuance_disallowed`: the policy contains `CAA 0 issue ";"`
|
|
(explicitly disallowing issuance) but a TLS cert was still observed.
|
|
Reported `CRIT`.
|
|
- `caa_issuer_unknown`: CCADB has no mapping for the observed issuer
|
|
(AKI + DN). Reported `INFO`; action is to file a CCADB update.
|
|
|
|
## Issuer -> CAA domain mapping (CCADB)
|
|
|
|
The file `checker/AllCAAIdentifiersReport.csv` is an unmodified
|
|
snapshot of the "CAA Identifiers (V2)" report from the Common CA
|
|
Database (https://www.ccadb.org/resources). It is embedded into the
|
|
binary via `//go:embed` but is **not committed to the repository**.
|
|
To fetch or refresh it, run:
|
|
|
|
```bash
|
|
go generate ./checker/
|
|
```
|
|
|
|
This downloads the current CSV from CCADB. No code changes are needed
|
|
to pick up a new snapshot: only a re-embed (recompile) is required
|
|
after the file is refreshed. Note that the download depends on CCADB
|
|
being reachable; `go build` itself has no network dependency.
|
|
|
|
The lookup key is:
|
|
|
|
1. `IssuerAKI` (uppercase hex of `leaf.AuthorityKeyId`), matched
|
|
against CCADB's `"Subject Key Identifier (Hex)"` column.
|
|
2. `IssuerDN` (Go's `leaf.Issuer.String()`), matched against CCADB's
|
|
`"Subject"` column after normalization (RDNs sorted by type,
|
|
whitespace trimmed, comma/semicolon separators collapsed).
|
|
|
|
## Options
|
|
|
|
| Id | Type | Default | Description |
|
|
|-----------|--------|---------|------------------------------------|
|
|
| `domain` | string | (auto) | Domain being checked (`AutoFill`). |
|
|
| `service` | (n/a) | (auto) | `svcs.CAAPolicy` service body. |
|
|
|
|
## Running
|
|
|
|
```bash
|
|
# Plugin (loaded by happyDomain at startup)
|
|
make plugin
|
|
|
|
# Standalone HTTP server
|
|
make && ./checker-caa -listen :8080
|
|
```
|