Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 18 additions & 10 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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 {
Expand All @@ -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
}
Expand Down
43 changes: 40 additions & 3 deletions main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"encoding/pem"
"errors"
"io"
"maps"
"net/http"
"net/http/httptest"
"os"
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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":
Expand Down
Loading