Closes#363 (OBS-7). Codifies the secret-safety discipline the observability program has been maintaining by hand, and adds an enforcement net so it can't silently regress. No proto change.
What this adds
1. Redaction policy (docs + code).
docs/LOGGING.md: the authoritative statement of what must NEVER be logged (message bodies/subjects, sender/recipient addresses, credentials, bearer tokens, keyring secret values, key material, vCard/iCal PII) vs what IS allowed (ids, counts, op/rule/transport kinds, folder names, hostnames, UIDs, request_id, and the keyring lookup key).
The same policy restated in the module doc of nuncio_core::redact.
2. Redacted<T> newtype (nuncio_core::redact).
Debug, Display, and Serialize all render <redacted>; inner value reachable only via explicit expose_secret() / into_inner(). Deserialize is transparent so containing structs still derive it; Serialize is deliberately lossy (guards the serde_json leak path).
Applied to the clearest reachable secret fields: CalDavAccountConfig::auth_token and CardDavAccountConfig::auth_token. Their hand-written redacting Debug impls are removed in favour of a derived Debug that is now leak-proof by construction. (AccountConfig.keyring_secret_key is intentionally left as-is: it is a vault lookup key / identifier, not the stored secret, and it is persisted — see policy doc.)
3. No-secrets canary (crates/nunciod/tests/no_secrets_canary_test.rs).
A single cross-crate workspace test that installs a capturing tracing subscriber, seeds sentinel secrets/PII, then drives — with mock backends and loopback wiremock only, no live network — and asserts NONE of the sentinels appear in captured telemetry:
Closes #363 (OBS-7). Codifies the secret-safety discipline the observability program has been maintaining by hand, and adds an enforcement net so it can't silently regress. No proto change.
## What this adds
**1. Redaction policy (docs + code).**
- `docs/LOGGING.md`: the authoritative statement of what must NEVER be logged (message bodies/subjects, sender/recipient addresses, credentials, bearer tokens, keyring secret values, key material, vCard/iCal PII) vs what IS allowed (ids, counts, op/rule/transport kinds, folder names, hostnames, UIDs, request_id, and the keyring *lookup key*).
- The same policy restated in the module doc of `nuncio_core::redact`.
**2. `Redacted<T>` newtype (`nuncio_core::redact`).**
- `Debug`, `Display`, and `Serialize` all render `<redacted>`; inner value reachable only via explicit `expose_secret()` / `into_inner()`. `Deserialize` is transparent so containing structs still derive it; `Serialize` is deliberately lossy (guards the serde_json leak path).
- Applied to the clearest reachable secret fields: `CalDavAccountConfig::auth_token` and `CardDavAccountConfig::auth_token`. Their hand-written redacting `Debug` impls are removed in favour of a derived `Debug` that is now leak-proof by construction. (`AccountConfig.keyring_secret_key` is intentionally left as-is: it is a vault *lookup key* / identifier, not the stored secret, and it is persisted — see policy doc.)
**3. No-secrets canary (`crates/nunciod/tests/no_secrets_canary_test.rs`).**
A single cross-crate workspace test that installs a capturing `tracing` subscriber, seeds sentinel secrets/PII, then drives — with mock backends and loopback wiremock only, no live network — and asserts NONE of the sentinels appear in captured telemetry:
- gRPC **auth reject** (sentinel bearer token) + authenticated calls;
- **account add** (sentinel password);
- **mock inbound sync** carrying sentinel subject/body/sender/recipient;
- **real JMAP wiremock sync** carrying the same content + sentinel credentials;
- **mark-read** (outbox-affecting) and **outbound send** (sentinel content);
- **filter rule-fire** on the sentinel message;
- **webhook dispatch** signed with a sentinel HMAC key;
- **CalDAV + CardDAV sync** authenticated with a sentinel token, carrying sentinel event/contact PII.
The test also asserts the log-emitting paths actually ran (webhook + rule-fire logged), so it can never pass by capturing nothing.
## Real leak found?
No. The canary exercises the paths and all sentinels stay out of telemetry.
## Gate (all green, 0 ignored)
- `cargo fmt --all -- --check` — clean
- `cargo check-all` (clippy `--all-targets -D warnings`) — clean
- `cargo test-all` — 596 passed, 0 failed, 0 ignored
SSH-signed commit.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Closes #363 (OBS-7). Codifies the secret-safety discipline the observability program has been maintaining by hand, and adds an enforcement net so it can't silently regress. No proto change.
What this adds
1. Redaction policy (docs + code).
docs/LOGGING.md: the authoritative statement of what must NEVER be logged (message bodies/subjects, sender/recipient addresses, credentials, bearer tokens, keyring secret values, key material, vCard/iCal PII) vs what IS allowed (ids, counts, op/rule/transport kinds, folder names, hostnames, UIDs, request_id, and the keyring lookup key).nuncio_core::redact.2.
Redacted<T>newtype (nuncio_core::redact).Debug,Display, andSerializeall render<redacted>; inner value reachable only via explicitexpose_secret()/into_inner().Deserializeis transparent so containing structs still derive it;Serializeis deliberately lossy (guards the serde_json leak path).CalDavAccountConfig::auth_tokenandCardDavAccountConfig::auth_token. Their hand-written redactingDebugimpls are removed in favour of a derivedDebugthat is now leak-proof by construction. (AccountConfig.keyring_secret_keyis intentionally left as-is: it is a vault lookup key / identifier, not the stored secret, and it is persisted — see policy doc.)3. No-secrets canary (
crates/nunciod/tests/no_secrets_canary_test.rs).A single cross-crate workspace test that installs a capturing
tracingsubscriber, seeds sentinel secrets/PII, then drives — with mock backends and loopback wiremock only, no live network — and asserts NONE of the sentinels appear in captured telemetry:The test also asserts the log-emitting paths actually ran (webhook + rule-fire logged), so it can never pass by capturing nothing.
Real leak found?
No. The canary exercises the paths and all sentinels stay out of telemetry.
Gate (all green, 0 ignored)
cargo fmt --all -- --check— cleancargo check-all(clippy--all-targets -D warnings) — cleancargo test-all— 596 passed, 0 failed, 0 ignoredSSH-signed commit.