Files
james.maesandJames Maes 99351310ec feat(core): redaction policy, Redacted<T>, and no-secrets-in-logs canary (#378)
Add a Redacted<T> newtype in nuncio-core whose Debug, Display, and serde
render a fixed placeholder, so a wrapped credential cannot leak through a
derived Debug or serde_json; the value is reachable only via an explicit
expose_secret(). Migrate the CalDAV/CardDAV account auth tokens to it and
drop their hand-written redacting Debug impls.

Document the logging redaction policy (what must never be logged vs what
is allowed) in docs/LOGGING.md and the redact module doc.

Add a cross-crate no-secrets canary that installs a capturing subscriber,
drives auth reject, account add, mock and JMAP-wiremock sync, mark-read,
send, a filter rule-fire, a webhook dispatch, and CalDAV/CardDAV sync with
sentinel secrets/PII, and asserts none appear in captured telemetry.

Refs #363

Co-authored-by: James Maes <jmaes@dmdbrands.com>
2026-07-31 20:07:03 -05:00

57 lines
2.8 KiB
Markdown

# Logging & redaction policy
Nuncio's telemetry (`tracing` spans and events, plus anything that reaches a log
sink through a `Debug` / `Display` / `serde` rendering) is a plaintext side
channel. This document is the authoritative statement of what may and may not
appear in it. It is enforced mechanically by two things:
- **`nuncio_core::redact::Redacted<T>`** — a newtype whose `Debug`, `Display`,
and `serde` all render the fixed placeholder `<redacted>`, so a secret field
wrapped in it cannot leak even through a *derived* `Debug` on the containing
struct. The inner value is reachable only via an explicit, greppable
`expose_secret()` / `into_inner()` call. The module doc comment restates this
policy next to the type.
- **the no-secrets canary** — `crates/nunciod/tests/no_secrets_canary_test.rs`,
a workspace test that drives a representative slice of the instrumented code
paths with known sentinel secrets and asserts none of them appear in captured
telemetry. It is the regression net: if a future change starts logging a
secret, this test fails.
## MUST NEVER be logged (in any encoding)
- Message bodies, subjects, and snippets.
- Sender / recipient email addresses and any other message correspondents.
- Account credentials: passwords and app-specific passwords/tokens.
- gRPC bearer tokens.
- Keyring secret **values** (the material stored in the OS vault) and any
derived key material (encryption keys, HMAC signing keys).
- vCard / iCal personal data: contact names, phone numbers, addresses; event
summaries, locations, attendees, and descriptions.
## MAY be logged (identifying but non-sensitive)
Operating the daemon is impossible without correlation identifiers, and none of
the following reveals content or grants access:
- Opaque ids: account id, message id, rule id, folder id — and UIDs.
- Counts, sizes, and durations.
- Operation / rule / transport / action **kinds** (the discriminant only, never
its payload).
- Folder names, hostnames, ports, and the per-request `request_id`.
- The keyring **lookup key** (e.g. `nuncio/<account-id>`) — an identifier that
names a vault entry but is not itself the stored secret.
## Guidance for contributors
- Prefer logging an **id or a kind** over the value it points at. If you need to
correlate a log line with a message/event/contact, log its id/UID, never its
content.
- When a struct must hold a credential/token/key, make the field type
`Redacted<T>` rather than relying on a hand-written `Debug`. Reach the value
only at the point of use, via `expose_secret()`.
- Never interpolate a whole config/request struct into a log or error with
`{:?}` unless every sensitive field it holds is `Redacted<T>` (or otherwise
self-redacting).
- If you add an instrumented path that could plausibly touch any of the
"MUST NEVER" values, extend the canary to drive it with a sentinel.