dataToAuthor: Mohamed Rekiba Date: 2026-01-20 Status: Proposed Related Issue: #5221 — Revamp PushSecret PR: #5850
PushSecret today requires an explicit data entry for every key you want to push. This creates three problems:
ExternalSecret already solved the equivalent inbound problem with dataFrom (bulk-pull from providers). PushSecret has no equivalent outbound mechanism.
| Workaround | Drawback |
|---|---|
Enumerate every key in spec.data |
Verbose; falls out of sync when keys change |
| External tooling (scripts, Helm helpers) to generate PushSecret YAML | Adds build-time dependency; not declarative |
| One PushSecret per key | Explodes resource count; harder to reason about |
None of these are satisfactory for teams with dynamic secret sets that change frequently.
data entries, with explicit entries taking precedence.dataFrom where the push direction makes sense.spec.data — explicit per-key control remains available and takes priority.Extract or Find — the source is always the Kubernetes Secret selected by spec.selector, not a provider query.Merge rewrite — PushSecret has a single source, so there is nothing to merge.RefreshPolicy (tracked separately in #5221).SecretsClient).type PushSecretDataTo struct {
StoreRef *PushSecretStoreRef // required — which store to push to
RemoteKey string // optional — bundle mode target
Match *PushSecretDataToMatch // optional — regexp key filter
Rewrite []PushSecretRewrite // optional — key transformations
Metadata *apiextensionsv1.JSON // optional — provider-specific metadata
ConversionStrategy PushSecretConversionStrategy // optional — key name encoding
}
type PushSecretDataToMatch struct {
RegExp string // empty or nil = match all keys
}
type PushSecretRewrite struct {
// Exactly one of:
Regexp *esv1.ExternalSecretRewriteRegexp
Transform *esv1.ExternalSecretRewriteTransform
}
dataTo?The name mirrors ExternalSecret's dataFrom:
dataFrom = pull data from the provider into K8sdataTo = push data to the provider from K8sThe direction is unambiguous and the symmetry aids discoverability.
| Mode | Trigger | Behavior | Use case |
|---|---|---|---|
| Per-key | remoteKey not set |
Each matched key becomes its own provider secret/variable | Env-var providers (GitHub Actions, Doppler) |
| Bundle | remoteKey set |
All matched keys bundled as JSON into one named provider secret | Named-secret providers (AWS SM, Vault, Azure KV, GCP SM) |
Bundle mode and rewrite are mutually exclusive — when keys are bundled into a JSON object, the key names inside the JSON are the source names (after conversion), not individually rewritten provider paths.
dataFrom| Aspect | ExternalSecret dataFrom |
PushSecret dataTo |
|---|---|---|
| Direction | Provider → K8s | K8s → Provider |
| Source | Provider (Extract, Find, GeneratorRef) | K8s Secret (via spec.selector) |
| Source discovery | Find by tags/name in provider | Filter by regexp on K8s key names |
| Key transformation | Regexp, Transform, Merge | Regexp, Transform (no Merge — single source) |
| Store targeting | Single secretStoreRef per ES |
Per-entry storeRef (required) |
| Merge strategy | Multiple dataFrom merged into one Secret | dataTo + explicit data merged (explicit wins) |
dataFrom?Extract or Find — the source is always the K8s Secret; there's nothing to query.Merge rewrite — single source means no multi-source key collisions to resolve.PushSecretRewrite reuses the inner types from ExternalSecret:
esv1.ExternalSecretRewriteRegexp (source/target regexp replacement)esv1.ExternalSecretRewriteTransform (Go template transformation)This avoids type duplication while intentionally excluding ExternalSecretRewriteMerge which doesn't apply to the push direction.
The controller uses rewriteWithKeyMapping() instead of esutils.RewriteMap() because PushSecret needs a source → destination key mapping for conflict resolution and status tracking. RewriteMap operates on map[string][]byte (transforming the map in place), while PushSecret needs to track which original key produced which remote key. This divergence is intentional and documented.
storeRef is requiredEvery dataTo entry must specify a storeRef with either name or labelSelector. This was added after maintainer feedback to prevent accidentally pushing to all stores when secretStoreRefs contains multiple entries.
Each dataTo entry carries its own Metadata field. Since different providers need structurally different metadata (e.g., AWS tags vs. Azure properties), and each entry targets a specific store via storeRef, users can provide per-store metadata naturally by having separate dataTo entries per store.
| Feature | Interaction with dataTo |
|---|---|
Template (spec.template) |
Template is applied before dataTo expansion. dataTo matches against template output keys. |
| UpdatePolicy=IfNotExists | Honored per-entry: if the remote secret already exists, the push is skipped. |
| DeletionPolicy=Delete | All dataTo-expanded entries are tracked in status.syncedPushSecrets. When the source Secret is deleted, all tracked provider secrets are cleaned up. |
| ConversionStrategy | Applied before key matching and rewriting, so regexp patterns see converted key names. |
Explicit data |
Explicit entries override dataTo for the same source key. Comparison uses original (unconverted) K8s key names. |
| Scenario | Behavior |
|---|---|
| Empty match pattern | Matches all keys |
| No keys match | Info log, continue (not an error) |
| Invalid regexp | PushSecret enters error state with details in status |
| Duplicate remote keys (within or across entries) | Reconciliation fails listing all conflicting sources |
Explicit data for same source key |
data wins; dataTo entry is dropped |
| Invalid template | Fail with template parsing error |
Both regexp and transform on a rewrite |
Blocked by CRD XValidation |
storeRef not in secretStoreRefs |
Validation error |
| Source Secret deleted + DeletionPolicy=Delete | Provider secrets cleaned up via status tracking |
remoteKey + rewrite on same entry |
rewrite is ignored in bundle mode (documented) |
dataFrom field nameRejected because dataFrom implies pulling from a source, while PushSecret pushes to a destination. Using dataFrom on PushSecret would be semantically confusing.
data is emptyRejected because implicit behavior is dangerous for secrets. A typo or misconfiguration could push keys to unintended stores. Explicit opt-in via dataTo is safer.
Instead of per-entry metadata, use a map keyed by provider type. Rejected because storeRef per entry already enables per-store metadata naturally, and a provider-keyed map would require the API to enumerate provider types.
ExternalSecretRewriteUsing a direct type alias would include Merge which doesn't apply to PushSecret. A new struct with shared inner types provides the right subset.
dataTo is fully optional — existing PushSecrets work exactly as before.data field semantics are unchanged.