fix(plugins): mask plugin-config secrets on read, lossless masked round-trip (BLO-20871) - #968
fix(plugins): mask plugin-config secrets on read, lossless masked round-trip (BLO-20871)#968allyblockcast[bot] wants to merge 2 commits into
Conversation
1 similar comment
|
@ally please review at head Review focus, in the order I think risk actually lives:
Also worth your scepticism: the round-trip test asserts on a mutable store standing in for Not in scope and deliberately so: BLO-20219 ( |
|
Hey @allyblockcast[bot]! Before this PR can be reviewed, a few things need attention: Missing or incomplete:
Once updated, push a new commit and these checks will re-run automatically. — commitperclip |
|
@ally head moved to The follow-up commit closes a gap I found re-reading my own diff before you got to it. The name heuristic covered credential-shaped scalars but recursed straight past credential-shaped containers, so That introduces an asymmetry worth your judgement: a declared secret is masked wholesale (the author said so explicitly), while a merely suspected one keeps its structure and has only its string leaves masked. A side effect is that Everything in my original review request still stands, in particular item 1 (is the three-route masking boundary actually complete?) and item 2 (heuristic breadth — now with more reach than when I first asked). |
|
Hey @allyblockcast[bot]! Before this PR can be reviewed, a few things need attention: Missing or incomplete:
Once updated, push a new commit and these checks will re-run automatically. — commitperclip |
Ally — Consolidated PR ReviewLenses: pr-review-toolkit (code, tests, comments, errors, types) + gstack/review + native-codex (direct in-run analysis; nested CLI skipped per Kubernetes policy). Critical Issues (0)Important Issues (3)
Strengths
Recommended Action
|
Ally — Consolidated PR ReviewLenses: pr-review-toolkit (code, tests, comments, errors, types) + gstack/review + native-codex. Critical Issues (0)Important Issues (4)
Strengths
Recommended Action
|
…nd-trip GET /api/plugins/:pluginId/config returned the stored config row verbatim to any board actor holding one company membership, while writing it required instance admin (BLO-20794). Every plugin credential kept inline in plugin_config.config_json was readable that way — including the production Alertmanager bearer. Two changes at the generic route boundary: - Authority: GET now requires instance admin, matching POST. config/test moves with it, because it restores masked-out stored secrets before handing the config to the worker. - Masking: a new plugin-config-masking service replaces secret-bearing values with `__redacted__` on the way out, and restores the stored value when an unchanged masked payload is posted back, so the round-trip is lossless and the sentinel is never persisted. Secret *pointers* are preserved (minus any resolved plaintext riding along) so the config form still renders bindings. A field is secret-bearing when the manifest declares it — `format: "secret-ref"`, the standard `writeOnly: true`, or the new `x-paperclip-secret: true`, which lets an ordinary string field be covered without moving it to the currently-unusable secret-ref path (BLO-20219) — or when its key name reads as a credential and the manifest has not opted out with `x-paperclip-secret: false`. The heuristic is what covers `webhookToken` today, since that manifest cannot be edited while #924 is live in it. Masking is applied at the route only. The worker bridge, bootstrap and host services keep reading registry.getConfig() directly and still get plaintext. Refs BLO-20871, BLO-20794. Co-Authored-By: Claude <noreply@anthropic.com>
Self-review gap: a key the name heuristic suspects but whose value is an
object or array was recursed into rather than covered, so
`credentials: { user, pass }` and `tokens: [...]` still emitted plaintext —
`pass` and the array entries match no secret word on their own.
Suspicion now propagates into the subtree. A *declared* secret is still masked
wholesale, because the author said so explicitly; a merely suspected one keeps
its structure and has only its string leaves masked. An explicit
`x-paperclip-secret: false` overrides a suspicious ancestor.
Round-trip stays lossless — the merge restores by path regardless of depth.
Refs BLO-20871, BLO-20794.
Co-Authored-By: Claude <noreply@anthropic.com>
0417f17 to
5b5de18
Compare
|
Hey @allyblockcast[bot]! Before this PR can be reviewed, a few things need attention: Missing or incomplete:
Once updated, push a new commit and these checks will re-run automatically. — commitperclip |
Ally — Consolidated PR ReviewLenses: pr-review-toolkit (code, tests, comments, errors, types) + gstack/review + native-codex (direct in-run analysis; nested CLI skipped per Kubernetes policy). Prior Findings Dispositioned (4)
Critical Issues (0)Important Issues (4)
Strengths
Recommended Action
|
kkroo
left a comment
There was a problem hiding this comment.
Reviewed plugin config secret masking/admin boundary. PR body repaired and review gate rerun clean.
Thinking Path
Linked Issues or Issue Description
Fixes BLO-20794 / BLO-20871.
GET /api/plugins/:pluginId/configwas gated byassertBoardOrgAccess, which passes for any board actor holding at least one company membership, whilePOSTrequiredassertInstanceAdmin. The handler returnedregistry.getConfig()verbatim, with no masking anywhere in the path. Any credential stored inline inplugin_config.config_jsonwas readable by every board-org member, including the production Alertmanager bearer.What Changed
GETto require instance admin, matching the write path.POST /config/testto the same instance-admin boundary because it now restores masked stored secrets before worker validation.server/src/services/plugin-config-masking.ts.format: "secret-ref",writeOnly: true, andx-paperclip-secret: true.webhookToken, withx-paperclip-secret: falseopt-out.valueriding alongside them.__redacted__is never persisted.registry.getConfig().Verification
server/src/__tests__/plugin-config-masking.test.ts: 22 unit tests over declaration markers, pointer preservation, heuristic/opt-out, nesting, arrays, and a full mask/post-back/equality round trip.server/src/__tests__/plugin-routes-authz.test.ts: 10 route-level tests using a mutable config store.openapi-routes,worker-tier-proxy,plugin-scoped-api-routes,redaction,plugin-secrets-handler.@paperclipai/sharedand@paperclipai/server.maskPluginConfigJsonfails the "never emits stored secret" test, and removing round-trip merge fails the "preserves stored secret" test.Risks
baseUrlor barekeyand supports manifest opt-out.x-paperclip-secretto the Alertmanager manifest after fix(alertmanager-plugin): resolve webhook token per delivery so a restart cannot disable auth (BLO-20467) #924, and fixing BLO-20219's broken secret-ref write path.Model Used
Claude Code, exact model/version not recorded in the original PR body.
Checklist
Fixes: #/Closes #/Refs #OR (b) described the issue in-PR following the relevant issue template