gemini-gate
A small credential vault. Captures keys via a single web form, stores each as chmod 600 server-side, and serves them back to authenticated callers on demand.
what it is
One declared registry of credentials. Per-name file under ~/.config/gemini-gate/. Atomic replace on update, regex shape-validate before any disk write, never logged or echoed in error tuples. Same shared ADMIN_TOKEN gates the form (HTTP Basic) and the API (Bearer); constant-time comparison.
The historical /v1/* Gemini proxy is preserved as a special case — it reads the gemini credential by name and forwards to generativelanguage.googleapis.com with the key injected. Callers never see the raw key.
shape
┌──────────────── Internet ────────────────┐
│ │
▼ ▼
browser (form) curl/SDK
│ Basic │ Bearer
▼ ▼
Caddy ─────▶ 127.0.0.1:9850 (Phoenix LiveView)
│
│ reads on demand
▼
~/.config/gemini-gate/<name> chmod 600
declared at v1:
gemini · anthropic · openrouter
porkbun_api · porkbun_secret · github_pat
using it
$ # the form lives at the root $ open https://gemini.hyperstitious.org/ $ # read any stored credential, anywhere with HTTPS $ PORKBUN=$(curl -sS \ -H "Authorization: Bearer $ADMIN_TOKEN" \ https://gemini.hyperstitious.org/credentials/porkbun_api) $ # proxy a Gemini call (key injected from vault) $ curl -sS https://gemini.hyperstitious.org/v1/models/gemini-1.5-flash:generateContent \ -H "Authorization: Bearer $ADMIN_TOKEN" \ -H 'Content-Type: application/json' \ -d '{"contents":[{"parts":[{"text":"hello"}]}]}'
threat model
- at-rest
- filesystem perms only · adequate when host disk is LUKS
- auth
- one shared token · rotate via env file + restart
- history
- one value per credential · atomic replace, no rollback
- rate-limit
- none in app · use Caddy
rate_limitdirective in front - body cap
- 10 MB on the proxy · raise for vision-large payloads
add a credential
Append a %{name, label, pattern, hint} map to :gemini_gate, :credentials in config/config.exs, restart. The LiveView picks it up; the read endpoint serves it.