vault/audit/headers.go
Chris Capurso 69411d7925
VAULT-30108: Include User-Agent header in audit requests by default (#28596)
* 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
2024-10-07 10:02:17 -04:00

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
}