Migration Guide¶
Recommended: Plan-based workflow
For enterprise deployments, the Migration Plan Workflow is the recommended approach. It uses a reviewable YAML plan file (discover, apply, verify, migrate) and supports air-gapped environments, multi-cluster deployments, and interactive per-workload approval.
The cloudtaser-cli target protect --from command scans your cluster for workloads using existing secret management tools and generates a migration script that transitions them to cloudtaser. It supports External Secrets Operator, Sealed Secrets, and SOPS-encrypted Kubernetes Secrets.
Leadership summary
cloudtaser can automatically generate migration scripts from your current secret management tool (ESO, Sealed Secrets, SOPS) -- reducing migration from weeks to hours.
Overview¶
Migration follows a consistent pattern regardless of the source tool:
- Scan --
cloudtaser-cli target protect --fromdiscovers workloads using the source tool - Generate -- it produces a shell script with annotate, delete, and restart commands
- Review -- you review the script and ensure the secret store has the secrets at the expected paths
- Execute -- run the script to migrate workloads
The secret store must have the secrets first
The migration script does not copy secrets into the secret store. It assumes your EU-hosted secret store already has the secrets at paths matching the cloudtaser annotations. For tools where this is not automatic (Sealed Secrets, SOPS), you must import secrets into the secret store before running the migration.
Migrate from External Secrets Operator¶
External Secrets Operator (ESO) synchronizes secrets from an external store (AWS Secrets Manager, GCP Secret Manager, Azure Key Vault) into Kubernetes Secrets. cloudtaser replaces both ESO and the Kubernetes Secrets it creates.
Scan¶
cloudtaser-cli target protect --from=eso \
--secretstore-address https://vault.eu.example.com \
--secretstore-role cloudtaser
This scans all namespaces for ExternalSecret CRDs and their associated Kubernetes Secrets. For each, it:
- Reads the
ExternalSecretspec to identify the remote secret store, key, and property mappings - Identifies the Deployments, StatefulSets, or DaemonSets that reference the generated Kubernetes Secret via
secretKeyReforenvFrom - Maps the remote keys to secret-store paths and generates
cloudtaser.io/env-mapannotations
Output¶
The command generates a migration script:
#!/bin/bash
# Generated by: cloudtaser-cli target protect --from=eso
# Namespace: production
# Date: 2026-03-21
# --- myapp ---
# ExternalSecret: myapp-secrets (source: aws-secretsmanager/prod/myapp)
# Vault path: secret/data/prod/myapp
kubectl annotate deployment myapp -n production \
cloudtaser.io/inject=true \
cloudtaser.io/secretstore-address=https://vault.eu.example.com \
cloudtaser.io/secretstore-role=cloudtaser \
cloudtaser.io/secret-paths=secret/data/prod/myapp \
"cloudtaser.io/env-map=db_password=PGPASSWORD,api_key=API_KEY"
kubectl delete externalsecret myapp-secrets -n production
kubectl delete secret myapp-secrets -n production
kubectl rollout restart deployment/myapp -n production
Prerequisites¶
- cloudtaser operator and eBPF agent must be deployed in the cluster
- The secret store must be configured with Kubernetes auth (
cloudtaser-cli target connect) - The secret store must contain the secrets at the mapped paths
ESO already syncs from an external store
If your ESO SecretStore points to AWS Secrets Manager, GCP Secret Manager, or Azure Key Vault, use cloudtaser-cli target import to copy those secrets into your EU secret store first:
Migrate from Sealed Secrets¶
Sealed Secrets encrypts secret values in Git using the cluster's sealing key. The Sealed Secrets controller decrypts them in-cluster and creates standard Kubernetes Secrets.
Scan¶
cloudtaser-cli target protect --from=sealed-secrets \
--secretstore-address https://vault.eu.example.com \
--secretstore-role cloudtaser
This scans all namespaces for SealedSecret CRDs and their corresponding Kubernetes Secrets. For each, it:
- Reads the SealedSecret metadata to identify the target Secret name and namespace
- Reads the corresponding Kubernetes Secret to identify data keys
- Identifies workloads that reference the Secret
- Generates cloudtaser annotations with secret-store path mappings
Output¶
#!/bin/bash
# Generated by: cloudtaser-cli target protect --from=sealed-secrets
# Namespace: production
# --- myapp ---
# SealedSecret: myapp-sealed (generates Secret: myapp-secrets)
# Vault path: secret/data/production/myapp-secrets
kubectl annotate deployment myapp -n production \
cloudtaser.io/inject=true \
cloudtaser.io/secretstore-address=https://vault.eu.example.com \
cloudtaser.io/secretstore-role=cloudtaser \
cloudtaser.io/secret-paths=secret/data/production/myapp-secrets \
"cloudtaser.io/env-map=db_password=PGPASSWORD,api_key=API_KEY"
kubectl delete sealedsecret myapp-sealed -n production
kubectl delete secret myapp-secrets -n production
kubectl rollout restart deployment/myapp -n production
Prerequisites¶
Action required: import secrets into the secret store
Sealed Secrets stores encrypted values in Git. The actual plaintext secrets exist only inside the cluster as Kubernetes Secrets. Before migrating, you must extract the current secret values and write them to the secret store.
Option 1: Extract from existing Kubernetes Secrets
# Read the current values from the K8s Secret
kubectl get secret myapp-secrets -n production -o json | \
jq -r '.data | to_entries[] | "\(.key)=\(.value | @base64d)"'
# Write to the secret store (OpenBao/HashiCorp Vault shown; use `bao` for OpenBao)
vault kv put secret/production/myapp-secrets \
db_password="extracted_value" \
api_key="extracted_value"
Option 2: Re-encrypt from source of truth
If you have the original plaintext values elsewhere (password manager, deployment scripts), write them directly to the secret store.
Migrate from SOPS¶
SOPS encrypts Kubernetes Secret manifests (or values files) using KMS, PGP, or age keys. The decrypted Secret is applied to the cluster at deploy time (via ArgoCD SOPS plugin, Flux Kustomize, or CI/CD pipeline).
Scan¶
cloudtaser-cli target protect --from=sops \
--secretstore-address https://vault.eu.example.com \
--secretstore-role cloudtaser
This scans all namespaces for Kubernetes Secrets and identifies those that are likely SOPS-managed by checking for:
- SOPS metadata annotations on the Secret
- Corresponding SOPS-encrypted files in Git (if
--git-repois specified) - Secrets referenced by workloads that do not have an ExternalSecret or SealedSecret source
For each identified Secret, the command generates cloudtaser annotations and cleanup commands.
Output¶
#!/bin/bash
# Generated by: cloudtaser-cli target protect --from=sops
# Namespace: production
# --- myapp ---
# Secret: myapp-secrets (SOPS-managed)
# Vault path: secret/data/production/myapp-secrets
kubectl annotate deployment myapp -n production \
cloudtaser.io/inject=true \
cloudtaser.io/secretstore-address=https://vault.eu.example.com \
cloudtaser.io/secretstore-role=cloudtaser \
cloudtaser.io/secret-paths=secret/data/production/myapp-secrets \
"cloudtaser.io/env-map=db_password=PGPASSWORD,api_key=API_KEY"
kubectl delete secret myapp-secrets -n production
kubectl rollout restart deployment/myapp -n production
Prerequisites¶
Action required: import secrets into the secret store
SOPS-encrypted secrets exist as encrypted files in Git and as plaintext Kubernetes Secrets in the cluster. Before migrating, extract the current values and write them to the secret store.
Decrypt and import:
# Decrypt the SOPS file
sops -d secrets.enc.yaml | yq '.data | to_entries[] | .key + "=" + (.value | @base64d)' -r
# Write to the secret store (OpenBao/HashiCorp Vault shown; use `bao` for OpenBao)
vault kv put secret/production/myapp-secrets \
db_password="decrypted_value" \
api_key="decrypted_value"
After migration, remove the SOPS-encrypted files from Git. They are no longer needed.
What the Migration Script Does¶
Every generated migration script follows the same three-step pattern per workload:
| Step | Command | Purpose |
|---|---|---|
| 1. Annotate | kubectl annotate deployment ... |
Adds cloudtaser annotations to the pod template, triggering injection on next rollout |
| 2. Delete | kubectl delete externalsecret/sealedsecret/secret ... |
Removes the old secret resources from the cluster |
| 3. Restart | kubectl rollout restart deployment ... |
Triggers a new rollout so pods pick up the cloudtaser annotations |
The script is idempotent
Running the script multiple times is safe. kubectl annotate with --overwrite replaces existing annotations. kubectl delete on a missing resource returns a warning, not an error. kubectl rollout restart is always safe.
Secret Store Path Mapping¶
The migration command maps source secrets to secret-store paths using the following convention:
| Source | Generated Secret-Store Path |
|---|---|
ESO: ExternalSecret with remote key prod/myapp |
secret/data/prod/myapp |
Sealed Secrets: SealedSecret generating Secret myapp-secrets in namespace production |
secret/data/production/myapp-secrets |
SOPS: Secret myapp-secrets in namespace production |
secret/data/production/myapp-secrets |
Override the path prefix with --secretstore-path-prefix:
cloudtaser-cli target protect --from=eso \
--secretstore-address https://vault.eu.example.com \
--secretstore-role cloudtaser \
--secretstore-path-prefix "secret/data/eu"
Post-Migration Verification¶
After running the migration script:
# Verify workloads are running with cloudtaser injection
cloudtaser-cli target status -n production
# Run an audit to confirm no orphaned Kubernetes Secrets remain
cloudtaser-cli target audit \
--secretstore-address https://vault.eu.example.com \
-n production
# Check that pods are receiving secrets
kubectl logs <pod-name> -c <container-name> | head -20
Migrate one namespace at a time
Start with a staging or development namespace. Verify everything works before migrating production workloads.