# bank-vaults: the unseal problem, solved across five clouds

> HashiCorp Vault has three separate hard days: the day you initialise it, the day it restarts and needs unsealing, and the day you configure auth methods and secret engines. The Bank-Vaults CLI targets exactly those three, and the unseal implementation is where the interesting engineering sits, because it has to reach a key management service on whichever cloud you happen to run on.

**bank-vaults/bank-vaults** — A Vault swiss-army knife: A CLI tool to init, unseal and configure Vault (auth methods, secret engines).

- Repository: https://github.com/bank-vaults/bank-vaults
- Website: https://bank-vaults.dev
- Stars: 2,274 · Forks: 490
- Language: Go
- License: Apache-2.0
- Published: 2026-09-30 · Updated: 2026-09-30 · Language: en
- Canonical page: https://hysenlabs.com/projects/bank-vaults-bank-vaults

## Three hard days, and a CLI aimed at all three

The project's own description is a list of three jobs: initialise Vault, unseal Vault, and configure Vault's auth methods and secret engines. That list is a good summary of why Vault is hard to run, and it is worth separating because the three problems have different characters.

Initialisation is a one-time ceremony with a well-understood failure mode, and you will do it once. Unsealing is the recurring one: every restart, every node, every replica, and a Vault that does not come back is a service outage that starts with somebody finding a key share. Configuration is the treadmill, because auth methods and secret engines are exactly the state you want in version control and exactly the state people end up editing by hand.

Bank-Vaults is an umbrella project rather than one tool. The CLI makes configuring Vault easier. A separate Vault Operator repository makes operating Vault on Kubernetes easier, a Secrets Webhook injects secrets straight into pods, and a Vault SDK makes working with Vault easier from Go. The word in the README is umbrella and it is accurate: this repository is the piece you install on your laptop, and the others are pieces you deploy.

That structure has a practical consequence for adoption. The CLI is by far the least coupled of the four, since it is a binary you run and then forget, while the operator and the webhook are controllers with their own lifecycle in your cluster. You can adopt the CLI on its own and decide separately whether you want a mutating webhook rewriting your pod specs.

The project is a CNCF Sandbox project and credits HashiCorp for open sourcing Vault, which is the right order of priority to keep in mind: the CLI is the ergonomics layer, not a replacement for the thing it wraps.

## One binary that can reach a KMS on five clouds

The go.mod file is where this project's real scope becomes visible, and the direct dependencies make the argument better than any feature list.

There are AWS SDK v2 modules for configuration, KMS and S3. There is the Azure SDK for Go, including the Key Vault secrets module. There is Google Cloud storage and the Google API client. There is Alibaba Cloud's SDK and its object storage SDK. There is Oracle's OCI SDK. Five clouds, five KMS clients, one executable.

That is what makes this the swiss-army knife its description claims, and it maps directly onto the problem auto-unseal solves. Vault's default seal is a set of Shamir shares held by humans, which is excellent for a laptop and terrible for a Kubernetes cluster where a pod needs to restart at 3am. Auto-unseal moves that trust to a key management service, and the practical consequence is that the choice of cloud now dictates which KMS integration you need. Having all five in one binary removes the usual excuse, which is that the unseal plugin for your provider is a separate project with its own release cadence.

Two dependencies explain the hardware story. `github.com/miekg/pkcs11` is the PKCS#11 binding, which is how you unseal from a real HSM or a smart card rather than from a cloud service. And `github.com/jpillora/backoff` sits next to it, which is the retry primitive you would expect for calls to a network service that might be briefly unavailable.

The rest of the dependency list is a good survey of what this tool does. Cobra and Viper for the command line and configuration. The HashiCorp HCL fork at a vault-specific version, which is for reading Vault's own config format. Prometheus client, so the operator side can export metrics. OAuth2 and testify for the plumbing and the tests.

## Digest-pinned images, xx-verified cross-compiles, and a SmartCard variant

The container build is where you can judge how seriously this project takes its supply chain, and it comes out well.

Every base image is pinned by digest, not just by tag. The cross-compilation helper and the Go builder look like this:

```dockerfile
FROM --platform=$BUILDPLATFORM tonistiigi/xx:1.9.0@sha256:c64defb9ed5a91eacb37f96ccc3d4cd72521c4bd18d5442905b95e2226b0e707 AS xx
FROM --platform=$BUILDPLATFORM golang:1.26-alpine3.22@sha256:727cfc3c40be55cd1bc9a4a059406b28a059857e3be752aa9d09531e12c20c56 AS builder
```

Digest pinning means a rebuild six months from now gets the same bytes, and a tag that moves under you cannot change what you shipped. For a project whose entire job involves handling key material, that is the right default rather than a nice-to-have.

The cross-compilation approach is also unusually thorough. The build sets the target platform from the build argument, wraps the Go toolchain with the helper, and then verifies each produced binary against its target:

```dockerfile
ENV CGO_ENABLED=1
RUN go build -o /usr/local/bin/bank-vaults ./cmd/bank-vaults/
RUN xx-verify /usr/local/bin/bank-vaults
```

Running a verification step per artefact is the part most projects skip, and it catches the class of bug where a binary compiles for an architecture but cannot actually execute there.

`CGO_ENABLED=1` is there because of the PKCS#11 binding, and it has visible consequences in the runtime image, which installs the smart-card stack, and the final image is Alpine at a different version from the builder. There are several image variants built from a common base, and one of them adds SoftHSM, the software HSM used for development. That variant drops to an unprivileged user and initialises a token in the image with a fixed PIN, which is fine for development and is exactly the kind of thing to check you are not shipping to production. The SmartCard support has its own entrypoint script copied in alongside the binaries.

## A Unicode attack detector in a Vault tool

One dependency in that list is worth pulling out, because nobody adds it by accident.

`github.com/dimchansky/utfbom` is a library for detecting the Unicode bidirectional override character in text. Those invisible characters can make source code render differently from how it executes, which is the basis of what is usually called a Trojan Source attack: a change that looks like a harmless reordering of an identifier to a human reviewer and reorders it differently to the compiler.

For a build tool, that is a supply-chain and review-integrity problem rather than a runtime bug. If your CI tooling parses or generates files that humans review in pull requests, an override character in the wrong place can make a diff say one thing and the pipeline do another. There have been real incidents of this class in package registries and CI configurations.

So why is it here? Because this tool sits in the path where your secrets configuration is defined and reviewed. A CLI that applies a declarative Vault configuration is exactly the sort of component where a subtle input-manipulation bug would be most damaging and hardest to spot, and the presence of this dependency says the maintainers thought about that class of attack rather than only about authentication and encryption.

It is a small signal, and one dependency does not make a project secure. But when you are choosing between two tools that both claim to manage your Vault, the one whose dependency list contains an override-character detector has told you something about how its authors read their threat model, and that is worth more than another badge.

## Configuration as a file, applied by the CLI

The mechanism behind the configure half of the description is a file. `vault-config.yml` sits at the repository root and is the shape of the configuration the CLI applies.

That design is the whole argument for using this over ad-hoc commands. Auth methods and secret engines are the state you want to diff, so putting them in a declarative document that the CLI applies makes Vault's configuration reviewable in a pull request and re-appliable after a rebuild. The comparison to Helm charts and Terraform is not accidental: this is the same idea applied to Vault's API surface, and the topics list even includes a Helm chart.

The supporting dependencies say how the parsing is done. Viper handles configuration and environment overlay. A HashiCorp HCL fork at a Vault-pinned version reads Vault's native configuration format, so a document can come from YAML or from HCL. A parser library and a mapstructure decoder sit alongside, which is the standard pairing for decoding loosely typed configuration into typed structures.

One file in the tree tells you this goes further than Vault itself: `ldap_example.ldif` sits at the root. That is a directory server fixture, which means the project ships an example of backing an auth method with LDAP, and you can reproduce the topology locally rather than guessing at the LDAP configuration.

For the Kubernetes side, the sibling projects fill in the rest. The Secrets Webhook injects values into pod specs at admission time, and the Operator handles the lifecycle. If you use the webhook, note that a mutating webhook changes every pod in scope, which is the reason the topics include a Helm chart and why the operator's configuration deserves a review of its own.

## The development Vault is dev mode, and it is bound to loopback

The compose file is a single service, and it is worth reading line by line because it shows both the right instincts and a trap.

```yaml
  vault:
    container_name: bank-vaults-vault
    image: hashicorp/vault:2.0.1
    ports:
      - 127.0.0.1:8200:8200
    cap_add:
      - IPC_LOCK
```

Two things are done properly. The port mapping binds to `127.0.0.1`, not to all interfaces, so a development Vault is not accidentally reachable from the network. And `IPC_LOCK` is the capability Vault needs for memory locking, which is what stops decrypted secrets being written to swap. If you have ever run Vault without it and wondered why it warns about mlock, this is the answer in one line.

The trap is the environment block, which sets both a token variable and a development root token variable to the same value. That is dev mode, and dev mode has two properties that matter. It does not persist, so everything you put in it disappears when the container goes. And it does not seal, so nothing in it is protected by the unseal flow you are presumably reading this article to configure.

So the compose file is a test fixture, not a template. If you copy it into anything resembling a real deployment you get a Vault that is unsealed, unpersisted and protected by a token that is committed to a public repository. The CLI's own test suite runs against exactly this, which is the right use for it.

For running the project itself, the documented development loop is a small set of make targets. Install Go, have Docker with Compose and Buildx available, then:

```shell
make deps
make up
make test
make test-integration
make lint
```

`make up` starts the dependencies, the two test targets run the unit and integration suites, and the linter target takes a `-j` option to run linters in parallel. There are also `make fmt` for the subset of lint findings that can be fixed automatically, and `make artifacts` for a container image plus a release snapshot.

## One manager per Vault, and the drift that two of them cause

The alternative to this CLI is not nothing. It is running Vault by hand, with `vault operator init`, `vault operator unseal` and a sequence of auth and secret-engine commands, which is entirely viable for a single instance that a person administers.

The difference in approach is not capability. Hand-written commands can do anything the CLI does. It is who holds the intent. With a declarative config file, the desired state is a document; with commands, it is a person who remembers. That is the whole trade, and it is why teams that have outgrown hand administration tend to move this way.

The comparison that actually matters, though, is against other declarative managers. Terraform has a Vault provider. Helm has a Vault chart, referenced in the README as a separate configuration chart. An operator manages Vault's own storage. All of them will happily configure the same Vault that this CLI configures, and running two of them against one Vault is how you get a policy that exists on disk in one system and not in the other, discovered during an incident.

So the decision is about ownership. If this CLI is the only thing configuring a given Vault, a file in version control is a clear improvement over shell history. If something else already owns that Vault, adding this as a second configuration path creates a problem that did not exist before, and the CLI does not appear to advertise a mechanism for arbitrating between them.

The unseal half is different and does not compete. There is exactly one unseal configuration per Vault, nothing else is trying to own it, and the multi-cloud KMS integrations are the reason to prefer this implementation over hand-rolling provider-specific seal logic.

## CNCF Sandbox, a NOTICE file, and where to report a vulnerability

Maintenance is in reasonable shape. The project is a CNCF Sandbox project, which is the CNCF's entry stage for new projects and a public signal that it is being watched. The last push was on 2026-09-28.

The release history shows a patch-level cadence rather than feature releases: v1.33.0 and v1.33.1 were both cut on 2026-05-25, two hours apart, and v1.33.2 followed on 2026-08-30. Two patch releases minutes or hours apart is what a bug fix and its follow-up look like, which is a healthy sign for a tool you would rather not have to babysit.

The licence is Apache 2.0, recorded in LICENSE, with a NOTICE file beside it as the licence requires. The repository also carries `.licensei.toml`, which is a dependency licence-compliance checker, and given that the dependency list spans five cloud providers, that is not a formality. There is a `.hadolint.yaml` for the Dockerfiles, `.yamllint.yaml`, `.golangci.yml` and a `.go-version` file, so the Go toolchain, the shell linting, the Go linting and the image linting are all pinned or configured rather than left to defaults.

The governance files are MAINTAINERS.md and ADOPTERS.md, and the top-level file list does not include a root-level security policy file. For a project in this category that is the one gap worth closing out: if you are going to run a tool that handles unsealing keys and secret configuration, you want to know where to send a vulnerability report before you install it, and the place to look is the `.github/` directory and the MAINTAINERS file rather than a policy document.

The build pipeline is also worth one sentence, since it shapes what you get as a consumer. Releases are produced by goreleaser running inside a container with the Docker socket mounted, at a pinned goreleaser-cross version, with a snapshot mode for local builds. That is a common and reasonable pattern for cross-platform Go releases, and the pinning means the release tooling is reproducible too.

## Conclusion

Adopt bank-vaults if you run Vault on Kubernetes and want both auto-unseal and a declarative config file you can review, and check the cloud KMS integrations and the PKCS#11 path against your actual hardware before committing. Do not adopt it as the only way to configure Vault if Terraform or another operator already manages the same instance, because two managers of one Vault is where drift begins. Verify that the images you pull are still digest-pinned and cross-compile-verified, and decide whether you want the default image running as root or the SoftHSM variant that drops to an unprivileged user.

## FAQ

### What does the bank-vaults CLI actually do?

Three things, according to its own description: it initialises a HashiCorp Vault, it unseals a Vault, and it configures auth methods and secret engines. Configuration is applied from a declarative file such as vault-config.yml, so the intent can be reviewed in version control instead of remembered.

### Which clouds can bank-vaults auto-unseal Vault on?

The direct dependencies in go.mod cover AWS KMS, Azure Key Vault, Google Cloud, Alibaba Cloud and Oracle OCI, so a single binary can reach a key management service on any of them. Unsealing from hardware is also supported through a PKCS#11 binding, which is how an HSM or smart card is used instead of a cloud service.

### What is the relationship between bank-vaults and the other Bank-Vaults projects?

It is an umbrella project. This repository holds the CLI for configuring Vault; the Vault Operator is a separate repository for running Vault on Kubernetes; a Secrets Webhook injects secrets into pods; and a Vault SDK makes Vault easier to use from Go. The CLI is the least coupled of the four because it is a binary you run rather than a controller you deploy.

### How do I run the bank-vaults development environment?

Install Go, have Docker with Compose and Buildx available, then run `make deps` and `make up` to start the dependencies. The compose file runs a single Vault service on 127.0.0.1:8200 with the IPC_LOCK capability. `make test` and `make test-integration` run the suites, and `make lint` runs the linters with a `-j` option for parallel execution.

### Is the bank-vaults container image safe to run in production?

The build pins every base image by digest and verifies each cross-compiled binary against its target with xx-verify. It also ships several variants, one of which installs SoftHSM and initialises a token with a fixed PIN for development, while the default image does not set a non-root user. Check which variant you are pulling before deploying it.

### Can bank-vaults be used alongside Terraform or Helm for the same Vault?

It can run, and the configuration is declarative, but the two would both be trying to own the same auth methods and secret engines, which is how configuration drifts between systems. Prefer one manager per Vault. The unseal configuration is different, since nothing else competes for it.

## Sources

- [bank-vaults/bank-vaults on GitHub](https://github.com/bank-vaults/bank-vaults)
- [License: Apache-2.0](https://github.com/bank-vaults/bank-vaults/blob/main/LICENSE)
- [Project website](https://bank-vaults.dev)
- [README](https://github.com/bank-vaults/bank-vaults/blob/main/README.md)
- [Releases](https://github.com/bank-vaults/bank-vaults/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/bank-vaults-bank-vaults
