From d5b29f697a23d833066b22a752bb7da1a70c43e5 Mon Sep 17 00:00:00 2001 From: divyaac Date: Mon, 14 Aug 2023 12:35:34 -0700 Subject: [PATCH] Chroot Listener (#22304) * Initial oss-patch apply * Added changelog * Renamed changelog txt * Added the imports to the handler file * Added a check that no two ports are the same, and modified changelog * Edited go sum entry * Tidy up using go mod * Use strutil instead * Revert go sum and go mod * Revert sdk go sum * Edited go.sum to before * Edited go.sum again to initial * Revert changes --- changelog/22304.txt | 3 + command/server/config_test_helpers.go | 5 +- command/server/test-fixtures/config3.hcl | 1 + go.sum | 2 + http/handler.go | 9 +- http/handler_stubs_oss.go | 16 ++++ http/util.go | 4 - internalshared/configutil/listener.go | 20 ++++- sdk/go.mod | 1 + sdk/go.sum | 3 + sdk/helper/testcluster/docker/environment.go | 94 +++++++++++++++++--- sdk/helper/testcluster/types.go | 8 +- 12 files changed, 146 insertions(+), 20 deletions(-) create mode 100644 changelog/22304.txt create mode 100644 http/handler_stubs_oss.go diff --git a/changelog/22304.txt b/changelog/22304.txt new file mode 100644 index 000000000000..eeec038ae9ed --- /dev/null +++ b/changelog/22304.txt @@ -0,0 +1,3 @@ +```release-note:improvement +core: add a listener configuration "chroot_namespace" that forces requests to use a namespace hierarchy +``` \ No newline at end of file diff --git a/command/server/config_test_helpers.go b/command/server/config_test_helpers.go index 4c54ad6202f4..9f84cea7ca9e 100644 --- a/command/server/config_test_helpers.go +++ b/command/server/config_test_helpers.go @@ -789,7 +789,8 @@ func testConfig_Sanitized(t *testing.T) { "listeners": []interface{}{ map[string]interface{}{ "config": map[string]interface{}{ - "address": "127.0.0.1:443", + "address": "127.0.0.1:443", + "chroot_namespace": "admin/", }, "type": "tcp", }, @@ -882,6 +883,7 @@ listener "tcp" { proxy_api { enable_quit = true } + chroot_namespace = "admin" }`)) config := Config{ @@ -926,6 +928,7 @@ listener "tcp" { EnableQuit: true, }, CustomResponseHeaders: DefaultCustomHeaders, + ChrootNamespace: "admin/", }, }, }, diff --git a/command/server/test-fixtures/config3.hcl b/command/server/test-fixtures/config3.hcl index 14d1a64fbdc1..dcc2afedf54c 100644 --- a/command/server/test-fixtures/config3.hcl +++ b/command/server/test-fixtures/config3.hcl @@ -12,6 +12,7 @@ cluster_addr = "top_level_cluster_addr" listener "tcp" { address = "127.0.0.1:443" + chroot_namespace="admin/" } backend "consul" { diff --git a/go.sum b/go.sum index 6f41e8ee63d9..aba769f2400e 100644 --- a/go.sum +++ b/go.sum @@ -1815,6 +1815,7 @@ github.com/hashicorp/go-secure-stdlib/strutil v0.1.2 h1:kes8mmyCpxJsI7FTwtzRqEy9 github.com/hashicorp/go-secure-stdlib/strutil v0.1.2/go.mod h1:Gou2R9+il93BqX25LAKCLuM+y9U2T4hlwvT1yprcna4= github.com/hashicorp/go-secure-stdlib/tlsutil v0.1.2 h1:phcbL8urUzF/kxA/Oj6awENaRwfWsjP59GW7u2qlDyY= github.com/hashicorp/go-secure-stdlib/tlsutil v0.1.2/go.mod h1:l8slYwnJA26yBz+ErHpp2IRCLr0vuOMGBORIz4rRiAs= +github.com/hashicorp/go-set v0.1.13/go.mod h1:0/D+R4MFUzJ6XmvjU7liXtznF1eQDxh84GJlhXw+lvo= github.com/hashicorp/go-slug v0.11.1 h1:c6lLdQnlhUWbS5I7hw8SvfymoFuy6EmiFDedy6ir994= github.com/hashicorp/go-slug v0.11.1/go.mod h1:Ib+IWBYfEfJGI1ZyXMGNbu2BU+aa3Dzu41RKLH301v4= github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= @@ -2620,6 +2621,7 @@ github.com/sethvargo/go-limiter v0.7.1/go.mod h1:C0kbSFbiriE5k2FFOe18M1YZbAR2Fiw github.com/shirou/gopsutil/v3 v3.22.6 h1:FnHOFOh+cYAM0C30P+zysPISzlknLC5Z1G4EAElznfQ= github.com/shirou/gopsutil/v3 v3.22.6/go.mod h1:EdIubSnZhbAvBS1yJ7Xi+AShB/hxwLHOMz4MCYz7yMs= github.com/shoenig/test v0.6.4 h1:kVTaSd7WLz5WZ2IaoM0RSzRsUD+m8wRR+5qvntpn4LU= +github.com/shoenig/test v0.6.4/go.mod h1:byHiCGXqrVaflBLAMq/srcZIHynQPQgeyvkvXnjqq0k= github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4= github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= github.com/shopspring/decimal v1.3.1 h1:2Usl1nmF/WZucqkFZhnfFYxxxu8LG21F6nPQBE5gKV8= diff --git a/http/handler.go b/http/handler.go index a0a23d54206f..aac805115bd2 100644 --- a/http/handler.go +++ b/http/handler.go @@ -358,6 +358,7 @@ func wrapGenericHandler(core *vault.Core, h http.Handler, props *vault.HandlerPr } else { ctx, cancelFunc = context.WithTimeout(ctx, maxRequestDuration) } + // if maxRequestSize < 0, no need to set context value // Add a size limiter if desired if maxRequestSize > 0 { @@ -379,11 +380,14 @@ func wrapGenericHandler(core *vault.Core, h http.Handler, props *vault.HandlerPr nw.Header().Set("X-Vault-Hostname", hostname) } + // Extract the namespace from the header before we modify it + ns := r.Header.Get(consts.NamespaceHeaderName) switch { case strings.HasPrefix(r.URL.Path, "/v1/"): - newR, status := adjustRequest(core, r) + // Setting the namespace in the header to be included in the error message + newR, status, err := adjustRequest(core, props.ListenerConfig, r) if status != 0 { - respondError(nw, status, nil) + respondError(nw, status, err) cancelFunc() return } @@ -434,7 +438,6 @@ func wrapGenericHandler(core *vault.Core, h http.Handler, props *vault.HandlerPr }() // Setting the namespace in the header to be included in the error message - ns := r.Header.Get(consts.NamespaceHeaderName) if ns != "" { nw.Header().Set(consts.NamespaceHeaderName, ns) } diff --git a/http/handler_stubs_oss.go b/http/handler_stubs_oss.go new file mode 100644 index 000000000000..b28c3260aa54 --- /dev/null +++ b/http/handler_stubs_oss.go @@ -0,0 +1,16 @@ +//go:build !enterprise + +package http + +import ( + "net/http" + + "github.com/hashicorp/vault/internalshared/configutil" + "github.com/hashicorp/vault/vault" +) + +//go:generate go run github.com/hashicorp/vault/tools/stubmaker + +func adjustRequest(c *vault.Core, listener *configutil.Listener, r *http.Request) (*http.Request, int, error) { + return r, 0, nil +} diff --git a/http/util.go b/http/util.go index 204e8a141e15..61dc8360c846 100644 --- a/http/util.go +++ b/http/util.go @@ -20,10 +20,6 @@ import ( ) var ( - adjustRequest = func(c *vault.Core, r *http.Request) (*http.Request, int) { - return r, 0 - } - genericWrapping = func(core *vault.Core, in http.Handler, props *vault.HandlerProperties) http.Handler { // Wrap the help wrapped handler with another layer with a generic // handler diff --git a/internalshared/configutil/listener.go b/internalshared/configutil/listener.go index 0db3b1e5798b..7f97a7274dc6 100644 --- a/internalshared/configutil/listener.go +++ b/internalshared/configutil/listener.go @@ -19,6 +19,7 @@ import ( "github.com/hashicorp/go-sockaddr/template" "github.com/hashicorp/hcl" "github.com/hashicorp/hcl/hcl/ast" + "github.com/hashicorp/vault/helper/namespace" ) type ListenerTelemetry struct { @@ -118,6 +119,10 @@ type Listener struct { // Custom Http response headers CustomResponseHeaders map[string]map[string]string `hcl:"-"` CustomResponseHeadersRaw interface{} `hcl:"custom_response_headers"` + + // ChrootNamespace will prepend the specified namespace to requests + ChrootNamespaceRaw interface{} `hcl:"chroot_namespace"` + ChrootNamespace string `hcl:"-"` } // AgentAPI allows users to select which parts of the Agent API they want enabled. @@ -201,7 +206,6 @@ func ParseListeners(result *SharedConfig, list *ast.ObjectList) error { return multierror.Prefix(fmt.Errorf("unsupported listener role %q", l.Role), fmt.Sprintf("listeners.%d:", i)) } } - // Request Parameters { if l.MaxRequestSizeRaw != nil { @@ -423,6 +427,20 @@ func ParseListeners(result *SharedConfig, list *ast.ObjectList) error { } result.Listeners = append(result.Listeners, &l) + + // Chroot Namespace + { + // If a valid ChrootNamespace value exists, then canonicalize the namespace value + if l.ChrootNamespaceRaw != nil { + if l.ChrootNamespace, err = parseutil.ParseString(l.ChrootNamespaceRaw); err != nil { + return multierror.Prefix(fmt.Errorf("invalid value for chroot_namespace: %w", err), fmt.Sprintf("listeners.%d", i)) + } else { + l.ChrootNamespace = namespace.Canonicalize(l.ChrootNamespace) + } + + l.ChrootNamespaceRaw = nil + } + } } return nil diff --git a/sdk/go.mod b/sdk/go.mod index c4a2ec99fdbc..47bebacef57a 100644 --- a/sdk/go.mod +++ b/sdk/go.mod @@ -30,6 +30,7 @@ require ( github.com/hashicorp/go-secure-stdlib/password v0.1.1 github.com/hashicorp/go-secure-stdlib/strutil v0.1.2 github.com/hashicorp/go-secure-stdlib/tlsutil v0.1.2 + github.com/hashicorp/go-set v0.1.13 github.com/hashicorp/go-sockaddr v1.0.2 github.com/hashicorp/go-uuid v1.0.3 github.com/hashicorp/go-version v1.6.0 diff --git a/sdk/go.sum b/sdk/go.sum index a6b0f59b096c..5f30ec59b39d 100644 --- a/sdk/go.sum +++ b/sdk/go.sum @@ -119,6 +119,8 @@ github.com/hashicorp/go-secure-stdlib/strutil v0.1.2 h1:kes8mmyCpxJsI7FTwtzRqEy9 github.com/hashicorp/go-secure-stdlib/strutil v0.1.2/go.mod h1:Gou2R9+il93BqX25LAKCLuM+y9U2T4hlwvT1yprcna4= github.com/hashicorp/go-secure-stdlib/tlsutil v0.1.2 h1:phcbL8urUzF/kxA/Oj6awENaRwfWsjP59GW7u2qlDyY= github.com/hashicorp/go-secure-stdlib/tlsutil v0.1.2/go.mod h1:l8slYwnJA26yBz+ErHpp2IRCLr0vuOMGBORIz4rRiAs= +github.com/hashicorp/go-set v0.1.13 h1:k1B5goY3c7OKEzpK+gwAhJexxzAJwDN8kId8YvWrihA= +github.com/hashicorp/go-set v0.1.13/go.mod h1:0/D+R4MFUzJ6XmvjU7liXtznF1eQDxh84GJlhXw+lvo= github.com/hashicorp/go-sockaddr v1.0.2 h1:ztczhD1jLxIRjVejw8gFomI1BQZOe2WoVOu0SyteCQc= github.com/hashicorp/go-sockaddr v1.0.2/go.mod h1:rB4wwRAUzs07qva3c5SdrY/NEtAUjGlgmH/UkBUC97A= github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= @@ -231,6 +233,7 @@ github.com/rogpeppe/go-internal v1.8.1/go.mod h1:JeRgkft04UBgHMgCIwADu4Pn6Mtm5d4 github.com/ryanuber/columnize v2.1.0+incompatible/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= github.com/ryanuber/go-glob v1.0.0 h1:iQh3xXAumdQ+4Ufa5b25cRpC5TYKlno6hsv6Cb3pkBk= github.com/ryanuber/go-glob v1.0.0/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc= +github.com/shoenig/test v0.6.4 h1:kVTaSd7WLz5WZ2IaoM0RSzRsUD+m8wRR+5qvntpn4LU= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0= diff --git a/sdk/helper/testcluster/docker/environment.go b/sdk/helper/testcluster/docker/environment.go index b53de23a98dd..da51b13bc5b0 100644 --- a/sdk/helper/testcluster/docker/environment.go +++ b/sdk/helper/testcluster/docker/environment.go @@ -39,6 +39,7 @@ import ( "github.com/hashicorp/vault/api" dockhelper "github.com/hashicorp/vault/sdk/helper/docker" "github.com/hashicorp/vault/sdk/helper/logging" + "github.com/hashicorp/vault/sdk/helper/strutil" "github.com/hashicorp/vault/sdk/helper/testcluster" uberAtomic "go.uber.org/atomic" "golang.org/x/net/http2" @@ -479,6 +480,7 @@ type DockerClusterNode struct { ImageTag string DataVolumeName string cleanupVolume func() + AllClients []*api.Client } func (n *DockerClusterNode) TLSConfig() *tls.Config { @@ -506,6 +508,30 @@ func (n *DockerClusterNode) APIClient() *api.Client { return client } +func (n *DockerClusterNode) APIClientN(listenerNumber int) (*api.Client, error) { + // We clone to ensure that whenever this method is called, the caller gets + // back a pristine client, without e.g. any namespace or token changes that + // might pollute a shared client. We clone the config instead of the + // client because (1) Client.clone propagates the replicationStateStore and + // the httpClient pointers, (2) it doesn't copy the tlsConfig at all, and + // (3) if clone returns an error, it doesn't feel as appropriate to panic + // below. Who knows why clone might return an error? + if listenerNumber >= len(n.AllClients) { + return nil, fmt.Errorf("invalid listener number %d", listenerNumber) + } + cfg := n.AllClients[listenerNumber].CloneConfig() + client, err := api.NewClient(cfg) + if err != nil { + // It seems fine to panic here, since this should be the same input + // we provided to NewClient when we were setup, and we didn't panic then. + // Better not to completely ignore the error though, suppose there's a + // bug in CloneConfig? + panic(fmt.Sprintf("NewClient error on cloned config: %v", err)) + } + client.SetToken(n.Cluster.rootToken) + return client, nil +} + // NewAPIClient creates and configures a Vault API client to communicate with // the running Vault Cluster for this DockerClusterNode func (n *DockerClusterNode) apiConfig() (*api.Config, error) { @@ -544,6 +570,20 @@ func (n *DockerClusterNode) newAPIClient() (*api.Client, error) { return client, nil } +func (n *DockerClusterNode) newAPIClientForAddress(address string) (*api.Client, error) { + config, err := n.apiConfig() + if err != nil { + return nil, err + } + config.Address = fmt.Sprintf("https://%s", address) + client, err := api.NewClient(config) + if err != nil { + return nil, err + } + client.SetToken(n.Cluster.GetRootToken()) + return client, nil +} + // Cleanup kills the container of the node and deletes its data volume func (n *DockerClusterNode) Cleanup() { n.cleanup() @@ -563,6 +603,17 @@ func (n *DockerClusterNode) cleanup() error { return nil } +func (n *DockerClusterNode) createDefaultListenerConfig() map[string]interface{} { + return map[string]interface{}{"tcp": map[string]interface{}{ + "address": fmt.Sprintf("%s:%d", "0.0.0.0", 8200), + "tls_cert_file": "/vault/config/cert.pem", + "tls_key_file": "/vault/config/key.pem", + "telemetry": map[string]interface{}{ + "unauthenticated_metrics_access": true, + }, + }} +} + func (n *DockerClusterNode) Start(ctx context.Context, opts *DockerClusterOptions) error { if n.DataVolumeName == "" { vol, err := n.DockerAPI.VolumeCreate(ctx, volume.CreateOptions{}) @@ -575,16 +626,25 @@ func (n *DockerClusterNode) Start(ctx context.Context, opts *DockerClusterOption } } vaultCfg := map[string]interface{}{} - vaultCfg["listener"] = map[string]interface{}{ - "tcp": map[string]interface{}{ - "address": fmt.Sprintf("%s:%d", "0.0.0.0", 8200), - "tls_cert_file": "/vault/config/cert.pem", - "tls_key_file": "/vault/config/key.pem", - "telemetry": map[string]interface{}{ - "unauthenticated_metrics_access": true, - }, - }, + var listenerConfig []map[string]interface{} + listenerConfig = append(listenerConfig, n.createDefaultListenerConfig()) + ports := []string{"8200/tcp", "8201/tcp"} + + if opts.VaultNodeConfig != nil && opts.VaultNodeConfig.AdditionalListeners != nil { + for _, config := range opts.VaultNodeConfig.AdditionalListeners { + cfg := n.createDefaultListenerConfig() + listener := cfg["tcp"].(map[string]interface{}) + listener["address"] = fmt.Sprintf("%s:%d", "0.0.0.0", config.Port) + listener["chroot_namespace"] = config.ChrootNamespace + listenerConfig = append(listenerConfig, cfg) + portStr := fmt.Sprintf("%d/tcp", config.Port) + if strutil.StrListContains(ports, portStr) { + return fmt.Errorf("duplicate port %d specified", config.Port) + } + ports = append(ports, portStr) + } } + vaultCfg["listener"] = listenerConfig vaultCfg["telemetry"] = map[string]interface{}{ "disable_hostname": true, } @@ -675,6 +735,7 @@ func (n *DockerClusterNode) Start(ctx context.Context, opts *DockerClusterOption } testcluster.JSONLogNoTimestamp(n.Logger, s) }} + r, err := dockhelper.NewServiceRunner(dockhelper.RunOptions{ ImageRepo: n.ImageRepo, ImageTag: n.ImageTag, @@ -689,7 +750,7 @@ func (n *DockerClusterNode) Start(ctx context.Context, opts *DockerClusterOption "VAULT_LOG_FORMAT=json", "VAULT_LICENSE=" + opts.VaultLicense, }, - Ports: []string{"8200/tcp", "8201/tcp"}, + Ports: ports, ContainerName: n.Name(), NetworkName: opts.NetworkName, CopyFromTo: copyFromTo, @@ -772,6 +833,19 @@ func (n *DockerClusterNode) Start(ctx context.Context, opts *DockerClusterOption } client.SetToken(n.Cluster.rootToken) n.client = client + + n.AllClients = append(n.AllClients, client) + + for _, addr := range svc.StartResult.Addrs[2:] { + // The second element of this list of addresses is the cluster address + // We do not want to create a client for the cluster address mapping + client, err := n.newAPIClientForAddress(addr) + if err != nil { + return err + } + client.SetToken(n.Cluster.rootToken) + n.AllClients = append(n.AllClients, client) + } return nil } diff --git a/sdk/helper/testcluster/types.go b/sdk/helper/testcluster/types.go index 16725157c312..d20bc18a0ed1 100644 --- a/sdk/helper/testcluster/types.go +++ b/sdk/helper/testcluster/types.go @@ -53,7 +53,8 @@ type VaultNodeConfig struct { // ServiceRegistrationType string // ServiceRegistrationOptions map[string]string - StorageOptions map[string]string + StorageOptions map[string]string + AdditionalListeners []VaultNodeListenerConfig DefaultMaxRequestDuration time.Duration `json:"default_max_request_duration"` LogFormat string `json:"log_format"` @@ -102,6 +103,11 @@ type ClusterOptions struct { AdministrativeNamespacePath string } +type VaultNodeListenerConfig struct { + Port int + ChrootNamespace string +} + type CA struct { CACert *x509.Certificate CACertBytes []byte