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.
|
// 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)
|
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 {
|
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{
|
return &auditLogger{
|
||||||
file: file,
|
file: file,
|
||||||
logger: writer,
|
logger: writer,
|
||||||
}, nil
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Close flushes and closes the audit log file.
|
// Close flushes and closes the audit log file.
|
||||||
|
|
|
||||||
|
|
@ -18,10 +18,7 @@ func TestAuditLogValidationFailure(t *testing.T) {
|
||||||
tmp.Close()
|
tmp.Close()
|
||||||
defer os.Remove(path)
|
defer os.Remove(path)
|
||||||
|
|
||||||
logger, err := newAuditLogger(path)
|
logger := newAuditLogger(path)
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("newAuditLogger: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
cred := &peerCredentials{uid: 1000, gid: 1000, pid: 4242}
|
cred := &peerCredentials{uid: 1000, gid: 1000, pid: 4242}
|
||||||
logger.LogValidationFailure("corr-123", cred, "remote", "get_temperature", []string{"node"}, "invalid_node")
|
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)
|
scanner := bufio.NewScanner(file)
|
||||||
if !scanner.Scan() {
|
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
|
var record auditRecord
|
||||||
if err := json.Unmarshal(scanner.Bytes(), &record); err != nil {
|
if err := json.Unmarshal(line, &record); err != nil {
|
||||||
t.Fatalf("unmarshal: %v", err)
|
t.Fatalf("unmarshal (line=%s): %v", string(line), err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
t.Logf("Parsed record: %+v", record)
|
||||||
|
|
||||||
if record["event_type"] != "command.validation_failed" {
|
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" {
|
if record["correlation_id"] != "corr-123" {
|
||||||
t.Fatalf("unexpected correlation id: %v", record["correlation_id"])
|
t.Fatalf("unexpected correlation id: %v", record["correlation_id"])
|
||||||
|
|
|
||||||
|
|
@ -354,10 +354,8 @@ func runProxy() {
|
||||||
auditPath = defaultAuditLogPath
|
auditPath = defaultAuditLogPath
|
||||||
}
|
}
|
||||||
|
|
||||||
auditLogger, err := newAuditLogger(auditPath)
|
// Initialize audit logger with automatic fallback to stderr if file is unavailable
|
||||||
if err != nil {
|
auditLogger := newAuditLogger(auditPath)
|
||||||
log.Fatal().Err(err).Str("path", auditPath).Msg("Failed to initialize audit logger")
|
|
||||||
}
|
|
||||||
defer auditLogger.Close()
|
defer auditLogger.Close()
|
||||||
|
|
||||||
// Initialize metrics
|
// Initialize metrics
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue