Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1179,6 +1179,7 @@ All settings live in [`src/NodePilot.Api/appsettings.json`](src/NodePilot.Api/ap
| `Logging:StepDetail:MaxOutputChars` | `10000` | Truncation limit for step output |
| `OpenTelemetry:Enabled` | `false` | Enable OTLP export & instrumentation |
| `OpenTelemetry:Exporters:PrometheusScrape` | `false` | Enable `GET /metrics` |
| `OpenTelemetry:RedactHostnames` | `true` | Keep the host name out of telemetry: `service.instance.id` becomes a process-stable random id instead of `hostname:pid`, the `host.name` resource attribute is dropped, and the Serilog bridge suppresses the field. Set to `false` to restore host attribution in Tempo/Grafana |

### Retention

Expand Down
2 changes: 1 addition & 1 deletion docs/av-exclusions.md
Original file line number Diff line number Diff line change
Expand Up @@ -185,7 +185,7 @@ Diese Einträge lassen sich auf ein Wartungsfenster befristen.

| Pfad | Rolle | Zweck | Priorität | Restrisiko |
|---|---|---|---|---|
| `%TEMP%\nodepilot-artifact-*\` | Server | Entpacktes, signaturgeprüftes Installationsartefakt | Pflicht während der Installation | Der Inhalt wurde vor dem Entpacken gegen einen festgelegten Signer-Thumbprint geprüft. Befristung empfohlen |
| `%TEMP%\nodepilot-artifact-*\` | Server | Entpacktes, signaturgeprüftes Installationsartefakt — Installer **und** Updater nutzen denselben Pfad (Details und Begründung in A.1) | Empfohlen, für Installation und Update | Der Inhalt wurde vor dem Entpacken gegen einen festgelegten Signer-Thumbprint geprüft. Befristung empfohlen |
| `%TEMP%\nodepilot-provision.log` | Desktop | Mitschrift der Einrichtung, für die Fehlersuche | Empfohlen | Reine Textdatei |
| `NodePilot-Desktop-Setup-*.exe` | Desktop | Der Offline-Installer | Empfohlen | Signiertes Setup; Publisher-Regel bevorzugen |
| `unins000.exe` in `C:\Program Files\NodePilot\` | Desktop | Deinstallationsroutine | Empfohlen | gering |
Expand Down
5 changes: 3 additions & 2 deletions docs/claude-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -619,11 +619,12 @@ Die Guard-Flags sind **hardened by default**: appsettings.json shippt sie als `t
| `RestApi:BlockPrivateNetworks` | `true` | Blockiert RFC1918/Loopback in `restApi` (Dev: `false`) |
| `RestApi:AllowedHosts` | `[]` | Exakte Host-/IP-Allow-Liste für tatsächlich proxied `restApi`-Ziele/Redirects — die Ausnahme von `BlockPrivateNetworks`. Link-Local/Cloud-Metadata bleibt immer gesperrt (Dev: `localhost`/`127.0.0.1`/`::1`) |
| `WaitForCondition:AllowedHosts` | `["localhost"]` | **Eigene** Liste für die PowerShell-Probes `portOpen`/`httpOk`. Diese können das Ziel beim Connect nicht erneut prüfen (kein `ConnectCallback`), akzeptieren deshalb nur exakt gelistete Hosts; leere Liste lehnt jede Probe ab. Bewusst getrennt von `RestApi:AllowedHosts`, damit „eigenen Dienst prüfen" nicht zugleich `restApi` zu Loopback öffnet — dessen URLs können aus Trigger-Payloads stammen. **Alleinige Autorität für beide Probe-Typen:** `httpOk` lief bis 2026-08-07 zusätzlich durch `NetworkGuard.ValidateUrl` (den restApi-SSRF-Guard) und war damit auf Loopback/RFC1918 unabhängig von dieser Liste blockiert — der ausgelieferte `localhost`-Default war auf jeder Nicht-Dev-Instanz wirkungslos und die Fehlermeldung nannte den falschen Key (`RestApi:BlockPrivateNetworks`, dazu restart-pflichtig). Heute prüft `NetworkGuard.ValidateProbeUrl` nur URL-Form, Scheme und diese Liste. Link-Local/Cloud-Metadata bleibt für beide Typen ungeachtet der Liste gesperrt. Vergleich exakt: `127.0.0.1` deckt `localhost` nicht ab (Dev: alle drei Schreibweisen) |
| `FileSystemOperation:RejectTraversal` | `true` | Lehnt `..` in File-System-Op-Paths ab (Dev: `false`) |
| `FileSystemOperation:RejectTraversal` | `true` | Lehnt `..` in File-System-Op-Paths ab (Dev: `false`). Zusammen mit `FileSystemOperation:AllowedRoots` (Root-Containment + Reparse-Point-Sperre, aktiv auch bei leerer Root-Liste) gilt die Prüfung **zweimal**: lokal in `PathGuard` und noch einmal im PowerShell-Kontext des tatsächlichen WinRM-Ziels (`TargetPathGuardScript`, injiziert von den pfadführenden Activities `fileOperation`/`folderOperation`, `textFileEdit`, `fileHash`, `zipOperation`, `startProgram`). Ein nicht-leerer konfigurierter Root muss auf dem Ziel existieren, sonst schlägt der Step fehl |
| `SqlActivity:RequireConnectionRef` | `true` | Nur benannte `connectionRef` statt inline `connectionString` (Dev: `false`) |
| `StartProgram:DisallowShellExecute` | `true` | Verwirft `useShellExecute=true` (Dev: `false`) |
| `Trigger:Database:RequireConnectionRef` | `true` | Nur benannte `connectionRef` für `databaseTrigger` (Dev: `false`) |
| `Security:StrictAllowedHosts` | `true` | Boot-Abbruch bei unsicherem `AllowedHosts` (z.B. `*`) (Dev: `false`) |
| `OpenTelemetry:RedactHostnames` | `true` | Hält den Hostnamen aus der Telemetrie: `service.instance.id` wird eine prozessstabile Zufalls-Id statt `hostname:pid`, das Resource-Attribut `host.name` entfällt, die Serilog-Bridge unterdrückt das Feld. Produktionsempfehlung ist der Default; `false` nur, wenn Host-Attribution in Tempo/Grafana gebraucht wird (siehe `docs/siem-logging.md`) |
| `Webhook:RequireSecret` | `true` | `webhookTrigger` erzwingt ein konfiguriertes Secret — verifiziert je nach `signatureMode` als `X-Webhook-Secret`-Header oder NodePilot-HMAC-v2-Signatur (Dev: `false`) |
| `Database:AllowInsecureTls` | `false` | Relaxation: deaktiviert die strikte DB-TLS-Prüfung (`Encrypt=Strict` / `SSL Mode=VerifyFull`) — greift nur bei Loopback-Host + (Development **oder** `Deployment:Mode=Desktop`). Prod-Default fail-closed; Dev: `true` |
| `OpenTelemetry:Exporters:PrometheusScrapeAllowAnonymous` | `false` | `/metrics` anonym erreichbar |
Expand Down Expand Up @@ -761,7 +762,7 @@ Admin-Settings-Saves persistieren atomar nach `appsettings.runtime.json` (hängt
| `Retention` | ✓ | `Execution`/`AuditLog`/`WorkflowVersions`/`Notification`/`SupportEvent`-RetentionService lesen `IOptionsMonitor<RetentionOptions>.CurrentValue` pro Schleifen-Pass (`RunIterationAsync`-Seam); `ArchivePath`-Wechsel invalidiert den Cache → Re-Probe (AuditLog bewahrt Compliance-Invariante). `IdempotencyKeyCleanupService` bleibt bewusst config-frei (fixe 24h-TTL) |
| `Stats` | ✓ | `WorkflowStatsRefresher` liest `IConfiguration.GetValue` pro Pass |
| `Threading` | ✓ | `ThreadPoolTuningService` re-appliert `ThreadPool.SetMinThreads` bei Start + `ChangeToken.OnChange` (Boot-Call bleibt für Cold-Start-Prewarm). **Nur bei `Performance:ManualTuning=true`** — unter Auto-Dimensionierung folgt der Service dem Boot-Plan, sonst würde ein Reload allein den ThreadPool in einen anderen Modus ziehen als Runspace-Pool und Dispatch-Queue |
| `FileSystemOperation` | ✓ | `PathGuard` liest `FileSystemOperation:RejectTraversal`/`AllowedRoots` pro Use aus `IConfiguration` |
| `FileSystemOperation` | ✓ | `PathGuard` liest `FileSystemOperation:RejectTraversal`/`AllowedRoots` pro Use aus `IConfiguration`. Der Remote-Zweig baut daraus pro Step den injizierten `TargetPathGuardScript` — auch dort greift eine Änderung ohne Restart, sie wirkt aber erst beim nächsten Step-Start |
| `WaitForCondition` | ✓ | `NetworkGuard.RequireExplicitlyAllowlistedHost` liest `WaitForCondition:AllowedHosts` aus der Live-`IConfiguration` bei jedem Probe-Aufruf — gilt sofort ohne Restart. Gilt auch für `httpOk` (über `ValidateProbeUrl`); die restart-pflichtige `RestApi`-Sektion liegt nicht mehr im Pfad, sonst hätte eine hot-reload beworbene Karte einen Restart erzwungen |
| `SqlActivity` | ✓ | `SqlActivity` liest `SqlActivity:RequireConnectionRef` pro Use aus `IConfiguration` |
| `StartProgram` | ✓ | `StartProgramActivity` liest `StartProgram:DisallowShellExecute` pro Use aus `IConfiguration` |
Expand Down
6 changes: 6 additions & 0 deletions docs/enterprise-features.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,11 @@ jedoch bewusst auf dasselbe Identitäts-, Session-, Membership- und Offboarding-
- **`LeaderRequiredMiddleware`** (`src/NodePilot.Api/Security/`) blockt jeden mutierenden
Pfad auf einem Follower mit 503. Erlaubt: `/healthz/*`, `/openapi/*`, read-only Endpoints.
Defense-in-Depth — der Loadbalancer sollte Follower eh nicht ansprechen.
**Endpoint-Metadata schlägt Pfad-Heuristik:** die Middleware prüft zuerst auf ein
`[LeaderOnly]` (`Security/LeaderOnlyAttribute.cs`) am Endpoint und erst danach die
Methode/Pfad-Regeln. Nötig für jeden Endpoint, dessen HTTP-Verb harmlos aussieht, der
aber tatsächlich Zustand ändert — ein `GET`-Webhook-Ingress ist genau dieser Fall
(`WebhooksController` trägt das Attribut). Neue semantisch mutierende GETs bekommen es.
- **`ClusterFailoverRecoveryHost`** subscribed im **Constructor** (nicht in `StartAsync`,
damit das erste Acquire-Event nicht in eine leere Handler-Liste feuert) auf
`OnLeadershipAcquired` und ruft `StartupRecovery.RecoverOrphanedExecutionsAsync`.
Expand Down Expand Up @@ -94,6 +99,7 @@ TTL=30s + Renew=10s + Sweep=~5s → ~45s Worst Case.
- [src/NodePilot.Api/Hosting/ClusterFailoverRecoveryHost.cs](../src/NodePilot.Api/Hosting/ClusterFailoverRecoveryHost.cs)
- [src/NodePilot.Api/Hosting/ClusterFencingHost.cs](../src/NodePilot.Api/Hosting/ClusterFencingHost.cs)
- [src/NodePilot.Api/Security/LeaderRequiredMiddleware.cs](../src/NodePilot.Api/Security/LeaderRequiredMiddleware.cs)
- [src/NodePilot.Api/Security/LeaderOnlyAttribute.cs](../src/NodePilot.Api/Security/LeaderOnlyAttribute.cs)
- [src/NodePilot.Engine/Execution/StartupRecovery.cs](../src/NodePilot.Engine/Execution/StartupRecovery.cs)

### Bewusst nicht in Scope
Expand Down
2 changes: 1 addition & 1 deletion src/nodepilot-docs-ui/content/deployment/av-exclusions.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ Die **Standard-Ausführungsart schreibt keine temporäre Datei und startet keine

| Pfad | Betriebsart | Priorität |
|---|---|---|
| `%TEMP%\nodepilot-artifact-*` | Server | Pflicht während der Installation |
| `%TEMP%\nodepilot-artifact-*` (Installer **und** Updater, siehe A.1) | Server | Empfohlen, für Installation und Update |
| `%TEMP%\nodepilot-provision.log` | Desktop | Empfohlen |
| `NodePilot-Desktop-Setup-*.exe`, `unins000.exe` | Desktop | Empfohlen |
| `C:\ProgramData\NodePilot\backups\pre-update-*.dump`, `…\rollback` | Desktop | Empfohlen |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ Das Zertifikat mit Private Key muss auf jedem Node in `LocalMachine\My` vorhande
- **`ClusterLeaderService`** ist `BackgroundService` (Renew-Loop) und `IClusterStateProvider` ("am I leader?").
- Lease-Acquire/Renew: atomares `UPDATE … WHERE OwnerNodeId = me AND ExpiresAt > now` — zwei Nodes können nicht gleichzeitig Leader sein.
- **DB-Clock, nicht App-Clock:** vor jeder Lease-Operation wird `SYSUTCDATETIME()` (SQL Server) bzw. `now() AT TIME ZONE 'UTC'` (Postgres) gelesen → kein Split-Brain bei divergenten Wall-Clocks.
- **`LeaderRequiredMiddleware`** blockt jeden mutierenden Pfad auf einem Follower mit 503. Erlaubt: `/healthz/*`, `/openapi/*`, Read-Only-Endpoints.
- **`LeaderRequiredMiddleware`** blockt jeden mutierenden Pfad auf einem Follower mit 503. Erlaubt: `/healthz/*`, `/openapi/*`, Read-Only-Endpoints. Ein `[LeaderOnly]` am Endpoint gewinnt vor der Pfad-Heuristik — für Endpoints, deren HTTP-Verb harmlos aussieht, die aber Zustand ändern (Webhook-Ingress per `GET`).
- **`ClusterLeader`**-Tabelle mit Single-Row-Sentinel `Resource='primary'`, geseedet im `MigrationBootstrapper`.

## Health-Probe
Expand Down
4 changes: 4 additions & 0 deletions src/nodepilot-docs-ui/content/observability.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ OpenTelemetry ist opt-in. Setup in `NodePilot.Telemetry` — Constants, Options,

`OpenTelemetry:Exporters:PrometheusScrapeAllowAnonymous` (default `false`) — `/metrics` ist **nicht** anonym erreichbar. Auf `true` setzen, wenn der Scraper ohne Auth zugreifen soll (Relaxation, bewusst setzen).

## Hostname-Redaktion

`OpenTelemetry:RedactHostnames` (default `true`) hält den Hostnamen aus der Telemetrie: `service.instance.id` ist eine prozessstabile Zufalls-Id statt `hostname:pid`, das Resource-Attribut `host.name` entfällt, und die Serilog-Bridge unterdrückt das Feld. Wer Host-Attribution in Tempo/Grafana braucht, setzt den Wert bewusst auf `false` — nach einem Upgrade verschwindet die Zuordnung sonst ohne weiteren Hinweis.

## Observability-API

| Endpoint | Zweck |
Expand Down
Loading