credential vault · phoenix · liveview

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_limit directive 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.