Real-world quality, end-to-end runnable HashiCorp Vault OSS (Raft) cluster with Nginx front door, a reusable .NET client package, and an ASP.NET Core WebAPI sample.
- .NET SDK 9.x (preview acceptable for now)
- Docker + Docker Compose
+-------------------+
| Sample.WebApi |
| Company.VaultClient
+---------+---------+
|
| https://localhost:8443
v
+-------+--------+
| Nginx (TLS) |
| reverse proxy |
+-------+--------+
|
+---------------+----------------+
| internal Docker net |
v v v
+----+----+ +---+----+ +----+----+
| Vault 1 |<---->| Vault 2|<----->| Vault 3 |
| Raft | | Raft | | Raft |
+----+----+ +---+----+ +----+----+
|
v
+-----------+
| Postgres* |
+-----------+
* Optional dynamic DB creds demo via Vault database engine
cd infra/compose
cp .env.example .env
bash ../vault/scripts/00-generate-certs.sh
docker compose up -d
bash ../vault/scripts/10-bootstrap-vault.sh
bash ../vault/scripts/20-smoke-test.sh
cd ../../
dotnet test
dotnet run --project src/Sample.WebApiBootstrap writes local-only credentials to infra/compose/.secrets/approle.json (gitignored). Export to env:
export VAULT_ROLE_ID=$(python3 - <<'PY'
import json
print(json.load(open('infra/compose/.secrets/approle.json'))['role_id'])
PY
)
export VAULT_SECRET_ID=$(python3 - <<'PY'
import json
print(json.load(open('infra/compose/.secrets/approle.json'))['secret_id'])
PY
)GET /api/secrets/app-configPOST /api/secrets/app-configGET /api/crypto/encrypt?text=...GET /api/crypto/decrypt?cipher=...GET /api/db/whoami(requires Postgres and database engine enabled)GET /health
Database defaults:
POSTGRES_HOST=localhostPOSTGRES_PORT=5432POSTGRES_DB=appdb
10-bootstrap-vault.shrunsvault operator initwith a single Shamir key.- Unseal key + root token are stored in
infra/compose/.secrets/vault-init.json(gitignored). - This is LOCAL DEMO ONLY. Never store unseal keys in git or on shared machines.
For production, remove Shamir keys and configure auto-unseal with a cloud KMS:
- AWS KMS: set
seal "awskms"in Vault config and attach IAM policy for decrypt/encrypt. - Azure Key Vault: set
seal "azurekeyvault"with AAD credentials. - GCP KMS: set
seal "gcpckms"with service account key.
After enabling auto-unseal, the bootstrap script should only initialize and skip manual unseal.
- TLS enabled for Vault and Nginx; self-signed certs for local dev.
- AppRole auth with least-privilege policy (
infra/vault/policies/webapi.hcl). - Audit device enabled to file volume for immutable audit trail.
- Tokens are renewed automatically; no secret values are logged.
- Nginx is the only exposed entry point; Vault API stays on internal Docker network.
- Upstream includes all three Vault nodes.
proxy_next_upstreamretries on 307/308 redirects to avoid leaking internal node URLs to clients.- Headers
X-Vault-RequestandX-Vault-Tokenare forwarded.
- 503 / standby: Leader changed; retry, or check
docker logs vault-1andvault operator raft list-peers. - TLS errors: Run
bash infra/vault/scripts/00-generate-certs.shand ensureAllowInsecureDevTls=truelocally. - Clock skew: Vault tokens are time sensitive; ensure host time is synced.
- Permission denied: Validate policies in
infra/vault/policies/webapi.hcland AppRole binding.
infra/composeDocker Compose, .env exampleinfra/vaultVault configs, policies, scriptsinfra/nginxNginx config and certssrc/Company.VaultClientNuGet packagesrc/Sample.WebApiASP.NET Core APItestsUnit + integration tests
GitHub Actions runs:
dotnet restore/build/test- Docker compose + bootstrap + smoke test
dotnet packfor the NuGet package
From repo root:
cd infra/compose
cp .env.example .env
bash ../vault/scripts/00-generate-certs.sh
docker compose up -d
bash ../vault/scripts/10-bootstrap-vault.sh
bash ../vault/scripts/20-smoke-test.sh