Skip to content

Migration Plan Workflow

Not yet verified; do not use for production

The checks below describe candidate CLI source and focused test evidence; a signed matched release and live end-to-end migration are not yet verified. An earlier live CLI reported migrated while admission refused the new Pods and old unprotected Pods kept using Kubernetes Secrets. A successful patch, Available Deployment, source verification or old status marker is insufficient. Regenerate a v2 plan, verify the replacement application through its Service and review source Secret retirement separately.

This workflow uses a YAML plan file to review workloads, secret mappings and OpenBao paths before changes. This draft describes the separate direct-store migration candidate; it is not a way to qualify or repair the US-cluster/EU-Port customer procedure. Do not run its legacy source-auth setup against the managed authority procedure.

The workflow has four stages:

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

The plan file can be reviewed by platform, security and application owners. It does not prove runtime connectivity: direct workload login and external Kubernetes TokenReview still need their configured network paths and CA trust.


Prerequisites

Tool Purpose
Reviewed CLI migration candidate Generates and consumes v2 plans; release qualification is not yet verified
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.

cloudtaser-cli target discover -o plan.yaml

This scans all non-system namespaces for Deployments, StatefulSets, and DaemonSets. Workloads already annotated with secretstore.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-cli target discover -n production -o plan.yaml

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

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

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/v2
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
    namespace: payments
    serviceAccountName: <live-service-account-name>

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 \
  --secretstore-address https://vault.eu.example.com \
  --ca-cert ./source-ca.pem \
  --secretstore-token-file ./private-bootstrap.token

For each tenant in the plan, this command:

  1. Ensures the KV v2 secrets engine is mounted (default mount: secret)
  2. Creates an OpenBao 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

In the candidate documented here, the role is also bound to the exact workload ServiceAccounts recorded in the v2 plan. Confirm that the final apply-plan writer and protect consumer use the same role convention before cutover; integration with other role-writing changes is not yet verified. An older wildcard role left on the source can defeat narrow tenant roles. Inventory and reconcile existing grants; creating a new role alone does not prove isolation.

Flags

Flag Description Required Default
--secretstore-address OpenBao address Yes --
--secretstore-token-file Private admin token file or - for stdin Yes --
--ca-cert, --secretstore-ca-file Public source CA PEM No --
--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 \
  --secretstore-address https://vault.eu.example.com \
  --ca-cert ./source-ca.pem \
  --secretstore-token-file ./private-bootstrap.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.

Dry-run renders intended changes without contacting the store. It is not a CA, authentication, connectivity or secret-existence receipt.


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 \
  --secretstore-address https://vault.eu.example.com \
  --ca-cert ./source-ca.pem \
  --secretstore-token-file ./private-bootstrap.token

Flags

Flag Description Required Default
--secretstore-address OpenBao address Yes --
--secretstore-token-file Private admin token file or - for stdin Yes --
--ca-cert, --secretstore-ca-file Public source CA PEM; otherwise configured/system trust No --

Verification checks

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

  1. OpenBao path exists
  2. The path has data (values populated, not empty)
  3. 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.

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. One approval permits the atomic template change and its resulting rollout; there is no second restart patch:

--- Deployment payments/payment-api ---
Inject CloudTaser protection? [y/N] y
  -> migrated (new injected rollout Ready; mapped Secret references removed)
Source Secret retained: payments/db-credentials; retirement requires separate consumer/rollback review.

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 Source-based scanning only; plan mode requires exact namespaces in the reviewed file No --

What migrate does

The candidate source and focused tests implement these checks for each approved workload; successful live cutover is not yet verified:

  1. Check the live workload namespace/SA against the reviewed v2 plan, then make one resourceVersion-guarded template update. It sets canonical secretstore.cloudtaser.io/inject: "true", the store address, cloudtaser.io/secretstore-role: <tenant>-role, secret paths and env-map.
  2. Encode env-map as password=DB_PASS,username=DB_USER;key=API_KEY: commas separate fields, semicolons separate groups in secret-path order. No path prefix belongs before a field. Explicit field mappings are required; ambiguous destinations or unsupported source volumes/envFrom refuse.
  3. Remove only exact mapped Kubernetes secretKeyRef inputs from the template. The template update itself starts the rollout. A paused/zero Deployment or OnDelete/partitioned update requires a separate cutover plan.
  4. Wait for the whole current rollout, with exact controller ownership, current generation, webhook-redacted annotation digests, wrapper runtime config and Ready/started running containers. Each affected container must have a readiness probe and zero restarts. The CLI checks probe presence; you must make the probe sensitive to actual application delivery. A probe-less Pod that reports Ready or an old Available replica is insufficient. Any refusal, crash, incomplete rollout, identity drift or deadline leaves pending and exits nonzero; only verified startup is counted as migrated.
  5. Retain and explicitly report each source Secret for later reviewed retirement. This does not claim that its plaintext or historical copies have left etcd.

Failure does not automatically roll back a template update. Inspect the current template, owned Pods, rollout conditions and application Service before retry or an owner-reviewed rollback. A retained source Secret alone does not restore its removed Pod references. Keep the original reviewed workload manifest and rollback procedure; a pending result is not a guarantee of unchanged workloads.

Private source-CA trust must also be configured in the operator/workload profile; apply-plan --ca-cert configures the CLI's source connection only. If the audience fence is enabled, approve any alternate workload endpoint before cutover.

The startup checks establish that the new wrapper fetched/mapped secrets and started the replacement app without the mapped Secret fallback. They do not inspect app memory or prove that its runtime API consumed the value. Qualify the image/delivery mode, and verify a digest or a real application operation through its own Service before retiring the source Secret.

Additional plan flag Meaning
--rollout-timeout 5m Per-workload maximum verification wait; caller cancellation can shorten it
--dry-run Validate/render intent without workload or plan-status writes; not a connectivity receipt

Resumable migration

The plan is saved after the run, including per-workload pending/error results. Re-running revalidates historical migrated entries instead of trusting an older CLI's marker; explicitly skipped entries remain skipped. A status-save failure is an error. Keep and review the plan, not just the summary count.

# 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

# Inspect replacement readiness and Service membership without protected exec.
kubectl get pods -n payments
kubectl get endpointslices -n payments

Source Secret retirement

After the application's own value/behavior check, inventory other Pod and controller references, rollback needs, ESO/GitOps producers, backups and etcd retention. Stop or update any producer that would recreate the Secret. Only an authorized owner may then delete the exact obsolete source Secret. Never bulk- delete a namespace's Secrets from the plan alone. The migration command does not do that destructive action or assert erasure of old data/backups.