// Copyright (c) HashiCorp, Inc. // SPDX-License-Identifier: BUSL-1.1 package syslog import ( "bytes" "context" "fmt" "strconv" "sync" "github.com/hashicorp/eventlogger" gsyslog "github.com/hashicorp/go-syslog" "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" ) func Factory(ctx context.Context, conf *audit.BackendConfig, useEventLogger bool, headersConfig audit.HeaderFormatter) (audit.Backend, error) { if conf.SaltConfig == nil { return nil, fmt.Errorf("nil salt config") } if conf.SaltView == nil { return nil, fmt.Errorf("nil salt view") } // Get facility or default to AUTH facility, ok := conf.Config["facility"] if !ok { facility = "AUTH" } // Get tag or default to 'vault' tag, ok := conf.Config["tag"] if !ok { tag = "vault" } var cfgOpts []audit.Option if format, ok := conf.Config["format"]; ok { cfgOpts = append(cfgOpts, audit.WithFormat(format)) } // Check if hashing of accessor is disabled if hmacAccessorRaw, ok := conf.Config["hmac_accessor"]; ok { v, err := strconv.ParseBool(hmacAccessorRaw) if err != nil { return nil, err } cfgOpts = append(cfgOpts, audit.WithHMACAccessor(v)) } // Check if raw logging is enabled if raw, ok := conf.Config["log_raw"]; ok { v, err := strconv.ParseBool(raw) if err != nil { return nil, err } cfgOpts = append(cfgOpts, audit.WithRaw(v)) } if elideListResponsesRaw, ok := conf.Config["elide_list_responses"]; ok { v, err := strconv.ParseBool(elideListResponsesRaw) if err != nil { return nil, err } cfgOpts = append(cfgOpts, audit.WithElision(v)) } cfg, err := audit.NewFormatterConfig(cfgOpts...) if err != nil { return nil, err } // Get the logger logger, err := gsyslog.NewLogger(gsyslog.LOG_INFO, facility, tag) if err != nil { return nil, err } b := &Backend{ logger: logger, saltConfig: conf.SaltConfig, saltView: conf.SaltView, formatConfig: cfg, } // Configure the formatter for either case. f, err := audit.NewEntryFormatter(b.formatConfig, b, audit.WithHeaderFormatter(headersConfig), audit.WithPrefix(conf.Config["prefix"])) if err != nil { return nil, fmt.Errorf("error creating formatter: %w", err) } var w audit.Writer switch b.formatConfig.RequiredFormat { case audit.JSONFormat: w = &audit.JSONWriter{Prefix: conf.Config["prefix"]} case audit.JSONxFormat: w = &audit.JSONxWriter{Prefix: conf.Config["prefix"]} } fw, err := audit.NewEntryFormatterWriter(b.formatConfig, f, w) if err != nil { return nil, fmt.Errorf("error creating formatter writer: %w", err) } b.formatter = fw if useEventLogger { var opts []event.Option // Get facility or default to AUTH if facility, ok := conf.Config["facility"]; ok { opts = append(opts, event.WithFacility(facility)) } if tag, ok := conf.Config["tag"]; ok { opts = append(opts, event.WithTag(tag)) } b.nodeIDList = make([]eventlogger.NodeID, 2) b.nodeMap = make(map[eventlogger.NodeID]eventlogger.Node) formatterNodeID, err := event.GenerateNodeID() if err != nil { return nil, fmt.Errorf("error generating random NodeID for formatter node: %w", err) } b.nodeIDList[0] = formatterNodeID b.nodeMap[formatterNodeID] = f n, err := event.NewSyslogSink(b.formatConfig.RequiredFormat.String(), opts...) if err != nil { return nil, fmt.Errorf("error creating syslog sink node: %w", err) } sinkNode := &audit.SinkWrapper{Name: conf.MountPath, Sink: n} sinkNodeID, err := event.GenerateNodeID() if err != nil { return nil, fmt.Errorf("error generating random NodeID for sink node: %w", err) } b.nodeIDList[1] = sinkNodeID b.nodeMap[sinkNodeID] = sinkNode } return b, nil } // Backend is the audit backend for the syslog-based audit store. type Backend struct { logger gsyslog.Syslogger formatter *audit.EntryFormatterWriter formatConfig audit.FormatterConfig saltMutex sync.RWMutex salt *salt.Salt saltConfig *salt.Config saltView logical.Storage nodeIDList []eventlogger.NodeID nodeMap map[eventlogger.NodeID]eventlogger.Node } var _ audit.Backend = (*Backend)(nil) func (b *Backend) LogRequest(ctx context.Context, in *logical.LogInput) error { var buf bytes.Buffer if err := b.formatter.FormatAndWriteRequest(ctx, &buf, in); err != nil { return err } // Write out to syslog _, err := b.logger.Write(buf.Bytes()) return err } func (b *Backend) LogResponse(ctx context.Context, in *logical.LogInput) error { var buf bytes.Buffer if err := b.formatter.FormatAndWriteResponse(ctx, &buf, in); err != nil { return err } // Write out to syslog _, err := b.logger.Write(buf.Bytes()) return err } func (b *Backend) LogTestMessage(ctx context.Context, in *logical.LogInput, config map[string]string) error { // Event logger behavior - manually Process each node if len(b.nodeIDList) > 0 { return audit.ProcessManual(ctx, in, b.nodeIDList, b.nodeMap) } // Old behavior var buf bytes.Buffer temporaryFormatter, err := audit.NewTemporaryFormatter(config["format"], config["prefix"]) if err != nil { return err } if err = temporaryFormatter.FormatAndWriteRequest(ctx, &buf, in); err != nil { return err } // Send to syslog _, err = b.logger.Write(buf.Bytes()) return err } func (b *Backend) Reload(_ context.Context) error { return nil } func (b *Backend) Salt(ctx context.Context) (*salt.Salt, error) { b.saltMutex.RLock() if b.salt != nil { defer b.saltMutex.RUnlock() return b.salt, nil } b.saltMutex.RUnlock() b.saltMutex.Lock() defer b.saltMutex.Unlock() if b.salt != nil { return b.salt, nil } salt, err := salt.NewSalt(ctx, b.saltView, b.saltConfig) if err != nil { return nil, err } b.salt = salt return salt, nil } func (b *Backend) Invalidate(_ context.Context) { b.saltMutex.Lock() defer b.saltMutex.Unlock() b.salt = nil } // RegisterNodesAndPipeline registers the nodes and a pipeline as required by // the audit.Backend interface. func (b *Backend) RegisterNodesAndPipeline(broker *eventlogger.Broker, name string) error { for id, node := range b.nodeMap { if err := broker.RegisterNode(id, node); err != nil { return err } } pipeline := eventlogger.Pipeline{ PipelineID: eventlogger.PipelineID(name), EventType: eventlogger.EventType("audit"), NodeIDs: b.nodeIDList, } return broker.RegisterPipeline(pipeline) }