| // Copyright (c) HashiCorp, Inc. | |
| // SPDX-License-Identifier: MPL-2.0 | |
| package command | |
| import ( | |
| "crypto/tls" | |
| "crypto/x509" | |
| "encoding/json" | |
| "fmt" | |
| "io" | |
| "net" | |
| "net/http" | |
| "os" | |
| "path/filepath" | |
| "reflect" | |
| "strings" | |
| "sync" | |
| "testing" | |
| "time" | |
| "github.com/hashicorp/go-hclog" | |
| vaultjwt "github.com/hashicorp/vault-plugin-auth-jwt" | |
| logicalKv "github.com/hashicorp/vault-plugin-secrets-kv" | |
| "github.com/hashicorp/vault/api" | |
| credAppRole "github.com/hashicorp/vault/builtin/credential/approle" | |
| "github.com/hashicorp/vault/command/agent" | |
| agentConfig "github.com/hashicorp/vault/command/agent/config" | |
| "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/helper/pointerutil" | |
| "github.com/hashicorp/vault/sdk/logical" | |
| "github.com/hashicorp/vault/vault" | |
| "github.com/mitchellh/cli" | |
| "github.com/stretchr/testify/assert" | |
| "github.com/stretchr/testify/require" | |
| ) | |
| const ( | |
| BasicHclConfig = ` | |
| log_file = "TMPDIR/juan.log" | |
| log_level="warn" | |
| log_rotate_max_files=2 | |
| log_rotate_bytes=1048576 | |
| vault { | |
| address = "http://127.0.0.1:8200" | |
| retry { | |
| num_retries = 5 | |
| } | |
| } | |
| listener "tcp" { | |
| address = "127.0.0.1:8100" | |
| tls_disable = false | |
| tls_cert_file = "TMPDIR/reload_cert.pem" | |
| tls_key_file = "TMPDIR/reload_key.pem" | |
| }` | |
| BasicHclConfig2 = ` | |
| log_file = "TMPDIR/juan.log" | |
| log_level="debug" | |
| log_rotate_max_files=-1 | |
| log_rotate_bytes=1048576 | |
| vault { | |
| address = "http://127.0.0.1:8200" | |
| retry { | |
| num_retries = 5 | |
| } | |
| } | |
| listener "tcp" { | |
| address = "127.0.0.1:8100" | |
| tls_disable = false | |
| tls_cert_file = "TMPDIR/reload_cert.pem" | |
| tls_key_file = "TMPDIR/reload_key.pem" | |
| }` | |
| ) | |
| func testAgentCommand(tb testing.TB, logger hclog.Logger) (*cli.MockUi, *AgentCommand) { | |
| tb.Helper() | |
| ui := cli.NewMockUi() | |
| return ui, &AgentCommand{ | |
| BaseCommand: &BaseCommand{ | |
| UI: ui, | |
| }, | |
| ShutdownCh: MakeShutdownCh(), | |
| SighupCh: MakeSighupCh(), | |
| logger: logger, | |
| startedCh: make(chan struct{}, 5), | |
| reloadedCh: make(chan struct{}, 5), | |
| } | |
| } | |
| func TestAgent_ExitAfterAuth(t *testing.T) { | |
| t.Run("via_config", func(t *testing.T) { | |
| testAgentExitAfterAuth(t, false) | |
| }) | |
| t.Run("via_flag", func(t *testing.T) { | |
| testAgentExitAfterAuth(t, true) | |
| }) | |
| } | |
| func testAgentExitAfterAuth(t *testing.T, viaFlag bool) { | |
| logger := logging.NewVaultLogger(hclog.Trace) | |
| coreConfig := &vault.CoreConfig{ | |
| Logger: logger, | |
| CredentialBackends: map[string]logical.Factory{ | |
| "jwt": vaultjwt.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 | |
| err := client.Sys().EnableAuthWithOptions("jwt", &api.EnableAuthOptions{ | |
| Type: "jwt", | |
| }) | |
| if err != nil { | |
| t.Fatal(err) | |
| } | |
| _, err = client.Logical().Write("auth/jwt/config", map[string]interface{}{ | |
| "bound_issuer": "https://team-vault.auth0.com/", | |
| "jwt_validation_pubkeys": agent.TestECDSAPubKey, | |
| "jwt_supported_algs": "ES256", | |
| }) | |
| if err != nil { | |
| t.Fatal(err) | |
| } | |
| _, err = client.Logical().Write("auth/jwt/role/test", map[string]interface{}{ | |
| "role_type": "jwt", | |
| "bound_subject": "r3qXcK2bix9eFECzsU3Sbmh0K16fatW6@clients", | |
| "bound_audiences": "https://vault.plugin.auth.jwt.test", | |
| "user_claim": "https://vault/user", | |
| "groups_claim": "https://vault/groups", | |
| "policies": "test", | |
| "period": "3s", | |
| }) | |
| if err != nil { | |
| t.Fatal(err) | |
| } | |
| inf, err := os.CreateTemp("", "auth.jwt.test.") | |
| if err != nil { | |
| t.Fatal(err) | |
| } | |
| in := inf.Name() | |
| inf.Close() | |
| os.Remove(in) | |
| t.Logf("input: %s", in) | |
| sink1f, err := os.CreateTemp("", "sink1.jwt.test.") | |
| if err != nil { | |
| t.Fatal(err) | |
| } | |
| sink1 := sink1f.Name() | |
| sink1f.Close() | |
| os.Remove(sink1) | |
| t.Logf("sink1: %s", sink1) | |
| sink2f, err := os.CreateTemp("", "sink2.jwt.test.") | |
| if err != nil { | |
| t.Fatal(err) | |
| } | |
| sink2 := sink2f.Name() | |
| sink2f.Close() | |
| os.Remove(sink2) | |
| t.Logf("sink2: %s", sink2) | |
| conff, err := os.CreateTemp("", "conf.jwt.test.") | |
| if err != nil { | |
| t.Fatal(err) | |
| } | |
| conf := conff.Name() | |
| conff.Close() | |
| os.Remove(conf) | |
| t.Logf("config: %s", conf) | |
| jwtToken, _ := agent.GetTestJWT(t) | |
| if err := os.WriteFile(in, []byte(jwtToken), 0o600); err != nil { | |
| t.Fatal(err) | |
| } else { | |
| logger.Trace("wrote test jwt", "path", in) | |
| } | |
| exitAfterAuthTemplText := "exit_after_auth = true" | |
| if viaFlag { | |
| exitAfterAuthTemplText = "" | |
| } | |
| config := ` | |
| %s | |
| auto_auth { | |
| method { | |
| type = "jwt" | |
| config = { | |
| role = "test" | |
| path = "%s" | |
| } | |
| } | |
| sink { | |
| type = "file" | |
| config = { | |
| path = "%s" | |
| } | |
| } | |
| sink "file" { | |
| config = { | |
| path = "%s" | |
| } | |
| } | |
| } | |
| ` | |
| config = fmt.Sprintf(config, exitAfterAuthTemplText, in, sink1, sink2) | |
| if err := os.WriteFile(conf, []byte(config), 0o600); err != nil { | |
| t.Fatal(err) | |
| } else { | |
| logger.Trace("wrote test config", "path", conf) | |
| } | |
| doneCh := make(chan struct{}) | |
| go func() { | |
| ui, cmd := testAgentCommand(t, logger) | |
| cmd.client = client | |
| args := []string{"-config", conf} | |
| if viaFlag { | |
| args = append(args, "-exit-after-auth") | |
| } | |
| code := cmd.Run(args) | |
| if code != 0 { | |
| t.Errorf("expected %d to be %d", code, 0) | |
| t.Logf("output from agent:\n%s", ui.OutputWriter.String()) | |
| t.Logf("error from agent:\n%s", ui.ErrorWriter.String()) | |
| } | |
| close(doneCh) | |
| }() | |
| select { | |
| case <-doneCh: | |
| break | |
| case <-time.After(1 * time.Minute): | |
| t.Fatal("timeout reached while waiting for agent to exit") | |
| } | |
| sink1Bytes, err := os.ReadFile(sink1) | |
| if err != nil { | |
| t.Fatal(err) | |
| } | |
| if len(sink1Bytes) == 0 { | |
| t.Fatal("got no output from sink 1") | |
| } | |
| sink2Bytes, err := os.ReadFile(sink2) | |
| if err != nil { | |
| t.Fatal(err) | |
| } | |
| if len(sink2Bytes) == 0 { | |
| t.Fatal("got no output from sink 2") | |
| } | |
| if string(sink1Bytes) != string(sink2Bytes) { | |
| t.Fatal("sink 1/2 values don't match") | |
| } | |
| } | |
| func TestAgent_RequireRequestHeader(t *testing.T) { | |
| // newApiClient creates an *api.Client. | |
| newApiClient := func(addr string, includeVaultRequestHeader bool) *api.Client { | |
| conf := api.DefaultConfig() | |
| conf.Address = addr | |
| cli, err := api.NewClient(conf) | |
| if err != nil { | |
| t.Fatalf("err: %s", err) | |
| } | |
| h := cli.Headers() | |
| val, ok := h[consts.RequestHeaderName] | |
| if !ok || !reflect.DeepEqual(val, []string{"true"}) { | |
| t.Fatalf("invalid %s header", consts.RequestHeaderName) | |
| } | |
| if !includeVaultRequestHeader { | |
| delete(h, consts.RequestHeaderName) | |
| cli.SetHeaders(h) | |
| } | |
| return cli | |
| } | |
| //---------------------------------------------------- | |
| // Start the server and agent | |
| //---------------------------------------------------- | |
| // Start a vault server | |
| logger := logging.NewVaultLogger(hclog.Trace) | |
| cluster := vault.NewTestCluster(t, | |
| &vault.CoreConfig{ | |
| Logger: logger, | |
| CredentialBackends: map[string]logical.Factory{ | |
| "approle": credAppRole.Factory, | |
| }, | |
| }, | |
| &vault.TestClusterOptions{ | |
| HandlerFunc: vaulthttp.Handler, | |
| }) | |
| cluster.Start() | |
| defer cluster.Cleanup() | |
| vault.TestWaitActive(t, cluster.Cores[0].Core) | |
| serverClient := cluster.Cores[0].Client | |
| // Enable the approle auth method | |
| roleIDPath, secretIDPath := setupAppRole(t, serverClient) | |
| // Create a config file | |
| config := ` | |
| auto_auth { | |
| method "approle" { | |
| mount_path = "auth/approle" | |
| config = { | |
| role_id_file_path = "%s" | |
| secret_id_file_path = "%s" | |
| } | |
| } | |
| } | |
| cache { | |
| use_auto_auth_token = true | |
| } | |
| listener "tcp" { | |
| address = "%s" | |
| tls_disable = true | |
| } | |
| listener "tcp" { | |
| address = "%s" | |
| tls_disable = true | |
| require_request_header = false | |
| } | |
| listener "tcp" { | |
| address = "%s" | |
| tls_disable = true | |
| require_request_header = true | |
| } | |
| ` | |
| listenAddr1 := generateListenerAddress(t) | |
| listenAddr2 := generateListenerAddress(t) | |
| listenAddr3 := generateListenerAddress(t) | |
| config = fmt.Sprintf( | |
| config, | |
| roleIDPath, | |
| secretIDPath, | |
| listenAddr1, | |
| listenAddr2, | |
| listenAddr3, | |
| ) | |
| configPath := makeTempFile(t, "config.hcl", config) | |
| defer os.Remove(configPath) | |
| // Start the agent | |
| ui, cmd := testAgentCommand(t, logger) | |
| cmd.client = serverClient | |
| cmd.startedCh = make(chan struct{}) | |
| var output string | |
| var code int | |
| wg := &sync.WaitGroup{} | |
| wg.Add(1) | |
| go func() { | |
| code = cmd.Run([]string{"-config", configPath}) | |
| if code != 0 { | |
| output = ui.ErrorWriter.String() + ui.OutputWriter.String() | |
| } | |
| wg.Done() | |
| }() | |
| select { | |
| case <-cmd.startedCh: | |
| case <-time.After(5 * time.Second): | |
| t.Errorf("timeout") | |
| } | |
| // defer agent shutdown | |
| defer func() { | |
| cmd.ShutdownCh <- struct{}{} | |
| wg.Wait() | |
| if code != 0 { | |
| t.Fatalf("got a non-zero exit status: %d, stdout/stderr: %s", code, output) | |
| } | |
| }() | |
| //---------------------------------------------------- | |
| // Perform the tests | |
| //---------------------------------------------------- | |
| // Test against a listener configuration that omits | |
| // 'require_request_header', with the header missing from the request. | |
| agentClient := newApiClient("http://"+listenAddr1, false) | |
| req := agentClient.NewRequest("GET", "/v1/sys/health") | |
| request(t, agentClient, req, 200) | |
| // Test against a listener configuration that sets 'require_request_header' | |
| // to 'false', with the header missing from the request. | |
| agentClient = newApiClient("http://"+listenAddr2, false) | |
| req = agentClient.NewRequest("GET", "/v1/sys/health") | |
| request(t, agentClient, req, 200) | |
| // Test against a listener configuration that sets 'require_request_header' | |
| // to 'true', with the header missing from the request. | |
| agentClient = newApiClient("http://"+listenAddr3, false) | |
| req = agentClient.NewRequest("GET", "/v1/sys/health") | |
| resp, err := agentClient.RawRequest(req) | |
| if err == nil { | |
| t.Fatalf("expected error") | |
| } | |
| if resp.StatusCode != http.StatusPreconditionFailed { | |
| t.Fatalf("expected status code %d, not %d", http.StatusPreconditionFailed, resp.StatusCode) | |
| } | |
| // Test against a listener configuration that sets 'require_request_header' | |
| // to 'true', with an invalid header present in the request. | |
| agentClient = newApiClient("http://"+listenAddr3, false) | |
| h := agentClient.Headers() | |
| h[consts.RequestHeaderName] = []string{"bogus"} | |
| agentClient.SetHeaders(h) | |
| req = agentClient.NewRequest("GET", "/v1/sys/health") | |
| resp, err = agentClient.RawRequest(req) | |
| if err == nil { | |
| t.Fatalf("expected error") | |
| } | |
| if resp.StatusCode != http.StatusPreconditionFailed { | |
| t.Fatalf("expected status code %d, not %d", http.StatusPreconditionFailed, resp.StatusCode) | |
| } | |
| // Test against a listener configuration that sets 'require_request_header' | |
| // to 'true', with the proper header present in the request. | |
| agentClient = newApiClient("http://"+listenAddr3, true) | |
| req = agentClient.NewRequest("GET", "/v1/sys/health") | |
| request(t, agentClient, req, 200) | |
| } | |
| // TestAgent_RequireAutoAuthWithForce ensures that the client exits with a | |
| // non-zero code if configured to force the use of an auto-auth token without | |
| // configuring the auto_auth block | |
| func TestAgent_RequireAutoAuthWithForce(t *testing.T) { | |
| logger := logging.NewVaultLogger(hclog.Trace) | |
| // Create a config file | |
| config := fmt.Sprintf(` | |
| cache { | |
| use_auto_auth_token = "force" | |
| } | |
| listener "tcp" { | |
| address = "%s" | |
| tls_disable = true | |
| } | |
| `, generateListenerAddress(t)) | |
| configPath := makeTempFile(t, "config.hcl", config) | |
| defer os.Remove(configPath) | |
| // Start the agent | |
| ui, cmd := testAgentCommand(t, logger) | |
| cmd.startedCh = make(chan struct{}) | |
| code := cmd.Run([]string{"-config", configPath}) | |
| if code == 0 { | |
| t.Errorf("expected error code, but got 0: %d", code) | |
| t.Logf("STDOUT from agent:\n%s", ui.OutputWriter.String()) | |
| t.Logf("STDERR from agent:\n%s", ui.ErrorWriter.String()) | |
| } | |
| } | |
| // TestAgent_Template_UserAgent Validates that the User-Agent sent to Vault | |
| // as part of Templating requests is correct. Uses the custom handler | |
| // userAgentHandler struct defined in this test package, so that Vault validates the | |
| // User-Agent on requests sent by Agent. | |
| func TestAgent_Template_UserAgent(t *testing.T) { | |
| //---------------------------------------------------- | |
| // Start the server and agent | |
| //---------------------------------------------------- | |
| logger := logging.NewVaultLogger(hclog.Trace) | |
| var h userAgentHandler | |
| cluster := vault.NewTestCluster(t, | |
| &vault.CoreConfig{ | |
| Logger: logger, | |
| CredentialBackends: map[string]logical.Factory{ | |
| "approle": credAppRole.Factory, | |
| }, | |
| LogicalBackends: map[string]logical.Factory{ | |
| "kv": logicalKv.Factory, | |
| }, | |
| }, | |
| &vault.TestClusterOptions{ | |
| NumCores: 1, | |
| HandlerFunc: vaulthttp.HandlerFunc( | |
| func(properties *vault.HandlerProperties) http.Handler { | |
| h.props = properties | |
| h.userAgentToCheckFor = useragent.AgentTemplatingString() | |
| h.pathToCheck = "/v1/secret/data" | |
| h.requestMethodToCheck = "GET" | |
| h.t = t | |
| return &h | |
| }), | |
| }) | |
| cluster.Start() | |
| defer cluster.Cleanup() | |
| vault.TestWaitActive(t, cluster.Cores[0].Core) | |
| serverClient := cluster.Cores[0].Client | |
| // Unset the environment variable so that agent picks up the right test | |
| // cluster address | |
| defer os.Setenv(api.EnvVaultAddress, os.Getenv(api.EnvVaultAddress)) | |
| os.Setenv(api.EnvVaultAddress, serverClient.Address()) | |
| roleIDPath, secretIDPath := setupAppRoleAndKVMounts(t, serverClient) | |
| // make a temp directory to hold renders. Each test will create a temp dir | |
| // inside this one | |
| tmpDirRoot, err := os.MkdirTemp("", "agent-test-renders") | |
| if err != nil { | |
| t.Fatal(err) | |
| } | |
| defer os.RemoveAll(tmpDirRoot) | |
| // create temp dir for this test run | |
| tmpDir, err := os.MkdirTemp(tmpDirRoot, "TestAgent_Template_UserAgent") | |
| if err != nil { | |
| t.Fatal(err) | |
| } | |
| // make some template files | |
| var templatePaths []string | |
| fileName := filepath.Join(tmpDir, "render_0.tmpl") | |
| if err := os.WriteFile(fileName, []byte(templateContents(0)), 0o600); err != nil { | |
| t.Fatal(err) | |
| } | |
| templatePaths = append(templatePaths, fileName) | |
| // build up the template config to be added to the Agent config.hcl file | |
| var templateConfigStrings []string | |
| for i, t := range templatePaths { | |
| index := fmt.Sprintf("render_%d.json", i) | |
| s := fmt.Sprintf(templateConfigString, t, tmpDir, index) | |
| templateConfigStrings = append(templateConfigStrings, s) | |
| } | |
| // Create a config file | |
| config := ` | |
| vault { | |
| address = "%s" | |
| tls_skip_verify = true | |
| } | |
| auto_auth { | |
| method "approle" { | |
| mount_path = "auth/approle" | |
| config = { | |
| role_id_file_path = "%s" | |
| secret_id_file_path = "%s" | |
| remove_secret_id_file_after_reading = false | |
| } | |
| } | |
| } | |
| %s | |
| ` | |
| // flatten the template configs | |
| templateConfig := strings.Join(templateConfigStrings, " ") | |
| config = fmt.Sprintf(config, serverClient.Address(), roleIDPath, secretIDPath, templateConfig) | |
| configPath := makeTempFile(t, "config.hcl", config) | |
| defer os.Remove(configPath) | |
| // Start the agent | |
| ui, cmd := testAgentCommand(t, logger) | |
| cmd.client = serverClient | |
| cmd.startedCh = make(chan struct{}) | |
| wg := &sync.WaitGroup{} | |
| wg.Add(1) | |
| go func() { | |
| code := cmd.Run([]string{"-config", configPath}) | |
| if code != 0 { | |
| t.Errorf("non-zero return code when running agent: %d", code) | |
| t.Logf("STDOUT from agent:\n%s", ui.OutputWriter.String()) | |
| t.Logf("STDERR from agent:\n%s", ui.ErrorWriter.String()) | |
| } | |
| wg.Done() | |
| }() | |
| select { | |
| case <-cmd.startedCh: | |
| case <-time.After(5 * time.Second): | |
| t.Errorf("timeout") | |
| } | |
| // We need to shut down the Agent command | |
| defer func() { | |
| cmd.ShutdownCh <- struct{}{} | |
| wg.Wait() | |
| }() | |
| verify := func(suffix string) { | |
| t.Helper() | |
| // We need to poll for a bit to give Agent time to render the | |
| // templates. Without this, the test will attempt to read | |
| // the temp dir before Agent has had time to render and will | |
| // likely fail the test | |
| tick := time.Tick(1 * time.Second) | |
| timeout := time.After(10 * time.Second) | |
| var err error | |
| for { | |
| select { | |
| case <-timeout: | |
| t.Fatalf("timed out waiting for templates to render, last error: %v", err) | |
| case <-tick: | |
| } | |
| // Check for files rendered in the directory and break | |
| // early for shutdown if we do have all the files | |
| // rendered | |
| //---------------------------------------------------- | |
| // Perform the tests | |
| //---------------------------------------------------- | |
| if numFiles := testListFiles(t, tmpDir, ".json"); numFiles != len(templatePaths) { | |
| err = fmt.Errorf("expected (%d) templates, got (%d)", len(templatePaths), numFiles) | |
| continue | |
| } | |
| for i := range templatePaths { | |
| fileName := filepath.Join(tmpDir, fmt.Sprintf("render_%d.json", i)) | |
| var c []byte | |
| c, err = os.ReadFile(fileName) | |
| if err != nil { | |
| continue | |
| } | |
| if string(c) != templateRendered(i)+suffix { | |
| err = fmt.Errorf("expected=%q, got=%q", templateRendered(i)+suffix, string(c)) | |
| continue | |
| } | |
| } | |
| return | |
| } | |
| } | |
| verify("") | |
| fileName = filepath.Join(tmpDir, "render_0.tmpl") | |
| if err := os.WriteFile(fileName, []byte(templateContents(0)+"{}"), 0o600); err != nil { | |
| t.Fatal(err) | |
| } | |
| verify("{}") | |
| } | |
| // TestAgent_Template tests rendering templates | |
| func TestAgent_Template_Basic(t *testing.T) { | |
| //---------------------------------------------------- | |
| // Start the server and agent | |
| //---------------------------------------------------- | |
| logger := logging.NewVaultLogger(hclog.Trace) | |
| cluster := vault.NewTestCluster(t, | |
| &vault.CoreConfig{ | |
| Logger: logger, | |
| CredentialBackends: map[string]logical.Factory{ | |
| "approle": credAppRole.Factory, | |
| }, | |
| LogicalBackends: map[string]logical.Factory{ | |
| "kv": logicalKv.Factory, | |
| }, | |
| }, | |
| &vault.TestClusterOptions{ | |
| HandlerFunc: vaulthttp.Handler, | |
| }) | |
| cluster.Start() | |
| defer cluster.Cleanup() | |
| vault.TestWaitActive(t, cluster.Cores[0].Core) | |
| serverClient := cluster.Cores[0].Client | |
| // Unset the environment variable so that agent picks up the right test | |
| // cluster address | |
| defer os.Setenv(api.EnvVaultAddress, os.Getenv(api.EnvVaultAddress)) | |
| os.Setenv(api.EnvVaultAddress, serverClient.Address()) | |
| roleIDPath, secretIDPath := setupAppRoleAndKVMounts(t, serverClient) | |
| // make a temp directory to hold renders. Each test will create a temp dir | |
| // inside this one | |
| tmpDirRoot, err := os.MkdirTemp("", "agent-test-renders") | |
| if err != nil { | |
| t.Fatal(err) | |
| } | |
| defer os.RemoveAll(tmpDirRoot) | |
| // start test cases here | |
| testCases := map[string]struct { | |
| templateCount int | |
| exitAfterAuth bool | |
| }{ | |
| "one": { | |
| templateCount: 1, | |
| }, | |
| "one_with_exit": { | |
| templateCount: 1, | |
| exitAfterAuth: true, | |
| }, | |
| "many": { | |
| templateCount: 15, | |
| }, | |
| "many_with_exit": { | |
| templateCount: 13, | |
| exitAfterAuth: true, | |
| }, | |
| } | |
| for tcname, tc := range testCases { | |
| t.Run(tcname, func(t *testing.T) { | |
| // create temp dir for this test run | |
| tmpDir, err := os.MkdirTemp(tmpDirRoot, tcname) | |
| if err != nil { | |
| t.Fatal(err) | |
| } | |
| // make some template files | |
| var templatePaths []string | |
| for i := 0; i < tc.templateCount; i++ { | |
| fileName := filepath.Join(tmpDir, fmt.Sprintf("render_%d.tmpl", i)) | |
| if err := os.WriteFile(fileName, []byte(templateContents(i)), 0o600); err != nil { | |
| t.Fatal(err) | |
| } | |
| templatePaths = append(templatePaths, fileName) | |
| } | |
| // build up the template config to be added to the Agent config.hcl file | |
| var templateConfigStrings []string | |
| for i, t := range templatePaths { | |
| index := fmt.Sprintf("render_%d.json", i) | |
| s := fmt.Sprintf(templateConfigString, t, tmpDir, index) | |
| templateConfigStrings = append(templateConfigStrings, s) | |
| } | |
| // Create a config file | |
| config := ` | |
| vault { | |
| address = "%s" | |
| tls_skip_verify = true | |
| } | |
| auto_auth { | |
| method "approle" { | |
| mount_path = "auth/approle" | |
| config = { | |
| role_id_file_path = "%s" | |
| secret_id_file_path = "%s" | |
| remove_secret_id_file_after_reading = false | |
| } | |
| } | |
| } | |
| %s | |
| %s | |
| ` | |
| // conditionally set the exit_after_auth flag | |
| exitAfterAuth := "" | |
| if tc.exitAfterAuth { | |
| exitAfterAuth = "exit_after_auth = true" | |
| } | |
| // flatten the template configs | |
| templateConfig := strings.Join(templateConfigStrings, " ") | |
| config = fmt.Sprintf(config, serverClient.Address(), roleIDPath, secretIDPath, templateConfig, exitAfterAuth) | |
| configPath := makeTempFile(t, "config.hcl", config) | |
| defer os.Remove(configPath) | |
| // Start the agent | |
| ui, cmd := testAgentCommand(t, logger) | |
| cmd.client = serverClient | |
| cmd.startedCh = make(chan struct{}) | |
| wg := &sync.WaitGroup{} | |
| wg.Add(1) | |
| go func() { | |
| code := cmd.Run([]string{"-config", configPath}) | |
| if code != 0 { | |
| t.Errorf("non-zero return code when running agent: %d", code) | |
| t.Logf("STDOUT from agent:\n%s", ui.OutputWriter.String()) | |
| t.Logf("STDERR from agent:\n%s", ui.ErrorWriter.String()) | |
| } | |
| wg.Done() | |
| }() | |
| select { | |
| case <-cmd.startedCh: | |
| case <-time.After(5 * time.Second): | |
| t.Errorf("timeout") | |
| } | |
| // if using exit_after_auth, then the command will have returned at the | |
| // end and no longer be running. If we are not using exit_after_auth, then | |
| // we need to shut down the command | |
| if !tc.exitAfterAuth { | |
| defer func() { | |
| cmd.ShutdownCh <- struct{}{} | |
| wg.Wait() | |
| }() | |
| } | |
| verify := func(suffix string) { | |
| t.Helper() | |
| // We need to poll for a bit to give Agent time to render the | |
| // templates. Without this this, the test will attempt to read | |
| // the temp dir before Agent has had time to render and will | |
| // likely fail the test | |
| tick := time.Tick(1 * time.Second) | |
| timeout := time.After(10 * time.Second) | |
| var err error | |
| for { | |
| select { | |
| case <-timeout: | |
| t.Fatalf("timed out waiting for templates to render, last error: %v", err) | |
| case <-tick: | |
| } | |
| // Check for files rendered in the directory and break | |
| // early for shutdown if we do have all the files | |
| // rendered | |
| //---------------------------------------------------- | |
| // Perform the tests | |
| //---------------------------------------------------- | |
| if numFiles := testListFiles(t, tmpDir, ".json"); numFiles != len(templatePaths) { | |
| err = fmt.Errorf("expected (%d) templates, got (%d)", len(templatePaths), numFiles) | |
| continue | |
| } | |
| for i := range templatePaths { | |
| fileName := filepath.Join(tmpDir, fmt.Sprintf("render_%d.json", i)) | |
| var c []byte | |
| c, err = os.ReadFile(fileName) | |
| if err != nil { | |
| continue | |
| } | |
| if string(c) != templateRendered(i)+suffix { | |
| err = fmt.Errorf("expected=%q, got=%q", templateRendered(i)+suffix, string(c)) | |
| continue | |
| } | |
| } | |
| return | |
| } | |
| } | |
| verify("") | |
| for i := 0; i < tc.templateCount; i++ { | |
| fileName := filepath.Join(tmpDir, fmt.Sprintf("render_%d.tmpl", i)) | |
| if err := os.WriteFile(fileName, []byte(templateContents(i)+"{}"), 0o600); err != nil { | |
| t.Fatal(err) | |
| } | |
| } | |
| verify("{}") | |
| }) | |
| } | |
| } | |
| func setupAppRole(t *testing.T, serverClient *api.Client) (string, string) { | |
| t.Helper() | |
| // Enable the approle auth method | |
| req := serverClient.NewRequest("POST", "/v1/sys/auth/approle") | |
| req.BodyBytes = []byte(`{ | |
| "type": "approle" | |
| }`) | |
| request(t, serverClient, req, 204) | |
| // Create a named role | |
| req = serverClient.NewRequest("PUT", "/v1/auth/approle/role/test-role") | |
| req.BodyBytes = []byte(`{ | |
| "token_ttl": "5m", | |
| "token_policies":"default,myapp-read", | |
| "policies":"default,myapp-read" | |
| }`) | |
| request(t, serverClient, req, 204) | |
| // Fetch the RoleID of the named role | |
| req = serverClient.NewRequest("GET", "/v1/auth/approle/role/test-role/role-id") | |
| body := request(t, serverClient, req, 200) | |
| data := body["data"].(map[string]interface{}) | |
| roleID := data["role_id"].(string) | |
| // Get a SecretID issued against the named role | |
| req = serverClient.NewRequest("PUT", "/v1/auth/approle/role/test-role/secret-id") | |
| body = request(t, serverClient, req, 200) | |
| data = body["data"].(map[string]interface{}) | |
| secretID := data["secret_id"].(string) | |
| // Write the RoleID and SecretID to temp files | |
| roleIDPath := makeTempFile(t, "role_id.txt", roleID+"\n") | |
| secretIDPath := makeTempFile(t, "secret_id.txt", secretID+"\n") | |
| t.Cleanup(func() { | |
| os.Remove(roleIDPath) | |
| os.Remove(secretIDPath) | |
| }) | |
| return roleIDPath, secretIDPath | |
| } | |
| func setupAppRoleAndKVMounts(t *testing.T, serverClient *api.Client) (string, string) { | |
| roleIDPath, secretIDPath := setupAppRole(t, serverClient) | |
| // give test-role permissions to read the kv secret | |
| req := serverClient.NewRequest("PUT", "/v1/sys/policy/myapp-read") | |
| req.BodyBytes = []byte(`{ | |
| "policy": "path \"secret/*\" { capabilities = [\"read\", \"list\"] }" | |
| }`) | |
| request(t, serverClient, req, 204) | |
| // setup the kv secrets | |
| req = serverClient.NewRequest("POST", "/v1/sys/mounts/secret/tune") | |
| req.BodyBytes = []byte(`{ | |
| "options": {"version": "2"} | |
| }`) | |
| request(t, serverClient, req, 200) | |
| // Secret: myapp | |
| req = serverClient.NewRequest("POST", "/v1/secret/data/myapp") | |
| req.BodyBytes = []byte(`{ | |
| "data": { | |
| "username": "bar", | |
| "password": "zap" | |
| } | |
| }`) | |
| request(t, serverClient, req, 200) | |
| // Secret: myapp2 | |
| req = serverClient.NewRequest("POST", "/v1/secret/data/myapp2") | |
| req.BodyBytes = []byte(`{ | |
| "data": { | |
| "username": "barstuff", | |
| "password": "zap" | |
| } | |
| }`) | |
| request(t, serverClient, req, 200) | |
| // Secret: otherapp | |
| req = serverClient.NewRequest("POST", "/v1/secret/data/otherapp") | |
| req.BodyBytes = []byte(`{ | |
| "data": { | |
| "username": "barstuff", | |
| "password": "zap", | |
| "cert": "something" | |
| } | |
| }`) | |
| request(t, serverClient, req, 200) | |
| return roleIDPath, secretIDPath | |
| } | |
| // TestAgent_Template_VaultClientFromEnv tests that Vault Agent can read in its | |
| // required `vault` client details from environment variables instead of config. | |
| func TestAgent_Template_VaultClientFromEnv(t *testing.T) { | |
| //---------------------------------------------------- | |
| // Start the server and agent | |
| //---------------------------------------------------- | |
| logger := logging.NewVaultLogger(hclog.Trace) | |
| cluster := vault.NewTestCluster(t, | |
| &vault.CoreConfig{ | |
| CredentialBackends: map[string]logical.Factory{ | |
| "approle": credAppRole.Factory, | |
| }, | |
| LogicalBackends: map[string]logical.Factory{ | |
| "kv": logicalKv.Factory, | |
| }, | |
| }, | |
| &vault.TestClusterOptions{ | |
| HandlerFunc: vaulthttp.Handler, | |
| }) | |
| cluster.Start() | |
| defer cluster.Cleanup() | |
| vault.TestWaitActive(t, cluster.Cores[0].Core) | |
| serverClient := cluster.Cores[0].Client | |
| roleIDPath, secretIDPath := setupAppRoleAndKVMounts(t, serverClient) | |
| // make a temp directory to hold renders. Each test will create a temp dir | |
| // inside this one | |
| tmpDirRoot, err := os.MkdirTemp("", "agent-test-renders") | |
| if err != nil { | |
| t.Fatal(err) | |
| } | |
| defer os.RemoveAll(tmpDirRoot) | |
| vaultAddr := "https://" + cluster.Cores[0].Listeners[0].Address.String() | |
| testCases := map[string]struct { | |
| env map[string]string | |
| }{ | |
| "VAULT_ADDR and VAULT_CACERT": { | |
| env: map[string]string{ | |
| api.EnvVaultAddress: vaultAddr, | |
| api.EnvVaultCACert: cluster.CACertPEMFile, | |
| }, | |
| }, | |
| "VAULT_ADDR and VAULT_CACERT_BYTES": { | |
| env: map[string]string{ | |
| api.EnvVaultAddress: vaultAddr, | |
| api.EnvVaultCACertBytes: string(cluster.CACertPEM), | |
| }, | |
| }, | |
| } | |
| for tcname, tc := range testCases { | |
| t.Run(tcname, func(t *testing.T) { | |
| for k, v := range tc.env { | |
| t.Setenv(k, v) | |
| } | |
| tmpDir := t.TempDir() | |
| // Make a template. | |
| templateFile := filepath.Join(tmpDir, "render.tmpl") | |
| if err := os.WriteFile(templateFile, []byte(templateContents(0)), 0o600); err != nil { | |
| t.Fatal(err) | |
| } | |
| // build up the template config to be added to the Agent config.hcl file | |
| targetFile := filepath.Join(tmpDir, "render.json") | |
| templateConfig := fmt.Sprintf(` | |
| template { | |
| source = "%s" | |
| destination = "%s" | |
| } | |
| `, templateFile, targetFile) | |
| // Create a config file | |
| config := ` | |
| auto_auth { | |
| method "approle" { | |
| mount_path = "auth/approle" | |
| config = { | |
| role_id_file_path = "%s" | |
| secret_id_file_path = "%s" | |
| remove_secret_id_file_after_reading = false | |
| } | |
| } | |
| } | |
| %s | |
| ` | |
| config = fmt.Sprintf(config, roleIDPath, secretIDPath, templateConfig) | |
| configPath := makeTempFile(t, "config.hcl", config) | |
| defer os.Remove(configPath) | |
| // Start the agent | |
| ui, cmd := testAgentCommand(t, logger) | |
| cmd.client = serverClient | |
| cmd.startedCh = make(chan struct{}) | |
| wg := &sync.WaitGroup{} | |
| wg.Add(1) | |
| go func() { | |
| code := cmd.Run([]string{"-config", configPath}) | |
| if code != 0 { | |
| t.Errorf("non-zero return code when running agent: %d", code) | |
| t.Logf("STDOUT from agent:\n%s", ui.OutputWriter.String()) | |
| t.Logf("STDERR from agent:\n%s", ui.ErrorWriter.String()) | |
| } | |
| wg.Done() | |
| }() | |
| select { | |
| case <-cmd.startedCh: | |
| case <-time.After(5 * time.Second): | |
| t.Errorf("timeout") | |
| } | |
| defer func() { | |
| cmd.ShutdownCh <- struct{}{} | |
| wg.Wait() | |
| }() | |
| // We need to poll for a bit to give Agent time to render the | |
| // templates. Without this this, the test will attempt to read | |
| // the temp dir before Agent has had time to render and will | |
| // likely fail the test | |
| tick := time.Tick(1 * time.Second) | |
| timeout := time.After(10 * time.Second) | |
| for { | |
| select { | |
| case <-timeout: | |
| t.Fatalf("timed out waiting for templates to render, last error: %v", err) | |
| case <-tick: | |
| } | |
| contents, err := os.ReadFile(targetFile) | |
| if err != nil { | |
| // If the file simply doesn't exist, continue waiting for | |
| // the template rendering to complete. | |
| if os.IsNotExist(err) { | |
| continue | |
| } | |
| t.Fatal(err) | |
| } | |
| if string(contents) != templateRendered(0) { | |
| t.Fatalf("expected=%q, got=%q", templateRendered(0), string(contents)) | |
| } | |
| // Success! Break out of the retry loop. | |
| break | |
| } | |
| }) | |
| } | |
| } | |
| func testListFiles(t *testing.T, dir, extension string) int { | |
| t.Helper() | |
| files, err := os.ReadDir(dir) | |
| if err != nil { | |
| t.Fatal(err) | |
| } | |
| var count int | |
| for _, f := range files { | |
| if filepath.Ext(f.Name()) == extension { | |
| count++ | |
| } | |
| } | |
| return count | |
| } | |
| // TestAgent_Template_ExitCounter tests that Vault Agent correctly renders all | |
| // templates before exiting when the configuration uses exit_after_auth. This is | |
| // similar to TestAgent_Template_Basic, but differs by using a consistent number | |
| // of secrets from multiple sources, where as the basic test could possibly | |
| // generate a random number of secrets, but all using the same source. This test | |
| // reproduces https://github.com/hashicorp/vault/issues/7883 | |
| func TestAgent_Template_ExitCounter(t *testing.T) { | |
| //---------------------------------------------------- | |
| // Start the server and agent | |
| //---------------------------------------------------- | |
| logger := logging.NewVaultLogger(hclog.Trace) | |
| cluster := vault.NewTestCluster(t, | |
| &vault.CoreConfig{ | |
| Logger: logger, | |
| CredentialBackends: map[string]logical.Factory{ | |
| "approle": credAppRole.Factory, | |
| }, | |
| LogicalBackends: map[string]logical.Factory{ | |
| "kv": logicalKv.Factory, | |
| }, | |
| }, | |
| &vault.TestClusterOptions{ | |
| HandlerFunc: vaulthttp.Handler, | |
| }) | |
| cluster.Start() | |
| defer cluster.Cleanup() | |
| vault.TestWaitActive(t, cluster.Cores[0].Core) | |
| serverClient := cluster.Cores[0].Client | |
| // Unset the environment variable so that agent picks up the right test | |
| // cluster address | |
| defer os.Setenv(api.EnvVaultAddress, os.Getenv(api.EnvVaultAddress)) | |
| os.Setenv(api.EnvVaultAddress, serverClient.Address()) | |
| roleIDPath, secretIDPath := setupAppRoleAndKVMounts(t, serverClient) | |
| // make a temp directory to hold renders. Each test will create a temp dir | |
| // inside this one | |
| tmpDirRoot, err := os.MkdirTemp("", "agent-test-renders") | |
| if err != nil { | |
| t.Fatal(err) | |
| } | |
| defer os.RemoveAll(tmpDirRoot) | |
| // create temp dir for this test run | |
| tmpDir, err := os.MkdirTemp(tmpDirRoot, "agent-test") | |
| if err != nil { | |
| t.Fatal(err) | |
| } | |
| // Create a config file | |
| config := ` | |
| vault { | |
| address = "%s" | |
| tls_skip_verify = true | |
| } | |
| auto_auth { | |
| method "approle" { | |
| mount_path = "auth/approle" | |
| config = { | |
| role_id_file_path = "%s" | |
| secret_id_file_path = "%s" | |
| remove_secret_id_file_after_reading = false | |
| } | |
| } | |
| } | |
| template { | |
| contents = "{{ with secret \"secret/myapp\" }}{{ range $k, $v := .Data.data }}{{ $v }}{{ end }}{{ end }}" | |
| destination = "%s/render-pass.txt" | |
| } | |
| template { | |
| contents = "{{ with secret \"secret/myapp2\" }}{{ .Data.data.username}}{{ end }}" | |
| destination = "%s/render-user.txt" | |
| } | |
| template { | |
| contents = <<EOF | |
| {{ with secret "secret/otherapp"}} | |
| { | |
| {{ if .Data.data.username}}"username":"{{ .Data.data.username}}",{{ end }} | |
| {{ if .Data.data.password }}"password":"{{ .Data.data.password }}",{{ end }} | |
| {{ .Data.data.cert }} | |
| } | |
| {{ end }} | |
| EOF | |
| destination = "%s/render-other.txt" | |
| } | |
| exit_after_auth = true | |
| ` | |
| config = fmt.Sprintf(config, serverClient.Address(), roleIDPath, secretIDPath, tmpDir, tmpDir, tmpDir) | |
| configPath := makeTempFile(t, "config.hcl", config) | |
| defer os.Remove(configPath) | |
| // Start the agent | |
| ui, cmd := testAgentCommand(t, logger) | |
| cmd.client = serverClient | |
| cmd.startedCh = make(chan struct{}) | |
| wg := &sync.WaitGroup{} | |
| wg.Add(1) | |
| go func() { | |
| code := cmd.Run([]string{"-config", configPath}) | |
| if code != 0 { | |
| t.Errorf("non-zero return code when running agent: %d", code) | |
| t.Logf("STDOUT from agent:\n%s", ui.OutputWriter.String()) | |
| t.Logf("STDERR from agent:\n%s", ui.ErrorWriter.String()) | |
| } | |
| wg.Done() | |
| }() | |
| select { | |
| case <-cmd.startedCh: | |
| case <-time.After(5 * time.Second): | |
| t.Errorf("timeout") | |
| } | |
| wg.Wait() | |
| //---------------------------------------------------- | |
| // Perform the tests | |
| //---------------------------------------------------- | |
| files, err := os.ReadDir(tmpDir) | |
| if err != nil { | |
| t.Fatal(err) | |
| } | |
| if len(files) != 3 { | |
| t.Fatalf("expected (%d) templates, got (%d)", 3, len(files)) | |
| } | |
| } | |
| // a slice of template options | |
| var templates = []string{ | |
| `{{- with secret "secret/otherapp"}}{"secret": "other", | |
| {{- if .Data.data.username}}"username":"{{ .Data.data.username}}",{{- end }} | |
| {{- if .Data.data.password }}"password":"{{ .Data.data.password }}"{{- end }}} | |
| {{- end }}`, | |
| `{{- with secret "secret/myapp"}}{"secret": "myapp", | |
| {{- if .Data.data.username}}"username":"{{ .Data.data.username}}",{{- end }} | |
| {{- if .Data.data.password }}"password":"{{ .Data.data.password }}"{{- end }}} | |
| {{- end }}`, | |
| `{{- with secret "secret/myapp"}}{"secret": "myapp", | |
| {{- if .Data.data.password }}"password":"{{ .Data.data.password }}"{{- end }}} | |
| {{- end }}`, | |
| } | |
| var rendered = []string{ | |
| `{"secret": "other","username":"barstuff","password":"zap"}`, | |
| `{"secret": "myapp","username":"bar","password":"zap"}`, | |
| `{"secret": "myapp","password":"zap"}`, | |
| } | |
| // templateContents returns a template from the above templates slice. Each | |
| // invocation with incrementing seed will return "the next" template, and loop. | |
| // This ensures as we use multiple templates that we have a increasing number of | |
| // sources before we reuse a template. | |
| func templateContents(seed int) string { | |
| index := seed % len(templates) | |
| return templates[index] | |
| } | |
| func templateRendered(seed int) string { | |
| index := seed % len(templates) | |
| return rendered[index] | |
| } | |
| var templateConfigString = ` | |
| template { | |
| source = "%s" | |
| destination = "%s/%s" | |
| } | |
| ` | |
| // request issues HTTP requests. | |
| func request(t *testing.T, client *api.Client, req *api.Request, expectedStatusCode int) map[string]interface{} { | |
| t.Helper() | |
| resp, err := client.RawRequest(req) | |
| if err != nil { | |
| t.Fatalf("err: %s", err) | |
| } | |
| if resp.StatusCode != expectedStatusCode { | |
| t.Fatalf("expected status code %d, not %d", expectedStatusCode, resp.StatusCode) | |
| } | |
| bytes, err := io.ReadAll(resp.Body) | |
| if err != nil { | |
| t.Fatalf("err: %s", err) | |
| } | |
| if len(bytes) == 0 { | |
| return nil | |
| } | |
| var body map[string]interface{} | |
| err = json.Unmarshal(bytes, &body) | |
| if err != nil { | |
| t.Fatalf("err: %s", err) | |
| } | |
| return body | |
| } | |
| // makeTempFile creates a temp file and populates it. | |
| func makeTempFile(t *testing.T, name, contents string) string { | |
| t.Helper() | |
| f, err := os.CreateTemp("", name) | |
| if err != nil { | |
| t.Fatal(err) | |
| } | |
| path := f.Name() | |
| f.WriteString(contents) | |
| f.Close() | |
| return path | |
| } | |
| func populateTempFile(t *testing.T, name, contents string) *os.File { | |
| t.Helper() | |
| file, err := os.CreateTemp(t.TempDir(), name) | |
| if err != nil { | |
| t.Fatal(err) | |
| } | |
| _, err = file.WriteString(contents) | |
| if err != nil { | |
| t.Fatal(err) | |
| } | |
| err = file.Close() | |
| if err != nil { | |
| t.Fatal(err) | |
| } | |
| return file | |
| } | |
| // handler makes 500 errors happen for reads on /v1/secret. | |
| // Definitely not thread-safe, do not use t.Parallel with this. | |
| type handler struct { | |
| props *vault.HandlerProperties | |
| failCount int | |
| t *testing.T | |
| } | |
| func (h *handler) ServeHTTP(resp http.ResponseWriter, req *http.Request) { | |
| if req.Method == "GET" && strings.HasPrefix(req.URL.Path, "/v1/secret") { | |
| if h.failCount > 0 { | |
| h.failCount-- | |
| h.t.Logf("%s failing GET request on %s, failures left: %d", time.Now(), req.URL.Path, h.failCount) | |
| resp.WriteHeader(500) | |
| return | |
| } | |
| h.t.Logf("passing GET request on %s", req.URL.Path) | |
| } | |
| vaulthttp.Handler.Handler(h.props).ServeHTTP(resp, req) | |
| } | |
| // userAgentHandler makes it easy to test the User-Agent header received | |
| // by Vault | |
| type userAgentHandler struct { | |
| props *vault.HandlerProperties | |
| failCount int | |
| userAgentToCheckFor string | |
| pathToCheck string | |
| requestMethodToCheck string | |
| t *testing.T | |
| } | |
| func (h *userAgentHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) { | |
| if req.Method == h.requestMethodToCheck && strings.Contains(req.RequestURI, h.pathToCheck) { | |
| userAgent := req.UserAgent() | |
| if !(userAgent == h.userAgentToCheckFor) { | |
| h.t.Fatalf("User-Agent string not as expected. Expected to find %s, got %s", h.userAgentToCheckFor, userAgent) | |
| } | |
| } | |
| vaulthttp.Handler.Handler(h.props).ServeHTTP(w, req) | |
| } | |
| // TestAgent_Template_Retry verifies that the template server retries requests | |
| // based on retry configuration. | |
| func TestAgent_Template_Retry(t *testing.T) { | |
| //---------------------------------------------------- | |
| // Start the server and agent | |
| //---------------------------------------------------- | |
| logger := logging.NewVaultLogger(hclog.Trace) | |
| var h handler | |
| cluster := vault.NewTestCluster(t, | |
| &vault.CoreConfig{ | |
| Logger: logger, | |
| CredentialBackends: map[string]logical.Factory{ | |
| "approle": credAppRole.Factory, | |
| }, | |
| LogicalBackends: map[string]logical.Factory{ | |
| "kv": logicalKv.Factory, | |
| }, | |
| }, | |
| &vault.TestClusterOptions{ | |
| NumCores: 1, | |
| HandlerFunc: vaulthttp.HandlerFunc( | |
| func(properties *vault.HandlerProperties) http.Handler { | |
| h.props = properties | |
| h.t = t | |
| return &h | |
| }), | |
| }) | |
| cluster.Start() | |
| defer cluster.Cleanup() | |
| vault.TestWaitActive(t, cluster.Cores[0].Core) | |
| serverClient := cluster.Cores[0].Client | |
| // Unset the environment variable so that agent picks up the right test | |
| // cluster address | |
| defer os.Setenv(api.EnvVaultAddress, os.Getenv(api.EnvVaultAddress)) | |
| os.Unsetenv(api.EnvVaultAddress) | |
| methodConf, cleanup := prepAgentApproleKV(t, serverClient) | |
| defer cleanup() | |
| err := serverClient.Sys().TuneMount("secret", api.MountConfigInput{ | |
| Options: map[string]string{ | |
| "version": "2", | |
| }, | |
| }) | |
| if err != nil { | |
| t.Fatal(err) | |
| } | |
| _, err = serverClient.Logical().Write("secret/data/otherapp", map[string]interface{}{ | |
| "data": map[string]interface{}{ | |
| "username": "barstuff", | |
| "password": "zap", | |
| "cert": "something", | |
| }, | |
| }) | |
| if err != nil { | |
| t.Fatal(err) | |
| } | |
| // make a temp directory to hold renders. Each test will create a temp dir | |
| // inside this one | |
| tmpDirRoot, err := os.MkdirTemp("", "agent-test-renders") | |
| if err != nil { | |
| t.Fatal(err) | |
| } | |
| defer os.RemoveAll(tmpDirRoot) | |
| intRef := func(i int) *int { | |
| return &i | |
| } | |
| // start test cases here | |
| testCases := map[string]struct { | |
| retries *int | |
| expectError bool | |
| }{ | |
| "none": { | |
| retries: intRef(-1), | |
| expectError: true, | |
| }, | |
| "one": { | |
| retries: intRef(1), | |
| expectError: true, | |
| }, | |
| "two": { | |
| retries: intRef(2), | |
| expectError: false, | |
| }, | |
| "missing": { | |
| retries: nil, | |
| expectError: false, | |
| }, | |
| "default": { | |
| retries: intRef(0), | |
| expectError: false, | |
| }, | |
| } | |
| for tcname, tc := range testCases { | |
| t.Run(tcname, func(t *testing.T) { | |
| // We fail the first 6 times. The consul-template code creates | |
| // a Vault client with MaxRetries=2, so for every consul-template | |
| // retry configured, it will in practice make up to 3 requests. | |
| // Thus if consul-template is configured with "one" retry, it will | |
| // fail given our failCount, but if configured with "two" retries, | |
| // they will consume our 6th failure, and on the "third (from its | |
| // perspective) attempt, it will succeed. | |
| h.failCount = 6 | |
| // create temp dir for this test run | |
| tmpDir, err := os.MkdirTemp(tmpDirRoot, tcname) | |
| if err != nil { | |
| t.Fatal(err) | |
| } | |
| // make some template files | |
| templatePath := filepath.Join(tmpDir, "render_0.tmpl") | |
| if err := os.WriteFile(templatePath, []byte(templateContents(0)), 0o600); err != nil { | |
| t.Fatal(err) | |
| } | |
| templateConfig := fmt.Sprintf(templateConfigString, templatePath, tmpDir, "render_0.json") | |
| var retryConf string | |
| if tc.retries != nil { | |
| retryConf = fmt.Sprintf("retry { num_retries = %d }", *tc.retries) | |
| } | |
| config := fmt.Sprintf(` | |
| %s | |
| vault { | |
| address = "%s" | |
| %s | |
| tls_skip_verify = true | |
| } | |
| %s | |
| template_config { | |
| exit_on_retry_failure = true | |
| } | |
| `, methodConf, serverClient.Address(), retryConf, templateConfig) | |
| configPath := makeTempFile(t, "config.hcl", config) | |
| defer os.Remove(configPath) | |
| // Start the agent | |
| _, cmd := testAgentCommand(t, logger) | |
| cmd.startedCh = make(chan struct{}) | |
| wg := &sync.WaitGroup{} | |
| wg.Add(1) | |
| var code int | |
| go func() { | |
| code = cmd.Run([]string{"-config", configPath}) | |
| wg.Done() | |
| }() | |
| select { | |
| case <-cmd.startedCh: | |
| case <-time.After(5 * time.Second): | |
| t.Errorf("timeout") | |
| } | |
| verify := func() error { | |
| t.Helper() | |
| // We need to poll for a bit to give Agent time to render the | |
| // templates. Without this this, the test will attempt to read | |
| // the temp dir before Agent has had time to render and will | |
| // likely fail the test | |
| tick := time.Tick(1 * time.Second) | |
| timeout := time.After(15 * time.Second) | |
| var err error | |
| for { | |
| select { | |
| case <-timeout: | |
| return fmt.Errorf("timed out waiting for templates to render, last error: %v", err) | |
| case <-tick: | |
| } | |
| // Check for files rendered in the directory and break | |
| // early for shutdown if we do have all the files | |
| // rendered | |
| //---------------------------------------------------- | |
| // Perform the tests | |
| //---------------------------------------------------- | |
| if numFiles := testListFiles(t, tmpDir, ".json"); numFiles != 1 { | |
| err = fmt.Errorf("expected 1 template, got (%d)", numFiles) | |
| continue | |
| } | |
| fileName := filepath.Join(tmpDir, "render_0.json") | |
| var c []byte | |
| c, err = os.ReadFile(fileName) | |
| if err != nil { | |
| continue | |
| } | |
| if string(c) != templateRendered(0) { | |
| err = fmt.Errorf("expected=%q, got=%q", templateRendered(0), string(c)) | |
| continue | |
| } | |
| return nil | |
| } | |
| } | |
| err = verify() | |
| close(cmd.ShutdownCh) | |
| wg.Wait() | |
| switch { | |
| case (code != 0 || err != nil) && tc.expectError: | |
| case code == 0 && err == nil && !tc.expectError: | |
| default: | |
| t.Fatalf("%s expectError=%v error=%v code=%d", tcname, tc.expectError, err, code) | |
| } | |
| }) | |
| } | |
| } | |
| // prepAgentApproleKV configures a Vault instance for approle authentication, | |
| // such that the resulting token will have global permissions across /kv | |
| // and /secret mounts. Returns the auto_auth config stanza to setup an Agent | |
| // to connect using approle. | |
| func prepAgentApproleKV(t *testing.T, client *api.Client) (string, func()) { | |
| t.Helper() | |
| policyAutoAuthAppRole := ` | |
| path "/kv/*" { | |
| capabilities = ["create", "read", "update", "delete", "list"] | |
| } | |
| path "/secret/*" { | |
| capabilities = ["create", "read", "update", "delete", "list"] | |
| } | |
| ` | |
| // 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": "1h", | |
| "token_max_ttl": "2h", | |
| "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) | |
| } | |
| secretID := resp.Data["secret_id"].(string) | |
| secretIDFile := makeTempFile(t, "secret_id.txt", secretID+"\n") | |
| resp, err = client.Logical().Read("auth/approle/role/test1/role-id") | |
| if err != nil { | |
| t.Fatal(err) | |
| } | |
| roleID := resp.Data["role_id"].(string) | |
| roleIDFile := makeTempFile(t, "role_id.txt", roleID+"\n") | |
| config := fmt.Sprintf(` | |
| auto_auth { | |
| method "approle" { | |
| mount_path = "auth/approle" | |
| config = { | |
| role_id_file_path = "%s" | |
| secret_id_file_path = "%s" | |
| remove_secret_id_file_after_reading = false | |
| } | |
| } | |
| } | |
| `, roleIDFile, secretIDFile) | |
| cleanup := func() { | |
| _ = os.Remove(roleIDFile) | |
| _ = os.Remove(secretIDFile) | |
| } | |
| return config, cleanup | |
| } | |
| // TestAgent_AutoAuth_UserAgent tests that the User-Agent sent | |
| // to Vault by Vault Agent is correct when performing Auto-Auth. | |
| // Uses the custom handler userAgentHandler (defined above) so | |
| // that Vault validates the User-Agent on requests sent by Agent. | |
| func TestAgent_AutoAuth_UserAgent(t *testing.T) { | |
| logger := logging.NewVaultLogger(hclog.Trace) | |
| var h userAgentHandler | |
| cluster := vault.NewTestCluster(t, &vault.CoreConfig{ | |
| Logger: logger, | |
| CredentialBackends: map[string]logical.Factory{ | |
| "approle": credAppRole.Factory, | |
| }, | |
| }, &vault.TestClusterOptions{ | |
| NumCores: 1, | |
| HandlerFunc: vaulthttp.HandlerFunc( | |
| func(properties *vault.HandlerProperties) http.Handler { | |
| h.props = properties | |
| h.userAgentToCheckFor = useragent.AgentAutoAuthString() | |
| h.requestMethodToCheck = "PUT" | |
| h.pathToCheck = "auth/approle/login" | |
| h.t = t | |
| return &h | |
| }), | |
| }) | |
| cluster.Start() | |
| defer cluster.Cleanup() | |
| serverClient := cluster.Cores[0].Client | |
| // Enable the approle auth method | |
| roleIDPath, secretIDPath := setupAppRole(t, serverClient) | |
| sinkf, err := os.CreateTemp("", "sink.test.") | |
| if err != nil { | |
| t.Fatal(err) | |
| } | |
| sink := sinkf.Name() | |
| sinkf.Close() | |
| os.Remove(sink) | |
| autoAuthConfig := fmt.Sprintf(` | |
| auto_auth { | |
| method "approle" { | |
| mount_path = "auth/approle" | |
| config = { | |
| role_id_file_path = "%s" | |
| secret_id_file_path = "%s" | |
| } | |
| } | |
| sink "file" { | |
| config = { | |
| path = "%s" | |
| } | |
| } | |
| }`, roleIDPath, secretIDPath, sink) | |
| listenAddr := generateListenerAddress(t) | |
| listenConfig := fmt.Sprintf(` | |
| listener "tcp" { | |
| address = "%s" | |
| tls_disable = true | |
| } | |
| `, listenAddr) | |
| config := fmt.Sprintf(` | |
| vault { | |
| address = "%s" | |
| tls_skip_verify = true | |
| } | |
| api_proxy { | |
| use_auto_auth_token = true | |
| } | |
| %s | |
| %s | |
| `, serverClient.Address(), listenConfig, autoAuthConfig) | |
| configPath := makeTempFile(t, "config.hcl", config) | |
| defer os.Remove(configPath) | |
| // Unset the environment variable so that agent picks up the right test | |
| // cluster address | |
| defer os.Setenv(api.EnvVaultAddress, os.Getenv(api.EnvVaultAddress)) | |
| os.Unsetenv(api.EnvVaultAddress) | |
| // Start the agent | |
| _, cmd := testAgentCommand(t, logger) | |
| cmd.startedCh = make(chan struct{}) | |
| wg := &sync.WaitGroup{} | |
| wg.Add(1) | |
| go func() { | |
| cmd.Run([]string{"-config", configPath}) | |
| wg.Done() | |
| }() | |
| select { | |
| case <-cmd.startedCh: | |
| case <-time.After(5 * time.Second): | |
| t.Errorf("timeout") | |
| } | |
| // Validate that the auto-auth token has been correctly attained | |
| // and works for LookupSelf | |
| conf := api.DefaultConfig() | |
| conf.Address = "http://" + listenAddr | |
| agentClient, err := api.NewClient(conf) | |
| if err != nil { | |
| t.Fatalf("err: %s", err) | |
| } | |
| agentClient.SetToken("") | |
| err = agentClient.SetAddress("http://" + listenAddr) | |
| if err != nil { | |
| t.Fatal(err) | |
| } | |
| // Wait for the token to be sent to syncs and be available to be used | |
| time.Sleep(5 * time.Second) | |
| req := agentClient.NewRequest("GET", "/v1/auth/token/lookup-self") | |
| request(t, agentClient, req, 200) | |
| close(cmd.ShutdownCh) | |
| wg.Wait() | |
| } | |
| // TestAgent_APIProxyWithoutCache_UserAgent tests that the User-Agent sent | |
| // to Vault by Vault Agent is correct using the API proxy without | |
| // the cache configured. Uses the custom handler | |
| // userAgentHandler struct defined in this test package, so that Vault validates the | |
| // User-Agent on requests sent by Agent. | |
| func TestAgent_APIProxyWithoutCache_UserAgent(t *testing.T) { | |
| logger := logging.NewVaultLogger(hclog.Trace) | |
| userAgentForProxiedClient := "proxied-client" | |
| var h userAgentHandler | |
| cluster := vault.NewTestCluster(t, nil, &vault.TestClusterOptions{ | |
| NumCores: 1, | |
| HandlerFunc: vaulthttp.HandlerFunc( | |
| func(properties *vault.HandlerProperties) http.Handler { | |
| h.props = properties | |
| h.userAgentToCheckFor = useragent.AgentProxyStringWithProxiedUserAgent(userAgentForProxiedClient) | |
| h.pathToCheck = "/v1/auth/token/lookup-self" | |
| h.requestMethodToCheck = "GET" | |
| h.t = t | |
| return &h | |
| }), | |
| }) | |
| cluster.Start() | |
| defer cluster.Cleanup() | |
| serverClient := cluster.Cores[0].Client | |
| // Unset the environment variable so that agent picks up the right test | |
| // cluster address | |
| defer os.Setenv(api.EnvVaultAddress, os.Getenv(api.EnvVaultAddress)) | |
| os.Unsetenv(api.EnvVaultAddress) | |
| listenAddr := generateListenerAddress(t) | |
| listenConfig := fmt.Sprintf(` | |
| listener "tcp" { | |
| address = "%s" | |
| tls_disable = true | |
| } | |
| `, listenAddr) | |
| config := fmt.Sprintf(` | |
| vault { | |
| address = "%s" | |
| tls_skip_verify = true | |
| } | |
| %s | |
| `, serverClient.Address(), listenConfig) | |
| configPath := makeTempFile(t, "config.hcl", config) | |
| defer os.Remove(configPath) | |
| // Start the agent | |
| _, cmd := testAgentCommand(t, logger) | |
| cmd.startedCh = make(chan struct{}) | |
| wg := &sync.WaitGroup{} | |
| wg.Add(1) | |
| go func() { | |
| cmd.Run([]string{"-config", configPath}) | |
| wg.Done() | |
| }() | |
| select { | |
| case <-cmd.startedCh: | |
| case <-time.After(5 * time.Second): | |
| t.Errorf("timeout") | |
| } | |
| agentClient, err := api.NewClient(api.DefaultConfig()) | |
| if err != nil { | |
| t.Fatal(err) | |
| } | |
| agentClient.AddHeader("User-Agent", userAgentForProxiedClient) | |
| agentClient.SetToken(serverClient.Token()) | |
| agentClient.SetMaxRetries(0) | |
| err = agentClient.SetAddress("http://" + listenAddr) | |
| if err != nil { | |
| t.Fatal(err) | |
| } | |
| _, err = agentClient.Auth().Token().LookupSelf() | |
| if err != nil { | |
| t.Fatal(err) | |
| } | |
| close(cmd.ShutdownCh) | |
| wg.Wait() | |
| } | |
| // TestAgent_APIProxyWithCache_UserAgent tests that the User-Agent sent | |
| // to Vault by Vault Agent is correct using the API proxy with | |
| // the cache configured. Uses the custom handler | |
| // userAgentHandler struct defined in this test package, so that Vault validates the | |
| // User-Agent on requests sent by Agent. | |
| func TestAgent_APIProxyWithCache_UserAgent(t *testing.T) { | |
| logger := logging.NewVaultLogger(hclog.Trace) | |
| userAgentForProxiedClient := "proxied-client" | |
| var h userAgentHandler | |
| cluster := vault.NewTestCluster(t, nil, &vault.TestClusterOptions{ | |
| NumCores: 1, | |
| HandlerFunc: vaulthttp.HandlerFunc( | |
| func(properties *vault.HandlerProperties) http.Handler { | |
| h.props = properties | |
| h.userAgentToCheckFor = useragent.AgentProxyStringWithProxiedUserAgent(userAgentForProxiedClient) | |
| h.pathToCheck = "/v1/auth/token/lookup-self" | |
| h.requestMethodToCheck = "GET" | |
| h.t = t | |
| return &h | |
| }), | |
| }) | |
| cluster.Start() | |
| defer cluster.Cleanup() | |
| serverClient := cluster.Cores[0].Client | |
| // Unset the environment variable so that agent picks up the right test | |
| // cluster address | |
| defer os.Setenv(api.EnvVaultAddress, os.Getenv(api.EnvVaultAddress)) | |
| os.Unsetenv(api.EnvVaultAddress) | |
| listenAddr := generateListenerAddress(t) | |
| listenConfig := fmt.Sprintf(` | |
| listener "tcp" { | |
| address = "%s" | |
| tls_disable = true | |
| } | |
| `, listenAddr) | |
| cacheConfig := ` | |
| cache { | |
| }` | |
| config := fmt.Sprintf(` | |
| vault { | |
| address = "%s" | |
| tls_skip_verify = true | |
| } | |
| %s | |
| %s | |
| `, serverClient.Address(), listenConfig, cacheConfig) | |
| configPath := makeTempFile(t, "config.hcl", config) | |
| defer os.Remove(configPath) | |
| // Start the agent | |
| _, cmd := testAgentCommand(t, logger) | |
| cmd.startedCh = make(chan struct{}) | |
| wg := &sync.WaitGroup{} | |
| wg.Add(1) | |
| go func() { | |
| cmd.Run([]string{"-config", configPath}) | |
| wg.Done() | |
| }() | |
| select { | |
| case <-cmd.startedCh: | |
| case <-time.After(5 * time.Second): | |
| t.Errorf("timeout") | |
| } | |
| agentClient, err := api.NewClient(api.DefaultConfig()) | |
| if err != nil { | |
| t.Fatal(err) | |
| } | |
| agentClient.AddHeader("User-Agent", userAgentForProxiedClient) | |
| agentClient.SetToken(serverClient.Token()) | |
| agentClient.SetMaxRetries(0) | |
| err = agentClient.SetAddress("http://" + listenAddr) | |
| if err != nil { | |
| t.Fatal(err) | |
| } | |
| _, err = agentClient.Auth().Token().LookupSelf() | |
| if err != nil { | |
| t.Fatal(err) | |
| } | |
| close(cmd.ShutdownCh) | |
| wg.Wait() | |
| } | |
| func TestAgent_Cache_DynamicSecret(t *testing.T) { | |
| logger := logging.NewVaultLogger(hclog.Trace) | |
| cluster := vault.NewTestCluster(t, nil, &vault.TestClusterOptions{ | |
| HandlerFunc: vaulthttp.Handler, | |
| }) | |
| cluster.Start() | |
| defer cluster.Cleanup() | |
| serverClient := cluster.Cores[0].Client | |
| // Unset the environment variable so that agent picks up the right test | |
| // cluster address | |
| defer os.Setenv(api.EnvVaultAddress, os.Getenv(api.EnvVaultAddress)) | |
| os.Unsetenv(api.EnvVaultAddress) | |
| cacheConfig := ` | |
| cache { | |
| } | |
| ` | |
| listenAddr := generateListenerAddress(t) | |
| listenConfig := fmt.Sprintf(` | |
| listener "tcp" { | |
| address = "%s" | |
| tls_disable = true | |
| } | |
| `, listenAddr) | |
| config := fmt.Sprintf(` | |
| vault { | |
| address = "%s" | |
| tls_skip_verify = true | |
| } | |
| %s | |
| %s | |
| `, serverClient.Address(), cacheConfig, listenConfig) | |
| configPath := makeTempFile(t, "config.hcl", config) | |
| defer os.Remove(configPath) | |
| // Start the agent | |
| _, cmd := testAgentCommand(t, logger) | |
| cmd.startedCh = make(chan struct{}) | |
| wg := &sync.WaitGroup{} | |
| wg.Add(1) | |
| go func() { | |
| cmd.Run([]string{"-config", configPath}) | |
| wg.Done() | |
| }() | |
| select { | |
| case <-cmd.startedCh: | |
| case <-time.After(5 * time.Second): | |
| t.Errorf("timeout") | |
| } | |
| agentClient, err := api.NewClient(api.DefaultConfig()) | |
| if err != nil { | |
| t.Fatal(err) | |
| } | |
| agentClient.SetToken(serverClient.Token()) | |
| agentClient.SetMaxRetries(0) | |
| err = agentClient.SetAddress("http://" + listenAddr) | |
| if err != nil { | |
| t.Fatal(err) | |
| } | |
| renewable := true | |
| tokenCreateRequest := &api.TokenCreateRequest{ | |
| Policies: []string{"default"}, | |
| TTL: "30m", | |
| Renewable: &renewable, | |
| } | |
| // This was the simplest test I could find to trigger the caching behaviour, | |
| // i.e. the most concise I could make the test that I can tell | |
| // creating an orphan token returns Auth, is renewable, and isn't a token | |
| // that's managed elsewhere (since it's an orphan) | |
| secret, err := agentClient.Auth().Token().CreateOrphan(tokenCreateRequest) | |
| if err != nil { | |
| t.Fatal(err) | |
| } | |
| if secret == nil || secret.Auth == nil { | |
| t.Fatalf("secret not as expected: %v", secret) | |
| } | |
| token := secret.Auth.ClientToken | |
| secret, err = agentClient.Auth().Token().CreateOrphan(tokenCreateRequest) | |
| if err != nil { | |
| t.Fatal(err) | |
| } | |
| if secret == nil || secret.Auth == nil { | |
| t.Fatalf("secret not as expected: %v", secret) | |
| } | |
| token2 := secret.Auth.ClientToken | |
| if token != token2 { | |
| t.Fatalf("token create response not cached when it should have been, as tokens differ") | |
| } | |
| close(cmd.ShutdownCh) | |
| wg.Wait() | |
| } | |
| func TestAgent_ApiProxy_Retry(t *testing.T) { | |
| //---------------------------------------------------- | |
| // Start the server and agent | |
| //---------------------------------------------------- | |
| logger := logging.NewVaultLogger(hclog.Trace) | |
| var h handler | |
| cluster := vault.NewTestCluster(t, | |
| &vault.CoreConfig{ | |
| Logger: logger, | |
| CredentialBackends: map[string]logical.Factory{ | |
| "approle": credAppRole.Factory, | |
| }, | |
| LogicalBackends: map[string]logical.Factory{ | |
| "kv": logicalKv.Factory, | |
| }, | |
| }, | |
| &vault.TestClusterOptions{ | |
| NumCores: 1, | |
| HandlerFunc: vaulthttp.HandlerFunc(func(properties *vault.HandlerProperties) http.Handler { | |
| h.props = properties | |
| h.t = t | |
| return &h | |
| }), | |
| }) | |
| cluster.Start() | |
| defer cluster.Cleanup() | |
| vault.TestWaitActive(t, cluster.Cores[0].Core) | |
| serverClient := cluster.Cores[0].Client | |
| // Unset the environment variable so that agent picks up the right test | |
| // cluster address | |
| defer os.Setenv(api.EnvVaultAddress, os.Getenv(api.EnvVaultAddress)) | |
| os.Unsetenv(api.EnvVaultAddress) | |
| _, err := serverClient.Logical().Write("secret/foo", map[string]interface{}{ | |
| "bar": "baz", | |
| }) | |
| if err != nil { | |
| t.Fatal(err) | |
| } | |
| intRef := func(i int) *int { | |
| return &i | |
| } | |
| // start test cases here | |
| testCases := map[string]struct { | |
| retries *int | |
| expectError bool | |
| }{ | |
| "none": { | |
| retries: intRef(-1), | |
| expectError: true, | |
| }, | |
| "one": { | |
| retries: intRef(1), | |
| expectError: true, | |
| }, | |
| "two": { | |
| retries: intRef(2), | |
| expectError: false, | |
| }, | |
| "missing": { | |
| retries: nil, | |
| expectError: false, | |
| }, | |
| "default": { | |
| retries: intRef(0), | |
| expectError: false, | |
| }, | |
| } | |
| for tcname, tc := range testCases { | |
| t.Run(tcname, func(t *testing.T) { | |
| h.failCount = 2 | |
| cacheConfig := ` | |
| cache { | |
| } | |
| ` | |
| listenAddr := generateListenerAddress(t) | |
| listenConfig := fmt.Sprintf(` | |
| listener "tcp" { | |
| address = "%s" | |
| tls_disable = true | |
| } | |
| `, listenAddr) | |
| var retryConf string | |
| if tc.retries != nil { | |
| retryConf = fmt.Sprintf("retry { num_retries = %d }", *tc.retries) | |
| } | |
| config := fmt.Sprintf(` | |
| vault { | |
| address = "%s" | |
| %s | |
| tls_skip_verify = true | |
| } | |
| %s | |
| %s | |
| `, serverClient.Address(), retryConf, cacheConfig, listenConfig) | |
| configPath := makeTempFile(t, "config.hcl", config) | |
| defer os.Remove(configPath) | |
| // Start the agent | |
| _, cmd := testAgentCommand(t, logger) | |
| cmd.startedCh = make(chan struct{}) | |
| wg := &sync.WaitGroup{} | |
| wg.Add(1) | |
| go func() { | |
| cmd.Run([]string{"-config", configPath}) | |
| wg.Done() | |
| }() | |
| select { | |
| case <-cmd.startedCh: | |
| case <-time.After(5 * time.Second): | |
| t.Errorf("timeout") | |
| } | |
| client, err := api.NewClient(api.DefaultConfig()) | |
| if err != nil { | |
| t.Fatal(err) | |
| } | |
| client.SetToken(serverClient.Token()) | |
| client.SetMaxRetries(0) | |
| err = client.SetAddress("http://" + listenAddr) | |
| if err != nil { | |
| t.Fatal(err) | |
| } | |
| secret, err := client.Logical().Read("secret/foo") | |
| switch { | |
| case (err != nil || secret == nil) && tc.expectError: | |
| case (err == nil || secret != nil) && !tc.expectError: | |
| default: | |
| t.Fatalf("%s expectError=%v error=%v secret=%v", tcname, tc.expectError, err, secret) | |
| } | |
| if secret != nil && secret.Data["foo"] != nil { | |
| val := secret.Data["foo"].(map[string]interface{}) | |
| if !reflect.DeepEqual(val, map[string]interface{}{"bar": "baz"}) { | |
| t.Fatalf("expected key 'foo' to yield bar=baz, got: %v", val) | |
| } | |
| } | |
| time.Sleep(time.Second) | |
| close(cmd.ShutdownCh) | |
| wg.Wait() | |
| }) | |
| } | |
| } | |
| func TestAgent_TemplateConfig_ExitOnRetryFailure(t *testing.T) { | |
| //---------------------------------------------------- | |
| // Start the server and agent | |
| //---------------------------------------------------- | |
| logger := logging.NewVaultLogger(hclog.Trace) | |
| cluster := vault.NewTestCluster(t, | |
| &vault.CoreConfig{ | |
| // Logger: logger, | |
| CredentialBackends: map[string]logical.Factory{ | |
| "approle": credAppRole.Factory, | |
| }, | |
| LogicalBackends: map[string]logical.Factory{ | |
| "kv": logicalKv.Factory, | |
| }, | |
| }, | |
| &vault.TestClusterOptions{ | |
| NumCores: 1, | |
| HandlerFunc: vaulthttp.Handler, | |
| }) | |
| cluster.Start() | |
| defer cluster.Cleanup() | |
| vault.TestWaitActive(t, cluster.Cores[0].Core) | |
| serverClient := cluster.Cores[0].Client | |
| // Unset the environment variable so that agent picks up the right test | |
| // cluster address | |
| defer os.Setenv(api.EnvVaultAddress, os.Getenv(api.EnvVaultAddress)) | |
| os.Unsetenv(api.EnvVaultAddress) | |
| autoAuthConfig, cleanup := prepAgentApproleKV(t, serverClient) | |
| defer cleanup() | |
| err := serverClient.Sys().TuneMount("secret", api.MountConfigInput{ | |
| Options: map[string]string{ | |
| "version": "2", | |
| }, | |
| }) | |
| if err != nil { | |
| t.Fatal(err) | |
| } | |
| _, err = serverClient.Logical().Write("secret/data/otherapp", map[string]interface{}{ | |
| "data": map[string]interface{}{ | |
| "username": "barstuff", | |
| "password": "zap", | |
| "cert": "something", | |
| }, | |
| }) | |
| if err != nil { | |
| t.Fatal(err) | |
| } | |
| // make a temp directory to hold renders. Each test will create a temp dir | |
| // inside this one | |
| tmpDirRoot, err := os.MkdirTemp("", "agent-test-renders") | |
| if err != nil { | |
| t.Fatal(err) | |
| } | |
| defer os.RemoveAll(tmpDirRoot) | |
| // Note that missing key is different from a non-existent secret. A missing | |
| // key (2xx response with missing keys in the response map) can still yield | |
| // a successful render unless error_on_missing_key is specified, whereas a | |
| // missing secret (4xx response) always results in an error. | |
| missingKeyTemplateContent := `{{- with secret "secret/otherapp"}}{"secret": "other", | |
| {{- if .Data.data.foo}}"foo":"{{ .Data.data.foo}}"{{- end }}} | |
| {{- end }}` | |
| missingKeyTemplateRender := `{"secret": "other",}` | |
| badTemplateContent := `{{- with secret "secret/non-existent"}}{"secret": "other", | |
| {{- if .Data.data.foo}}"foo":"{{ .Data.data.foo}}"{{- end }}} | |
| {{- end }}` | |
| testCases := map[string]struct { | |
| exitOnRetryFailure *bool | |
| templateContents string | |
| expectTemplateRender string | |
| templateErrorOnMissingKey bool | |
| expectError bool | |
| expectExitFromError bool | |
| }{ | |
| "true, no template error": { | |
| exitOnRetryFailure: pointerutil.BoolPtr(true), | |
| templateContents: templateContents(0), | |
| expectTemplateRender: templateRendered(0), | |
| templateErrorOnMissingKey: false, | |
| expectError: false, | |
| expectExitFromError: false, | |
| }, | |
| "true, with non-existent secret": { | |
| exitOnRetryFailure: pointerutil.BoolPtr(true), | |
| templateContents: badTemplateContent, | |
| expectTemplateRender: "", | |
| templateErrorOnMissingKey: false, | |
| expectError: true, | |
| expectExitFromError: true, | |
| }, | |
| "true, with missing key": { | |
| exitOnRetryFailure: pointerutil.BoolPtr(true), | |
| templateContents: missingKeyTemplateContent, | |
| expectTemplateRender: missingKeyTemplateRender, | |
| templateErrorOnMissingKey: false, | |
| expectError: false, | |
| expectExitFromError: false, | |
| }, | |
| "true, with missing key, with error_on_missing_key": { | |
| exitOnRetryFailure: pointerutil.BoolPtr(true), | |
| templateContents: missingKeyTemplateContent, | |
| expectTemplateRender: "", | |
| templateErrorOnMissingKey: true, | |
| expectError: true, | |
| expectExitFromError: true, | |
| }, | |
| "false, no template error": { | |
| exitOnRetryFailure: pointerutil.BoolPtr(false), | |
| templateContents: templateContents(0), | |
| expectTemplateRender: templateRendered(0), | |
| templateErrorOnMissingKey: false, | |
| expectError: false, | |
| expectExitFromError: false, | |
| }, | |
| "false, with non-existent secret": { | |
| exitOnRetryFailure: pointerutil.BoolPtr(false), | |
| templateContents: badTemplateContent, | |
| expectTemplateRender: "", | |
| templateErrorOnMissingKey: false, | |
| expectError: true, | |
| expectExitFromError: false, | |
| }, | |
| "false, with missing key": { | |
| exitOnRetryFailure: pointerutil.BoolPtr(false), | |
| templateContents: missingKeyTemplateContent, | |
| expectTemplateRender: missingKeyTemplateRender, | |
| templateErrorOnMissingKey: false, | |
| expectError: false, | |
| expectExitFromError: false, | |
| }, | |
| "false, with missing key, with error_on_missing_key": { | |
| exitOnRetryFailure: pointerutil.BoolPtr(false), | |
| templateContents: missingKeyTemplateContent, | |
| expectTemplateRender: missingKeyTemplateRender, | |
| templateErrorOnMissingKey: true, | |
| expectError: true, | |
| expectExitFromError: false, | |
| }, | |
| "missing": { | |
| exitOnRetryFailure: nil, | |
| templateContents: templateContents(0), | |
| expectTemplateRender: templateRendered(0), | |
| templateErrorOnMissingKey: false, | |
| expectError: false, | |
| expectExitFromError: false, | |
| }, | |
| } | |
| for tcName, tc := range testCases { | |
| t.Run(tcName, func(t *testing.T) { | |
| // create temp dir for this test run | |
| tmpDir, err := os.MkdirTemp(tmpDirRoot, tcName) | |
| if err != nil { | |
| t.Fatal(err) | |
| } | |
| listenAddr := generateListenerAddress(t) | |
| listenConfig := fmt.Sprintf(` | |
| listener "tcp" { | |
| address = "%s" | |
| tls_disable = true | |
| } | |
| `, listenAddr) | |
| var exitOnRetryFailure string | |
| if tc.exitOnRetryFailure != nil { | |
| exitOnRetryFailure = fmt.Sprintf("exit_on_retry_failure = %t", *tc.exitOnRetryFailure) | |
| } | |
| templateConfig := fmt.Sprintf(` | |
| template_config = { | |
| %s | |
| } | |
| `, exitOnRetryFailure) | |
| template := fmt.Sprintf(` | |
| template { | |
| contents = <<EOF | |
| %s | |
| EOF | |
| destination = "%s/render_0.json" | |
| error_on_missing_key = %t | |
| } | |
| `, tc.templateContents, tmpDir, tc.templateErrorOnMissingKey) | |
| config := fmt.Sprintf(` | |
| # auto-auth stanza | |
| %s | |
| vault { | |
| address = "%s" | |
| tls_skip_verify = true | |
| retry { | |
| num_retries = 3 | |
| } | |
| } | |
| # listener stanza | |
| %s | |
| # template_config stanza | |
| %s | |
| # template stanza | |
| %s | |
| `, autoAuthConfig, serverClient.Address(), listenConfig, templateConfig, template) | |
| configPath := makeTempFile(t, "config.hcl", config) | |
| defer os.Remove(configPath) | |
| // Start the agent | |
| ui, cmd := testAgentCommand(t, logger) | |
| cmd.startedCh = make(chan struct{}) | |
| // Channel to let verify() know to stop early if agent | |
| // has exited | |
| cmdRunDoneCh := make(chan struct{}) | |
| var exitedEarly bool | |
| wg := &sync.WaitGroup{} | |
| wg.Add(1) | |
| var code int | |
| go func() { | |
| code = cmd.Run([]string{"-config", configPath}) | |
| close(cmdRunDoneCh) | |
| wg.Done() | |
| }() | |
| verify := func() error { | |
| t.Helper() | |
| // We need to poll for a bit to give Agent time to render the | |
| // templates. Without this this, the test will attempt to read | |
| // the temp dir before Agent has had time to render and will | |
| // likely fail the test | |
| tick := time.Tick(1 * time.Second) | |
| timeout := time.After(15 * time.Second) | |
| var err error | |
| for { | |
| select { | |
| case <-cmdRunDoneCh: | |
| exitedEarly = true | |
| return nil | |
| case <-timeout: | |
| return fmt.Errorf("timed out waiting for templates to render, last error: %w", err) | |
| case <-tick: | |
| } | |
| // Check for files rendered in the directory and break | |
| // early for shutdown if we do have all the files | |
| // rendered | |
| //---------------------------------------------------- | |
| // Perform the tests | |
| //---------------------------------------------------- | |
| if numFiles := testListFiles(t, tmpDir, ".json"); numFiles != 1 { | |
| err = fmt.Errorf("expected 1 template, got (%d)", numFiles) | |
| continue | |
| } | |
| fileName := filepath.Join(tmpDir, "render_0.json") | |
| var c []byte | |
| c, err = os.ReadFile(fileName) | |
| if err != nil { | |
| continue | |
| } | |
| if strings.TrimSpace(string(c)) != tc.expectTemplateRender { | |
| err = fmt.Errorf("expected=%q, got=%q", tc.expectTemplateRender, strings.TrimSpace(string(c))) | |
| continue | |
| } | |
| return nil | |
| } | |
| } | |
| err = verify() | |
| close(cmd.ShutdownCh) | |
| wg.Wait() | |
| switch { | |
| case (code != 0 || err != nil) && tc.expectError: | |
| if exitedEarly != tc.expectExitFromError { | |
| t.Fatalf("expected program exit due to error to be '%t', got '%t'", tc.expectExitFromError, exitedEarly) | |
| } | |
| case code == 0 && err == nil && !tc.expectError: | |
| if exitedEarly { | |
| t.Fatalf("did not expect program to exit before verify completes") | |
| } | |
| default: | |
| if code != 0 { | |
| t.Logf("output from agent:\n%s", ui.OutputWriter.String()) | |
| t.Logf("error from agent:\n%s", ui.ErrorWriter.String()) | |
| } | |
| t.Fatalf("expectError=%v error=%v code=%d", tc.expectError, err, code) | |
| } | |
| }) | |
| } | |
| } | |
| func TestAgent_Metrics(t *testing.T) { | |
| //---------------------------------------------------- | |
| // Start the server and agent | |
| //---------------------------------------------------- | |
| // Start a vault server | |
| logger := logging.NewVaultLogger(hclog.Trace) | |
| cluster := vault.NewTestCluster(t, | |
| &vault.CoreConfig{ | |
| Logger: logger, | |
| }, | |
| &vault.TestClusterOptions{ | |
| HandlerFunc: vaulthttp.Handler, | |
| }) | |
| cluster.Start() | |
| defer cluster.Cleanup() | |
| vault.TestWaitActive(t, cluster.Cores[0].Core) | |
| serverClient := cluster.Cores[0].Client | |
| // Create a config file | |
| listenAddr := generateListenerAddress(t) | |
| config := fmt.Sprintf(` | |
| cache {} | |
| listener "tcp" { | |
| address = "%s" | |
| tls_disable = true | |
| } | |
| `, listenAddr) | |
| configPath := makeTempFile(t, "config.hcl", config) | |
| defer os.Remove(configPath) | |
| // Start the agent | |
| ui, cmd := testAgentCommand(t, logger) | |
| cmd.client = serverClient | |
| cmd.startedCh = make(chan struct{}) | |
| var output string | |
| var code int | |
| wg := &sync.WaitGroup{} | |
| wg.Add(1) | |
| go func() { | |
| code = cmd.Run([]string{"-config", configPath}) | |
| if code != 0 { | |
| output = ui.ErrorWriter.String() + ui.OutputWriter.String() | |
| } | |
| wg.Done() | |
| }() | |
| select { | |
| case <-cmd.startedCh: | |
| case <-time.After(5 * time.Second): | |
| t.Errorf("timeout") | |
| } | |
| // defer agent shutdown | |
| defer func() { | |
| cmd.ShutdownCh <- struct{}{} | |
| wg.Wait() | |
| if code != 0 { | |
| t.Fatalf("got a non-zero exit status: %d, stdout/stderr: %s", code, output) | |
| } | |
| }() | |
| conf := api.DefaultConfig() | |
| conf.Address = "http://" + listenAddr | |
| agentClient, err := api.NewClient(conf) | |
| if err != nil { | |
| t.Fatalf("err: %s", err) | |
| } | |
| req := agentClient.NewRequest("GET", "/agent/v1/metrics") | |
| body := request(t, agentClient, req, 200) | |
| keys := []string{} | |
| for k := range body { | |
| keys = append(keys, k) | |
| } | |
| require.ElementsMatch(t, keys, []string{ | |
| "Counters", | |
| "Samples", | |
| "Timestamp", | |
| "Gauges", | |
| "Points", | |
| }) | |
| } | |
| func TestAgent_Quit(t *testing.T) { | |
| //---------------------------------------------------- | |
| // Start the server and agent | |
| //---------------------------------------------------- | |
| logger := logging.NewVaultLogger(hclog.Error) | |
| cluster := vault.NewTestCluster(t, | |
| &vault.CoreConfig{ | |
| Logger: logger, | |
| CredentialBackends: map[string]logical.Factory{ | |
| "approle": credAppRole.Factory, | |
| }, | |
| LogicalBackends: map[string]logical.Factory{ | |
| "kv": logicalKv.Factory, | |
| }, | |
| }, | |
| &vault.TestClusterOptions{ | |
| NumCores: 1, | |
| }) | |
| cluster.Start() | |
| defer cluster.Cleanup() | |
| vault.TestWaitActive(t, cluster.Cores[0].Core) | |
| serverClient := cluster.Cores[0].Client | |
| // Unset the environment variable so that agent picks up the right test | |
| // cluster address | |
| defer os.Setenv(api.EnvVaultAddress, os.Getenv(api.EnvVaultAddress)) | |
| err := os.Unsetenv(api.EnvVaultAddress) | |
| if err != nil { | |
| t.Fatal(err) | |
| } | |
| listenAddr := generateListenerAddress(t) | |
| listenAddr2 := generateListenerAddress(t) | |
| config := fmt.Sprintf(` | |
| vault { | |
| address = "%s" | |
| tls_skip_verify = true | |
| } | |
| listener "tcp" { | |
| address = "%s" | |
| tls_disable = true | |
| } | |
| listener "tcp" { | |
| address = "%s" | |
| tls_disable = true | |
| agent_api { | |
| enable_quit = true | |
| } | |
| } | |
| cache {} | |
| `, serverClient.Address(), listenAddr, listenAddr2) | |
| configPath := makeTempFile(t, "config.hcl", config) | |
| defer os.Remove(configPath) | |
| // Start the agent | |
| _, cmd := testAgentCommand(t, logger) | |
| cmd.startedCh = make(chan struct{}) | |
| wg := &sync.WaitGroup{} | |
| wg.Add(1) | |
| go func() { | |
| cmd.Run([]string{"-config", configPath}) | |
| wg.Done() | |
| }() | |
| select { | |
| case <-cmd.startedCh: | |
| case <-time.After(5 * time.Second): | |
| t.Errorf("timeout") | |
| } | |
| client, err := api.NewClient(api.DefaultConfig()) | |
| if err != nil { | |
| t.Fatal(err) | |
| } | |
| client.SetToken(serverClient.Token()) | |
| client.SetMaxRetries(0) | |
| err = client.SetAddress("http://" + listenAddr) | |
| if err != nil { | |
| t.Fatal(err) | |
| } | |
| // First try on listener 1 where the API should be disabled. | |
| resp, err := client.RawRequest(client.NewRequest(http.MethodPost, "/agent/v1/quit")) | |
| if err == nil { | |
| t.Fatalf("expected error") | |
| } | |
| if resp != nil && resp.StatusCode != http.StatusNotFound { | |
| t.Fatalf("expected %d but got: %d", http.StatusNotFound, resp.StatusCode) | |
| } | |
| // Now try on listener 2 where the quit API should be enabled. | |
| err = client.SetAddress("http://" + listenAddr2) | |
| if err != nil { | |
| t.Fatal(err) | |
| } | |
| _, err = client.RawRequest(client.NewRequest(http.MethodPost, "/agent/v1/quit")) | |
| if err != nil { | |
| t.Fatalf("unexpected error: %s", err) | |
| } | |
| select { | |
| case <-cmd.ShutdownCh: | |
| case <-time.After(5 * time.Second): | |
| t.Errorf("timeout") | |
| } | |
| wg.Wait() | |
| } | |
| func TestAgent_LogFile_CliOverridesConfig(t *testing.T) { | |
| // Create basic config | |
| configFile := populateTempFile(t, "agent-config.hcl", BasicHclConfig) | |
| cfg, err := agentConfig.LoadConfigFile(configFile.Name()) | |
| if err != nil { | |
| t.Fatal("Cannot load config to test update/merge", err) | |
| } | |
| // Sanity check that the config value is the current value | |
| assert.Equal(t, "TMPDIR/juan.log", cfg.LogFile) | |
| // Initialize the command and parse any flags | |
| cmd := &AgentCommand{BaseCommand: &BaseCommand{}} | |
| f := cmd.Flags() | |
| // Simulate the flag being specified | |
| err = f.Parse([]string{"-log-file=/foo/bar/test.log"}) | |
| if err != nil { | |
| t.Fatal(err) | |
| } | |
| // Update the config based on the inputs. | |
| cmd.applyConfigOverrides(f, cfg) | |
| assert.NotEqual(t, "TMPDIR/juan.log", cfg.LogFile) | |
| assert.NotEqual(t, "/squiggle/logs.txt", cfg.LogFile) | |
| assert.Equal(t, "/foo/bar/test.log", cfg.LogFile) | |
| } | |
| func TestAgent_LogFile_Config(t *testing.T) { | |
| configFile := populateTempFile(t, "agent-config.hcl", BasicHclConfig) | |
| cfg, err := agentConfig.LoadConfigFile(configFile.Name()) | |
| if err != nil { | |
| t.Fatal("Cannot load config to test update/merge", err) | |
| } | |
| // Sanity check that the config value is the current value | |
| assert.Equal(t, "TMPDIR/juan.log", cfg.LogFile, "sanity check on log config failed") | |
| assert.Equal(t, 2, cfg.LogRotateMaxFiles) | |
| assert.Equal(t, 1048576, cfg.LogRotateBytes) | |
| // Parse the cli flags (but we pass in an empty slice) | |
| cmd := &AgentCommand{BaseCommand: &BaseCommand{}} | |
| f := cmd.Flags() | |
| err = f.Parse([]string{}) | |
| if err != nil { | |
| t.Fatal(err) | |
| } | |
| // Should change nothing... | |
| cmd.applyConfigOverrides(f, cfg) | |
| assert.Equal(t, "TMPDIR/juan.log", cfg.LogFile, "actual config check") | |
| assert.Equal(t, 2, cfg.LogRotateMaxFiles) | |
| assert.Equal(t, 1048576, cfg.LogRotateBytes) | |
| } | |
| func TestAgent_Config_NewLogger_Default(t *testing.T) { | |
| cmd := &AgentCommand{BaseCommand: &BaseCommand{}} | |
| cmd.config = agentConfig.NewConfig() | |
| logger, err := cmd.newLogger() | |
| assert.NoError(t, err) | |
| assert.NotNil(t, logger) | |
| assert.Equal(t, hclog.Info.String(), logger.GetLevel().String()) | |
| } | |
| func TestAgent_Config_ReloadLogLevel(t *testing.T) { | |
| cmd := &AgentCommand{BaseCommand: &BaseCommand{}} | |
| var err error | |
| tempDir := t.TempDir() | |
| // Load an initial config | |
| hcl := strings.ReplaceAll(BasicHclConfig, "TMPDIR", tempDir) | |
| configFile := populateTempFile(t, "agent-config.hcl", hcl) | |
| cmd.config, err = agentConfig.LoadConfigFile(configFile.Name()) | |
| if err != nil { | |
| t.Fatal("Cannot load config to test update/merge", err) | |
| } | |
| // Tweak the loaded config to make sure we can put log files into a temp dir | |
| // and systemd log attempts work fine, this would usually happen during Run. | |
| cmd.logWriter = os.Stdout | |
| cmd.logger, err = cmd.newLogger() | |
| if err != nil { | |
| t.Fatal("logger required for systemd log messages", err) | |
| } | |
| // Sanity check | |
| assert.Equal(t, "warn", cmd.config.LogLevel) | |
| // Load a new config | |
| hcl = strings.ReplaceAll(BasicHclConfig2, "TMPDIR", tempDir) | |
| configFile = populateTempFile(t, "agent-config.hcl", hcl) | |
| err = cmd.reloadConfig([]string{configFile.Name()}) | |
| assert.NoError(t, err) | |
| assert.Equal(t, "debug", cmd.config.LogLevel) | |
| } | |
| func TestAgent_Config_ReloadTls(t *testing.T) { | |
| var wg sync.WaitGroup | |
| wd, err := os.Getwd() | |
| if err != nil { | |
| t.Fatal("unable to get current working directory") | |
| } | |
| workingDir := filepath.Join(wd, "/agent/test-fixtures/reload") | |
| fooCert := "reload_foo.pem" | |
| fooKey := "reload_foo.key" | |
| barCert := "reload_bar.pem" | |
| barKey := "reload_bar.key" | |
| reloadCert := "reload_cert.pem" | |
| reloadKey := "reload_key.pem" | |
| caPem := "reload_ca.pem" | |
| tempDir := t.TempDir() | |
| // Set up initial 'foo' certs | |
| inBytes, err := os.ReadFile(filepath.Join(workingDir, fooCert)) | |
| if err != nil { | |
| t.Fatal("unable to read cert required for test", fooCert, err) | |
| } | |
| err = os.WriteFile(filepath.Join(tempDir, reloadCert), inBytes, 0o777) | |
| if err != nil { | |
| t.Fatal("unable to write temp cert required for test", reloadCert, err) | |
| } | |
| inBytes, err = os.ReadFile(filepath.Join(workingDir, fooKey)) | |
| if err != nil { | |
| t.Fatal("unable to read cert key required for test", fooKey, err) | |
| } | |
| err = os.WriteFile(filepath.Join(tempDir, reloadKey), inBytes, 0o777) | |
| if err != nil { | |
| t.Fatal("unable to write temp cert key required for test", reloadKey, err) | |
| } | |
| inBytes, err = os.ReadFile(filepath.Join(workingDir, caPem)) | |
| if err != nil { | |
| t.Fatal("unable to read CA pem required for test", caPem, err) | |
| } | |
| certPool := x509.NewCertPool() | |
| ok := certPool.AppendCertsFromPEM(inBytes) | |
| if !ok { | |
| t.Fatal("not ok when appending CA cert") | |
| } | |
| replacedHcl := strings.ReplaceAll(BasicHclConfig, "TMPDIR", tempDir) | |
| configFile := populateTempFile(t, "agent-config.hcl", replacedHcl) | |
| // Set up Agent/cmd | |
| logger := logging.NewVaultLogger(hclog.Trace) | |
| ui, cmd := testAgentCommand(t, logger) | |
| var output string | |
| var code int | |
| wg.Add(1) | |
| args := []string{"-config", configFile.Name()} | |
| go func() { | |
| if code = cmd.Run(args); code != 0 { | |
| output = ui.ErrorWriter.String() + ui.OutputWriter.String() | |
| } | |
| wg.Done() | |
| }() | |
| testCertificateName := func(cn string) error { | |
| conn, err := tls.Dial("tcp", "127.0.0.1:8100", &tls.Config{ | |
| RootCAs: certPool, | |
| }) | |
| if err != nil { | |
| return err | |
| } | |
| defer conn.Close() | |
| if err = conn.Handshake(); err != nil { | |
| return err | |
| } | |
| servName := conn.ConnectionState().PeerCertificates[0].Subject.CommonName | |
| if servName != cn { | |
| return fmt.Errorf("expected %s, got %s", cn, servName) | |
| } | |
| return nil | |
| } | |
| // Start | |
| select { | |
| case <-cmd.startedCh: | |
| case <-time.After(5 * time.Second): | |
| t.Fatalf("timeout") | |
| } | |
| if err := testCertificateName("foo.example.com"); err != nil { | |
| t.Fatalf("certificate name didn't check out: %s", err) | |
| } | |
| // Swap out certs | |
| inBytes, err = os.ReadFile(filepath.Join(workingDir, barCert)) | |
| if err != nil { | |
| t.Fatal("unable to read cert required for test", barCert, err) | |
| } | |
| err = os.WriteFile(filepath.Join(tempDir, reloadCert), inBytes, 0o777) | |
| if err != nil { | |
| t.Fatal("unable to write temp cert required for test", reloadCert, err) | |
| } | |
| inBytes, err = os.ReadFile(filepath.Join(workingDir, barKey)) | |
| if err != nil { | |
| t.Fatal("unable to read cert key required for test", barKey, err) | |
| } | |
| err = os.WriteFile(filepath.Join(tempDir, reloadKey), inBytes, 0o777) | |
| if err != nil { | |
| t.Fatal("unable to write temp cert key required for test", reloadKey, err) | |
| } | |
| // Reload | |
| cmd.SighupCh <- struct{}{} | |
| select { | |
| case <-cmd.reloadedCh: | |
| case <-time.After(5 * time.Second): | |
| t.Fatalf("timeout") | |
| } | |
| if err := testCertificateName("bar.example.com"); err != nil { | |
| t.Fatalf("certificate name didn't check out: %s", err) | |
| } | |
| // Shut down | |
| cmd.ShutdownCh <- struct{}{} | |
| wg.Wait() | |
| if code != 0 { | |
| t.Fatalf("got a non-zero exit status: %d, stdout/stderr: %s", code, output) | |
| } | |
| } | |
| // TestAgent_NonTLSListener_SIGHUP tests giving a SIGHUP signal to a listener | |
| // without a TLS configuration. Prior to fixing GitHub issue #19480, this | |
| // would cause a panic. | |
| func TestAgent_NonTLSListener_SIGHUP(t *testing.T) { | |
| logger := logging.NewVaultLogger(hclog.Trace) | |
| cluster := vault.NewTestCluster(t, nil, &vault.TestClusterOptions{ | |
| HandlerFunc: vaulthttp.Handler, | |
| }) | |
| cluster.Start() | |
| defer cluster.Cleanup() | |
| serverClient := cluster.Cores[0].Client | |
| // Unset the environment variable so that agent picks up the right test | |
| // cluster address | |
| defer os.Setenv(api.EnvVaultAddress, os.Getenv(api.EnvVaultAddress)) | |
| os.Unsetenv(api.EnvVaultAddress) | |
| listenAddr := generateListenerAddress(t) | |
| listenConfig := fmt.Sprintf(` | |
| listener "tcp" { | |
| address = "%s" | |
| tls_disable = true | |
| } | |
| `, listenAddr) | |
| config := fmt.Sprintf(` | |
| vault { | |
| address = "%s" | |
| tls_skip_verify = true | |
| } | |
| %s | |
| `, serverClient.Address(), listenConfig) | |
| configPath := makeTempFile(t, "config.hcl", config) | |
| defer os.Remove(configPath) | |
| // Start the agent | |
| ui, cmd := testAgentCommand(t, logger) | |
| cmd.startedCh = make(chan struct{}) | |
| var output string | |
| var code int | |
| wg := &sync.WaitGroup{} | |
| wg.Add(1) | |
| go func() { | |
| if code = cmd.Run([]string{"-config", configPath}); code != 0 { | |
| output = ui.ErrorWriter.String() + ui.OutputWriter.String() | |
| } | |
| wg.Done() | |
| }() | |
| select { | |
| case <-cmd.startedCh: | |
| case <-time.After(5 * time.Second): | |
| t.Errorf("timeout") | |
| } | |
| // Reload | |
| cmd.SighupCh <- struct{}{} | |
| select { | |
| case <-cmd.reloadedCh: | |
| case <-time.After(5 * time.Second): | |
| t.Fatalf("timeout") | |
| } | |
| close(cmd.ShutdownCh) | |
| wg.Wait() | |
| if code != 0 { | |
| t.Fatalf("got a non-zero exit status: %d, stdout/stderr: %s", code, output) | |
| } | |
| } | |
| // Get a randomly assigned port and then free it again before returning it. | |
| // There is still a race when trying to use it, but should work better | |
| // than a static port. | |
| func generateListenerAddress(t *testing.T) string { | |
| t.Helper() | |
| ln1, err := net.Listen("tcp", "127.0.0.1:0") | |
| if err != nil { | |
| t.Fatal(err) | |
| } | |
| listenAddr := ln1.Addr().String() | |
| ln1.Close() | |
| return listenAddr | |
| } |