92dc054bb3
* VAULT-15547 Additional tests, refactoring, for proxy split * VAULT-15547 Additional tests, refactoring, for proxy split * VAULT-15547 Import reorganization * VAULT-15547 Some missed updates for PersistConfig * VAULT-15547 address comments * VAULT-15547 address comments
423 lines
10 KiB
Go
423 lines
10 KiB
Go
// Copyright (c) HashiCorp, Inc.
|
|
// SPDX-License-Identifier: MPL-2.0
|
|
|
|
package agent
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"io/ioutil"
|
|
"net"
|
|
"net/http"
|
|
"os"
|
|
"testing"
|
|
"time"
|
|
|
|
hclog "github.com/hashicorp/go-hclog"
|
|
log "github.com/hashicorp/go-hclog"
|
|
"github.com/hashicorp/vault/api"
|
|
credAppRole "github.com/hashicorp/vault/builtin/credential/approle"
|
|
"github.com/hashicorp/vault/command/agentproxyshared/auth"
|
|
agentapprole "github.com/hashicorp/vault/command/agentproxyshared/auth/approle"
|
|
cache "github.com/hashicorp/vault/command/agentproxyshared/cache"
|
|
"github.com/hashicorp/vault/command/agentproxyshared/sink"
|
|
"github.com/hashicorp/vault/command/agentproxyshared/sink/file"
|
|
"github.com/hashicorp/vault/command/agentproxyshared/sink/inmem"
|
|
"github.com/hashicorp/vault/helper/useragent"
|
|
vaulthttp "github.com/hashicorp/vault/http"
|
|
"github.com/hashicorp/vault/sdk/helper/consts"
|
|
"github.com/hashicorp/vault/sdk/helper/logging"
|
|
"github.com/hashicorp/vault/sdk/logical"
|
|
"github.com/hashicorp/vault/vault"
|
|
)
|
|
|
|
const policyAutoAuthAppRole = `
|
|
path "/kv/*" {
|
|
capabilities = ["sudo", "create", "read", "update", "delete", "list"]
|
|
}
|
|
|
|
path "/auth/token/create" {
|
|
capabilities = ["create", "update"]
|
|
}
|
|
`
|
|
|
|
func TestCache_UsingAutoAuthToken(t *testing.T) {
|
|
var err error
|
|
logger := logging.NewVaultLogger(log.Trace)
|
|
coreConfig := &vault.CoreConfig{
|
|
DisableMlock: true,
|
|
DisableCache: true,
|
|
Logger: log.NewNullLogger(),
|
|
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()
|
|
|
|
cores := cluster.Cores
|
|
|
|
vault.TestWaitActive(t, cores[0].Core)
|
|
|
|
client := cores[0].Client
|
|
|
|
defer os.Setenv(api.EnvVaultAddress, os.Getenv(api.EnvVaultAddress))
|
|
os.Setenv(api.EnvVaultAddress, client.Address())
|
|
|
|
defer os.Setenv(api.EnvVaultCACert, os.Getenv(api.EnvVaultCACert))
|
|
os.Setenv(api.EnvVaultCACert, fmt.Sprintf("%s/ca_cert.pem", cluster.TempDir))
|
|
|
|
err = client.Sys().Mount("kv", &api.MountInput{
|
|
Type: "kv",
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// Create a secret in the backend
|
|
_, err = client.Logical().Write("kv/foo", map[string]interface{}{
|
|
"value": "bar",
|
|
"ttl": "1h",
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// Add an kv-admin policy
|
|
if err := client.Sys().PutPolicy("test-autoauth", policyAutoAuthAppRole); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// Enable approle
|
|
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",
|
|
"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)
|
|
|
|
// 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, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
|
|
|
conf := map[string]interface{}{
|
|
"role_id_file_path": role,
|
|
"secret_id_file_path": secret,
|
|
"remove_secret_id_file_after_reading": true,
|
|
}
|
|
|
|
cacheLogger := logging.NewVaultLogger(hclog.Trace).Named("cache")
|
|
|
|
// Create the API proxier
|
|
apiProxy, err := cache.NewAPIProxy(&cache.APIProxyConfig{
|
|
Client: client,
|
|
Logger: cacheLogger.Named("apiproxy"),
|
|
UserAgentStringFunction: useragent.ProxyStringWithProxiedUserAgent,
|
|
UserAgentString: useragent.ProxyAPIProxyString(),
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// Create the lease cache proxier and set its underlying proxier to
|
|
// the API proxier.
|
|
leaseCache, err := cache.NewLeaseCache(&cache.LeaseCacheConfig{
|
|
Client: client,
|
|
BaseContext: ctx,
|
|
Proxier: apiProxy,
|
|
Logger: cacheLogger.Named("leasecache"),
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
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)
|
|
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)
|
|
}
|
|
}
|
|
}()
|
|
|
|
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,
|
|
})
|
|
|
|
inmemSinkConfig := &sink.SinkConfig{
|
|
Logger: logger.Named("sink.inmem"),
|
|
}
|
|
|
|
inmemSink, err := inmem.New(inmemSinkConfig, leaseCache)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
inmemSinkConfig.Sink = inmemSink
|
|
|
|
go func() {
|
|
errCh <- ss.Run(ctx, ah.OutputCh, []*sink.SinkConfig{config, inmemSinkConfig})
|
|
}()
|
|
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()
|
|
|
|
// 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), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
} else {
|
|
logger.Trace("wrote test role 1", "path", role)
|
|
}
|
|
|
|
if err := ioutil.WriteFile(secret, []byte(secretID1), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
} else {
|
|
logger.Trace("wrote test secret 1", "path", secret)
|
|
}
|
|
|
|
getToken := func() string {
|
|
timeout := time.Now().Add(10 * 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")
|
|
}
|
|
|
|
_, err = os.Stat(secret)
|
|
if err == nil {
|
|
t.Fatal("secret file exists but was supposed to be removed")
|
|
}
|
|
|
|
return string(val)
|
|
}
|
|
time.Sleep(250 * time.Millisecond)
|
|
}
|
|
}
|
|
|
|
t.Logf("auto-auth token: %q", getToken())
|
|
|
|
listener, err := net.Listen("tcp", "127.0.0.1:0")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
defer listener.Close()
|
|
|
|
// Create a muxer and add paths relevant for the lease cache layer
|
|
mux := http.NewServeMux()
|
|
mux.Handle(consts.AgentPathCacheClear, leaseCache.HandleCacheClear(ctx))
|
|
|
|
// Passing a non-nil inmemsink tells the agent to use the auto-auth token
|
|
mux.Handle("/", cache.ProxyHandler(ctx, cacheLogger, leaseCache, inmemSink, true))
|
|
server := &http.Server{
|
|
Handler: mux,
|
|
ReadHeaderTimeout: 10 * time.Second,
|
|
ReadTimeout: 30 * time.Second,
|
|
IdleTimeout: 5 * time.Minute,
|
|
ErrorLog: cacheLogger.StandardLogger(nil),
|
|
}
|
|
go server.Serve(listener)
|
|
|
|
testClient, err := api.NewClient(api.DefaultConfig())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
if err := testClient.SetAddress("http://" + listener.Addr().String()); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// Wait for listeners to come up
|
|
time.Sleep(2 * time.Second)
|
|
|
|
// This block tests that no token on the client is detected by the agent
|
|
// and the auto-auth token is used
|
|
{
|
|
// Empty the token in the client to ensure that auto-auth token is used
|
|
testClient.SetToken("")
|
|
|
|
resp, err = testClient.Logical().Read("auth/token/lookup-self")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if resp == nil {
|
|
t.Fatalf("failed to use the auto-auth token to perform lookup-self")
|
|
}
|
|
}
|
|
|
|
// This block tests lease creation caching using the auto-auth token.
|
|
{
|
|
resp, err = testClient.Logical().Read("kv/foo")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
origReqID := resp.RequestID
|
|
|
|
resp, err = testClient.Logical().Read("kv/foo")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// Sleep for a bit to allow renewer logic to kick in
|
|
time.Sleep(20 * time.Millisecond)
|
|
|
|
cacheReqID := resp.RequestID
|
|
|
|
if origReqID != cacheReqID {
|
|
t.Fatalf("request ID mismatch, expected second request to be a cached response: %s != %s", origReqID, cacheReqID)
|
|
}
|
|
}
|
|
|
|
// This block tests auth token creation caching (child, non-orphan tokens)
|
|
// using the auto-auth token.
|
|
{
|
|
resp, err = testClient.Logical().Write("auth/token/create", nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
origReqID := resp.RequestID
|
|
|
|
// Sleep for a bit to allow renewer logic to kick in
|
|
time.Sleep(20 * time.Millisecond)
|
|
|
|
resp, err = testClient.Logical().Write("auth/token/create", nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
cacheReqID := resp.RequestID
|
|
|
|
if origReqID != cacheReqID {
|
|
t.Fatalf("request ID mismatch, expected second request to be a cached response: %s != %s", origReqID, cacheReqID)
|
|
}
|
|
}
|
|
|
|
// This blocks tests that despite being allowed to use auto-auth token, the
|
|
// token on the request will be prioritized.
|
|
{
|
|
// Empty the token in the client to ensure that auto-auth token is used
|
|
testClient.SetToken(client.Token())
|
|
|
|
resp, err = testClient.Logical().Read("auth/token/lookup-self")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if resp == nil || resp.Data["id"] != client.Token() {
|
|
t.Fatalf("failed to use the cluster client token to perform lookup-self")
|
|
}
|
|
}
|
|
}
|