Skip to content

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:

  1. Scan -- cloudtaser-cli target protect --from discovers workloads using the source tool
  2. Generate -- it produces a shell script with annotate, delete, and restart commands
  3. Review -- you review the script and ensure the secret store has the secrets at the expected paths
  4. 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:

  1. Reads the ExternalSecret spec to identify the remote secret store, key, and property mappings
  2. Identifies the Deployments, StatefulSets, or DaemonSets that reference the generated Kubernetes Secret via secretKeyRef or envFrom
  3. Maps the remote keys to secret-store paths and generates cloudtaser.io/env-map annotations

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
cloudtaser-cli target protect --from=eso \
  -n production \
  --secretstore-address https://vault.eu.example.com \
  --secretstore-role cloudtaser
cloudtaser-cli target protect --from=eso \
  --secretstore-address https://vault.eu.example.com \
  --secretstore-role cloudtaser \
  --dry-run
cloudtaser-cli target protect --from=eso \
  --secretstore-address https://vault.eu.example.com \
  --secretstore-role cloudtaser \
  -o migrate-eso.sh

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:

cloudtaser-cli target import --from=aws-sm \
  --secretstore-address https://vault.eu.example.com \
  --secretstore-token hvs.YOUR_TOKEN \
  --prefix secret/data/prod

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:

  1. Reads the SealedSecret metadata to identify the target Secret name and namespace
  2. Reads the corresponding Kubernetes Secret to identify data keys
  3. Identifies workloads that reference the Secret
  4. 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-repo is 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.