From 9c294f1ef05a6b14f1214d0d57507b74abe5ac65 Mon Sep 17 00:00:00 2001 From: Conor Mongey Date: Wed, 20 Apr 2022 19:06:25 +0100 Subject: [PATCH] Bootstrap Nomad ACL system if no token is given (#12451) * Bootstrap Nomad ACL system if no token is given Similar to the [Bootstrap the Consul ACL system if no token is given][boostrap-consul] it would be very useful to bootstrap Nomads ACL system and manage it in Vault. [boostrap-consul]:https://github.com/hashicorp/vault/pull/10751 * Add changelog entry * Remove debug log line * Remove redundant else * Rename Nomad acl bootstrap param * Replace sleep with attempt to list nomad leader, setup will retry until successful * fmt --- builtin/logical/nomad/backend.go | 14 +- builtin/logical/nomad/backend_test.go | 178 +++++++++++++++----- builtin/logical/nomad/path_config_access.go | 12 ++ changelog/12451.txt | 3 + 4 files changed, 156 insertions(+), 51 deletions(-) create mode 100644 changelog/12451.txt diff --git a/builtin/logical/nomad/backend.go b/builtin/logical/nomad/backend.go index 56c99f0f1..e1df32e87 100644 --- a/builtin/logical/nomad/backend.go +++ b/builtin/logical/nomad/backend.go @@ -48,12 +48,7 @@ type backend struct { *framework.Backend } -func (b *backend) client(ctx context.Context, s logical.Storage) (*api.Client, error) { - conf, err := b.readConfigAccess(ctx, s) - if err != nil { - return nil, err - } - +func clientFromConfig(conf *accessConfig) (*api.Client, error) { nomadConf := api.DefaultConfig() if conf != nil { if conf.Address != "" { @@ -72,11 +67,14 @@ func (b *backend) client(ctx context.Context, s logical.Storage) (*api.Client, e nomadConf.TLSConfig.ClientKeyPEM = []byte(conf.ClientKey) } } + return api.NewClient(nomadConf) +} - client, err := api.NewClient(nomadConf) +func (b *backend) client(ctx context.Context, s logical.Storage) (*api.Client, error) { + conf, err := b.readConfigAccess(ctx, s) if err != nil { return nil, err } - return client, nil + return clientFromConfig(conf) } diff --git a/builtin/logical/nomad/backend_test.go b/builtin/logical/nomad/backend_test.go index 61b3df04f..985ef4c9b 100644 --- a/builtin/logical/nomad/backend_test.go +++ b/builtin/logical/nomad/backend_test.go @@ -5,6 +5,7 @@ import ( "fmt" "os" "reflect" + "strings" "testing" "time" @@ -27,7 +28,13 @@ func (c *Config) APIConfig() *nomadapi.Config { return apiConfig } -func prepareTestContainer(t *testing.T) (func(), *Config) { +func (c *Config) Client() (*nomadapi.Client, error) { + apiConfig := c.APIConfig() + + return nomadapi.NewClient(apiConfig) +} + +func prepareTestContainer(t *testing.T, bootstrap bool) (func(), *Config) { if retAddress := os.Getenv("NOMAD_ADDR"); retAddress != "" { s, err := docker.NewServiceURLParse(retAddress) if err != nil { @@ -37,8 +44,8 @@ func prepareTestContainer(t *testing.T) (func(), *Config) { } runner, err := docker.NewServiceRunner(docker.RunOptions{ - ImageRepo: "catsby/nomad", - ImageTag: "0.8.4", + ImageRepo: "multani/nomad", + ImageTag: "1.1.6", ContainerName: "nomad", Ports: []string{"4646/tcp"}, Cmd: []string{"agent", "-dev"}, @@ -57,49 +64,39 @@ func prepareTestContainer(t *testing.T) (func(), *Config) { if err != nil { return nil, err } - aclbootstrap, _, err := nomad.ACLTokens().Bootstrap(nil) + + _, err = nomad.Status().Leader() if err != nil { + t.Logf("[DEBUG] Nomad is not ready yet: %s", err) return nil, err } - nomadToken = aclbootstrap.SecretID - t.Logf("[WARN] Generated Master token: %s", nomadToken) - policy := &nomadapi.ACLPolicy{ - Name: "test", - Description: "test", - Rules: `namespace "default" { - policy = "read" - } - `, - } - anonPolicy := &nomadapi.ACLPolicy{ - Name: "anonymous", - Description: "Deny all access for anonymous requests", - Rules: `namespace "default" { - policy = "deny" - } - agent { - policy = "deny" - } - node { - policy = "deny" - } - `, + + if bootstrap { + aclbootstrap, _, err := nomad.ACLTokens().Bootstrap(nil) + if err != nil { + return nil, err + } + nomadToken = aclbootstrap.SecretID + t.Logf("[WARN] Generated Master token: %s", nomadToken) } + nomadAuthConfig := nomadapi.DefaultConfig() nomadAuthConfig.Address = nomad.Address() - nomadAuthConfig.SecretID = nomadToken - nomadAuth, err := nomadapi.NewClient(nomadAuthConfig) - if err != nil { - return nil, err - } - _, err = nomadAuth.ACLPolicies().Upsert(policy, nil) - if err != nil { - return nil, err - } - _, err = nomadAuth.ACLPolicies().Upsert(anonPolicy, nil) - if err != nil { - return nil, err + + if bootstrap { + nomadAuthConfig.SecretID = nomadToken + + nomadAuth, err := nomadapi.NewClient(nomadAuthConfig) + if err != nil { + return nil, err + } + + err = preprePolicies(nomadAuth) + if err != nil { + return nil, err + } } + u, _ := docker.NewServiceURLParse(nomadapiConfig.Address) return &Config{ ServiceURL: *u, @@ -113,6 +110,101 @@ func prepareTestContainer(t *testing.T) (func(), *Config) { return svc.Cleanup, svc.Config.(*Config) } +func preprePolicies(nomadClient *nomadapi.Client) error { + policy := &nomadapi.ACLPolicy{ + Name: "test", + Description: "test", + Rules: `namespace "default" { + policy = "read" + } + `, + } + anonPolicy := &nomadapi.ACLPolicy{ + Name: "anonymous", + Description: "Deny all access for anonymous requests", + Rules: `namespace "default" { + policy = "deny" + } + agent { + policy = "deny" + } + node { + policy = "deny" + } + `, + } + + _, err := nomadClient.ACLPolicies().Upsert(policy, nil) + if err != nil { + return err + } + + _, err = nomadClient.ACLPolicies().Upsert(anonPolicy, nil) + if err != nil { + return err + } + + return nil +} + +func TestBackend_config_Bootstrap(t *testing.T) { + config := logical.TestBackendConfig() + config.StorageView = &logical.InmemStorage{} + b, err := Factory(context.Background(), config) + if err != nil { + t.Fatal(err) + } + + cleanup, svccfg := prepareTestContainer(t, false) + defer cleanup() + + connData := map[string]interface{}{ + "address": svccfg.URL().String(), + "token": "", + } + + confReq := &logical.Request{ + Operation: logical.UpdateOperation, + Path: "config/access", + Storage: config.StorageView, + Data: connData, + } + + resp, err := b.HandleRequest(context.Background(), confReq) + if err != nil || (resp != nil && resp.IsError()) || resp != nil { + t.Fatalf("failed to write configuration: resp:%#v err:%s", resp, err) + } + + confReq.Operation = logical.ReadOperation + resp, err = b.HandleRequest(context.Background(), confReq) + if err != nil || (resp != nil && resp.IsError()) { + t.Fatalf("failed to write configuration: resp:%#v err:%s", resp, err) + } + + expected := map[string]interface{}{ + "address": connData["address"].(string), + "max_token_name_length": 0, + } + if !reflect.DeepEqual(expected, resp.Data) { + t.Fatalf("bad: expected:%#v\nactual:%#v\n", expected, resp.Data) + } + + nomadClient, err := svccfg.Client() + if err != nil { + t.Fatalf("failed to construct nomaad client, %v", err) + } + + token, _, err := nomadClient.ACLTokens().Bootstrap(nil) + if err == nil { + t.Fatalf("expected acl system to be bootstrapped already, but was able to get the bootstrap token : %v", token) + } + // NOTE: fragile test, but it's the only way, AFAIK, to check that nomad is + // bootstrapped + if !strings.Contains(err.Error(), "bootstrap already done") { + t.Fatalf("expected acl system to be bootstrapped already: err: %v", err) + } +} + func TestBackend_config_access(t *testing.T) { config := logical.TestBackendConfig() config.StorageView = &logical.InmemStorage{} @@ -121,7 +213,7 @@ func TestBackend_config_access(t *testing.T) { t.Fatal(err) } - cleanup, svccfg := prepareTestContainer(t) + cleanup, svccfg := prepareTestContainer(t, true) defer cleanup() connData := map[string]interface{}{ @@ -167,7 +259,7 @@ func TestBackend_renew_revoke(t *testing.T) { t.Fatal(err) } - cleanup, svccfg := prepareTestContainer(t) + cleanup, svccfg := prepareTestContainer(t, true) defer cleanup() connData := map[string]interface{}{ @@ -280,7 +372,7 @@ func TestBackend_CredsCreateEnvVar(t *testing.T) { t.Fatal(err) } - cleanup, svccfg := prepareTestContainer(t) + cleanup, svccfg := prepareTestContainer(t, true) defer cleanup() req := logical.TestRequest(t, logical.UpdateOperation, "role/test") @@ -320,7 +412,7 @@ func TestBackend_max_token_name_length(t *testing.T) { t.Fatal(err) } - cleanup, svccfg := prepareTestContainer(t) + cleanup, svccfg := prepareTestContainer(t, true) defer cleanup() testCases := []struct { diff --git a/builtin/logical/nomad/path_config_access.go b/builtin/logical/nomad/path_config_access.go index a81200de2..d834b0b19 100644 --- a/builtin/logical/nomad/path_config_access.go +++ b/builtin/logical/nomad/path_config_access.go @@ -129,6 +129,18 @@ func (b *backend) pathConfigAccessWrite(ctx context.Context, req *logical.Reques conf.ClientKey = clientKey.(string) } + if conf.Token == "" { + client, err := clientFromConfig(conf) + if err != nil { + return logical.ErrorResponse("Token not provided and failed to constuct client"), err + } + token, _, err := client.ACLTokens().Bootstrap(nil) + if err != nil { + return logical.ErrorResponse("Token not provided and failed to bootstrap ACLs"), err + } + conf.Token = token.SecretID + } + conf.MaxTokenNameLength = data.Get("max_token_name_length").(int) entry, err := logical.StorageEntryJSON("config/access", conf) diff --git a/changelog/12451.txt b/changelog/12451.txt new file mode 100644 index 000000000..9cd265f7c --- /dev/null +++ b/changelog/12451.txt @@ -0,0 +1,3 @@ +```release-note:feature +nomad: Bootstrap Nomad ACL system if no token is provided +```