[Bug] Agent enrollt bei unbekanntem Pubkey nicht neu → Dauerschleife type=challenge + Rate-Limit (Re-Enroll-Sackgasse) #195

Closed
opened 2026-06-26 19:32:00 +00:00 by chinux · 3 comments
Owner

Bug — Re-Enroll-Sackgasse (live reproduziert an ITD-PROX01)

Wenn der Backend-Pubkey eines Nodes geleert wird (via regenerate-token, #185) ODER aus anderem Grund nicht (mehr) zur lokalen Identity passt, läuft der Agent in eine Endlosschleife statt neu zu enrollen:

  • Agent hat eine Identity-Datei (/etc/theprox-agent/identity_ed25519) → freshly_created=falsekein Enroll-Versuch (identity.rs:21, main.rs:105).
  • Connect → Challenge-Auth schlägt fehl: auth rejected (type=challenge) (Backend kennt den Pubkey nicht).
  • Das zählt als Fehlversuch → nach wenigen Versuchen greift der Rate-Limiter (#181): auth rejected (type=auth_fail): Rate limit.
  • Agent hämmert weiter (systemd Restart=always + Reconnect), kommt aber nie aus dem Zustand raus → Node bleibt dauerhaft offline.

Kernproblem: "Backend kennt meinen Pubkey nicht" (type=challenge) wird wie ein normaler Auth-Fehler behandelt — der Agent versucht es stur mit demselben (im Backend unbekannten) Key erneut, statt neu zu enrollen. Der Enroll-Pfad wird nur bei freshly_created betreten.

Verschärfend

  • Manueller Ausweg ist umständlich: Agent stoppen, Rate-Limit auslaufen lassen, Identity-Datei löschen, regenerate-token, neu installieren. Das ist nicht zumutbar für normalen Betrieb.
  • Re-Install räumt die Identity-Datei NICHT (Install-Script entfernt sie nicht).

Gewünschtes Verhalten

Bei type=challenge-Ablehnung (= "Pubkey unbekannt/passt nicht") soll der Agent automatisch einen Re-Enroll versuchen, statt in den Rate-Limit zu laufen:

  1. Agent-seitig (Kern): Erhält der Agent type=challenge (klar abgrenzbar von "falsche Signatur" und von Rate-Limit), und liegt ein gültiger Enroll-Token in der Config vor → Identity-Datei verwerfen + neues Keypair erzeugen + /enroll/{token} aufrufen + mit neuem Key verbinden. (Nicht bei type=auth_fail/Rate-Limit — da nur warten.)
    • Schutz gegen Enroll-Schleife: Re-Enroll pro Token-Wert nur 1x; schlägt der Enroll fehl (Token verbraucht/abgelaufen) → langsamer Backoff + klare Logmeldung "Enroll-Token verbraucht/abgelaufen — regenerate-token nötig", NICHT endlos.
  2. Token-Konsistenz: Ist der Enroll-Token single-use und schon verbrannt (häufig nach erstem erfolgreichem Enroll), kann der Agent nicht selbst re-enrollen → dann ist die klare Logmeldung + Backoff das richtige Verhalten (Betreiber muss regenerate-token). Das ist ok, solange es NICHT als Rate-Limit-Hämmern endet.
  3. Install-Script (#-ergänzend): Beim Install mit FRISCHEM Token die alte Identity-Datei räumen (oder: Agent erkennt "agent.conf-Token != der Token, mit dem ich enrollt war" → Re-Enroll). So macht ein Re-Install das Erwartete.
  4. Rate-Limit-Abgrenzung (#181): Ein Node, der gerade (legitim) re-enrollt, darf nicht durch den Auth-Rate-Limiter dauerhaft ausgesperrt werden. Ggf. erfolgreiche Enroll-Aktion setzt den Fail-Zähler der IP zurück.

Reproduktion

ITD-PROX01 (VPN-Node): nach regenerate-token + gelöschter Identity-Datei + altem Token in agent.conf → Dauerschleife aus type=challenge + Rate limit, kein Enroll-Log (enrollment failed taucht NICHT auf, weil freshly_created=false war / Token verbrannt).

Akzeptanz

  • Node mit im Backend unbekanntem Pubkey + gültigem Enroll-Token → Agent enrollt automatisch neu und kommt online, ohne manuellen Eingriff.
  • Verbrauchter/abgelaufener Token → klare Logmeldung + Backoff, KEIN Rate-Limit-Dauerhämmern.
  • Re-Install mit frischem Token räumt/ersetzt die alte Identity sauber.
  • type=challenge, type=auth_fail (Rate-Limit) und Signatur-Fehler werden im Agent klar unterschieden und unterschiedlich behandelt.

Bezug

#185 (Challenge-Response/Enroll), #181 (Rate-Limit), #193/#194 (regenerate-token URL/UI). Kein Public-Blocker, aber macht das Neu-Ausrollen/Re-Enrollen von Nodes aktuell praktisch kaputt.

Branch

fix/agent-reenroll-on-unknown-pubkey

## Bug — Re-Enroll-Sackgasse (live reproduziert an ITD-PROX01) Wenn der Backend-Pubkey eines Nodes geleert wird (via `regenerate-token`, #185) ODER aus anderem Grund nicht (mehr) zur lokalen Identity passt, läuft der Agent in eine **Endlosschleife** statt neu zu enrollen: - Agent hat eine Identity-Datei (`/etc/theprox-agent/identity_ed25519`) → `freshly_created=false` → **kein** Enroll-Versuch (identity.rs:21, main.rs:105). - Connect → Challenge-Auth schlägt fehl: `auth rejected (type=challenge)` (Backend kennt den Pubkey nicht). - Das zählt als Fehlversuch → nach wenigen Versuchen greift der **Rate-Limiter (#181)**: `auth rejected (type=auth_fail): Rate limit`. - Agent hämmert weiter (systemd Restart=always + Reconnect), kommt aber nie aus dem Zustand raus → Node bleibt dauerhaft offline. **Kernproblem:** "Backend kennt meinen Pubkey nicht" (type=challenge) wird wie ein normaler Auth-Fehler behandelt — der Agent versucht es stur mit demselben (im Backend unbekannten) Key erneut, statt **neu zu enrollen**. Der Enroll-Pfad wird nur bei `freshly_created` betreten. ## Verschärfend - Manueller Ausweg ist umständlich: Agent stoppen, Rate-Limit auslaufen lassen, Identity-Datei löschen, `regenerate-token`, neu installieren. Das ist nicht zumutbar für normalen Betrieb. - Re-Install räumt die Identity-Datei NICHT (Install-Script entfernt sie nicht). ## Gewünschtes Verhalten Bei `type=challenge`-Ablehnung (= "Pubkey unbekannt/passt nicht") soll der Agent **automatisch einen Re-Enroll versuchen**, statt in den Rate-Limit zu laufen: 1. **Agent-seitig (Kern):** Erhält der Agent `type=challenge` (klar abgrenzbar von "falsche Signatur" und von Rate-Limit), und liegt ein **gültiger Enroll-Token** in der Config vor → Identity-Datei verwerfen + neues Keypair erzeugen + `/enroll/{token}` aufrufen + mit neuem Key verbinden. (Nicht bei `type=auth_fail`/Rate-Limit — da nur warten.) - Schutz gegen Enroll-Schleife: Re-Enroll pro Token-Wert nur 1x; schlägt der Enroll fehl (Token verbraucht/abgelaufen) → langsamer Backoff + klare Logmeldung "Enroll-Token verbraucht/abgelaufen — regenerate-token nötig", NICHT endlos. 2. **Token-Konsistenz:** Ist der Enroll-Token single-use und schon verbrannt (häufig nach erstem erfolgreichem Enroll), kann der Agent nicht selbst re-enrollen → dann ist die klare Logmeldung + Backoff das richtige Verhalten (Betreiber muss regenerate-token). Das ist ok, solange es NICHT als Rate-Limit-Hämmern endet. 3. **Install-Script (#-ergänzend):** Beim Install mit FRISCHEM Token die alte Identity-Datei räumen (oder: Agent erkennt "agent.conf-Token != der Token, mit dem ich enrollt war" → Re-Enroll). So macht ein Re-Install das Erwartete. 4. **Rate-Limit-Abgrenzung (#181):** Ein Node, der gerade (legitim) re-enrollt, darf nicht durch den Auth-Rate-Limiter dauerhaft ausgesperrt werden. Ggf. erfolgreiche Enroll-Aktion setzt den Fail-Zähler der IP zurück. ## Reproduktion ITD-PROX01 (VPN-Node): nach `regenerate-token` + gelöschter Identity-Datei + altem Token in agent.conf → Dauerschleife aus `type=challenge` + `Rate limit`, kein Enroll-Log (`enrollment failed` taucht NICHT auf, weil freshly_created=false war / Token verbrannt). ## Akzeptanz - Node mit im Backend unbekanntem Pubkey + gültigem Enroll-Token → Agent enrollt automatisch neu und kommt online, ohne manuellen Eingriff. - Verbrauchter/abgelaufener Token → klare Logmeldung + Backoff, KEIN Rate-Limit-Dauerhämmern. - Re-Install mit frischem Token räumt/ersetzt die alte Identity sauber. - type=challenge, type=auth_fail (Rate-Limit) und Signatur-Fehler werden im Agent klar unterschieden und unterschiedlich behandelt. ## Bezug #185 (Challenge-Response/Enroll), #181 (Rate-Limit), #193/#194 (regenerate-token URL/UI). Kein Public-Blocker, aber macht das Neu-Ausrollen/Re-Enrollen von Nodes aktuell praktisch kaputt. ## Branch `fix/agent-reenroll-on-unknown-pubkey`
Author
Owner

Entscheidung: VARIANTE 1 — single-use bleibt, Agent hört auf zu hämmern

Re-Enroll bleibt ein bewusster Betreiber-Akt (regenerate-token). Der Enroll-Token bleibt single-use (#185/#182-Härte unverändert). Der Fix behebt nur die Dauerschleife + Rate-Limit-Self-DoS, nicht die Token-Semantik.

Verifizierte Code-Stellen

  • auth.rs:77 wertet type aus; :107 loggt generisch auth rejected (type={other}). Der Typ (challenge vs auth_fail) ist also bereits unterscheidbar — wird nur nicht unterschiedlich behandelt.
  • main.rs:139-147 Reconnect-Loop mit festem reconnect_delay → konstantes Hämmern, kein Backoff.
  • /enroll ist hart single-use (main.py:390-396: enroll_used_at + node.token=None, sonst 403). Self-Re-Enroll ist konstruktiv unmöglich → "nicht hämmern" ist die korrekte Antwort.

Gewünschtes Verhalten (Variante 1)

  1. Fehlertypen im Agent unterscheiden (auth.rs):
    • type=challenge → Backend kennt den Pubkey nicht / Signatur passt nicht zum registrierten Key. Nicht als transienten Fehler behandeln.
    • type=auth_fail (Rate-Limit) → transient, normal weiter.
  2. Bei type=challenge: in einen langsamen Backoff + klare Logmeldung gehen, statt im festen reconnect_delay weiterzuhämmern:
    • Logmeldung (WARN/ERROR, einmalig prägnant): "Backend kennt diesen Node-Pubkey nicht — Re-Enrollment nötig: in der UI 'Token neu generieren' und Install-Script erneut ausführen."
    • Reconnect-Intervall für diesen Zustand stark erhöhen (z. B. exponentiell bis Deckel 5-15 min), damit der Auth-Rate-Limiter (#181) NICHT durch Eigen-Traffic ausgelöst wird (kein Self-DoS).
  3. Kein Spam: die erklärende Meldung nicht in jeder Iteration voll loggen (einmal prägnant, danach knapp).
  4. Rate-Limit-Wechselwirkung (#181): durch den langen Backoff bei type=challenge darf der Node sich nicht mehr selbst in den Rate-Limit treiben. (Optional, falls einfach: erfolgreicher Handshake setzt den Fail-Zähler dieser IP zurück.)

Ausdrücklich NICHT in Variante 1

  • Token NICHT wiederverwendbar machen.
  • KEIN automatischer Self-Re-Enroll, KEIN automatisches Löschen der Identity-Datei durch den Agent.

Begleitend (klein, sinnvoll mitnehmen)

  • Install-Script soll beim Lauf mit FRISCHEM Token die alte Identity-Datei räumen (rm -f /etc/theprox-agent/identity_ed25519), damit ein bewusstes Re-Install (nach regenerate-token) sauber neu enrollt. Das ist der vorgesehene Re-Enroll-Weg und soll reibungslos funktionieren.

Akzeptanz

  • Node mit im Backend unbekanntem Pubkey: Agent loggt EINMAL klar die Handlungsanweisung und geht in langen Backoff; KEIN Rate-Limit-Dauerhämmern mehr.
  • type=challenge / type=auth_fail / Transport-Fehler werden im Agent klar unterschieden.
  • Re-Install mit frischem Token (nach regenerate-token) räumt die alte Identity und enrollt sauber neu → Node kommt online.
  • Token-Semantik unverändert single-use.

Branch

fix/agent-challenge-backoff-no-selfddos

## Entscheidung: VARIANTE 1 — single-use bleibt, Agent hört auf zu hämmern Re-Enroll bleibt ein **bewusster Betreiber-Akt** (regenerate-token). Der Enroll-Token bleibt **single-use** (#185/#182-Härte unverändert). Der Fix behebt nur die **Dauerschleife + Rate-Limit-Self-DoS**, nicht die Token-Semantik. ## Verifizierte Code-Stellen - `auth.rs:77` wertet `type` aus; `:107` loggt generisch `auth rejected (type={other})`. Der Typ (`challenge` vs `auth_fail`) ist also bereits unterscheidbar — wird nur nicht unterschiedlich behandelt. - `main.rs:139-147` Reconnect-Loop mit festem `reconnect_delay` → konstantes Hämmern, kein Backoff. - `/enroll` ist hart single-use (main.py:390-396: `enroll_used_at` + `node.token=None`, sonst 403). Self-Re-Enroll ist konstruktiv unmöglich → "nicht hämmern" ist die korrekte Antwort. ## Gewünschtes Verhalten (Variante 1) 1. **Fehlertypen im Agent unterscheiden** (auth.rs): - `type=challenge` → Backend kennt den Pubkey nicht / Signatur passt nicht zum registrierten Key. **Nicht** als transienten Fehler behandeln. - `type=auth_fail` (Rate-Limit) → transient, normal weiter. 2. **Bei `type=challenge`: in einen langsamen Backoff + klare Logmeldung gehen**, statt im festen `reconnect_delay` weiterzuhämmern: - Logmeldung (WARN/ERROR, einmalig prägnant): "Backend kennt diesen Node-Pubkey nicht — Re-Enrollment nötig: in der UI 'Token neu generieren' und Install-Script erneut ausführen." - Reconnect-Intervall für diesen Zustand stark erhöhen (z. B. exponentiell bis Deckel 5-15 min), damit der Auth-Rate-Limiter (#181) NICHT durch Eigen-Traffic ausgelöst wird (kein Self-DoS). 3. **Kein Spam:** die erklärende Meldung nicht in jeder Iteration voll loggen (einmal prägnant, danach knapp). 4. **Rate-Limit-Wechselwirkung (#181):** durch den langen Backoff bei `type=challenge` darf der Node sich nicht mehr selbst in den Rate-Limit treiben. (Optional, falls einfach: erfolgreicher Handshake setzt den Fail-Zähler dieser IP zurück.) ## Ausdrücklich NICHT in Variante 1 - Token NICHT wiederverwendbar machen. - KEIN automatischer Self-Re-Enroll, KEIN automatisches Löschen der Identity-Datei durch den Agent. ## Begleitend (klein, sinnvoll mitnehmen) - **Install-Script** soll beim Lauf mit FRISCHEM Token die alte Identity-Datei räumen (`rm -f /etc/theprox-agent/identity_ed25519`), damit ein bewusstes Re-Install (nach regenerate-token) sauber neu enrollt. Das ist der vorgesehene Re-Enroll-Weg und soll reibungslos funktionieren. ## Akzeptanz - Node mit im Backend unbekanntem Pubkey: Agent loggt EINMAL klar die Handlungsanweisung und geht in langen Backoff; KEIN Rate-Limit-Dauerhämmern mehr. - `type=challenge` / `type=auth_fail` / Transport-Fehler werden im Agent klar unterschieden. - Re-Install mit frischem Token (nach regenerate-token) räumt die alte Identity und enrollt sauber neu → Node kommt online. - Token-Semantik unverändert single-use. ## Branch `fix/agent-challenge-backoff-no-selfddos`
Author
Owner

Umgesetzt in zwei Commits auf main:

Teil A — Agent Backoff + Fehlertypen (d5dc082, Crate 2.17.0 → 2.18.0):

  • Neues enum AuthReject { Challenge, RateLimited, Other } (auth_reject.rs).
  • ws.rs: WS-Upgrade HTTP 403 → Challenge. auth.rs: auth_fail VOR Challenge → RateLimited (#181-Block); auth_fail NACH Challenge → Challenge (Pubkey unbekannt/Sig ungültig).
  • main.rs Reconnect-Loop: Challenge → exponentieller Backoff ab reconnect_delay, Deckel 10 min, EINMAL prägnanter WARN (Re-Enroll-Anweisung), danach knapp; RateLimited → Warten bis Block-Ablauf; Transport/sauberes Ende → Reset.
  • Variante 1 eingehalten: single-use bleibt, KEIN Self-Re-Enroll, KEIN Auto-Identity-Löschen durch den laufenden Agent. #185/#181/#189 inhaltlich unverändert.

Teil B — Install-Script räumt Identity (5e0a9e2): generiertes Script entfernt /etc/theprox-agent/identity_ed25519 vor Service-Start → Re-Install mit frischem Token enrollt sauber neu (freshly_created=true).

Verifiziert: cargo check + cargo build --release --target x86_64-unknown-linux-musl grün; Binary 2.18.0 neu gebaut + committed (= advertised latest-version, kein Self-Update-Loop). Bleibt offen für deinen Build/Test auf echtem Node.

Umgesetzt in zwei Commits auf `main`: **Teil A — Agent Backoff + Fehlertypen (`d5dc082`, Crate 2.17.0 → 2.18.0):** - Neues `enum AuthReject { Challenge, RateLimited, Other }` (`auth_reject.rs`). - `ws.rs`: WS-Upgrade HTTP 403 → `Challenge`. `auth.rs`: `auth_fail` VOR Challenge → `RateLimited` (#181-Block); `auth_fail` NACH Challenge → `Challenge` (Pubkey unbekannt/Sig ungültig). - `main.rs` Reconnect-Loop: Challenge → exponentieller Backoff ab `reconnect_delay`, Deckel 10 min, EINMAL prägnanter WARN (Re-Enroll-Anweisung), danach knapp; RateLimited → Warten bis Block-Ablauf; Transport/sauberes Ende → Reset. - **Variante 1 eingehalten**: single-use bleibt, KEIN Self-Re-Enroll, KEIN Auto-Identity-Löschen durch den laufenden Agent. #185/#181/#189 inhaltlich unverändert. **Teil B — Install-Script räumt Identity (`5e0a9e2`):** generiertes Script entfernt `/etc/theprox-agent/identity_ed25519` vor Service-Start → Re-Install mit frischem Token enrollt sauber neu (`freshly_created=true`). **Verifiziert**: `cargo check` + `cargo build --release --target x86_64-unknown-linux-musl` grün; Binary 2.18.0 neu gebaut + committed (= advertised latest-version, kein Self-Update-Loop). Bleibt offen für deinen Build/Test auf echtem Node.
Author
Owner

Verifiziert → erfüllt (Variante 1), wird geschlossen.

  • AuthReject-Enum + challenge/auth_fail-Unterscheidung (auth.rs:41-48) ✓
  • main.rs Reconnect (156-185): Challenge → einmal prägnant loggen (challenge_logged) + exponentieller Backoff *2 .min(BACKOFF_CAP); RateLimited → wartet BACKOFF_CAP; clean → reset. Kein Rate-Limit-Self-DoS mehr ✓
  • Install-Script räumt Identity vor Start (rm -f "$CONF_DIR/identity_ed25519", main.py:922) → Re-Install mit frischem Token enrollt sauber ✓
  • Token-Semantik unverändert single-use ✓
**Verifiziert → erfüllt (Variante 1), wird geschlossen.** - `AuthReject`-Enum + challenge/auth_fail-Unterscheidung (auth.rs:41-48) ✓ - main.rs Reconnect (156-185): Challenge → einmal prägnant loggen (`challenge_logged`) + **exponentieller Backoff** `*2 .min(BACKOFF_CAP)`; RateLimited → wartet BACKOFF_CAP; clean → reset. Kein Rate-Limit-Self-DoS mehr ✓ - Install-Script räumt Identity vor Start (`rm -f "$CONF_DIR/identity_ed25519"`, main.py:922) → Re-Install mit frischem Token enrollt sauber ✓ - Token-Semantik unverändert single-use ✓
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#195
No description provided.