-
Notifications
You must be signed in to change notification settings - Fork 4.3k
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
Nomad Secret Backend integration #3401
Merged
Changes from 48 commits
Commits
Show all changes
57 commits
Select commit
Hold shift + click to select a range
4cda42a
MVP of working Nomad Secret Backend
ncorrare 393e7bf
Fixing data model
ncorrare bcd1477
Adding Nomad secret backend documentation
ncorrare bc1ea9a
Adding Nomad Secret Backend API documentation
ncorrare 9338277
Added tests
ncorrare ca9ad73
Adding Global tokens to the data model
ncorrare bf68079
Various fixes (Null pointer, wait for Nomad go up, Auth before policy…
ncorrare 7e5c465
Working tests
ncorrare 222b9d1
Removing ignore to cleanup function
ncorrare b581716
Updated API Docs with the Global Token Parameter
ncorrare 84b57b2
Adding vendor dependency
ncorrare ff4f920
Adding vendor dir
ncorrare 331068d
Added nomad as dependency after include fix
ncorrare 6a33bf0
Adding further nomad deps
ncorrare 44aed8f
fixing dependencies
chrishoffman a393b20
fixing dependencies
chrishoffman 72b0a2f
Adding Nomad docs to the nav. Minor cosmetics fixes
ncorrare 482d73a
Minor/Cosmetic fixes
ncorrare 3a0d7ac
Unifying Storage and API path in role
ncorrare ffb9343
Should return an error if trying create a management token with polic…
ncorrare 5d3513b
tokenType can never be nil/empty string as there are default values
ncorrare c4bf80c
Ignoring userErr as it will be nil anyway
ncorrare dcaec0a
Refactored config error to just have a single error exit path
ncorrare ca92922
Refactoring readAcessConfig to return a single type of error instead …
ncorrare f3aaacc
Overhauling the client method and attaching it to the backend
ncorrare 7015139
Not storing the Nomad token as we have the accesor for administrative…
ncorrare 6560e3c
Attaching secretToken to backend
ncorrare d1e3eff
Refactored Lease into the Backend configuration
ncorrare f9c30bf
Updated documentation
ncorrare cbe172f
minor cleanup
26daf9d
minor cleanup
b2549f3
adding ttl to secret, refactoring for consistency
e1e63f8
Removing legacy field scheme that belonged to the Consul API
ncorrare a280884
Validating that Address and Token are provided in path_config_access.go
ncorrare 3134c72
Updating descriptions, defaults for roles
ncorrare e3a73ea
Renaming tokenRaw to accessorIDRaw to avoid confusion, as the token i…
ncorrare a5f01d4
Sanitizing error outputs
ncorrare 1db26e7
Return error before creating a client if conf is nil
ncorrare f8babf1
Moving LeaseConfig function to path_config_lease.go
ncorrare cfa0715
Returning nil config if is actually nil, and catching the error befor…
ncorrare e6b3438
Return an error if accesor_id is nil
ncorrare a3df394
Pull master into f-nomad
ncorrare 9d78bfa
Refactoring check for empty accessor as per Vishals suggestion
ncorrare 66840ac
%q quotes automatically
ncorrare 0780c62
Checking if client is not nil before deleting token
ncorrare 12e77fa
Rename policy into policies
ncorrare ea66973
Fix docs up to current standards
ncorrare 884e250
Adding SealWrap configuration, protecting the config/access path
ncorrare 96b0c31
Merge branch 'master' into f-nomad
jefferai 16e2edf
Merge remote-tracking branch 'oss/master' into f-nomad
152b6e4
address some feedback
b82493f
adding access config existence check and delete endpoint
20aac4d
adding existence check for roles
6c19fa3
Merge remote-tracking branch 'oss/master' into f-nomad
737dbca
fixing up config to allow environment vars supported by api client
abbb1c6
use defaultconfig as base, adding env var test
4f31ee7
Merge branch 'master' into f-nomad
jefferai File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,66 @@ | ||
package nomad | ||
|
||
import ( | ||
"github.com/hashicorp/nomad/api" | ||
"github.com/hashicorp/vault/logical" | ||
"github.com/hashicorp/vault/logical/framework" | ||
) | ||
|
||
func Factory(conf *logical.BackendConfig) (logical.Backend, error) { | ||
b := Backend() | ||
if err := b.Setup(conf); err != nil { | ||
return nil, err | ||
} | ||
return b, nil | ||
} | ||
|
||
func Backend() *backend { | ||
var b backend | ||
b.Backend = &framework.Backend{ | ||
PathsSpecial: &logical.Paths{ | ||
SealWrapStorage: []string{ | ||
"config/access", | ||
}, | ||
}, | ||
|
||
Paths: []*framework.Path{ | ||
pathConfigAccess(&b), | ||
pathConfigLease(&b), | ||
pathListRoles(&b), | ||
pathRoles(&b), | ||
pathCredsCreate(&b), | ||
}, | ||
|
||
Secrets: []*framework.Secret{ | ||
secretToken(&b), | ||
}, | ||
BackendType: logical.TypeLogical, | ||
} | ||
|
||
return &b | ||
} | ||
|
||
type backend struct { | ||
*framework.Backend | ||
} | ||
|
||
func (b *backend) client(s logical.Storage) (*api.Client, error) { | ||
conf, err := b.readConfigAccess(s) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
if conf == nil { | ||
return nil, err | ||
} | ||
|
||
nomadConf := api.DefaultConfig() | ||
nomadConf.Address = conf.Address | ||
nomadConf.SecretID = conf.Token | ||
|
||
client, err := api.NewClient(nomadConf) | ||
if err != nil { | ||
return nil, err | ||
} | ||
return client, nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,262 @@ | ||
package nomad | ||
|
||
import ( | ||
"fmt" | ||
"os" | ||
"reflect" | ||
"testing" | ||
"time" | ||
|
||
nomadapi "github.com/hashicorp/nomad/api" | ||
"github.com/hashicorp/vault/logical" | ||
"github.com/mitchellh/mapstructure" | ||
dockertest "gopkg.in/ory-am/dockertest.v3" | ||
) | ||
|
||
func prepareTestContainer(t *testing.T) (cleanup func(), retAddress string, nomadToken string) { | ||
nomadToken = os.Getenv("NOMAD_TOKEN") | ||
|
||
retAddress = os.Getenv("NOMAD_ADDR") | ||
|
||
if retAddress != "" { | ||
return func() {}, retAddress, nomadToken | ||
} | ||
|
||
pool, err := dockertest.NewPool("") | ||
if err != nil { | ||
t.Fatalf("Failed to connect to docker: %s", err) | ||
} | ||
|
||
dockerOptions := &dockertest.RunOptions{ | ||
Repository: "djenriquez/nomad", | ||
Tag: "latest", | ||
Cmd: []string{"agent", "-dev"}, | ||
Env: []string{`NOMAD_LOCAL_CONFIG=bind_addr = "0.0.0.0" acl { enabled = true }`}, | ||
} | ||
resource, err := pool.RunWithOptions(dockerOptions) | ||
if err != nil { | ||
t.Fatalf("Could not start local Nomad docker container: %s", err) | ||
} | ||
|
||
cleanup = func() { | ||
err := pool.Purge(resource) | ||
if err != nil { | ||
t.Fatalf("Failed to cleanup local container: %s", err) | ||
} | ||
} | ||
|
||
retAddress = fmt.Sprintf("http://localhost:%s/", resource.GetPort("4646/tcp")) | ||
// Give Nomad time to initialize | ||
|
||
time.Sleep(5000 * time.Millisecond) | ||
// exponential backoff-retry | ||
if err = pool.Retry(func() error { | ||
var err error | ||
nomadapiConfig := nomadapi.DefaultConfig() | ||
nomadapiConfig.Address = retAddress | ||
nomad, err := nomadapi.NewClient(nomadapiConfig) | ||
if err != nil { | ||
return err | ||
} | ||
aclbootstrap, _, err := nomad.ACLTokens().Bootstrap(nil) | ||
if err != nil { | ||
t.Fatalf("err: %v", err) | ||
} | ||
nomadToken = aclbootstrap.SecretID | ||
t.Log("[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" | ||
} | ||
`, | ||
} | ||
nomadAuthConfig := nomadapi.DefaultConfig() | ||
nomadAuthConfig.Address = retAddress | ||
nomadAuthConfig.SecretID = nomadToken | ||
nomadAuth, err := nomadapi.NewClient(nomadAuthConfig) | ||
_, err = nomadAuth.ACLPolicies().Upsert(policy, nil) | ||
if err != nil { | ||
t.Fatal(err) | ||
} | ||
_, err = nomadAuth.ACLPolicies().Upsert(anonPolicy, nil) | ||
if err != nil { | ||
t.Fatal(err) | ||
} | ||
return err | ||
}); err != nil { | ||
cleanup() | ||
t.Fatalf("Could not connect to docker: %s", err) | ||
} | ||
return cleanup, retAddress, nomadToken | ||
} | ||
|
||
func TestBackend_config_access(t *testing.T) { | ||
config := logical.TestBackendConfig() | ||
config.StorageView = &logical.InmemStorage{} | ||
b, err := Factory(config) | ||
if err != nil { | ||
t.Fatal(err) | ||
} | ||
|
||
cleanup, connURL, connToken := prepareTestContainer(t) | ||
defer cleanup() | ||
|
||
connData := map[string]interface{}{ | ||
"address": connURL, | ||
"token": connToken, | ||
} | ||
|
||
confReq := &logical.Request{ | ||
Operation: logical.UpdateOperation, | ||
Path: "config/access", | ||
Storage: config.StorageView, | ||
Data: connData, | ||
} | ||
|
||
resp, err := b.HandleRequest(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(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), | ||
} | ||
if !reflect.DeepEqual(expected, resp.Data) { | ||
t.Fatalf("bad: expected:%#v\nactual:%#v\n", expected, resp.Data) | ||
} | ||
if resp.Data["token"] != nil { | ||
t.Fatalf("token should not be set in the response") | ||
} | ||
} | ||
|
||
func TestBackend_renew_revoke(t *testing.T) { | ||
config := logical.TestBackendConfig() | ||
config.StorageView = &logical.InmemStorage{} | ||
b, err := Factory(config) | ||
if err != nil { | ||
t.Fatal(err) | ||
} | ||
|
||
cleanup, connURL, connToken := prepareTestContainer(t) | ||
defer cleanup() | ||
connData := map[string]interface{}{ | ||
"address": connURL, | ||
"token": connToken, | ||
} | ||
|
||
req := &logical.Request{ | ||
Storage: config.StorageView, | ||
Operation: logical.UpdateOperation, | ||
Path: "config/access", | ||
Data: connData, | ||
} | ||
resp, err := b.HandleRequest(req) | ||
if err != nil { | ||
t.Fatal(err) | ||
} | ||
|
||
req.Path = "role/test" | ||
req.Data = map[string]interface{}{ | ||
"policies": []string{"policy"}, | ||
"lease": "6h", | ||
} | ||
resp, err = b.HandleRequest(req) | ||
if err != nil { | ||
t.Fatal(err) | ||
} | ||
|
||
req.Operation = logical.ReadOperation | ||
req.Path = "creds/test" | ||
resp, err = b.HandleRequest(req) | ||
if err != nil { | ||
t.Fatal(err) | ||
} | ||
if resp == nil { | ||
t.Fatal("resp nil") | ||
} | ||
if resp.IsError() { | ||
t.Fatalf("resp is error: %v", resp.Error()) | ||
} | ||
|
||
generatedSecret := resp.Secret | ||
generatedSecret.IssueTime = time.Now() | ||
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) | ||
} | ||
t.Log("[WARN] Generated token: %s with accesor %s", d.Token, d.Accessor) | ||
|
||
// 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) | ||
} | ||
|
||
t.Log("[WARN] Verifying that the generated token works...") | ||
_, err = client.Agent().Members, nil | ||
if err != nil { | ||
t.Fatal(err) | ||
} | ||
|
||
req.Operation = logical.RenewOperation | ||
req.Secret = generatedSecret | ||
resp, err = b.HandleRequest(req) | ||
if err != nil { | ||
t.Fatal(err) | ||
} | ||
if resp == nil { | ||
t.Fatal("got nil response from renew") | ||
} | ||
|
||
req.Operation = logical.RevokeOperation | ||
resp, err = b.HandleRequest(req) | ||
if err != nil { | ||
t.Fatal(err) | ||
} | ||
|
||
// Build a management client and verify that the token does not exist anymore | ||
nomadmgmtConfig := nomadapi.DefaultConfig() | ||
nomadmgmtConfig.Address = connData["address"].(string) | ||
nomadmgmtConfig.SecretID = connData["token"].(string) | ||
mgmtclient, err := nomadapi.NewClient(nomadmgmtConfig) | ||
|
||
q := &nomadapi.QueryOptions{ | ||
Namespace: "default", | ||
} | ||
|
||
t.Log("[WARN] Verifying that the generated token does not exist...") | ||
_, _, err = mgmtclient.ACLTokens().Info(d.Accessor, q) | ||
if err == nil { | ||
t.Fatal("err: expected error") | ||
} | ||
} |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The
address
field is not enforced in the configuration. This can overwrite the address in the default configuration by an empty string. If the address is supposed to be overwritten here every time, can we make it mandatory on the config/access endpoint?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed in a280884, making it mandatory to provide an address in config/access
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If env vars are being used to provide address and token, the call above would end up returning a nil client, and even if it didn't, this would then override the env vars with blanks.
Rather than making it mandatory to provide access config, you should switch this around:
Optionally after this you may want to check to make sure the Address and SecretID in nomadConf are populated and return an err if so; I'm not sure if that's appropriate or not with the Nomad libs.
Also, if the official name for Nomad access tokens are Secret IDs, you may want to rename the conf object to match instead of using Token.