Skip to content

Migration Plan Workflow

The migration plan workflow is the recommended approach for enterprise deployments. It uses a YAML plan file as a reviewable compliance artifact that captures every workload, secret mapping, and vault path before any changes are made.

The workflow has four stages:

  1. Discover -- scan the target cluster and generate a plan
  2. Apply -- provision vault policies and Kubernetes auth roles from the plan
  3. Verify -- confirm that all secret values exist in vault
  4. Migrate -- apply CloudTaser annotations and rolling-restart workloads

The plan file travels between teams (platform, security, application owners) without requiring direct access between the vault and the Kubernetes cluster. This makes the workflow suitable for air-gapped environments and multi-cluster deployments.


Prerequisites

Tool Purpose
cloudtaser CLI v0.7+ Generates and consumes plan files
kubectl Kubernetes cluster access
bao or vault CLI Populating secret values in vault
An EU-hosted OpenBao/Vault instance Secret storage
A running Kubernetes cluster Target for migration

Step 1: Discover

Scan the target cluster for workloads that reference Kubernetes Secrets or Vault injector annotations. The --output-plan flag (-o) writes a MigrationPlan YAML file instead of SecretMapping CRDs.

cloudtaser target discover -o plan.yaml

This scans all non-system namespaces for Deployments, StatefulSets, and DaemonSets. Workloads already annotated with cloudtaser.io/inject: "true" are skipped. System namespaces (kube-system, kube-public, kube-node-lease) are excluded.

To scope the scan to a single namespace:

cloudtaser target discover -n production -o plan.yaml

To use a specific kubeconfig (for example, when managing multiple clusters):

cloudtaser target discover --kubeconfig ~/.kube/target-cluster -o plan.yaml

When --output-plan is used, the --vault-address and --vault-role flags are not required. The plan file captures workload-to-vault-path mappings without needing vault connectivity at discovery time.

What the plan contains

The generated plan groups workloads by namespace as tenants. Each workload lists its secret references with suggested vault paths following the pattern secret/data/<namespace>/<secret-name>.

apiVersion: cloudtaser.io/v1
kind: MigrationPlan
metadata:
  name: migration-plan
  cluster: https://k8s.prod.example.com
  createdAt: "2026-04-01T10:00:00Z"
  createdBy: platform-team
tenants:
  - name: payments
    namespace: payments
    workloads:
      - kind: Deployment
        name: payment-api
        replicas: 3
        secrets:
          - source: k8s-secret/db-credentials
            vaultPath: secret/data/payments/db-credentials
            fields:
              username: DB_USER
              password: DB_PASS
        status: pending

See Plan File Format Reference for the full schema.

Review the plan

The plan file is a compliance artifact. Security and platform teams should review it before proceeding:

  • Are the vault paths correct for your naming convention?
  • Are the field-to-environment-variable mappings accurate?
  • Should any workloads be excluded?

Edit the plan file directly to adjust paths, field mappings, or remove workloads that should not be migrated.


Step 2: Apply the Plan to Vault

The source apply-plan command reads the plan and provisions vault with the policies and Kubernetes auth roles required for each tenant. It does NOT write secret values.

cloudtaser source apply-plan plan.yaml \
  --openbao-addr https://vault.eu.example.com \
  --token hvs.ADMIN_TOKEN

For each tenant in the plan, this command:

  1. Ensures the KV v2 secrets engine is mounted (default mount: secret)
  2. Creates a vault policy <tenant-name>-read granting read access to the tenant's secret paths
  3. Creates a Kubernetes auth role <tenant-name>-role bound to the tenant's namespace

Flags

Flag Description Required Default
--openbao-addr Vault/OpenBao address Yes --
--token Vault token with admin privileges Yes --
--dry-run Print what would be created without making changes No false
--auth-path Kubernetes auth mount path No kubernetes
--kv-mount KV v2 mount path No secret

Preview with dry run

Always preview before applying:

cloudtaser source apply-plan plan.yaml \
  --openbao-addr https://vault.eu.example.com \
  --token hvs.ADMIN_TOKEN \
  --dry-run

Example dry-run output:

Applying plan "migration-plan" to https://vault.eu.example.com
(dry-run mode -- no changes will be made)

Would ensure KV mount:
  secret (ensure mounted)

Would create policies:
  payments-read

Would create roles:
  payments-role

Re-run without --dry-run to execute.

Step 3: Populate Secret Values

After apply-plan creates the vault structure, populate the actual secret values. This step is manual -- the CLI deliberately does not move secret values to preserve the separation of concerns.

Use the bao or vault CLI:

export BAO_ADDR=https://vault.eu.example.com
export BAO_TOKEN=hvs.ADMIN_TOKEN

# Write secrets for each vault path in the plan
bao kv put secret/payments/db-credentials \
  username="app_user" \
  password="s3cret-from-eu-vault"

Where to get the values

  • From existing Kubernetes Secrets: extract with kubectl get secret <name> -n <namespace> -o jsonpath='{.data}' and base64-decode
  • From the source of truth: password manager, CI/CD variables, or cloud provider secret manager
  • From cloudtaser target import: import directly from AWS Secrets Manager, GCP Secret Manager, or Azure Key Vault

Step 4: Verify the Plan

Before migrating workloads, verify that every secret path in the plan exists in vault and has the expected fields populated.

cloudtaser source verify-plan plan.yaml \
  --openbao-addr https://vault.eu.example.com \
  --token hvs.ADMIN_TOKEN

Flags

Flag Description Required Default
--openbao-addr Vault/OpenBao address Yes --
--token Vault token Yes --

Verification checks

For each secret mapping in the plan, verify-plan checks:

  1. The vault path exists
  2. The path has data (values populated, not empty)
  3. All fields listed in the mapping exist in the vault data

Example output

Plan Verification: plan.yaml
========================================
Tenant: payments (namespace: payments)
  payment-api:
    v secret/data/payments/db-credentials -- exists, 2 field(s) populated

Result: 1/1 secrets ready.

If any secret is missing or incomplete:

Plan Verification: plan.yaml
========================================
Tenant: payments (namespace: payments)
  payment-api:
    x secret/data/payments/db-credentials -- path not found

Result: 0/1 secrets ready. 1 need(s) attention.

The command exits with a non-zero status if any secrets are not ready, making it suitable for CI/CD gates.


Step 5: Migrate Workloads

Once all secrets are verified, apply CloudTaser protection to the workloads in the plan using target protect --plan.

cloudtaser target protect --plan plan.yaml \
  --vault-address https://vault.eu.example.com \
  --interactive

In interactive mode, each workload is presented with its secret mappings. You are prompted to confirm injection and rolling restart for each workload individually:

--- Deployment payments/payment-api (namespace: payments, replicas: 3) ---
  k8s-secret/db-credentials -> secret/data/payments/db-credentials
    username -> DB_USER
    password -> DB_PASS
Inject CloudTaser protection? [y/N] y
  -> injected
Trigger rolling restart? [y/N] y
  -> restarted
  -> migrated

Auto-approve mode (CI/automation)

For non-interactive environments:

cloudtaser target protect --plan plan.yaml \
  --vault-address https://vault.eu.example.com \
  --yes

The --yes flag auto-approves all workloads without prompting.

Flags

Flag Description Required Default
--plan Path to the MigrationPlan YAML file Yes --
--vault-address Vault/OpenBao address Yes --
--interactive Prompt for each workload No false
--yes Auto-approve all workloads (CI mode) No false
--kubeconfig Path to kubeconfig file No ~/.kube/config
-n, --namespace Namespace filter No all namespaces

What migrate does

For each workload in the plan with status: pending:

  1. Patches the pod template with CloudTaser annotations:
    • cloudtaser.io/inject: "true"
    • cloudtaser.io/vault-address: <vault-address>
    • cloudtaser.io/secret-paths: <comma-separated vault paths>
    • cloudtaser.io/env-map: <vault-path:field=ENV_VAR,...>
  2. Optionally triggers a rolling restart by adding a cloudtaser.io/restartedAt annotation
  3. Updates the workload's status in the plan file (migrated, skipped, or pending)

Resumable migration

The plan file is updated after each workload. If the migration is interrupted, re-run the same command -- workloads already marked as migrated or skipped are not processed again.

# Resume after interruption
cloudtaser target protect --plan plan.yaml \
  --vault-address https://vault.eu.example.com \
  --interactive

Post-Migration Verification

After migrating workloads, confirm everything is working:

# Check workload protection status
cloudtaser target status

# Validate vault connectivity and protection coverage
cloudtaser target validate \
  --vault-address https://vault.eu.example.com

# Generate a compliance audit report
cloudtaser target audit \
  --vault-address https://vault.eu.example.com

Check that pods are running with the CloudTaser wrapper:

# Verify init container was injected
kubectl get pod -l app=payment-api -n payments \
  -o jsonpath='{.items[0].spec.initContainers[*].name}'
# Expected: cloudtaser-init

# Verify secrets are NOT in /proc/1/environ
kubectl exec deploy/payment-api -n payments -- \
  cat /proc/1/environ | tr '\0' '\n' | grep DB_PASS
# Expected: no output

Complete Example

This section walks through the entire workflow for a cluster with two namespaces (payments and trading).

1. Discover

cloudtaser target discover --kubeconfig ~/.kube/prod -o plan.yaml

2. Review and edit the plan

cat plan.yaml   # review vault paths, field mappings
vi plan.yaml    # adjust if needed

3. Apply vault structure (dry run first)

cloudtaser source apply-plan plan.yaml \
  --openbao-addr https://vault.eu.example.com \
  --token hvs.ADMIN_TOKEN \
  --dry-run

# Then apply for real
cloudtaser source apply-plan plan.yaml \
  --openbao-addr https://vault.eu.example.com \
  --token hvs.ADMIN_TOKEN

4. Populate secrets

bao kv put secret/payments/db-credentials username=app password=s3cret
bao kv put secret/trading/api-key key=ak-12345

5. Verify

cloudtaser source verify-plan plan.yaml \
  --openbao-addr https://vault.eu.example.com \
  --token hvs.ADMIN_TOKEN

6. Migrate interactively

cloudtaser target protect --plan plan.yaml \
  --vault-address https://vault.eu.example.com \
  --interactive

7. Confirm

cloudtaser target status
cloudtaser target audit --vault-address https://vault.eu.example.com