fix(check): Log-Verzeichnis darf beim Start angelegt werden

Der Audit-Logger legt sein Verzeichnis per MkdirAll selbst an, check brach
aber ab, wenn es noch nicht existierte — der Pruefer war strenger als die
Laufzeit und blockierte die Erstinbetriebnahme. Fehlt zusaetzlich das
uebergeordnete Verzeichnis, bleibt es ein Fehler: dann steckt fast immer
ein Tippfehler im Pfad.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NBHF4R9EAejDJUMdwr6C68
This commit is contained in:
Carsten Abele 2026-08-14 10:14:06 +02:00
parent 6694aa5d2e
commit a2bce25424
2 changed files with 80 additions and 10 deletions

View file

@ -103,8 +103,11 @@ func (c *Checker) Run(ctx context.Context, testAuthUser string) *Report {
add("Konfiguration", c.Cfg.Validate(), "gelesen und plausibel") add("Konfiguration", c.Cfg.Validate(), "gelesen und plausibel")
// 2. Dateirechte // 2. Dateirechte
add("Dateirechte", c.checkPermissions(), permsDetail := "Konfiguration, Secrets und Log-Verzeichnis sind ausreichend geschützt"
"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 // 3. OPNsense
serverTime, fwErr := c.checkFirewall(ctx) serverTime, fwErr := c.checkFirewall(ctx)
@ -152,18 +155,52 @@ func (c *Checker) checkPermissions() error {
} }
} }
if c.Cfg.Logging.AuditLog != "" { if c.Cfg.Logging.AuditLog != "" {
dir := filepath.Dir(c.Cfg.Logging.AuditLog) if err := checkLogDir(filepath.Dir(c.Cfg.Logging.AuditLog)); err != nil {
if _, err := os.Stat(dir); err == nil {
if err := config.CheckDirMode(dir, 0o750); err != nil {
errs = append(errs, err) errs = append(errs, err)
} }
} else {
errs = append(errs, fmt.Errorf("Log-Verzeichnis %s existiert nicht: %w", dir, err))
}
} }
return errors.Join(errs...) 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 // checkFirewall ruft den Export-Endpunkt auf und übersetzt die Fehler in
// handlungsleitende Meldungen. // handlungsleitende Meldungen.
func (c *Checker) checkFirewall(ctx context.Context) (time.Time, error) { func (c *Checker) checkFirewall(ctx context.Context) (time.Time, error) {

View file

@ -152,10 +152,43 @@ func TestLooseTLSKeyIsDetected(t *testing.T) {
func TestMissingLogDirectoryIsDetected(t *testing.T) { func TestMissingLogDirectoryIsDetected(t *testing.T) {
c := newChecker(t, &fakePinger{at: checkNow}, &fakeDir{dn: "CN=x"}) 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" c.Cfg.Logging.AuditLog = "/gibt/es/nicht/audit.log"
rep := c.Run(context.Background(), "") rep := c.Run(context.Background(), "")
if rep.OK() { 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")
} }
} }