PRZEWODNIK · OPS

Z AUTH_MODE=off do enforce, bez psucia żywych agentów.

Domyślny AUTH_MODE=off jest właściwy dla pojedynczego dewelopera. W chwili, gdy ekosystemu dotyka więcej niż jeden człowiek lub jeden host, chcesz podpisanych żądań. Trzystopniowe pokrętło (offobserveenforce) istnieje po to, żeby migracja nigdy nie była big bangiem.

Dlaczego trzy tryby zamiast booleana

Binarne AUTH_ENFORCED=true/false zabija ścieżkę migracji. W realnym wdrożeniu auth włączasz po uruchomieniu agentów. Jeśli flip jest binarny, w dniu enforce połowa agentów pada, bo ktoś zapomniał podpiąć klienta podpisującego.

TrybZachowanieUse case
off Middleware to no-op. Niepodpisane żądania przelatują wprost. Key-server nie jest pytany. Pojedynczy user, jeden host, sieć domowa, smoke w CI.
observe Niepodpisane / nieprawidłowe żądania nadal przelatują, ale powód odrzucenia trafia do logu. Migracja z niepodpisanego do podpisanego. Czytasz log, naprawiasz callerów, potem flipujesz.
enforce Podpisane żądania przelatują. Niepodpisane dostają 401; 503 jeśli key-server jest down. Wieloagentowe wdrożenie, współdzielony host, produkcja.

AUTH_MODE może się różnić między blokami w trakcie migracji — consciousness-server może siedzieć w enforce, podczas gdy test-runner zostaje w observe dla jednego upartego callera.

1. Wygeneruj jedną parę kluczy na agenta

Każdy agent dostaje własną parę ed25519. Uruchom na hoście, na którym agent będzie żył, żeby klucz prywatny nigdy nie podróżował:

terminal
# Jedna para kluczy na agenta. Uruchom na hoście, gdzie agent będzie żył.
ssh-keygen -t ed25519 -C "ecosystem-scribe" \
  -f ~/.ssh/ecosystem-scribe -N ""

# Wynik: dwa pliki —
#   ~/.ssh/ecosystem-scribe         (klucz prywatny — zostaje na hoście agenta)
#   ~/.ssh/ecosystem-scribe.pub     (klucz publiczny — publikujesz na key-server)

-N "" oznacza brak hasła. Jeśli agenci mają startować bez nadzoru (kontener workera, jednostka systemd), to realny wybór — granicą bezpieczeństwa staje się system plików hosta zamiast prompta o hasło.

2. Bootstrap kluczy publicznych na key-server

Uwierzytelnianie polega na sprawdzeniu, czy X-Agent: <name> w nagłówku mapuje się na klucz publiczny, który key-server już zna. Mapowanie to po prostu pliki na dysku:

terminal
# Na hoście z key-server: wrzuć klucz publiczny każdego agenta do
# katalogu agents/. Key-server podejmie go przy następnym żądaniu;
# restart nie jest potrzebny.
scp ~/.ssh/ecosystem-scribe.pub \
    operator@key-server-host:/opt/ecosystem/key-server/keys/agents/scribe.pub

# Powtórz dla każdego agenta, który ma się uwierzytelniać.

Każdy plik .pub w key-server/keys/agents/ definiuje agenta, który może się uwierzytelnić. Bez bazy danych, bez panelu admina; plik jest źródłem prawdy. Usunięcie pliku odbiera dostęp przy następnym żądaniu.

3. Flip do observe i obserwuj log

Teraz włączasz auth bez psucia czegokolwiek:

terminal
# Przełącz każdy blok z off na observe. Restart, żeby env się załadowało.
AUTH_MODE=observe docker compose up -d

# Obserwuj log — każda linia to żądanie, które byłoby odrzucone w enforce.
tail -f deploy/volumes/*-logs/auth-observe.log

# Powody, które zobaczysz, i co naprawić:
#   missing_headers           caller jeszcze nie podpisuje
#   unknown_agent             podpisuje, ale kluczem niezbootstrapowanym
#   bad_signature             rozjazd protokołu w kodzie podpisującym callera
#   timestamp_out_of_window   dryf zegara callera (włącz NTP)
#   nonce_replayed            caller używa ponownie nonce'a (musi rotować)

Iteruj, aż log siedzi czysty przez parę dni normalnego ruchu. Czysty oznacza: każdy wpis to znany, świadomie niepodpisany caller (probe healthchecka, lokalny skrypt do debugowania), nie żywy agent produkcyjny.

4. Flip do enforce

terminal
# Gdy auth-observe.log siedzi czysty przez parę dni, flipnij:
AUTH_MODE=enforce docker compose up -d

# Natychmiastowy rollback, gdyby coś poszło nie tak:
AUTH_MODE=off docker compose up -d
# Bez migracji stanu. Wygenerowane klucze pozostają ważne;
# system po prostu przestaje je sprawdzać.

Od tego momentu niepodpisani callerzy dostają twardy 401. Awaryjna furtka off jest o jedną zmienną środowiskową — bez migracji stanu, bez rewokacji kluczy, bez restartu zewnętrznego systemu.

Jak wygląda podpisywanie żądania w kodzie

Cortex i Claude Code podpisują wywołania CS, gdy są skonfigurowane. Dla własnych klientów protokół opisuje SIGNING-PROTOCOL.md. Implementacja w Pythonie jest krótka:

signing.py
import time, json, secrets, base64
from nacl.signing import SigningKey

priv = SigningKey(open("/home/scribe/.ssh/ecosystem-scribe", "rb").read())

def sign_request(method, path, body_bytes=b""):
    ts = str(int(time.time()))
    nonce = base64.urlsafe_b64encode(secrets.token_bytes(16)).decode()
    canonical = f"{method}\n{path}\n{ts}\n{nonce}\n".encode() + body_bytes
    sig = priv.sign(canonical).signature
    return {
        "X-Agent": "scribe",
        "X-Timestamp": ts,
        "X-Nonce": nonce,
        "X-Signature": base64.urlsafe_b64encode(sig).decode(),
    }

# I potem przy każdym żądaniu:
headers = sign_request("POST", "/api/notes", json.dumps(payload).encode())
requests.post(f"{CS}/api/notes", json=payload, headers=headers)

Stringiem kanonicznym jest METHOD\n PATH\n TIMESTAMP\n NONCE\n BODY. Serwer rekonstruuje go z nagłówków i request-line, potem weryfikuje podpis ed25519 wobec klucza publicznego zmapowanego na X-Agent. Replayy blokuje krótko żyjący cache nonce'ów; dryf zegara powyżej ~60 s jest odrzucany.

Lista hardeningu

  • Trzymaj port 3040 (key-server) z dala od publicznego internetu. Tylko loopback, VPN albo localhost-bind — port wydaje sekrety.
  • Włącz IP allow-list na key-server nawet w zaufanym LAN-ie. Format CIDR; jedna linia na peera.
  • Audytuj log audytowydeploy/volumes/key-server-logs/audit.jsonl to strukturalny JSONL. Tail-and-alert.
  • NTP na każdym hoście. Podpisane żądania są odrzucane, gdy zegar przekroczy okno.
  • Rotuj klucze przy decommisioningu hosta: usuń .pub na key-server, regeneruj na hoście agenta.

Pełen model zagrożeń: consciousness-server/SECURITY.md.