HashiCorp Vault

Published: 04 Sep. 2026 Last updated: 04 Sep. 2026

Summary

HashiCorp Vault is a widely used identity-based secrets and encryption management system. It stores credentials, certificates, and encryption keys, and issues them dynamically to applications and users. When deployed on Kubernetes, Vault is most commonly configured with Raft integrated storage, in which each Vault Pod keeps a full replica of the data on its own PersistentVolume.

This Application Note discusses how to use CloudCasa to properly protect and restore HashiCorp Vault running in containers under Kubernetes.

CloudCasa has been tested for this application note with a 3 node Vault cluster using Raft integrated storage v2.0.4, deployed with the official HashiCorp Vault Helm chart v0.34.1, and configured for auto-unseal using the Transit secrets engine of a second standalone Vault instance. The information herein is expected to apply to more recent versions, to different cluster sizes, and to other seal types such as cloud KMS auto-unseal as well, though minor adjustments may be needed.

Configure the CloudCasa Vault Snapshotter

To perform a consistent backup of Vault with Raft storage, the vault operator raft snapshot save command is used to create a snapshot during the backup. CloudCasa requires a running Pod with access to Vault to run the backup hook. The following instructions can be used to create the Pod that CloudCasa will use.

Note

A Vault snapshot contains the cluster’s data encrypted under its root key, and the root key is in turn wrapped by the seal. A snapshot taken from a cluster using Transit auto-unseal cannot be restored unless the same Transit key is still available. If the Transit Vault is deployed in the same cluster, it must be included in the same CloudCasa backup as the snapshotter.

Note

The commands below that run inside a Vault Pod require an administrative Vault token. In the examples this is referred to as $VROOT, and can be obtained from the output saved when the cluster was initialized: VROOT=$(jq -r '.root_token' vault-init.json)

  1. Create a snapshot policy in Vault. This policy grants read access to the snapshot endpoint:

    kubectl -n vault exec -i vault-0 -- sh -c "
    export VAULT_TOKEN=$VROOT
    vault policy write snapshot - <<'EOP'
    path \"sys/storage/raft/snapshot\" { capabilities = [\"read\"] }
    EOP
    "
    
  2. Create the namespace, service account, and Kubernetes authentication:

    • Create the namespace:

      kubectl create namespace cc-vault-backup
      
    • Create the serviceaccount:

      kubectl -n cc-vault-backup create serviceaccount cc-vault-snapshot
      
    • Check if a clusterrolebinding also needs to be created. Vault validates the snapshotter’s service account token by calling the Kubernetes TokenReview API, so Vault’s own service account requires permission to do so:

      kubectl auth can-i create tokenreviews --as=system:serviceaccount:vault:vault
      
    • If it returns no, run the following command to create it:

      kubectl create clusterrolebinding vault-token-review --clusterrole=system:auth-delegator --serviceaccount=vault:vault
      
    • Enable and configure the Kubernetes authentication method:

      kubectl -n vault exec -i vault-0 -- sh -c "
      export VAULT_TOKEN=$VROOT
      vault auth enable kubernetes
      vault write auth/kubernetes/config \
      kubernetes_host=\"https://\$KUBERNETES_PORT_443_TCP_ADDR:443\"
      "
      
    • Create the role that binds the service account to the snapshot policy:

      kubectl -n vault exec -i vault-0 -- sh -c "
      export VAULT_TOKEN=$VROOT
      vault write auth/kubernetes/role/snapshot \
      bound_service_account_names=cc-vault-snapshot \
      bound_service_account_namespaces=cc-vault-backup \
      policies=snapshot \
      ttl=20m
      "
      
      .. note:: The value of ``bound_service_account_namespaces`` must match the namespace in which the snapshotter is deployed.
      
  3. Create a PVC for the Vault snapshotter. Replace STORAGE_CLASS_NAME with the name of your storage class:

    apiVersion: v1
    kind: PersistentVolumeClaim
    metadata:
        name: cc-vault-snapshots
        namespace: cc-vault-backup
        labels:
            app: cloudcasa-vault-snapshotter
    spec:
        accessModes: [ReadWriteOnce]
        storageClassName: <STORAGE_CLASS_NAME>
        resources:
            requests:
                storage: 10Gi
    
  4. Deploy the Vault snapshotter. Adjust the image, VAULT_ADDR` environment variable, and ``securityContext as necessary for your environment:

    apiVersion: apps/v1
    kind: Deployment
    metadata:
    name: cloudcasa-vault-snapshotter
    namespace: cc-vault-backup
    labels:
        app: cloudcasa-vault-snapshotter
    spec:
        replicas: 1
        strategy:
            type: Recreate
        selector:
            matchLabels:
                app: cloudcasa-vault-snapshotter
        template:
            metadata:
                labels:
                    app: cloudcasa-vault-snapshotter
            spec:
                serviceAccountName: cc-vault-snapshot
                securityContext:
                    runAsNonRoot: true
                    runAsUser: 100
                    fsGroup: 1000
                containers:
                    - name: snap
                    image: hashicorp/vault:1.17.2
                    command: ["sleep", "infinity"]
                    env:
                        - name: VAULT_ADDR
                        value: "http://vault-active.vault.svc.cluster.local:8200"
                    resources:
                        requests: {cpu: 50m, memory: 64Mi}
                        limits: {memory: 256Mi}
                    volumeMounts:
                        - name: snap
                        mountPath: /snapshots
                volumes:
                    - name: snap
                    persistentVolumeClaim:
                        claimName: cc-vault-snapshots
    
  5. Verify that the snapshot command works from within the Vault snapshotter pod.

    • Open a shell in the Pod:

      kubectl -n cc-vault-backup exec -it deploy/vault-snapshotter -- sh
      
    • Create the snapshot:

      export VAULT_TOKEN=$(vault write -field=token auth/kubernetes/login role=snapshot jwt=@/var/run/secrets/kubernetes.io/serviceaccount/token)
      F=/snapshots/vault-cc-test.snap
      vault operator raft snapshot save "$F"
      
    • Verify the snapshot contents:

      ls -lh "$F"
      tar tzf "$F"
      tar xzf "$F" meta.json -O
      

      The archive should list meta.json, state.bin, and the checksum files, and meta.json should report a non-zero Raft index.

    • Delete the test snapshot:

      rm -f "$F"
      

Backup

The following instructions can be used to configure backup of HashiCorp Vault with Raft storage using the cloudcasa-vault-snapshotter.

  1. From the CloudCasa UI, navigate to Configuration → App Hooks and click the “Add app hook” button. We will create an app hook for the cloudcasa-vault-snapshotter.

  2. Specify a name for the App Hook and select “Pre-backup” as the type. Set the pod selector to app = vault-snapshotter and the container to snap. Add the following into the command field and then click “Save” to create the App Hook:

    /bin/sh -c "export VAULT_TOKEN=$(vault write -field=token auth/kubernetes/login role=snapshot jwt=@/var/run/secrets/kubernetes.io/serviceaccount/token) && \
    vault operator raft snapshot save /snapshots/vault.snap && \
    test -s /snapshots/vault.snap && \
    tar tzf /snapshots/vault.snap | grep -q meta.json"
    
  3. Navigate to Clusters → Overview, select the cluster containing the Vault deployment, and then click “Define backup”.

  4. In the App hooks section of the backup definition, add a pre-backup app hook. Select the cc-vault-backup namespace and select the App Hook created previously.

  5. Run the backup and confirm the job completes.

Restore

Restoring Vault is a two-part operation. CloudCasa restores the Vault snapshot file and, where applicable, the Transit instance that holds the unseal key. A Vault cluster is then deployed and the snapshot is loaded into it using the Vault CLI.

Note

The Vault snapshot must be restored into a Vault cluster that is already running, unsealed, and has an elected Raft leader. The restore operation is submitted through Raft, so the target cluster must be initialized first even though the credentials created during that initialization are discarded immediately afterwards.

  1. Navigate to Clusters → Recovery Points, find the recovery point you want to restore from, and then click Actions → Restore.

  2. Enable “Select namespaces” and select the cc-vault-backup namespace.

    • Also select the namespace of the Vault Transit instance (i.e. vault-transit) if it was deployed in the same cluster.

    Note

    The namespace containing the Vault cluster itself (i.e. vault ) does not need to be selected, and its data-vault-* PersistentVolumes should not be relied upon for recovery. The snapshot file produced by the App Hook is the recoverable copy of the Vault data. Both the snapshotter namespace and the Transit namespace must be protected together, as the snapshot cannot be decrypted without the Transit key.

  3. In the Destination section, select the target cluster for the restore.

  4. Run the restore job and verify that the namespaces, including the cloudcasa-volume-snapshotter, were restored.

    • Unseal the Vault Transit instance if applicable. The Transit instance does not auto-unseal, so it must be unsealed manually after every restart or restore:

      UNSEAL=$(jq -r '.unseal_keys_b64[0]' transit-init.json)
      kubectl -n vault-transit exec vault-transit-0 -- vault operator unseal "$UNSEAL"
      
    • Confirm that the Transit key is valid:

      TROOT=$(jq -r '.root_token' transit-init.json)
      kubectl -n vault-transit exec vault-transit-0 -- sh -c "VAULT_TOKEN=$TROOT vault read transit/keys/autounseal"
      
  5. Copy the Vault snapshot file out of the restored snapshotter Pod:

    POD=$(kubectl -n cc-vault-backup get pod -l app=cloudcasa-vault-snapshotter -o name)
    kubectl -n cc-vault-backup cp ${POD#pod/}:/snapshots/vault.snap ./vault.snap
    
  6. Deploy Vault with Raft storage following the official guide: Deploy Vault with Raft storage following the official guide

  7. Create temporary credentials by initializing the new cluster. These credentials are only used to submit the restore and become invalid as soon as it completes:

    kubectl -n vault exec vault-0 -- vault operator init -recovery-shares=1 -recovery-threshold=1 -format=json > tmp-cred.json
    TMPROOT=$(jq -r '.root_token' tmp-cred.json)
    
  8. Wait for quorum. All three Pods should reach the Running state and report as voters, with one leader:

    kubectl -n vault get pods -w
    kubectl -n vault exec vault-0 -- sh -c "VAULT_TOKEN=$TMPROOT vault operator raft list-peers"
    
  9. Copy the Vault snapshot into the leader Pod and restore it:

    kubectl -n vault cp ./vault.snap vault-0:/tmp/vault.snap
    kubectl -n vault exec vault-0 -- sh -c "VAULT_TOKEN=$TMPROOT vault operator raft snapshot restore /tmp/vault.snap"
    
  10. Switch to the original credentials. The snapshot replaces the target cluster’s keyring with the one from the source cluster, so the temporary root token and recovery keys created previously no longer work, and the source cluster’s credentials must be used from this point onwards:

    VROOT=$(jq -r '.root_token' vault-init.json)
    kubectl -n vault exec vault-0 -- sh -c "VAULT_TOKEN=$VROOT vault token lookup"
    
  11. Verify the restored data. Confirm that the expected mounts, authentication methods, and secret versions are present:

    kubectl -n vault exec vault-0 -- sh -c "VAULT_TOKEN=$VROOT vault secrets list"
    kubectl -n vault exec vault-0 -- sh -c "VAULT_TOKEN=$VROOT vault auth list"
    kubectl -n vault exec vault-0 -- sh -c "VAULT_TOKEN=$VROOT vault kv metadata get <path>"