From e86f4a1a21032aee2ab2f009c70a32354bf67c1f Mon Sep 17 00:00:00 2001 From: Joe Corall Date: Sun, 30 Aug 2026 22:43:24 +0000 Subject: [PATCH] [patch] Enforce protected Vault audit options --- main.go | 28 ++++++++++++++++++---------- main_test.go | 43 ++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 58 insertions(+), 13 deletions(-) diff --git a/main.go b/main.go index e5aa88b..14ea084 100644 --- a/main.go +++ b/main.go @@ -708,13 +708,7 @@ func ensureStdoutAuditDevice(rootToken string) error { payload, err := json.Marshal(map[string]any{ "type": "file", "description": "Cloud Run stdout audit stream", - "options": map[string]string{ - "file_path": "stdout", - "format": "json", - "hmac_accessor": "true", - "log_raw": "false", - "elide_list_responses": "true", - }, + "options": protectedStdoutAuditOptions(), }) if err != nil { return fmt.Errorf("marshal audit configuration: %w", err) @@ -737,6 +731,16 @@ func ensureStdoutAuditDevice(rootToken string) error { return validateStdoutAuditDevice(device) } +func protectedStdoutAuditOptions() map[string]string { + return map[string]string{ + "file_path": "stdout", + "format": "json", + "hmac_accessor": "true", + "log_raw": "false", + "elide_list_responses": "true", + } +} + func readAuditDevices(rootToken string) (map[string]auditDevice, error) { status, body, err := doRootVaultRequest(http.MethodGet, "/v1/sys/audit", nil, rootToken) if err != nil { @@ -763,9 +767,13 @@ func readAuditDevices(rootToken string) (map[string]auditDevice, error) { } func validateStdoutAuditDevice(device auditDevice) error { - if device.Type != "file" || device.Options["file_path"] != "stdout" || - device.Options["log_raw"] == "true" { - return fmt.Errorf("cloudrun/ audit device must be file output to stdout with raw secret logging disabled") + if device.Type != "file" { + return fmt.Errorf("cloudrun/ audit device type %q must be file", device.Type) + } + for name, want := range protectedStdoutAuditOptions() { + if got := device.Options[name]; got != want { + return fmt.Errorf("cloudrun/ audit device option %s=%q must be %q", name, got, want) + } } return nil } diff --git a/main_test.go b/main_test.go index 25a1860..b175523 100644 --- a/main_test.go +++ b/main_test.go @@ -10,6 +10,7 @@ import ( "encoding/pem" "errors" "io" + "maps" "net/http" "net/http/httptest" "os" @@ -684,7 +685,7 @@ func TestSecureBootstrapEnablesAuditBeforeRevokingRoot(t *testing.T) { _, _ = response.Write([]byte(`{}`)) return } - _, _ = response.Write([]byte(`{"request_id":"audit-list","lease_id":"","renewable":false,"lease_duration":0,"data":{"cloudrun/":{"type":"file","options":{"file_path":"stdout","log_raw":"false"}}},"wrap_info":null,"warnings":null,"auth":null,"mount_type":"system"}`)) + _, _ = response.Write([]byte(`{"request_id":"audit-list","lease_id":"","renewable":false,"lease_duration":0,"data":{"cloudrun/":{"type":"file","options":{"file_path":"stdout","format":"json","hmac_accessor":"true","log_raw":"false","elide_list_responses":"true"}}},"wrap_info":null,"warnings":null,"auth":null,"mount_type":"system"}`)) case "POST /v1/sys/audit/cloudrun": events = append(events, "audit") auditEnabled = true @@ -719,6 +720,42 @@ func TestSecureBootstrapEnablesAuditBeforeRevokingRoot(t *testing.T) { } } +func TestValidateStdoutAuditDeviceRequiresProtectedStructuredOptions(t *testing.T) { + valid := auditDevice{ + Type: "file", + Options: map[string]string{ + "file_path": "stdout", + "format": "json", + "hmac_accessor": "true", + "log_raw": "false", + "elide_list_responses": "true", + }, + } + if err := validateStdoutAuditDevice(valid); err != nil { + t.Fatalf("valid protected audit device rejected: %v", err) + } + + tests := []struct { + name string + option string + value string + }{ + {name: "non-JSON format", option: "format", value: "jsonx"}, + {name: "unhashed accessors", option: "hmac_accessor", value: "false"}, + {name: "raw secrets", option: "log_raw", value: "true"}, + {name: "unbounded list bodies", option: "elide_list_responses", value: "false"}, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + device := auditDevice{Type: valid.Type, Options: maps.Clone(valid.Options)} + device.Options[test.option] = test.value + if err := validateStdoutAuditDevice(device); err == nil { + t.Fatalf("unsafe %s=%q audit device accepted", test.option, test.value) + } + }) + } +} + func TestSecureBootstrapRetriesTransientAuditFailure(t *testing.T) { originalVaultAddr := vaultAddr originalHTTPClient := httpClient @@ -747,7 +784,7 @@ func TestSecureBootstrapRetriesTransientAuditFailure(t *testing.T) { response.WriteHeader(http.StatusInternalServerError) return } - _, _ = response.Write([]byte(`{"cloudrun/":{"type":"file","options":{"file_path":"stdout","log_raw":"false"}}}`)) + _, _ = response.Write([]byte(`{"cloudrun/":{"type":"file","options":{"file_path":"stdout","format":"json","hmac_accessor":"true","log_raw":"false","elide_list_responses":"true"}}}`)) case "POST /v1/auth/token/revoke-self": rootRevoked = true response.WriteHeader(http.StatusNoContent) @@ -967,7 +1004,7 @@ func TestResumeIncompleteKMSBootstrap(t *testing.T) { } writeJSON(response, map[string]any{ "cloudrun/": map[string]any{ - "type": "file", "options": map[string]string{"file_path": "stdout", "log_raw": "false"}, + "type": "file", "options": protectedStdoutAuditOptions(), }, }) case "POST /v1/sys/audit/cloudrun":