diff --git a/README.md b/README.md index 7b40a05..375f849 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/docs/av-exclusions.md b/docs/av-exclusions.md index f87612d..6650a1b 100644 --- a/docs/av-exclusions.md +++ b/docs/av-exclusions.md @@ -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 | diff --git a/docs/claude-reference.md b/docs/claude-reference.md index 836d7ba..fb78106 100644 --- a/docs/claude-reference.md +++ b/docs/claude-reference.md @@ -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 | @@ -761,7 +762,7 @@ Admin-Settings-Saves persistieren atomar nach `appsettings.runtime.json` (hängt | `Retention` | ✓ | `Execution`/`AuditLog`/`WorkflowVersions`/`Notification`/`SupportEvent`-RetentionService lesen `IOptionsMonitor.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` | diff --git a/docs/enterprise-features.md b/docs/enterprise-features.md index f96208a..07298d7 100644 --- a/docs/enterprise-features.md +++ b/docs/enterprise-features.md @@ -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`. @@ -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 diff --git a/src/nodepilot-docs-ui/content/deployment/av-exclusions.md b/src/nodepilot-docs-ui/content/deployment/av-exclusions.md index a054023..82fbfa3 100644 --- a/src/nodepilot-docs-ui/content/deployment/av-exclusions.md +++ b/src/nodepilot-docs-ui/content/deployment/av-exclusions.md @@ -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 | diff --git a/src/nodepilot-docs-ui/content/enterprise/high-availability.md b/src/nodepilot-docs-ui/content/enterprise/high-availability.md index 82f4064..8dff9ea 100644 --- a/src/nodepilot-docs-ui/content/enterprise/high-availability.md +++ b/src/nodepilot-docs-ui/content/enterprise/high-availability.md @@ -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 diff --git a/src/nodepilot-docs-ui/content/observability.md b/src/nodepilot-docs-ui/content/observability.md index d4469fc..060ef8b 100644 --- a/src/nodepilot-docs-ui/content/observability.md +++ b/src/nodepilot-docs-ui/content/observability.md @@ -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 |