Skip to content

docs(connectors): fix the inverted serde_secret redaction claim - #3803

Open
mlevkov wants to merge 2 commits into
apache:masterfrom
mlevkov:serde-secret-guidance
Open

docs(connectors): fix the inverted serde_secret redaction claim#3803
mlevkov wants to merge 2 commits into
apache:masterfrom
mlevkov:serde-secret-guidance

Conversation

@mlevkov

@mlevkov mlevkov commented Aug 2, 2026

Copy link
Copy Markdown
Contributor

Closes #3801.

What was wrong

.claude/skills/connectors-overview/SKILL.md told plugin authors that annotating
a SecretString field with iggy_common::serde_secret::serialize_secret made
Debug and serialization redact. Only the Debug half is true. The helper
calls expose_secret() and writes the plaintext:

pub fn serialize_secret<S: serde::Serializer>(
    secret: &SecretString,
    serializer: S,
) -> Result<S::Ok, S::Error> {
    serializer.serialize_str(secret.expose_secret())
}

serde_secret.rs's own module doc already said the opposite of the skill
("Do not add serialize_with to fields that should remain redacted"), so the
two documents contradicted each other and the skill is the one plugin authors
read.

The inversion matters more than a typo would, because SecretString has no
Serialize impl by design — a struct holding one cannot derive Serialize at
all. Adding the attribute is what unblocks the derive. The guidance therefore
recommended the exact step that converts a compile-time guarantee into plaintext
output, while describing it as protection.

Nine plugins carry the annotation on credential fields. It is inert today (the
runtime keeps plugin config as serde_json::Value and never deserializes into a
plugin's config struct, so nothing calls these serializers), so this is a
correctness-of-documentation fix rather than a live leak — but the protection
those authors believe they have does not exist.

What this changes

docs(connectors) — the Secrets section now says which half of the claim was
true, and gives the default: do not derive Serialize on a plugin config
struct at all
, since nothing needs it and leaving it off makes the property
compiler-enforced rather than convention-enforced.
iggy_connector_http_source (#3798) does exactly that.

It also notes that none of this protects the credential from the runtime's own
control API, which returns plugin configuration verbatim — that is #3802, and it
is not addressable from the plugin side.

While in there: the "In-tree uses" list named delta_sink, which does not use
these helpers, and omitted s3_sink and surrealdb_sink, which do.

feat(common) — adds serialize_redacted and serialize_optional_redacted
so the corrected guidance has something to point at. Suggestion 3 in the issue.
Without them, an author who genuinely needs Serialize has no redacting option
and reaches for the exposing one, which is the trap. The optional form keeps
Some distinguishable from None: whether a credential is configured is not
itself secret, and collapsing it to null would report a configured field as
unset. Documented as not round-tripping, so nobody feeds redacted output back
into a config loader.

Deliberately not in scope

Suggestion 2 in the issue — dropping Serialize from the nine plugin config
structs that do not need it. It is the right follow-up and it is safe, but it
touches nine crates and each needs checking for a real serializing caller, so it
does not belong in the same review as the documentation fix. Happy to do it as a
separate PR; say the word and I will.

Verification

cargo fmt --all --check, cargo sort --check --no-format --workspace,
cargo clippy -p iggy_common --all-features --all-targets -- -D warnings,
cargo test -p iggy_common serde_secret (6 pass), taplo fmt --check,
hawkeye check, typos, markdownlint, trailing whitespace/newline — all exit
0.

`serialize_secret` and `serialize_optional_secret` write the plaintext,
which leaves an author who needs `Serialize` on a struct holding a
credential with no redacting option — so they reach for the exposing one
and believe it protects them. That is the trap apache#3801 documents.

`serialize_redacted` and `serialize_optional_redacted` write a
placeholder instead. The optional form keeps `Some` distinguishable from
`None`: whether a credential is configured is not itself secret, and
collapsing it to null would report a configured field as unset.

The module doc now leads with what these helpers actually do, since the
absent `Serialize` impl on `SecretString` is the protection and any
helper here is a decision to give it up.
@github-actions

github-actions Bot commented Aug 2, 2026

Copy link
Copy Markdown

Thanks for the PR. It is labeled S-waiting-on-review and queued for review.

Slash commands (own line, regular comment) move it around the queue:

  • /ready - back to S-waiting-on-review after addressing feedback
  • /author - flip to S-waiting-on-author while you finish changes
  • /request-review @user-or-team - request a reviewer

See CONTRIBUTING.md for details.

@github-actions github-actions Bot added the S-waiting-on-review PR is waiting on a reviewer label Aug 2, 2026
@codecov

codecov Bot commented Aug 2, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 74.05%. Comparing base (1453114) to head (fd1e1cc).
⚠️ Report is 19 commits behind head on master.

Additional details and impacted files
@@             Coverage Diff              @@
##             master    #3803      +/-   ##
============================================
- Coverage     75.72%   74.05%   -1.68%     
  Complexity      969      969              
============================================
  Files          1322     1322              
  Lines        159363   154110    -5253     
  Branches     132746   127569    -5177     
============================================
- Hits         120684   114125    -6559     
- Misses        35041    36047    +1006     
- Partials       3638     3938     +300     
Components Coverage Δ
Rust Core 73.69% <100.00%> (-2.00%) ⬇️
Java SDK 62.71% <ø> (ø)
C# SDK 71.13% <ø> (-1.17%) ⬇️
Python SDK 93.10% <ø> (ø)
PHP SDK 84.52% <ø> (ø)
Node SDK 95.31% <ø> (+0.08%) ⬆️
Go SDK 43.08% <ø> (ø)
Files with missing lines Coverage Δ
core/common/src/utils/serde_secret.rs 100.00% <100.00%> (ø)

... and 148 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

The Secrets guidance told plugin authors that annotating a `SecretString`
with `serialize_secret` made serialization redact. It does the opposite:
the helper calls `expose_secret()`. `SecretString` has no `Serialize`
impl precisely so a struct holding one cannot be serialized, so adding
the attribute is what unblocks the derive and gives up the guarantee.
Nine plugins followed that guidance.

The claim appeared twice: in the Secrets prose and again as an
"Auto-redact on Debug/Display + serialization" row in the patterns table,
where it read as a recommended pairing. Both now say which half was true
- `Debug` does redact - and the section gives the default for a plugin
config struct: do not derive `Serialize` at all, since nothing needs it
and leaving it off makes the property compiler-enforced. It also names
the redacting helpers for structs that genuinely need serialization, and
notes that none of this protects against the runtime control API
returning plugin config verbatim, which is apache#3802 and not fixable from
the plugin side.

Corrects the in-tree list too: it named delta_sink, which does not use
these helpers, and omitted s3_sink and surrealdb_sink, which do.
@mlevkov
mlevkov force-pushed the serde-secret-guidance branch from 76a7612 to fd1e1cc Compare August 2, 2026 21:36
@mlevkov

mlevkov commented Aug 3, 2026

Copy link
Copy Markdown
Contributor Author

/request-review @hubcio

@github-actions
github-actions Bot requested a review from hubcio August 3, 2026 03:05

@hubcio hubcio left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

follow-up outside this diff: runtime/src/api/config.rs Debug impl hardcodes "[REDACTED]", which now duplicates the new REDACTED const. same literal is also hardcoded in core/common auth credentials, server state models and the s3_sink URL redaction - worth a small sweep to the const later.


**`serde_secret::serialize_secret` EXPOSES the secret. It does not redact.** It calls `expose_secret()` and writes the plaintext. `SecretString` deliberately has no `Serialize` impl, and that absence is the protection - so adding `serialize_with` is what *unblocks* the derive and turns a compile-time guarantee into plaintext output. Use it only where the plaintext is the point: a wire payload, a persisted config, an API response that exposes credentials by design.

So the default for a plugin config struct is **do not derive `Serialize` at all**. The runtime keeps plugin configuration as the `serde_json::Value` it parsed from TOML and never deserializes into a plugin's config struct, so nothing needs the impl. Leaving it off makes the property compiler-enforced instead of convention-enforced (`sources/http_source/src/lib.rs::HttpSourceConfig` does this, and comments the omission so nobody adds it back).

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

two wording nits in this sentence. the plugin itself does deserialize into its config struct - the sdk glue calls serde_json::from_str::<C> with a DeserializeOwned bound, so Deserialize stays required and "never deserializes" reads wrong; the load-bearing fact for dropping the impl is that nothing ever re-serializes the struct. and plugin_config doesn't only come from TOML - the control API accepts it as JSON and env vars can inject it too.


Note that none of this protects the credential from the runtime's own control API, which returns plugin configuration verbatim - see #3802. Plugin-side annotations are inert there because the runtime never routes through them.

In-tree uses of the exposing helpers: `sinks/{postgres,mongodb,elasticsearch,influxdb,s3,surrealdb}_sink`, `sources/{postgres,elasticsearch,influxdb}_source`.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this reads as an exhaustive inventory but only covers plugins. runtime/src/api/config.rs also puts serialize_secret on HttpConfig::api_key (equally inert - nothing serializes the runtime config today), and a handful of core/common wire-payload types (login, create-user, change-password, PAT) use the exposing helpers by design. scoping the sentence to plugin-side uses is probably the cleanest fix.

//! [`serialize_redacted`] and [`serialize_optional_redacted`] write
//! [`REDACTED`] in place of the value, for a struct that must be serializable
//! for unrelated reasons but whose credential no reader is entitled to.
//! Redacted output does not round-trip: deserializing it yields the literal

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

worth a paired deserialize_with that rejects the [REDACTED] literal, so redacted output fed back into a config loader fails loudly instead of becoming the actual secret? nothing in-tree can hit this today - the redacting helpers have no consumers yet and the only persist/reload path round-trips the raw serde_json::Value - so a doc warning may be enough for now. question is whether to make it mechanical now or when the first consumer shows up.

@github-actions github-actions Bot added S-waiting-on-author PR is waiting on author response and removed S-waiting-on-review PR is waiting on a reviewer labels Aug 3, 2026

@hubcio hubcio left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

S-waiting-on-author PR is waiting on author response

Projects

None yet

Development

Successfully merging this pull request may close these issues.

docs(connectors): serde_secret helpers expose secrets, but the connectors guidance says they redact

2 participants