Skip to content

Commit

Permalink
Merge pull request #5520 from hashicorp/f-nmd-1314-vault-namespaces
Browse files Browse the repository at this point in the history
Support for vault namespaces
  • Loading branch information
Chris Baker authored Apr 6, 2019
2 parents dde26c3 + f8698a8 commit c5f9c10
Show file tree
Hide file tree
Showing 70 changed files with 6,030 additions and 212 deletions.
3 changes: 3 additions & 0 deletions client/allocrunner/taskrunner/template/template.go
Original file line number Diff line number Diff line change
Expand Up @@ -645,6 +645,9 @@ func newRunnerConfig(config *TaskTemplateManagerConfig,
conf.Vault.Address = &cc.VaultConfig.Addr
conf.Vault.Token = &config.VaultToken
conf.Vault.Grace = helper.TimeToPtr(vaultGrace)
if config.ClientConfig.VaultConfig.Namespace != "" {
conf.Vault.Namespace = &config.ClientConfig.VaultConfig.Namespace
}

if strings.HasPrefix(cc.VaultConfig.Addr, "https") || cc.VaultConfig.TLSCertFile != "" {
skipVerify := cc.VaultConfig.TLSSkipVerify != nil && *cc.VaultConfig.TLSSkipVerify
Expand Down
32 changes: 32 additions & 0 deletions client/allocrunner/taskrunner/template/template_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1326,6 +1326,38 @@ func TestTaskTemplateManager_Config_VaultGrace(t *testing.T) {
assert.Equal(10*time.Second, *ctconf.Vault.Grace, "Vault Grace Value")
}

// TestTaskTemplateManager_Config_VaultNamespace asserts the Vault namespace setting is
// propagated to consul-template's configuration.
func TestTaskTemplateManager_Config_VaultNamespace(t *testing.T) {
t.Parallel()
assert := assert.New(t)

testNS := "test-namespace"
c := config.DefaultConfig()
c.Node = mock.Node()
c.VaultConfig = &sconfig.VaultConfig{
Enabled: helper.BoolToPtr(true),
Addr: "https://localhost/",
TLSServerName: "notlocalhost",
Namespace: testNS,
}

alloc := mock.Alloc()
config := &TaskTemplateManagerConfig{
ClientConfig: c,
VaultToken: "token",
EnvBuilder: taskenv.NewBuilder(c.Node, alloc, alloc.Job.TaskGroups[0].Tasks[0], c.Region),
}

ctmplMapping, err := parseTemplateConfigs(config)
assert.Nil(err, "Parsing Templates")

ctconf, err := newRunnerConfig(config, ctmplMapping)
assert.Nil(err, "Building Runner Config")
assert.NotNil(ctconf.Vault.Grace, "Vault Grace Pointer")
assert.Equal(testNS, *ctconf.Vault.Namespace, "Vault Namespace Value")
}

func TestTaskTemplateManager_BlockedEvents(t *testing.T) {
t.Parallel()
require := require.New(t)
Expand Down
6 changes: 6 additions & 0 deletions client/vaultclient/vaultclient.go
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,12 @@ func NewVaultClient(config *config.VaultConfig, logger hclog.Logger, tokenDerive
"User-Agent": []string{"hashicorp/nomad"},
})

// SetHeaders above will replace all headers, make this call second
if config.Namespace != "" {
logger.Debug("configuring Vault namespace", "namespace", config.Namespace)
client.SetNamespace(config.Namespace)
}

c.client = client

return c, nil
Expand Down
20 changes: 20 additions & 0 deletions client/vaultclient/vaultclient_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"github.com/hashicorp/nomad/helper/testlog"
"github.com/hashicorp/nomad/testutil"
vaultapi "github.com/hashicorp/vault/api"
vaultconsts "github.com/hashicorp/vault/helper/consts"
"github.com/stretchr/testify/require"
)

Expand Down Expand Up @@ -92,6 +93,25 @@ func TestVaultClient_TokenRenewals(t *testing.T) {
}
}

// TestVaultClient_NamespaceSupport tests that the Vault namespace config, if present, will result in the
// namespace header being set on the created Vault client.
func TestVaultClient_NamespaceSupport(t *testing.T) {
t.Parallel()
require := require.New(t)
tr := true
testNs := "test-namespace"

logger := testlog.HCLogger(t)

conf := config.DefaultConfig()
conf.VaultConfig.Enabled = &tr
conf.VaultConfig.Token = "testvaulttoken"
conf.VaultConfig.Namespace = testNs
c, err := NewVaultClient(conf.VaultConfig, logger, nil)
require.NoError(err)
require.Equal(testNs, c.client.Headers().Get(vaultconsts.NamespaceHeaderName))
}

func TestVaultClient_Heap(t *testing.T) {
t.Parallel()
tr := true
Expand Down
10 changes: 7 additions & 3 deletions command/agent/command.go
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,7 @@ func (c *Command) readConfig() *Config {
}), "vault-allow-unauthenticated", "")
flags.StringVar(&cmdConfig.Vault.Token, "vault-token", "", "")
flags.StringVar(&cmdConfig.Vault.Addr, "vault-address", "", "")
flags.StringVar(&cmdConfig.Vault.Namespace, "vault-namespace", "", "")
flags.StringVar(&cmdConfig.Vault.Role, "vault-create-from-role", "", "")
flags.StringVar(&cmdConfig.Vault.TLSCaFile, "vault-ca-file", "", "")
flags.StringVar(&cmdConfig.Vault.TLSCaPath, "vault-ca-path", "", "")
Expand Down Expand Up @@ -255,9 +256,12 @@ func (c *Command) readConfig() *Config {

// Check to see if we should read the Vault token from the environment
if config.Vault.Token == "" {
if token, ok := os.LookupEnv("VAULT_TOKEN"); ok {
config.Vault.Token = token
}
config.Vault.Token = os.Getenv("VAULT_TOKEN")
}

// Check to see if we should read the Vault namespace from the environment
if config.Vault.Namespace == "" {
config.Vault.Namespace = os.Getenv("VAULT_NAMESPACE")
}

// Default the plugin directory to be under that of the data directory if it
Expand Down
1 change: 1 addition & 0 deletions command/agent/config_parse.go
Original file line number Diff line number Diff line change
Expand Up @@ -892,6 +892,7 @@ func parseVaultConfig(result **config.VaultConfig, list *ast.ObjectList) error {
"tls_server_name",
"tls_skip_verify",
"token",
"namespace",
}

if err := helper.CheckHCLKeys(listVal, valid); err != nil {
Expand Down
4 changes: 4 additions & 0 deletions e2e/vault/matrix_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@ package vault
var (
// versions is the set of Vault versions we test for backwards compatibility
versions = []string{
"1.1.0",
"1.0.3",
"1.0.2",
"1.0.1",
"1.0.0",
"0.11.5",
"0.11.4",
Expand Down
35 changes: 29 additions & 6 deletions e2e/vault/vault_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ import (
"testing"
"time"

"github.com/hashicorp/go-version"

"github.com/hashicorp/nomad/command/agent"
"github.com/hashicorp/nomad/helper"
"github.com/hashicorp/nomad/nomad/structs/config"
Expand Down Expand Up @@ -184,20 +186,20 @@ func TestVaultCompatibility(t *testing.T) {
for version, vaultBin := range vaultBinaries {
vbin := vaultBin
t.Run(version, func(t *testing.T) {
testVaultCompatibility(t, vbin)
testVaultCompatibility(t, vbin, version)
})
}
}

// testVaultCompatibility tests compatibility with the given vault binary
func testVaultCompatibility(t *testing.T, vault string) {
func testVaultCompatibility(t *testing.T, vault string, version string) {
require := require.New(t)

// Create a Vault server
v := testutil.NewTestVaultFromPath(t, vault)
defer v.Stop()

token := setupVault(t, v.Client)
token := setupVault(t, v.Client, version)

// Create a Nomad agent using the created vault
nomad := agent.NewTestAgent(t, t.Name(), func(c *agent.Config) {
Expand Down Expand Up @@ -251,11 +253,32 @@ func testVaultCompatibility(t *testing.T, vault string) {

// setupVault takes the Vault client and creates the required policies and
// roles. It returns the token that should be used by Nomad
func setupVault(t *testing.T, client *vapi.Client) string {
func setupVault(t *testing.T, client *vapi.Client, vaultVersion string) string {
// Write the policy
sys := client.Sys()
if err := sys.PutPolicy("nomad-server", policy); err != nil {
t.Fatalf("failed to create policy: %v", err)

// pre-0.9.0 vault servers do not work with our new vault client for the policy endpoint
// perform this using a raw HTTP request
newApi, _ := version.NewVersion("0.9.0")
testVersion, err := version.NewVersion(vaultVersion)
if err != nil {
t.Fatalf("failed to parse test version from '%v': %v", t.Name(), err)
}
if testVersion.LessThan(newApi) {
body := map[string]string{
"rules": policy,
}
request := client.NewRequest("PUT", fmt.Sprintf("/v1/sys/policy/%s", "nomad-server"))
if err := request.SetJSONBody(body); err != nil {
t.Fatalf("failed to set JSON body on legacy policy creation: %v", err)
}
if _, err := client.RawRequest(request); err != nil {
t.Fatalf("failed to create legacy policy: %v", err)
}
} else {
if err := sys.PutPolicy("nomad-server", policy); err != nil {
t.Fatalf("failed to create policy: %v", err)
}
}

// Build the role
Expand Down
7 changes: 7 additions & 0 deletions nomad/structs/config/vault.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,10 @@ type VaultConfig struct {
// role the token is from.
Role string `mapstructure:"create_from_role"`

// Namespace sets the Vault namespace used for all calls against the
// Vault API. If this is unset, then Nomad does not use Vault namespaces.
Namespace string `mapstructure:"namespace"`

// AllowUnauthenticated allows users to submit jobs requiring Vault tokens
// without providing a Vault token proving they have access to these
// policies.
Expand Down Expand Up @@ -106,6 +110,9 @@ func (a *VaultConfig) Merge(b *VaultConfig) *VaultConfig {
if b.Token != "" {
result.Token = b.Token
}
if b.Namespace != "" {
result.Namespace = b.Namespace
}
if b.Role != "" {
result.Role = b.Role
}
Expand Down
41 changes: 32 additions & 9 deletions nomad/vault.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,14 @@ import (
"sync/atomic"
"time"

tomb "gopkg.in/tomb.v2"
"gopkg.in/tomb.v2"

metrics "github.com/armon/go-metrics"
"github.com/armon/go-metrics"
log "github.com/hashicorp/go-hclog"
multierror "github.com/hashicorp/go-multierror"
vapi "github.com/hashicorp/vault/api"

"github.com/hashicorp/go-multierror"
"github.com/hashicorp/nomad/nomad/structs"
"github.com/hashicorp/nomad/nomad/structs/config"
vapi "github.com/hashicorp/vault/api"
"github.com/mitchellh/mapstructure"

"golang.org/x/sync/errgroup"
Expand Down Expand Up @@ -173,9 +172,18 @@ type vaultClient struct {
// limiter is used to rate limit requests to Vault
limiter *rate.Limiter

// client is the Vault API client
// client is the Vault API client used for Namespace-relative integrations
// with the Vault API (anything except `/v1/sys`). If this server is not
// configured to reference a Vault namespace, this will point to the same
// client as clientSys
client *vapi.Client

// clientSys is the Vault API client used for non-Namespace-relative integrations
// with the Vault API (anything involving `/v1/sys`). This client is never configured
// with a Vault namespace, because these endpoints may return errors if a namespace
// header is provided
clientSys *vapi.Client

// auth is the Vault token auth API client
auth *vapi.TokenAuth

Expand Down Expand Up @@ -305,6 +313,7 @@ func (v *vaultClient) flush() {
defer v.l.Unlock()

v.client = nil
v.clientSys = nil
v.auth = nil
v.connEstablished = false
v.connEstablishedErr = nil
Expand Down Expand Up @@ -400,11 +409,25 @@ func (v *vaultClient) buildClient() error {
return err
}

// Set the token and store the client
// Store the client, create/assign the /sys client
v.client = client
if v.config.Namespace != "" {
v.logger.Debug("configuring Vault namespace", "namespace", v.config.Namespace)
v.clientSys, err = vapi.NewClient(apiConf)
if err != nil {
v.logger.Error("failed to create Vault sys client and not retrying", "error", err)
return err
}
client.SetNamespace(v.config.Namespace)
} else {
v.clientSys = client
}

// Set the token
v.token = v.config.Token
client.SetToken(v.token)
v.client = client
v.auth = client.Auth().Token()

return nil
}

Expand All @@ -425,7 +448,7 @@ OUTER:
case <-retryTimer.C:
// Ensure the API is reachable
if !initStatus {
if _, err := v.client.Sys().InitStatus(); err != nil {
if _, err := v.clientSys.Sys().InitStatus(); err != nil {
v.logger.Warn("failed to contact Vault API", "retry", v.config.ConnectionRetryIntv, "error", err)
retryTimer.Reset(v.config.ConnectionRetryIntv)
continue OUTER
Expand Down
52 changes: 52 additions & 0 deletions nomad/vault_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import (
"github.com/hashicorp/nomad/nomad/structs/config"
"github.com/hashicorp/nomad/testutil"
vapi "github.com/hashicorp/vault/api"
vaultconsts "github.com/hashicorp/vault/helper/consts"
)

const (
Expand Down Expand Up @@ -175,6 +176,57 @@ func TestVaultClient_BadConfig(t *testing.T) {
}
}

// TestVaultClient_WithNamespaceSupport tests that the Vault namespace config, if present, will result in the
// namespace header being set on the created Vault client.
func TestVaultClient_WithNamespaceSupport(t *testing.T) {
t.Parallel()
require := require.New(t)
tr := true
testNs := "test-namespace"
conf := &config.VaultConfig{
Addr: "https://vault.service.consul:8200",
Enabled: &tr,
Token: "testvaulttoken",
Namespace: testNs,
}
logger := testlog.HCLogger(t)

// Should be no error since Vault is not enabled
c, err := NewVaultClient(conf, logger, nil)
if err != nil {
t.Fatalf("failed to build vault client: %v", err)
}

require.Equal(testNs, c.client.Headers().Get(vaultconsts.NamespaceHeaderName))
require.Equal("", c.clientSys.Headers().Get(vaultconsts.NamespaceHeaderName))
require.NotEqual(c.clientSys, c.client)
}

// TestVaultClient_WithoutNamespaceSupport tests that the Vault namespace config, if present, will result in the
// namespace header being set on the created Vault client.
func TestVaultClient_WithoutNamespaceSupport(t *testing.T) {
t.Parallel()
require := require.New(t)
tr := true
conf := &config.VaultConfig{
Addr: "https://vault.service.consul:8200",
Enabled: &tr,
Token: "testvaulttoken",
Namespace: "",
}
logger := testlog.HCLogger(t)

// Should be no error since Vault is not enabled
c, err := NewVaultClient(conf, logger, nil)
if err != nil {
t.Fatalf("failed to build vault client: %v", err)
}

require.Equal("", c.client.Headers().Get(vaultconsts.NamespaceHeaderName))
require.Equal("", c.clientSys.Headers().Get(vaultconsts.NamespaceHeaderName))
require.Equal(c.clientSys, c.client)
}

// started separately.
// Test that the Vault Client can establish a connection even if it is started
// before Vault is available.
Expand Down
Loading

0 comments on commit c5f9c10

Please sign in to comment.