A secret retrieval service that requires human approval via Telegram. Built on Cloudflare Workers with Durable Objects.
Designed for headless servers (VPS, bare metal) that need to fetch sensitive secrets (e.g., LUKS disk encryption keys) at boot time via curl.
curl GET /secret/mysecret?token=credential → blocks → Telegram notification with Approve/Deny
→ admin clicks Approve → curl returns the secret
- Client (
curl) requests a secret by ID, providing a per-secret access token - keywa checks IP allowlist (if configured) and token (if configured)
- keywa sends a Telegram notification with inline Approve/Deny buttons
curlblocks (HTTP long-polling) until the admin acts- Admin clicks Approve → secret is returned to
curl - Or: admin clicks Deny →
curlgets 403, or timeout (default 1 hour) → 504
See docs/architecture.md for design decisions and why Durable Objects over KV polling.
- pnpm
- Cloudflare account with Workers enabled
- A Telegram bot (create via @BotFather)
git clone https://github.com/Gowee/keywa && cd keywa
pnpm install
cp wrangler.toml.sample wrangler.tomlEdit wrangler.toml and fill in your D1 database ID:
pnpm wrangler d1 create keywa
# Copy the database_id into wrangler.tomlecho "YOUR_BOT_TOKEN" | pnpm wrangler secret put TELEGRAM_BOT_TOKEN
echo "YOUR_CHAT_ID" | pnpm wrangler secret put TELEGRAM_CHAT_ID
echo "your-admin-api-key" | pnpm wrangler secret put ADMIN_TOKEN
# Optional: verify Telegram webhook requests (defense-in-depth)
echo "some-random-secret" | pnpm wrangler secret put TELEGRAM_WEBHOOK_SECRETIn wrangler.toml:
[vars]
MAX_TIMEOUT_SECONDS = "3600" # upper bound (seconds) for ?timeout= (default 1 hour)
# DISABLE_TELEGRAM_LOGIN = "true" # uncomment to disable Telegram loginMAX_TIMEOUT_SECONDS caps the approval wait time. Clients may pass ?timeout=N
(seconds) per request — clamped to [1, MAX_TIMEOUT_SECONDS]. 0 or absent
means "use the max".
Rate limits are configured via [[ratelimits]] bindings (see wrangler.toml.sample).
pnpm deploycurl -X POST https://keywa.example.org/admin/webhook \
-H "Authorization: Bearer YOUR_ADMIN_TOKEN"# Add a secret (value + per-secret access token and/or IP allowlist)
curl -X PUT https://keywa.example.org/admin/api/secrets/mysecret \
-H "Authorization: Bearer YOUR_ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d '{"secret": "luks-passphrase", "token": "per-secret-credential", "cidrs": "203.0.113.0/24"}'
# Update just the value (keep existing token and IPs)
curl -X PUT https://keywa.example.org/admin/api/secrets/mysecret \
-H "Authorization: Bearer YOUR_ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d '{"secret": "new-passphrase"}'
# Clear IP restriction (empty string = clear)
curl -X PUT https://keywa.example.org/admin/api/secrets/mysecret \
-H "Authorization: Bearer YOUR_ADMIN_TOKEN" \
-H "Content-Type: application/json" \
-d '{"cidrs": ""}'
# Delete a secret
curl -X DELETE https://keywa.example.org/admin/api/secrets/mysecret \
-H "Authorization: Bearer YOUR_ADMIN_TOKEN"# This blocks until approved, denied, or timed out (default 1 hour)
# Via query param
curl "https://keywa.example.org/secret/mysecret?token=per-secret-credential"
# Or via Bearer header
curl -H "Authorization: Bearer per-secret-credential" "https://keywa.example.org/secret/mysecret"
# Per-request timeout (seconds). Clamped to [1, MAX_TIMEOUT_SECONDS].
# Omit or use 0 to mean "use the max".
curl "https://keywa.example.org/secret/mysecret?token=per-secret-credential&timeout=300"With a named session (for idempotent re-fetching):
curl -H "Authorization: Bearer per-secret-credential" "https://keywa.example.org/secret/mysecret?session=my-session"Visit /admin in a browser. Login via Telegram approval or admin token:
- Click "Login with Telegram" → approve the notification
- Or enter
ADMIN_TOKENdirectly
The dashboard lets you list, add, edit, and delete secrets. Telegram login can be disabled by setting DISABLE_TELEGRAM_LOGIN = "true" in wrangler.toml.
#!/bin/sh
# /etc/keyserver-unlock.sh — runs in NixOS initrd after SSH login
KEYSERVER="https://keywa.example.org"
SECRET_ID="tyo2-luks"
SECRET_TOKEN="per-secret-credential"
DEVICE="/dev/vda2"
echo "Fetching LUKS key..."
SECRET=$(curl -fsSH "Authorization: Bearer $SECRET_TOKEN" \
--max-time 3600 --retry 3 \
"$KEYSERVER/secret/$SECRET_ID?session=boot&timeout=3600")
if [ -z "$SECRET" ]; then
echo "Failed to fetch secret"
exit 1
fi
echo -n "$SECRET" | cryptsetup open "$DEVICE" crypted --key-file -
echo "✓ Disk unlocked. Continuing boot."
exit 0| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /secret/:secretId |
?token= or Bearer |
Request secret (long-poll). Optional ?session=, ?timeout=N (seconds, clamped to [1, MAX_TIMEOUT_SECONDS], 0 or absent = max) |
| GET | / |
— | Health check |
| Method | Path | Auth | Description |
|---|---|---|---|
| GET | /admin |
— | Login page (or redirect if session exists) |
| GET | /admin/dashboard |
session cookie | Admin dashboard |
| POST | /admin/auth/login |
— | Start login (long-poll for Telegram approval) |
| POST | /admin/auth/logout |
session cookie | Clear session |
All admin endpoints accept either Authorization: Bearer $ADMIN_TOKEN or a session cookie (from web login).
| Method | Path | Description |
|---|---|---|
| GET | /admin/api/secrets |
List secrets (JSON) |
| PUT | /admin/api/secrets/:secretId |
Add/update secret ({secret, token, cidrs} body) |
| DELETE | /admin/api/secrets/:secretId |
Delete secret |
| POST | /admin/webhook |
Register Telegram webhook |
| Method | Path | Auth | Description |
|---|---|---|---|
| POST | /telegram/webhook |
X-Telegram-Bot-Api-Secret-Token |
Receives Telegram callback queries |
pnpm dev # local development with wrangler
pnpm build # type-check and build
pnpm lint # format check
pnpm test # run tests
pnpm test:watch # run tests in watch modepnpm wrangler d1 export keywa --remote --output=backup.sqlSee docs/architecture.md for:
- Why Durable Objects over KV polling
- The HTTP long-polling pattern
- Authentication model (admin API key vs Telegram login)
- Threat model and security considerations
MIT