Vault to OpenBao Migration¶
Scope: HashiCorp Vault 1.4.2 (GCS backend) → OpenBao 2.5.4 (PostgreSQL backend)
Environment: GKE, GCP KMS auto-unseal, Helm-managed installation
Overview¶
| Start | Intermediate | Final | |
|---|---|---|---|
| App | HashiCorp Vault 1.4.2 | HashiCorp Vault 1.9.2 | OpenBao 2.5.4 |
| Helm Chart | vault-0.6.0 | vault-0.19.0 | openbao-0.28.3 |
| Storage | GCS (appcircle-dev2-vault) |
GCS | PostgreSQL |
| Seal | GCP KMS (gcpckms) | GCP KMS (gcpckms) | GCP KMS (gcpckms) |
Why OpenBao?¶
HashiCorp switched Vault to the BSL (Business Source License) starting with version 1.14.x. OpenBao is an open-source fork maintained by the Linux Foundation. GCP KMS auto-unseal is fully supported in OpenBao 2.x.
Why Switch from GCS to PostgreSQL?¶
OpenBao v2.0.0 removed cloud-specific storage backends (GCS, S3, Azure). Only Raft and PostgreSQL (v2.1.0+) are supported.
Prerequisites¶
1. Backup¶
BACKUP_DATE=$(date +%Y%m%d-%H%M%S)
gsutil -m cp -r \
gs://appcircle-dev2-vault/* \
gs://appcircle-backup/dev-vault-pre-pg-migration-${BACKUP_DATE}/
# Verify
gsutil du -sh gs://appcircle-backup/dev-vault-pre-pg-migration-${BACKUP_DATE}/
2. Check Vault Status¶
VAULT_POD=$(kubectl get pod -l app.kubernetes.io/name=vault -o jsonpath='{.items[0].metadata.name}')
kubectl exec ${VAULT_POD} -- vault status
# Sealed: false is expected
GCP KMS Permissions¶
Vault 1.9+ requires the cloudkms.cryptoKeys.get permission. This must be granted before the 1.9.2 upgrade. roles/cloudkms.viewer was added to all environments.
# DEV (project: dev-appcircle)
gcloud kms keys add-iam-policy-binding dev-appcircle-key \
--keyring=dev-appcircle-ring \
--location=global \
--project=dev-appcircle \
--member="serviceAccount:dev-appcircle-vault-sa@dev-appcircle.iam.gserviceaccount.com" \
--role="roles/cloudkms.viewer"
# PREP (project: appcircle)
gcloud kms keys add-iam-policy-binding prep-appcircle-key \
--keyring=prep-appcircle-ring \
--location=global \
--project=appcircle \
--member="serviceAccount:prep-appcircle-vault@appcircle.iam.gserviceaccount.com" \
--role="roles/cloudkms.viewer"
# PROD (project: appcircle)
gcloud kms keys add-iam-policy-binding prod-appcircle-vault \
--keyring=prod-appcircle-ring \
--location=global \
--project=appcircle \
--member="serviceAccount:prod-appcircle-vault@appcircle.iam.gserviceaccount.com" \
--role="roles/cloudkms.viewer"
Note
The permission is bound to the KMS key resource, not the project. This follows the principle of least privilege.
Stage 1: Vault 1.4.2 → 1.9.2¶
Remove Existing Resources¶
cd ./export/appcircle-helm/charts/vault/templates
kubectl --context dev2-appcircle delete -f .
Chart.yaml Changes¶
# Before
- name: vault
version: 0.6.0
repository: https://helm.releases.hashicorp.com
# After
- name: vault
version: 0.19.0
repository: https://helm.releases.hashicorp.com
Helm Upgrade¶
pwsh export.ps1 google dev
# Pull chart
helm repo add hashicorp https://helm.releases.hashicorp.com
helm pull hashicorp/vault --version 0.19.0 -d appcircle-helm/charts/
cd ./export/appcircle-helm/charts/vault/templates
kubectl --context dev2-appcircle apply -f .
# OnDelete strategy — pod must be restarted manually
kubectl delete pod appcircle-v1-vault-0 -n dev-appcircle
Stage 2: GCS to PostgreSQL Migration¶
Create Cloud SQL Database¶
Create a new database and user on the existing Cloud SQL instance:
# Create database
gcloud sql databases create vault \
--instance=<instance-name> \
--project=dev-appcircle
# Create user (use a strong, unique password)
gcloud sql users create vault-user \
--instance=<instance-name> \
--password=<strong-password> \
--project=dev-appcircle
Create PostgreSQL Table¶
CREATE TABLE IF NOT EXISTS vault_kv_store (
parent_path TEXT COLLATE "C" NOT NULL,
path TEXT COLLATE "C",
key TEXT COLLATE "C",
value BYTEA,
CONSTRAINT pkey PRIMARY KEY (path, key)
);
CREATE INDEX IF NOT EXISTS parent_path_idx ON vault_kv_store (parent_path);
Kubernetes Secret — Migration¶
Warning
Do not use these special characters in the password: @ / : ? # + &
kubectl create secret generic vault-pg-secret \
-n dev-appcircle \
--from-literal=connection_url="postgres://USER:PASS@HOST:5432/DBNAME?sslmode=require"
Migration Job¶
Since OpenBao v2.x does not support the GCS backend, the migration is done using the Vault 1.14.0 binary.
apiVersion: v1
kind: ConfigMap
metadata:
name: vault-migrate-config
namespace: dev-appcircle
data:
migrate.hcl: |
storage_source "gcs" {
bucket = "appcircle-dev2-vault"
}
storage_destination "postgresql" {
connection_url = "${PG_CONN_URL}"
table = "vault_kv_store"
}
---
apiVersion: batch/v1
kind: Job
metadata:
name: vault-migrate-gcs-to-pg
namespace: dev-appcircle
spec:
template:
spec:
restartPolicy: Never
serviceAccountName: appcircle-v1-vault
volumes:
- name: google-secret
secret:
secretName: vault-google-secret
- name: migrate-config
configMap:
name: vault-migrate-config
containers:
- name: vault-migrate
image: hashicorp/vault:1.14.0
command:
- "/bin/sh"
- "-c"
- |
sed "s|\${PG_CONN_URL}|${PG_CONN_URL}|g" /migrate/migrate.hcl > /tmp/migrate.hcl
vault operator migrate -config=/tmp/migrate.hcl
env:
- name: GOOGLE_APPLICATION_CREDENTIALS
value: /vault/userconfig/vault-google-secret/credentials.json
- name: GOOGLE_PROJECT
value: dev-appcircle
- name: GOOGLE_REGION
value: global
- name: PG_CONN_URL
valueFrom:
secretKeyRef:
name: vault-pg-secret
key: connection_url
volumeMounts:
- name: google-secret
mountPath: /vault/userconfig/vault-google-secret
readOnly: true
- name: migrate-config
mountPath: /migrate
kubectl apply -f migration-job.yaml
kubectl logs job/vault-migrate-gcs-to-pg -n dev-appcircle -f
Cleanup
The Job and Secret are one-time resources. Delete them after the migration completes.
kubectl delete job vault-migrate-gcs-to-pg -n dev-appcircle
kubectl delete secret vault-pg-secret -n dev-appcircle
kubectl delete configmap vault-migrate-config -n dev-appcircle
Stage 3: Vault 1.9.2 → OpenBao 2.x¶
Chart.yaml Changes¶
# Before
- name: vault
version: 0.19.0
repository: https://helm.releases.hashicorp.com
# After
- name: openbao
version: 0.28.3
repository: https://openbao.github.io/openbao-helm/
alias: vault # existing values files work without changes
condition: vault.enabled
Kubernetes Secret — PostgreSQL Config¶
The storage block is stored in a Kubernetes Secret instead of a ConfigMap, so the connection URL is not stored in plaintext. Because the HCL parser does not expand shell environment variables, the connection details are written directly into the Secret.
kubectl create secret generic vault-pg-config \
-n dev-appcircle \
--from-literal=pg.hcl='storage "postgresql" {
connection_url = "postgres://USER:PASS@HOST:5432/DBNAME?sslmode=require"
table = "vault_kv_store"
ha_enabled = "false"
}'
Values Structure¶
vault:
server:
extraEnvironmentVars:
GOOGLE_REGION: global
GOOGLE_PROJECT: <project>
GOOGLE_APPLICATION_CREDENTIALS: /openbao/userconfig/vault-google-secret/credentials.json
extraArgs: "-config=/openbao/userconfig/vault-pg-config/pg.hcl"
extraVolumes:
- type: 'secret'
name: 'vault-google-secret'
- type: 'secret'
name: 'vault-pg-config'
standalone:
enabled: true
config: |
ui = true
listener "tcp" {
tls_disable = 1
address = "[::]:8200"
cluster_address = "[::]:8201"
}
seal "gcpckms" {
project = "<project>"
region = "global"
key_ring = "<keyring>"
crypto_key = "<key>"
}
# no storage block — loaded via extraArgs from the vault-pg-config secret
Warning
The OpenBao chart does not merge extraVolumes lists — it replaces them. Define the full list only in the most specific (lowest) values file instead of splitting it across multiple overrides.
Helm Upgrade¶
# Remove existing resources
cd ./export/appcircle-helm/charts/vault/templates
kubectl --context dev2-appcircle delete -f .
# Pull chart
helm repo add openbao https://openbao.github.io/openbao-helm/
helm pull openbao/openbao --version 0.28.3 -d appcircle-helm/charts/
# Upgrade
pwsh export.ps1 google dev
cd ./export/appcircle-helm/charts/vault/templates
kubectl --context dev2-appcircle apply -f .
# OnDelete strategy — pod must be restarted manually
kubectl delete pod appcircle-v1-vault-0 -n dev-appcircle
Verification¶
# Check pod status
kubectl get pod -n dev-appcircle -l app.kubernetes.io/name=vault
# Check volumes
kubectl get pod appcircle-v1-vault-0 -n dev-appcircle \
-o jsonpath='{.spec.volumes[*].name}' | tr ' ' '\n'
# vault-google-secret and vault-pg-config should appear
# OpenBao status
kubectl exec appcircle-v1-vault-0 -n dev-appcircle -- bao status
# Sealed: false, Version: 2.5.4
Known Issues¶
| Issue | Cause | Fix |
|---|---|---|
unknown storage type gcs |
GCS was removed in OpenBao v2.0.0 | Use PostgreSQL or Raft |
cloudkms.cryptoKeys.get denied |
New permission required in Vault 1.9+ | Add roles/cloudkms.viewer before upgrade |
cannot parse ${PG_CONN_URL} |
HCL parser does not expand env vars | Move storage block to a Secret, load with extraArgs |
Credentials not found /openbao/userconfig/... |
extraVolumes list lost after override |
Redefine the full list in the overriding values file |
| Pod keeps running with old config | StatefulSet OnDelete update strategy |
Delete the pod manually after helm upgrade |