mirror of
https://github.com/hashicorp/vault.git
synced 2025-08-18 04:27:02 +02:00
* Adding explicit MPL license for sub-package. This directory and its subdirectories (packages) contain files licensed with the MPLv2 `LICENSE` file in this directory and are intentionally licensed separately from the BSL `LICENSE` file at the root of this repository. * Adding explicit MPL license for sub-package. This directory and its subdirectories (packages) contain files licensed with the MPLv2 `LICENSE` file in this directory and are intentionally licensed separately from the BSL `LICENSE` file at the root of this repository. * Updating the license from MPL to Business Source License. Going forward, this project will be licensed under the Business Source License v1.1. Please see our blog post for more details at https://hashi.co/bsl-blog, FAQ at www.hashicorp.com/licensing-faq, and details of the license at www.hashicorp.com/bsl. * add missing license headers * Update copyright file headers to BUS-1.1 * Fix test that expected exact offset on hcl file --------- Co-authored-by: hashicorp-copywrite[bot] <110428419+hashicorp-copywrite[bot]@users.noreply.github.com> Co-authored-by: Sarah Thompson <sthompson@hashicorp.com> Co-authored-by: Brian Kassouf <bkassouf@hashicorp.com>
438 lines
9.9 KiB
Go
438 lines
9.9 KiB
Go
// Copyright (c) HashiCorp, Inc.
|
|
// SPDX-License-Identifier: BUSL-1.1
|
|
|
|
package file
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"path/filepath"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
"sync/atomic"
|
|
|
|
"github.com/hashicorp/eventlogger"
|
|
"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")
|
|
}
|
|
|
|
path, ok := conf.Config["file_path"]
|
|
if !ok {
|
|
path, ok = conf.Config["path"]
|
|
if !ok {
|
|
return nil, fmt.Errorf("file_path is required")
|
|
}
|
|
}
|
|
|
|
// normalize path if configured for stdout
|
|
if strings.EqualFold(path, "stdout") {
|
|
path = "stdout"
|
|
}
|
|
if strings.EqualFold(path, "discard") {
|
|
path = "discard"
|
|
}
|
|
|
|
format, ok := conf.Config["format"]
|
|
if !ok {
|
|
format = audit.JSONFormat.String()
|
|
}
|
|
switch format {
|
|
case audit.JSONFormat.String(), audit.JSONxFormat.String():
|
|
default:
|
|
return nil, fmt.Errorf("unknown format type %q", format)
|
|
}
|
|
|
|
// Check if hashing of accessor is disabled
|
|
hmacAccessor := true
|
|
if hmacAccessorRaw, ok := conf.Config["hmac_accessor"]; ok {
|
|
value, err := strconv.ParseBool(hmacAccessorRaw)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
hmacAccessor = value
|
|
}
|
|
|
|
// Check if raw logging is enabled
|
|
logRaw := false
|
|
if raw, ok := conf.Config["log_raw"]; ok {
|
|
b, err := strconv.ParseBool(raw)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
logRaw = b
|
|
}
|
|
|
|
elideListResponses := false
|
|
if elideListResponsesRaw, ok := conf.Config["elide_list_responses"]; ok {
|
|
value, err := strconv.ParseBool(elideListResponsesRaw)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
elideListResponses = value
|
|
}
|
|
|
|
// Check if mode is provided
|
|
mode := os.FileMode(0o600)
|
|
if modeRaw, ok := conf.Config["mode"]; ok {
|
|
m, err := strconv.ParseUint(modeRaw, 8, 32)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
switch m {
|
|
case 0:
|
|
// if mode is 0000, then do not modify file mode
|
|
if path != "stdout" && path != "discard" {
|
|
fileInfo, err := os.Stat(path)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
mode = fileInfo.Mode()
|
|
}
|
|
default:
|
|
mode = os.FileMode(m)
|
|
}
|
|
}
|
|
|
|
cfg, err := audit.NewFormatterConfig(
|
|
audit.WithElision(elideListResponses),
|
|
audit.WithFormat(format),
|
|
audit.WithHMACAccessor(hmacAccessor),
|
|
audit.WithRaw(logRaw),
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
b := &Backend{
|
|
path: path,
|
|
mode: mode,
|
|
saltConfig: conf.SaltConfig,
|
|
saltView: conf.SaltView,
|
|
salt: new(atomic.Value),
|
|
formatConfig: cfg,
|
|
}
|
|
|
|
// Ensure we are working with the right type by explicitly storing a nil of
|
|
// the right type
|
|
b.salt.Store((*salt.Salt)(nil))
|
|
|
|
// 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 format {
|
|
case "json":
|
|
w = &audit.JSONWriter{Prefix: conf.Config["prefix"]}
|
|
case "jsonx":
|
|
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 {
|
|
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
|
|
|
|
var sinkNode eventlogger.Node
|
|
|
|
switch path {
|
|
case "stdout":
|
|
sinkNode = &audit.SinkWrapper{Name: path, Sink: event.NewStdoutSinkNode(format)}
|
|
case "discard":
|
|
sinkNode = &audit.SinkWrapper{Name: path, Sink: event.NewNoopSink()}
|
|
default:
|
|
var err error
|
|
|
|
// The NewFileSink function attempts to open the file and will
|
|
// return an error if it can't.
|
|
n, err := event.NewFileSink(b.path, format, event.WithFileMode(strconv.FormatUint(uint64(mode), 8)))
|
|
if err != nil {
|
|
return nil, fmt.Errorf("file sink creation failed for path %q: %w", path, 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
|
|
} else {
|
|
switch path {
|
|
case "stdout":
|
|
case "discard":
|
|
default:
|
|
// Ensure that the file can be successfully opened for writing;
|
|
// otherwise it will be too late to catch later without problems
|
|
// (ref: https://github.com/hashicorp/vault/issues/550)
|
|
if err := b.open(); err != nil {
|
|
return nil, fmt.Errorf("sanity check failed; unable to open %q for writing: %w", path, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
return b, 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 {
|
|
path string
|
|
|
|
formatter *audit.EntryFormatterWriter
|
|
formatConfig audit.FormatterConfig
|
|
|
|
fileLock sync.RWMutex
|
|
f *os.File
|
|
mode os.FileMode
|
|
|
|
saltMutex sync.RWMutex
|
|
salt *atomic.Value
|
|
saltConfig *salt.Config
|
|
saltView logical.Storage
|
|
|
|
nodeIDList []eventlogger.NodeID
|
|
nodeMap map[eventlogger.NodeID]eventlogger.Node
|
|
}
|
|
|
|
var _ audit.Backend = (*Backend)(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) LogRequest(ctx context.Context, in *logical.LogInput) error {
|
|
var writer io.Writer
|
|
switch b.path {
|
|
case "stdout":
|
|
writer = os.Stdout
|
|
case "discard":
|
|
return nil
|
|
}
|
|
|
|
buf := bytes.NewBuffer(make([]byte, 0, 2000))
|
|
err := b.formatter.FormatAndWriteRequest(ctx, buf, in)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return b.log(ctx, buf, writer)
|
|
}
|
|
|
|
func (b *Backend) log(_ context.Context, buf *bytes.Buffer, writer io.Writer) error {
|
|
reader := bytes.NewReader(buf.Bytes())
|
|
|
|
b.fileLock.Lock()
|
|
|
|
if writer == nil {
|
|
if err := b.open(); err != nil {
|
|
b.fileLock.Unlock()
|
|
return err
|
|
}
|
|
writer = b.f
|
|
}
|
|
|
|
if _, err := reader.WriteTo(writer); err == nil {
|
|
b.fileLock.Unlock()
|
|
return nil
|
|
} else if b.path == "stdout" {
|
|
b.fileLock.Unlock()
|
|
return err
|
|
}
|
|
|
|
// If writing to stdout there's no real reason to think anything would have
|
|
// changed so return above. Otherwise, opportunistically try to re-open the
|
|
// FD, once per call.
|
|
b.f.Close()
|
|
b.f = nil
|
|
|
|
if err := b.open(); err != nil {
|
|
b.fileLock.Unlock()
|
|
return err
|
|
}
|
|
|
|
reader.Seek(0, io.SeekStart)
|
|
_, err := reader.WriteTo(writer)
|
|
b.fileLock.Unlock()
|
|
return err
|
|
}
|
|
|
|
func (b *Backend) LogResponse(ctx context.Context, in *logical.LogInput) error {
|
|
var writer io.Writer
|
|
switch b.path {
|
|
case "stdout":
|
|
writer = os.Stdout
|
|
case "discard":
|
|
return nil
|
|
}
|
|
|
|
buf := bytes.NewBuffer(make([]byte, 0, 6000))
|
|
err := b.formatter.FormatAndWriteResponse(ctx, buf, in)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return b.log(ctx, buf, writer)
|
|
}
|
|
|
|
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 writer io.Writer
|
|
switch b.path {
|
|
case "stdout":
|
|
writer = os.Stdout
|
|
case "discard":
|
|
return nil
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
return b.log(ctx, &buf, writer)
|
|
}
|
|
|
|
// The file lock must be held before calling this
|
|
func (b *Backend) open() error {
|
|
if b.f != nil {
|
|
return nil
|
|
}
|
|
if err := os.MkdirAll(filepath.Dir(b.path), b.mode); err != nil {
|
|
return err
|
|
}
|
|
|
|
var err error
|
|
b.f, err = os.OpenFile(b.path, os.O_APPEND|os.O_WRONLY|os.O_CREATE, b.mode)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Change the file mode in case the log file already existed. We special
|
|
// case /dev/null since we can't chmod it and bypass if the mode is zero
|
|
switch b.path {
|
|
case "/dev/null":
|
|
default:
|
|
if b.mode != 0 {
|
|
err = os.Chmod(b.path, b.mode)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (b *Backend) Reload(_ context.Context) error {
|
|
switch b.path {
|
|
case "stdout", "discard":
|
|
return nil
|
|
}
|
|
|
|
b.fileLock.Lock()
|
|
defer b.fileLock.Unlock()
|
|
|
|
if b.f == nil {
|
|
return b.open()
|
|
}
|
|
|
|
err := b.f.Close()
|
|
// Set to nil here so that even if we error out, on the next access open()
|
|
// will be tried
|
|
b.f = nil
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return b.open()
|
|
}
|
|
|
|
func (b *Backend) Invalidate(_ context.Context) {
|
|
b.saltMutex.Lock()
|
|
defer b.saltMutex.Unlock()
|
|
b.salt.Store((*salt.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)
|
|
}
|