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:
parent
af55362009
commit
5b89b2371a
3 changed files with 48 additions and 16 deletions
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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"])
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in a new issue