diff --git a/changelog/21215.txt b/changelog/21215.txt new file mode 100644 index 000000000..ec4a63af9 --- /dev/null +++ b/changelog/21215.txt @@ -0,0 +1,4 @@ +```release-note:change +core/namespace (enterprise): Introduce the concept of high-privilege namespace (administrative namespace), +which will have access to some system backend paths that were previously only accessible in the root namespace. +``` \ No newline at end of file diff --git a/command/server.go b/command/server.go index f5153e57d..6171c60e7 100644 --- a/command/server.go +++ b/command/server.go @@ -1432,6 +1432,9 @@ func (c *ServerCommand) Run(args []string) int { info["HCP resource ID"] = config.HCPLinkConf.Resource.ID } + infoKeys = append(infoKeys, "administrative namespace") + info["administrative namespace"] = config.AdministrativeNamespacePath + sort.Strings(infoKeys) c.UI.Output("==> Vault server configuration:\n") @@ -2794,6 +2797,7 @@ func createCoreConfig(c *ServerCommand, config *server.Config, backend physical. LicensePath: config.LicensePath, DisableSSCTokens: config.DisableSSCTokens, Experiments: config.Experiments, + AdministrativeNamespacePath: config.AdministrativeNamespacePath, } if c.flagDev { diff --git a/command/server/config.go b/command/server/config.go index 34e484800..2d7943147 100644 --- a/command/server/config.go +++ b/command/server/config.go @@ -447,6 +447,11 @@ func (c *Config) Merge(c2 *Config) *Config { } } + result.AdministrativeNamespacePath = c.AdministrativeNamespacePath + if c2.AdministrativeNamespacePath != "" { + result.AdministrativeNamespacePath = c2.AdministrativeNamespacePath + } + result.entConfig = c.entConfig.Merge(c2.entConfig) result.Experiments = mergeExperiments(c.Experiments, c2.Experiments) diff --git a/command/server/config_test.go b/command/server/config_test.go index b570f1e76..c6ff96525 100644 --- a/command/server/config_test.go +++ b/command/server/config_test.go @@ -64,6 +64,12 @@ func TestParseStorage(t *testing.T) { testParseStorageTemplate(t) } +// TestConfigWithAdministrativeNamespace tests that .hcl and .json configurations are correctly parsed when the administrative_namespace_path is present. +func TestConfigWithAdministrativeNamespace(t *testing.T) { + testConfigWithAdministrativeNamespaceHcl(t) + testConfigWithAdministrativeNamespaceJson(t) +} + func TestUnknownFieldValidation(t *testing.T) { testUnknownFieldValidation(t) } diff --git a/command/server/config_test_helpers.go b/command/server/config_test_helpers.go index f5136449c..2a44d7a5f 100644 --- a/command/server/config_test_helpers.go +++ b/command/server/config_test_helpers.go @@ -572,6 +572,28 @@ func testUnknownFieldValidationHcl(t *testing.T) { } } +// testConfigWithAdministrativeNamespaceJson tests that a config with a valid administrative namespace path is correctly validated and loaded. +func testConfigWithAdministrativeNamespaceJson(t *testing.T) { + config, err := LoadConfigFile("./test-fixtures/config_with_valid_admin_ns.json") + require.NoError(t, err) + + configErrors := config.Validate("./test-fixtures/config_with_valid_admin_ns.json") + require.Empty(t, configErrors) + + require.NotEmpty(t, config.AdministrativeNamespacePath) +} + +// testConfigWithAdministrativeNamespaceHcl tests that a config with a valid administrative namespace path is correctly validated and loaded. +func testConfigWithAdministrativeNamespaceHcl(t *testing.T) { + config, err := LoadConfigFile("./test-fixtures/config_with_valid_admin_ns.hcl") + require.NoError(t, err) + + configErrors := config.Validate("./test-fixtures/config_with_valid_admin_ns.hcl") + require.Empty(t, configErrors) + + require.NotEmpty(t, config.AdministrativeNamespacePath) +} + func testLoadConfigFile_json(t *testing.T) { config, err := LoadConfigFile("./test-fixtures/config.hcl.json") if err != nil { @@ -819,6 +841,7 @@ func testConfig_Sanitized(t *testing.T) { "num_lease_metrics_buckets": 168, "add_lease_metrics_namespace_labels": false, }, + "administrative_namespace_path": "admin/", } addExpectedEntSanitizedConfig(expected, []string{"http"}) diff --git a/command/server/test-fixtures/config3.hcl b/command/server/test-fixtures/config3.hcl index 96b93318f..87adb96a9 100644 --- a/command/server/test-fixtures/config3.hcl +++ b/command/server/test-fixtures/config3.hcl @@ -55,3 +55,4 @@ pid_file = "./pidfile" raw_storage_endpoint = true disable_sealwrap = true disable_sentinel_trace = true +administrative_namespace_path = "admin/" diff --git a/command/server/test-fixtures/config_with_valid_admin_ns.hcl b/command/server/test-fixtures/config_with_valid_admin_ns.hcl new file mode 100644 index 000000000..312a42a79 --- /dev/null +++ b/command/server/test-fixtures/config_with_valid_admin_ns.hcl @@ -0,0 +1,19 @@ +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: MPL-2.0 + +storage "raft" { + path = "/path/to/raft" + node_id = "raft_node_1" +} +listener "tcp" { + address = "127.0.0.1:8200" + tls_cert_file = "/path/to/cert.pem" + tls_key_file = "/path/to/key.key" +} +seal "awskms" { + kms_key_id = "alias/kms-unseal-key" +} +service_registration "consul" { + address = "127.0.0.1:8500" +} +administrative_namespace_path = "admin/" \ No newline at end of file diff --git a/command/server/test-fixtures/config_with_valid_admin_ns.json b/command/server/test-fixtures/config_with_valid_admin_ns.json new file mode 100644 index 000000000..9f6041381 --- /dev/null +++ b/command/server/test-fixtures/config_with_valid_admin_ns.json @@ -0,0 +1,28 @@ +{ + "listener": { + "tcp": { + "address": "0.0.0.0:8200", + "tls_cert_file": "/path/to/cert.pem", + "tls_key_file": "/path/to/key.key" + } + }, + "seal": { + "awskms": { + "kms_key_id": "alias/kms-unseal-key" + } + }, + "storage": { + "raft": { + "path": "/path/to/raft", + "node_id": "raft_node_1" + } + }, + "cluster_addr": "http://127.0.0.1:8201", + "api_addr": "http://127.0.0.1:8200", + "service_registration": { + "consul": { + "address": "127.0.0.1:8500" + } + }, + "administrative_namespace_path": "admin/" +} \ No newline at end of file diff --git a/http/sys_config_state_test.go b/http/sys_config_state_test.go index c8f6c402b..f6967531e 100644 --- a/http/sys_config_state_test.go +++ b/http/sys_config_state_test.go @@ -173,7 +173,8 @@ func TestSysConfigState_Sanitized(t *testing.T) { "type": "tcp", }, }, - "storage": tc.expectedStorageOutput, + "storage": tc.expectedStorageOutput, + "administrative_namespace_path": "", } if tc.expectedHAStorageOutput != nil { diff --git a/internalshared/configutil/config.go b/internalshared/configutil/config.go index 5e462fdd6..04f94a854 100644 --- a/internalshared/configutil/config.go +++ b/internalshared/configutil/config.go @@ -53,6 +53,8 @@ type SharedConfig struct { PidFile string `hcl:"pid_file"` ClusterName string `hcl:"cluster_name"` + + AdministrativeNamespacePath string `hcl:"administrative_namespace_path"` } func ParseConfig(d string) (*SharedConfig, error) { @@ -167,12 +169,13 @@ func (c *SharedConfig) Sanitized() map[string]interface{} { } result := map[string]interface{}{ - "cluster_name": c.ClusterName, - "default_max_request_duration": c.DefaultMaxRequestDuration, - "disable_mlock": c.DisableMlock, - "log_format": c.LogFormat, - "log_level": c.LogLevel, - "pid_file": c.PidFile, + "default_max_request_duration": c.DefaultMaxRequestDuration, + "disable_mlock": c.DisableMlock, + "log_level": c.LogLevel, + "log_format": c.LogFormat, + "pid_file": c.PidFile, + "cluster_name": c.ClusterName, + "administrative_namespace_path": c.AdministrativeNamespacePath, } // Optional log related settings diff --git a/sdk/helper/testcluster/docker/environment.go b/sdk/helper/testcluster/docker/environment.go index 204bacbdb..b53de23a9 100644 --- a/sdk/helper/testcluster/docker/environment.go +++ b/sdk/helper/testcluster/docker/environment.go @@ -609,6 +609,8 @@ func (n *DockerClusterNode) Start(ctx context.Context, opts *DockerClusterOption vaultCfg["api_addr"] = `https://{{- GetAllInterfaces | exclude "flags" "loopback" | attr "address" -}}:8200` vaultCfg["cluster_addr"] = `https://{{- GetAllInterfaces | exclude "flags" "loopback" | attr "address" -}}:8201` + vaultCfg["administrative_namespace_path"] = opts.AdministrativeNamespacePath + systemJSON, err := json.Marshal(vaultCfg) if err != nil { return err diff --git a/sdk/helper/testcluster/types.go b/sdk/helper/testcluster/types.go index 084413521..16725157c 100644 --- a/sdk/helper/testcluster/types.go +++ b/sdk/helper/testcluster/types.go @@ -90,15 +90,16 @@ type ClusterJson struct { } type ClusterOptions struct { - ClusterName string - KeepStandbysSealed bool - SkipInit bool - CACert []byte - NumCores int - TmpDir string - Logger hclog.Logger - VaultNodeConfig *VaultNodeConfig - VaultLicense string + ClusterName string + KeepStandbysSealed bool + SkipInit bool + CACert []byte + NumCores int + TmpDir string + Logger hclog.Logger + VaultNodeConfig *VaultNodeConfig + VaultLicense string + AdministrativeNamespacePath string } type CA struct { diff --git a/vault/core.go b/vault/core.go index 11427e837..473de0362 100644 --- a/vault/core.go +++ b/vault/core.go @@ -852,6 +852,10 @@ type CoreConfig struct { PendingRemovalMountsAllowed bool ExpirationRevokeRetryBase time.Duration + + // AdministrativeNamespacePath is used to configure the administrative namespace, which has access to some sys endpoints that are + // only accessible in the root namespace, currently sys/audit-hash and sys/monitor. + AdministrativeNamespacePath string } // GetServiceRegistration returns the config's ServiceRegistration, or nil if it does @@ -1204,7 +1208,7 @@ func NewCore(conf *CoreConfig) (*Core, error) { c.AddLogger(identityLogger) return NewIdentityStore(ctx, c, config, identityLogger) } - addExtraLogicalBackends(c, logicalBackends) + addExtraLogicalBackends(c, logicalBackends, conf.AdministrativeNamespacePath) c.logicalBackends = logicalBackends credentialBackends := make(map[string]logical.Factory) diff --git a/vault/core_util.go b/vault/core_util.go index 7cf66075a..5a63f313c 100644 --- a/vault/core_util.go +++ b/vault/core_util.go @@ -81,7 +81,7 @@ func (c *Core) PersistUndoLogs() error { return nil } func (c *Core) teardownReplicationResolverHandler() {} func createSecondaries(*Core, *CoreConfig) {} -func addExtraLogicalBackends(*Core, map[string]logical.Factory) {} +func addExtraLogicalBackends(*Core, map[string]logical.Factory, string) {} func addExtraCredentialBackends(*Core, map[string]logical.Factory) {} diff --git a/vault/logical_system_paths.go b/vault/logical_system_paths.go index ea3bc35ed..57e09c658 100644 --- a/vault/logical_system_paths.go +++ b/vault/logical_system_paths.go @@ -1486,48 +1486,52 @@ func (b *SystemBackend) statusPaths() []*framework.Path { } } +func (b *SystemBackend) auditHashPath() *framework.Path { + return &framework.Path{ + Pattern: "audit-hash/(?P.+)", + + DisplayAttrs: &framework.DisplayAttributes{ + OperationPrefix: "auditing", + OperationVerb: "calculate", + OperationSuffix: "hash", + }, + + Fields: map[string]*framework.FieldSchema{ + "path": { + Type: framework.TypeString, + Description: strings.TrimSpace(sysHelp["audit_path"][0]), + }, + + "input": { + Type: framework.TypeString, + }, + }, + + Operations: map[logical.Operation]framework.OperationHandler{ + logical.UpdateOperation: &framework.PathOperation{ + Callback: b.handleAuditHash, + Responses: map[int][]framework.Response{ + http.StatusOK: {{ + Description: "OK", + Fields: map[string]*framework.FieldSchema{ + "hash": { + Type: framework.TypeString, + Required: true, + }, + }, + }}, + }, + }, + }, + + HelpSynopsis: strings.TrimSpace(sysHelp["audit-hash"][0]), + HelpDescription: strings.TrimSpace(sysHelp["audit-hash"][1]), + } +} + func (b *SystemBackend) auditPaths() []*framework.Path { return []*framework.Path{ - { - Pattern: "audit-hash/(?P.+)", - - DisplayAttrs: &framework.DisplayAttributes{ - OperationPrefix: "auditing", - OperationVerb: "calculate", - OperationSuffix: "hash", - }, - - Fields: map[string]*framework.FieldSchema{ - "path": { - Type: framework.TypeString, - Description: strings.TrimSpace(sysHelp["audit_path"][0]), - }, - - "input": { - Type: framework.TypeString, - }, - }, - - Operations: map[logical.Operation]framework.OperationHandler{ - logical.UpdateOperation: &framework.PathOperation{ - Callback: b.handleAuditHash, - Responses: map[int][]framework.Response{ - http.StatusOK: {{ - Description: "OK", - Fields: map[string]*framework.FieldSchema{ - "hash": { - Type: framework.TypeString, - Required: true, - }, - }, - }}, - }, - }, - }, - - HelpSynopsis: strings.TrimSpace(sysHelp["audit-hash"][0]), - HelpDescription: strings.TrimSpace(sysHelp["audit-hash"][1]), - }, + b.auditHashPath(), { Pattern: "audit$", diff --git a/vault/mount.go b/vault/mount.go index 0fca6c5e3..45ec7cc09 100644 --- a/vault/mount.go +++ b/vault/mount.go @@ -1702,6 +1702,7 @@ func (c *Core) newLogicalBackend(ctx context.Context, entry *MountEntry, sysView config.EventsSender = pluginEventSender } + ctx = namespace.ContextWithNamespace(ctx, entry.namespace) ctx = context.WithValue(ctx, "core_number", c.coreNumber) b, err := f(ctx, config) if err != nil { diff --git a/vault/testing.go b/vault/testing.go index 35d384116..a264a0e75 100644 --- a/vault/testing.go +++ b/vault/testing.go @@ -219,6 +219,7 @@ func TestCoreWithSealAndUINoCleanup(t testing.T, opts *CoreConfig) *Core { conf.DetectDeadlocks = opts.DetectDeadlocks conf.Experiments = []string{experiments.VaultExperimentEventsAlpha1} conf.CensusAgent = opts.CensusAgent + conf.AdministrativeNamespacePath = opts.AdministrativeNamespacePath if opts.Logger != nil { conf.Logger = opts.Logger @@ -1543,6 +1544,7 @@ func NewTestCluster(t testing.T, base *CoreConfig, opts *TestClusterOptions) *Te coreConfig.DisableSentinelTrace = base.DisableSentinelTrace coreConfig.ClusterName = base.ClusterName coreConfig.DisableAutopilot = base.DisableAutopilot + coreConfig.AdministrativeNamespacePath = base.AdministrativeNamespacePath if base.BuiltinRegistry != nil { coreConfig.BuiltinRegistry = base.BuiltinRegistry