diff --git a/audit/headers.go b/audit/headers.go index a6ba6b00cd..d505ffaf36 100644 --- a/audit/headers.go +++ b/audit/headers.go @@ -175,6 +175,7 @@ func (a *HeadersConfig) DefaultHeaders() map[string]*headerSettings { return map[string]*headerSettings{ correlationID: {}, xCorrelationID: {}, + "user-agent": {}, } } diff --git a/audit/headers_test.go b/audit/headers_test.go index 025f4a422f..54fce0f7ed 100644 --- a/audit/headers_test.go +++ b/audit/headers_test.go @@ -254,9 +254,11 @@ func TestAuditedHeadersConfig_ApplyConfig(t *testing.T) { t.Fatal(err) } + const hmacPrefix = "hmac-sha256:" + expected := map[string][]string{ "x-test-header": {"foo"}, - "x-vault-header": {"hmac-sha256:", "hmac-sha256:"}, + "x-vault-header": {hmacPrefix, hmacPrefix}, } if len(expected) != len(result) { @@ -271,7 +273,7 @@ func TestAuditedHeadersConfig_ApplyConfig(t *testing.T) { } for i, e := range expectedValues { - if e == "hmac-sha256:" { + if e == hmacPrefix { if !strings.HasPrefix(resultValues[i], e) { t.Fatalf("Expected headers did not match actual: Expected %#v...\n Got %#v\n", e, resultValues[i]) } @@ -609,13 +611,28 @@ func TestAuditedHeaders_invalidate_defaults(t *testing.T) { require.Equal(t, len(ahc.DefaultHeaders())+1, len(ahc.headerSettings)) // (defaults + 1 new header) _, ok := ahc.headerSettings["x-magic-header"] require.True(t, ok) + s, ok := ahc.headerSettings["x-correlation-id"] require.True(t, ok) require.False(t, s.HMAC) - // Add correlation ID specifically with HMAC and make sure it doesn't get blasted away. - fakeHeaders1 = map[string]*headerSettings{"x-magic-header": {}, "X-Correlation-ID": {HMAC: true}} + s, ok = ahc.headerSettings["user-agent"] + require.True(t, ok) + require.False(t, s.HMAC) + + // Add correlation ID and user-agent specifically with HMAC and make sure it doesn't get blasted away. + fakeHeaders1 = map[string]*headerSettings{ + "x-magic-header": {}, + "X-Correlation-ID": { + HMAC: true, + }, + "User-Agent": { + HMAC: true, + }, + } + fakeBytes1, err = json.Marshal(fakeHeaders1) + require.NoError(t, err) err = view.Put(context.Background(), &logical.StorageEntry{Key: auditedHeadersEntry, Value: fakeBytes1}) require.NoError(t, err) @@ -626,7 +643,12 @@ func TestAuditedHeaders_invalidate_defaults(t *testing.T) { require.Equal(t, len(ahc.DefaultHeaders())+1, len(ahc.headerSettings)) // (defaults + 1 new header, 1 is also a default) _, ok = ahc.headerSettings["x-magic-header"] require.True(t, ok) + s, ok = ahc.headerSettings["x-correlation-id"] require.True(t, ok) require.True(t, s.HMAC) + + s, ok = ahc.headerSettings["user-agent"] + require.True(t, ok) + require.True(t, s.HMAC) } diff --git a/changelog/28596.txt b/changelog/28596.txt new file mode 100644 index 0000000000..9e79f89a40 --- /dev/null +++ b/changelog/28596.txt @@ -0,0 +1,4 @@ +```release-note:improvement +audit: Audit logs will contain User-Agent headers when they are present in the incoming request. They are not +HMAC'ed by default but can be configured to be via the `/sys/config/auditing/request-headers/user-agent` endpoint. +``` diff --git a/vault/external_tests/audit/audit_test.go b/vault/external_tests/audit/audit_test.go index dbe56fd731..3dedb298d0 100644 --- a/vault/external_tests/audit/audit_test.go +++ b/vault/external_tests/audit/audit_test.go @@ -52,8 +52,8 @@ func TestAudit_HMACFields(t *testing.T) { require.NoError(t, err) // Request 1 - // Enable the audit device. A test probe request will audited along with the associated - // to the creation response + // Enable the audit device. A test probe request will audited along + // with the associated creation response _, err = client.Logical().Write("sys/audit/"+devicePath, deviceData) require.NoError(t, err) @@ -212,3 +212,89 @@ func TestAudit_HMACFields(t *testing.T) { require.True(t, strings.HasPrefix(wrapInfo["token"].(string), hmacPrefix)) require.Equal(t, wrapInfo["token"].(string), hashedWrapToken) } + +// TestAudit_Headers validates that headers are audited correctly. This includes +// the default headers (x-correlation-id and user-agent) along with user-specified +// headers. +func TestAudit_Headers(t *testing.T) { + cluster := minimal.NewTestSoloCluster(t, nil) + client := cluster.Cores[0].Client + + tempDir := t.TempDir() + logFile, err := os.CreateTemp(tempDir, "") + require.NoError(t, err) + devicePath := "file" + deviceData := map[string]any{ + "type": "file", + "description": "", + "local": false, + "options": map[string]any{ + "file_path": logFile.Name(), + }, + } + + _, err = client.Logical().Write("sys/config/auditing/request-headers/x-some-header", map[string]interface{}{ + "hmac": false, + }) + require.NoError(t, err) + + // User-Agent header is audited by default + client.AddHeader("User-Agent", "foo-agent") + + // X-Some-Header has been added to audited headers manually + client.AddHeader("X-Some-Header", "some-value") + + // X-Some-Other-Header will not be audited + client.AddHeader("X-Some-Other-Header", "some-other-value") + + // Request 1 + // Enable the audit device. A test probe request will audited along + // with the associated creation response + _, err = client.Logical().Write("sys/audit/"+devicePath, deviceData) + require.NoError(t, err) + + // Request 2 + // Ensure the device has been created. + devices, err := client.Sys().ListAudit() + require.NoError(t, err) + require.Len(t, devices, 1) + + // Request 3 + resp, err := client.Sys().SealStatus() + require.NoError(t, err) + require.NotEmpty(t, resp) + + expectedHeaders := map[string]interface{}{ + "user-agent": []interface{}{"foo-agent"}, + "x-some-header": []interface{}{"some-value"}, + } + + entries := make([]map[string]interface{}, 0) + scanner := bufio.NewScanner(logFile) + + for scanner.Scan() { + entry := make(map[string]interface{}) + + err := json.Unmarshal(scanner.Bytes(), &entry) + require.NoError(t, err) + + request, ok := entry["request"].(map[string]interface{}) + require.True(t, ok) + + // test probe will not have headers set + requestPath, ok := request["path"].(string) + require.True(t, ok) + + if requestPath != "sys/audit/test" { + headers, ok := request["headers"].(map[string]interface{}) + + require.True(t, ok) + require.Equal(t, expectedHeaders, headers) + } + + entries = append(entries, entry) + } + + // This count includes the initial test probe upon creation of the audit device + require.Equal(t, 4, len(entries)) +} diff --git a/website/content/docs/audit/index.mdx b/website/content/docs/audit/index.mdx index 3908ac40e7..a3abf8ffd7 100644 --- a/website/content/docs/audit/index.mdx +++ b/website/content/docs/audit/index.mdx @@ -121,6 +121,10 @@ curl \ --data '{ "hmac": true }' ``` +Another way to identify the source of a request is through the User-Agent request header. +Vault will automatically record this value as `user-agent` within the `headers` of a +request entry within the audit log. + ## Enabling/Disabling audit devices