diff --git a/internal/check/check.go b/internal/check/check.go index ba39c42..c74bb2a 100644 --- a/internal/check/check.go +++ b/internal/check/check.go @@ -103,8 +103,11 @@ func (c *Checker) Run(ctx context.Context, testAuthUser string) *Report { add("Konfiguration", c.Cfg.Validate(), "gelesen und plausibel") // 2. Dateirechte - add("Dateirechte", c.checkPermissions(), - "Konfiguration, Secrets und Log-Verzeichnis sind ausreichend geschützt") + permsDetail := "Konfiguration, Secrets und Log-Verzeichnis sind ausreichend geschützt" + if note := logDirNote(c.Cfg.Logging.AuditLog); note != "" { + permsDetail += "\n" + note + } + add("Dateirechte", c.checkPermissions(), permsDetail) // 3. OPNsense serverTime, fwErr := c.checkFirewall(ctx) @@ -152,18 +155,52 @@ func (c *Checker) checkPermissions() error { } } if c.Cfg.Logging.AuditLog != "" { - dir := filepath.Dir(c.Cfg.Logging.AuditLog) - if _, err := os.Stat(dir); err == nil { - if err := config.CheckDirMode(dir, 0o750); err != nil { - errs = append(errs, err) - } - } else { - errs = append(errs, fmt.Errorf("Log-Verzeichnis %s existiert nicht: %w", dir, err)) + if err := checkLogDir(filepath.Dir(c.Cfg.Logging.AuditLog)); err != nil { + errs = append(errs, err) } } return errors.Join(errs...) } +// checkLogDir prüft das Verzeichnis des Audit-Logs. +// +// Der Audit-Logger legt sein Verzeichnis beim Start selbst an. Der Check darf +// deshalb nicht strenger sein als die Laufzeit: fehlt nur das letzte +// Pfadelement, ist das in Ordnung. Fehlt auch das übergeordnete Verzeichnis, +// steckt fast immer ein Tippfehler im Pfad — das bleibt ein Fehler. +func checkLogDir(dir string) error { + info, err := os.Stat(dir) + if err == nil { + if !info.IsDir() { + return fmt.Errorf("Log-Pfad %s ist kein Verzeichnis", dir) + } + return config.CheckDirMode(dir, 0o750) + } + if !os.IsNotExist(err) { + return fmt.Errorf("Log-Verzeichnis %s nicht lesbar: %w", dir, err) + } + + parent := filepath.Dir(dir) + parentInfo, perr := os.Stat(parent) + if perr != nil || !parentInfo.IsDir() { + return fmt.Errorf("Log-Verzeichnis %s existiert nicht und kann nicht angelegt werden, "+ + "weil auch %s fehlt — bitte logging.audit_log prüfen", dir, parent) + } + return nil +} + +// logDirNote beschreibt den Zustand des Log-Verzeichnisses für die Ausgabe. +func logDirNote(auditLog string) string { + if auditLog == "" { + return "" + } + dir := filepath.Dir(auditLog) + if _, err := os.Stat(dir); os.IsNotExist(err) { + return fmt.Sprintf("Log-Verzeichnis %s wird beim Start angelegt", dir) + } + return "" +} + // checkFirewall ruft den Export-Endpunkt auf und übersetzt die Fehler in // handlungsleitende Meldungen. func (c *Checker) checkFirewall(ctx context.Context) (time.Time, error) { diff --git a/internal/check/check_test.go b/internal/check/check_test.go index 039324d..5d9def6 100644 --- a/internal/check/check_test.go +++ b/internal/check/check_test.go @@ -152,10 +152,43 @@ func TestLooseTLSKeyIsDetected(t *testing.T) { func TestMissingLogDirectoryIsDetected(t *testing.T) { c := newChecker(t, &fakePinger{at: checkNow}, &fakeDir{dn: "CN=x"}) + // Auch das übergeordnete Verzeichnis existiert nicht — das ist ein + // echter Konfigurationsfehler, meist ein Tippfehler im Pfad. c.Cfg.Logging.AuditLog = "/gibt/es/nicht/audit.log" rep := c.Run(context.Background(), "") if rep.OK() { - t.Fatal("fehlendes Log-Verzeichnis muss auffallen") + t.Fatal("fehlendes Log-Verzeichnis mit fehlendem Elternverzeichnis muss auffallen") + } + if out := report(t, rep); !strings.Contains(out, "/gibt/es/nicht") { + t.Errorf("der beanstandete Pfad muss genannt werden:\n%s", out) + } +} + +func TestLogDirectoryIsAcceptedIfItCanBeCreated(t *testing.T) { + // Der Audit-Logger legt sein Verzeichnis beim Start selbst an. Der Check + // darf deshalb nicht strenger sein als die Laufzeit: existiert das + // Elternverzeichnis, ist alles in Ordnung. + c := newChecker(t, &fakePinger{at: checkNow}, + &fakeDir{dn: "CN=VPN-Users,DC=firma,DC=local"}) + parent := filepath.Dir(filepath.Dir(c.Cfg.Logging.AuditLog)) + c.Cfg.Logging.AuditLog = filepath.Join(parent, "nochnichtda", "audit.log") + + rep := c.Run(context.Background(), "") + if !rep.OK() { + t.Fatalf("noch nicht angelegtes Log-Verzeichnis darf nicht scheitern:\n%s", report(t, rep)) + } + if out := report(t, rep); !strings.Contains(out, "wird beim Start angelegt") { + t.Errorf("der Hinweis auf die spätere Anlage fehlt:\n%s", out) + } +} + +func TestLogDirectoryWithLoosePermissionsStillFails(t *testing.T) { + // Ein existierendes, zu offenes Verzeichnis bleibt ein Fehler. + c := newChecker(t, &fakePinger{at: checkNow}, &fakeDir{dn: "CN=x"}) + os.Chmod(filepath.Dir(c.Cfg.Logging.AuditLog), 0o777) + rep := c.Run(context.Background(), "") + if rep.OK() { + t.Fatal("0777-Log-Verzeichnis muss weiterhin auffallen") } }