Skip to content

Commit

Permalink
agent: allow auto-auth to use an existing token (#10850)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
jasonodonnell authored Feb 11, 2021
1 parent 1a4f2d4 commit a2c1f2b
Show file tree
Hide file tree
Showing 3 changed files with 292 additions and 15 deletions.
3 changes: 3 additions & 0 deletions changelog/10850.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
```release-note:improvement
agent: change auto-auth to preload an existing token on start
```
66 changes: 51 additions & 15 deletions command/agent/auth/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package auth

import (
"context"
"encoding/json"
"errors"
"math/rand"
"net/http"
Expand Down Expand Up @@ -42,6 +43,7 @@ type AuthConfig struct {
type AuthHandler struct {
OutputCh chan string
TemplateTokenCh chan string
token string
logger hclog.Logger
client *api.Client
random *rand.Rand
Expand All @@ -54,6 +56,7 @@ type AuthHandlerConfig struct {
Logger hclog.Logger
Client *api.Client
WrapTTL time.Duration
Token string
EnableReauthOnNewCredentials bool
EnableTemplateTokenCh bool
}
Expand All @@ -64,6 +67,7 @@ func NewAuthHandler(conf *AuthHandlerConfig) *AuthHandler {
// has been shut down, during agent shutdown, we won't block
OutputCh: make(chan string, 1),
TemplateTokenCh: make(chan string, 1),
token: conf.Token,
logger: conf.Logger,
client: conf.Client,
random: rand.New(rand.NewSource(int64(time.Now().Nanosecond()))),
Expand Down Expand Up @@ -116,6 +120,7 @@ func (ah *AuthHandler) Run(ctx context.Context, am AuthMethod) error {
}

var watcher *api.LifetimeWatcher
first := true

for {
select {
Expand All @@ -128,16 +133,11 @@ func (ah *AuthHandler) Run(ctx context.Context, am AuthMethod) error {
// Create a fresh backoff value
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 err error
var path string
var data map[string]interface{}
var header http.Header

switch am.(type) {
case AuthMethodWithClient:
Expand All @@ -151,6 +151,38 @@ func (ah *AuthHandler) Run(ctx context.Context, am AuthMethod) error {
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 {
wrapClient, err := clientToUse.Clone()
if err != nil {
Expand All @@ -169,12 +201,16 @@ func (ah *AuthHandler) Run(ctx context.Context, am AuthMethod) error {
}
}

secret, err := clientToUse.Logical().Write(path, data)
// Check errors/sanity
if err != nil {
ah.logger.Error("error authenticating", "error", err, "backoff", backoff.Seconds())
backoffOrQuit(ctx, backoff)
continue
// This should only happen if there's no preloaded token (regular auto-auth login)
// or if a preloaded token has expired and is now switching to auto-auth.
if secret.Auth == nil {
secret, err = clientToUse.Logical().Write(path, data)
// Check errors/sanity
if err != nil {
ah.logger.Error("error authenticating", "error", err, "backoff", backoff.Seconds())
backoffOrQuit(ctx, backoff)
continue
}
}

switch {
Expand Down
238 changes: 238 additions & 0 deletions command/agent/auto_auth_preload_token_end_to_end_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
}

0 comments on commit a2c1f2b

Please sign in to comment.