Fix: store sensitive step outputs in a companion Secret instead of the plaintext context ConfigMap - #245
Conversation
… the plaintext context ConfigMap All workflow vars, including step outputs derived from Kubernetes Secrets, are persisted to the workflow-<name>-context ConfigMap in plaintext. Let step templates declare $sensitivePaths; matching outputs are routed through a new SetSensitiveVar into an owner-referenced Opaque Secret instead. GetVar reads both stores transparently, ambiguous valueFrom expressions fail closed, and a failed Secret write fails the Commit rather than falling back to the ConfigMap. Workflow-side counterpart of kubevela/kubevela#6840. Signed-off-by: sakirr05 <sakirahmed75531@gmail.com>
Codecov Report❌ Patch coverage is
❌ Your patch check has failed because the patch coverage (24.32%) is below the target coverage (70.00%). You can increase the patch coverage or adjust the target coverage.
Additional details and impacted files@@ Coverage Diff @@
## main #245 +/- ##
===========================================
- Coverage 62.49% 24.77% -37.72%
===========================================
Files 62 64 +2
Lines 4415 5336 +921
===========================================
- Hits 2759 1322 -1437
- Misses 1324 3780 +2456
+ Partials 332 234 -98
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
3 issues found across 5 files
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="pkg/hooks/data_passing.go">
<violation number="1" location="pkg/hooks/data_passing.go:78">
P1: Malformed `$sensitivePaths` silently disables protection and routes every output to the plaintext ConfigMap. Reject invalid declarations or fail closed by routing outputs through `SetSensitiveVar` when this field exists but cannot decode.</violation>
</file>
<file name="pkg/context/context.go">
<violation number="1" location="pkg/context/context.go:83">
P1: A sensitive output reusing an existing normal-context name still resolves to, and leaves behind, the ConfigMap value. Clear/migrate that ordinary path when classifying it sensitive and make the sensitive value authoritative, so opt-in updates cannot retain or consume stale plaintext.</violation>
<violation number="2" location="pkg/context/context.go:310">
P1: A pre-existing Secret with the companion name is overwritten with sensitive workflow output without proving it belongs to this context. Validate owner references/identity before patching (or fail and choose a safe store) to avoid disclosing data to a pre-created Secret and corrupting it.</violation>
</file>
Reply with feedback, questions, or to request a fix.
Fix all with cubic | Re-trigger cubic
| return nil | ||
| } | ||
| var paths []string | ||
| if err := v.Decode(&paths); err != nil { |
There was a problem hiding this comment.
P1: Malformed $sensitivePaths silently disables protection and routes every output to the plaintext ConfigMap. Reject invalid declarations or fail closed by routing outputs through SetSensitiveVar when this field exists but cannot decode.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At pkg/hooks/data_passing.go, line 78:
<comment>Malformed `$sensitivePaths` silently disables protection and routes every output to the plaintext ConfigMap. Reject invalid declarations or fail closed by routing outputs through `SetSensitiveVar` when this field exists but cannot decode.</comment>
<file context>
@@ -59,9 +59,70 @@ func Input(ctx wfContext.Context, paramValue cue.Value, step oamv1alpha1.Workflo
+ return nil
+ }
+ var paths []string
+ if err := v.Decode(&paths); err != nil {
+ return nil
+ }
</file context>
| } | ||
| return err | ||
| } | ||
| return cli.Patch(ctx, secret, client.MergeFrom(existing.DeepCopy())) |
There was a problem hiding this comment.
P1: A pre-existing Secret with the companion name is overwritten with sensitive workflow output without proving it belongs to this context. Validate owner references/identity before patching (or fail and choose a safe store) to avoid disclosing data to a pre-created Secret and corrupting it.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At pkg/context/context.go, line 310:
<comment>A pre-existing Secret with the companion name is overwritten with sensitive workflow output without proving it belongs to this context. Validate owner references/identity before patching (or fail and choose a safe store) to avoid disclosing data to a pre-created Secret and corrupting it.</comment>
<file context>
@@ -189,6 +244,72 @@ func (wf *WorkflowContext) sync(ctx context.Context) error {
+ }
+ return err
+ }
+ return cli.Patch(ctx, secret, client.MergeFrom(existing.DeepCopy()))
+}
+
</file context>
| return cli.Patch(ctx, secret, client.MergeFrom(existing.DeepCopy())) | |
| if !reflect.DeepEqual(existing.OwnerReferences, wf.store.OwnerReferences) { | |
| return fmt.Errorf("sensitive context Secret %s has unexpected owner references", secret.Name) | |
| } | |
| return cli.Patch(ctx, secret, client.MergeFrom(existing.DeepCopy())) |
| func (wf *WorkflowContext) GetVar(paths ...string) (cue.Value, error) { | ||
| v := wf.vars.LookupPath(value.FieldPath(paths...)) | ||
| if !v.Exists() { | ||
| if sv := wf.sensitiveVars.LookupPath(value.FieldPath(paths...)); sv.Exists() { |
There was a problem hiding this comment.
P1: A sensitive output reusing an existing normal-context name still resolves to, and leaves behind, the ConfigMap value. Clear/migrate that ordinary path when classifying it sensitive and make the sensitive value authoritative, so opt-in updates cannot retain or consume stale plaintext.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At pkg/context/context.go, line 83:
<comment>A sensitive output reusing an existing normal-context name still resolves to, and leaves behind, the ConfigMap value. Clear/migrate that ordinary path when classifying it sensitive and make the sensitive value authoritative, so opt-in updates cannot retain or consume stale plaintext.</comment>
<file context>
@@ -56,12 +62,27 @@ type WorkflowContext struct {
func (wf *WorkflowContext) GetVar(paths ...string) (cue.Value, error) {
v := wf.vars.LookupPath(value.FieldPath(paths...))
if !v.Exists() {
+ if sv := wf.sensitiveVars.LookupPath(value.FieldPath(paths...)); sv.Exists() {
+ return sv, nil
+ }
</file context>
Description of your changes
Workflow vars — including step outputs whose values come straight out of
Kubernetes Secrets — all get serialized into
workflow-<name>-contextConfigMapdata by
writeToStore. So a step like kubevela's generate-jdbc-connection,which reads a Secret and exposes the decoded password, leaks that password in
plaintext to anyone with configmap read access the moment a user declares an
output on it. Same class of problem as kubevela/kubevela#6840, which recently
got fixed on the policy side — this is the workflow-side variant.
The fix follows the same idea, adapted to how the context store works:
$sensitivePaths: ["password", ...]in theirrendered value. Outputs whose
valueFromoverlaps a declared path (eitherdirection — extracting a parent of a sensitive path still includes the value)
get routed through a new
SetSensitiveVarinstead ofSetVar.(
workflow-<name>-context-sensitive) with the same owner references as theConfigMap, so GC is unchanged. The Secret is only created once something
sensitive exists — plain workflows never get one.
GetVarreads both stores transparently, so inputs/data-passing betweensteps work exactly as before regardless of where a var lives.
valueFromis an expression rather than a plain dotted path and thetemplate declared any sensitive paths, it's treated as sensitive — can't
prove an expression doesn't embed the value, so it fails closed.
back to writing the ConfigMap.
EnableInMemoryContext) keeps sensitive vars in thein-memory object, which never reaches the API server anyway.
Couple of honest limitations: templates have to opt in by declaring
$sensitivePaths, existing templates aren't retroactively protected. And thealternative — a
sensitivefield on the outputs API type — would be akubevela/pkg change plus CRD schema updates across repos, which felt too heavy
for a first pass. Open to going that way if preferred.
How has this code been tested
GetVar), no-Secret-when-nothing-sensitive, LoadContext recovery from an
existing Secret, sync-on-later-commits, end-to-end Output() routing, and a
table test for the path matching.
clean main checkout (SHA1-signed test cert rejected by go1.23), so those are
pre-existing and unrelated.
Summary by cubic
Stores sensitive step outputs in a companion Secret instead of the plaintext workflow context ConfigMap to prevent leaks. Templates declare
$sensitivePaths; matching outputs are saved securely, andGetVarreads remain seamless.Bug Fixes
SetSensitiveVarto persist sensitive vars toworkflow-<name>-context-sensitive(Opaque, owner-referenced).Output()routes outputs whosevalueFromoverlaps$sensitivePaths; expressions are treated as sensitive.GetVarreads from both ConfigMap and Secret;EnableInMemoryContextkeeps sensitive data in memory only.Migration
$sensitivePathsto step templates to protect outputs; existing templates stay unchanged.Written for commit 210737c. Summary will update on new commits.