diff --git a/docs/infra/nøkkelhåndtering.md b/docs/infra/nøkkelhåndtering.md new file mode 100644 index 0000000..3910fe1 --- /dev/null +++ b/docs/infra/nøkkelhåndtering.md @@ -0,0 +1,162 @@ +# Nøkkelhåndtering — API-nøkler i PG, administrert via UI + +## Problemstilling + +API-nøkler (OpenRouter, Anthropic, Gemini, xAI, OpenAI, ElevenLabs +etc.) ligger i klartekst i `/srv/synops/.env`. Ingen audit trail, +ingen granulær tilgang, lett å glemme å oppdatere. + +## Modell + +Maskinrommet er nøkkelforvalter. Nøkler lagres kryptert i PG, +administreres via admin-UI, injiseres i verktøy ved oppstart. + +``` +Admin-UI → maskinrommet → PG (kryptert) + ↓ + spawner verktøy med nøkler som env + ↓ + synops-agent, synops-ai, synops-clip osv. +``` + +## Database-skjema + +```sql +CREATE TABLE api_keys ( + id UUID PRIMARY KEY DEFAULT gen_random_uuid(), + provider TEXT NOT NULL, -- 'openrouter', 'anthropic', 'gemini', 'xai', 'openai', 'elevenlabs' + label TEXT, -- valgfritt navn ("Vegards OpenRouter", "Prod Anthropic") + key_encrypted BYTEA NOT NULL, -- AES-256-GCM kryptert + key_hint TEXT, -- siste 4 tegn for identifisering ("...c08b") + is_active BOOLEAN DEFAULT true, -- deaktivert uten å slette + created_at TIMESTAMPTZ DEFAULT now(), + updated_at TIMESTAMPTZ DEFAULT now(), + created_by UUID REFERENCES nodes(id), -- hvem la inn nøkkelen + last_used TIMESTAMPTZ, -- sist brukt (oppdateres ved bruk) + usage_count BIGINT DEFAULT 0 -- antall ganger brukt +); + +CREATE INDEX idx_api_keys_provider ON api_keys(provider, is_active); +``` + +## Kryptering + +Nøkkelen krypteres med AES-256-GCM. Krypteringsnøkkelen +(master key) lever som: +- Env-variabel `SYNOPS_MASTER_KEY` på hosten +- Eller i systemd credentials (LoadCredential) +- Aldri i PG, aldri i .env + +```rust +// Maskinrommet krypterer ved lagring +let encrypted = aes_gcm_encrypt(&api_key, &master_key); + +// Maskinrommet dekrypterer ved bruk +let api_key = aes_gcm_decrypt(&encrypted, &master_key); +``` + +Kun maskinrommet kjenner master key. Admin-UI sender klartekst +til maskinrommet via HTTPS, maskinrommet krypterer og lagrer. + +## Nøkkeloppslag + +Når et verktøy trenger en API-nøkkel: + +```rust +// I maskinrommet, ved spawning av synops-agent: +let key = db::get_active_key("openrouter").await?; +cmd.env("OPENROUTER_API_KEY", key.decrypt(&master_key)?); +``` + +Verktøyet ser nøkkelen som vanlig env-variabel. Ingen endring +i verktøykoden — de leser allerede fra env. + +Alternativt for synops-agent (som kjører lenge): +maskinrommet eksponerer et internt endepunkt: +``` +GET /internal/api-key/{provider} +Authorization: Bearer +→ { "key": "sk-..." } +``` + +## Admin-UI + +### Nøkkelliste + +``` +┌─ API-nøkler ──────────────────────────────────┐ +│ │ +│ Provider Label Status Sist │ +│ OpenRouter Vegards OR ✅ Aktiv nå │ +│ Anthropic Prod ✅ Aktiv 2t │ +│ Gemini — ⚫ Inaktiv │ +│ xAI — ⚫ Mangler │ +│ OpenAI — ⚫ Mangler │ +│ ElevenLabs Prod TTS ✅ Aktiv 1d │ +│ │ +│ [+ Legg til nøkkel] │ +└────────────────────────────────────────────────┘ +``` + +### Legg til / rediger + +``` +┌─ Ny nøkkel ───────────────────────────────────┐ +│ │ +│ Provider: [OpenRouter ▼] │ +│ Label: [Vegards OpenRouter ] │ +│ Nøkkel: [sk-or-v1-... ] │ +│ │ +│ [Test tilkobling] [Lagre] [Avbryt] │ +└────────────────────────────────────────────────┘ +``` + +"Test tilkobling" gjør et minimalt API-kall (list models) +for å verifisere at nøkkelen fungerer før lagring. + +### Sikkerhetsregler + +- Nøkkelen vises aldri etter lagring — kun hint ("...c08b") +- For å endre: slett gammel, legg inn ny +- Deaktivering beholder nøkkelen men bruker den ikke +- Sletting fjerner den permanent +- Audit: created_by, created_at, last_used, usage_count + +## Flere nøkler per provider + +En provider kan ha flere nøkler (f.eks. ulike OpenRouter-kontoer): + +```sql +SELECT key_encrypted FROM api_keys +WHERE provider = 'openrouter' AND is_active = true +ORDER BY usage_count ASC -- round-robin / minst brukt +LIMIT 1 +``` + +Gir enkel lastbalansering og mulighet for å rotere nøkler +uten nedetid. + +## Migrering fra .env + +1. Admin legger inn nøkler via UI +2. Maskinrommet bruker PG-nøkler i stedet for env +3. .env-filen beholder nøklene som fallback +4. Når alt fungerer: fjern nøkler fra .env +5. Beholder kun `SYNOPS_MASTER_KEY` som env + +## Hva som endres + +| Komponent | Endring | +|-----------|---------| +| PG | Ny `api_keys`-tabell | +| Maskinrommet | Kryptering, nøkkeloppslag, API-endepunkt | +| Admin-UI | Ny side for nøkkelhåndtering | +| synops-agent | Ingen (leser fortsatt fra env) | +| CLI-verktøy | Ingen (leser fortsatt fra env) | +| .env | Nøkler fjernes gradvis | + +## Prioritet + +Middels. Fungerer med .env nå. Viktigere når flere brukere +skal administrere egne nøkler (BYOK-modell) eller når nøkler +roteres jevnlig.