Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Chroot Listener #22304

Merged
merged 15 commits into from
Aug 14, 2023
3 changes: 3 additions & 0 deletions changelog/22304.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
```release-note:improvement
core: add a listener configuration "chroot_namespace" that forces requests to use a namespace hierarchy
```
5 changes: 4 additions & 1 deletion command/server/config_test_helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -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",
},
Expand Down Expand Up @@ -882,6 +883,7 @@ listener "tcp" {
proxy_api {
enable_quit = true
}
chroot_namespace = "admin"
}`))

config := Config{
Expand Down Expand Up @@ -926,6 +928,7 @@ listener "tcp" {
EnableQuit: true,
},
CustomResponseHeaders: DefaultCustomHeaders,
ChrootNamespace: "admin/",
},
},
},
Expand Down
1 change: 1 addition & 0 deletions command/server/test-fixtures/config3.hcl
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ cluster_addr = "top_level_cluster_addr"

listener "tcp" {
address = "127.0.0.1:443"
chroot_namespace="admin/"
}

backend "consul" {
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -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=
Expand Down Expand Up @@ -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=
Expand Down
9 changes: 6 additions & 3 deletions http/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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
}
Expand Down Expand Up @@ -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)
}
Expand Down
16 changes: 16 additions & 0 deletions http/handler_stubs_oss.go
Original file line number Diff line number Diff line change
@@ -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
}
4 changes: 0 additions & 4 deletions http/util.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
20 changes: 19 additions & 1 deletion internalshared/configutil/listener.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions sdk/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
3 changes: 3 additions & 0 deletions sdk/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -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=
Expand Down Expand Up @@ -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=
Expand Down
94 changes: 84 additions & 10 deletions sdk/helper/testcluster/docker/environment.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -479,6 +480,7 @@ type DockerClusterNode struct {
ImageTag string
DataVolumeName string
cleanupVolume func()
AllClients []*api.Client
}

func (n *DockerClusterNode) TLSConfig() *tls.Config {
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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()
Expand All @@ -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{})
Expand All @@ -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,
}
Expand Down Expand Up @@ -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,
Expand All @@ -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,
Expand Down Expand Up @@ -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
}

Expand Down
8 changes: 7 additions & 1 deletion sdk/helper/testcluster/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
Expand Down Expand Up @@ -102,6 +103,11 @@ type ClusterOptions struct {
AdministrativeNamespacePath string
}

type VaultNodeListenerConfig struct {
Port int
ChrootNamespace string
}

type CA struct {
CACert *x509.Certificate
CACertBytes []byte
Expand Down