Make pulse-sensor-proxy resilient to read-only filesystems

Related to #637

The sensor-proxy was failing to start on systems with read-only filesystems
because audit logging required a writable /var/log/pulse/sensor-proxy directory.

Changes:
- Modified newAuditLogger() to automatically fall back to stderr (systemd journal)
  if the audit log file cannot be opened
- Removed error return from newAuditLogger() since it now always succeeds
- Added warning logs when fallback mode is used to alert operators
- Updated tests to handle the new signature
- Added better debugging to audit log tests

This allows the sensor-proxy to run on:
- Immutable/read-only root filesystems
- Hardened systems with restricted /var mounts
- Containerized environments with limited write access

Audit events are still captured via systemd journal when file logging is
unavailable, maintaining the security audit trail.
This commit is contained in:
rcourtman 2025-11-06 00:18:51 +00:00
parent af55362009
commit 5b89b2371a
3 changed files with 48 additions and 16 deletions

View file

@ -47,18 +47,46 @@ type AuditEvent struct {
}
// newAuditLogger opens the audit log file and prepares hash chaining.
func newAuditLogger(path string) (*auditLogger, error) {
// If the file cannot be opened (e.g., read-only filesystem), it automatically
// falls back to stderr which integrates with systemd journal.
// This function always succeeds and returns a valid audit logger.
func newAuditLogger(path string) *auditLogger {
// Try to open the audit log file
file, err := os.OpenFile(path, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0o640)
var writer zerolog.Logger
var usedFallback bool
if err != nil {
return nil, err
// Fallback to stderr if file cannot be opened
log.Warn().
Err(err).
Str("path", path).
Msg("Cannot open audit log file, falling back to stderr (systemd journal)")
writer = zerolog.New(os.Stderr).With().Timestamp().Logger()
usedFallback = true
file = nil
} else {
writer = zerolog.New(file).With().Timestamp().Logger()
}
writer := zerolog.New(file).With().Timestamp().Logger()
// Log initialization event to standard logger (not to audit log itself)
if usedFallback {
log.Warn().
Str("path", path).
Str("mode", "stderr").
Msg("Audit logger initialized with stderr fallback due to filesystem constraints")
} else {
log.Info().
Str("path", path).
Str("mode", "file").
Msg("Audit logger initialized with file backend")
}
return &auditLogger{
file: file,
logger: writer,
}, nil
}
}
// Close flushes and closes the audit log file.

View file

@ -18,10 +18,7 @@ func TestAuditLogValidationFailure(t *testing.T) {
tmp.Close()
defer os.Remove(path)
logger, err := newAuditLogger(path)
if err != nil {
t.Fatalf("newAuditLogger: %v", err)
}
logger := newAuditLogger(path)
cred := &peerCredentials{uid: 1000, gid: 1000, pid: 4242}
logger.LogValidationFailure("corr-123", cred, "remote", "get_temperature", []string{"node"}, "invalid_node")
@ -35,16 +32,25 @@ func TestAuditLogValidationFailure(t *testing.T) {
scanner := bufio.NewScanner(file)
if !scanner.Scan() {
t.Fatalf("expected at least one audit entry")
t.Fatalf("expected at least one audit entry (file may be empty)")
}
line := scanner.Bytes()
if len(line) == 0 {
t.Fatalf("empty line in audit log")
}
t.Logf("Audit log line: %s", string(line))
var record auditRecord
if err := json.Unmarshal(scanner.Bytes(), &record); err != nil {
t.Fatalf("unmarshal: %v", err)
if err := json.Unmarshal(line, &record); err != nil {
t.Fatalf("unmarshal (line=%s): %v", string(line), err)
}
t.Logf("Parsed record: %+v", record)
if record["event_type"] != "command.validation_failed" {
t.Fatalf("unexpected event_type: %v", record["event_type"])
t.Fatalf("unexpected event_type: %v (full record: %+v)", record["event_type"], record)
}
if record["correlation_id"] != "corr-123" {
t.Fatalf("unexpected correlation id: %v", record["correlation_id"])

View file

@ -354,10 +354,8 @@ func runProxy() {
auditPath = defaultAuditLogPath
}
auditLogger, err := newAuditLogger(auditPath)
if err != nil {
log.Fatal().Err(err).Str("path", auditPath).Msg("Failed to initialize audit logger")
}
// Initialize audit logger with automatic fallback to stderr if file is unavailable
auditLogger := newAuditLogger(auditPath)
defer auditLogger.Close()
// Initialize metrics