| index a349319..1b8628b 100644 |
| --- a/bootstrap.go |
| +++ b/bootstrap.go |
| @@ -200,7 +200,19 @@ var collectorTimers = []string{"lchat-ssh-ledger.timer", "lchat-hardening.timer" |
| // appDir is the on-box install root (/opt/lchat): each unit's ExecStart points at |
| // <app-dir>/<script>, and the collectors upload the pages from <app-dir>/status- |
| // pages. unitDir is the systemd unit directory. |
| -func writeCollectorFiles(appDir, unitDir string) { |
| +func writeCollectorFiles(appDir, unitDir, dataDir string) { |
| + // The collectors' state dirs must exist BEFORE their units first start: |
| + // each unit lists its dir in ReadWritePaths, and on the fleet's systemd a |
| + // missing ReadWritePaths target fails namespace setup for the WHOLE unit — |
| + // including the `ExecStartPre=+mkdir` that was supposed to create it (the |
| + // `+` doesn't escape mount-namespace construction there). Found on the |
| + // hardening collector's first real-box run, 2026-07-19. |
| + for _, d := range []string{filepath.Join(dataDir, "ssh-ledger"), filepath.Join(dataDir, "hardening")} { |
| + if err := os.MkdirAll(d, 0o755); err != nil { |
| + die("bootstrap: mkdir %s: %v", d, err) |
| + } |
| + } |
| + |
| // The scripts are executable — their units fire them via ExecStart. |
| writeFile(filepath.Join(appDir, "ssh-ledger-collect.sh"), scriptSSHLedger, 0o755) |
| writeFile(filepath.Join(appDir, "hardening-posture.sh"), scriptHardening, 0o755) |
| @@ -263,7 +275,7 @@ func runBootstrap(args []string) { |
| // install root, /opt/lchat) is where the collectors' units expect their |
| // scripts + status-pages dir. |
| if *collectorsOnly { |
| - writeCollectorFiles(optDir, *unitDir) |
| + writeCollectorFiles(optDir, *unitDir, *dataDir) |
| if err := systemctl("daemon-reload"); err != nil { |
| die("bootstrap: %v", err) |
| } |
| @@ -311,7 +323,7 @@ func runBootstrap(args []string) { |
| // The status collectors ride along here so the single daemon-reload below |
| // registers their units too; --no-start still lays down the files (like every |
| // other unit), and the started path enables their timers. |
| - writeCollectorFiles(optDir, *unitDir) |
| + writeCollectorFiles(optDir, *unitDir, *dataDir) |
| |
| if err := ensureControlToken(); err != nil { |
| die("bootstrap: control token: %v", err) |