[HIGH] Agent→API-Auth: ed25519 Challenge-Response (Proof-of-Possession) statt Bearer-Token #185

Closed
opened 2026-06-26 12:20:30 +00:00 by chinux · 2 comments
Owner

Teil von Epic #184 (Public-Readiness). Loest die Bearer-Token-Schwaeche der Agent→API-Auth.

Problem

Der Agent authentifiziert heute per Bearer-Token (agent_ws.py): hoch-entropisch + at-rest gehasht (nicht faelschbar), ABER wiederverwendbar — wer ihn aus Install-Script/Logs/Leak abgreift, kann sich beliebig oft als der Node ausgeben; der Token laeuft nie ab. wss:// (#113) schuetzt den Transport, aber nicht gegen einen geleakten Token.

Loesung: ed25519 Proof-of-Possession (Challenge-Response) pro Agent

  1. Pro Agent ein eigenes ed25519-Identity-Keypair, privater Key bleibt root-only auf dem Node, verlaesst die Maschine nie.
  2. Handshake: Agent verbindet → Server schickt zufaelligen Nonce → Agent signiert den Nonce mit seinem privaten Key → Server verifiziert gegen den gespeicherten Public Key des Nodes (ed25519-dalek ist durch #113 Phase 2 schon im Agent).
  3. Vorteile ggue. Bearer:
    • Nichts Wiederverwendbares geht ueber die Leitung — kein Token zum Klauen, jeder Nonce einmalig.
    • Nicht replaybar — abgefangene Signatur gilt nur fuer genau den Nonce.
    • Pro Node widerrufbar — Public Key loeschen sperrt genau einen Node, ohne die anderen.

#182 hier eingefaltet — Enrollment-Token wird der einmalige Bootstrap

  • Der kurzlebige, single-use Enrollment-Token (#182) dient NUR EINMAL dazu, dass der Agent sein selbst erzeugtes Public Key beim Server registriert. Danach: Token verbrannt, Dauerbetrieb laeuft ueber Challenge-Response.
  • Damit ist Bearer nur noch fuer die erste Sekunde im Spiel. #182 wird mit diesem Issue zusammen umgesetzt/geschlossen.

Datenmodell

  • Node.identity_pubkey (ed25519 public key, base64) — beim Enrollment gesetzt.
  • Bearer-token/token_hash entfaellt im Dauerbetrieb (Dev-Umgebung: alte Nodes neu enrollen, keine Kompat noetig).
  • Nonce: serverseitig kurzlebig (z. B. 30s, single-use), pro Verbindung.

Backend (agent_ws.py)

  • Auth-Handshake auf Nonce→Signatur umstellen; Pubkey-Lookup statt Token-Hash-Lookup.
  • Konstante Antwortzeit; ungueltige Signatur → auth_fail (zaehlt fuer Rate-Limit #181).

Agent (Rust)

  • Identity-Keypair bei erster Inbetriebnahme erzeugen (root-only, /etc/theprox-agent/identity_ed25519).
  • Beim Enroll: Public Key via single-use-Token registrieren.
  • Beim Connect: Nonce signieren.

Akzeptanz

  • Kein wiederverwendbares Secret mehr im Dauerbetrieb ueber die Leitung.
  • Gestohlene Handshake-Daten sind nicht replaybar.
  • Ein Node sperren = ein Pubkey loeschen (andere unberuehrt).
  • Enrollment-Token ist single-use + kurzlebig (#182 erfuellt).

Bezug

#184 (Epic), #182 (eingefaltet), #181 (Rate-Limit on top), #115 (Pflicht-Begleiter: cmd-Owner-Binding), #113 (wss + ed25519 bereits im Agent).

Branch

feat/agent-challenge-response-auth

Teil von Epic #184 (Public-Readiness). **Loest die Bearer-Token-Schwaeche der Agent→API-Auth.** ## Problem Der Agent authentifiziert heute per **Bearer-Token** (`agent_ws.py`): hoch-entropisch + at-rest gehasht (nicht faelschbar), ABER **wiederverwendbar** — wer ihn aus Install-Script/Logs/Leak abgreift, kann sich beliebig oft als der Node ausgeben; der Token laeuft nie ab. `wss://` (#113) schuetzt den Transport, aber nicht gegen einen geleakten Token. ## Loesung: ed25519 Proof-of-Possession (Challenge-Response) pro Agent 1. **Pro Agent ein eigenes ed25519-Identity-Keypair**, privater Key bleibt **root-only auf dem Node**, verlaesst die Maschine nie. 2. **Handshake:** Agent verbindet → Server schickt zufaelligen **Nonce** → Agent **signiert** den Nonce mit seinem privaten Key → Server verifiziert gegen den **gespeicherten Public Key** des Nodes (ed25519-dalek ist durch #113 Phase 2 schon im Agent). 3. Vorteile ggue. Bearer: - **Nichts Wiederverwendbares** geht ueber die Leitung — kein Token zum Klauen, jeder Nonce einmalig. - **Nicht replaybar** — abgefangene Signatur gilt nur fuer genau den Nonce. - **Pro Node widerrufbar** — Public Key loeschen sperrt genau einen Node, ohne die anderen. ## #182 hier eingefaltet — Enrollment-Token wird der einmalige Bootstrap - Der **kurzlebige, single-use Enrollment-Token** (#182) dient NUR EINMAL dazu, dass der Agent sein selbst erzeugtes **Public Key** beim Server registriert. Danach: Token verbrannt, Dauerbetrieb laeuft ueber Challenge-Response. - Damit ist Bearer nur noch fuer die erste Sekunde im Spiel. #182 wird mit diesem Issue zusammen umgesetzt/geschlossen. ## Datenmodell - `Node.identity_pubkey` (ed25519 public key, base64) — beim Enrollment gesetzt. - Bearer-`token`/`token_hash` entfaellt im Dauerbetrieb (Dev-Umgebung: alte Nodes neu enrollen, keine Kompat noetig). - Nonce: serverseitig kurzlebig (z. B. 30s, single-use), pro Verbindung. ## Backend (`agent_ws.py`) - Auth-Handshake auf Nonce→Signatur umstellen; Pubkey-Lookup statt Token-Hash-Lookup. - Konstante Antwortzeit; ungueltige Signatur → auth_fail (zaehlt fuer Rate-Limit #181). ## Agent (Rust) - Identity-Keypair bei erster Inbetriebnahme erzeugen (root-only, `/etc/theprox-agent/identity_ed25519`). - Beim Enroll: Public Key via single-use-Token registrieren. - Beim Connect: Nonce signieren. ## Akzeptanz - Kein wiederverwendbares Secret mehr im Dauerbetrieb ueber die Leitung. - Gestohlene Handshake-Daten sind nicht replaybar. - Ein Node sperren = ein Pubkey loeschen (andere unberuehrt). - Enrollment-Token ist single-use + kurzlebig (#182 erfuellt). ## Bezug #184 (Epic), #182 (eingefaltet), #181 (Rate-Limit on top), #115 (Pflicht-Begleiter: cmd-Owner-Binding), #113 (wss + ed25519 bereits im Agent). ## Branch `feat/agent-challenge-response-auth`
Author
Owner

#185 umgesetzt (faltet #182 ein) · Commit 53fdcfe. Issue bleibt offen.

Agent (2.15.0→2.16.0)

  • identity.rs: persistentes ed25519-Keypair, Seed root-only 0600 unter /etc/theprox-agent/identity_ed25519 (verlaesst die Maschine nie).
  • Erstboot: Key erzeugt + Pubkey via POST /enroll/{token} registriert (single-use). Enroll-Fehler → Key geloescht + Abbruch → systemd-Retry.
  • auth.rs: Handshake jetzt Challenge-Response — Server-Nonce signieren, pubkey+signature senden statt token.

Backend

  • Node.identity_pubkey (base64, unique) + enroll_expires_at/enroll_used_at (TTL 60min + single-use). Migration 0024_node_identity.
  • POST /enroll/{token}: registriert Pubkey, verbrennt Token (generische Ablehnung, kein Oracle).
  • agent_ws.py: per-Verbindung-Nonce (single-use, nicht replaybar) → verify_signature (cryptography) gegen identity_pubkey; Token-Hash-Lookup entfaellt. Konstante Antwortzeit bei Fehlschlag.
  • admin_router: create/regenerate setzen Enroll-TTL; regenerate-token resettet identity_pubkey (= Re-Enrollment).

Akzeptanz: kein wiederverwendbares Secret im Dauerbetrieb · Handshake nicht replaybar (Nonce pro Verbindung) · Node sperren = Pubkey loeschen · Enrollment single-use+kurzlebig

#182: hierueber miterledigt (single-use + kurzlebiger Enrollment-Token mit enroll_expires_at/enroll_used_at).

⚠️ Setup/Deploy (Sebastian) — koordiniert noetig, bricht alte Nodes

  1. Backend neu bauen: cd server && docker compose up -d --build → Migration 0024 + neue Dep cryptography==44.0.0.
  2. Node-Agent lokal bauen (kein cargo im Container) + Binary committen/ausrollen.
  3. Jeden Node neu enrollen: POST /api/admin/nodes/{name}/regenerate-token → neuen Install/Enroll-Token nutzen. Alte token-basierte Agents brechen bewusst (Dev, keine Kompat).

Backend wurde bewusst NOCH NICHT neu gebaut, um die 6 Live-Agents nicht ungeplant offline zu nehmen — Deploy zusammen mit dem neuen Agent-Binary.

**#185 umgesetzt (faltet #182 ein)** · Commit `53fdcfe`. Issue bleibt offen. **Agent (2.15.0→2.16.0)** - `identity.rs`: persistentes ed25519-Keypair, Seed root-only 0600 unter `/etc/theprox-agent/identity_ed25519` (verlaesst die Maschine nie). - Erstboot: Key erzeugt + Pubkey via `POST /enroll/{token}` registriert (single-use). Enroll-Fehler → Key geloescht + Abbruch → systemd-Retry. - `auth.rs`: Handshake jetzt Challenge-Response — Server-Nonce signieren, `pubkey`+`signature` senden statt `token`. **Backend** - `Node.identity_pubkey` (base64, unique) + `enroll_expires_at`/`enroll_used_at` (TTL 60min + single-use). Migration `0024_node_identity`. - `POST /enroll/{token}`: registriert Pubkey, verbrennt Token (generische Ablehnung, kein Oracle). - `agent_ws.py`: per-Verbindung-Nonce (single-use, nicht replaybar) → `verify_signature` (cryptography) gegen `identity_pubkey`; **Token-Hash-Lookup entfaellt**. Konstante Antwortzeit bei Fehlschlag. - `admin_router`: create/regenerate setzen Enroll-TTL; `regenerate-token` resettet `identity_pubkey` (= Re-Enrollment). **Akzeptanz**: kein wiederverwendbares Secret im Dauerbetrieb ✅ · Handshake nicht replaybar (Nonce pro Verbindung) ✅ · Node sperren = Pubkey loeschen ✅ · Enrollment single-use+kurzlebig ✅ **#182**: hierueber miterledigt (single-use + kurzlebiger Enrollment-Token mit `enroll_expires_at`/`enroll_used_at`). ### ⚠️ Setup/Deploy (Sebastian) — koordiniert noetig, bricht alte Nodes 1. Backend neu bauen: `cd server && docker compose up -d --build` → Migration `0024` + neue Dep `cryptography==44.0.0`. 2. Node-Agent lokal bauen (kein cargo im Container) + Binary committen/ausrollen. 3. Jeden Node neu enrollen: `POST /api/admin/nodes/{name}/regenerate-token` → neuen Install/Enroll-Token nutzen. Alte token-basierte Agents brechen bewusst (Dev, keine Kompat). Backend wurde bewusst NOCH NICHT neu gebaut, um die 6 Live-Agents nicht ungeplant offline zu nehmen — Deploy zusammen mit dem neuen Agent-Binary.
Author
Owner

Verifiziert @ 53fdcfe → erfüllt, wird geschlossen.

  • identity.rs: ed25519-Keypair, Seed root-only /etc/theprox-agent/identity_ed25519
  • agent_ws.py: Nonce-Challenge-Response (per-Connection, single-use), Signatur gegen Node.identity_pubkey; alter Bearer-Token-Pfad komplett entfernt
  • auth/agent_identity.py verify_signature: ed25519 via cryptography, fail-closed (False bei jedem Fehler) ✓
  • /enroll/{token} (main.py:338): single-use (enroll_used_at) + TTL (enroll_expires_at) ✓
  • regenerate-token (admin_router:639): setzt identity_pubkey/enroll_used_at zurück → Re-Enrollment ✓
  • Migration 0024 (1 Head, Kette OK), cryptography==44.0.0 gepinnt ✓
  • Anti-Timing: uniformer verify-Pfad ✓
**Verifiziert @ 53fdcfe → erfüllt, wird geschlossen.** - `identity.rs`: ed25519-Keypair, Seed root-only `/etc/theprox-agent/identity_ed25519` ✓ - `agent_ws.py`: Nonce-Challenge-Response (per-Connection, single-use), Signatur gegen `Node.identity_pubkey`; **alter Bearer-Token-Pfad komplett entfernt** ✓ - `auth/agent_identity.py verify_signature`: ed25519 via cryptography, fail-closed (False bei jedem Fehler) ✓ - `/enroll/{token}` (main.py:338): single-use (`enroll_used_at`) + TTL (`enroll_expires_at`) ✓ - `regenerate-token` (admin_router:639): setzt identity_pubkey/enroll_used_at zurück → Re-Enrollment ✓ - Migration 0024 (1 Head, Kette OK), `cryptography==44.0.0` gepinnt ✓ - Anti-Timing: uniformer verify-Pfad ✓
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
chinux/theProx#185
No description provided.