mirror of
				https://github.com/optim-enterprises-bv/vault.git
				synced 2025-10-31 02:28:09 +00:00 
			
		
		
		
	 aeb1b1e72f
			
		
	
	aeb1b1e72f
	
	
	
		
			
			This PR relates to a feature request logged through HashiCorp commercial
support.
Vault lacks pagination in its APIs. As a result, certain list operations
can return **very** large responses.  The user's chosen audit sinks may
experience difficulty consuming audit records that swell to tens of
megabytes of JSON.
In our case, one of the systems consuming audit log data could not cope,
and failed.
The responses of list operations are typically not very interesting, as
they are mostly lists of keys, or, even when they include a "key_info"
field, are not returning confidential information. They become even less
interesting once HMAC-ed by the audit system.
Some example Vault "list" operations that are prone to becoming very
large in an active Vault installation are:
    auth/token/accessors/
    identity/entity/id/
    identity/entity-alias/id/
    pki/certs/
In response, I've coded a new option that can be applied to audit
backends, `elide_list_responses`. When enabled, response data is elided
from audit logs, only when the operation type is "list".
For added safety, the elision only applies to the "keys" and "key_info"
fields within the response data - these are conventionally the only
fields present in a list response - see logical.ListResponse, and
logical.ListResponseWithInfo. However, other fields are technically
possible if a plugin author writes unusual code, and these will be
preserved in the audit log even with this option enabled.
The elision replaces the values of the "keys" and "key_info" fields with
an integer count of the number of entries. This allows even the elided
audit logs to still be useful for answering questions like "Was any data
returned?" or "How many records were listed?".
		
	
		
			
				
	
	
		
			360 lines
		
	
	
		
			7.5 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			360 lines
		
	
	
		
			7.5 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
| package file
 | |
| 
 | |
| import (
 | |
| 	"bytes"
 | |
| 	"context"
 | |
| 	"fmt"
 | |
| 	"io"
 | |
| 	"os"
 | |
| 	"path/filepath"
 | |
| 	"strconv"
 | |
| 	"strings"
 | |
| 	"sync"
 | |
| 	"sync/atomic"
 | |
| 
 | |
| 	"github.com/hashicorp/vault/audit"
 | |
| 	"github.com/hashicorp/vault/sdk/helper/salt"
 | |
| 	"github.com/hashicorp/vault/sdk/logical"
 | |
| )
 | |
| 
 | |
| func Factory(ctx context.Context, conf *audit.BackendConfig) (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 = "json"
 | |
| 	}
 | |
| 	switch format {
 | |
| 	case "json", "jsonx":
 | |
| 	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)
 | |
| 
 | |
| 		}
 | |
| 
 | |
| 	}
 | |
| 
 | |
| 	b := &Backend{
 | |
| 		path:       path,
 | |
| 		mode:       mode,
 | |
| 		saltConfig: conf.SaltConfig,
 | |
| 		saltView:   conf.SaltView,
 | |
| 		salt:       new(atomic.Value),
 | |
| 		formatConfig: audit.FormatterConfig{
 | |
| 			Raw:                logRaw,
 | |
| 			HMACAccessor:       hmacAccessor,
 | |
| 			ElideListResponses: elideListResponses,
 | |
| 		},
 | |
| 	}
 | |
| 
 | |
| 	// Ensure we are working with the right type by explicitly storing a nil of
 | |
| 	// the right type
 | |
| 	b.salt.Store((*salt.Salt)(nil))
 | |
| 
 | |
| 	switch format {
 | |
| 	case "json":
 | |
| 		b.formatter.AuditFormatWriter = &audit.JSONFormatWriter{
 | |
| 			Prefix:   conf.Config["prefix"],
 | |
| 			SaltFunc: b.Salt,
 | |
| 		}
 | |
| 	case "jsonx":
 | |
| 		b.formatter.AuditFormatWriter = &audit.JSONxFormatWriter{
 | |
| 			Prefix:   conf.Config["prefix"],
 | |
| 			SaltFunc: b.Salt,
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	switch path {
 | |
| 	case "stdout", "discard":
 | |
| 		// no need to test opening file if outputting to stdout or discarding
 | |
| 	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.AuditFormatter
 | |
| 	formatConfig audit.FormatterConfig
 | |
| 
 | |
| 	fileLock sync.RWMutex
 | |
| 	f        *os.File
 | |
| 	mode     os.FileMode
 | |
| 
 | |
| 	saltMutex  sync.RWMutex
 | |
| 	salt       *atomic.Value
 | |
| 	saltConfig *salt.Config
 | |
| 	saltView   logical.Storage
 | |
| }
 | |
| 
 | |
| 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) GetHash(ctx context.Context, data string) (string, error) {
 | |
| 	salt, err := b.Salt(ctx)
 | |
| 	if err != nil {
 | |
| 		return "", err
 | |
| 	}
 | |
| 
 | |
| 	return audit.HashString(salt, data), 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.FormatRequest(ctx, buf, b.formatConfig, in)
 | |
| 	if err != nil {
 | |
| 		return err
 | |
| 	}
 | |
| 
 | |
| 	return b.log(ctx, buf, writer)
 | |
| }
 | |
| 
 | |
| func (b *Backend) log(ctx 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.FormatResponse(ctx, buf, b.formatConfig, 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 {
 | |
| 	var writer io.Writer
 | |
| 	switch b.path {
 | |
| 	case "stdout":
 | |
| 		writer = os.Stdout
 | |
| 	case "discard":
 | |
| 		return nil
 | |
| 	}
 | |
| 
 | |
| 	var buf bytes.Buffer
 | |
| 	temporaryFormatter := audit.NewTemporaryFormatter(config["format"], config["prefix"])
 | |
| 	if err := temporaryFormatter.FormatRequest(ctx, &buf, b.formatConfig, 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))
 | |
| }
 |