Skip to content

Repository files navigation

HyperionDB Vault (hyperion-vault)

A self-contained, replicated secrets vault: a single Rust binary that stores secrets encrypted at rest on local disk and exposes a REST API to create, read, update, delete, verify, and automatically rotate them.

Secrets are sealed with envelope encryption — a per-secret data key (DEK) protected by AWS KMS, with the secret bytes sealed under XChaCha20-Poly1305. Only ciphertext, the wrapped DEK, and a nonce are ever written to disk; plaintext never lands on disk.

Storage is an embedded redb key-value store (one file per node). High availability comes from openraft: the vault runs as a Raft cluster, writes go through consensus to the leader and replicate to a quorum, and reads are served locally from any node. There is no external database to operate.

Comparable to supabase/vault and OpenBao (KMS-backed envelope encryption + API + rotation) — this project packages that model as a small, dependency-free, self-replicating cluster.

Status: in production


Features

  • Encryption at rest, KMS-backed. Envelope encryption: AWS KMS wraps a 256-bit data key (via GenerateDataKey/Decrypt); the secret is sealed with XChaCha20-Poly1305. Disk stores only {wrapped_dek, nonce, ciphertext, aad}.
  • Embedded storage (redb). Each node owns a single redb file — no Postgres, no external services. The Raft log, snapshots, and the secret state all live in that one embedded store.
  • Raft replication & failover (openraft). Writes are committed through Raft to the leader and replicated to a quorum; leader election provides automatic failover. Reads are served from the local node (optionally linearizable through the leader via VAULT_READ_CONSISTENCY=linearizable).
  • Two secret kinds.
    • manual — set and changed explicitly through the API.
    • automatic — rotated on a schedule by a background worker (running only on the leader). Old versions stay valid for a configurable grace period.
  • REST API. POST/GET/PUT/DELETE /v1/secrets, plus /rotate and a constant-time /verify endpoint for grace-window validation.
  • IP allowlist for reads. Secret reads are permitted only from an IPv4/CIDR allowlist (VAULT_ALLOWED_IPS). Fail-closed: an empty allowlist denies all reads.
  • Role-based access control (RBAC). create/update/delete/rotate and role/token administration require a bearer token. Each token maps to a role whose rules are action × secret-path glob (e.g. a payment role scoped to stripe/*), with a built-in admin superuser role. Tokens are stored only as SHA-256 fingerprints and checked in constant time.
  • Brute-force lockout. Auth/authz failures are counted per client IP (per node); after VAULT_AUTH_MAX_FAILURES the IP is locked out (HTTP 429) for VAULT_AUTH_LOCKOUT_SECS.
  • KMS-outage resilience. Unwrapped data keys are cached in memory for VAULT_DEK_CACHE_TTL_SECS, and KMS calls are retried with exponential backoff up to VAULT_KMS_MAX_RETRIES.
  • KMS key rotation. Automatic CMK rotation is transparent (each version records the key that wrapped it). When VAULT_KMS_REWRAP_ENABLED is on, a leader-gated worker also detects rotations (kms:ListKeyRotations) and re-wraps every live version's data key onto the new key material via kms:ReEncrypt — secret bytes are never re-encrypted and the DEK never leaves KMS. Force/inspect with POST/GET /v1/admin/kms/rewrap.
  • Audit log. Every operation is recorded (actor, client IP, action, outcome) in a local audit_log table per node.
  • Backup & restore. Admin-only GET /v1/backup exports a consistent, point-in-time snapshot of every data table (secrets, versions, roles, tokens, audit); secret values stay encrypted — only sealed records leave the store, never plaintext. POST /v1/restore rebuilds a node from such a snapshot. No external tooling, cron, or background worker — just two API calls.
  • Defense in depth. Application-enforced access invariants, version-bound AEAD associated data (a ciphertext can't be replayed under another secret name or version), and zeroized key material in memory.

Workspace layout

Crate / dir Kind Purpose
crates/hyperion-vault-core lib Pure-Rust security core: AEAD envelope, IP allowlist, token auth, rotation policy. No storage, no network.
crates/hyperion-vault-api bin The REST API service (axum): handlers, IP/token guards, the redb store (store/), the Raft layer (raft/), KMS, rotation worker.
crates/hyperion-vault lib Umbrella crate re-exporting the security core.
docs/ Architecture, decisions, threat model, API and security docs.
docker/ Single-binary node image + N-node Raft cluster compose + WireGuard overlay + e2e overlay.

The storage layer is built around a ports & adapters seam: the service layer depends only on the VaultReader / VaultWriter traits (store/ports.rs). The single-node RedbStore and the Raft-backed RaftStore are interchangeable adapters; both funnel every mutation through one deterministic apply_command. See docs/DECISIONS.md.


Architecture

          ┌─ vault1 ─┐   ┌─ vault2 ─┐   ┌─ vault3 ─┐
client ──>│  API     │   │  API     │   │  API     │   reads: local redb
          │  redb    │<=>│  redb    │<=>│  redb    │   writes: Raft → leader
          └──────────┘   └──────────┘   └──────────┘            → quorum → apply
                    openraft consensus (HTTP RPC)
  • The leader is the equivalent of a primary: it accepts writes, replicates the log to followers, and runs the rotation worker. A follower transparently forwards writes to the current leader.
  • Each node persists everything to its own redb file (VAULT_DB_PATH).
  • Nodes find each other through VAULT_PEERS (an id=host:port cluster map, identical on every node); a node's own identity is NODE_ID.

Test

cargo test --workspace

# fuzz tests (nightly; see fuzz/README.md)
cargo +nightly fuzz run crypto_roundtrip -- -max_total_time=60
cargo +nightly fuzz run --features api raft_rpc_decode -- -max_total_time=60

make fuzz-build          # check every target still compiles
make fuzz BUDGET=60      # sweep all 22 targets, 60s each

Quick start (local dev, no AWS)

# Bring up a 3-node Raft cluster (KMS in local dev mode)
# BE SURE TO CHANGE DEFAULT VAULT_LOCAL_MASTER_KEY and remove it from production .env after creating your secrets!
cd docker && cp .env.example .env && docker compose up --build

APIs listen on localhost:8200 (vault1), :8201 (vault2), :8202 (vault3); Raft RPC runs on the internal :7400 of each node. See docs/API.md for full request/response examples.

# Mint a first admin token by setting VAULT_BOOTSTRAP_TOKEN on the nodes,
# then use it (the dev compose seeds 'dev-admin-token-change-me' via the e2e overlay):
TOKEN=dev-admin-token-change-me

# Create a manual secret
curl -sS -X POST localhost:8200/v1/secrets \
  -H "Authorization: Bearer $TOKEN" \
  -H 'content-type: application/json' \
  -d '{"name":"db/password","kind":"manual","value":"s3cr3t"}'

# Read it back (must come from an allowlisted IP)
curl -sS localhost:8200/v1/secrets/db/password

# Create an auto-rotating secret with a 24h interval and 1h grace
curl -sS -X POST localhost:8200/v1/secrets \
  -H "Authorization: Bearer $TOKEN" \
  -H 'content-type: application/json' \
  -d '{"name":"svc/api-key","kind":"automatic","rotation_interval_secs":86400,"grace_period_secs":3600}'

Required KMS permissions

{
	"Version": "2012-10-17",
	"Statement": [
		{
			"Effect": "Allow",
			"Action": [
				"kms:GenerateDataKey",
				"kms:Encrypt",
				"kms:Decrypt",
				"kms:DescribeKey",
				"kms:ListKeyRotations",
				"kms:ReEncryptFrom",
				"kms:ReEncryptTo"
			],
			"Resource": "arn:aws:kms:[region]:[account-id]:key/[key-id]"
		}
	]
}

Configuration (environment)

Variable Default Description
NODE_ID 1 This node's id (Raft node id + audit label). The only value that differs per node.
VAULT_PEERS (empty) Cluster map id=host:port,… of the Raft addresses, identical on every node. One entry (or empty) ⇒ single-node, Raft disabled; two or more ⇒ replicated cluster.
VAULT_RAFT_BIND 0.0.0.0 Address the node-to-node port binds to. Set it to this node's private address so the port is not reachable from any other interface.
VAULT_CLUSTER_TOKEN Shared credential sent on every node-to-node request. Same value on every node.
VAULT_CLUSTER_AUTH_REQUIRED false Refuse node-to-node requests that do not carry VAULT_CLUSTER_TOKEN. Set the token on every node first, then turn this on everywhere; the other order splits the cluster!
VAULT_RAFT_PEER_CHECK false Refuse node-to-node connections whose source address is not one of the addresses in VAULT_PEERS. Leave it off if peers reach each other through address translation.
VAULT_API_PORT 8200 Public REST API bind port (0.0.0.0:PORT). The external address is whatever your load balancer / port mapping exposes.
VAULT_DB_PATH vault.redb Path to this node's redb file.
VAULT_BOOTSTRAP_TOKEN If set, seeds a bootstrap-admin token (mapped to the built-in admin role) on startup. Set the same value on every node for a fresh cluster, then rotate it.
VAULT_ALLOWED_IPS (empty → deny all reads) Comma-separated IPv4 / CIDR allowed to read secrets.
VAULT_TRUST_PROXY false Trust X-Forwarded-For for the client IP (only behind a trusted proxy).
VAULT_READ_CONSISTENCY local local (read from this node) or linearizable (route reads through the leader).
VAULT_KMS_MODE aws aws (production) or local (dev only).
VAULT_KMS_KEY_ID AWS KMS key id/ARN (required for aws).
VAULT_LOCAL_MASTER_KEY base64 32-byte master key for local mode.
VAULT_ROTATION_POLL_SECS 15 How often the leader scans for due rotations. rotation_interval_secs and grace_period_secs on a secret are capped at 100 years.
VAULT_PG_TARGET_HOSTS (empty → the hosts already used by stored pg_replica targets) Comma-separated host:port list of the Postgres endpoints a rotation target is allowed to connect to. A target naming any other host is rejected at create time and refused again at rotation time. When unset, the node seeds the list at startup from the targets already in its database, so an upgrade keeps rotating the secrets it already had; set it explicitly to stop depending on that.
VAULT_PG_TARGET_TLS prefer verify-full = require TLS and verify the server certificate chain and hostname; prefer = use TLS when the Postgres server offers it, plaintext when it does not; disable = always plaintext.
VAULT_PG_TARGET_CA_FILE (system trust store) PEM file of CA certificates used to verify the Postgres server certificate.
VAULT_DEK_CACHE_TTL_SECS 300 TTL of the in-memory decrypted-DEK cache. 0 disables.
VAULT_KMS_MAX_RETRIES 5 Retry KMS calls up to N times with exponential backoff. 0 disables.
VAULT_KMS_REWRAP_ENABLED false Re-wrap data keys onto new CMK material after a KMS key rotation. Enable only after every node runs a build that understands it.
VAULT_KMS_REWRAP_POLL_SECS 86400 How often the leader polls kms:ListKeyRotations for a new rotation.
VAULT_KMS_REWRAP_MAX_PER_SEC 10 Re-wrap pacing (KMS ReEncrypt + Raft writes per second). 0 disables pacing.
VAULT_AUTH_MAX_FAILURES 5 Failed attempts before lockout, counted per sending address for auth/authz failures and per sending address and secret name for /verify mismatches. The address is always the TCP peer, never X-Forwarded-For. 0 disables.
VAULT_AUTH_LOCKOUT_SECS 900 How long a locked-out IP stays locked.
VAULT_AUTH_WINDOW_SECS 300 Window over which failures accumulate.
VAULT_TOKEN_TOUCH_SECS 300 Shortest gap between writes of a token's last_used_at. On a cluster each write goes through Raft, so this bounds that cost; 0 writes on every authenticated request.

For AWS mode, AWS credentials and region come from the standard AWS SDK environment (AWS_REGION, AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, or an instance role) — not from VAULT_* variables. The key's IAM policy needs only kms:GenerateDataKey and kms:Decrypt (add kms:ListKeyRotations and kms:ReEncryptFrom/kms:ReEncryptTo when VAULT_KMS_REWRAP_ENABLED=true).


Security

This is security-critical software. Read docs/SECURITY.md and docs/THREAT_MODEL.md before deploying.

Docs

License

GPL-3.0-or-later. See LICENCE.

About

Dead simple and secure Raft replicated secrets vault

Topics

Resources

Security policy

Stars

3 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages