agent: allow auto-auth to use an existing token (#10850)
* agent/auto-auth: add use_existing_token * Add better logging for lookup errors * Fix test * changelog * Remove preload config, add token var * Update filename * Update changelog * Revert test name * Remove unused function * Remove redundant error message * Short circuit authenticate for preloaded token * Add comment for auto-auth login
This commit is contained in:
parent
acdf42dfbf
commit
ba9b3318d8
|
@ -0,0 +1,3 @@
|
||||||
|
```release-note:improvement
|
||||||
|
agent: change auto-auth to preload an existing token on start
|
||||||
|
```
|
|
@ -2,6 +2,7 @@ package auth
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"math/rand"
|
"math/rand"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
@ -42,6 +43,7 @@ type AuthConfig struct {
|
||||||
type AuthHandler struct {
|
type AuthHandler struct {
|
||||||
OutputCh chan string
|
OutputCh chan string
|
||||||
TemplateTokenCh chan string
|
TemplateTokenCh chan string
|
||||||
|
token string
|
||||||
logger hclog.Logger
|
logger hclog.Logger
|
||||||
client *api.Client
|
client *api.Client
|
||||||
random *rand.Rand
|
random *rand.Rand
|
||||||
|
@ -54,6 +56,7 @@ type AuthHandlerConfig struct {
|
||||||
Logger hclog.Logger
|
Logger hclog.Logger
|
||||||
Client *api.Client
|
Client *api.Client
|
||||||
WrapTTL time.Duration
|
WrapTTL time.Duration
|
||||||
|
Token string
|
||||||
EnableReauthOnNewCredentials bool
|
EnableReauthOnNewCredentials bool
|
||||||
EnableTemplateTokenCh bool
|
EnableTemplateTokenCh bool
|
||||||
}
|
}
|
||||||
|
@ -64,6 +67,7 @@ func NewAuthHandler(conf *AuthHandlerConfig) *AuthHandler {
|
||||||
// has been shut down, during agent shutdown, we won't block
|
// has been shut down, during agent shutdown, we won't block
|
||||||
OutputCh: make(chan string, 1),
|
OutputCh: make(chan string, 1),
|
||||||
TemplateTokenCh: make(chan string, 1),
|
TemplateTokenCh: make(chan string, 1),
|
||||||
|
token: conf.Token,
|
||||||
logger: conf.Logger,
|
logger: conf.Logger,
|
||||||
client: conf.Client,
|
client: conf.Client,
|
||||||
random: rand.New(rand.NewSource(int64(time.Now().Nanosecond()))),
|
random: rand.New(rand.NewSource(int64(time.Now().Nanosecond()))),
|
||||||
|
@ -116,6 +120,7 @@ func (ah *AuthHandler) Run(ctx context.Context, am AuthMethod) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
var watcher *api.LifetimeWatcher
|
var watcher *api.LifetimeWatcher
|
||||||
|
first := true
|
||||||
|
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
|
@ -128,16 +133,11 @@ func (ah *AuthHandler) Run(ctx context.Context, am AuthMethod) error {
|
||||||
// Create a fresh backoff value
|
// Create a fresh backoff value
|
||||||
backoff := 2*time.Second + time.Duration(ah.random.Int63()%int64(time.Second*2)-int64(time.Second))
|
backoff := 2*time.Second + time.Duration(ah.random.Int63()%int64(time.Second*2)-int64(time.Second))
|
||||||
|
|
||||||
ah.logger.Info("authenticating")
|
|
||||||
|
|
||||||
path, header, data, err := am.Authenticate(ctx, ah.client)
|
|
||||||
if err != nil {
|
|
||||||
ah.logger.Error("error getting path or data from method", "error", err, "backoff", backoff.Seconds())
|
|
||||||
backoffOrQuit(ctx, backoff)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
var clientToUse *api.Client
|
var clientToUse *api.Client
|
||||||
|
var err error
|
||||||
|
var path string
|
||||||
|
var data map[string]interface{}
|
||||||
|
var header http.Header
|
||||||
|
|
||||||
switch am.(type) {
|
switch am.(type) {
|
||||||
case AuthMethodWithClient:
|
case AuthMethodWithClient:
|
||||||
|
@ -151,6 +151,38 @@ func (ah *AuthHandler) Run(ctx context.Context, am AuthMethod) error {
|
||||||
clientToUse = ah.client
|
clientToUse = ah.client
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var secret *api.Secret = new(api.Secret)
|
||||||
|
if first && ah.token != "" {
|
||||||
|
ah.logger.Debug("using preloaded token")
|
||||||
|
|
||||||
|
first = false
|
||||||
|
ah.logger.Debug("lookup-self with preloaded token")
|
||||||
|
clientToUse.SetToken(ah.token)
|
||||||
|
|
||||||
|
secret, err = clientToUse.Logical().Read("auth/token/lookup-self")
|
||||||
|
if err != nil {
|
||||||
|
ah.logger.Error("could not look up token", "err", err, "backoff", backoff.Seconds())
|
||||||
|
backoffOrQuit(ctx, backoff)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
duration, _ := secret.Data["ttl"].(json.Number).Int64()
|
||||||
|
secret.Auth = &api.SecretAuth{
|
||||||
|
ClientToken: secret.Data["id"].(string),
|
||||||
|
LeaseDuration: int(duration),
|
||||||
|
Renewable: secret.Data["renewable"].(bool),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ah.logger.Info("authenticating")
|
||||||
|
|
||||||
|
path, header, data, err = am.Authenticate(ctx, ah.client)
|
||||||
|
if err != nil {
|
||||||
|
ah.logger.Error("error getting path or data from method", "error", err, "backoff", backoff.Seconds())
|
||||||
|
backoffOrQuit(ctx, backoff)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if ah.wrapTTL > 0 {
|
if ah.wrapTTL > 0 {
|
||||||
wrapClient, err := clientToUse.Clone()
|
wrapClient, err := clientToUse.Clone()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -169,12 +201,16 @@ func (ah *AuthHandler) Run(ctx context.Context, am AuthMethod) error {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
secret, err := clientToUse.Logical().Write(path, data)
|
// This should only happen if there's no preloaded token (regular auto-auth login)
|
||||||
// Check errors/sanity
|
// or if a preloaded token has expired and is now switching to auto-auth.
|
||||||
if err != nil {
|
if secret.Auth == nil {
|
||||||
ah.logger.Error("error authenticating", "error", err, "backoff", backoff.Seconds())
|
secret, err = clientToUse.Logical().Write(path, data)
|
||||||
backoffOrQuit(ctx, backoff)
|
// Check errors/sanity
|
||||||
continue
|
if err != nil {
|
||||||
|
ah.logger.Error("error authenticating", "error", err, "backoff", backoff.Seconds())
|
||||||
|
backoffOrQuit(ctx, backoff)
|
||||||
|
continue
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
switch {
|
switch {
|
||||||
|
|
|
@ -0,0 +1,238 @@
|
||||||
|
package agent
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
hclog "github.com/hashicorp/go-hclog"
|
||||||
|
"github.com/hashicorp/vault/api"
|
||||||
|
credAppRole "github.com/hashicorp/vault/builtin/credential/approle"
|
||||||
|
"github.com/hashicorp/vault/command/agent/auth"
|
||||||
|
agentAppRole "github.com/hashicorp/vault/command/agent/auth/approle"
|
||||||
|
"github.com/hashicorp/vault/command/agent/sink"
|
||||||
|
"github.com/hashicorp/vault/command/agent/sink/file"
|
||||||
|
vaulthttp "github.com/hashicorp/vault/http"
|
||||||
|
"github.com/hashicorp/vault/sdk/helper/logging"
|
||||||
|
"github.com/hashicorp/vault/sdk/logical"
|
||||||
|
"github.com/hashicorp/vault/vault"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestTokenPreload_UsingAutoAuth(t *testing.T) {
|
||||||
|
logger := logging.NewVaultLogger(hclog.Trace)
|
||||||
|
coreConfig := &vault.CoreConfig{
|
||||||
|
Logger: logger,
|
||||||
|
LogicalBackends: map[string]logical.Factory{
|
||||||
|
"kv": vault.LeasedPassthroughBackendFactory,
|
||||||
|
},
|
||||||
|
CredentialBackends: map[string]logical.Factory{
|
||||||
|
"approle": credAppRole.Factory,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
cluster := vault.NewTestCluster(t, coreConfig, &vault.TestClusterOptions{
|
||||||
|
HandlerFunc: vaulthttp.Handler,
|
||||||
|
})
|
||||||
|
cluster.Start()
|
||||||
|
defer cluster.Cleanup()
|
||||||
|
|
||||||
|
vault.TestWaitActive(t, cluster.Cores[0].Core)
|
||||||
|
client := cluster.Cores[0].Client
|
||||||
|
|
||||||
|
// Setup Vault
|
||||||
|
if err := client.Sys().EnableAuthWithOptions("approle", &api.EnableAuthOptions{
|
||||||
|
Type: "approle",
|
||||||
|
}); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Setup Approle
|
||||||
|
_, err := client.Logical().Write("auth/approle/role/test1", map[string]interface{}{
|
||||||
|
"bind_secret_id": "true",
|
||||||
|
"token_ttl": "3s",
|
||||||
|
"token_max_ttl": "10s",
|
||||||
|
"policies": []string{"test-autoauth"},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := client.Logical().Write("auth/approle/role/test1/secret-id", nil)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
secretID1 := resp.Data["secret_id"].(string)
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||||
|
|
||||||
|
conf := map[string]interface{}{
|
||||||
|
"role_id_file_path": role,
|
||||||
|
"secret_id_file_path": secret,
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Setup Preload Token
|
||||||
|
tokenRespRaw, err := client.Logical().Write("auth/token/create", map[string]interface{}{
|
||||||
|
"ttl": "10s",
|
||||||
|
"explicit-max-ttl": "15s",
|
||||||
|
"policies": []string{""},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if tokenRespRaw.Auth == nil || tokenRespRaw.Auth.ClientToken == "" {
|
||||||
|
t.Fatal("expected token but got none")
|
||||||
|
}
|
||||||
|
token := tokenRespRaw.Auth.ClientToken
|
||||||
|
|
||||||
|
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,
|
||||||
|
Token: token,
|
||||||
|
}
|
||||||
|
|
||||||
|
ah := auth.NewAuthHandler(ahConfig)
|
||||||
|
|
||||||
|
tmpFile, err := ioutil.TempFile("", "auth.tokensink.test.")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
tokenSinkFileName := tmpFile.Name()
|
||||||
|
tmpFile.Close()
|
||||||
|
os.Remove(tokenSinkFileName)
|
||||||
|
t.Logf("output: %s", tokenSinkFileName)
|
||||||
|
|
||||||
|
config := &sink.SinkConfig{
|
||||||
|
Logger: logger.Named("sink.file"),
|
||||||
|
Config: map[string]interface{}{
|
||||||
|
"path": tokenSinkFileName,
|
||||||
|
},
|
||||||
|
WrapTTL: 10 * time.Second,
|
||||||
|
}
|
||||||
|
|
||||||
|
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,
|
||||||
|
})
|
||||||
|
|
||||||
|
errCh := make(chan error)
|
||||||
|
go func() {
|
||||||
|
errCh <- ah.Run(ctx, am)
|
||||||
|
}()
|
||||||
|
defer func() {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
case err := <-errCh:
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
errCh <- ss.Run(ctx, ah.OutputCh, []*sink.SinkConfig{config})
|
||||||
|
}()
|
||||||
|
defer func() {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
case err := <-errCh:
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// This has to be after the other defers so it happens first. It allows
|
||||||
|
// successful test runs to immediately cancel all of the runner goroutines
|
||||||
|
// and unblock any of the blocking defer calls by the runner's DoneCh that
|
||||||
|
// comes before this and avoid successful tests from taking the entire
|
||||||
|
// timeout duration.
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
if stat, err := os.Lstat(tokenSinkFileName); err == nil {
|
||||||
|
t.Fatalf("expected err but got %s", stat)
|
||||||
|
} else if !os.IsNotExist(err) {
|
||||||
|
t.Fatal("expected notexist err")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait 2 seconds for the env variables to be detected and an auth to be generated.
|
||||||
|
time.Sleep(time.Second * 2)
|
||||||
|
|
||||||
|
authToken, err := readToken(tokenSinkFileName)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if authToken.Token == "" {
|
||||||
|
t.Fatal("expected token but didn't receive it")
|
||||||
|
}
|
||||||
|
|
||||||
|
wrappedToken := map[string]interface{}{
|
||||||
|
"token": authToken.Token,
|
||||||
|
}
|
||||||
|
unwrapResp, err := client.Logical().Write("sys/wrapping/unwrap", wrappedToken)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("error unwrapping token: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
sinkToken, ok := unwrapResp.Data["token"].(string)
|
||||||
|
if !ok {
|
||||||
|
t.Fatal("expected token but didn't receive it")
|
||||||
|
}
|
||||||
|
|
||||||
|
if sinkToken != token {
|
||||||
|
t.Fatalf("auth token and preload token should be the same: expected: %s, actual: %s", token, sinkToken)
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue