Add the ability for secret IDs in agent approle to be wrapped (#5654)
This commit is contained in:
parent
71f68f2199
commit
605a7e30ad
|
@ -294,3 +294,254 @@ func testAppRoleEndToEnd(t *testing.T, removeSecretIDFile bool) {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestAppRoleWithWrapping(t *testing.T) {
|
||||
var err error
|
||||
logger := logging.NewVaultLogger(log.Trace)
|
||||
coreConfig := &vault.CoreConfig{
|
||||
DisableMlock: true,
|
||||
DisableCache: true,
|
||||
Logger: log.NewNullLogger(),
|
||||
CredentialBackends: map[string]logical.Factory{
|
||||
"approle": credAppRole.Factory,
|
||||
},
|
||||
}
|
||||
|
||||
cluster := vault.NewTestCluster(t, coreConfig, &vault.TestClusterOptions{
|
||||
HandlerFunc: vaulthttp.Handler,
|
||||
})
|
||||
|
||||
cluster.Start()
|
||||
defer cluster.Cleanup()
|
||||
|
||||
cores := cluster.Cores
|
||||
|
||||
vault.TestWaitActive(t, cores[0].Core)
|
||||
|
||||
client := cores[0].Client
|
||||
origToken := client.Token()
|
||||
|
||||
err = client.Sys().EnableAuthWithOptions("approle", &api.EnableAuthOptions{
|
||||
Type: "approle",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
_, err = client.Logical().Write("auth/approle/role/test1", map[string]interface{}{
|
||||
"bind_secret_id": "true",
|
||||
"token_ttl": "3s",
|
||||
"token_max_ttl": "10s",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
client.SetWrappingLookupFunc(func(operation, path string) string {
|
||||
if path == "auth/approle/role/test1/secret-id" {
|
||||
return "10s"
|
||||
}
|
||||
return ""
|
||||
})
|
||||
|
||||
resp, err := client.Logical().Write("auth/approle/role/test1/secret-id", nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
secretID1 := resp.WrapInfo.Token
|
||||
|
||||
resp, err = client.Logical().Read("auth/approle/role/test1/role-id")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
roleID1 := resp.Data["role_id"].(string)
|
||||
|
||||
rolef, err := ioutil.TempFile("", "auth.role-id.test.")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
role := rolef.Name()
|
||||
rolef.Close() // WriteFile doesn't need it open
|
||||
defer os.Remove(role)
|
||||
t.Logf("input role_id_file_path: %s", role)
|
||||
|
||||
secretf, err := ioutil.TempFile("", "auth.secret-id.test.")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
secret := secretf.Name()
|
||||
secretf.Close()
|
||||
defer os.Remove(secret)
|
||||
t.Logf("input secret_id_file_path: %s", secret)
|
||||
|
||||
// We close these right away because we're just basically testing
|
||||
// permissions and finding a usable file name
|
||||
ouf, err := ioutil.TempFile("", "auth.tokensink.test.")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
out := ouf.Name()
|
||||
ouf.Close()
|
||||
os.Remove(out)
|
||||
t.Logf("output: %s", out)
|
||||
|
||||
ctx, cancelFunc := context.WithCancel(context.Background())
|
||||
timer := time.AfterFunc(30*time.Second, func() {
|
||||
cancelFunc()
|
||||
})
|
||||
defer timer.Stop()
|
||||
|
||||
conf := map[string]interface{}{
|
||||
"role_id_file_path": role,
|
||||
"secret_id_file_path": secret,
|
||||
"secret_id_response_wrapping_path": "auth/approle/role/test1/secret-id",
|
||||
"remove_secret_id_file_after_reading": true,
|
||||
}
|
||||
|
||||
am, err := agentapprole.NewApproleAuthMethod(&auth.AuthConfig{
|
||||
Logger: logger.Named("auth.approle"),
|
||||
MountPath: "auth/approle",
|
||||
Config: conf,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
ahConfig := &auth.AuthHandlerConfig{
|
||||
Logger: logger.Named("auth.handler"),
|
||||
Client: client,
|
||||
}
|
||||
ah := auth.NewAuthHandler(ahConfig)
|
||||
go ah.Run(ctx, am)
|
||||
defer func() {
|
||||
<-ah.DoneCh
|
||||
}()
|
||||
|
||||
config := &sink.SinkConfig{
|
||||
Logger: logger.Named("sink.file"),
|
||||
Config: map[string]interface{}{
|
||||
"path": out,
|
||||
},
|
||||
}
|
||||
fs, err := file.NewFileSink(config)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
config.Sink = fs
|
||||
|
||||
ss := sink.NewSinkServer(&sink.SinkServerConfig{
|
||||
Logger: logger.Named("sink.server"),
|
||||
Client: client,
|
||||
})
|
||||
go ss.Run(ctx, ah.OutputCh, []*sink.SinkConfig{config})
|
||||
defer func() {
|
||||
<-ss.DoneCh
|
||||
}()
|
||||
|
||||
// This has to be after the other defers so it happens first
|
||||
defer cancelFunc()
|
||||
|
||||
// Check that no sink file exists
|
||||
_, err = os.Lstat(out)
|
||||
if err == nil {
|
||||
t.Fatal("expected err")
|
||||
}
|
||||
if !os.IsNotExist(err) {
|
||||
t.Fatal("expected notexist err")
|
||||
}
|
||||
|
||||
if err := ioutil.WriteFile(role, []byte(roleID1), 0600); err != nil {
|
||||
t.Fatal(err)
|
||||
} else {
|
||||
logger.Trace("wrote test role 1", "path", role)
|
||||
}
|
||||
|
||||
if err := ioutil.WriteFile(secret, []byte(secretID1), 0600); err != nil {
|
||||
t.Fatal(err)
|
||||
} else {
|
||||
logger.Trace("wrote test secret 1", "path", secret)
|
||||
}
|
||||
|
||||
checkToken := func() string {
|
||||
timeout := time.Now().Add(5 * time.Second)
|
||||
for {
|
||||
if time.Now().After(timeout) {
|
||||
t.Fatal("did not find a written token after timeout")
|
||||
}
|
||||
val, err := ioutil.ReadFile(out)
|
||||
if err == nil {
|
||||
os.Remove(out)
|
||||
if len(val) == 0 {
|
||||
t.Fatal("written token was empty")
|
||||
}
|
||||
|
||||
if _, err := os.Stat(secret); err == nil {
|
||||
t.Fatal("secret ID file does not exist but was not supposed to be removed")
|
||||
}
|
||||
|
||||
client.SetToken(string(val))
|
||||
secret, err := client.Auth().Token().LookupSelf()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return secret.Data["entity_id"].(string)
|
||||
}
|
||||
time.Sleep(250 * time.Millisecond)
|
||||
}
|
||||
}
|
||||
origEntity := checkToken()
|
||||
|
||||
// Make sure it gets renewed
|
||||
timeout := time.Now().Add(4 * time.Second)
|
||||
for {
|
||||
if time.Now().After(timeout) {
|
||||
break
|
||||
}
|
||||
secret, err := client.Auth().Token().LookupSelf()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
ttl, err := secret.Data["ttl"].(json.Number).Int64()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if ttl > 3 {
|
||||
t.Fatalf("unexpected ttl: %v", secret.Data["ttl"])
|
||||
}
|
||||
}
|
||||
|
||||
// Write new values
|
||||
client.SetToken(origToken)
|
||||
resp, err = client.Logical().Write("auth/approle/role/test1/secret-id", nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
secretID2 := resp.WrapInfo.Token
|
||||
if err := ioutil.WriteFile(secret, []byte(secretID2), 0600); err != nil {
|
||||
t.Fatal(err)
|
||||
} else {
|
||||
logger.Trace("wrote test secret 2", "path", secret)
|
||||
}
|
||||
|
||||
newEntity := checkToken()
|
||||
if newEntity != origEntity {
|
||||
t.Fatal("did not find same entity")
|
||||
}
|
||||
|
||||
timeout = time.Now().Add(4 * time.Second)
|
||||
for {
|
||||
if time.Now().After(timeout) {
|
||||
break
|
||||
}
|
||||
secret, err := client.Auth().Token().LookupSelf()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
ttl, err := secret.Data["ttl"].(json.Number).Int64()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if ttl > 3 {
|
||||
t.Fatalf("unexpected ttl: %v", secret.Data["ttl"])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -24,6 +24,7 @@ type approleMethod struct {
|
|||
cachedRoleID string
|
||||
cachedSecretID string
|
||||
removeSecretIDFileAfterReading bool
|
||||
secretIDResponseWrappingPath string
|
||||
}
|
||||
|
||||
func NewApproleAuthMethod(conf *auth.AuthConfig) (auth.AuthMethod, error) {
|
||||
|
@ -73,6 +74,17 @@ func NewApproleAuthMethod(conf *auth.AuthConfig) (auth.AuthMethod, error) {
|
|||
a.removeSecretIDFileAfterReading = removeSecretIDFileAfterReading
|
||||
}
|
||||
|
||||
secretIDResponseWrappingPathRaw, ok := conf.Config["secret_id_response_wrapping_path"]
|
||||
if ok {
|
||||
a.secretIDResponseWrappingPath, ok = secretIDResponseWrappingPathRaw.(string)
|
||||
if !ok {
|
||||
return nil, errors.New("could not convert 'secret_id_response_wrapping_path' config value to string")
|
||||
}
|
||||
if a.secretIDResponseWrappingPath == "" {
|
||||
return nil, errors.New("'secret_id_response_wrapping_path' value is empty")
|
||||
}
|
||||
}
|
||||
|
||||
return a, nil
|
||||
}
|
||||
|
||||
|
@ -108,7 +120,58 @@ func (a *approleMethod) Authenticate(ctx context.Context, client *api.Client) (s
|
|||
}
|
||||
a.logger.Warn("secret ID file exists but read empty value, re-using cached value")
|
||||
} else {
|
||||
a.cachedSecretID = strings.TrimSpace(string(secretID))
|
||||
stringSecretID := strings.TrimSpace(string(secretID))
|
||||
if a.secretIDResponseWrappingPath != "" {
|
||||
clonedClient, err := client.Clone()
|
||||
if err != nil {
|
||||
return "", nil, errwrap.Wrapf("error cloning client to unwrap secret ID: {{err}}", err)
|
||||
}
|
||||
clonedClient.SetToken(stringSecretID)
|
||||
// Validate the creation path
|
||||
resp, err := clonedClient.Logical().Read("sys/wrapping/lookup")
|
||||
if err != nil {
|
||||
return "", nil, errwrap.Wrapf("error looking up wrapped secret ID: {{err}}", err)
|
||||
}
|
||||
if resp == nil {
|
||||
return "", nil, errors.New("response nil when looking up wrapped secret ID")
|
||||
}
|
||||
if resp.Data == nil {
|
||||
return "", nil, errors.New("data in repsonse nil when looking up wrapped secret ID")
|
||||
}
|
||||
creationPathRaw, ok := resp.Data["creation_path"]
|
||||
if !ok {
|
||||
return "", nil, errors.New("creation_path in repsonse nil when looking up wrapped secret ID")
|
||||
}
|
||||
creationPath, ok := creationPathRaw.(string)
|
||||
if !ok {
|
||||
return "", nil, errors.New("creation_path in repsonse could not be parsed as string when looking up wrapped secret ID")
|
||||
}
|
||||
if creationPath != a.secretIDResponseWrappingPath {
|
||||
a.logger.Error("SECURITY: unable to validate wrapping token creation path", "expected", a.secretIDResponseWrappingPath, "found", creationPath)
|
||||
return "", nil, errors.New("unable to validate wrapping token creation path")
|
||||
}
|
||||
// Now get the secret ID
|
||||
resp, err = clonedClient.Logical().Unwrap("")
|
||||
if err != nil {
|
||||
return "", nil, errwrap.Wrapf("error unwrapping secret ID: {{err}}", err)
|
||||
}
|
||||
if resp == nil {
|
||||
return "", nil, errors.New("response nil when unwrapping secret ID")
|
||||
}
|
||||
if resp.Data == nil {
|
||||
return "", nil, errors.New("data in repsonse nil when unwrapping secret ID")
|
||||
}
|
||||
secretIDRaw, ok := resp.Data["secret_id"]
|
||||
if !ok {
|
||||
return "", nil, errors.New("secret_id in repsonse nil when unwrapping secret ID")
|
||||
}
|
||||
secretID, ok := secretIDRaw.(string)
|
||||
if !ok {
|
||||
return "", nil, errors.New("secret_id in repsonse could not be parsed as string when unwrapping secret ID")
|
||||
}
|
||||
stringSecretID = secretID
|
||||
}
|
||||
a.cachedSecretID = stringSecretID
|
||||
if a.removeSecretIDFileAfterReading {
|
||||
if err := os.Remove(a.secretIDFilePath); err != nil {
|
||||
a.logger.Error("error removing secret ID file after reading", "error", err)
|
||||
|
|
|
@ -29,3 +29,10 @@ cached.
|
|||
* `remove_secret_id_file_after_reading` `(bool: optional, defaults to true)` -
|
||||
This can be set to `false` to disable the default behavior of removing the
|
||||
secret ID file after it's been read.
|
||||
|
||||
* `secret_id_response_wrapping_path` `(string: optional)` - If set, the value
|
||||
at `secret_id_file_path` will be expected to be a [Response-Wrapping
|
||||
Token](https://www.vaultproject.io/docs/concepts/response-wrapping.html)
|
||||
containing the output of the secret ID retrieval endpoint for the role (e.g.
|
||||
`auth/approle/role/webservers/secret-id`) and the creation path for the
|
||||
response-wrapping token must match the value set here.
|
||||
|
|
Loading…
Reference in New Issue