Repository navigation
feat(pass)!: add --reveal to get, gated by engine authorization - #671
Conversation
9d3a9b8 to
b7c5d2f
Compare
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.
b7c5d2f to
074faa2
Compare
docker-agent
left a comment
There was a problem hiding this comment.
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 { |
There was a problem hiding this comment.
[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:
- Non-reveal path (line 69–70):
printSecretis called with the masked value andpvis abandoned — the GC may hold the backing memory for an indeterminate time. - Authorization denied / engine unreachable (line 72–73):
authorizeRevealreturns an error and the function returns immediately, leavingpv'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 |
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.