Skip to content

Migration Guide

The cloudtaser migrate 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.


Overview

Migration follows a consistent pattern regardless of the source tool:

  1. Scan -- cloudtaser migrate 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 vault has the secrets at the expected paths
  4. Execute -- run the script to migrate workloads

Vault must have the secrets first

The migration script does not copy secrets into vault. It assumes your EU-hosted vault 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 vault 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 migrate --from=eso \
  --vault-address https://vault.eu.example.com \
  --vault-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 vault paths and generates cloudtaser.io/env-map annotations

Output

The command generates a migration script:

#!/bin/bash
# Generated by: cloudtaser migrate --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/vault-address=https://vault.eu.example.com \
  cloudtaser.io/vault-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 migrate --from=eso \
  -n production \
  --vault-address https://vault.eu.example.com \
  --vault-role cloudtaser
cloudtaser migrate --from=eso \
  --vault-address https://vault.eu.example.com \
  --vault-role cloudtaser \
  --dry-run
cloudtaser migrate --from=eso \
  --vault-address https://vault.eu.example.com \
  --vault-role cloudtaser \
  -o migrate-eso.sh

Prerequisites

  • CloudTaser operator and eBPF agent must be deployed in the cluster
  • Vault must be configured with Kubernetes auth (cloudtaser connect)
  • Vault 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 import to copy those secrets into your EU vault first:

cloudtaser import --from=aws-sm \
  --vault-address https://vault.eu.example.com \
  --vault-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 migrate --from=sealed-secrets \
  --vault-address https://vault.eu.example.com \
  --vault-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 vault path mappings

Output

#!/bin/bash
# Generated by: cloudtaser migrate --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/vault-address=https://vault.eu.example.com \
  cloudtaser.io/vault-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 vault

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 vault.

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 vault
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 vault.


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 migrate --from=sops \
  --vault-address https://vault.eu.example.com \
  --vault-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 migrate --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/vault-address=https://vault.eu.example.com \
  cloudtaser.io/vault-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 vault

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 vault.

Decrypt and import:

# Decrypt the SOPS file
sops -d secrets.enc.yaml | yq '.data | to_entries[] | .key + "=" + (.value | @base64d)' -r

# Write to vault
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.


Vault Path Mapping

The migration command maps source secrets to vault paths using the following convention:

Source Generated Vault 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 --vault-path-prefix:

cloudtaser migrate --from=eso \
  --vault-address https://vault.eu.example.com \
  --vault-role cloudtaser \
  --vault-path-prefix "secret/data/eu"

Post-Migration Verification

After running the migration script:

# Verify workloads are running with CloudTaser injection
cloudtaser status -n production

# Run an audit to confirm no orphaned Kubernetes Secrets remain
cloudtaser audit \
  --vault-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.