Self-hosted secrets vault with AWS-style IAM for Docker, classic SSH deployments, Docker Swarm, k3s and Kubernetes, built on OpenBao.
Status: alpha. The daemon, the operator CLI, the host agent and the
swarm,dockerandsshtargets work end to end in tests against real OpenBao, Docker, systemd-creds and OpenSSH. They have not run in production yet: every release first spends a week on a staging host.k3sandk8sare designed but on standby. See the roadmap.
hashvault is a small control plane that becomes the single source of truth for production secrets, and the only way a human or a machine reads, writes, rotates or publishes them. It publishes each secret to where your workloads run: a plain Docker host, a server you deploy to over SSH, a Swarm, a k3s or a Kubernetes cluster. Your services keep running even while the vault is sealed.
Running a few services on Docker Swarm or a small cluster, secrets tend to end up scattered:
- Swarm secrets are immutable and unreadable. Rotating one means a new name
(
db_password_v2) and a coordinated stack update, by hand. - Out-of-band changes get reverted. A
docker service update --secret-addthat is not mirrored in the stack file disappears at the next deploy. - A typo is a silent outage. When a secret's target doubles as an environment variable name, a misspelled target breaks nothing at boot and one feature in production, for weeks.
- Operator passwords have no home. They live in a
0600file on a laptop.
Full-featured vaults solve storage, but not publication to a Swarm, a plain Docker host or an SSH-deployed server, and their policy languages have no conditions on MFA age, source channel or approvals. hashvault adds exactly that layer on top of OpenBao, and nothing else.
- Envelope encryption: one data key per secret version, XChaCha20-Poly1305, ciphertexts bound to their resource, environment and version. Key encryption keys live in OpenBao Transit and can move to an HSM or KMS.
- AWS-style IAM: JSON policies, explicit Deny wins, permission boundaries, conditions on MFA age, source channel, approvals, target and time. Closed catalogs turn typos into parse errors. A simulator explains every decision.
- Push publishing to five targets: plain Docker (
docker run, Compose), classic SSH deployments (.env, systemd, PM2, supervisor, PHP-FPM), Docker Swarm (directly, or as the secrets backend of a deployment tool), k3s and Kubernetes. Optional pull mode (systemd credential socket, Secrets Store CSI provider). - Sealed by default: hardware-key unseal (YubiKey through
age-plugin-yubikey), automatic re-seal after inactivity, no unseal secret on disk. - Tamper-evident audit: hash-chained log, signed checkpoints, off-host anchors.
- No network exposure: unix sockets only, operators come in through an SSH tunnel, remote targets through an outbound agent.
+--------------------+ +--------------------+ +--------------------+
| hashvault CLI | | deployment helper | | web UI (later) |
| (SSH tunnel) | | (signed plans) | | (SSH tunnel) |
+---------+----------+ +---------+----------+ +---------+----------+
| | |
+------------------------+------------------------+
| unix socket
v
+----------------------------------+-----------------------------------+
| hashvaultd |
| authn : FIDO2 SSH keys, signed plans, OIDC (CI) |
| iam : evaluator, simulator, approvals |
| crypto: hv1 envelope, one DEK per version |
| audit : hash chain, signed checkpoints |
| targets: docker, ssh, swarm, k3s, k8s |
+----------------------------------+-----------------------------------+
| unix socket, token in memory only
v
+----------------------------------+-----------------------------------+
| OpenBao: KV v2 (envelopes), Transit (KEK), Shamir seal, Raft |
+----------------------------------------------------------------------+
Workloads read a copy that was already published (Swarm Raft, encrypted credential store on the host, Kubernetes Secret). They never call hashvault, so sealing the vault blocks publications, not production.
Target (hv:TargetKind) |
What it is | Mechanism | Works while sealed |
|---|---|---|---|
docker |
docker run or Docker Compose, no Swarm |
systemd-creds store, materialised to tmpfs at boot, bind-mounted to /run/secrets |
yes, across reboots |
ssh |
Classic deployment over SSH (rsync, git pull) run by systemd, PM2, supervisor or PHP-FPM | Encrypted store on the host, then a .env on tmpfs, LoadCredentialEncrypted=, or hashvault-agent exec |
yes, across reboots |
swarm |
Docker Swarm | Secret objects named by value digest, rolled by the deployer | yes |
k3s |
k3s cluster | Immutable Secret objects named by digest, rollout on change; refused unless k3s secrets-encrypt is enabled |
yes (push mode) |
k8s |
Any other Kubernetes (vanilla, EKS, GKE, AKS...) | Same, refused without an EncryptionConfiguration (KMS v2 preferred) |
yes (push mode) |
ssh -N -L /tmp/hv.sock:/run/hashvault/api.sock ops@vault & # the API is a unix socket
export HASHVAULT_SOCKET=/tmp/hv.sock
hashvault unseal --bundle unseal-bundle.age --identity yubikey.txt # PIN, touch
hashvault login --principal human/alice --key ~/.ssh/id_ed25519_sk # touch
hashvault secret put prod/acme-api/DATABASE_URL < db-url.txt
hashvault import dotenv .env --env prod --app legacy --dry-run
hashvault iam simulate --principal machine/deployer \
--action secret:Publish --resource hv:prod:secret/acme-api/DATABASE_URL
hashvault publish --target swarm-main --manifest hashvault.json
hashvault audit verifyInstall, bootstrap ceremony and unseal: docs/operations.md.
Host agent for docker and ssh targets: docs/host-agent.md.
Read docs/threat-model.md,
docs/crypto-spec.md and
docs/iam-spec.md. In short:
- only standard, audited primitives (
golang.org/x/crypto, OpenBao Transit, age); no home-made cipher; - every security claim has a test, a property test or a fuzz target behind it;
- root on a host that runs your workloads can read what is published there. hashvault narrows exposure windows; it does not defeat a malicious root.
Report vulnerabilities privately: see SECURITY.md.
| hashvault | OpenBao / Vault | Infisical | SOPS + age | Swarm secrets | |
|---|---|---|---|---|---|
| Encrypted storage | via OpenBao | yes | yes | files in Git | Raft |
| Conditional IAM (MFA age, channel, approvals) | yes | path ACLs | roles | no | no |
| Publishes to Docker, SSH hosts, Swarm | yes | no | partial | no | n/a |
| Runs while the vault is sealed | yes | n/a | n/a | yes | yes |
| Moving parts | 2 daemons | 1 | app + Postgres + Redis | none | none |
hashvault does not do dynamic database credentials, PKI, or SaaS synchronisation. Use OpenBao directly for those.
| Phase | Content | Status |
|---|---|---|
| P0 | Envelope format, IAM engine, specs, CI | done |
| P1 | Daemon, CLI, Swarm target, staging on a Proxmox LXC | code done, staging next |
| P2 | Plain Docker and classic SSH deployments (host agent) | code done, staging next |
| P3 | Web UI through the tunnel, OIDC for humans, docs site | planned |
| P4 | k3s and k8s, CSI provider, CI OIDC | planned |
Every release runs for at least 7 days on a staging LXC, with recovery drills, before it reaches a production host.
Requires Go 1.26 or later (the module pins its toolchain).
make help # every target, by section
make openbao # pinned OpenBao binary for integration tests (checksum verified)
make test # unit, property and integration tests, race detector
make cover # every test, integration required, per-package thresholds
make lint # gofmt, go vet, staticcheck, gosec
make fuzz # every fuzz target, FUZZTIME=30s each
make ci # everything CI runsSee CONTRIBUTING.md.
Apache-2.0. OpenBao is a separate project under MPL-2.0; hashvault talks to it over its API and does not include its code.