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

2.8 KiB

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.