@rimiti/vault-to-openbao
v1.5.0
Published
Migrate secrets from HashiCorp Vault to OpenBao
Downloads
427
Readme
@rimiti/vault-to-openbao
Migrate secrets, policies and auth methods from HashiCorp Vault to OpenBao, designed to run against a Kubernetes cluster.
Prerequisites
- Node.js >= 18
- Network access to both instances (Vault and OpenBao)
- Root token, or a token with read/list rights on Vault and write rights on OpenBao
Installation
Global (CLI usage)
npm install -g @rimiti/vault-to-openbaoLocal (programmatic usage)
yarn add @rimiti/vault-to-openbaoCLI
Configuration
All options can be passed as CLI flags or environment variables. CLI flags take precedence.
| Flag | Env var | Description | Default |
| --------------------- | -------------------- | -------------------------------------------------- | ------------------------ |
| --vault-addr | VAULT_ADDR | URL of the source Vault instance | — |
| --vault-token | VAULT_TOKEN | Vault authentication token | — |
| --openbao-addr | OPENBAO_ADDR | URL of the destination OpenBao instance | — |
| --openbao-token | OPENBAO_TOKEN | OpenBao authentication token | — |
| --dry-run | DRY_RUN=true | Simulate migration without writing | false |
| --skip-tls-verify | SKIP_TLS_VERIFY=true | Disable TLS certificate verification | false |
| --skip-mounts | SKIP_MOUNTS | Comma-separated KV mounts to skip | sys,identity,cubbyhole |
| --skip-policies | SKIP_POLICIES | Comma-separated policy names to skip | — |
| --skip-auth-methods | SKIP_AUTH_METHODS | Comma-separated auth method paths to skip | — |
| --concurrency | CONCURRENCY | Number of secrets migrated in parallel | 5 |
| -V, --version | | Display version | |
| -h, --help | | Display help | |
Usage
Dry-run first (recommended):
vault-to-openbao \
--vault-addr https://vault.example.com \
--vault-token hvs.xxxx \
--openbao-addr https://openbao.example.com \
--openbao-token s.xxxx \
--dry-runReal migration:
vault-to-openbao \
--vault-addr https://vault.example.com \
--vault-token hvs.xxxx \
--openbao-addr https://openbao.example.com \
--openbao-token s.xxxxUsing a .env file instead of flags:
cp .env.example .env
# edit .env, then:
vault-to-openbaoProgrammatic API
import { migrate } from "@rimiti/vault-to-openbao";
const stats = await migrate({
vault: {
addr: "https://vault.example.com",
token: "hvs.xxxx",
},
openbao: {
addr: "https://openbao.example.com",
token: "s.xxxx",
},
dryRun: false,
skipTlsVerify: false,
skipMounts: ["sys", "identity", "cubbyhole"],
skipPolicies: [],
skipAuthMethods: [],
concurrency: 5,
});
console.log(`Policies : ${stats.migratedPolicies} / ${stats.totalPolicies}`);
console.log(`Auth : ${stats.migratedAuthMethods} / ${stats.totalAuthMethods}`);
console.log(`Entities : ${stats.migratedEntities} / ${stats.totalEntities}`);
console.log(`Groups : ${stats.migratedGroups} / ${stats.totalGroups}`);
console.log(`Leases : ${stats.totalLeases} active (inventory only)`);
console.log(`Secrets : ${stats.migratedSecrets} / ${stats.totalSecrets}`);migrate(config) return value
interface MigrationStats {
// Policies
totalPolicies: number;
migratedPolicies: number;
failedPolicies: number;
// Auth methods
totalAuthMethods: number;
migratedAuthMethods: number;
failedAuthMethods: number;
// Identity — entities
totalEntities: number;
migratedEntities: number;
failedEntities: number;
// Identity — groups
totalGroups: number;
migratedGroups: number;
failedGroups: number;
// Leases (inventory only — not migrated)
totalLeases: number;
// KV secrets
totalMounts: number;
skippedMounts: number;
totalSecrets: number;
migratedSecrets: number;
failedSecrets: number;
// All errors
errors: Array<{ path: string; error: string }>;
}How it works
The migration runs in seven steps:
Step 1/7 — Migrating policies
→ Lists all ACL policies via LIST /v1/sys/policies/acl
→ Skips built-in policies (root, default)
→ Writes each policy to OpenBao
Step 2/7 — Migrating auth methods
→ Lists all auth mounts via /v1/sys/auth
→ Skips built-in auth (token)
→ Enables each method in OpenBao, then migrates its config and roles/users
Step 3/7 — Migrating identity (entities & groups)
→ Builds an accessor mapping (Vault auth accessor → OpenBao auth accessor)
→ Creates all entities (upsert by name), resolves old→new entity ID mapping
→ Creates entity aliases with remapped accessors and entity IDs
→ Creates internal groups (two-pass: create without members, then update members)
→ Creates group aliases for external groups with remapped accessors
Step 4/7 — Lease inventory
→ Lists all active leases recursively from /v1/sys/leases/lookup/
→ Logs a summary grouped by prefix
→ Does NOT migrate leases (they are runtime credentials tied to Vault)
Step 5/7 — Discovering KV mounts
→ Queries /v1/sys/mounts on Vault
→ Filters mounts of type KV (v1 and v2)
→ Excludes system mounts (sys, identity, cubbyhole)
Step 6/7 — Enumerating secrets
→ Recursively walks each mount using the LIST operation
→ Creates any missing mounts in OpenBao
Step 7/7 — Migrating secrets
→ Reads each secret from Vault
→ Writes it to OpenBao (in parallel batches)Supported auth method types
| Type | Config | Roles | Users / Groups | Notes |
| ------------ | :----: | :---: | :------------: | ------------------------------------------- |
| approle | — | ✓ | — | Secret IDs cannot be extracted |
| kubernetes | ✓ | ✓ | — | |
| userpass | — | — | ✓ | Passwords are hashed — users must reset |
| jwt | ✓ | ✓ | — | |
| oidc | ✓ | ✓ | — | |
| ldap | ✓ | — | ✓ + groups | |
| github | ✓ | — | ✓ + teams | |
| others | — | — | — | Mount is enabled, content is skipped (warn) |
KV version handling
| Version | Vault read | OpenBao write |
| ------- | ----------------------------- | ------------------------------ |
| KV v1 | GET /v1/<mount>/<path> | POST /v1/<mount>/<path> |
| KV v2 | GET /v1/<mount>/data/<path> | POST /v1/<mount>/data/<path> |
The version is detected automatically from the mount options (options.version).
Output
============================================================
Vault → OpenBao Migration
============================================================
2026-03-27T10:00:00.000Z INFO Source: https://vault.example.com
2026-03-27T10:00:00.000Z INFO Destination: https://openbao.example.com
...
2026-03-27T10:00:00.000Z OK Migrated policy: app-readonly
2026-03-27T10:00:00.000Z OK Migrated policy: app-readwrite
2026-03-27T10:00:01.000Z OK Migrated auth [kubernetes] kubernetes/
2026-03-27T10:00:01.000Z OK Migrated: secret/app/database
2026-03-27T10:00:01.000Z OK Migrated: secret/app/api-keys
...
============================================================
Migration Summary
============================================================
Policies : 2 / 2 (0 failed)
Auth methods : 1 / 1 (0 failed)
Entities : 5 / 5 (0 failed)
Groups : 3 / 3 (0 failed)
Active leases : 12 (inventory only — not migrated)
KV mounts : 3 discovered, 0 skipped
Secrets : 42 / 42 (0 failed)The CLI exits with code 1 if at least one item failed to migrate.
Required permissions
Vault (source) — minimal policy
# KV secrets
path "sys/mounts" {
capabilities = ["read"]
}
path "<mount>/metadata/*" {
capabilities = ["list"]
}
path "<mount>/data/*" {
capabilities = ["read"]
}
# KV v1
path "<mount>/*" {
capabilities = ["read", "list"]
}
# Policies
path "sys/policies/acl" {
capabilities = ["list"]
}
path "sys/policies/acl/*" {
capabilities = ["read"]
}
# Auth methods
path "sys/auth" {
capabilities = ["read"]
}
path "auth/*" {
capabilities = ["read", "list"]
}
# Identity
path "identity/*" {
capabilities = ["read", "list"]
}
# Leases inventory
path "sys/leases/lookup/*" {
capabilities = ["list", "sudo"]
}OpenBao (destination) — minimal policy
# KV secrets
path "sys/mounts" {
capabilities = ["read"]
}
path "sys/mounts/<mount>" {
capabilities = ["create", "update"]
}
path "<mount>/data/*" {
capabilities = ["create", "update"]
}
# KV v1
path "<mount>/*" {
capabilities = ["create", "update"]
}
# Policies
path "sys/policies/acl/*" {
capabilities = ["create", "update"]
}
# Auth methods
path "sys/auth" {
capabilities = ["read"]
}
path "sys/auth/*" {
capabilities = ["create", "update"]
}
path "auth/*" {
capabilities = ["create", "update"]
}
# Identity
path "identity/*" {
capabilities = ["create", "update", "read", "list"]
}