Files
vault/builtin/audit/file/backend.go
Peter Wilson 6e98274bba VAULT-24449: Migrate 'audit filtering' feature to Enterprise (#25711)
* Fix an audit filtering test
Move configureFilterNode to ent-specific files and add non-ent stubs
Update tests for file audit devices
Add tests for socket audit device
Add syslog audit device tests
Prevent enabling an audit device with 'enterprise only' options in CE
Check enterprise only audit options on db load (unseal)
newAuditBackend test

* Fix assignment of audit broker to core during audit setup

* Removed Enterprise only audit feature tests (maintained in Enterprise repo)

* Replace enterprise filtering tests with ones for CE

* Remove redundant temp file creation calls in CE tests for filtering

---------

Co-authored-by: Kuba Wieczorek <kuba.wieczorek@hashicorp.com>
2024-02-29 15:12:33 +00:00

358 lines
10 KiB
Go

// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package file
import (
"context"
"fmt"
"reflect"
"strconv"
"strings"
"sync"
"sync/atomic"
"github.com/hashicorp/eventlogger"
"github.com/hashicorp/go-hclog"
"github.com/hashicorp/go-secure-stdlib/parseutil"
"github.com/hashicorp/vault/audit"
"github.com/hashicorp/vault/internal/observability/event"
"github.com/hashicorp/vault/sdk/helper/salt"
"github.com/hashicorp/vault/sdk/logical"
)
const (
stdout = "stdout"
discard = "discard"
)
var _ audit.Backend = (*Backend)(nil)
// Backend is the audit backend for the file-based audit store.
//
// NOTE: This audit backend is currently very simple: it appends to a file.
// It doesn't do anything more at the moment to assist with rotation
// or reset the write cursor, this should be done in the future.
type Backend struct {
fallback bool
name string
nodeIDList []eventlogger.NodeID
nodeMap map[eventlogger.NodeID]eventlogger.Node
salt *atomic.Value
saltConfig *salt.Config
saltMutex sync.RWMutex
saltView logical.Storage
}
func Factory(_ context.Context, conf *audit.BackendConfig, headersConfig audit.HeaderFormatter) (audit.Backend, error) {
const op = "file.Factory"
if conf.SaltConfig == nil {
return nil, fmt.Errorf("%s: nil salt config", op)
}
if conf.SaltView == nil {
return nil, fmt.Errorf("%s: nil salt view", op)
}
if conf.Logger == nil || reflect.ValueOf(conf.Logger).IsNil() {
return nil, fmt.Errorf("%s: nil logger", op)
}
// The config options 'fallback' and 'filter' are mutually exclusive, a fallback
// device catches everything, so it cannot be allowed to filter.
var fallback bool
var err error
if fallbackRaw, ok := conf.Config["fallback"]; ok {
fallback, err = parseutil.ParseBool(fallbackRaw)
if err != nil {
return nil, fmt.Errorf("%s: unable to parse 'fallback': %w", op, err)
}
}
if _, ok := conf.Config["filter"]; ok && fallback {
return nil, fmt.Errorf("%s: cannot configure a fallback device with a filter: %w", op, event.ErrInvalidParameter)
}
// Get file path from config or fall back to the old option name ('path') for compatibility
// (see commit bac4fe0799a372ba1245db642f3f6cd1f1d02669).
var filePath string
if p, ok := conf.Config["file_path"]; ok {
filePath = p
} else if p, ok = conf.Config["path"]; ok {
filePath = p
} else {
return nil, fmt.Errorf("%s: file_path is required", op)
}
// normalize file path if configured for stdout
if strings.EqualFold(filePath, stdout) {
filePath = stdout
}
if strings.EqualFold(filePath, discard) {
filePath = discard
}
b := &Backend{
fallback: fallback,
name: conf.MountPath,
saltConfig: conf.SaltConfig,
saltView: conf.SaltView,
salt: new(atomic.Value),
nodeIDList: []eventlogger.NodeID{},
nodeMap: make(map[eventlogger.NodeID]eventlogger.Node),
}
// Ensure we are working with the right type by explicitly storing a nil of
// the right type
b.salt.Store((*salt.Salt)(nil))
err = b.configureFilterNode(conf.Config["filter"])
if err != nil {
return nil, fmt.Errorf("%s: error configuring filter node: %w", op, err)
}
cfg, err := formatterConfig(conf.Config)
if err != nil {
return nil, fmt.Errorf("%s: failed to create formatter config: %w", op, err)
}
formatterOpts := []audit.Option{
audit.WithHeaderFormatter(headersConfig),
audit.WithPrefix(conf.Config["prefix"]),
}
err = b.configureFormatterNode(conf.MountPath, cfg, conf.Logger, formatterOpts...)
if err != nil {
return nil, fmt.Errorf("%s: error configuring formatter node: %w", op, err)
}
err = b.configureSinkNode(conf.MountPath, filePath, conf.Config["mode"], cfg.RequiredFormat.String())
if err != nil {
return nil, fmt.Errorf("%s: error configuring sink node: %w", op, err)
}
return b, nil
}
func (b *Backend) Salt(ctx context.Context) (*salt.Salt, error) {
s := b.salt.Load().(*salt.Salt)
if s != nil {
return s, nil
}
b.saltMutex.Lock()
defer b.saltMutex.Unlock()
s = b.salt.Load().(*salt.Salt)
if s != nil {
return s, nil
}
newSalt, err := salt.NewSalt(ctx, b.saltView, b.saltConfig)
if err != nil {
b.salt.Store((*salt.Salt)(nil))
return nil, err
}
b.salt.Store(newSalt)
return newSalt, nil
}
func (b *Backend) LogTestMessage(ctx context.Context, in *logical.LogInput) error {
if len(b.nodeIDList) > 0 {
return audit.ProcessManual(ctx, in, b.nodeIDList, b.nodeMap)
}
return nil
}
func (b *Backend) Reload(_ context.Context) error {
for _, n := range b.nodeMap {
if n.Type() == eventlogger.NodeTypeSink {
return n.Reopen()
}
}
return nil
}
func (b *Backend) Invalidate(_ context.Context) {
b.saltMutex.Lock()
defer b.saltMutex.Unlock()
b.salt.Store((*salt.Salt)(nil))
}
// formatterConfig creates the configuration required by a formatter node using
// the config map supplied to the factory.
func formatterConfig(config map[string]string) (audit.FormatterConfig, error) {
const op = "file.formatterConfig"
var opts []audit.Option
if format, ok := config["format"]; ok {
opts = append(opts, audit.WithFormat(format))
}
// Check if hashing of accessor is disabled
if hmacAccessorRaw, ok := config["hmac_accessor"]; ok {
v, err := strconv.ParseBool(hmacAccessorRaw)
if err != nil {
return audit.FormatterConfig{}, fmt.Errorf("%s: unable to parse 'hmac_accessor': %w", op, err)
}
opts = append(opts, audit.WithHMACAccessor(v))
}
// Check if raw logging is enabled
if raw, ok := config["log_raw"]; ok {
v, err := strconv.ParseBool(raw)
if err != nil {
return audit.FormatterConfig{}, fmt.Errorf("%s: unable to parse 'log_raw': %w", op, err)
}
opts = append(opts, audit.WithRaw(v))
}
if elideListResponsesRaw, ok := config["elide_list_responses"]; ok {
v, err := strconv.ParseBool(elideListResponsesRaw)
if err != nil {
return audit.FormatterConfig{}, fmt.Errorf("%s: unable to parse 'elide_list_responses': %w", op, err)
}
opts = append(opts, audit.WithElision(v))
}
return audit.NewFormatterConfig(opts...)
}
// configureFormatterNode is used to configure a formatter node and associated ID on the Backend.
func (b *Backend) configureFormatterNode(name string, formatConfig audit.FormatterConfig, logger hclog.Logger, opts ...audit.Option) error {
const op = "file.(Backend).configureFormatterNode"
formatterNodeID, err := event.GenerateNodeID()
if err != nil {
return fmt.Errorf("%s: error generating random NodeID for formatter node: %w", op, err)
}
formatterNode, err := audit.NewEntryFormatter(name, formatConfig, b, logger, opts...)
if err != nil {
return fmt.Errorf("%s: error creating formatter: %w", op, err)
}
b.nodeIDList = append(b.nodeIDList, formatterNodeID)
b.nodeMap[formatterNodeID] = formatterNode
return nil
}
// configureSinkNode is used to configure a sink node and associated ID on the Backend.
func (b *Backend) configureSinkNode(name string, filePath string, mode string, format string) error {
const op = "file.(Backend).configureSinkNode"
name = strings.TrimSpace(name)
if name == "" {
return fmt.Errorf("%s: name is required: %w", op, event.ErrInvalidParameter)
}
filePath = strings.TrimSpace(filePath)
if filePath == "" {
return fmt.Errorf("%s: file path is required: %w", op, event.ErrInvalidParameter)
}
format = strings.TrimSpace(format)
if format == "" {
return fmt.Errorf("%s: format is required: %w", op, event.ErrInvalidParameter)
}
sinkNodeID, err := event.GenerateNodeID()
if err != nil {
return fmt.Errorf("%s: error generating random NodeID for sink node: %w", op, err)
}
// normalize file path if configured for stdout or discard
if strings.EqualFold(filePath, stdout) {
filePath = stdout
} else if strings.EqualFold(filePath, discard) {
filePath = discard
}
var sinkNode eventlogger.Node
var sinkName string
switch filePath {
case stdout:
sinkName = stdout
sinkNode, err = event.NewStdoutSinkNode(format)
case discard:
sinkName = discard
sinkNode = event.NewNoopSink()
default:
// The NewFileSink function attempts to open the file and will return an error if it can't.
sinkName = name
sinkNode, err = event.NewFileSink(filePath, format, []event.Option{event.WithFileMode(mode)}...)
}
if err != nil {
return fmt.Errorf("%s: file sink creation failed for path %q: %w", op, filePath, err)
}
// Wrap the sink node with metrics middleware
sinkMetricTimer, err := audit.NewSinkMetricTimer(sinkName, sinkNode)
if err != nil {
return fmt.Errorf("%s: unable to add timing metrics to sink for path %q: %w", op, filePath, err)
}
// Decide what kind of labels we want and wrap the sink node inside a metrics counter.
var metricLabeler event.Labeler
switch {
case b.fallback:
metricLabeler = &audit.MetricLabelerAuditFallback{}
default:
metricLabeler = &audit.MetricLabelerAuditSink{}
}
sinkMetricCounter, err := event.NewMetricsCounter(sinkName, sinkMetricTimer, metricLabeler)
if err != nil {
return fmt.Errorf("%s: unable to add counting metrics to sink for path %q: %w", op, filePath, err)
}
b.nodeIDList = append(b.nodeIDList, sinkNodeID)
b.nodeMap[sinkNodeID] = sinkMetricCounter
return nil
}
// Name for this backend, this would ideally correspond to the mount path for the audit device.
func (b *Backend) Name() string {
return b.name
}
// Nodes returns the nodes which should be used by the event framework to process audit entries.
func (b *Backend) Nodes() map[eventlogger.NodeID]eventlogger.Node {
return b.nodeMap
}
// NodeIDs returns the IDs of the nodes, in the order they are required.
func (b *Backend) NodeIDs() []eventlogger.NodeID {
return b.nodeIDList
}
// EventType returns the event type for the backend.
func (b *Backend) EventType() eventlogger.EventType {
return eventlogger.EventType(event.AuditType.String())
}
// HasFiltering determines if the first node for the pipeline is an eventlogger.NodeTypeFilter.
func (b *Backend) HasFiltering() bool {
if b.nodeMap == nil {
return false
}
return len(b.nodeIDList) > 0 && b.nodeMap[b.nodeIDList[0]].Type() == eventlogger.NodeTypeFilter
}
// IsFallback can be used to determine if this audit backend device is intended to
// be used as a fallback to catch all events that are not written when only using
// filtered pipelines.
func (b *Backend) IsFallback() bool {
return b.fallback
}