Skip to content

feat(pass)!: add --reveal to get, gated by engine authorization - #671

Merged
joe0BAB merged 1 commit into
mainfrom
feat/get-reveal
Oct 5, 2026
Merged

joe0BAB merged 1 commit into
mainfrom
feat/get-reveal

Conversation

@joe0BAB

@joe0BAB joe0BAB commented Oct 5, 2026 •

Copy link
Copy Markdown
Collaborator

GetCommand takes the same client options as RunCommand and returns an error. With --reveal, the command fetches the secret from the keychain, asks the engine to authorize the read, then prints the value in plaintext. Output goes to stdout so a revealed value can be piped.

Closes docker/secrets-engine-private#729.

@joe0BAB
joe0BAB force-pushed the feat/get-reveal branch 2 times, most recently from 9d3a9b8 to b7c5d2f Compare October 5, 2026 14:26
@joe0BAB
joe0BAB marked this pull request as ready for review October 5, 2026 14:37
GetCommand takes the same client options as RunCommand and returns an
error. With --reveal, the command fetches the secret from the keychain,
asks the engine to authorize the read, then prints the value in
plaintext. Output goes to stdout so a revealed value can be piped.

@docker-agent docker-agent 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.

Assessment: 🟡 NEEDS ATTENTION

This PR correctly implements the --reveal flag for the get command, gated by engine authorization. The flag wiring, preflight-ping guard for unbounded clients, and test coverage are all sound — no authorization bypass was found. One medium-confidence security design concern was identified in the new code.

if !reveal {
return printSecret(cmd.OutOrStdout(), id, []byte(maskedValue))
}
if err := authorizeReveal(cmd.Context(), clientOpts, id); err != nil {

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.

[medium] Secret plaintext loaded into memory before authorization check; not zeroed on denied/non-reveal paths

kc.Get() is called unconditionally (before the reveal flag is checked or authorization is performed), so the *pass.PassValue holding the decrypted secret lands in heap memory on every invocation of get. There are two paths where pv is never explicitly zeroed before the function returns:

  1. Non-reveal path (line 69–70): printSecret is called with the masked value and pv is abandoned — the GC may hold the backing memory for an indeterminate time.
  2. Authorization denied / engine unreachable (line 72–73): authorizeReveal returns an error and the function returns immediately, leaving pv's plaintext in memory despite the denial.

Only the successful reveal path has defer clear(value), and that only clears the marshaled byte slice — not the pv struct itself.

Impact: This is a defense-in-depth concern rather than an exploitable vulnerability: an attacker with process memory access already has broader privileges, and the GC will eventually reclaim the memory. However, for a --reveal feature explicitly gated by authorization, explicitly zeroing sensitive bytes on ALL exit paths is the expected practice.

Suggested fix: Reorder the logic to authorize first, then fetch the secret (moving kc.Get() after authorizeReveal). If reordering is infeasible (e.g. the keychain fetch is needed to derive the authorization scope), add explicit clear calls on the deny/error paths:

if err := authorizeReveal(cmd.Context(), clientOpts, id); err != nil {
    // pv is not yet loaded if kc.Get is moved here; or if pre-loaded, clear it:
    return err
}
Confidence Score
🟡 moderate 67/100

@joe0BAB
joe0BAB merged commit 0c06120 into main Oct 5, 2026
16 checks passed
@joe0BAB
joe0BAB deleted the feat/get-reveal branch October 5, 2026 14:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants