# πŸ” Vaultwarden API > **Stop using `.env` files.** Fetch secrets directly from your self-hosted [Vaultwarden](https://github.com/dani-garcia/vaultwarden) instance at runtime. A lightweight, production-ready Go API that acts as a secrets bridge between your apps and Vaultwarden. No more scattered `.env` files, no more accidentally committed credentials. ## ✨ Highlights - **πŸš€ Zero external dependencies** β€” Pure Go binary, no Node.js, no Bitwarden CLI - **πŸ”’ Native Bitwarden crypto** β€” AES-256-CBC + HMAC-SHA256, PBKDF2/Argon2id key derivation - **♻️ Auto token refresh** β€” Never worry about expired sessions again - **πŸ“¦ ~15MB Docker image** β€” Alpine-based, runs as non-root - **πŸ›‘οΈ Defense in depth** β€” API key auth, IP whitelisting, rate limiting, security headers - **⚑ Background vault sync** β€” Secrets always up-to-date (configurable interval) - **🏭 Production-ready** β€” Health checks, graceful shutdown, structured logging ## Architecture ``` β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” HTTPS + API Key β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” Native Go β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”‚ Your App β”‚ ────────────────────> β”‚ Vaultwarden API β”‚ ──────────────> β”‚ Vaultwarden β”‚ β”‚ (any lang) β”‚ <──────────────────── β”‚ (Go, ~15MB image) β”‚ <────────────── β”‚ Server β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ JSON response β”‚ β”‚ Encrypted API β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚ β€’ Auto token refreshβ”‚ β”‚ β€’ Background sync β”‚ β”‚ β€’ In-memory cache β”‚ β”‚ β€’ IP whitelisting β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ ``` **How it works under the hood:** 1. Authenticates with Vaultwarden using your master password (same as the web vault) 2. Derives encryption keys using PBKDF2-SHA256 or Argon2id (server-negotiated) 3. Decrypts your vault items in-memory using AES-256-CBC 4. Serves secrets via a simple REST API with API key authentication 5. Periodically re-syncs the vault and auto-refreshes auth tokens ## Quick Start ### 1. Pull and run with Docker ```bash docker run -d \ --name vaultwarden-api \ -p 8080:8080 \ -e VAULTWARDEN_URL=https://vault.yourdomain.com \ -e VAULTWARDEN_EMAIL=you@example.com \ -e VAULTWARDEN_PASSWORD=your-master-password \ -e API_KEY=$(openssl rand -base64 32) \ ghcr.io/turbootzz/vaultwarden-api:latest ``` ### 2. Fetch a secret ```bash curl -H "Authorization: Bearer YOUR_API_KEY" \ http://localhost:8080/secret/DATABASE_URL ``` ```json { "name": "DATABASE_URL", "value": "postgresql://user:pass@db:5432/myapp" } ``` That's it. Your app reads secrets from the API instead of `.env` files. ## API Endpoints | Method | Path | Auth | Description | |--------|------|------|-------------| | `GET` | `/health` | No | Health check | | `GET` | `/secret/:name` | API Key | Fetch a secret by name | | `POST` | `/refresh` | API Key | Force vault re-sync | ## Configuration | Variable | Required | Default | Description | |----------|----------|---------|-------------| | `VAULTWARDEN_URL` | **Yes** | β€” | Your Vaultwarden instance URL | | `VAULTWARDEN_EMAIL` | **Yes** | β€” | Your Vaultwarden email | | `VAULTWARDEN_PASSWORD` | **Yes** | β€” | Your master password | | `API_KEY` | **Yes** | β€” | API key for this service (min 32 chars) | | `ALLOWED_IPS` | No | (all) | Comma-separated IPs/CIDRs to whitelist | | `ENABLE_GITHUB_IP_RANGES` | No | `false` | Auto-whitelist GitHub Actions IPs | | `SYNC_INTERVAL` | No | `5m` | How often to re-sync the vault | | `CACHE_TTL` | No | `5m` | Secret cache duration | | `TRUSTED_PROXY_IP` | No | `localhost` | Trusted reverse proxy IPs | | `ENVIRONMENT` | No | `development` | Set to `production` to hide errors | | `DEBUG` | No | `false` | Enable debug logging | ## Docker Compose ```yaml services: vaultwarden-api: image: ghcr.io/turbootzz/vaultwarden-api:latest container_name: vaultwarden-api restart: unless-stopped ports: - "127.0.0.1:8080:8080" environment: - VAULTWARDEN_URL=${VAULTWARDEN_URL} - VAULTWARDEN_EMAIL=${VAULTWARDEN_EMAIL} - VAULTWARDEN_PASSWORD=${VAULTWARDEN_PASSWORD} - API_KEY=${API_KEY} - ENVIRONMENT=production - ALLOWED_IPS=${ALLOWED_IPS} read_only: true tmpfs: - /tmp cap_drop: - ALL security_opt: - no-new-privileges:true deploy: resources: limits: cpus: '0.5' memory: 128M healthcheck: test: ["CMD", "wget", "--spider", "-q", "http://localhost:8080/health"] interval: 30s timeout: 3s retries: 3 ``` ## Usage Examples ### Shell / CI Pipeline ```bash DB_URL=$(curl -sf -H "Authorization: Bearer $API_KEY" \ https://api.yourdomain.com/secret/DATABASE_URL | jq -r '.value') ``` ### Python ```python import requests def get_secret(name): r = requests.get(f"https://api.yourdomain.com/secret/{name}", headers={"Authorization": f"Bearer {API_KEY}"}) return r.json()["value"] db_url = get_secret("DATABASE_URL") ``` ### Node.js ```javascript const res = await fetch(`https://api.yourdomain.com/secret/DATABASE_URL`, { headers: { Authorization: `Bearer ${API_KEY}` }, }); const { value } = await res.json(); ``` ### Go ```go req, _ := http.NewRequest("GET", "https://api.yourdomain.com/secret/DATABASE_URL", nil) req.Header.Set("Authorization", "Bearer "+apiKey) resp, _ := http.DefaultClient.Do(req) ``` ### GitHub Actions ```yaml - name: Fetch secrets run: | export DB_URL=$(curl -sf -H "Authorization: Bearer ${{ secrets.VAULT_API_KEY }}" \ https://api.yourdomain.com/secret/DATABASE_URL | jq -r '.value') ``` ## Security - **API key authentication** with constant-time comparison (timing-attack resistant) - **IP whitelisting** with CIDR support + optional GitHub Actions IP auto-import - **Rate limiting** (30 requests/minute per IP) - **Read-only filesystem** in Docker (only `/tmp` writable) - **Non-root user** in container - **No capabilities** (`cap_drop: ALL`) - **Security headers** via Helmet middleware - **No secret names in production logs** (only at debug level) - Secrets are **decrypted in-memory only** β€” never written to disk ## Project Structure ``` β”œβ”€β”€ cmd/api/main.go # Entry point β”œβ”€β”€ internal/ β”‚ β”œβ”€β”€ auth/middleware.go # API key authentication β”‚ β”œβ”€β”€ config/config.go # Configuration β”‚ β”œβ”€β”€ handlers/handlers.go # HTTP handlers β”‚ β”œβ”€β”€ ipwhitelist/ipwhitelist.go # IP access control β”‚ β”œβ”€β”€ validators/validators.go # Input validation β”‚ └── vaultwarden/ β”‚ β”œβ”€β”€ api_client.go # Native HTTP client for Vaultwarden β”‚ β”œβ”€β”€ crypto.go # Bitwarden-compatible encryption β”‚ β”œβ”€β”€ crypto_test.go # Crypto unit tests β”‚ β”œβ”€β”€ client.go # Secret lookup + caching β”‚ └── init.go # Initialization with retry β”œβ”€β”€ pkg/logger/logger.go # Structured logging β”œβ”€β”€ Dockerfile # Multi-stage build (~15MB image) β”œβ”€β”€ docker-compose.yml # Production-ready compose └── go.mod ``` ## Building from Source ```bash # Build go build -o vaultwarden-api ./cmd/api # Test go test ./... # Docker docker build -t vaultwarden-api . ``` ## How Secrets are Matched When you request `/secret/DATABASE_URL`, the API: 1. **Exact match** (case-insensitive) against vault item names 2. **Partial match** if no exact match is found 3. Returns the most relevant value: password β†’ custom field β†’ notes This means you can name your Vaultwarden items naturally (e.g., "Database URL") and fetch them with any casing. ## Contributing Contributions welcome! Fork β†’ branch β†’ PR. ## License MIT β€” see [LICENSE](LICENSE) --- **Built by [Thijs Herman](https://github.com/Turbootzz)** β€” because `.env` files are so 2020.