Skip to content

Commit

Permalink
[WIP] Support custom max Nomad token name length [supersedes #4361] (#…
Browse files Browse the repository at this point in the history
…5117)

* Nomad: updating max token length to 256

* Initial support for supporting custom max token name length for Nomad

* simplify/correct tests

* document nomad max_token_name_length

* removed support for max token length env var. Rename field for clarity

* cleanups after removing env var support

* move RandomWithPrefix to testhelpers

* fix spelling

* Remove default 256 value. Use zero as a sentinel value and ignore it

* update docs
  • Loading branch information
catsby authored and chrishoffman committed Aug 16, 2018
1 parent 1b8b425 commit ca1e2a7
Show file tree
Hide file tree
Showing 6 changed files with 201 additions and 11 deletions.
3 changes: 3 additions & 0 deletions builtin/logical/nomad/backend.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"github.com/hashicorp/vault/logical/framework"
)

// Factory returns a Nomad backend that satisfies the logical.Backend interface
func Factory(ctx context.Context, conf *logical.BackendConfig) (logical.Backend, error) {
b := Backend()
if err := b.Setup(ctx, conf); err != nil {
Expand All @@ -16,6 +17,7 @@ func Factory(ctx context.Context, conf *logical.BackendConfig) (logical.Backend,
return b, nil
}

// Backend returns the configured Nomad backend
func Backend() *backend {
var b backend
b.Backend = &framework.Backend{
Expand Down Expand Up @@ -66,5 +68,6 @@ func (b *backend) client(ctx context.Context, s logical.Storage) (*api.Client, e
if err != nil {
return nil, err
}

return client, nil
}
158 changes: 155 additions & 3 deletions builtin/logical/nomad/backend_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"time"

nomadapi "github.com/hashicorp/nomad/api"
"github.com/hashicorp/vault/helper/testhelpers"
"github.com/hashicorp/vault/logical"
"github.com/mitchellh/mapstructure"
"github.com/ory/dockertest"
Expand All @@ -29,8 +30,8 @@ func prepareTestContainer(t *testing.T) (cleanup func(), retAddress string, noma
}

dockerOptions := &dockertest.RunOptions{
Repository: "djenriquez/nomad",
Tag: "latest",
Repository: "catsby/nomad",
Tag: "0.8.4",
Cmd: []string{"agent", "-dev"},
Env: []string{`NOMAD_LOCAL_CONFIG=bind_addr = "0.0.0.0" acl { enabled = true }`},
}
Expand Down Expand Up @@ -142,7 +143,8 @@ func TestBackend_config_access(t *testing.T) {
}

expected := map[string]interface{}{
"address": connData["address"].(string),
"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)
Expand Down Expand Up @@ -300,3 +302,153 @@ func TestBackend_CredsCreateEnvVar(t *testing.T) {
t.Fatalf("resp is error: %v", resp.Error())
}
}

func TestBackend_max_token_name_length(t *testing.T) {
config := logical.TestBackendConfig()
config.StorageView = &logical.InmemStorage{}
b, err := Factory(context.Background(), config)
if err != nil {
t.Fatal(err)
}

cleanup, connURL, connToken := prepareTestContainer(t)
defer cleanup()

testCases := []struct {
title string
roleName string
tokenLength int
}{
{
title: "Default",
},
{
title: "ConfigOverride",
tokenLength: 64,
},
{
title: "ConfigOverride-LongName",
roleName: "testlongerrolenametoexceed64charsdddddddddddddddddddddddd",
tokenLength: 64,
},
{
title: "Notrim",
roleName: "testlongersubrolenametoexceed64charsdddddddddddddddddddddddd",
},
}

for _, tc := range testCases {
t.Run(tc.title, func(t *testing.T) {
// setup config/access
connData := map[string]interface{}{
"address": connURL,
"token": connToken,
"max_token_name_length": tc.tokenLength,
}
expected := map[string]interface{}{
"address": connURL,
"max_token_name_length": tc.tokenLength,
}

expectedMaxTokenNameLength := maxTokenNameLength
if tc.tokenLength != 0 {
expectedMaxTokenNameLength = tc.tokenLength
}

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)
}

// verify token length is returned in the config/access query
if !reflect.DeepEqual(expected, resp.Data) {
t.Fatalf("bad: expected:%#v\nactual:%#v\n", expected, resp.Data)
}
// verify token is not returned
if resp.Data["token"] != nil {
t.Fatalf("token should not be set in the response")
}

// create a role to create nomad credentials with
// Seeds random with current timestamp

if tc.roleName == "" {
tc.roleName = "test"
}
roleTokenName := testhelpers.RandomWithPrefix(tc.roleName)

confReq.Path = "role/" + roleTokenName
confReq.Operation = logical.UpdateOperation
confReq.Data = map[string]interface{}{
"policies": []string{"policy"},
"lease": "6h",
}
resp, err = b.HandleRequest(context.Background(), &confReq)
if err != nil {
t.Fatal(err)
}

confReq.Operation = logical.ReadOperation
confReq.Path = "creds/" + roleTokenName
resp, err = b.HandleRequest(context.Background(), &confReq)
if err != nil {
t.Fatal(err)
}
if resp == nil {
t.Fatal("resp nil")
}
if resp.IsError() {
t.Fatalf("resp is error: %v", resp.Error())
}

// extract the secret, so we can query nomad directly
generatedSecret := resp.Secret
generatedSecret.TTL = 6 * time.Hour

var d struct {
Token string `mapstructure:"secret_id"`
Accessor string `mapstructure:"accessor_id"`
}
if err := mapstructure.Decode(resp.Data, &d); err != nil {
t.Fatal(err)
}

// Build a client and verify that the credentials work
nomadapiConfig := nomadapi.DefaultConfig()
nomadapiConfig.Address = connData["address"].(string)
nomadapiConfig.SecretID = d.Token
client, err := nomadapi.NewClient(nomadapiConfig)
if err != nil {
t.Fatal(err)
}

// default query options for Nomad queries ... not sure if needed
qOpts := &nomadapi.QueryOptions{
Namespace: "default",
}

// connect to Nomad and verify the token name does not exceed the
// max_token_name_length
token, _, err := client.ACLTokens().Self(qOpts)
if err != nil {
t.Fatal(err)
}

if len(token.Name) > expectedMaxTokenNameLength {
t.Fatalf("token name exceeds max length (%d): %s (%d)", expectedMaxTokenNameLength, token.Name, len(token.Name))
}
})
}
}
15 changes: 12 additions & 3 deletions builtin/logical/nomad/path_config_access.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,11 @@ func pathConfigAccess(b *backend) *framework.Path {
Type: framework.TypeString,
Description: "Token for API calls",
},

"max_token_name_length": &framework.FieldSchema{
Type: framework.TypeInt,
Description: "Max length for name of generated Nomad tokens",
},
},

Callbacks: map[logical.Operation]framework.OperationFunc{
Expand Down Expand Up @@ -73,7 +78,8 @@ func (b *backend) pathConfigAccessRead(ctx context.Context, req *logical.Request

return &logical.Response{
Data: map[string]interface{}{
"address": conf.Address,
"address": conf.Address,
"max_token_name_length": conf.MaxTokenNameLength,
},
}, nil
}
Expand All @@ -96,6 +102,8 @@ func (b *backend) pathConfigAccessWrite(ctx context.Context, req *logical.Reques
conf.Token = token.(string)
}

conf.MaxTokenNameLength = data.Get("max_token_name_length").(int)

entry, err := logical.StorageEntryJSON("config/access", conf)
if err != nil {
return nil, err
Expand All @@ -115,6 +123,7 @@ func (b *backend) pathConfigAccessDelete(ctx context.Context, req *logical.Reque
}

type accessConfig struct {
Address string `json:"address"`
Token string `json:"token"`
Address string `json:"address"`
Token string `json:"token"`
MaxTokenNameLength int `json:"max_token_name_length"`
}
19 changes: 15 additions & 4 deletions builtin/logical/nomad/path_creds_create.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ import (
"github.com/hashicorp/vault/logical/framework"
)

// maxTokenNameLength is the maximum length for the name of a Nomad access
// token
const maxTokenNameLength = 256

func pathCredsCreate(b *backend) *framework.Path {
return &framework.Path{
Pattern: "creds/" + framework.GenericNameRegex("name"),
Expand All @@ -29,6 +33,12 @@ func pathCredsCreate(b *backend) *framework.Path {

func (b *backend) pathTokenRead(ctx context.Context, req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
name := d.Get("name").(string)
conf, _ := b.readConfigAccess(ctx, req.Storage)
// establish a default
tokenNameLength := maxTokenNameLength
if conf != nil && conf.MaxTokenNameLength > 0 {
tokenNameLength = conf.MaxTokenNameLength
}

role, err := b.Role(ctx, req.Storage, name)
if err != nil {
Expand Down Expand Up @@ -56,10 +66,11 @@ func (b *backend) pathTokenRead(ctx context.Context, req *logical.Request, d *fr
// Generate a name for the token
tokenName := fmt.Sprintf("vault-%s-%s-%d", name, req.DisplayName, time.Now().UnixNano())

// Handling nomad maximum token length
// https://github.com/hashicorp/nomad/blob/d9276e22b3b74674996fb548cdb6bc4c70d5b0e4/nomad/structs/structs.go#L115
if len(tokenName) > 64 {
tokenName = tokenName[0:63]
// Note: if the given role name is sufficiently long, the UnixNano() portion
// of the pseudo randomized token name is the part that gets trimmed off,
// weakening it's randomness.
if len(tokenName) > tokenNameLength {
tokenName = tokenName[:tokenNameLength]
}

// Create it
Expand Down
7 changes: 7 additions & 0 deletions helper/testhelpers/testhelpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"errors"
"fmt"
"math/rand"
"time"

uuid "github.com/hashicorp/go-uuid"
"github.com/hashicorp/vault/helper/xor"
Expand Down Expand Up @@ -80,3 +81,9 @@ func GenerateRootWithError(t testing.T, cluster *vault.TestCluster, drToken bool
}
return token, nil
}

// RandomWithPrefix is used to generate a unique name with a prefix, for
// randomizing names in acceptance tests
func RandomWithPrefix(name string) string {
return fmt.Sprintf("%s-%d", name, rand.New(rand.NewSource(time.Now().UnixNano())).Int())
}
10 changes: 9 additions & 1 deletion website/source/api/secret/nomad/index.html.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,12 +37,20 @@ Nomad tokens.
This value can also be provided on individual calls with the NOMAD_TOKEN
environment variable.

- `max_token_name_length` `(int: <optional>)` – Specifies the maximum length to
use for the name of the Nomad token generated with [Generate
Credential](#generate-credential). If omitted, `0` is used and ignored,
defaulting to the max value allowed by the Nomad version. For Nomad versions
0.8.3 and earlier, the default is `64`. For Nomad version 0.8.4 and later, the default is
`256`.

### Sample Payload

```json
{
"address": "http://127.0.0.1:4646",
"token": "adha..."
"token": "adha...",
"max_token_name_length": 256
}
```

Expand Down

0 comments on commit ca1e2a7

Please sign in to comment.