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 OpenBao path before any changes are made.
The workflow has four stages:
- Discover -- scan the target cluster and generate a plan
- Apply -- provision OpenBao policies and Kubernetes auth roles from the plan
- Verify -- confirm that all secret values exist in OpenBao
- Migrate -- apply cloudtaser annotations and rolling-restart workloads
The plan file travels between teams (platform, security, application owners) without requiring direct access between OpenBao and the Kubernetes cluster. This makes the workflow suitable for air-gapped environments and multi-cluster deployments.
Prerequisites¶
| Tool | Purpose |
|---|---|
cloudtaser-cli v0.12+ |
Generates and consumes plan files |
kubectl |
Kubernetes cluster access |
bao or vault CLI |
Populating secret values in OpenBao |
| An EU-hosted OpenBao instance | Secret storage |
| A running Kubernetes cluster | Target for migration |
Step 1: Discover¶
Scan the target cluster for workloads that reference Kubernetes Secrets or OpenBao injector annotations. The --output-plan flag (-o) writes a MigrationPlan YAML file instead of SecretMapping CRDs.
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:
To use a specific kubeconfig (for example, when managing multiple clusters):
When --output-plan is used, the --secretstore-address and --secretstore-role flags are not required. The plan file captures workload-to-OpenBao-path mappings without needing OpenBao 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 OpenBao 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 OpenBao 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 OpenBao¶
The source apply-plan command reads the plan and provisions OpenBao with the policies and Kubernetes auth roles required for each tenant. It does NOT write secret values.
cloudtaser-cli source apply-plan plan.yaml \
--openbao-addr https://vault.eu.example.com \
--token hvs.ADMIN_TOKEN
For each tenant in the plan, this command:
- Ensures the KV v2 secrets engine is mounted (default mount:
secret) - Creates an OpenBao policy
<tenant-name>-readgranting read access to the tenant's secret paths - Creates a Kubernetes auth role
<tenant-name>-rolebound to the tenant's namespace
Flags¶
| Flag | Description | Required | Default |
|---|---|---|---|
--openbao-addr |
OpenBao address | Yes | -- |
--token |
OpenBao 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-cli 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 OpenBao 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-cli 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 OpenBao and has the expected fields populated.
cloudtaser-cli source verify-plan plan.yaml \
--openbao-addr https://vault.eu.example.com \
--token hvs.ADMIN_TOKEN
Flags¶
| Flag | Description | Required | Default |
|---|---|---|---|
--openbao-addr |
OpenBao address | Yes | -- |
--token |
OpenBao token | Yes | -- |
Verification checks¶
For each secret mapping in the plan, verify-plan checks:
- OpenBao path exists
- The path has data (values populated, not empty)
- All fields listed in the mapping exist in OpenBao 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.
Interactive mode (recommended for first migration)¶
cloudtaser-cli target protect --plan plan.yaml \
--secretstore-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-cli target protect --plan plan.yaml \
--secretstore-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 | -- |
--secretstore-address |
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:
- Patches the pod template with cloudtaser annotations:
cloudtaser.io/inject: "true"cloudtaser.io/secretstore-address: <vault-address>cloudtaser.io/secret-paths: <comma-separated vault paths>cloudtaser.io/env-map: <vault-path:field=ENV_VAR,...>
- Optionally triggers a rolling restart by adding a
cloudtaser.io/restartedAtannotation - Updates the workload's
statusin the plan file (migrated,skipped, orpending)
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-cli target protect --plan plan.yaml \
--secretstore-address https://vault.eu.example.com \
--interactive
Post-Migration Verification¶
After migrating workloads, confirm everything is working:
# Check workload protection status
cloudtaser-cli target status
# Validate vault connectivity and protection coverage
cloudtaser-cli target validate \
--secretstore-address https://vault.eu.example.com
# Generate a compliance audit report
cloudtaser-cli target audit \
--secretstore-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¶
2. Review and edit the plan¶
3. Apply OpenBao structure (dry run first)¶
cloudtaser-cli source apply-plan plan.yaml \
--openbao-addr https://vault.eu.example.com \
--token hvs.ADMIN_TOKEN \
--dry-run
# Then apply for real
cloudtaser-cli 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-cli source verify-plan plan.yaml \
--openbao-addr https://vault.eu.example.com \
--token hvs.ADMIN_TOKEN
6. Migrate interactively¶
cloudtaser-cli target protect --plan plan.yaml \
--secretstore-address https://vault.eu.example.com \
--interactive
7. Confirm¶
cloudtaser-cli target status
cloudtaser-cli target audit --secretstore-address https://vault.eu.example.com
Related¶
- Plan File Format Reference -- full YAML schema documentation
- Enterprise Deployment Architecture -- CLI-as-orchestrator model and multi-cluster topology
- Migration Guide -- legacy
--frombased migration (ESO, Sealed Secrets, SOPS) - CLI Reference -- full command documentation