mirror of
https://github.com/hashicorp/vault.git
synced 2026-05-05 04:16:31 +02:00
* SECVULN-41437 Require sudo for mounts auth tune * SECVULN-41437 Handle read case and update description * SECVULN-41437 Update godoc linter error * SECVULN-41437 Add changelog entry * SECVULN-41437 Rename changelog entry * SECVULN-41437 Fixing tests * SECVULN-41437 enforce sudo parity for mounts auth tune via root path policy • add mounts/auth/* to system PathsSpecial.Root so sys/mounts/auth/<path>/tune is sudo-gated through core policy checks • remove explicit handler-level sudo/token capability checks for auth-tune routes (OSS + enterprise) that were causing replication/perf invalid-token failures • update TestSystemBackend_mountsAuthTuneRequiresSudo policy expectations for the new enforcement point • align replication overload sys-auth-tune subtest expectations with current behavior * SECVULN-41437 Add static sudo path for API * SECVULN-41437 Update test based on review * SECVULN-41437 Handle incorrect paths special config for sudo * Update changelog/_13738.txt * VAULT-41437 Update system path description per pr feedback * SECVULN-41437 Add external auth tune test with NewTestCluster * SECVULN-41437 Remove un-needed lines in external test * Apply suggestion from @VioletHynes --------- Co-authored-by: Jason Pilz <jasonpilz@gmail.com> Co-authored-by: Violet Hynes <violet.hynes@hashicorp.com>
This commit is contained in:
parent
31fb778a51
commit
fee2a76a3e
@ -22,6 +22,7 @@ var sudoPaths = map[string]*regexp.Regexp{
|
||||
"/sys/audit/{path}": regexp.MustCompile(`^/sys/audit/.+$`),
|
||||
"/sys/auth/{path}": regexp.MustCompile(`^/sys/auth/.+$`),
|
||||
"/sys/auth/{path}/tune": regexp.MustCompile(`^/sys/auth/.+/tune$`),
|
||||
"/sys/mounts/auth/{path}/tune": regexp.MustCompile(`^/sys/mounts/auth/.+/tune$`),
|
||||
"/sys/config/auditing/request-headers": regexp.MustCompile(`^/sys/config/auditing/request-headers$`),
|
||||
"/sys/config/auditing/request-headers/{header}": regexp.MustCompile(`^/sys/config/auditing/request-headers/.+$`),
|
||||
"/sys/config/cors": regexp.MustCompile(`^/sys/config/cors$`),
|
||||
|
||||
3
changelog/_13738.txt
Normal file
3
changelog/_13738.txt
Normal file
@ -0,0 +1,3 @@
|
||||
```release-note:change
|
||||
core: Require sudo capability for `sys/mounts/auth/<path>/tune`, matching `sys/auth/<path>/tune`.
|
||||
```
|
||||
@ -122,7 +122,10 @@ type Factory func(context.Context, *BackendConfig) (Backend, error)
|
||||
|
||||
// Paths is the structure of special paths that is used for SpecialPaths.
|
||||
type Paths struct {
|
||||
// Root are the API paths that require a root token to access
|
||||
// Root are the API paths that require a root token to access.
|
||||
// These can't be regular expressions; each entry is either an exact match,
|
||||
// a prefix match (append '*' as a suffix), or a wildcard segment match
|
||||
// (use '+' in a segment, e.g. 'foo/+/bar').
|
||||
Root []string
|
||||
|
||||
// Unauthenticated are the API paths that can be accessed without any auth.
|
||||
|
||||
@ -0,0 +1,105 @@
|
||||
// Copyright IBM Corp. 2016, 2026
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
package mountsauthtune
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/hashicorp/vault/api"
|
||||
credUserpass "github.com/hashicorp/vault/builtin/credential/userpass"
|
||||
vaulthttp "github.com/hashicorp/vault/http"
|
||||
"github.com/hashicorp/vault/sdk/logical"
|
||||
"github.com/hashicorp/vault/vault"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// TestMountsAuthTuneRequiresSudo ensures sys/mounts/auth/<path>/tune requires
|
||||
// sudo-equivalent privilege while sys/mounts/auth/<path> remains readable
|
||||
// without sudo when ACL allows it.
|
||||
func TestMountsAuthTuneRequiresSudo(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
cluster := vault.NewTestCluster(t, &vault.CoreConfig{
|
||||
CredentialBackends: map[string]logical.Factory{
|
||||
"userpass": credUserpass.Factory,
|
||||
},
|
||||
}, &vault.TestClusterOptions{
|
||||
HandlerFunc: vaulthttp.Handler,
|
||||
NumCores: 1,
|
||||
})
|
||||
|
||||
client := cluster.Cores[0].Client
|
||||
rootToken := cluster.RootToken
|
||||
|
||||
require.NoError(t, client.Sys().EnableAuthWithOptions("userpass", &api.EnableAuthOptions{
|
||||
Type: "userpass",
|
||||
}))
|
||||
|
||||
noSudoPolicy := `
|
||||
path "sys/mounts/auth/userpass/tune" {
|
||||
capabilities = ["read", "update"]
|
||||
}
|
||||
path "sys/mounts/auth/userpass*" {
|
||||
capabilities = ["read"]
|
||||
}
|
||||
`
|
||||
require.NoError(t, client.Sys().PutPolicy("mounts-auth-tune-no-sudo", noSudoPolicy))
|
||||
|
||||
noSudoTokenResp, err := client.Auth().Token().Create(&api.TokenCreateRequest{
|
||||
Policies: []string{"mounts-auth-tune-no-sudo"},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, noSudoTokenResp)
|
||||
require.NotNil(t, noSudoTokenResp.Auth)
|
||||
require.NotEmpty(t, noSudoTokenResp.Auth.ClientToken)
|
||||
|
||||
client.SetToken(noSudoTokenResp.Auth.ClientToken)
|
||||
|
||||
// Non-tune path should remain readable without sudo.
|
||||
nonTuneResp, err := client.Logical().Read("sys/mounts/auth/userpass")
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, nonTuneResp)
|
||||
|
||||
// Tune endpoints should fail without sudo.
|
||||
_, err = client.Logical().Write("sys/mounts/auth/userpass/tune", map[string]interface{}{
|
||||
"max_lease_ttl": "2h",
|
||||
})
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), logical.ErrPermissionDenied.Error())
|
||||
|
||||
_, err = client.Logical().Read("sys/mounts/auth/userpass/tune")
|
||||
require.Error(t, err)
|
||||
require.Contains(t, err.Error(), logical.ErrPermissionDenied.Error())
|
||||
|
||||
client.SetToken(rootToken)
|
||||
|
||||
sudoPolicy := `
|
||||
path "sys/mounts/auth/userpass/tune" {
|
||||
capabilities = ["sudo", "read", "update"]
|
||||
}
|
||||
`
|
||||
require.NoError(t, client.Sys().PutPolicy("mounts-auth-tune-with-sudo", sudoPolicy))
|
||||
|
||||
withSudoTokenResp, err := client.Auth().Token().Create(&api.TokenCreateRequest{
|
||||
Policies: []string{"mounts-auth-tune-with-sudo"},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, withSudoTokenResp)
|
||||
require.NotNil(t, withSudoTokenResp.Auth)
|
||||
require.NotEmpty(t, withSudoTokenResp.Auth.ClientToken)
|
||||
|
||||
client.SetToken(withSudoTokenResp.Auth.ClientToken)
|
||||
|
||||
_, err = client.Logical().Write("sys/mounts/auth/userpass/tune", map[string]interface{}{
|
||||
"max_lease_ttl": "3h",
|
||||
})
|
||||
require.NoError(t, err)
|
||||
|
||||
tuneResp, err := client.Logical().Read("sys/mounts/auth/userpass/tune")
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, tuneResp)
|
||||
require.Contains(t, tuneResp.Data, "max_lease_ttl")
|
||||
require.Equal(t, "10800", fmt.Sprint(tuneResp.Data["max_lease_ttl"]))
|
||||
}
|
||||
@ -169,6 +169,7 @@ func NewSystemBackend(core *Core, logger log.Logger, config *logical.BackendConf
|
||||
PathsSpecial: &logical.Paths{
|
||||
Root: []string{
|
||||
"auth/*",
|
||||
"mounts/auth/+/tune",
|
||||
"remount",
|
||||
"audit",
|
||||
"audit/*",
|
||||
|
||||
@ -4015,7 +4015,7 @@ func (b *SystemBackend) authPaths() []*framework.Path {
|
||||
OperationSuffix: "tuning-information",
|
||||
},
|
||||
Summary: "Reads the given auth path's configuration.",
|
||||
Description: "This endpoint requires sudo capability on the final path, but the same functionality can be achieved without sudo via `sys/mounts/auth/[auth-path]/tune`.",
|
||||
Description: "This endpoint requires sudo capability on the final path. The equivalent endpoint at `sys/mounts/auth/[auth-path]/tune` also requires sudo capability.",
|
||||
Responses: map[int][]framework.Response{
|
||||
http.StatusOK: {{
|
||||
Description: "OK",
|
||||
@ -4030,7 +4030,7 @@ func (b *SystemBackend) authPaths() []*framework.Path {
|
||||
OperationSuffix: "configuration-parameters",
|
||||
},
|
||||
Summary: "Tune configuration parameters for a given auth path.",
|
||||
Description: "This endpoint requires sudo capability on the final path, but the same functionality can be achieved without sudo via `sys/mounts/auth/[auth-path]/tune`.",
|
||||
Description: "This endpoint requires sudo capabilities on the final path. The equivalent endpoint at `sys/mounts/auth/[auth-path]/tune` also requires sudo capabilities.",
|
||||
Responses: map[int][]framework.Response{
|
||||
http.StatusNoContent: {{
|
||||
Description: "OK",
|
||||
@ -4671,7 +4671,7 @@ func (b *SystemBackend) mountsPaths() []*framework.Path {
|
||||
OperationSuffix: "tuning-information",
|
||||
},
|
||||
Summary: "Reads the given auth path's configuration.",
|
||||
Description: "This endpoint does NOT require sudo capability. For the sudo-required alternative, use the endpoint at `sys/auth/[auth-path]/tune`.",
|
||||
Description: "This endpoint requires sudo capability on the final path. The equivalent endpoint at `sys/auth/[auth-path]/tune` also requires sudo capability.",
|
||||
Responses: map[int][]framework.Response{
|
||||
http.StatusOK: {{
|
||||
Description: "OK",
|
||||
@ -4686,7 +4686,7 @@ func (b *SystemBackend) mountsPaths() []*framework.Path {
|
||||
OperationSuffix: "configuration-parameters",
|
||||
},
|
||||
Summary: "Tune configuration parameters for a given auth path.",
|
||||
Description: "This endpoint does NOT require sudo capability. The same functionality can be achieved with sudo via the `sys/auth/[auth-path]/tune` endpoint.",
|
||||
Description: "This endpoint requires sudo capability on the final path. The equivalent endpoint at `sys/auth/[auth-path]/tune` also requires sudo capability.",
|
||||
Responses: map[int][]framework.Response{
|
||||
http.StatusNoContent: {{
|
||||
Description: "OK",
|
||||
|
||||
@ -270,7 +270,11 @@ func (c *Core) reloadBackendCommon(ctx context.Context, entry *MountEntry, isAut
|
||||
// Set paths as well
|
||||
paths := backend.SpecialPaths()
|
||||
if paths != nil {
|
||||
re.rootPaths.Store(pathsToRadix(paths.Root))
|
||||
rootPathsEntry, err := parseSpecialPaths(paths.Root)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
re.rootPaths.Store(rootPathsEntry)
|
||||
loginPathsEntry, err := parseSpecialPaths(paths.Unauthenticated)
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
@ -208,7 +208,11 @@ func (r *Router) Mount(backend logical.Backend, prefix string, mountEntry *Mount
|
||||
storageView: storageView,
|
||||
}
|
||||
re.tainted.Store(mountEntry.Tainted)
|
||||
re.rootPaths.Store(pathsToRadix(paths.Root))
|
||||
rootPathsEntry, err := parseSpecialPaths(paths.Root)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
re.rootPaths.Store(rootPathsEntry)
|
||||
loginPathsEntry, err := parseSpecialPaths(paths.Unauthenticated)
|
||||
if err != nil {
|
||||
return err
|
||||
@ -886,20 +890,35 @@ func (r *Router) RootPath(ctx context.Context, path string) bool {
|
||||
remain := strings.TrimPrefix(adjustedPath, mount)
|
||||
|
||||
// Check the rootPaths of this backend
|
||||
rootPaths := re.rootPaths.Load().(*radix.Tree)
|
||||
match, raw, ok := rootPaths.LongestPrefix(remain)
|
||||
if !ok {
|
||||
rootPaths := re.rootPaths.Load().(*specialPathsEntry)
|
||||
match, raw, ok := rootPaths.paths.LongestPrefix(remain)
|
||||
if !ok && len(rootPaths.wildcardPaths) == 0 {
|
||||
return false
|
||||
}
|
||||
prefixMatch := raw.(bool)
|
||||
|
||||
// Handle the prefix match case
|
||||
if prefixMatch {
|
||||
return strings.HasPrefix(remain, match)
|
||||
if ok {
|
||||
prefixMatch := raw.(bool)
|
||||
|
||||
// Handle the prefix match case
|
||||
if prefixMatch {
|
||||
return strings.HasPrefix(remain, match)
|
||||
}
|
||||
|
||||
// Handle the exact match case
|
||||
if match == remain {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// Handle the exact match case
|
||||
return match == remain
|
||||
// Check root paths containing wildcards
|
||||
reqPathParts := strings.Split(remain, "/")
|
||||
for _, w := range rootPaths.wildcardPaths {
|
||||
if pathMatchesWildcardPath(reqPathParts, w.segments, w.isPrefix) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// LoginPath checks if the given path is used for logins
|
||||
|
||||
@ -313,6 +313,7 @@ func TestRouter_RootPath(t *testing.T) {
|
||||
Root: []string{
|
||||
"root",
|
||||
"policy/*",
|
||||
"mounts/auth/+/tune",
|
||||
},
|
||||
}
|
||||
err = r.Mount(n, "prod/aws/", &MountEntry{UUID: meUUID, Accessor: "awsaccessor", NamespaceID: namespace.RootNamespaceID, namespace: namespace.RootNamespace}, view)
|
||||
@ -332,6 +333,9 @@ func TestRouter_RootPath(t *testing.T) {
|
||||
{"prod/aws/policy", false},
|
||||
{"prod/aws/policy/", true},
|
||||
{"prod/aws/policy/ops", true},
|
||||
{"prod/aws/mounts/auth/userpass/tune", true},
|
||||
{"prod/aws/mounts/auth/userpass", false},
|
||||
{"prod/aws/mounts/auth/userpass/roles", false},
|
||||
}
|
||||
|
||||
for _, tc := range tcases {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user