mirror of
				https://github.com/optim-enterprises-bv/vault.git
				synced 2025-10-31 02:28:09 +00:00 
			
		
		
		
	 69411d7925
			
		
	
	69411d7925
	
	
	
		
			
			* include user-agent header in audit by default * add user-agent audit tests * update audit default headers docs * add changelog entry * remove temp changes from TestAuditedHeadersConfig_ApplyConfig * more TestAuditedHeadersConfig_ApplyConfig fixes * add some test comments * verify type assertions in TestAudit_Headers * more type assertion checks
		
			
				
	
	
		
			266 lines
		
	
	
		
			8.0 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			266 lines
		
	
	
		
			8.0 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
| // Copyright (c) HashiCorp, Inc.
 | |
| // SPDX-License-Identifier: BUSL-1.1
 | |
| 
 | |
| package audit
 | |
| 
 | |
| import (
 | |
| 	"context"
 | |
| 	"fmt"
 | |
| 	"strings"
 | |
| 	"sync"
 | |
| 
 | |
| 	"github.com/hashicorp/vault/sdk/logical"
 | |
| )
 | |
| 
 | |
| // N.B.: While we could use textproto to get the canonical mime header, HTTP/2
 | |
| // requires all headers to be converted to lower case, so we just do that.
 | |
| 
 | |
| const (
 | |
| 	// auditedHeadersEntry is the key used in storage to store and retrieve the header config
 | |
| 	auditedHeadersEntry = "audited-headers"
 | |
| 
 | |
| 	// AuditedHeadersSubPath is the path used to create a sub view within storage.
 | |
| 	AuditedHeadersSubPath = "audited-headers-config/"
 | |
| )
 | |
| 
 | |
| type durableStorer interface {
 | |
| 	Get(ctx context.Context, key string) (*logical.StorageEntry, error)
 | |
| 	Put(ctx context.Context, entry *logical.StorageEntry) error
 | |
| }
 | |
| 
 | |
| // HeaderFormatter is an interface defining the methods of the
 | |
| // vault.HeadersConfig structure needed in this package.
 | |
| type HeaderFormatter interface {
 | |
| 	// ApplyConfig returns a map of header values that consists of the
 | |
| 	// intersection of the provided set of header values with a configured
 | |
| 	// set of headers and will hash headers that have been configured as such.
 | |
| 	ApplyConfig(context.Context, map[string][]string, Salter) (map[string][]string, error)
 | |
| }
 | |
| 
 | |
| // AuditedHeadersKey returns the key at which audit header configuration is stored.
 | |
| func AuditedHeadersKey() string {
 | |
| 	return AuditedHeadersSubPath + auditedHeadersEntry
 | |
| }
 | |
| 
 | |
| type headerSettings struct {
 | |
| 	// HMAC is used to indicate whether the value of the header should be HMAC'd.
 | |
| 	HMAC bool `json:"hmac"`
 | |
| }
 | |
| 
 | |
| // HeadersConfig is used by the Audit Broker to write only approved
 | |
| // headers to the audit logs. It uses a BarrierView to persist the settings.
 | |
| type HeadersConfig struct {
 | |
| 	// headerSettings stores the current headers that should be audited, and their settings.
 | |
| 	headerSettings map[string]*headerSettings
 | |
| 
 | |
| 	// view is the barrier view which should be used to access underlying audit header config data.
 | |
| 	view durableStorer
 | |
| 
 | |
| 	sync.RWMutex
 | |
| }
 | |
| 
 | |
| // NewHeadersConfig should be used to create HeadersConfig.
 | |
| func NewHeadersConfig(view durableStorer) (*HeadersConfig, error) {
 | |
| 	if view == nil {
 | |
| 		return nil, fmt.Errorf("barrier view cannot be nil")
 | |
| 	}
 | |
| 
 | |
| 	// This should be the only place where the HeadersConfig struct is initialized.
 | |
| 	// Store the view so that we can reload headers when we 'Invalidate'.
 | |
| 	return &HeadersConfig{
 | |
| 		view:           view,
 | |
| 		headerSettings: make(map[string]*headerSettings),
 | |
| 	}, nil
 | |
| }
 | |
| 
 | |
| // Header attempts to retrieve a copy of the settings associated with the specified header.
 | |
| // The second boolean return parameter indicates whether the header existed in configuration,
 | |
| // it should be checked as when 'false' the returned settings will have the default values.
 | |
| func (a *HeadersConfig) Header(name string) (headerSettings, bool) {
 | |
| 	a.RLock()
 | |
| 	defer a.RUnlock()
 | |
| 
 | |
| 	var s headerSettings
 | |
| 	v, ok := a.headerSettings[strings.ToLower(name)]
 | |
| 
 | |
| 	if ok {
 | |
| 		s.HMAC = v.HMAC
 | |
| 	}
 | |
| 
 | |
| 	return s, ok
 | |
| }
 | |
| 
 | |
| // Headers returns all existing headers along with a copy of their current settings.
 | |
| func (a *HeadersConfig) Headers() map[string]headerSettings {
 | |
| 	a.RLock()
 | |
| 	defer a.RUnlock()
 | |
| 
 | |
| 	// We know how many entries the map should have.
 | |
| 	headers := make(map[string]headerSettings, len(a.headerSettings))
 | |
| 
 | |
| 	// Clone the headers
 | |
| 	for name, setting := range a.headerSettings {
 | |
| 		headers[name] = headerSettings{HMAC: setting.HMAC}
 | |
| 	}
 | |
| 
 | |
| 	return headers
 | |
| }
 | |
| 
 | |
| // Add adds or overwrites a header in the config and updates the barrier view
 | |
| // NOTE: Add will acquire a write lock in order to update the underlying headers.
 | |
| func (a *HeadersConfig) Add(ctx context.Context, header string, hmac bool) error {
 | |
| 	if header == "" {
 | |
| 		return fmt.Errorf("header value cannot be empty")
 | |
| 	}
 | |
| 
 | |
| 	// Grab a write lock
 | |
| 	a.Lock()
 | |
| 	defer a.Unlock()
 | |
| 
 | |
| 	if a.headerSettings == nil {
 | |
| 		a.headerSettings = make(map[string]*headerSettings, 1)
 | |
| 	}
 | |
| 
 | |
| 	a.headerSettings[strings.ToLower(header)] = &headerSettings{hmac}
 | |
| 	entry, err := logical.StorageEntryJSON(auditedHeadersEntry, a.headerSettings)
 | |
| 	if err != nil {
 | |
| 		return fmt.Errorf("failed to persist audited headers config: %w", err)
 | |
| 	}
 | |
| 
 | |
| 	if err := a.view.Put(ctx, entry); err != nil {
 | |
| 		return fmt.Errorf("failed to persist audited headers config: %w", err)
 | |
| 	}
 | |
| 
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| // Remove deletes a header out of the header config and updates the barrier view
 | |
| // NOTE: Remove will acquire a write lock in order to update the underlying headers.
 | |
| func (a *HeadersConfig) Remove(ctx context.Context, header string) error {
 | |
| 	if header == "" {
 | |
| 		return fmt.Errorf("header value cannot be empty")
 | |
| 	}
 | |
| 
 | |
| 	// Grab a write lock
 | |
| 	a.Lock()
 | |
| 	defer a.Unlock()
 | |
| 
 | |
| 	// Nothing to delete
 | |
| 	if len(a.headerSettings) == 0 {
 | |
| 		return nil
 | |
| 	}
 | |
| 
 | |
| 	delete(a.headerSettings, strings.ToLower(header))
 | |
| 	entry, err := logical.StorageEntryJSON(auditedHeadersEntry, a.headerSettings)
 | |
| 	if err != nil {
 | |
| 		return fmt.Errorf("failed to persist audited headers config: %w", err)
 | |
| 	}
 | |
| 
 | |
| 	if err := a.view.Put(ctx, entry); err != nil {
 | |
| 		return fmt.Errorf("failed to persist audited headers config: %w", err)
 | |
| 	}
 | |
| 
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| // DefaultHeaders can be used to retrieve the set of default headers that will be
 | |
| // added to HeadersConfig in order to allow them to appear in audit logs in a raw
 | |
| // format. If the Vault Operator adds their own setting for any of the defaults,
 | |
| // their setting will be honored.
 | |
| func (a *HeadersConfig) DefaultHeaders() map[string]*headerSettings {
 | |
| 	// Support deprecated 'x-' prefix (https://datatracker.ietf.org/doc/html/rfc6648)
 | |
| 	const correlationID = "correlation-id"
 | |
| 	xCorrelationID := fmt.Sprintf("x-%s", correlationID)
 | |
| 
 | |
| 	return map[string]*headerSettings{
 | |
| 		correlationID:  {},
 | |
| 		xCorrelationID: {},
 | |
| 		"user-agent":   {},
 | |
| 	}
 | |
| }
 | |
| 
 | |
| // Invalidate attempts to refresh the allowed audit headers and their settings.
 | |
| // NOTE: Invalidate will acquire a write lock in order to update the underlying headers.
 | |
| func (a *HeadersConfig) Invalidate(ctx context.Context) error {
 | |
| 	a.Lock()
 | |
| 	defer a.Unlock()
 | |
| 
 | |
| 	// Get the actual headers entries, e.g. sys/audited-headers-config/audited-headers
 | |
| 	out, err := a.view.Get(ctx, auditedHeadersEntry)
 | |
| 	if err != nil {
 | |
| 		return fmt.Errorf("failed to read config: %w", err)
 | |
| 	}
 | |
| 
 | |
| 	// If we cannot update the stored 'new' headers, we will clear the existing
 | |
| 	// ones as part of invalidation.
 | |
| 	headers := make(map[string]*headerSettings)
 | |
| 	if out != nil {
 | |
| 		err = out.DecodeJSON(&headers)
 | |
| 		if err != nil {
 | |
| 			return fmt.Errorf("failed to parse config: %w", err)
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	// Ensure that we are able to case-sensitively access the headers;
 | |
| 	// necessary for the upgrade case
 | |
| 	lowerHeaders := make(map[string]*headerSettings, len(headers))
 | |
| 	for k, v := range headers {
 | |
| 		lowerHeaders[strings.ToLower(k)] = v
 | |
| 	}
 | |
| 
 | |
| 	// Ensure that we have default headers configured to appear in the audit log.
 | |
| 	// Add them if they're missing.
 | |
| 	for header, setting := range a.DefaultHeaders() {
 | |
| 		if _, ok := lowerHeaders[header]; !ok {
 | |
| 			lowerHeaders[header] = setting
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	a.headerSettings = lowerHeaders
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| // ApplyConfig returns a map of approved headers and their values, either HMAC'd or plaintext.
 | |
| // If the supplied headers are empty or nil, an empty set of headers will be returned.
 | |
| func (a *HeadersConfig) ApplyConfig(ctx context.Context, headers map[string][]string, salter Salter) (result map[string][]string, retErr error) {
 | |
| 	// Return early if we don't have headers.
 | |
| 	if len(headers) < 1 {
 | |
| 		return map[string][]string{}, nil
 | |
| 	}
 | |
| 
 | |
| 	// Grab a read lock
 | |
| 	a.RLock()
 | |
| 	defer a.RUnlock()
 | |
| 
 | |
| 	// Make a copy of the incoming headers with everything lower so we can
 | |
| 	// case-insensitively compare
 | |
| 	lowerHeaders := make(map[string][]string, len(headers))
 | |
| 	for k, v := range headers {
 | |
| 		lowerHeaders[strings.ToLower(k)] = v
 | |
| 	}
 | |
| 
 | |
| 	result = make(map[string][]string, len(a.headerSettings))
 | |
| 	for key, settings := range a.headerSettings {
 | |
| 		if val, ok := lowerHeaders[key]; ok {
 | |
| 			// copy the header values so we don't overwrite them
 | |
| 			hVals := make([]string, len(val))
 | |
| 			copy(hVals, val)
 | |
| 
 | |
| 			// Optionally hmac the values
 | |
| 			if settings.HMAC {
 | |
| 				for i, el := range hVals {
 | |
| 					hVal, err := hashString(ctx, salter, el)
 | |
| 					if err != nil {
 | |
| 						return nil, err
 | |
| 					}
 | |
| 					hVals[i] = hVal
 | |
| 				}
 | |
| 			}
 | |
| 
 | |
| 			result[key] = hVals
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	return result, nil
 | |
| }
 |