From 6a9329d8a6af94f5f4763d3de9306fa2cb7b5a65 Mon Sep 17 00:00:00 2001 From: Vault Automation Date: Wed, 22 Oct 2025 17:02:07 -0400 Subject: [PATCH] VAULT-39876 Add sys/reporting/scan to Vault, allowing an output of files with paths and names of Vault secrets (#10068) (#10323) * VAULT-39876 sys/reporting/scan for KV secrets * make fmt * changelog * stray t.log * typo * fix race probably * Bug fix, add local mount * remove comment * bolster external tests Co-authored-by: Violet Hynes --- api/sudo_paths.go | 1 + api/sys_reporting_scan.go | 49 +++++++++++++++++++ changelog/_10068.txt | 3 ++ command/server.go | 1 + command/server/config.go | 7 +++ command/server/config_test.go | 9 ++++ .../test-fixtures/reporting_directory.hcl | 4 ++ vault/core.go | 7 +++ vault/core_metrics.go | 20 +++++--- vault/external_tests/api/sudo_paths_test.go | 3 +- vault/logical_system.go | 1 + vault/logical_system_helpers.go | 5 ++ vault/testing.go | 1 + 13 files changed, 102 insertions(+), 9 deletions(-) create mode 100644 api/sys_reporting_scan.go create mode 100644 changelog/_10068.txt create mode 100644 command/server/test-fixtures/reporting_directory.hcl diff --git a/api/sudo_paths.go b/api/sudo_paths.go index 53480cf6a0..5aa7239fad 100644 --- a/api/sudo_paths.go +++ b/api/sudo_paths.go @@ -56,6 +56,7 @@ var sudoPaths = map[string]*regexp.Regexp{ "/sys/replication/reindex": regexp.MustCompile(`^/sys/replication/reindex$`), "/sys/storage/raft/snapshot-auto/config": regexp.MustCompile(`^/sys/storage/raft/snapshot-auto/config/?$`), "/sys/storage/raft/snapshot-auto/config/{name}": regexp.MustCompile(`^/sys/storage/raft/snapshot-auto/config/[^/]+$`), + "/sys/reporting/scan": regexp.MustCompile(`^/sys/reporting/scan$`), } func SudoPaths() map[string]*regexp.Regexp { diff --git a/api/sys_reporting_scan.go b/api/sys_reporting_scan.go new file mode 100644 index 0000000000..a91be6fbb1 --- /dev/null +++ b/api/sys_reporting_scan.go @@ -0,0 +1,49 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package api + +import ( + "context" + "errors" + "net/http" + + "github.com/mitchellh/mapstructure" +) + +func (c *Sys) ReportingScan() (*ReportingScanOutput, error) { + return c.ReportingScanWithContext(context.Background()) +} + +func (c *Sys) ReportingScanWithContext(ctx context.Context) (*ReportingScanOutput, error) { + ctx, cancelFunc := c.c.withConfiguredTimeout(ctx) + defer cancelFunc() + + r := c.c.NewRequest(http.MethodPost, "/v1/sys/reporting/scan") + + resp, err := c.c.rawRequestWithContext(ctx, r) + if err != nil { + return nil, err + } + defer resp.Body.Close() + + secret, err := ParseSecret(resp.Body) + if err != nil { + return nil, err + } + if secret == nil || secret.Data == nil { + return nil, errors.New("data from server response is empty") + } + + var result ReportingScanOutput + err = mapstructure.Decode(secret.Data, &result) + if err != nil { + return nil, err + } + + return &result, err +} + +type ReportingScanOutput struct { + Timestamp string `json:"timestamp" structs:"timestamp" mapstructure:"timestamp"` +} diff --git a/changelog/_10068.txt b/changelog/_10068.txt new file mode 100644 index 0000000000..a31b5d799e --- /dev/null +++ b/changelog/_10068.txt @@ -0,0 +1,3 @@ +```release-note:improvement +api: Added sudo-permissioned `sys/reporting/scan` endpoint which will output a set of files containing information about Vault state to the location specified by the `reporting_scan_directory` config item. +``` \ No newline at end of file diff --git a/command/server.go b/command/server.go index 67e90b79f1..1144d28a2c 100644 --- a/command/server.go +++ b/command/server.go @@ -2989,6 +2989,7 @@ func createCoreConfig(c *ServerCommand, config *server.Config, backend physical. Experiments: config.Experiments, AdministrativeNamespacePath: config.AdministrativeNamespacePath, ObservationSystemConfig: config.Observations, + ReportingScanDirectory: config.ReportingScanDirectory, } if c.flagDev { diff --git a/command/server/config.go b/command/server/config.go index 2b25f45114..f960f404ca 100644 --- a/command/server/config.go +++ b/command/server/config.go @@ -130,6 +130,8 @@ type Config struct { EnablePostUnsealTrace bool `hcl:"enable_post_unseal_trace"` PostUnsealTraceDir string `hcl:"post_unseal_trace_directory"` + + ReportingScanDirectory string `hcl:"reporting_scan_directory"` } const ( @@ -479,6 +481,11 @@ func (c *Config) Merge(c2 *Config) *Config { result.PostUnsealTraceDir = c2.PostUnsealTraceDir } + result.ReportingScanDirectory = c.ReportingScanDirectory + if c2.ReportingScanDirectory != "" { + result.ReportingScanDirectory = c2.ReportingScanDirectory + } + // Use values from top-level configuration for storage if set if storage := result.Storage; storage != nil { if result.APIAddr != "" { diff --git a/command/server/config_test.go b/command/server/config_test.go index 369a60e1ee..f5ca2ac163 100644 --- a/command/server/config_test.go +++ b/command/server/config_test.go @@ -95,6 +95,15 @@ func TestUnknownFieldValidationListenerAndStorage(t *testing.T) { testUnknownFieldValidationStorageAndListener(t) } +// Test_ReportingScanDirectory makes sure that the reporting scan directory is correctly parsed +func Test_ReportingScanDirectory(t *testing.T) { + config, err := LoadConfigFile("./test-fixtures/reporting_directory.hcl") + require.NoError(t, err) + require.NotNil(t, config) + require.NotEmpty(t, config.ReportingScanDirectory) + require.Equal(t, "/foo/bar/", config.ReportingScanDirectory) +} + // Test_ObservationSystemConfig makes sure that the observation system config // is properly loaded. func Test_ObservationSystemConfig(t *testing.T) { diff --git a/command/server/test-fixtures/reporting_directory.hcl b/command/server/test-fixtures/reporting_directory.hcl new file mode 100644 index 0000000000..f12e20bf0b --- /dev/null +++ b/command/server/test-fixtures/reporting_directory.hcl @@ -0,0 +1,4 @@ +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: BUSL-1.1 + +reporting_scan_directory = "/foo/bar/" \ No newline at end of file diff --git a/vault/core.go b/vault/core.go index 0d9bc3e3c1..b4dcf26d50 100644 --- a/vault/core.go +++ b/vault/core.go @@ -754,6 +754,9 @@ type Core struct { // Activation flags for enterprise features that require a one-time activation FeatureActivationFlags *activationflags.FeatureActivationFlags licenseReloadCh chan error + + // reportingScanDirectory is where the files emitted by /sys/reporting/scan go. + reportingScanDirectory string } func (c *Core) ActiveNodeClockSkewMillis() int64 { @@ -943,6 +946,9 @@ type CoreConfig struct { ClusterAddrBridge *raft.ClusterAddrBridge LicenseReload chan error + + // ReportingScanDirectory is where files generated by /sys/reporting/scan will go. + ReportingScanDirectory string } // GetServiceRegistration returns the config's ServiceRegistration, or nil if it does @@ -1125,6 +1131,7 @@ func CreateCore(conf *CoreConfig) (*Core, error) { activeNodeClockSkewMillis: uberAtomic.NewInt64(0), periodicLeaderRefreshInterval: conf.PeriodicLeaderRefreshInterval, rpcLastSuccessfulHeartbeat: new(atomic.Value), + reportingScanDirectory: conf.ReportingScanDirectory, } c.standbyStopCh.Store(make(chan struct{})) diff --git a/vault/core_metrics.go b/vault/core_metrics.go index 75a8537ea0..2c8bbbfd36 100644 --- a/vault/core_metrics.go +++ b/vault/core_metrics.go @@ -410,10 +410,12 @@ func (c *Core) emitMetricsActiveNode(stopCh chan struct{}) { } type kvMount struct { - Namespace *namespace.Namespace - MountPoint string - Version string - NumSecrets int + Namespace *namespace.Namespace + MountPoint string + MountAccessor string + Version string + Local bool + NumSecrets int } func (c *Core) findKvMounts() []*kvMount { @@ -436,10 +438,12 @@ func (c *Core) findKvMounts() []*kvMount { version = "1" } mounts = append(mounts, &kvMount{ - Namespace: entry.namespace, - MountPoint: entry.Path, - Version: version, - NumSecrets: 0, + Namespace: entry.namespace, + MountPoint: entry.Path, + MountAccessor: entry.Accessor, + Version: version, + NumSecrets: 0, + Local: entry.Local, }) } } diff --git a/vault/external_tests/api/sudo_paths_test.go b/vault/external_tests/api/sudo_paths_test.go index cd7944c628..989abb15d1 100644 --- a/vault/external_tests/api/sudo_paths_test.go +++ b/vault/external_tests/api/sudo_paths_test.go @@ -74,7 +74,8 @@ func TestSudoPaths(t *testing.T) { t.Fatalf( "A path in the static list of sudo paths in the api module "+ "is not marked as a sudo path in the OpenAPI spec (%s). Please reconcile the two "+ - "accordingly.", path) + "accordingly. This involves adding to the Root string array in the PathsSpecial declaration "+ + "for the backend in question. For example, for sys/, this would be in NewSystemBackend.", path) } } } diff --git a/vault/logical_system.go b/vault/logical_system.go index b348827e76..ba83f9e9be 100644 --- a/vault/logical_system.go +++ b/vault/logical_system.go @@ -135,6 +135,7 @@ func NewSystemBackend(core *Core, logger log.Logger, config *logical.BackendConf "leases/lookup/*", "storage/raft/snapshot-auto/config/*", "leases", + "reporting/scan", "internal/inspect/*", "internal/counters/activity/export", // sys/seal and sys/step-down actually have their sudo requirement enforced through hardcoding diff --git a/vault/logical_system_helpers.go b/vault/logical_system_helpers.go index f5630f626b..c0905516dd 100644 --- a/vault/logical_system_helpers.go +++ b/vault/logical_system_helpers.go @@ -133,6 +133,11 @@ var ( "config/group-policy-application$": {operations: []logical.Operation{logical.ReadOperation, logical.UpdateOperation}}, })...) + // reporting paths + paths = append(paths, buildEnterpriseOnlyPaths(map[string]enterprisePathStub{ + "reporting/scan$": {operations: []logical.Operation{logical.UpdateOperation}}, + })...) + // namespaces paths paths = append(paths, buildEnterpriseOnlyPaths(map[string]enterprisePathStub{ "namespaces/?$": {operations: []logical.Operation{logical.ListOperation}}, diff --git a/vault/testing.go b/vault/testing.go index 3fc9d84c2d..680284f97b 100644 --- a/vault/testing.go +++ b/vault/testing.go @@ -1494,6 +1494,7 @@ func NewTestCluster(t testing.TB, base *CoreConfig, opts *TestClusterOptions) *T coreConfig.AdministrativeNamespacePath = base.AdministrativeNamespacePath coreConfig.ServiceRegistration = base.ServiceRegistration coreConfig.ImpreciseLeaseRoleTracking = base.ImpreciseLeaseRoleTracking + coreConfig.ReportingScanDirectory = base.ReportingScanDirectory if base.BuiltinRegistry != nil { coreConfig.BuiltinRegistry = base.BuiltinRegistry