---
title: From HashiCorp Vault
path: migrations/from-hashicorp-vault
status: published
---

# Migrate from HashiCorp Vault

Move from HashiCorp Vault to ScaiVault. The recommended path: federate first (ScaiVault fronts your Vault), migrate per-path subtrees, retire Vault when nothing reads from it.

You don't need a maintenance window. Reads continue working throughout. The risky steps are obvious and isolated.

```mermaid
graph LR
    A[Apps reading<br/>Vault directly]
    B[Apps reading<br/>via ScaiVault<br/>federation proxy]
    C[Apps reading from<br/>new ScaiVault paths<br/>Vault still authoritative]
    D[Apps reading from<br/>new ScaiVault paths<br/>ScaiVault authoritative]

    A -->|1. set up federation| B
    B -->|2-3. migrate subtree,<br/>repoint consumers| C
    C -->|4. flip authority,<br/>retire Vault path| D
```

## When this fits

- You're already running Vault in production.
- You want unified identity (ScaiKey), unified audit, and managed PKI / dynamic secrets without standing up Vault Enterprise.
- You have time to move incrementally. Lift-and-shift in one weekend is technically possible but unwise.

If you have *very* simple Vault usage (KV v2 only, a few hundred secrets, one team), skip federation and do direct import — see step 5.

## What you need

- A ScaiVault token with `secrets:write`, `policies:write`, `federation:write`, `admin`.
- Vault credentials with read access to the KV mounts you want to migrate. AppRole is the recommended auth method.
- 30 minutes for setup; a few weeks for an incremental migration of any non-trivial Vault.

## 1. Inventory your Vault

Before anything moves, list what you have:

```bash
export VAULT_ADDR="https://vault.internal:8200"
export VAULT_TOKEN=...

# List mounts
vault secrets list -format=json | jq -r 'to_entries[] | "\(.key) \(.value.type) \(.value.options.version)"'
# kv/ kv 2
# pki/ pki -
# database/ database -

# For each KV mount, walk it
vault kv list -format=json kv/ | jq -r '.[]'
```

Categorize by mount:

- **KV v1 / v2** — move via federation or direct import.
- **PKI** — re-create CAs and roles in ScaiVault, gradually replace.
- **Database** — recreate dynamic engines in ScaiVault, point apps at the new ones.
- **AWS / Azure / GCP** — same as Database.
- **Transit** — ScaiVault doesn't currently expose a Transit-equivalent encryption API. Keep Vault for this if you need it, or migrate to KMS-native crypto.

This guide focuses on KV migration. Other backends follow the same federate-then-migrate pattern; the per-backend specifics live in the [Federation Reference](../reference/federation).

## 2. Set up AppRole auth for ScaiVault

Inside Vault, create an AppRole that ScaiVault can use:

```bash
vault auth enable approle

vault write auth/approle/role/scaivault-federation \
  token_policies="scaivault-reader" \
  token_ttl=1h \
  token_max_ttl=24h

vault policy write scaivault-reader -<<EOF
path "kv/data/*"     { capabilities = ["read", "list"] }
path "kv/metadata/*" { capabilities = ["read", "list"] }
EOF

ROLE_ID=$(vault read -field=role_id auth/approle/role/scaivault-federation/role-id)
SECRET_ID=$(vault write -f -field=secret_id auth/approle/role/scaivault-federation/secret-id)
```

Save `ROLE_ID` and `SECRET_ID`.

## 3. Store the AppRole credentials in ScaiVault

```bash
curl -X PUT https://scaivault.scailabs.ai/v1/secrets/infra/hashicorp/approle \
  -H "Authorization: Bearer $SCAIVAULT_TOKEN" \
  -H "Content-Type: application/json" \
  -d "{
    \"data\": {\"role_id\": \"$ROLE_ID\", \"secret_id\": \"$SECRET_ID\"},
    \"secret_type\": \"json\"
  }"
```

## 4. Configure federation

```bash
curl -X POST https://scaivault.scailabs.ai/v1/federation/backends \
  -H "Authorization: Bearer $SCAIVAULT_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "hashicorp-production",
    "type": "hashicorp-vault",
    "config": {
      "endpoint": "https://vault.internal:8200",
      "auth_method": "approle",
      "auth_config_path": "infra/hashicorp/approle",
      "tls_verify": true
    },
    "path_mapping": {
      "external/hashicorp/**": "kv/data/**"
    },
    "mode": "proxy"
  }'
# -> {"id": "fed_abc", "status": "healthy"}
```

`mode: "proxy"` means reads pass through in real time. Vault stays authoritative; nothing in ScaiVault is a copy yet.

Verify with a read:

```bash
# In Vault, this secret lives at kv/data/apps/billing/db
# Through ScaiVault, it's at external/hashicorp/apps/billing/db
curl -H "Authorization: Bearer $SCAIVAULT_TOKEN" \
     https://scaivault.scailabs.ai/v1/secrets/external/hashicorp/apps/billing/db
```

You should see the value Vault holds. ScaiVault's audit log records this read; Vault's audit log records the underlying read it proxied. Both trails exist.

## 5. Migrate one subtree

Pick the smallest, lowest-risk subtree. For each path, decide its new home in ScaiVault.

Typical mapping:

| Vault path | New ScaiVault path |
|------------|---------------------|
| `kv/data/apps/billing/db` | `environments/production/billing/database` |
| `kv/data/apps/billing/stripe` | `environments/production/billing/stripe` |
| `kv/data/shared/salesforce` | `shared/integrations/salesforce/oauth` |

The new scheme reflects what *should* be true going forward — see the [`.env` migration tutorial](../tutorials/migrate-from-env-files#2-decide-your-path-scheme) for path-scheme guidance.

Copy the secrets:

```python
#!/usr/bin/env python3
"""Copy a Vault subtree into ScaiVault under a new path."""
import os, sys, httpx

VAULT  = os.environ["VAULT_ADDR"]
VTOKEN = os.environ["VAULT_TOKEN"]
SV     = "https://scaivault.scailabs.ai"
SVT    = os.environ["SCAIVAULT_TOKEN"]

MAPPING = {
    "apps/billing/db":     "environments/production/billing/database",
    "apps/billing/stripe": "environments/production/billing/stripe",
}

for vault_path, sv_path in MAPPING.items():
    # Read from Vault
    r = httpx.get(
        f"{VAULT}/v1/kv/data/{vault_path}",
        headers={"X-Vault-Token": VTOKEN},
    )
    r.raise_for_status()
    data = r.json()["data"]["data"]  # KV v2 nests "data" twice

    # Write to ScaiVault
    r = httpx.put(
        f"{SV}/v1/secrets/{sv_path}",
        headers={"Authorization": f"Bearer {SVT}"},
        json={
            "data": data,
            "secret_type": "json",
            "metadata": {
                "tags": ["migrated-from-vault"],
                "description": f"Migrated from vault: kv/data/{vault_path}",
            },
            "options": {"max_versions": 10},
        },
    )
    r.raise_for_status()
    print(f"  {vault_path}  ->  {sv_path}")
```

Verify each migrated value matches what Vault returned. A diff script:

```python
for vault_path, sv_path in MAPPING.items():
    v_val = httpx.get(f"{VAULT}/v1/kv/data/{vault_path}", headers={"X-Vault-Token": VTOKEN}).json()["data"]["data"]
    s_val = httpx.get(f"{SV}/v1/secrets/{sv_path}", headers={"Authorization": f"Bearer {SVT}"}).json()["data"]
    if v_val != s_val:
        print(f"DIFF: {vault_path}\n  vault:     {v_val}\n  scaivault: {s_val}")
```

Zero output means clean.

## 6. Update one consumer to read from the new path

Pick one service that uses the migrated secret. Update its code to read the ScaiVault path instead of the Vault path. Keep both code paths available behind a config flag — like the [`.env` tutorial](../tutorials/migrate-from-env-files) describes for env vars.

Verify in production. Audit log shows the service reading the new path:

```bash
curl -H "Authorization: Bearer $SCAIVAULT_TOKEN" \
     "https://scaivault.scailabs.ai/v1/audit/logs?identity_id=sa:billing-prod&from=1+hour+ago"
```

## 7. Update remaining consumers

Repeat step 6 for every service reading the migrated secret. Use Vault's audit log to find them:

```bash
# Inside Vault's audit log, look for who's still reading kv/data/apps/billing/db
grep '"path":"kv/data/apps/billing/db"' /var/log/vault/audit.log \
  | jq -r '.auth.display_name' \
  | sort -u
```

Each consumer migration is independent. After a week or so with no Vault reads, you can be confident nothing left behind.

## 8. Retire the Vault path

Now's the time to decide: keep Vault as the long-term store too (and let ScaiVault sync from it), or remove it.

**To remove:** `vault kv delete kv/data/apps/billing/db`. ScaiVault still has the value — Vault doesn't.

**To keep but flip authority:** change the federation backend's mode from `proxy` to `sync`, with the ScaiVault paths becoming primary. Vault becomes a backup.

```bash
curl -X PATCH https://scaivault.scailabs.ai/v1/federation/backends/fed_abc \
  -H "Authorization: Bearer $SCAIVAULT_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"mode": "sync", "sync_interval": "1h", "conflict_resolution": "local_wins"}'
```

## 9. Repeat for the next subtree

Take the next set of Vault paths. Steps 5-8 again. Different subtrees migrate independently — there's no coordination required across teams.

## Migration of non-KV backends

**PKI:** Vault PKI roles map cleanly to ScaiVault PKI roles. Create new CAs in ScaiVault, issue new certs from there, let old certs from Vault PKI expire naturally. Don't re-issue Vault-PKI certs in ScaiVault — that just delays the migration.

**Database engines:** Recreate the engine in ScaiVault pointing at the same database with new root credentials. Update consumers to call ScaiVault's `/v1/dynamic/engines/.../creds/...` instead of Vault's `/v1/database/creds/...`. Existing Vault-issued leases keep working until they expire.

**AWS / Azure / GCP:** Same pattern. Create the engine in ScaiVault, update consumers, let Vault leases drain.

**Transit:** Out of scope today. If you use Transit heavily, keep Vault for that one workload.

## Common questions

**"Do I need to keep Vault and ScaiVault in sync during migration?"** No. While you're using federation in proxy mode, Vault is the only source of truth. After you copy a path into ScaiVault and start using the new path, that path is moot in Vault — let it diverge.

**"What if my Vault uses Vault Namespaces?"** Pass the namespace in the federation config: `config.namespace: "acme/finance"`. ScaiVault scopes reads to that namespace.

**"What about tokens issued by Vault?"** Vault tokens stop working once you move off Vault. Plan ScaiVault token rollout (via ScaiKey-issued JWTs) in parallel.

**"Can ScaiVault federate to multiple Vault instances at once?"** Yes. Create multiple federation backends, each with its own `path_mapping`. Useful when migrating from a multi-region Vault setup.

## What's next

- [Federation Deep Dive](../advanced/federation) — modes, conflicts, sync internals.
- [Migrate from .env files](../tutorials/migrate-from-env-files) — the path-scheme discussion applies here too.
- [Dynamic Postgres tutorial](../tutorials/dynamic-postgres-credentials) — replacing Vault's `database/` backend.
