diff --git a/deploy-map.toml b/deploy-map.toml index 7f19005..95e1b86 100644 --- a/deploy-map.toml +++ b/deploy-map.toml @@ -5,6 +5,7 @@ version = 1 "web-frontend" = { deploy_mode = "dokploy_webhook", server = "main" } "manager-frontend" = { deploy_mode = "dokploy_webhook", server = "main" } "hatchet-worker" = { deploy_mode = "dokploy_webhook", server = "main" } +"vault" = { deploy_mode = "dokploy_webhook", server = "main" } [servers] "main" = { tailscale_user = "root", tailscale_name = "main-prod" } diff --git a/vault/.dockerignore b/vault/.dockerignore new file mode 100644 index 0000000..ed26872 --- /dev/null +++ b/vault/.dockerignore @@ -0,0 +1,2 @@ +backups/ +*.json diff --git a/vault/Dockerfile b/vault/Dockerfile new file mode 100644 index 0000000..51d5757 --- /dev/null +++ b/vault/Dockerfile @@ -0,0 +1,11 @@ +FROM hashicorp/vault:1.21.3 + +COPY config /vault/config +COPY entrypoint.sh /vault/entrypoint.sh +RUN chmod +x /vault/entrypoint.sh + +EXPOSE 8200 8201 + +HEALTHCHECK --interval=30s --timeout=5s --start-period=30s --retries=3 CMD VAULT_ADDR=http://127.0.0.1:8200 vault status >/dev/null 2>&1 || exit 1 + +ENTRYPOINT ["/vault/entrypoint.sh"] diff --git a/vault/README.md b/vault/README.md new file mode 100644 index 0000000..ce646de --- /dev/null +++ b/vault/README.md @@ -0,0 +1,85 @@ +# Vault Setup (Dokploy) + +This folder is intended for Dokploy deployment via `Dockerfile` (not docker-compose). + +## Runtime + +Container image uses `vault/config/vault.hcl` and starts through `/vault/entrypoint.sh`. + +Required runtime settings in Dokploy: + +- add capability: `IPC_LOCK` +- mount persistent volume to `/vault/data` (mandatory) +- expose port `8200` (API) +- optionally expose `8201` (cluster) +- service update strategy: `stop-first` (important for single-node raft volume) + +## Auto-Unseal Strategy In This Project + +This project uses Shamir + startup unseal in entrypoint: + +- Vault starts normally with Shamir seal +- if Vault is sealed, entrypoint runs `vault operator unseal` +- unseal key is taken from environment variable + +Supported env vars: + +- `VAULT_UNSEAL_KEY` for single key +- `VAULT_UNSEAL_KEYS` for multiple keys (comma/space separated) + +If Vault is sealed and these vars are empty, container exits with error. + +## Recommended Init For This Flow + +To make restart-unseal automatic with one env var, initialize Vault with: + +```bash +vault operator init -key-shares=1 -key-threshold=1 +``` + +Then put generated unseal key into Dokploy env: + +```bash +VAULT_UNSEAL_KEY= +``` + +If Vault was initialized with threshold > 1, set all required keys in: + +```bash +VAULT_UNSEAL_KEYS=,, +``` + +## Quick Checks + +```bash +export VAULT_ADDR=http://127.0.0.1:8200 +vault status +``` + +If it is sealed after restart: + +- verify `VAULT_UNSEAL_KEY` / `VAULT_UNSEAL_KEYS` exists in Dokploy +- verify persistent volume is still mounted to `/vault/data` +- verify container logs from `/vault/entrypoint.sh` + +## Native Vault Auto-Unseal (Optional) + +If later you want native Vault auto-unseal (without storing unseal key in env), use seal providers: + +- [AWS KMS seal](https://developer.hashicorp.com/vault/docs/configuration/seal/awskms) +- [GCP KMS seal](https://developer.hashicorp.com/vault/docs/configuration/seal/gcpckms) +- [Azure Key Vault seal](https://developer.hashicorp.com/vault/docs/configuration/seal/azurekeyvault) +- [Transit seal](https://developer.hashicorp.com/vault/docs/configuration/seal/transit) + +## KV Layout + +Vault stores environment variables in KV v2 under: + +- `secret/shared/` +- `secret/projects//` + +Examples: + +- `secret/shared/prod` +- `secret/projects/web-backend/prod` +- `secret/projects/brand-center-backend/staging` diff --git a/vault/config/vault.hcl b/vault/config/vault.hcl new file mode 100644 index 0000000..654ea92 --- /dev/null +++ b/vault/config/vault.hcl @@ -0,0 +1,16 @@ +ui = true +disable_mlock = true + +storage "raft" { + path = "/vault/data" + node_id = "vault-1" +} + +listener "tcp" { + address = "0.0.0.0:8200" + cluster_address = "0.0.0.0:8201" + tls_disable = 1 +} + +cluster_addr = "http://127.0.0.1:8201" +api_addr = "http://0.0.0.0:8200" diff --git a/vault/entrypoint.sh b/vault/entrypoint.sh new file mode 100755 index 0000000..11181ab --- /dev/null +++ b/vault/entrypoint.sh @@ -0,0 +1,67 @@ +#!/bin/sh +set -e + +export VAULT_ADDR="http://127.0.0.1:8200" + +get_status_json() { + vault status -format=json 2>/dev/null || true +} + +read_flag() { + json="$1" + key="$2" + printf '%s' "$json" | tr -d '\n' | sed -n "s/.*\"$key\":[[:space:]]*\\(true\\|false\\).*/\\1/p" | head -n1 +} + +vault server -config=/vault/config/vault.hcl & +VAULT_PID=$! + +echo "Waiting for Vault status endpoint..." +while true; do + STATUS_JSON="$(get_status_json)" + INIT_FLAG="$(read_flag "$STATUS_JSON" initialized)" + SEALED_FLAG="$(read_flag "$STATUS_JSON" sealed)" + if [ -n "$INIT_FLAG" ] && [ -n "$SEALED_FLAG" ]; then + break + fi + sleep 1 +done + +if [ "$INIT_FLAG" = "false" ]; then + echo "Vault is not initialized yet; auto-unseal skipped." + wait $VAULT_PID + exit $? +fi + +if [ "$SEALED_FLAG" = "true" ]; then + UNSEAL_KEYS_RAW="${VAULT_UNSEAL_KEYS:-${VAULT_UNSEAL_KEY:-}}" + if [ -z "$UNSEAL_KEYS_RAW" ]; then + echo "Vault is sealed but VAULT_UNSEAL_KEY/VAULT_UNSEAL_KEYS is empty." + kill $VAULT_PID || true + exit 1 + fi + + echo "Vault is sealed; applying unseal keys from environment..." + for key in $(printf '%s' "$UNSEAL_KEYS_RAW" | tr ',;' ' '); do + [ -n "$key" ] || continue + vault operator unseal "$key" >/dev/null + STATUS_JSON="$(get_status_json)" + SEALED_FLAG="$(read_flag "$STATUS_JSON" sealed)" + if [ "$SEALED_FLAG" = "false" ]; then + echo "Vault unsealed." + break + fi + done + + STATUS_JSON="$(get_status_json)" + SEALED_FLAG="$(read_flag "$STATUS_JSON" sealed)" + if [ "$SEALED_FLAG" != "false" ]; then + echo "Vault is still sealed after provided key(s)." + kill $VAULT_PID || true + exit 1 + fi +else + echo "Vault is already unsealed." +fi + +wait $VAULT_PID