Skip to content

Commit

Permalink
feat: Audit log rotation support (#1766)
Browse files Browse the repository at this point in the history
Enables `file` audit logs to be automatically rotated.

Also adds ability to write audit entries to multiple destinations.

Fixes #1758

Signed-off-by: Charith Ellawala <charith@cerbos.dev>
  • Loading branch information
charithe committed Aug 29, 2023
1 parent 1130d12 commit 9652c90
Show file tree
Hide file tree
Showing 8 changed files with 208 additions and 68 deletions.
32 changes: 30 additions & 2 deletions docs/modules/configuration/pages/audit.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,13 @@ audit:
ignoreAll: false # IgnoreAll prevents any plan responses from being logged. Takes precedence over other filters.
ignoreAlwaysAllow: false # IgnoreAlwaysAllow ignores ALWAYS_ALLOWED plans.
backend: local # Audit backend to use.
file: # Configuration for the file audit backend
path: /path/to/file.log # Path to the log file to use as output. The special values stdout and stderr can be used to write to stdout or stderr respectively.
file:
additionalPaths: ["stdout"] # AdditionalPaths to mirror the log output. Has performance implications. Use with caution.
logRotation: # LogRotation settings (optional).
maxFileAgeDays: 10 # MaxFileAgeDays sets the maximum age in days of old log files before they are deleted.
maxFileCount: 10 # MaxFileCount sets the maximum number of files to retain.
maxFileSizeMB: 100 # MaxFileSizeMB sets the maximum size of individual log files in megabytes.
path: /path/to/file.log # Required. Path to the log file to use as output. The special values stdout and stderr can be used to write to stdout or stderr respectively.
local: # Configuration for the local audit backend
storagePath: /path/to/dir # Path to store the data
retentionPeriod: 168h # Records older than this will be automatically deleted
Expand Down Expand Up @@ -95,6 +100,7 @@ The `file` backend writes audit records as newline-delimited JSON to a file or s
NOTE: This backend cannot be queried using the Admin API, `cerbosctl audit` or `cerbosctl decisions`.


.Minimal configuration with file output and no log rotation
[source,yaml,linenums]
----
audit:
Expand All @@ -106,11 +112,33 @@ audit:
path: /path/to/audit.log
----

.Configuration with log rotation and output to both stdout and a file
[source,yaml,linenums]
----
audit:
enabled: true
accessLogsEnabled: true
decisionLogsEnabled: true
backend: file
file:
path: /path/to/file.log
additionalPaths:
- stdout
logRotation:
maxFileAgeDays: 10 # Maximum age in days of old log files before they are deleted.
maxFileCount: 10 # Maximum number of old log files to retain.
maxFileSizeMB: 100 # Maximum size of individual log files in megabytes.
----


The `path` field can be set to special names `stdout` or `stderr` to log to stdout or stderr. Note that this would result in audit logs being mixed up with normal Cerbos operational logs. It is recommended to use an actual file for audit log output if your container orchestrator has support for collecting logs from files in addition to stdout/stderr.

Audit log entries can be selected by setting a filter on `log.logger == "cerbos.audit"`. Access log entries have `log.kind == "access"` and decision log entries have `log.kind == "decision"`.

If log rotation is enabled, `maxFileSizeMB` is the only required setting. If `maxFileCount` and `maxFileAgeDays` settings are not defined, files are never deleted by the Cerbos process.




[#kafka]
== Kafka backend
Expand Down
7 changes: 6 additions & 1 deletion docs/modules/configuration/partials/fullconfiguration.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,12 @@ audit:
excludeMetadataKeys: ['authorization'] # ExcludeMetadataKeys defines which gRPC request metadata keys should be excluded from the audit logs. Takes precedence over includeMetadataKeys.
includeMetadataKeys: ['content-type'] # IncludeMetadataKeys defines which gRPC request metadata keys should be included in the audit logs.
file:
path: /path/to/file.log # Path to the log file to use as output. The special values stdout and stderr can be used to write to stdout or stderr respectively.
additionalPaths: [stdout] # AdditionalPaths to mirror the log output. Has performance implications. Use with caution.
logRotation: # LogRotation settings (optional).
maxFileAgeDays: 10 # MaxFileAgeDays sets the maximum age in days of old log files before they are deleted.
maxFileCount: 10 # MaxFileCount sets the maximum number of files to retain.
maxFileSizeMB: 100 # MaxFileSizeMB sets the maximum size of individual log files in megabytes.
path: /path/to/file.log # Required. Path to the log file to use as output. The special values stdout and stderr can be used to write to stdout or stderr respectively.
kafka:
ack: all # Ack mode for producing messages. Valid values are "none", "leader" or "all" (default). Idempotency is disabled when mode is not "all".
authentication: # Authentication
Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,7 @@ require (
google.golang.org/genproto/googleapis/api v0.0.0-20230822172742-b8732ec3820d
google.golang.org/grpc v1.57.0
google.golang.org/protobuf v1.31.0
gopkg.in/natefinch/lumberjack.v2 v2.2.1
gopkg.in/yaml.v3 v3.0.1
helm.sh/helm/v3 v3.12.3
modernc.org/sqlite v1.25.0
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -1408,6 +1408,8 @@ gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
gopkg.in/inconshreveable/log15.v2 v2.0.0-20180818164646-67afb5ed74ec/go.mod h1:aPpfJ7XW+gOuirDoZ8gHhLh3kZ1B08FtV2bbmy7Jv3s=
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc=
gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc=
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME=
gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI=
Expand Down
7 changes: 7 additions & 0 deletions hack/dev/conf.secure.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,13 @@ audit:
flushInterval: 5s
file:
path: stdout
additionalPaths:
- /tmp/audit_1.log
- /tmp/audit_2.log
logRotation:
maxFileSizeMB: 1
maxFileAgeDays: 1
maxFileCount: 3

tracing:
sampleProbability: 1.0
Expand Down
16 changes: 15 additions & 1 deletion internal/audit/file/conf.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,22 @@ const confKey = audit.ConfKey + ".file"

// Conf is optional configuration for file Audit.
type Conf struct {
// LogRotation settings (optional).
LogRotation *LogRotationConf `yaml:"logRotation"`
// Path to the log file to use as output. The special values stdout and stderr can be used to write to stdout or stderr respectively.
Path string `yaml:"path" conf:",example=/path/to/file.log"`
Path string `yaml:"path" conf:"required,example=/path/to/file.log"`
// AdditionalPaths to mirror the log output. Has performance implications. Use with caution.
AdditionalPaths []string `yaml:"additionalPaths" conf:",example=[stdout]"`
}

//nolint:tagliatelle
type LogRotationConf struct {
// MaxFileSizeMB sets the maximum size of individual log files in megabytes.
MaxFileSizeMB uint `yaml:"maxFileSizeMB" conf:",example=100"`
// MaxFileAgeDays sets the maximum age in days of old log files before they are deleted.
MaxFileAgeDays uint `yaml:"maxFileAgeDays" conf:",example=10"`
// MaxFileCount sets the maximum number of files to retain.
MaxFileCount uint `yaml:"maxFileCount" conf:",example=10"`
}

func (c *Conf) Key() string {
Expand Down
75 changes: 50 additions & 25 deletions internal/audit/file/log.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,20 @@ import (
"bytes"
"context"
"fmt"
"math"
"os"

"github.com/cerbos/cerbos/internal/audit"
"github.com/cerbos/cerbos/internal/config"
"go.elastic.co/ecszap"
"go.uber.org/multierr"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
"google.golang.org/protobuf/encoding/protojson"
"google.golang.org/protobuf/proto"
"google.golang.org/protobuf/reflect/protoreflect"
"gopkg.in/natefinch/lumberjack.v2"

"github.com/cerbos/cerbos/internal/audit"
"github.com/cerbos/cerbos/internal/config"
)

const Backend = "file"
Expand All @@ -33,10 +37,9 @@ func init() {
}

type Log struct {
accessLog *zap.Logger
decisionLog *zap.Logger
decisionFilter audit.DecisionLogEntryFilter
ignoreSyncErrors bool
accessLog *zap.Logger
decisionLog *zap.Logger
decisionFilter audit.DecisionLogEntryFilter
}

func NewLog(conf *Conf, decisionFilter audit.DecisionLogEntryFilter) (*Log, error) {
Expand All @@ -46,23 +49,40 @@ func NewLog(conf *Conf, decisionFilter audit.DecisionLogEntryFilter) (*Log, erro
encoderConf.TimeKey = ""
encoderConf.MessageKey = ""

zapConf := zap.NewProductionConfig()
zapConf.Sampling = nil
zapConf.DisableCaller = true
zapConf.DisableStacktrace = true
zapConf.EncoderConfig = encoderConf
zapConf.OutputPaths = []string{conf.Path}
outputPaths := append([]string{conf.Path}, conf.AdditionalPaths...)
outputSyncers := make([]zapcore.WriteSyncer, len(outputPaths))

logger, err := zapConf.Build()
if err != nil {
return nil, fmt.Errorf("failed to create logger: %w", err)
for i, path := range outputPaths {
path := path
switch path {
case "stdout":
outputSyncers[i] = zapcore.AddSync(syncErrIgnorer{WriteSyncer: os.Stdout})
case "stderr":
outputSyncers[i] = zapcore.AddSync(syncErrIgnorer{WriteSyncer: os.Stderr})
default:
rotator := &lumberjack.Logger{
Filename: path,
MaxSize: math.MaxInt32,
}

if conf.LogRotation != nil {
rotator.MaxSize = int(conf.LogRotation.MaxFileSizeMB)
rotator.MaxAge = int(conf.LogRotation.MaxFileAgeDays)
rotator.MaxBackups = int(conf.LogRotation.MaxFileCount)
}

outputSyncers[i] = zapcore.AddSync(rotator)
}
}

encoder := zapcore.NewJSONEncoder(encoderConf)
core := zapcore.NewCore(encoder, zapcore.NewMultiWriteSyncer(outputSyncers...), zap.NewAtomicLevelAt(zap.InfoLevel))
logger := zap.New(core)

return &Log{
accessLog: logger.Named("cerbos.audit").With(zap.String("log.kind", "access")),
decisionLog: logger.Named("cerbos.audit").With(zap.String("log.kind", "decision")),
decisionFilter: decisionFilter,
ignoreSyncErrors: (conf.Path == "stdout") || (conf.Path == "stderr"),
accessLog: logger.Named("cerbos.audit").With(zap.String("log.kind", "access")),
decisionLog: logger.Named("cerbos.audit").With(zap.String("log.kind", "decision")),
decisionFilter: decisionFilter,
}, nil
}

Expand Down Expand Up @@ -105,12 +125,7 @@ func (l *Log) Close() error {
err1 := l.accessLog.Sync()
err2 := l.decisionLog.Sync()

// See https://github.com/uber-go/zap/issues/328
if !l.ignoreSyncErrors {
return multierr.Combine(err1, err2)
}

return nil
return multierr.Combine(err1, err2)
}

type protoMsg struct {
Expand Down Expand Up @@ -226,3 +241,13 @@ func (pl protoList) MarshalLogArray(enc zapcore.ArrayEncoder) error {

return nil
}

type syncErrIgnorer struct {
zapcore.WriteSyncer
}

func (s syncErrIgnorer) Sync() error {
// https://github.com/uber-go/zap/issues/328
_ = s.WriteSyncer.Sync()
return nil
}

0 comments on commit 9652c90

Please sign in to comment.