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
This commit is contained in:
Conor Mongey 2022-04-20 19:06:25 +01:00 committed by GitHub
parent cb16c478e7
commit 9c294f1ef0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 156 additions and 51 deletions

View File

@ -48,12 +48,7 @@ type backend struct {
*framework.Backend *framework.Backend
} }
func (b *backend) client(ctx context.Context, s logical.Storage) (*api.Client, error) { func clientFromConfig(conf *accessConfig) (*api.Client, error) {
conf, err := b.readConfigAccess(ctx, s)
if err != nil {
return nil, err
}
nomadConf := api.DefaultConfig() nomadConf := api.DefaultConfig()
if conf != nil { if conf != nil {
if conf.Address != "" { 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) 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 { if err != nil {
return nil, err return nil, err
} }
return client, nil return clientFromConfig(conf)
} }

View File

@ -5,6 +5,7 @@ import (
"fmt" "fmt"
"os" "os"
"reflect" "reflect"
"strings"
"testing" "testing"
"time" "time"
@ -27,7 +28,13 @@ func (c *Config) APIConfig() *nomadapi.Config {
return apiConfig 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 != "" { if retAddress := os.Getenv("NOMAD_ADDR"); retAddress != "" {
s, err := docker.NewServiceURLParse(retAddress) s, err := docker.NewServiceURLParse(retAddress)
if err != nil { if err != nil {
@ -37,8 +44,8 @@ func prepareTestContainer(t *testing.T) (func(), *Config) {
} }
runner, err := docker.NewServiceRunner(docker.RunOptions{ runner, err := docker.NewServiceRunner(docker.RunOptions{
ImageRepo: "catsby/nomad", ImageRepo: "multani/nomad",
ImageTag: "0.8.4", ImageTag: "1.1.6",
ContainerName: "nomad", ContainerName: "nomad",
Ports: []string{"4646/tcp"}, Ports: []string{"4646/tcp"},
Cmd: []string{"agent", "-dev"}, Cmd: []string{"agent", "-dev"},
@ -57,49 +64,39 @@ func prepareTestContainer(t *testing.T) (func(), *Config) {
if err != nil { if err != nil {
return nil, err return nil, err
} }
aclbootstrap, _, err := nomad.ACLTokens().Bootstrap(nil)
_, err = nomad.Status().Leader()
if err != nil { if err != nil {
t.Logf("[DEBUG] Nomad is not ready yet: %s", err)
return nil, err return nil, err
} }
nomadToken = aclbootstrap.SecretID
t.Logf("[WARN] Generated Master token: %s", nomadToken) if bootstrap {
policy := &nomadapi.ACLPolicy{ aclbootstrap, _, err := nomad.ACLTokens().Bootstrap(nil)
Name: "test", if err != nil {
Description: "test", return nil, err
Rules: `namespace "default" { }
policy = "read" nomadToken = aclbootstrap.SecretID
} t.Logf("[WARN] Generated Master token: %s", nomadToken)
`,
}
anonPolicy := &nomadapi.ACLPolicy{
Name: "anonymous",
Description: "Deny all access for anonymous requests",
Rules: `namespace "default" {
policy = "deny"
}
agent {
policy = "deny"
}
node {
policy = "deny"
}
`,
} }
nomadAuthConfig := nomadapi.DefaultConfig() nomadAuthConfig := nomadapi.DefaultConfig()
nomadAuthConfig.Address = nomad.Address() nomadAuthConfig.Address = nomad.Address()
nomadAuthConfig.SecretID = nomadToken
nomadAuth, err := nomadapi.NewClient(nomadAuthConfig) if bootstrap {
if err != nil { nomadAuthConfig.SecretID = nomadToken
return nil, err
} nomadAuth, err := nomadapi.NewClient(nomadAuthConfig)
_, err = nomadAuth.ACLPolicies().Upsert(policy, nil) if err != nil {
if err != nil { return nil, err
return nil, err }
}
_, err = nomadAuth.ACLPolicies().Upsert(anonPolicy, nil) err = preprePolicies(nomadAuth)
if err != nil { if err != nil {
return nil, err return nil, err
}
} }
u, _ := docker.NewServiceURLParse(nomadapiConfig.Address) u, _ := docker.NewServiceURLParse(nomadapiConfig.Address)
return &Config{ return &Config{
ServiceURL: *u, ServiceURL: *u,
@ -113,6 +110,101 @@ func prepareTestContainer(t *testing.T) (func(), *Config) {
return svc.Cleanup, svc.Config.(*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) { func TestBackend_config_access(t *testing.T) {
config := logical.TestBackendConfig() config := logical.TestBackendConfig()
config.StorageView = &logical.InmemStorage{} config.StorageView = &logical.InmemStorage{}
@ -121,7 +213,7 @@ func TestBackend_config_access(t *testing.T) {
t.Fatal(err) t.Fatal(err)
} }
cleanup, svccfg := prepareTestContainer(t) cleanup, svccfg := prepareTestContainer(t, true)
defer cleanup() defer cleanup()
connData := map[string]interface{}{ connData := map[string]interface{}{
@ -167,7 +259,7 @@ func TestBackend_renew_revoke(t *testing.T) {
t.Fatal(err) t.Fatal(err)
} }
cleanup, svccfg := prepareTestContainer(t) cleanup, svccfg := prepareTestContainer(t, true)
defer cleanup() defer cleanup()
connData := map[string]interface{}{ connData := map[string]interface{}{
@ -280,7 +372,7 @@ func TestBackend_CredsCreateEnvVar(t *testing.T) {
t.Fatal(err) t.Fatal(err)
} }
cleanup, svccfg := prepareTestContainer(t) cleanup, svccfg := prepareTestContainer(t, true)
defer cleanup() defer cleanup()
req := logical.TestRequest(t, logical.UpdateOperation, "role/test") req := logical.TestRequest(t, logical.UpdateOperation, "role/test")
@ -320,7 +412,7 @@ func TestBackend_max_token_name_length(t *testing.T) {
t.Fatal(err) t.Fatal(err)
} }
cleanup, svccfg := prepareTestContainer(t) cleanup, svccfg := prepareTestContainer(t, true)
defer cleanup() defer cleanup()
testCases := []struct { testCases := []struct {

View File

@ -129,6 +129,18 @@ func (b *backend) pathConfigAccessWrite(ctx context.Context, req *logical.Reques
conf.ClientKey = clientKey.(string) 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) conf.MaxTokenNameLength = data.Get("max_token_name_length").(int)
entry, err := logical.StorageEntryJSON("config/access", conf) entry, err := logical.StorageEntryJSON("config/access", conf)

3
changelog/12451.txt Normal file
View File

@ -0,0 +1,3 @@
```release-note:feature
nomad: Bootstrap Nomad ACL system if no token is provided
```