Przegląd
Serwis HTTP, który robi dwie rzeczy:
- Weryfikuje podpisane ed25519 zapytania. Wywołujący
podpisuje
(method, path, timestamp, nonce, body-hash)swoim kluczem prywatnym. Serwer trzyma odpowiadający klucz publiczny w pliku i odpowiada{ valid: true | false }. Bezstanowo. Bez session store. Bez wystawiania tokenów. Bez zarządzania wygasaniem. - Wydaje sekrety na żądanie. Prywatne klucze SSH
dla deployu/pushu, tokeny API dla wywołań wychodzących. Lista
dozwolonych IP jako pierwsza brama; w trybie
AUTH_MODE=enforcedochodzi podpis ed25519.
Zaprojektowany jako sidecar dla Consciousness Server i Cortex, ale działa samodzielnie dla każdego serwisu HTTP, który chce uwierzytelniania na bazie klucza bez stawiania Vaulta, OAuth czy issuera JWT.
Po co to istnieje
Jeśli prowadzisz grupę serwisów albo agentów komunikujących się ze sobą po HTTP, dzisiejsze opcje auth są złe na trzy różne sposoby:
- Bearer tokeny w nagłówkach. Teraz masz: wystawianie tokenów, ich przechowywanie, rotację, odwoływanie, refresh flow, bazę danych tokenów do backupu. Połowa "auth" to teraz "token lifecycle".
- mTLS między każdą parą. Czystszy model zaufania, ale każdy operator wpada w tę samą ścianę: certificate authority, certy pośrednie, rotacja i trudna historia o odwoływaniu.
- Wspólny sekret w
.env. Działa do dnia, w którym wycieknie — wtedy wszyscy dowiadują się o tym równocześnie.
Podpisane zapytania siedzą pośrodku. Klucz publiczny agenta to plik na dysku weryfikatora. Skasowanie pliku to odwołanie agenta. Podpis jest per-request, więc nie ma tokena do ukradnięcia który daje długoterminowy dostęp. Weryfikator nie trzyma stanu o tym kto co podpisał — tylko "czy ten podpis jest prawidłowy dla tej kanonicznej wiadomości".
Ten wzorzec jest dobrze znany (AWS SigV4, GitHub Apps JWT, SSH agent forwarding). BuildOnAI Key Server to samodzielna implementacja tego wzorca.
Instalacja
git clone https://github.com/build-on-ai/buildonai-key-server.git
cd buildonai-key-server
cp auth/allowed-clients.json.example auth/allowed-clients.json
# edytuj auth/allowed-clients.json: dodaj IP klientów którzy będą się łączyć
docker compose up -d
curl http://localhost:3040/health Domyślny port to 3040 — część
zarezerwowanego zakresu ekosystemu BuildOnAI
3030–3050. Zmień przez KEY_SERVER_PORT.
Rejestracja agenta (ed25519)
# Na hoście agenta:
ssh-keygen -t ed25519 -C "agent1@$(hostname)" \
-f ~/.ssh/buildonai-agent1 -N ""
# Skopiuj klucz PUBLICZNY na hosta key-server:
scp ~/.ssh/buildonai-agent1.pub \
operator@key-server-host:/opt/buildonai-key-server/keys/agents/agent1.pub
# Sprawdź czy serwer go widzi:
curl http://key-server-host:3040/api/agents/identity
# {"agents": ["agent1"]}
Klucz prywatny zostaje na hoście agenta. Nigdy go nie opuszcza.
Odwołanie to rm keys/agents/agent1.pub — działa
natychmiast, nie ma cache.
Trzy tryby auth
AUTH_MODE jest czytany raz przy starcie. Trzy
prawidłowe wartości dają bezpieczną ścieżkę migracji z deploymentów
niepodpisanych do podpisanych.
| Tryb | Zachowanie | Kiedy używać |
|---|---|---|
off | Middleware no-op. Podpisy nie są sprawdzane. | Jeden host, jeden użytkownik, brak niezaufanych wywołujących. Domyślne. |
observe | Nieprawidłowe podpisy przechodzą, ale powód jest logowany do logs/auth-observe.log. | Migracja z unsigned do signed: włącz, napraw każdego wywołującego którego request byłby odrzucony, potem przejdź na enforce. |
enforce | Prawidłowe podpisy przechodzą. Reszta → 401 (lub 503 jeśli sam lookup zawiedzie). | Multi-agent, współdzielony host, wszystko co ma być audytowalne. |
Trzy-trybowy design jest celowy. Pojedynczy boolean
AUTH=on/off zabija ścieżkę migracji — w dniu kiedy
przełączasz, połowa agentów przestaje działać, bo ktoś zapomniał
podpiąć klienta podpisującego. observe to bezpieczna próba
generalna.
Pojęcia
Wiadomość kanoniczna
Agent, gated service i key-server muszą zrekonstruować te same bajty
żeby podpisać i zweryfikować. Pięć pól łączonych literalnym
\n (LF):
<METHOD>\n<PATH>\n<X-Timestamp>\n<X-Nonce>\nSHA256(body) SHA256(body) jest hex-encoded. PATH to
ścieżka requesta bez query string. Dla GET bez body,
SHA256("") = e3b0c442...52b855.
Weryfikacja podpisanego zapytania
Gated service (Consciousness Server, Twoje API, Twój odbiorca
webhooków) wyciąga cztery nagłówki + body hash z przychodzącego
requesta i POST-uje je do /api/verify:
# Gated service przesyła headery agenta + body hash do /api/verify.
curl -s -X POST http://key-server-host:3040/api/verify \
-H 'Content-Type: application/json' \
-d '{
"agent_id": "agent1",
"method": "POST",
"path": "/api/notes/create",
"timestamp": "2026-05-31T10:00:00Z",
"nonce": "a7b3c1d9e2f64a18b3c1d9e2f64a18b3",
"signature": "base64-ed25519-sig-tutaj",
"body_sha256": "e3b0c44298fc1c149afbf4c8996fb924..."
}'
# {"valid": true} Anti-replay
- Okno timestampu:
now-300sdonow+60s. Poza → odrzucone. - Cache nonce'ów: każdy
X-Nonceakceptowany najwyżej raz na 5 minut (RedisSET NX EX 300). - Powiązanie z body: wiadomość kanoniczna zawiera
SHA256(body)— przechwycony request nie da się odtworzyć z innym endpointem czy zmienionym body.
Układ vaulta
Układ na dysku pod keys/ jest celowo prosty —
ls pokazuje co tam jest. Per-agent klucze publiczne
ed25519 leżą w keys/agents/<AGENT>.pub. Klucze
prywatne SSH w keys/ssh/<name>; tokeny API w
keys/<service>/api-key.txt. chmod 600
na prywatne materiały.
Log audytu
Każde zapytanie — verify, vault read, sukces lub porażka — dodaje
jedną linię do logs/audit.log plus wpis JSONL do
logs/audit.jsonl. Self-rotation co 50 MB.
[2026-05-31T08:00:01Z] IP=10.0.0.20 ENDPOINT=/api/verify RESULT=VALID agent=agent1
[2026-05-31T08:00:03Z] IP=10.0.0.30 ENDPOINT=/api/verify RESULT=VALID agent=agent2
[2026-05-31T08:00:18Z] IP=10.0.0.30 ENDPOINT=/api/verify RESULT=INVALID reason=nonce_replayed
[2026-05-31T14:22:05Z] IP=10.0.0.99 ENDPOINT=/keys/ssh/git-deploy RESULT=FORBIDDEN IP_not_whitelisted
[2026-05-31T14:22:18Z] IP=10.0.0.20 ENDPOINT=/keys/ssh/../etc/passwd RESULT=REJECTED path_traversal_attempt Typowe zastosowania
Inter-service auth w małym monorepo
Każdy serwis trzyma własny klucz prywatny ed25519 i podpisuje
wychodzące requesty. Weryfikator to jeden kontener. Usuwa
SHARED_SECRET env var per serwis, którego nikt nie
rotuje.
Uwierzytelnianie webhooków
Twoje źródło podpisuje payload przed wysłaniem; odbiorca przesyła
nagłówki do /api/verify. Kryptograficzny dowód pochodzenia
bez współdzielonego sekretu HMAC per źródło. Skradziony plik klucza
publicznego? Jest publiczny — nie ma współdzielonego sekretu do
ukradnięcia.
Uwierzytelnianie urządzeń IoT
Każde urządzenie generuje własną parę kluczy ed25519 przy pierwszym
uruchomieniu. Klucz publiczny rejestrowany out of band. Telemetria
podpisana per request. Skradzione urządzenie → rm jego
plik .pub, telemetria przestaje być akceptowana, jego
zapisany klucz prywatny jest teraz bezużyteczny przeciw Tobie.
Vault read (opcjonalny, legacy)
# Vault read w trybie AUTH_MODE=off (tylko IP allow-list):
curl -s http://localhost:3040/keys/ssh/github-deploy > ~/.ssh/id_deploy
chmod 600 ~/.ssh/id_deploy
Endpointy vaulta (/keys/ssh/<name>,
/keys/api/<service>) dziedziczą ten sam model
zaufania co /api/verify: IP allow-list zawsze, podpis
ed25519 dodatkowo w trybie AUTH_MODE=enforce.
API
Siedem endpointów. Cała powierzchnia.
| Method | Path | Purpose |
|---|---|---|
| GET | /health | Sonda liveness |
| GET | /api/agents/identity | Lista zarejestrowanych agentów |
| GET | /api/agents/identity/:agent | Klucz publiczny + fingerprint dla agenta |
| POST | /api/verify | Weryfikacja podpisanego requesta (wywoływana przez gated services) |
| GET | /keys/ssh/:name | Pobierz prywatny klucz SSH z vaulta |
| GET | /keys/api/:service | Pobierz token API z vaulta |
| GET | /audit | Ostatnie 100 wpisów w logu audytu |
Model zagrożeń
Broni przed
- Replayem w tej samej sieci LAN — nonce + okno timestampu.
- Sfałszowanym pochodzeniem z obcego hosta — lista dozwolonych IP jako warstwa pierwsza.
- Przechwyconym-i-zreplayowanym pod innym endpointem — wiadomość kanoniczna wiąże method + path + body hash.
- Skompromitowanym agentem którego klucz prywatny jest w
rękach atakującego —
rm keys/agents/<AGENT>.pubna weryfikatorze. Działa natychmiast, bez cache. - Przypadkowym commitem sekretów — dostarczany
.gitignorewykluczakeys/,logs/i prawdziwyauth/allowed-clients.json.
Nie broni przed (celowy zakres)
- Podsłuchem treści requesta w sieci. ed25519 to schemat podpisu, nie szyfrowanie. Owiń za TLS (Caddy, nginx) jeśli requesty przenoszą wrażliwe payloady.
- Atakującym z rootem na hoście. Może czytać klucze prywatne, vault kluczy publicznych i log audytu bezpośrednio z dysku.
- Wysokowolumenowym DoS-em na
/api/verify. Serwer nie rate-limituje niczego; postaw go za reverse proxy jeśli niezaufani wywołujący mogą do niego dojść.
Dla deploymentów poza zaufaną LAN/VPN, oczywiste upgrade'y to:
terminować TLS na reverse proxy, postawić w prywatnej sieci,
ustawić ścisłe permissions na keys/ i rotować klucze
agentów w cyklu. Model zagrożeń napisany dla niskotarciowego
użycia LAN; dostrajaj wraz z poszerzaniem się perymetru.