PushSecret dataTo
The dataTo field in PushSecret enables bulk pushing of secrets without requiring explicit
per-key configuration. Instead of listing every key manually in data, you point dataTo at a
store and optionally filter or transform the keys that get pushed.
Overview
dataTo supports two distinct modes. Which one to use depends entirely on your provider's
secret model:
| Mode | When to use | remoteKey |
|---|---|---|
| Per-key | Provider uses one named variable/entry per secret (GitHub Actions, Doppler) | not set |
| Bundle | Provider stores structured config as a single named secret (AWS SM, Azure KV, GCP SM, Vault) | required |
Choosing the right mode
Per-key mode (env-var providers)
Providers like GitHub Actions and Doppler model secrets as individual named
variables — each key in your Kubernetes Secret maps to exactly one variable in the provider.
Do not set remoteKey in this case; the key names themselves become the provider variable names.
# GitHub Actions / Doppler — one variable per key
dataTo:
- storeRef:
name: github-store
# no remoteKey — each K8s key becomes its own GitHub secret
match:
regexp: "^APP_"
Result in GitHub Actions (assuming the K8s Secret has APP_TOKEN and APP_ENV):
APP_TOKEN → value of APP_TOKEN
APP_ENV → value of APP_ENV
Bundle mode (named-secret providers)
Providers like AWS Secrets Manager, Azure Key Vault, GCP Secret Manager, and
HashiCorp Vault model secrets as a single named object that holds a JSON payload. Use
remoteKey to name that object — all matched keys are bundled into it as a JSON object.
# AWS SM / Azure KV / GCP SM / Vault — all keys → one named secret
dataTo:
- storeRef:
name: aws-store
remoteKey: my-app/config # the AWS Secrets Manager secret name
match:
regexp: "^DB_"
Result in AWS Secrets Manager:
my-app/config → {"DB_HOST":"localhost","DB_USER":"admin","DB_PASS":"s3cr3t"}
Without remoteKey on named-secret providers
If you omit remoteKey on a provider like AWS Secrets Manager, dataTo falls back to
per-key mode and creates one AWS secret per matched key
(DB_HOST, DB_USER, DB_PASS each become separate secrets).
This is rarely what you want on AWS — always set remoteKey when targeting AWS SM,
Azure KV, GCP SM, or Vault.
Provider reference
| Provider | Secret model | Use remoteKey? |
Notes |
|---|---|---|---|
| AWS Secrets Manager | Named secret (JSON) | Yes | remoteKey = secret name; store prefix is prepended |
| AWS Parameter Store | Named parameter | Yes | remoteKey = parameter path |
| Azure Key Vault | Named secret/key/cert | Yes | remoteKey = object name |
| GCP Secret Manager | Named secret | Yes | remoteKey = secret ID |
| HashiCorp Vault | Named path (JSON) | Yes | remoteKey = Vault path |
| Oracle Vault | Named secret | Yes | remoteKey = secret name |
| Kubernetes | Named secret | Yes | remoteKey = target Secret name |
| Bitwarden | Named item | Yes | remoteKey = item key |
| GitHub Actions | Env-var (one per key) | No | Key name = Actions secret name |
| Doppler | Env-var (one per key) | No | Key name = Doppler variable name |
| Webhook | Configurable | Depends | Check your webhook implementation |
Examples by provider
AWS Secrets Manager
Prefix + remoteKey = concatenated name
The AWS SecretStore prefix is prepended to every remoteKey. If your store has
prefix: myapp/ and your dataTo has remoteKey: db-config, the resulting AWS secret
name is myapp/db-config — not db-config.
A common mistake is setting prefix: secrets-sync-temp/ and remoteKey: secrets-sync-temp,
which creates secrets-sync-temp/secrets-sync-temp — not secrets-sync-temp.
If you want the secret name to be exactly secrets-sync-temp, either remove the prefix
from the store or set remoteKey to the suffix portion only.
Make the value visible in the AWS Console
By default ESO stores secret values as binary (SecretBinary). The AWS Console
may show binary secrets as blank or unreadable. Add secretPushFormat: string to the
metadata to store the JSON as a readable SecretString instead.
apiVersion: external-secrets.io/v1
kind: SecretStore
metadata:
name: aws-store
spec:
provider:
aws:
service: SecretsManager
region: us-east-1
# No prefix — remoteKey is the full secret name.
# If you add a prefix, the final name is: prefix + remoteKey.
---
apiVersion: external-secrets.io/v1alpha1
kind: PushSecret
metadata:
name: push-to-aws
spec:
secretStoreRefs:
- name: aws-store
kind: SecretStore
selector:
secret:
name: app-secrets # K8s Secret with DB_HOST, DB_USER, DB_PASS
dataTo:
- storeRef:
name: aws-store
remoteKey: my-app/db-config # → AWS secret named exactly "my-app/db-config"
match:
regexp: "^DB_"
metadata:
apiVersion: kubernetes.external-secrets.io/v1alpha1
kind: PushSecretMetadata
spec:
secretPushFormat: string # store as SecretString (readable in console)
Result in AWS Secrets Manager:
my-app/db-config → {"DB_HOST":"localhost","DB_USER":"admin","DB_PASS":"s3cr3t"}
Metadata requires the full PushSecretMetadata wrapper
The metadata field is not a plain key-value map. It must be a valid
PushSecretMetadata object with apiVersion, kind, and spec. Putting
secretPushFormat: string directly under metadata: will cause a parse error.
With a store prefix:
# SecretStore has prefix: myapp/
# dataTo remoteKey: db-config
# → AWS secret name: myapp/db-config
Azure Key Vault
dataTo:
- storeRef:
name: azure-store
remoteKey: app-db-config # Azure Key Vault secret name
match:
regexp: "^DB_"
GCP Secret Manager
dataTo:
- storeRef:
name: gcp-store
remoteKey: projects/my-project/secrets/app-db-config
match:
regexp: "^DB_"
HashiCorp Vault
dataTo:
- storeRef:
name: vault-store
remoteKey: secret/data/myapp/db # Vault path (KV v2 style)
match:
regexp: "^DB_"
GitHub Actions
dataTo:
- storeRef:
name: github-store
# No remoteKey — each K8s key becomes its own Actions secret
match:
regexp: "^DEPLOY_"
Result: individual GitHub Actions secrets named DEPLOY_TOKEN, DEPLOY_ENV, etc.
Doppler
dataTo:
- storeRef:
name: doppler-store
# No remoteKey — each K8s key becomes its own Doppler variable
Filtering with match
Use match.regexp to push only a subset of keys. When omitted, all keys are included.
dataTo:
- storeRef:
name: aws-store
remoteKey: myapp/db-secrets
match:
regexp: "^DB_" # only keys starting with DB_
dataTo:
- storeRef:
name: aws-store
remoteKey: myapp/all-secrets
# no match → all keys in the source Secret
Key transformations with rewrite
rewrite only applies in per-key mode (no remoteKey). It transforms the key name before it
becomes the provider variable/secret name. Two rewrite types are available:
Regexp rewrite
dataTo:
- storeRef:
name: github-store
match:
regexp: "^db-"
rewrite:
- regexp:
source: "^db-"
target: "DATABASE_" # db-host → DATABASE_host
Template rewrite
dataTo:
- storeRef:
name: github-store
rewrite:
- transform:
template: "{{ .value | upper }}" # db-host → DB-HOST
Chained rewrites
Multiple rewrites are applied in order — each sees the output of the previous:
dataTo:
- storeRef:
name: github-store
match:
regexp: "^prod-db-"
rewrite:
- regexp: {source: "^prod-", target: ""} # prod-db-host → db-host
- regexp: {source: "^db-", target: "DATABASE_"} # db-host → DATABASE_host
Rewrites are ignored in bundle mode
When remoteKey is set, key names are not used as provider paths — only their values
appear in the JSON object. Rewrite entries are silently ignored in this case.
Multiple dataTo entries
Split matched keys across different targets in the same PushSecret:
# AWS: two separate secrets, each scoped to a category
dataTo:
- storeRef:
name: aws-store
remoteKey: myapp/database
match:
regexp: "^DB_"
- storeRef:
name: aws-store
remoteKey: myapp/api
match:
regexp: "^API_"
# GitHub: separate env-var groups pushed to different stores
dataTo:
- storeRef:
name: github-prod-store
match:
regexp: "^PROD_"
- storeRef:
name: github-staging-store
match:
regexp: "^STAGING_"
Combining dataTo with explicit data
Explicit data entries always override dataTo for the same source key. Use this to apply
bulk defaults and then carve out exceptions:
spec:
dataTo:
- storeRef:
name: aws-store
remoteKey: myapp/config # all keys bundled here by default
data:
- match:
secretKey: MASTER_PASSWORD
remoteRef:
remoteKey: myapp/security/master-password # this key gets its own secret
Conversion strategy
conversionStrategy: ReverseUnicode decodes Unicode-escaped key names before matching and
pushing. Applied before match and rewrite:
dataTo:
- storeRef:
name: aws-store
remoteKey: myapp/config
conversionStrategy: ReverseUnicode
Error handling
| Situation | Behavior |
|---|---|
Invalid regexp in match |
PushSecret enters error state; check .status.conditions |
| Rewrite produces empty key | Reconciliation fails with the offending source key named |
| Two entries produce the same remote key | Reconciliation fails listing all conflicting sources |
match matches no keys |
Not an error; info log, PushSecret stays Ready |
storeRef not in secretStoreRefs |
Validation error on apply |
Best practices
- Always set
remoteKeyfor named-secret providers (AWS SM, Azure KV, GCP SM, Vault) — omitting it creates one secret per key, which is almost never what you want on these providers - Never set
remoteKeyfor env-var providers (GitHub Actions, Doppler) — the key name IS the variable name - Filter before you bundle — use
match.regexpto be explicit about which keys end up in a bundle; avoids accidentally including sensitive keys - Test patterns first — inspect your source Secret's keys before writing patterns:
kubectl get secret my-secret -o jsonpath='{.data}' | jq 'keys' - Combine with
datafor exceptions — usedataTofor the common case, explicitdataentries for keys that need custom paths or properties - Monitor status — check
kubectl get pushsecret <name> -o yamlfor sync errors
See Also
- PushSecret Guide - Basic PushSecret usage
- PushSecret API Reference - Complete API specification
- Templating Guide - Advanced template usage
- ExternalSecret dataFrom - The mirror image: pulling secrets from providers