From b2468d3481400cca097e350c1da9c88eb22a119b Mon Sep 17 00:00:00 2001 From: Violet Hynes Date: Wed, 17 May 2023 09:38:34 -0400 Subject: [PATCH] VAULT-15547 First pass at agent/proxy decoupling (#20548) * VAULT-15547 First pass at agent/proxy decoupling * VAULT-15547 Fix some imports * VAULT-15547 cases instead of string.Title * VAULT-15547 changelog * VAULT-15547 Fix some imports * VAULT-15547 some more dependency updates * VAULT-15547 More dependency paths * VAULT-15547 godocs for tests * VAULT-15547 godocs for tests * VAULT-15547 test package updates * VAULT-15547 test packages * VAULT-15547 add proxy to test packages * VAULT-15547 gitignore * VAULT-15547 address comments * VAULT-15547 Some typos and small fixes --- .../scripts/generate-test-package-lists.sh | 23 +- changelog/20548.txt | 3 + command/agent.go | 70 +- command/agent/alicloud_end_to_end_test.go | 8 +- command/agent/approle_end_to_end_test.go | 8 +- ...auto_auth_preload_token_end_to_end_test.go | 8 +- command/agent/aws_end_to_end_test.go | 8 +- command/agent/cache_end_to_end_test.go | 19 +- command/agent/cert_end_to_end_test.go | 12 +- command/agent/cf_end_to_end_test.go | 8 +- command/agent/jwt_end_to_end_test.go | 8 +- command/agent/oci_end_to_end_test.go | 8 +- command/agent/token_file_end_to_end_test.go | 8 +- .../auth/alicloud/alicloud.go | 2 +- .../auth/approle/approle.go | 2 +- .../{agent => agentproxyshared}/auth/auth.go | 79 +- .../auth/auth_test.go | 4 +- .../auth/aws/aws.go | 2 +- .../auth/azure/azure.go | 2 +- .../auth/cert/cert.go | 2 +- .../auth/cert/cert_test.go | 2 +- .../auth/cert/test-fixtures/keys/cert.pem | 0 .../auth/cert/test-fixtures/keys/key.pem | 0 .../auth/cert/test-fixtures/keys/pkioutput | 0 .../auth/cert/test-fixtures/root/pkioutput | 0 .../auth/cert/test-fixtures/root/root.crl | 0 .../cert/test-fixtures/root/rootcacert.pem | 0 .../cert/test-fixtures/root/rootcakey.pem | 0 .../{agent => agentproxyshared}/auth/cf/cf.go | 2 +- .../auth/gcp/gcp.go | 2 +- .../auth/jwt/jwt.go | 2 +- .../auth/jwt/jwt_test.go | 2 +- .../kerberos/integtest/integrationtest.sh | 0 .../auth/kerberos/kerberos.go | 2 +- .../auth/kerberos/kerberos_test.go | 2 +- .../auth/kubernetes/kubernetes.go | 2 +- .../auth/kubernetes/kubernetes_test.go | 2 +- .../auth/oci/oci.go | 2 +- .../auth/token-file/token_file.go | 2 +- .../auth/token-file/token_file_test.go | 2 +- .../cache/api_proxy.go | 35 +- .../cache/api_proxy_test.go | 26 +- .../cache/cache_test.go | 4 +- .../cache/cacheboltdb/bolt.go | 0 .../cache/cacheboltdb/bolt_test.go | 2 +- .../cache/cachememdb/cache_memdb.go | 0 .../cache/cachememdb/cache_memdb_test.go | 0 .../cache/cachememdb/index.go | 0 .../cache/cachememdb/index_test.go | 0 .../cache/handler.go | 2 +- .../cache/keymanager/manager.go | 0 .../cache/keymanager/passthrough.go | 0 .../cache/keymanager/passthrough_test.go | 0 .../cache/lease_cache.go | 4 +- .../cache/lease_cache_test.go | 9 +- .../cache/listener.go | 0 .../cache/proxy.go | 0 .../cache/testing.go | 0 .../sink/file/file_sink.go | 2 +- .../sink/file/file_sink_test.go | 2 +- .../sink/file/sink_test.go | 2 +- .../sink/inmem/inmem_sink.go | 4 +- .../sink/mock/mock_sink.go | 2 +- .../{agent => agentproxyshared}/sink/sink.go | 0 .../winsvc/service.go | 0 .../winsvc/service_windows.go | 0 command/base.go | 34 +- command/commands.go | 9 + command/proxy.go | 1294 +++++++++++++++++ command/proxy/config/config.go | 840 +++++++++++ command/proxy/config/config_test.go | 118 ++ .../config-cache-embedded-type.hcl | 77 + .../config/test-fixtures/config-cache.hcl | 75 + command/proxy_test.go | 678 +++++++++ command/server/config_test_helpers.go | 6 + helper/useragent/useragent.go | 26 + internalshared/configutil/listener.go | 7 + sdk/helper/consts/proxy.go | 15 + 78 files changed, 3381 insertions(+), 200 deletions(-) create mode 100644 changelog/20548.txt rename command/{agent => agentproxyshared}/auth/alicloud/alicloud.go (99%) rename command/{agent => agentproxyshared}/auth/approle/approle.go (99%) rename command/{agent => agentproxyshared}/auth/auth.go (83%) rename command/{agent => agentproxyshared}/auth/auth_test.go (97%) rename command/{agent => agentproxyshared}/auth/aws/aws.go (99%) rename command/{agent => agentproxyshared}/auth/azure/azure.go (98%) rename command/{agent => agentproxyshared}/auth/cert/cert.go (98%) rename command/{agent => agentproxyshared}/auth/cert/cert_test.go (98%) rename command/{agent => agentproxyshared}/auth/cert/test-fixtures/keys/cert.pem (100%) rename command/{agent => agentproxyshared}/auth/cert/test-fixtures/keys/key.pem (100%) rename command/{agent => agentproxyshared}/auth/cert/test-fixtures/keys/pkioutput (100%) rename command/{agent => agentproxyshared}/auth/cert/test-fixtures/root/pkioutput (100%) rename command/{agent => agentproxyshared}/auth/cert/test-fixtures/root/root.crl (100%) rename command/{agent => agentproxyshared}/auth/cert/test-fixtures/root/rootcacert.pem (100%) rename command/{agent => agentproxyshared}/auth/cert/test-fixtures/root/rootcakey.pem (100%) rename command/{agent => agentproxyshared}/auth/cf/cf.go (97%) rename command/{agent => agentproxyshared}/auth/gcp/gcp.go (99%) rename command/{agent => agentproxyshared}/auth/jwt/jwt.go (99%) rename command/{agent => agentproxyshared}/auth/jwt/jwt_test.go (98%) rename command/{agent => agentproxyshared}/auth/kerberos/integtest/integrationtest.sh (100%) rename command/{agent => agentproxyshared}/auth/kerberos/kerberos.go (97%) rename command/{agent => agentproxyshared}/auth/kerberos/kerberos_test.go (97%) rename command/{agent => agentproxyshared}/auth/kubernetes/kubernetes.go (98%) rename command/{agent => agentproxyshared}/auth/kubernetes/kubernetes_test.go (98%) rename command/{agent => agentproxyshared}/auth/oci/oci.go (99%) rename command/{agent => agentproxyshared}/auth/token-file/token_file.go (97%) rename command/{agent => agentproxyshared}/auth/token-file/token_file_test.go (97%) rename command/{agent => agentproxyshared}/cache/api_proxy.go (81%) rename command/{agent => agentproxyshared}/cache/api_proxy_test.go (90%) rename command/{agent => agentproxyshared}/cache/cache_test.go (99%) rename command/{agent => agentproxyshared}/cache/cacheboltdb/bolt.go (100%) rename command/{agent => agentproxyshared}/cache/cacheboltdb/bolt_test.go (99%) rename command/{agent => agentproxyshared}/cache/cachememdb/cache_memdb.go (100%) rename command/{agent => agentproxyshared}/cache/cachememdb/cache_memdb_test.go (100%) rename command/{agent => agentproxyshared}/cache/cachememdb/index.go (100%) rename command/{agent => agentproxyshared}/cache/cachememdb/index_test.go (100%) rename command/{agent => agentproxyshared}/cache/handler.go (99%) rename command/{agent => agentproxyshared}/cache/keymanager/manager.go (100%) rename command/{agent => agentproxyshared}/cache/keymanager/passthrough.go (100%) rename command/{agent => agentproxyshared}/cache/keymanager/passthrough_test.go (100%) rename command/{agent => agentproxyshared}/cache/lease_cache.go (99%) rename command/{agent => agentproxyshared}/cache/lease_cache_test.go (99%) rename command/{agent => agentproxyshared}/cache/listener.go (100%) rename command/{agent => agentproxyshared}/cache/proxy.go (100%) rename command/{agent => agentproxyshared}/cache/testing.go (100%) rename command/{agent => agentproxyshared}/sink/file/file_sink.go (98%) rename command/{agent => agentproxyshared}/sink/file/file_sink_test.go (97%) rename command/{agent => agentproxyshared}/sink/file/sink_test.go (97%) rename command/{agent => agentproxyshared}/sink/inmem/inmem_sink.go (88%) rename command/{agent => agentproxyshared}/sink/mock/mock_sink.go (85%) rename command/{agent => agentproxyshared}/sink/sink.go (100%) rename command/{agent => agentproxyshared}/winsvc/service.go (100%) rename command/{agent => agentproxyshared}/winsvc/service_windows.go (100%) create mode 100644 command/proxy.go create mode 100644 command/proxy/config/config.go create mode 100644 command/proxy/config/config_test.go create mode 100644 command/proxy/config/test-fixtures/config-cache-embedded-type.hcl create mode 100644 command/proxy/config/test-fixtures/config-cache.hcl create mode 100644 command/proxy_test.go create mode 100644 sdk/helper/consts/proxy.go diff --git a/.github/scripts/generate-test-package-lists.sh b/.github/scripts/generate-test-package-lists.sh index 0a92518d1..73fd63202 100755 --- a/.github/scripts/generate-test-package-lists.sh +++ b/.github/scripts/generate-test-package-lists.sh @@ -27,7 +27,7 @@ fi # Total time: 1009 test_packages[3]+=" $base/builtin/credential/approle" -test_packages[3]+=" $base/command/agent/sink/file" +test_packages[3]+=" $base/command/agentproxyshared/sink/file" test_packages[3]+=" $base/command/agent/template" test_packages[3]+=" $base/helper/random" test_packages[3]+=" $base/helper/storagepacker" @@ -87,16 +87,17 @@ test_packages[6]+=" $base/builtin/audit/file" test_packages[6]+=" $base/builtin/credential/github" test_packages[6]+=" $base/builtin/credential/okta" test_packages[6]+=" $base/builtin/logical/database/dbplugin" -test_packages[6]+=" $base/command/agent/auth/cert" -test_packages[6]+=" $base/command/agent/auth/jwt" -test_packages[6]+=" $base/command/agent/auth/kerberos" -test_packages[6]+=" $base/command/agent/auth/kubernetes" -test_packages[6]+=" $base/command/agent/auth/token-file" -test_packages[6]+=" $base/command/agent/cache" -test_packages[6]+=" $base/command/agent/cache/cacheboltdb" -test_packages[6]+=" $base/command/agent/cache/cachememdb" -test_packages[6]+=" $base/command/agent/cache/keymanager" +test_packages[6]+=" $base/command/agentproxyshared/auth/cert" +test_packages[6]+=" $base/command/agentproxyshared/auth/jwt" +test_packages[6]+=" $base/command/agentproxyshared/auth/kerberos" +test_packages[6]+=" $base/command/agentproxyshared/auth/kubernetes" +test_packages[6]+=" $base/command/agentproxyshared/auth/token-file" +test_packages[6]+=" $base/command/agentproxyshared/cache" +test_packages[6]+=" $base/command/agentproxyshared/cache/cacheboltdb" +test_packages[6]+=" $base/command/agentproxyshared/cache/cachememdb" +test_packages[6]+=" $base/command/agentproxyshared/cache/keymanager" test_packages[6]+=" $base/command/agent/config" +test_packages[6]+=" $base/command/proxy/config" test_packages[6]+=" $base/command/config" test_packages[6]+=" $base/command/token" if [ "${ENTERPRISE:+x}" == "x" ] ; then @@ -199,7 +200,7 @@ test_packages[7]+=" $base/vault/quotas" # Total time: 779 test_packages[8]+=" $base/builtin/credential/aws/pkcs7" test_packages[8]+=" $base/builtin/logical/totp" -test_packages[8]+=" $base/command/agent/auth" +test_packages[8]+=" $base/command/agentproxyshared/auth" test_packages[8]+=" $base/physical/raft" test_packages[8]+=" $base/sdk/framework" test_packages[8]+=" $base/sdk/plugin" diff --git a/changelog/20548.txt b/changelog/20548.txt new file mode 100644 index 000000000..40b244366 --- /dev/null +++ b/changelog/20548.txt @@ -0,0 +1,3 @@ +```release-note:feature +proxy: Introduced Vault Proxy, a new subcommand of the Vault binary that can be invoked using `vault proxy -config=config.hcl`. It currently has the same feature set as Vault Agent's API proxy, but the two may diverge in the future. We plan to deprecate the API proxy functionality of Vault Agent in a future release. +``` diff --git a/command/agent.go b/command/agent.go index f4f9fb503..120c6f249 100644 --- a/command/agent.go +++ b/command/agent.go @@ -19,40 +19,37 @@ import ( "sync" "time" - token_file "github.com/hashicorp/vault/command/agent/auth/token-file" - - ctconfig "github.com/hashicorp/consul-template/config" - "github.com/hashicorp/go-multierror" - - "github.com/hashicorp/vault/command/agent/sink/inmem" - systemd "github.com/coreos/go-systemd/daemon" + ctconfig "github.com/hashicorp/consul-template/config" log "github.com/hashicorp/go-hclog" + "github.com/hashicorp/go-multierror" "github.com/hashicorp/go-secure-stdlib/gatedwriter" "github.com/hashicorp/go-secure-stdlib/parseutil" "github.com/hashicorp/go-secure-stdlib/reloadutil" "github.com/hashicorp/vault/api" - "github.com/hashicorp/vault/command/agent/auth" - "github.com/hashicorp/vault/command/agent/auth/alicloud" - "github.com/hashicorp/vault/command/agent/auth/approle" - "github.com/hashicorp/vault/command/agent/auth/aws" - "github.com/hashicorp/vault/command/agent/auth/azure" - "github.com/hashicorp/vault/command/agent/auth/cert" - "github.com/hashicorp/vault/command/agent/auth/cf" - "github.com/hashicorp/vault/command/agent/auth/gcp" - "github.com/hashicorp/vault/command/agent/auth/jwt" - "github.com/hashicorp/vault/command/agent/auth/kerberos" - "github.com/hashicorp/vault/command/agent/auth/kubernetes" - "github.com/hashicorp/vault/command/agent/auth/oci" - "github.com/hashicorp/vault/command/agent/cache" - "github.com/hashicorp/vault/command/agent/cache/cacheboltdb" - "github.com/hashicorp/vault/command/agent/cache/cachememdb" - "github.com/hashicorp/vault/command/agent/cache/keymanager" agentConfig "github.com/hashicorp/vault/command/agent/config" - "github.com/hashicorp/vault/command/agent/sink" - "github.com/hashicorp/vault/command/agent/sink/file" "github.com/hashicorp/vault/command/agent/template" - "github.com/hashicorp/vault/command/agent/winsvc" + "github.com/hashicorp/vault/command/agentproxyshared/auth" + "github.com/hashicorp/vault/command/agentproxyshared/auth/alicloud" + "github.com/hashicorp/vault/command/agentproxyshared/auth/approle" + "github.com/hashicorp/vault/command/agentproxyshared/auth/aws" + "github.com/hashicorp/vault/command/agentproxyshared/auth/azure" + "github.com/hashicorp/vault/command/agentproxyshared/auth/cert" + "github.com/hashicorp/vault/command/agentproxyshared/auth/cf" + "github.com/hashicorp/vault/command/agentproxyshared/auth/gcp" + "github.com/hashicorp/vault/command/agentproxyshared/auth/jwt" + "github.com/hashicorp/vault/command/agentproxyshared/auth/kerberos" + "github.com/hashicorp/vault/command/agentproxyshared/auth/kubernetes" + "github.com/hashicorp/vault/command/agentproxyshared/auth/oci" + token_file "github.com/hashicorp/vault/command/agentproxyshared/auth/token-file" + cache "github.com/hashicorp/vault/command/agentproxyshared/cache" + "github.com/hashicorp/vault/command/agentproxyshared/cache/cacheboltdb" + "github.com/hashicorp/vault/command/agentproxyshared/cache/cachememdb" + "github.com/hashicorp/vault/command/agentproxyshared/cache/keymanager" + "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/command/agentproxyshared/winsvc" "github.com/hashicorp/vault/helper/logging" "github.com/hashicorp/vault/helper/metricsutil" "github.com/hashicorp/vault/helper/useragent" @@ -65,6 +62,8 @@ import ( "github.com/mitchellh/cli" "github.com/oklog/run" "github.com/posener/complete" + "golang.org/x/text/cases" + "golang.org/x/text/language" "google.golang.org/grpc/test/bufconn" ) @@ -258,7 +257,7 @@ func (c *AgentCommand) Run(args []string) int { // Ignore any setting of Agent's address. This client is used by the Agent // to reach out to Vault. This should never loop back to agent. - c.flagAgentAddress = "" + c.flagAgentProxyAddress = "" client, err := c.Client() if err != nil { c.UI.Error(fmt.Sprintf( @@ -505,10 +504,12 @@ func (c *AgentCommand) Run(args []string) int { // The API proxy to be used, if listeners are configured apiProxy, err := cache.NewAPIProxy(&cache.APIProxyConfig{ - Client: proxyClient, - Logger: apiProxyLogger, - EnforceConsistency: enforceConsistency, - WhenInconsistentAction: whenInconsistent, + Client: proxyClient, + Logger: apiProxyLogger, + EnforceConsistency: enforceConsistency, + WhenInconsistentAction: whenInconsistent, + UserAgentStringFunction: useragent.AgentProxyStringWithProxiedUserAgent, + UserAgentString: useragent.AgentProxyString(), }) if err != nil { c.UI.Error(fmt.Sprintf("Error creating API proxy: %v", err)) @@ -873,6 +874,8 @@ func (c *AgentCommand) Run(args []string) int { EnableTemplateTokenCh: enableTokenCh, Token: previousToken, ExitOnError: config.AutoAuth.Method.ExitOnError, + UserAgent: useragent.AgentAutoAuthString(), + MetricsSignifier: "agent", }) ss := sink.NewSinkServer(&sink.SinkServerConfig{ @@ -949,12 +952,13 @@ func (c *AgentCommand) Run(args []string) int { // Server configuration output padding := 24 sort.Strings(infoKeys) + caser := cases.Title(language.English) c.UI.Output("==> Vault Agent configuration:\n") for _, k := range infoKeys { c.UI.Output(fmt.Sprintf( "%s%s: %s", strings.Repeat(" ", padding-len(k)), - strings.Title(k), + caser.String(k), info[k])) } c.UI.Output("") @@ -1200,7 +1204,7 @@ func (c *AgentCommand) handleMetrics() http.Handler { w.Header().Set("Content-Type", resp.Data[logical.HTTPContentType].(string)) switch v := resp.Data[logical.HTTPRawBody].(type) { case string: - w.WriteHeader((status)) + w.WriteHeader(status) w.Write([]byte(v)) case []byte: w.WriteHeader(status) diff --git a/command/agent/alicloud_end_to_end_test.go b/command/agent/alicloud_end_to_end_test.go index 969b06633..0f5cdfbe4 100644 --- a/command/agent/alicloud_end_to_end_test.go +++ b/command/agent/alicloud_end_to_end_test.go @@ -19,10 +19,10 @@ import ( uuid "github.com/hashicorp/go-uuid" vaultalicloud "github.com/hashicorp/vault-plugin-auth-alicloud" "github.com/hashicorp/vault/api" - "github.com/hashicorp/vault/command/agent/auth" - agentalicloud "github.com/hashicorp/vault/command/agent/auth/alicloud" - "github.com/hashicorp/vault/command/agent/sink" - "github.com/hashicorp/vault/command/agent/sink/file" + "github.com/hashicorp/vault/command/agentproxyshared/auth" + agentalicloud "github.com/hashicorp/vault/command/agentproxyshared/auth/alicloud" + "github.com/hashicorp/vault/command/agentproxyshared/sink" + "github.com/hashicorp/vault/command/agentproxyshared/sink/file" "github.com/hashicorp/vault/helper/testhelpers" vaulthttp "github.com/hashicorp/vault/http" "github.com/hashicorp/vault/sdk/helper/logging" diff --git a/command/agent/approle_end_to_end_test.go b/command/agent/approle_end_to_end_test.go index a0e51f0bb..515a13ec5 100644 --- a/command/agent/approle_end_to_end_test.go +++ b/command/agent/approle_end_to_end_test.go @@ -16,10 +16,10 @@ import ( log "github.com/hashicorp/go-hclog" "github.com/hashicorp/vault/api" credAppRole "github.com/hashicorp/vault/builtin/credential/approle" - "github.com/hashicorp/vault/command/agent/auth" - agentapprole "github.com/hashicorp/vault/command/agent/auth/approle" - "github.com/hashicorp/vault/command/agent/sink" - "github.com/hashicorp/vault/command/agent/sink/file" + "github.com/hashicorp/vault/command/agentproxyshared/auth" + agentapprole "github.com/hashicorp/vault/command/agentproxyshared/auth/approle" + "github.com/hashicorp/vault/command/agentproxyshared/sink" + "github.com/hashicorp/vault/command/agentproxyshared/sink/file" vaulthttp "github.com/hashicorp/vault/http" "github.com/hashicorp/vault/sdk/helper/logging" "github.com/hashicorp/vault/sdk/logical" diff --git a/command/agent/auto_auth_preload_token_end_to_end_test.go b/command/agent/auto_auth_preload_token_end_to_end_test.go index 2ad81d3e6..004e817da 100644 --- a/command/agent/auto_auth_preload_token_end_to_end_test.go +++ b/command/agent/auto_auth_preload_token_end_to_end_test.go @@ -13,10 +13,10 @@ import ( hclog "github.com/hashicorp/go-hclog" "github.com/hashicorp/vault/api" credAppRole "github.com/hashicorp/vault/builtin/credential/approle" - "github.com/hashicorp/vault/command/agent/auth" - agentAppRole "github.com/hashicorp/vault/command/agent/auth/approle" - "github.com/hashicorp/vault/command/agent/sink" - "github.com/hashicorp/vault/command/agent/sink/file" + "github.com/hashicorp/vault/command/agentproxyshared/auth" + agentAppRole "github.com/hashicorp/vault/command/agentproxyshared/auth/approle" + "github.com/hashicorp/vault/command/agentproxyshared/sink" + "github.com/hashicorp/vault/command/agentproxyshared/sink/file" vaulthttp "github.com/hashicorp/vault/http" "github.com/hashicorp/vault/sdk/helper/logging" "github.com/hashicorp/vault/sdk/logical" diff --git a/command/agent/aws_end_to_end_test.go b/command/agent/aws_end_to_end_test.go index 5b23461fa..08644bdc1 100644 --- a/command/agent/aws_end_to_end_test.go +++ b/command/agent/aws_end_to_end_test.go @@ -18,10 +18,10 @@ import ( uuid "github.com/hashicorp/go-uuid" "github.com/hashicorp/vault/api" vaultaws "github.com/hashicorp/vault/builtin/credential/aws" - "github.com/hashicorp/vault/command/agent/auth" - agentaws "github.com/hashicorp/vault/command/agent/auth/aws" - "github.com/hashicorp/vault/command/agent/sink" - "github.com/hashicorp/vault/command/agent/sink/file" + "github.com/hashicorp/vault/command/agentproxyshared/auth" + agentaws "github.com/hashicorp/vault/command/agentproxyshared/auth/aws" + "github.com/hashicorp/vault/command/agentproxyshared/sink" + "github.com/hashicorp/vault/command/agentproxyshared/sink/file" "github.com/hashicorp/vault/helper/testhelpers" vaulthttp "github.com/hashicorp/vault/http" "github.com/hashicorp/vault/sdk/helper/logging" diff --git a/command/agent/cache_end_to_end_test.go b/command/agent/cache_end_to_end_test.go index c3444ed58..3f8321cb2 100644 --- a/command/agent/cache_end_to_end_test.go +++ b/command/agent/cache_end_to_end_test.go @@ -17,12 +17,13 @@ import ( log "github.com/hashicorp/go-hclog" "github.com/hashicorp/vault/api" credAppRole "github.com/hashicorp/vault/builtin/credential/approle" - "github.com/hashicorp/vault/command/agent/auth" - agentapprole "github.com/hashicorp/vault/command/agent/auth/approle" - "github.com/hashicorp/vault/command/agent/cache" - "github.com/hashicorp/vault/command/agent/sink" - "github.com/hashicorp/vault/command/agent/sink/file" - "github.com/hashicorp/vault/command/agent/sink/inmem" + "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" @@ -166,8 +167,10 @@ func TestCache_UsingAutoAuthToken(t *testing.T) { // Create the API proxier apiProxy, err := cache.NewAPIProxy(&cache.APIProxyConfig{ - Client: client, - Logger: cacheLogger.Named("apiproxy"), + Client: client, + Logger: cacheLogger.Named("apiproxy"), + UserAgentStringFunction: useragent.ProxyStringWithProxiedUserAgent, + UserAgentString: useragent.ProxyString(), }) if err != nil { t.Fatal(err) diff --git a/command/agent/cert_end_to_end_test.go b/command/agent/cert_end_to_end_test.go index 9b2729e69..12ea933ed 100644 --- a/command/agent/cert_end_to_end_test.go +++ b/command/agent/cert_end_to_end_test.go @@ -12,16 +12,14 @@ import ( "testing" "time" - "github.com/hashicorp/vault/builtin/logical/pki" - hclog "github.com/hashicorp/go-hclog" - "github.com/hashicorp/vault/api" vaultcert "github.com/hashicorp/vault/builtin/credential/cert" - "github.com/hashicorp/vault/command/agent/auth" - agentcert "github.com/hashicorp/vault/command/agent/auth/cert" - "github.com/hashicorp/vault/command/agent/sink" - "github.com/hashicorp/vault/command/agent/sink/file" + "github.com/hashicorp/vault/builtin/logical/pki" + "github.com/hashicorp/vault/command/agentproxyshared/auth" + agentcert "github.com/hashicorp/vault/command/agentproxyshared/auth/cert" + "github.com/hashicorp/vault/command/agentproxyshared/sink" + "github.com/hashicorp/vault/command/agentproxyshared/sink/file" "github.com/hashicorp/vault/helper/dhutil" vaulthttp "github.com/hashicorp/vault/http" "github.com/hashicorp/vault/sdk/helper/jsonutil" diff --git a/command/agent/cf_end_to_end_test.go b/command/agent/cf_end_to_end_test.go index 3ccd3be61..e143223af 100644 --- a/command/agent/cf_end_to_end_test.go +++ b/command/agent/cf_end_to_end_test.go @@ -15,10 +15,10 @@ import ( "github.com/hashicorp/vault-plugin-auth-cf/testing/certificates" cfAPI "github.com/hashicorp/vault-plugin-auth-cf/testing/cf" "github.com/hashicorp/vault/api" - "github.com/hashicorp/vault/command/agent/auth" - agentcf "github.com/hashicorp/vault/command/agent/auth/cf" - "github.com/hashicorp/vault/command/agent/sink" - "github.com/hashicorp/vault/command/agent/sink/file" + "github.com/hashicorp/vault/command/agentproxyshared/auth" + agentcf "github.com/hashicorp/vault/command/agentproxyshared/auth/cf" + "github.com/hashicorp/vault/command/agentproxyshared/sink" + "github.com/hashicorp/vault/command/agentproxyshared/sink/file" vaulthttp "github.com/hashicorp/vault/http" "github.com/hashicorp/vault/sdk/helper/logging" "github.com/hashicorp/vault/sdk/logical" diff --git a/command/agent/jwt_end_to_end_test.go b/command/agent/jwt_end_to_end_test.go index 4136ac1eb..4739a65c3 100644 --- a/command/agent/jwt_end_to_end_test.go +++ b/command/agent/jwt_end_to_end_test.go @@ -14,10 +14,10 @@ import ( hclog "github.com/hashicorp/go-hclog" vaultjwt "github.com/hashicorp/vault-plugin-auth-jwt" "github.com/hashicorp/vault/api" - "github.com/hashicorp/vault/command/agent/auth" - agentjwt "github.com/hashicorp/vault/command/agent/auth/jwt" - "github.com/hashicorp/vault/command/agent/sink" - "github.com/hashicorp/vault/command/agent/sink/file" + "github.com/hashicorp/vault/command/agentproxyshared/auth" + agentjwt "github.com/hashicorp/vault/command/agentproxyshared/auth/jwt" + "github.com/hashicorp/vault/command/agentproxyshared/sink" + "github.com/hashicorp/vault/command/agentproxyshared/sink/file" "github.com/hashicorp/vault/helper/dhutil" vaulthttp "github.com/hashicorp/vault/http" "github.com/hashicorp/vault/sdk/helper/jsonutil" diff --git a/command/agent/oci_end_to_end_test.go b/command/agent/oci_end_to_end_test.go index 878239d43..2349f09ab 100644 --- a/command/agent/oci_end_to_end_test.go +++ b/command/agent/oci_end_to_end_test.go @@ -13,10 +13,10 @@ import ( hclog "github.com/hashicorp/go-hclog" vaultoci "github.com/hashicorp/vault-plugin-auth-oci" "github.com/hashicorp/vault/api" - "github.com/hashicorp/vault/command/agent/auth" - agentoci "github.com/hashicorp/vault/command/agent/auth/oci" - "github.com/hashicorp/vault/command/agent/sink" - "github.com/hashicorp/vault/command/agent/sink/file" + "github.com/hashicorp/vault/command/agentproxyshared/auth" + agentoci "github.com/hashicorp/vault/command/agentproxyshared/auth/oci" + "github.com/hashicorp/vault/command/agentproxyshared/sink" + "github.com/hashicorp/vault/command/agentproxyshared/sink/file" "github.com/hashicorp/vault/helper/testhelpers" vaulthttp "github.com/hashicorp/vault/http" "github.com/hashicorp/vault/sdk/helper/logging" diff --git a/command/agent/token_file_end_to_end_test.go b/command/agent/token_file_end_to_end_test.go index f774fc097..dc7115cb1 100644 --- a/command/agent/token_file_end_to_end_test.go +++ b/command/agent/token_file_end_to_end_test.go @@ -11,10 +11,10 @@ import ( "time" log "github.com/hashicorp/go-hclog" - "github.com/hashicorp/vault/command/agent/auth" - token_file "github.com/hashicorp/vault/command/agent/auth/token-file" - "github.com/hashicorp/vault/command/agent/sink" - "github.com/hashicorp/vault/command/agent/sink/file" + "github.com/hashicorp/vault/command/agentproxyshared/auth" + token_file "github.com/hashicorp/vault/command/agentproxyshared/auth/token-file" + "github.com/hashicorp/vault/command/agentproxyshared/sink" + "github.com/hashicorp/vault/command/agentproxyshared/sink/file" vaulthttp "github.com/hashicorp/vault/http" "github.com/hashicorp/vault/sdk/helper/logging" "github.com/hashicorp/vault/vault" diff --git a/command/agent/auth/alicloud/alicloud.go b/command/agentproxyshared/auth/alicloud/alicloud.go similarity index 99% rename from command/agent/auth/alicloud/alicloud.go rename to command/agentproxyshared/auth/alicloud/alicloud.go index 494dedb26..724597682 100644 --- a/command/agent/auth/alicloud/alicloud.go +++ b/command/agentproxyshared/auth/alicloud/alicloud.go @@ -17,7 +17,7 @@ import ( hclog "github.com/hashicorp/go-hclog" "github.com/hashicorp/vault-plugin-auth-alicloud/tools" "github.com/hashicorp/vault/api" - "github.com/hashicorp/vault/command/agent/auth" + "github.com/hashicorp/vault/command/agentproxyshared/auth" ) /* diff --git a/command/agent/auth/approle/approle.go b/command/agentproxyshared/auth/approle/approle.go similarity index 99% rename from command/agent/auth/approle/approle.go rename to command/agentproxyshared/auth/approle/approle.go index 889e7bd41..9f33980a9 100644 --- a/command/agent/auth/approle/approle.go +++ b/command/agentproxyshared/auth/approle/approle.go @@ -15,7 +15,7 @@ import ( hclog "github.com/hashicorp/go-hclog" "github.com/hashicorp/go-secure-stdlib/parseutil" "github.com/hashicorp/vault/api" - "github.com/hashicorp/vault/command/agent/auth" + "github.com/hashicorp/vault/command/agentproxyshared/auth" ) type approleMethod struct { diff --git a/command/agent/auth/auth.go b/command/agentproxyshared/auth/auth.go similarity index 83% rename from command/agent/auth/auth.go rename to command/agentproxyshared/auth/auth.go index 536d1be8b..776c678ea 100644 --- a/command/agent/auth/auth.go +++ b/command/agentproxyshared/auth/auth.go @@ -14,7 +14,6 @@ import ( "github.com/armon/go-metrics" "github.com/hashicorp/go-hclog" "github.com/hashicorp/vault/api" - "github.com/hashicorp/vault/helper/useragent" "github.com/hashicorp/vault/sdk/helper/jsonutil" ) @@ -23,7 +22,7 @@ const ( defaultMaxBackoff = 5 * time.Minute ) -// AuthMethod is the interface that auto-auth methods implement for the agent +// AuthMethod is the interface that auto-auth methods implement for the agent/proxy // to use. type AuthMethod interface { // Authenticate returns a mount path, header, request body, and error. @@ -54,6 +53,8 @@ type AuthHandler struct { OutputCh chan string TemplateTokenCh chan string token string + userAgent string + metricsSignifier string logger hclog.Logger client *api.Client random *rand.Rand @@ -66,12 +67,18 @@ type AuthHandler struct { } type AuthHandlerConfig struct { - Logger hclog.Logger - Client *api.Client - WrapTTL time.Duration - MaxBackoff time.Duration - MinBackoff time.Duration - Token string + Logger hclog.Logger + Client *api.Client + WrapTTL time.Duration + MaxBackoff time.Duration + MinBackoff time.Duration + Token string + // UserAgent is the HTTP UserAgent header auto-auth will use when + // communicating with Vault. + UserAgent string + // MetricsSignifier is the first argument we will give to + // metrics.IncrCounter, signifying what the name of the application is + MetricsSignifier string EnableReauthOnNewCredentials bool EnableTemplateTokenCh bool ExitOnError bool @@ -80,7 +87,7 @@ type AuthHandlerConfig struct { func NewAuthHandler(conf *AuthHandlerConfig) *AuthHandler { ah := &AuthHandler{ // This is buffered so that if we try to output after the sink server - // has been shut down, during agent shutdown, we won't block + // has been shut down, during agent/proxy shutdown, we won't block OutputCh: make(chan string, 1), TemplateTokenCh: make(chan string, 1), token: conf.Token, @@ -93,12 +100,14 @@ func NewAuthHandler(conf *AuthHandlerConfig) *AuthHandler { enableReauthOnNewCredentials: conf.EnableReauthOnNewCredentials, enableTemplateTokenCh: conf.EnableTemplateTokenCh, exitOnError: conf.ExitOnError, + userAgent: conf.UserAgent, + metricsSignifier: conf.MetricsSignifier, } return ah } -func backoff(ctx context.Context, backoff *agentBackoff) bool { +func backoff(ctx context.Context, backoff *autoAuthBackoff) bool { if backoff.exitOnErr { return false } @@ -123,7 +132,7 @@ func (ah *AuthHandler) Run(ctx context.Context, am AuthMethod) error { ah.minBackoff = defaultMinBackoff } - backoffCfg := newAgentBackoff(ah.minBackoff, ah.maxBackoff, ah.exitOnError) + backoffCfg := newAutoAuthBackoff(ah.minBackoff, ah.maxBackoff, ah.exitOnError) if backoffCfg.min >= backoffCfg.max { return errors.New("auth handler: min_backoff cannot be greater than max_backoff") @@ -162,7 +171,7 @@ func (ah *AuthHandler) Run(ctx context.Context, am AuthMethod) error { if headers == nil { headers = make(http.Header) } - headers.Set("User-Agent", useragent.AgentAutoAuthString()) + headers.Set("User-Agent", ah.userAgent) ah.client.SetHeaders(headers) } @@ -189,7 +198,7 @@ func (ah *AuthHandler) Run(ctx context.Context, am AuthMethod) error { clientToUse, err = am.(AuthMethodWithClient).AuthClient(ah.client) if err != nil { ah.logger.Error("error creating client for authentication call", "error", err, "backoff", backoff) - metrics.IncrCounter([]string{"agent", "auth", "failure"}, 1) + metrics.IncrCounter([]string{ah.metricsSignifier, "auth", "failure"}, 1) if backoff(ctx, backoffCfg) { continue @@ -216,7 +225,7 @@ func (ah *AuthHandler) Run(ctx context.Context, am AuthMethod) error { secret, err = clientToUse.Auth().Token().LookupSelfWithContext(ctx) if err != nil { ah.logger.Error("could not look up token", "err", err, "backoff", backoffCfg) - metrics.IncrCounter([]string{"agent", "auth", "failure"}, 1) + metrics.IncrCounter([]string{ah.metricsSignifier, "auth", "failure"}, 1) if backoff(ctx, backoffCfg) { continue @@ -236,7 +245,7 @@ func (ah *AuthHandler) Run(ctx context.Context, am AuthMethod) error { path, header, data, err = am.Authenticate(ctx, ah.client) if err != nil { ah.logger.Error("error getting path or data from method", "error", err, "backoff", backoffCfg) - metrics.IncrCounter([]string{"agent", "auth", "failure"}, 1) + metrics.IncrCounter([]string{ah.metricsSignifier, "auth", "failure"}, 1) if backoff(ctx, backoffCfg) { continue @@ -249,7 +258,7 @@ func (ah *AuthHandler) Run(ctx context.Context, am AuthMethod) error { wrapClient, err := clientToUse.Clone() if err != nil { ah.logger.Error("error creating client for wrapped call", "error", err, "backoff", backoffCfg) - metrics.IncrCounter([]string{"agent", "auth", "failure"}, 1) + metrics.IncrCounter([]string{ah.metricsSignifier, "auth", "failure"}, 1) if backoff(ctx, backoffCfg) { continue @@ -287,7 +296,7 @@ func (ah *AuthHandler) Run(ctx context.Context, am AuthMethod) error { // Check errors/sanity if err != nil { ah.logger.Error("error authenticating", "error", err, "backoff", backoffCfg) - metrics.IncrCounter([]string{"agent", "auth", "failure"}, 1) + metrics.IncrCounter([]string{ah.metricsSignifier, "auth", "failure"}, 1) if backoff(ctx, backoffCfg) { continue @@ -302,7 +311,7 @@ func (ah *AuthHandler) Run(ctx context.Context, am AuthMethod) error { case ah.wrapTTL > 0: if secret.WrapInfo == nil { ah.logger.Error("authentication returned nil wrap info", "backoff", backoffCfg) - metrics.IncrCounter([]string{"agent", "auth", "failure"}, 1) + metrics.IncrCounter([]string{ah.metricsSignifier, "auth", "failure"}, 1) if backoff(ctx, backoffCfg) { continue @@ -311,7 +320,7 @@ func (ah *AuthHandler) Run(ctx context.Context, am AuthMethod) error { } if secret.WrapInfo.Token == "" { ah.logger.Error("authentication returned empty wrapped client token", "backoff", backoffCfg) - metrics.IncrCounter([]string{"agent", "auth", "failure"}, 1) + metrics.IncrCounter([]string{ah.metricsSignifier, "auth", "failure"}, 1) if backoff(ctx, backoffCfg) { continue @@ -321,7 +330,7 @@ func (ah *AuthHandler) Run(ctx context.Context, am AuthMethod) error { wrappedResp, err := jsonutil.EncodeJSON(secret.WrapInfo) if err != nil { ah.logger.Error("failed to encode wrapinfo", "error", err, "backoff", backoffCfg) - metrics.IncrCounter([]string{"agent", "auth", "failure"}, 1) + metrics.IncrCounter([]string{ah.metricsSignifier, "auth", "failure"}, 1) if backoff(ctx, backoffCfg) { continue @@ -357,7 +366,7 @@ func (ah *AuthHandler) Run(ctx context.Context, am AuthMethod) error { // i.e. if the token is invalid, we will fail in the authentication step if secret == nil || secret.Data == nil { ah.logger.Error("token file validation failed, token may be invalid", "backoff", backoffCfg) - metrics.IncrCounter([]string{"agent", "auth", "failure"}, 1) + metrics.IncrCounter([]string{ah.metricsSignifier, "auth", "failure"}, 1) if backoff(ctx, backoffCfg) { continue @@ -367,7 +376,7 @@ func (ah *AuthHandler) Run(ctx context.Context, am AuthMethod) error { token, ok := secret.Data["id"].(string) if !ok || token == "" { ah.logger.Error("token file validation returned empty client token", "backoff", backoffCfg) - metrics.IncrCounter([]string{"agent", "auth", "failure"}, 1) + metrics.IncrCounter([]string{ah.metricsSignifier, "auth", "failure"}, 1) if backoff(ctx, backoffCfg) { continue @@ -396,7 +405,7 @@ func (ah *AuthHandler) Run(ctx context.Context, am AuthMethod) error { } else { if secret == nil || secret.Auth == nil { ah.logger.Error("authentication returned nil auth info", "backoff", backoffCfg) - metrics.IncrCounter([]string{"agent", "auth", "failure"}, 1) + metrics.IncrCounter([]string{ah.metricsSignifier, "auth", "failure"}, 1) if backoff(ctx, backoffCfg) { continue @@ -405,7 +414,7 @@ func (ah *AuthHandler) Run(ctx context.Context, am AuthMethod) error { } if secret.Auth.ClientToken == "" { ah.logger.Error("authentication returned empty client token", "backoff", backoffCfg) - metrics.IncrCounter([]string{"agent", "auth", "failure"}, 1) + metrics.IncrCounter([]string{ah.metricsSignifier, "auth", "failure"}, 1) if backoff(ctx, backoffCfg) { continue @@ -434,7 +443,7 @@ func (ah *AuthHandler) Run(ctx context.Context, am AuthMethod) error { }) if err != nil { ah.logger.Error("error creating lifetime watcher", "error", err, "backoff", backoffCfg) - metrics.IncrCounter([]string{"agent", "auth", "failure"}, 1) + metrics.IncrCounter([]string{ah.metricsSignifier, "auth", "failure"}, 1) if backoff(ctx, backoffCfg) { continue @@ -442,7 +451,7 @@ func (ah *AuthHandler) Run(ctx context.Context, am AuthMethod) error { return err } - metrics.IncrCounter([]string{"agent", "auth", "success"}, 1) + metrics.IncrCounter([]string{ah.metricsSignifier, "auth", "success"}, 1) // We don't want to trigger the renewal process for tokens with // unlimited TTL, such as the root token. if leaseDuration == 0 && isTokenFileMethod { @@ -463,13 +472,13 @@ func (ah *AuthHandler) Run(ctx context.Context, am AuthMethod) error { case err := <-watcher.DoneCh(): ah.logger.Info("lifetime watcher done channel triggered") if err != nil { - metrics.IncrCounter([]string{"agent", "auth", "failure"}, 1) + metrics.IncrCounter([]string{ah.metricsSignifier, "auth", "failure"}, 1) ah.logger.Error("error renewing token", "error", err) } break LifetimeWatcherLoop case <-watcher.RenewCh(): - metrics.IncrCounter([]string{"agent", "auth", "success"}, 1) + metrics.IncrCounter([]string{ah.metricsSignifier, "auth", "success"}, 1) ah.logger.Info("renewed auth token") case <-credCh: @@ -480,15 +489,15 @@ func (ah *AuthHandler) Run(ctx context.Context, am AuthMethod) error { } } -// agentBackoff tracks exponential backoff state. -type agentBackoff struct { +// autoAuthBackoff tracks exponential backoff state. +type autoAuthBackoff struct { min time.Duration max time.Duration current time.Duration exitOnErr bool } -func newAgentBackoff(min, max time.Duration, exitErr bool) *agentBackoff { +func newAutoAuthBackoff(min, max time.Duration, exitErr bool) *autoAuthBackoff { if max <= 0 { max = defaultMaxBackoff } @@ -497,7 +506,7 @@ func newAgentBackoff(min, max time.Duration, exitErr bool) *agentBackoff { min = defaultMinBackoff } - return &agentBackoff{ + return &autoAuthBackoff{ current: min, max: max, min: min, @@ -507,7 +516,7 @@ func newAgentBackoff(min, max time.Duration, exitErr bool) *agentBackoff { // next determines the next backoff duration that is roughly twice // the current value, capped to a max value, with a measure of randomness. -func (b *agentBackoff) next() { +func (b *autoAuthBackoff) next() { maxBackoff := 2 * b.current if maxBackoff > b.max { @@ -519,10 +528,10 @@ func (b *agentBackoff) next() { b.current = maxBackoff - time.Duration(trim) } -func (b *agentBackoff) reset() { +func (b *autoAuthBackoff) reset() { b.current = b.min } -func (b agentBackoff) String() string { +func (b autoAuthBackoff) String() string { return b.current.Truncate(10 * time.Millisecond).String() } diff --git a/command/agent/auth/auth_test.go b/command/agentproxyshared/auth/auth_test.go similarity index 97% rename from command/agent/auth/auth_test.go rename to command/agentproxyshared/auth/auth_test.go index 442541675..572943502 100644 --- a/command/agent/auth/auth_test.go +++ b/command/agentproxyshared/auth/auth_test.go @@ -112,7 +112,7 @@ consumption: func TestAgentBackoff(t *testing.T) { max := 1024 * time.Second - backoff := newAgentBackoff(defaultMinBackoff, max, false) + backoff := newAutoAuthBackoff(defaultMinBackoff, max, false) // Test initial value if backoff.current != defaultMinBackoff { @@ -162,7 +162,7 @@ func TestAgentMinBackoffCustom(t *testing.T) { for _, test := range tests { max := 1024 * time.Second - backoff := newAgentBackoff(test.minBackoff, max, false) + backoff := newAutoAuthBackoff(test.minBackoff, max, false) // Test initial value if backoff.current != test.want { diff --git a/command/agent/auth/aws/aws.go b/command/agentproxyshared/auth/aws/aws.go similarity index 99% rename from command/agent/auth/aws/aws.go rename to command/agentproxyshared/auth/aws/aws.go index b45192d8b..53d1623be 100644 --- a/command/agent/auth/aws/aws.go +++ b/command/agentproxyshared/auth/aws/aws.go @@ -20,7 +20,7 @@ import ( "github.com/hashicorp/go-secure-stdlib/awsutil" "github.com/hashicorp/go-uuid" "github.com/hashicorp/vault/api" - "github.com/hashicorp/vault/command/agent/auth" + "github.com/hashicorp/vault/command/agentproxyshared/auth" ) const ( diff --git a/command/agent/auth/azure/azure.go b/command/agentproxyshared/auth/azure/azure.go similarity index 98% rename from command/agent/auth/azure/azure.go rename to command/agentproxyshared/auth/azure/azure.go index d4689f0d5..77e661397 100644 --- a/command/agent/auth/azure/azure.go +++ b/command/agentproxyshared/auth/azure/azure.go @@ -13,7 +13,7 @@ import ( cleanhttp "github.com/hashicorp/go-cleanhttp" hclog "github.com/hashicorp/go-hclog" "github.com/hashicorp/vault/api" - "github.com/hashicorp/vault/command/agent/auth" + "github.com/hashicorp/vault/command/agentproxyshared/auth" "github.com/hashicorp/vault/helper/useragent" "github.com/hashicorp/vault/sdk/helper/jsonutil" ) diff --git a/command/agent/auth/cert/cert.go b/command/agentproxyshared/auth/cert/cert.go similarity index 98% rename from command/agent/auth/cert/cert.go rename to command/agentproxyshared/auth/cert/cert.go index ebcfca173..5270dcb1b 100644 --- a/command/agent/auth/cert/cert.go +++ b/command/agentproxyshared/auth/cert/cert.go @@ -11,7 +11,7 @@ import ( "github.com/hashicorp/go-hclog" "github.com/hashicorp/vault/api" - "github.com/hashicorp/vault/command/agent/auth" + "github.com/hashicorp/vault/command/agentproxyshared/auth" "github.com/hashicorp/vault/sdk/helper/consts" ) diff --git a/command/agent/auth/cert/cert_test.go b/command/agentproxyshared/auth/cert/cert_test.go similarity index 98% rename from command/agent/auth/cert/cert_test.go rename to command/agentproxyshared/auth/cert/cert_test.go index 005da3eaf..43a5f83f4 100644 --- a/command/agent/auth/cert/cert_test.go +++ b/command/agentproxyshared/auth/cert/cert_test.go @@ -12,7 +12,7 @@ import ( "github.com/hashicorp/go-hclog" "github.com/hashicorp/vault/api" - "github.com/hashicorp/vault/command/agent/auth" + "github.com/hashicorp/vault/command/agentproxyshared/auth" ) func TestCertAuthMethod_Authenticate(t *testing.T) { diff --git a/command/agent/auth/cert/test-fixtures/keys/cert.pem b/command/agentproxyshared/auth/cert/test-fixtures/keys/cert.pem similarity index 100% rename from command/agent/auth/cert/test-fixtures/keys/cert.pem rename to command/agentproxyshared/auth/cert/test-fixtures/keys/cert.pem diff --git a/command/agent/auth/cert/test-fixtures/keys/key.pem b/command/agentproxyshared/auth/cert/test-fixtures/keys/key.pem similarity index 100% rename from command/agent/auth/cert/test-fixtures/keys/key.pem rename to command/agentproxyshared/auth/cert/test-fixtures/keys/key.pem diff --git a/command/agent/auth/cert/test-fixtures/keys/pkioutput b/command/agentproxyshared/auth/cert/test-fixtures/keys/pkioutput similarity index 100% rename from command/agent/auth/cert/test-fixtures/keys/pkioutput rename to command/agentproxyshared/auth/cert/test-fixtures/keys/pkioutput diff --git a/command/agent/auth/cert/test-fixtures/root/pkioutput b/command/agentproxyshared/auth/cert/test-fixtures/root/pkioutput similarity index 100% rename from command/agent/auth/cert/test-fixtures/root/pkioutput rename to command/agentproxyshared/auth/cert/test-fixtures/root/pkioutput diff --git a/command/agent/auth/cert/test-fixtures/root/root.crl b/command/agentproxyshared/auth/cert/test-fixtures/root/root.crl similarity index 100% rename from command/agent/auth/cert/test-fixtures/root/root.crl rename to command/agentproxyshared/auth/cert/test-fixtures/root/root.crl diff --git a/command/agent/auth/cert/test-fixtures/root/rootcacert.pem b/command/agentproxyshared/auth/cert/test-fixtures/root/rootcacert.pem similarity index 100% rename from command/agent/auth/cert/test-fixtures/root/rootcacert.pem rename to command/agentproxyshared/auth/cert/test-fixtures/root/rootcacert.pem diff --git a/command/agent/auth/cert/test-fixtures/root/rootcakey.pem b/command/agentproxyshared/auth/cert/test-fixtures/root/rootcakey.pem similarity index 100% rename from command/agent/auth/cert/test-fixtures/root/rootcakey.pem rename to command/agentproxyshared/auth/cert/test-fixtures/root/rootcakey.pem diff --git a/command/agent/auth/cf/cf.go b/command/agentproxyshared/auth/cf/cf.go similarity index 97% rename from command/agent/auth/cf/cf.go rename to command/agentproxyshared/auth/cf/cf.go index 90ae802e3..3ee2077fb 100644 --- a/command/agent/auth/cf/cf.go +++ b/command/agentproxyshared/auth/cf/cf.go @@ -15,7 +15,7 @@ import ( cf "github.com/hashicorp/vault-plugin-auth-cf" "github.com/hashicorp/vault-plugin-auth-cf/signatures" "github.com/hashicorp/vault/api" - "github.com/hashicorp/vault/command/agent/auth" + "github.com/hashicorp/vault/command/agentproxyshared/auth" ) type cfMethod struct { diff --git a/command/agent/auth/gcp/gcp.go b/command/agentproxyshared/auth/gcp/gcp.go similarity index 99% rename from command/agent/auth/gcp/gcp.go rename to command/agentproxyshared/auth/gcp/gcp.go index 145589b78..bb7c6bab6 100644 --- a/command/agent/auth/gcp/gcp.go +++ b/command/agentproxyshared/auth/gcp/gcp.go @@ -17,7 +17,7 @@ import ( hclog "github.com/hashicorp/go-hclog" "github.com/hashicorp/go-secure-stdlib/parseutil" "github.com/hashicorp/vault/api" - "github.com/hashicorp/vault/command/agent/auth" + "github.com/hashicorp/vault/command/agentproxyshared/auth" "golang.org/x/oauth2" "google.golang.org/api/iamcredentials/v1" ) diff --git a/command/agent/auth/jwt/jwt.go b/command/agentproxyshared/auth/jwt/jwt.go similarity index 99% rename from command/agent/auth/jwt/jwt.go rename to command/agentproxyshared/auth/jwt/jwt.go index 50ffe1055..fa8782743 100644 --- a/command/agent/auth/jwt/jwt.go +++ b/command/agentproxyshared/auth/jwt/jwt.go @@ -17,7 +17,7 @@ import ( hclog "github.com/hashicorp/go-hclog" "github.com/hashicorp/vault/api" - "github.com/hashicorp/vault/command/agent/auth" + "github.com/hashicorp/vault/command/agentproxyshared/auth" "github.com/hashicorp/vault/sdk/helper/parseutil" ) diff --git a/command/agent/auth/jwt/jwt_test.go b/command/agentproxyshared/auth/jwt/jwt_test.go similarity index 98% rename from command/agent/auth/jwt/jwt_test.go rename to command/agentproxyshared/auth/jwt/jwt_test.go index 2fa21f0ab..3e0db4090 100644 --- a/command/agent/auth/jwt/jwt_test.go +++ b/command/agentproxyshared/auth/jwt/jwt_test.go @@ -12,7 +12,7 @@ import ( "testing" "github.com/hashicorp/go-hclog" - "github.com/hashicorp/vault/command/agent/auth" + "github.com/hashicorp/vault/command/agentproxyshared/auth" ) func TestIngressToken(t *testing.T) { diff --git a/command/agent/auth/kerberos/integtest/integrationtest.sh b/command/agentproxyshared/auth/kerberos/integtest/integrationtest.sh similarity index 100% rename from command/agent/auth/kerberos/integtest/integrationtest.sh rename to command/agentproxyshared/auth/kerberos/integtest/integrationtest.sh diff --git a/command/agent/auth/kerberos/kerberos.go b/command/agentproxyshared/auth/kerberos/kerberos.go similarity index 97% rename from command/agent/auth/kerberos/kerberos.go rename to command/agentproxyshared/auth/kerberos/kerberos.go index 31ab6c67d..67a3109f5 100644 --- a/command/agent/auth/kerberos/kerberos.go +++ b/command/agentproxyshared/auth/kerberos/kerberos.go @@ -13,7 +13,7 @@ import ( "github.com/hashicorp/go-secure-stdlib/parseutil" kerberos "github.com/hashicorp/vault-plugin-auth-kerberos" "github.com/hashicorp/vault/api" - "github.com/hashicorp/vault/command/agent/auth" + "github.com/hashicorp/vault/command/agentproxyshared/auth" "github.com/jcmturner/gokrb5/v8/spnego" ) diff --git a/command/agent/auth/kerberos/kerberos_test.go b/command/agentproxyshared/auth/kerberos/kerberos_test.go similarity index 97% rename from command/agent/auth/kerberos/kerberos_test.go rename to command/agentproxyshared/auth/kerberos/kerberos_test.go index 25ccccdfd..070893d75 100644 --- a/command/agent/auth/kerberos/kerberos_test.go +++ b/command/agentproxyshared/auth/kerberos/kerberos_test.go @@ -7,7 +7,7 @@ import ( "testing" "github.com/hashicorp/go-hclog" - "github.com/hashicorp/vault/command/agent/auth" + "github.com/hashicorp/vault/command/agentproxyshared/auth" ) func TestNewKerberosAuthMethod(t *testing.T) { diff --git a/command/agent/auth/kubernetes/kubernetes.go b/command/agentproxyshared/auth/kubernetes/kubernetes.go similarity index 98% rename from command/agent/auth/kubernetes/kubernetes.go rename to command/agentproxyshared/auth/kubernetes/kubernetes.go index 80bacd3a6..acbb8c044 100644 --- a/command/agent/auth/kubernetes/kubernetes.go +++ b/command/agentproxyshared/auth/kubernetes/kubernetes.go @@ -15,7 +15,7 @@ import ( hclog "github.com/hashicorp/go-hclog" "github.com/hashicorp/vault/api" - "github.com/hashicorp/vault/command/agent/auth" + "github.com/hashicorp/vault/command/agentproxyshared/auth" ) const ( diff --git a/command/agent/auth/kubernetes/kubernetes_test.go b/command/agentproxyshared/auth/kubernetes/kubernetes_test.go similarity index 98% rename from command/agent/auth/kubernetes/kubernetes_test.go rename to command/agentproxyshared/auth/kubernetes/kubernetes_test.go index d95c71bf7..cbf617029 100644 --- a/command/agent/auth/kubernetes/kubernetes_test.go +++ b/command/agentproxyshared/auth/kubernetes/kubernetes_test.go @@ -12,7 +12,7 @@ import ( "github.com/hashicorp/errwrap" hclog "github.com/hashicorp/go-hclog" - "github.com/hashicorp/vault/command/agent/auth" + "github.com/hashicorp/vault/command/agentproxyshared/auth" "github.com/hashicorp/vault/sdk/helper/logging" ) diff --git a/command/agent/auth/oci/oci.go b/command/agentproxyshared/auth/oci/oci.go similarity index 99% rename from command/agent/auth/oci/oci.go rename to command/agentproxyshared/auth/oci/oci.go index 4ce62ade3..ec4d9ebf7 100644 --- a/command/agent/auth/oci/oci.go +++ b/command/agentproxyshared/auth/oci/oci.go @@ -18,7 +18,7 @@ import ( "github.com/hashicorp/go-hclog" "github.com/hashicorp/go-secure-stdlib/parseutil" "github.com/hashicorp/vault/api" - "github.com/hashicorp/vault/command/agent/auth" + "github.com/hashicorp/vault/command/agentproxyshared/auth" "github.com/oracle/oci-go-sdk/common" ociAuth "github.com/oracle/oci-go-sdk/common/auth" ) diff --git a/command/agent/auth/token-file/token_file.go b/command/agentproxyshared/auth/token-file/token_file.go similarity index 97% rename from command/agent/auth/token-file/token_file.go rename to command/agentproxyshared/auth/token-file/token_file.go index c37a0866e..4c7eaa22a 100644 --- a/command/agent/auth/token-file/token_file.go +++ b/command/agentproxyshared/auth/token-file/token_file.go @@ -13,7 +13,7 @@ import ( "github.com/hashicorp/go-hclog" "github.com/hashicorp/vault/api" - "github.com/hashicorp/vault/command/agent/auth" + "github.com/hashicorp/vault/command/agentproxyshared/auth" ) type tokenFileMethod struct { diff --git a/command/agent/auth/token-file/token_file_test.go b/command/agentproxyshared/auth/token-file/token_file_test.go similarity index 97% rename from command/agent/auth/token-file/token_file_test.go rename to command/agentproxyshared/auth/token-file/token_file_test.go index 8932beb75..eb89fc023 100644 --- a/command/agent/auth/token-file/token_file_test.go +++ b/command/agentproxyshared/auth/token-file/token_file_test.go @@ -9,7 +9,7 @@ import ( "testing" log "github.com/hashicorp/go-hclog" - "github.com/hashicorp/vault/command/agent/auth" + "github.com/hashicorp/vault/command/agentproxyshared/auth" "github.com/hashicorp/vault/sdk/helper/logging" ) diff --git a/command/agent/cache/api_proxy.go b/command/agentproxyshared/cache/api_proxy.go similarity index 81% rename from command/agent/cache/api_proxy.go rename to command/agentproxyshared/cache/api_proxy.go index bebcb7a0d..e03bc1570 100644 --- a/command/agent/cache/api_proxy.go +++ b/command/agentproxyshared/cache/api_proxy.go @@ -12,7 +12,6 @@ import ( hclog "github.com/hashicorp/go-hclog" "github.com/hashicorp/go-retryablehttp" "github.com/hashicorp/vault/api" - "github.com/hashicorp/vault/helper/useragent" "github.com/hashicorp/vault/http" ) @@ -34,12 +33,14 @@ const ( // APIProxy is an implementation of the proxier interface that is used to // forward the request to Vault and get the response. type APIProxy struct { - client *api.Client - logger hclog.Logger - enforceConsistency EnforceConsistency - whenInconsistentAction WhenInconsistentAction - l sync.RWMutex - lastIndexStates []string + client *api.Client + logger hclog.Logger + enforceConsistency EnforceConsistency + whenInconsistentAction WhenInconsistentAction + l sync.RWMutex + lastIndexStates []string + userAgentString string + userAgentStringFunction func(string) string } var _ Proxier = &APIProxy{} @@ -49,6 +50,12 @@ type APIProxyConfig struct { Logger hclog.Logger EnforceConsistency EnforceConsistency WhenInconsistentAction WhenInconsistentAction + // UserAgentString is used as the User Agent when the proxied client + // does not have a user agent of its own. + UserAgentString string + // UserAgentStringFunction is the function to transform the proxied client's + // user agent into one that includes Vault-specific information. + UserAgentStringFunction func(string) string } func NewAPIProxy(config *APIProxyConfig) (Proxier, error) { @@ -56,10 +63,12 @@ func NewAPIProxy(config *APIProxyConfig) (Proxier, error) { return nil, fmt.Errorf("nil API client") } return &APIProxy{ - client: config.Client, - logger: config.Logger, - enforceConsistency: config.EnforceConsistency, - whenInconsistentAction: config.WhenInconsistentAction, + client: config.Client, + logger: config.Logger, + enforceConsistency: config.EnforceConsistency, + whenInconsistentAction: config.WhenInconsistentAction, + userAgentString: config.UserAgentString, + userAgentStringFunction: config.UserAgentStringFunction, }, nil } @@ -87,9 +96,9 @@ func (ap *APIProxy) Send(ctx context.Context, req *SendRequest) (*SendResponse, // If the sending client had one, preserve it. if req.Request.Header.Get("User-Agent") != "" { initialUserAgent := req.Request.Header.Get("User-Agent") - req.Request.Header.Set("User-Agent", useragent.AgentProxyStringWithProxiedUserAgent(initialUserAgent)) + req.Request.Header.Set("User-Agent", ap.userAgentStringFunction(initialUserAgent)) } else { - req.Request.Header.Set("User-Agent", useragent.AgentProxyString()) + req.Request.Header.Set("User-Agent", ap.userAgentString) } client.SetHeaders(req.Request.Header) diff --git a/command/agent/cache/api_proxy_test.go b/command/agentproxyshared/cache/api_proxy_test.go similarity index 90% rename from command/agent/cache/api_proxy_test.go rename to command/agentproxyshared/cache/api_proxy_test.go index 4efc21a76..30e64168a 100644 --- a/command/agent/cache/api_proxy_test.go +++ b/command/agentproxyshared/cache/api_proxy_test.go @@ -12,6 +12,8 @@ import ( "testing" "time" + "github.com/hashicorp/vault/helper/useragent" + "github.com/hashicorp/vault/builtin/credential/userpass" vaulthttp "github.com/hashicorp/vault/http" "github.com/hashicorp/vault/sdk/logical" @@ -35,8 +37,10 @@ func TestAPIProxy(t *testing.T) { defer cleanup() proxier, err := NewAPIProxy(&APIProxyConfig{ - Client: client, - Logger: logging.NewVaultLogger(hclog.Trace), + Client: client, + Logger: logging.NewVaultLogger(hclog.Trace), + UserAgentStringFunction: useragent.ProxyStringWithProxiedUserAgent, + UserAgentString: useragent.ProxyString(), }) if err != nil { t.Fatal(err) @@ -71,8 +75,10 @@ func TestAPIProxyNoCache(t *testing.T) { defer cleanup() proxier, err := NewAPIProxy(&APIProxyConfig{ - Client: client, - Logger: logging.NewVaultLogger(hclog.Trace), + Client: client, + Logger: logging.NewVaultLogger(hclog.Trace), + UserAgentStringFunction: useragent.ProxyStringWithProxiedUserAgent, + UserAgentString: useragent.ProxyString(), }) if err != nil { t.Fatal(err) @@ -109,8 +115,10 @@ func TestAPIProxy_queryParams(t *testing.T) { defer cleanup() proxier, err := NewAPIProxy(&APIProxyConfig{ - Client: client, - Logger: logging.NewVaultLogger(hclog.Trace), + Client: client, + Logger: logging.NewVaultLogger(hclog.Trace), + UserAgentStringFunction: useragent.ProxyStringWithProxiedUserAgent, + UserAgentString: useragent.ProxyString(), }) if err != nil { t.Fatal(err) @@ -253,8 +261,10 @@ func setupClusterAndAgentCommon(ctx context.Context, t *testing.T, coreConfig *v // Create the API proxier apiProxy, err := NewAPIProxy(&APIProxyConfig{ - Client: clienToUse, - Logger: apiProxyLogger, + Client: clienToUse, + Logger: apiProxyLogger, + UserAgentStringFunction: useragent.ProxyStringWithProxiedUserAgent, + UserAgentString: useragent.ProxyString(), }) if err != nil { t.Fatal(err) diff --git a/command/agent/cache/cache_test.go b/command/agentproxyshared/cache/cache_test.go similarity index 99% rename from command/agent/cache/cache_test.go rename to command/agentproxyshared/cache/cache_test.go index 19671b90b..4786950bd 100644 --- a/command/agent/cache/cache_test.go +++ b/command/agentproxyshared/cache/cache_test.go @@ -19,8 +19,8 @@ import ( "github.com/hashicorp/go-hclog" kv "github.com/hashicorp/vault-plugin-secrets-kv" "github.com/hashicorp/vault/api" - "github.com/hashicorp/vault/command/agent/cache/cachememdb" - "github.com/hashicorp/vault/command/agent/sink/mock" + "github.com/hashicorp/vault/command/agentproxyshared/cache/cachememdb" + "github.com/hashicorp/vault/command/agentproxyshared/sink/mock" "github.com/hashicorp/vault/helper/namespace" vaulthttp "github.com/hashicorp/vault/http" "github.com/hashicorp/vault/sdk/helper/consts" diff --git a/command/agent/cache/cacheboltdb/bolt.go b/command/agentproxyshared/cache/cacheboltdb/bolt.go similarity index 100% rename from command/agent/cache/cacheboltdb/bolt.go rename to command/agentproxyshared/cache/cacheboltdb/bolt.go diff --git a/command/agent/cache/cacheboltdb/bolt_test.go b/command/agentproxyshared/cache/cacheboltdb/bolt_test.go similarity index 99% rename from command/agent/cache/cacheboltdb/bolt_test.go rename to command/agentproxyshared/cache/cacheboltdb/bolt_test.go index c5c057d41..95aacd27c 100644 --- a/command/agent/cache/cacheboltdb/bolt_test.go +++ b/command/agentproxyshared/cache/cacheboltdb/bolt_test.go @@ -16,7 +16,7 @@ import ( "github.com/golang/protobuf/proto" "github.com/hashicorp/go-hclog" - "github.com/hashicorp/vault/command/agent/cache/keymanager" + "github.com/hashicorp/vault/command/agentproxyshared/cache/keymanager" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" bolt "go.etcd.io/bbolt" diff --git a/command/agent/cache/cachememdb/cache_memdb.go b/command/agentproxyshared/cache/cachememdb/cache_memdb.go similarity index 100% rename from command/agent/cache/cachememdb/cache_memdb.go rename to command/agentproxyshared/cache/cachememdb/cache_memdb.go diff --git a/command/agent/cache/cachememdb/cache_memdb_test.go b/command/agentproxyshared/cache/cachememdb/cache_memdb_test.go similarity index 100% rename from command/agent/cache/cachememdb/cache_memdb_test.go rename to command/agentproxyshared/cache/cachememdb/cache_memdb_test.go diff --git a/command/agent/cache/cachememdb/index.go b/command/agentproxyshared/cache/cachememdb/index.go similarity index 100% rename from command/agent/cache/cachememdb/index.go rename to command/agentproxyshared/cache/cachememdb/index.go diff --git a/command/agent/cache/cachememdb/index_test.go b/command/agentproxyshared/cache/cachememdb/index_test.go similarity index 100% rename from command/agent/cache/cachememdb/index_test.go rename to command/agentproxyshared/cache/cachememdb/index_test.go diff --git a/command/agent/cache/handler.go b/command/agentproxyshared/cache/handler.go similarity index 99% rename from command/agent/cache/handler.go rename to command/agentproxyshared/cache/handler.go index a1f0eda04..bfb4434dc 100644 --- a/command/agent/cache/handler.go +++ b/command/agentproxyshared/cache/handler.go @@ -18,7 +18,7 @@ import ( "github.com/armon/go-metrics" "github.com/hashicorp/go-hclog" "github.com/hashicorp/vault/api" - "github.com/hashicorp/vault/command/agent/sink" + "github.com/hashicorp/vault/command/agentproxyshared/sink" "github.com/hashicorp/vault/sdk/helper/consts" "github.com/hashicorp/vault/sdk/logical" ) diff --git a/command/agent/cache/keymanager/manager.go b/command/agentproxyshared/cache/keymanager/manager.go similarity index 100% rename from command/agent/cache/keymanager/manager.go rename to command/agentproxyshared/cache/keymanager/manager.go diff --git a/command/agent/cache/keymanager/passthrough.go b/command/agentproxyshared/cache/keymanager/passthrough.go similarity index 100% rename from command/agent/cache/keymanager/passthrough.go rename to command/agentproxyshared/cache/keymanager/passthrough.go diff --git a/command/agent/cache/keymanager/passthrough_test.go b/command/agentproxyshared/cache/keymanager/passthrough_test.go similarity index 100% rename from command/agent/cache/keymanager/passthrough_test.go rename to command/agentproxyshared/cache/keymanager/passthrough_test.go diff --git a/command/agent/cache/lease_cache.go b/command/agentproxyshared/cache/lease_cache.go similarity index 99% rename from command/agent/cache/lease_cache.go rename to command/agentproxyshared/cache/lease_cache.go index feb526af5..fef612471 100644 --- a/command/agent/cache/lease_cache.go +++ b/command/agentproxyshared/cache/lease_cache.go @@ -23,8 +23,8 @@ import ( "github.com/hashicorp/go-multierror" "github.com/hashicorp/go-secure-stdlib/base62" "github.com/hashicorp/vault/api" - "github.com/hashicorp/vault/command/agent/cache/cacheboltdb" - "github.com/hashicorp/vault/command/agent/cache/cachememdb" + "github.com/hashicorp/vault/command/agentproxyshared/cache/cacheboltdb" + "github.com/hashicorp/vault/command/agentproxyshared/cache/cachememdb" "github.com/hashicorp/vault/helper/namespace" nshelper "github.com/hashicorp/vault/helper/namespace" "github.com/hashicorp/vault/helper/useragent" diff --git a/command/agent/cache/lease_cache_test.go b/command/agentproxyshared/cache/lease_cache_test.go similarity index 99% rename from command/agent/cache/lease_cache_test.go rename to command/agentproxyshared/cache/lease_cache_test.go index a0de2660e..8548dfa8f 100644 --- a/command/agent/cache/lease_cache_test.go +++ b/command/agentproxyshared/cache/lease_cache_test.go @@ -17,15 +17,14 @@ import ( "testing" "time" - "github.com/hashicorp/vault/helper/useragent" - "github.com/go-test/deep" hclog "github.com/hashicorp/go-hclog" "github.com/hashicorp/go-multierror" "github.com/hashicorp/vault/api" - "github.com/hashicorp/vault/command/agent/cache/cacheboltdb" - "github.com/hashicorp/vault/command/agent/cache/cachememdb" - "github.com/hashicorp/vault/command/agent/cache/keymanager" + "github.com/hashicorp/vault/command/agentproxyshared/cache/cacheboltdb" + "github.com/hashicorp/vault/command/agentproxyshared/cache/cachememdb" + "github.com/hashicorp/vault/command/agentproxyshared/cache/keymanager" + "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" diff --git a/command/agent/cache/listener.go b/command/agentproxyshared/cache/listener.go similarity index 100% rename from command/agent/cache/listener.go rename to command/agentproxyshared/cache/listener.go diff --git a/command/agent/cache/proxy.go b/command/agentproxyshared/cache/proxy.go similarity index 100% rename from command/agent/cache/proxy.go rename to command/agentproxyshared/cache/proxy.go diff --git a/command/agent/cache/testing.go b/command/agentproxyshared/cache/testing.go similarity index 100% rename from command/agent/cache/testing.go rename to command/agentproxyshared/cache/testing.go diff --git a/command/agent/sink/file/file_sink.go b/command/agentproxyshared/sink/file/file_sink.go similarity index 98% rename from command/agent/sink/file/file_sink.go rename to command/agentproxyshared/sink/file/file_sink.go index 572320fc6..c25d99169 100644 --- a/command/agent/sink/file/file_sink.go +++ b/command/agentproxyshared/sink/file/file_sink.go @@ -12,7 +12,7 @@ import ( hclog "github.com/hashicorp/go-hclog" uuid "github.com/hashicorp/go-uuid" - "github.com/hashicorp/vault/command/agent/sink" + "github.com/hashicorp/vault/command/agentproxyshared/sink" ) // fileSink is a Sink implementation that writes a token to a file diff --git a/command/agent/sink/file/file_sink_test.go b/command/agentproxyshared/sink/file/file_sink_test.go similarity index 97% rename from command/agent/sink/file/file_sink_test.go rename to command/agentproxyshared/sink/file/file_sink_test.go index 1b9f3bd05..95db8df19 100644 --- a/command/agent/sink/file/file_sink_test.go +++ b/command/agentproxyshared/sink/file/file_sink_test.go @@ -12,7 +12,7 @@ import ( hclog "github.com/hashicorp/go-hclog" uuid "github.com/hashicorp/go-uuid" - "github.com/hashicorp/vault/command/agent/sink" + "github.com/hashicorp/vault/command/agentproxyshared/sink" "github.com/hashicorp/vault/sdk/helper/logging" ) diff --git a/command/agent/sink/file/sink_test.go b/command/agentproxyshared/sink/file/sink_test.go similarity index 97% rename from command/agent/sink/file/sink_test.go rename to command/agentproxyshared/sink/file/sink_test.go index 85089053e..de074003b 100644 --- a/command/agent/sink/file/sink_test.go +++ b/command/agentproxyshared/sink/file/sink_test.go @@ -15,7 +15,7 @@ import ( hclog "github.com/hashicorp/go-hclog" uuid "github.com/hashicorp/go-uuid" - "github.com/hashicorp/vault/command/agent/sink" + "github.com/hashicorp/vault/command/agentproxyshared/sink" "github.com/hashicorp/vault/sdk/helper/logging" ) diff --git a/command/agent/sink/inmem/inmem_sink.go b/command/agentproxyshared/sink/inmem/inmem_sink.go similarity index 88% rename from command/agent/sink/inmem/inmem_sink.go rename to command/agentproxyshared/sink/inmem/inmem_sink.go index abe0ce639..e5804d884 100644 --- a/command/agent/sink/inmem/inmem_sink.go +++ b/command/agentproxyshared/sink/inmem/inmem_sink.go @@ -7,8 +7,8 @@ import ( "errors" hclog "github.com/hashicorp/go-hclog" - "github.com/hashicorp/vault/command/agent/cache" - "github.com/hashicorp/vault/command/agent/sink" + "github.com/hashicorp/vault/command/agentproxyshared/cache" + "github.com/hashicorp/vault/command/agentproxyshared/sink" "go.uber.org/atomic" ) diff --git a/command/agent/sink/mock/mock_sink.go b/command/agentproxyshared/sink/mock/mock_sink.go similarity index 85% rename from command/agent/sink/mock/mock_sink.go rename to command/agentproxyshared/sink/mock/mock_sink.go index d5f11dff5..c39baf9c8 100644 --- a/command/agent/sink/mock/mock_sink.go +++ b/command/agentproxyshared/sink/mock/mock_sink.go @@ -4,7 +4,7 @@ package mock import ( - "github.com/hashicorp/vault/command/agent/sink" + "github.com/hashicorp/vault/command/agentproxyshared/sink" ) type mockSink struct { diff --git a/command/agent/sink/sink.go b/command/agentproxyshared/sink/sink.go similarity index 100% rename from command/agent/sink/sink.go rename to command/agentproxyshared/sink/sink.go diff --git a/command/agent/winsvc/service.go b/command/agentproxyshared/winsvc/service.go similarity index 100% rename from command/agent/winsvc/service.go rename to command/agentproxyshared/winsvc/service.go diff --git a/command/agent/winsvc/service_windows.go b/command/agentproxyshared/winsvc/service_windows.go similarity index 100% rename from command/agent/winsvc/service_windows.go rename to command/agentproxyshared/winsvc/service_windows.go diff --git a/command/base.go b/command/base.go index 06cd13dc7..641fcb34e 100644 --- a/command/base.go +++ b/command/base.go @@ -42,20 +42,20 @@ type BaseCommand struct { flags *FlagSets flagsOnce sync.Once - flagAddress string - flagAgentAddress string - flagCACert string - flagCAPath string - flagClientCert string - flagClientKey string - flagNamespace string - flagNS string - flagPolicyOverride bool - flagTLSServerName string - flagTLSSkipVerify bool - flagDisableRedirects bool - flagWrapTTL time.Duration - flagUnlockKey string + flagAddress string + flagAgentProxyAddress string + flagCACert string + flagCAPath string + flagClientCert string + flagClientKey string + flagNamespace string + flagNS string + flagPolicyOverride bool + flagTLSServerName string + flagTLSSkipVerify bool + flagDisableRedirects bool + flagWrapTTL time.Duration + flagUnlockKey string flagFormat string flagField string @@ -90,8 +90,8 @@ func (c *BaseCommand) Client() (*api.Client, error) { if c.flagAddress != "" { config.Address = c.flagAddress } - if c.flagAgentAddress != "" { - config.Address = c.flagAgentAddress + if c.flagAgentProxyAddress != "" { + config.Address = c.flagAgentProxyAddress } if c.flagOutputCurlString { @@ -330,7 +330,7 @@ func (c *BaseCommand) flagSet(bit FlagSetBit) *FlagSets { agentAddrStringVar := &StringVar{ Name: "agent-address", - Target: &c.flagAgentAddress, + Target: &c.flagAgentProxyAddress, EnvVar: api.EnvVaultAgentAddr, Completion: complete.PredictAnything, Usage: "Address of the Agent.", diff --git a/command/commands.go b/command/commands.go index 40fd57963..a3b023e67 100644 --- a/command/commands.go +++ b/command/commands.go @@ -603,6 +603,15 @@ func initCommands(ui, serverCmdUi cli.Ui, runOpts *RunOptions) map[string]cli.Co BaseCommand: getBaseCommand(), }, nil }, + "proxy": func() (cli.Command, error) { + return &ProxyCommand{ + BaseCommand: &BaseCommand{ + UI: serverCmdUi, + }, + ShutdownCh: MakeShutdownCh(), + SighupCh: MakeSighupCh(), + }, nil + }, "policy": func() (cli.Command, error) { return &PolicyCommand{ BaseCommand: getBaseCommand(), diff --git a/command/proxy.go b/command/proxy.go new file mode 100644 index 000000000..f4d7ff62a --- /dev/null +++ b/command/proxy.go @@ -0,0 +1,1294 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package command + +import ( + "context" + "crypto/tls" + "flag" + "fmt" + "io" + "net" + "net/http" + "os" + "path/filepath" + "sort" + "strings" + "sync" + "time" + + systemd "github.com/coreos/go-systemd/daemon" + ctconfig "github.com/hashicorp/consul-template/config" + log "github.com/hashicorp/go-hclog" + "github.com/hashicorp/go-multierror" + "github.com/hashicorp/go-secure-stdlib/gatedwriter" + "github.com/hashicorp/go-secure-stdlib/parseutil" + "github.com/hashicorp/go-secure-stdlib/reloadutil" + "github.com/hashicorp/vault/api" + "github.com/hashicorp/vault/command/agentproxyshared/auth" + "github.com/hashicorp/vault/command/agentproxyshared/auth/alicloud" + "github.com/hashicorp/vault/command/agentproxyshared/auth/approle" + "github.com/hashicorp/vault/command/agentproxyshared/auth/aws" + "github.com/hashicorp/vault/command/agentproxyshared/auth/azure" + "github.com/hashicorp/vault/command/agentproxyshared/auth/cert" + "github.com/hashicorp/vault/command/agentproxyshared/auth/cf" + "github.com/hashicorp/vault/command/agentproxyshared/auth/gcp" + "github.com/hashicorp/vault/command/agentproxyshared/auth/jwt" + "github.com/hashicorp/vault/command/agentproxyshared/auth/kerberos" + "github.com/hashicorp/vault/command/agentproxyshared/auth/kubernetes" + "github.com/hashicorp/vault/command/agentproxyshared/auth/oci" + token_file "github.com/hashicorp/vault/command/agentproxyshared/auth/token-file" + cache "github.com/hashicorp/vault/command/agentproxyshared/cache" + "github.com/hashicorp/vault/command/agentproxyshared/cache/cacheboltdb" + "github.com/hashicorp/vault/command/agentproxyshared/cache/cachememdb" + "github.com/hashicorp/vault/command/agentproxyshared/cache/keymanager" + "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/command/agentproxyshared/winsvc" + proxyConfig "github.com/hashicorp/vault/command/proxy/config" + "github.com/hashicorp/vault/helper/logging" + "github.com/hashicorp/vault/helper/metricsutil" + "github.com/hashicorp/vault/helper/useragent" + "github.com/hashicorp/vault/internalshared/configutil" + "github.com/hashicorp/vault/internalshared/listenerutil" + "github.com/hashicorp/vault/sdk/helper/consts" + "github.com/hashicorp/vault/sdk/logical" + "github.com/hashicorp/vault/version" + "github.com/kr/pretty" + "github.com/mitchellh/cli" + "github.com/oklog/run" + "github.com/posener/complete" + "golang.org/x/text/cases" + "golang.org/x/text/language" + "google.golang.org/grpc/test/bufconn" +) + +var ( + _ cli.Command = (*ProxyCommand)(nil) + _ cli.CommandAutocomplete = (*ProxyCommand)(nil) +) + +const ( + // flagNameProxyExitAfterAuth is used as a Proxy specific flag to indicate + // that proxy should exit after a single successful auth + flagNameProxyExitAfterAuth = "exit-after-auth" +) + +type ProxyCommand struct { + *BaseCommand + logFlags logFlags + + config *proxyConfig.Config + + ShutdownCh chan struct{} + SighupCh chan struct{} + + tlsReloadFuncsLock sync.RWMutex + tlsReloadFuncs []reloadutil.ReloadFunc + + logWriter io.Writer + logGate *gatedwriter.Writer + logger log.Logger + + // Telemetry object + metricsHelper *metricsutil.MetricsHelper + + cleanupGuard sync.Once + + startedCh chan struct{} // for tests + reloadedCh chan struct{} // for tests + + flagConfigs []string + flagExitAfterAuth bool + flagTestVerifyOnly bool +} + +func (c *ProxyCommand) Synopsis() string { + return "Start a Vault Proxy" +} + +func (c *ProxyCommand) Help() string { + helpText := ` +Usage: vault proxy [options] + + This command starts a Vault Proxy that can perform automatic authentication + in certain environments. + + Start a proxy with a configuration file: + + $ vault proxy -config=/etc/vault/config.hcl + + For a full list of examples, please see the documentation. + +` + c.Flags().Help() + return strings.TrimSpace(helpText) +} + +func (c *ProxyCommand) Flags() *FlagSets { + set := c.flagSet(FlagSetHTTP) + + f := set.NewFlagSet("Command Options") + + // Augment with the log flags + f.addLogFlags(&c.logFlags) + + f.StringSliceVar(&StringSliceVar{ + Name: "config", + Target: &c.flagConfigs, + Completion: complete.PredictOr( + complete.PredictFiles("*.hcl"), + complete.PredictFiles("*.json"), + ), + Usage: "Path to a configuration file. This configuration file should " + + "contain only proxy directives.", + }) + + f.BoolVar(&BoolVar{ + Name: flagNameProxyExitAfterAuth, + Target: &c.flagExitAfterAuth, + Default: false, + Usage: "If set to true, the proxy will exit with code 0 after a single " + + "successful auth, where success means that a token was retrieved and " + + "all sinks successfully wrote it", + }) + + // Internal-only flags to follow. + // + // Why hello there little source code reader! Welcome to the Vault source + // code. The remaining options are intentionally undocumented and come with + // no warranty or backwards-compatibility promise. Do not use these flags + // in production. Do not build automation using these flags. Unless you are + // developing against Vault, you should not need any of these flags. + f.BoolVar(&BoolVar{ + Name: "test-verify-only", + Target: &c.flagTestVerifyOnly, + Default: false, + Hidden: true, + }) + + // End internal-only flags. + + return set +} + +func (c *ProxyCommand) AutocompleteArgs() complete.Predictor { + return complete.PredictNothing +} + +func (c *ProxyCommand) AutocompleteFlags() complete.Flags { + return c.Flags().Completions() +} + +func (c *ProxyCommand) Run(args []string) int { + f := c.Flags() + + if err := f.Parse(args); err != nil { + c.UI.Error(err.Error()) + return 1 + } + + // Create a logger. We wrap it in a gated writer so that it doesn't + // start logging too early. + c.logGate = gatedwriter.NewWriter(os.Stderr) + c.logWriter = c.logGate + + if c.logFlags.flagCombineLogs { + c.logWriter = os.Stdout + } + + // Validation + if len(c.flagConfigs) < 1 { + c.UI.Error("Must specify exactly at least one config path using -config") + return 1 + } + + config, err := c.loadConfig(c.flagConfigs) + if err != nil { + c.outputErrors(err) + return 1 + } + + if config.AutoAuth == nil { + c.UI.Info("No auto_auth block found in config, the automatic authentication feature will not be started") + } + + c.applyConfigOverrides(f, config) // This only needs to happen on start-up to aggregate config from flags and env vars + c.config = config + + l, err := c.newLogger() + if err != nil { + c.outputErrors(err) + return 1 + } + c.logger = l + + infoKeys := make([]string, 0, 10) + info := make(map[string]string) + info["log level"] = config.LogLevel + infoKeys = append(infoKeys, "log level") + + infoKeys = append(infoKeys, "version") + verInfo := version.GetVersion() + info["version"] = verInfo.FullVersionNumber(false) + if verInfo.Revision != "" { + info["version sha"] = strings.Trim(verInfo.Revision, "'") + infoKeys = append(infoKeys, "version sha") + } + infoKeys = append(infoKeys, "cgo") + info["cgo"] = "disabled" + if version.CgoEnabled { + info["cgo"] = "enabled" + } + + // Tests might not want to start a vault server and just want to verify + // the configuration. + if c.flagTestVerifyOnly { + if os.Getenv("VAULT_TEST_VERIFY_ONLY_DUMP_CONFIG") != "" { + c.UI.Output(fmt.Sprintf( + "\nConfiguration:\n%s\n", + pretty.Sprint(*c.config))) + } + return 0 + } + + // Ignore any setting of Agent/Proxy's address. This client is used by the Proxy + // to reach out to Vault. This should never loop back to the proxy. + c.flagAgentProxyAddress = "" + client, err := c.Client() + if err != nil { + c.UI.Error(fmt.Sprintf( + "Error fetching client: %v", + err)) + return 1 + } + + serverHealth, err := client.Sys().Health() + // We don't have any special behaviour if the error != nil, as this + // is not worth stopping the Proxy process over. + if err == nil { + // Note that we don't exit if the versions don't match, as this is a valid + // configuration, but we should still let the user know. + serverVersion := serverHealth.Version + proxyVersion := version.GetVersion().VersionNumber() + if serverVersion != proxyVersion { + c.UI.Info("==> Note: Vault Proxy version does not match Vault server version. " + + fmt.Sprintf("Vault Proxy version: %s, Vault server version: %s", proxyVersion, serverVersion)) + } + } + + // telemetry configuration + inmemMetrics, _, prometheusEnabled, err := configutil.SetupTelemetry(&configutil.SetupTelemetryOpts{ + Config: config.Telemetry, + Ui: c.UI, + ServiceName: "vault", + DisplayName: "Vault", + UserAgent: useragent.ProxyString(), + ClusterName: config.ClusterName, + }) + if err != nil { + c.UI.Error(fmt.Sprintf("Error initializing telemetry: %s", err)) + return 1 + } + c.metricsHelper = metricsutil.NewMetricsHelper(inmemMetrics, prometheusEnabled) + + var method auth.AuthMethod + var sinks []*sink.SinkConfig + if config.AutoAuth != nil { + if client.Headers().Get(consts.NamespaceHeaderName) == "" && config.AutoAuth.Method.Namespace != "" { + client.SetNamespace(config.AutoAuth.Method.Namespace) + } + + sinkClient, err := client.CloneWithHeaders() + if err != nil { + c.UI.Error(fmt.Sprintf("Error cloning client for file sink: %v", err)) + return 1 + } + + if config.DisableIdleConnsAutoAuth { + sinkClient.SetMaxIdleConnections(-1) + } + + if config.DisableKeepAlivesAutoAuth { + sinkClient.SetDisableKeepAlives(true) + } + + for _, sc := range config.AutoAuth.Sinks { + switch sc.Type { + case "file": + config := &sink.SinkConfig{ + Logger: c.logger.Named("sink.file"), + Config: sc.Config, + Client: sinkClient, + WrapTTL: sc.WrapTTL, + DHType: sc.DHType, + DeriveKey: sc.DeriveKey, + DHPath: sc.DHPath, + AAD: sc.AAD, + } + s, err := file.NewFileSink(config) + if err != nil { + c.UI.Error(fmt.Errorf("error creating file sink: %w", err).Error()) + return 1 + } + config.Sink = s + sinks = append(sinks, config) + default: + c.UI.Error(fmt.Sprintf("Unknown sink type %q", sc.Type)) + return 1 + } + } + + authConfig := &auth.AuthConfig{ + Logger: c.logger.Named(fmt.Sprintf("auth.%s", config.AutoAuth.Method.Type)), + MountPath: config.AutoAuth.Method.MountPath, + Config: config.AutoAuth.Method.Config, + } + switch config.AutoAuth.Method.Type { + case "alicloud": + method, err = alicloud.NewAliCloudAuthMethod(authConfig) + case "aws": + method, err = aws.NewAWSAuthMethod(authConfig) + case "azure": + method, err = azure.NewAzureAuthMethod(authConfig) + case "cert": + method, err = cert.NewCertAuthMethod(authConfig) + case "cf": + method, err = cf.NewCFAuthMethod(authConfig) + case "gcp": + method, err = gcp.NewGCPAuthMethod(authConfig) + case "jwt": + method, err = jwt.NewJWTAuthMethod(authConfig) + case "kerberos": + method, err = kerberos.NewKerberosAuthMethod(authConfig) + case "kubernetes": + method, err = kubernetes.NewKubernetesAuthMethod(authConfig) + case "approle": + method, err = approle.NewApproleAuthMethod(authConfig) + case "oci": + method, err = oci.NewOCIAuthMethod(authConfig, config.Vault.Address) + case "token_file": + method, err = token_file.NewTokenFileAuthMethod(authConfig) + case "pcf": // Deprecated. + method, err = cf.NewCFAuthMethod(authConfig) + default: + c.UI.Error(fmt.Sprintf("Unknown auth method %q", config.AutoAuth.Method.Type)) + return 1 + } + if err != nil { + c.UI.Error(fmt.Errorf("Error creating %s auth method: %w", config.AutoAuth.Method.Type, err).Error()) + return 1 + } + } + + // We do this after auto-auth has been configured, because we don't want to + // confuse the issue of retries for auth failures which have their own + // config and are handled a bit differently. + if os.Getenv(api.EnvVaultMaxRetries) == "" { + client.SetMaxRetries(ctconfig.DefaultRetryAttempts) + if config.Vault != nil { + if config.Vault.Retry != nil { + client.SetMaxRetries(config.Vault.Retry.NumRetries) + } + } + } + + enforceConsistency := cache.EnforceConsistencyNever + whenInconsistent := cache.WhenInconsistentFail + if config.APIProxy != nil { + switch config.APIProxy.EnforceConsistency { + case "always": + enforceConsistency = cache.EnforceConsistencyAlways + case "never", "": + default: + c.UI.Error(fmt.Sprintf("Unknown api_proxy setting for enforce_consistency: %q", config.APIProxy.EnforceConsistency)) + return 1 + } + + switch config.APIProxy.WhenInconsistent { + case "retry": + whenInconsistent = cache.WhenInconsistentRetry + case "forward": + whenInconsistent = cache.WhenInconsistentForward + case "fail", "": + default: + c.UI.Error(fmt.Sprintf("Unknown api_proxy setting for when_inconsistent: %q", config.APIProxy.WhenInconsistent)) + return 1 + } + } + + // Warn if cache _and_ cert auto-auth is enabled but certificates were not + // provided in the auto_auth.method["cert"].config stanza. + if config.Cache != nil && (config.AutoAuth != nil && config.AutoAuth.Method != nil && config.AutoAuth.Method.Type == "cert") { + _, okCertFile := config.AutoAuth.Method.Config["client_cert"] + _, okCertKey := config.AutoAuth.Method.Config["client_key"] + + // If neither of these exists in the cert stanza, proxy will use the + // certs from the vault stanza. + if !okCertFile && !okCertKey { + c.UI.Warn(wrapAtLength("WARNING! Cache is enabled and using the same certificates " + + "from the 'cert' auto-auth method specified in the 'vault' stanza. Consider " + + "specifying certificate information in the 'cert' auto-auth's config stanza.")) + } + + } + + // Output the header that the proxy has started + if !c.logFlags.flagCombineLogs { + c.UI.Output("==> Vault Proxy started! Log data will stream in below:\n") + } + + var leaseCache *cache.LeaseCache + var previousToken string + + proxyClient, err := client.CloneWithHeaders() + if err != nil { + c.UI.Error(fmt.Sprintf("Error cloning client for proxying: %v", err)) + return 1 + } + + if config.DisableIdleConnsAPIProxy { + proxyClient.SetMaxIdleConnections(-1) + } + + if config.DisableKeepAlivesAPIProxy { + proxyClient.SetDisableKeepAlives(true) + } + + apiProxyLogger := c.logger.Named("apiproxy") + + // The API proxy to be used, if listeners are configured + apiProxy, err := cache.NewAPIProxy(&cache.APIProxyConfig{ + Client: proxyClient, + Logger: apiProxyLogger, + EnforceConsistency: enforceConsistency, + WhenInconsistentAction: whenInconsistent, + UserAgentStringFunction: useragent.ProxyStringWithProxiedUserAgent, + UserAgentString: useragent.ProxyString(), + }) + if err != nil { + c.UI.Error(fmt.Sprintf("Error creating API proxy: %v", err)) + return 1 + } + + // ctx and cancelFunc are passed to the AuthHandler, SinkServer, + // and other subsystems, so that they can listen for ctx.Done() to + // fire and shut down accordingly. + ctx, cancelFunc := context.WithCancel(context.Background()) + defer cancelFunc() + + // Parse proxy cache configurations + if config.Cache != nil { + cacheLogger := c.logger.Named("cache") + + // Create the lease cache proxier and set its underlying proxier to + // the API proxier. + leaseCache, err = cache.NewLeaseCache(&cache.LeaseCacheConfig{ + Client: proxyClient, + BaseContext: ctx, + Proxier: apiProxy, + Logger: cacheLogger.Named("leasecache"), + }) + if err != nil { + c.UI.Error(fmt.Sprintf("Error creating lease cache: %v", err)) + return 1 + } + + // Configure persistent storage and add to LeaseCache + if config.Cache.Persist != nil { + if config.Cache.Persist.Path == "" { + c.UI.Error("must specify persistent cache path") + return 1 + } + + // Set AAD based on key protection type + var aad string + switch config.Cache.Persist.Type { + case "kubernetes": + aad, err = getServiceAccountJWT(config.Cache.Persist.ServiceAccountTokenFile) + if err != nil { + c.UI.Error(fmt.Sprintf("failed to read service account token from %s: %s", config.Cache.Persist.ServiceAccountTokenFile, err)) + return 1 + } + default: + c.UI.Error(fmt.Sprintf("persistent key protection type %q not supported", config.Cache.Persist.Type)) + return 1 + } + + // Check if bolt file exists already + dbFileExists, err := cacheboltdb.DBFileExists(config.Cache.Persist.Path) + if err != nil { + c.UI.Error(fmt.Sprintf("failed to check if bolt file exists at path %s: %s", config.Cache.Persist.Path, err)) + return 1 + } + if dbFileExists { + // Open the bolt file, but wait to setup Encryption + ps, err := cacheboltdb.NewBoltStorage(&cacheboltdb.BoltStorageConfig{ + Path: config.Cache.Persist.Path, + Logger: cacheLogger.Named("cacheboltdb"), + }) + if err != nil { + c.UI.Error(fmt.Sprintf("Error opening persistent cache: %v", err)) + return 1 + } + + // Get the token from bolt for retrieving the encryption key, + // then setup encryption so that restore is possible + token, err := ps.GetRetrievalToken() + if err != nil { + c.UI.Error(fmt.Sprintf("Error getting retrieval token from persistent cache: %v", err)) + } + + if err := ps.Close(); err != nil { + c.UI.Warn(fmt.Sprintf("Failed to close persistent cache file after getting retrieval token: %s", err)) + } + + km, err := keymanager.NewPassthroughKeyManager(ctx, token) + if err != nil { + c.UI.Error(fmt.Sprintf("failed to configure persistence encryption for cache: %s", err)) + return 1 + } + + // Open the bolt file with the wrapper provided + ps, err = cacheboltdb.NewBoltStorage(&cacheboltdb.BoltStorageConfig{ + Path: config.Cache.Persist.Path, + Logger: cacheLogger.Named("cacheboltdb"), + Wrapper: km.Wrapper(), + AAD: aad, + }) + if err != nil { + c.UI.Error(fmt.Sprintf("Error opening persistent cache with wrapper: %v", err)) + return 1 + } + + // Restore anything in the persistent cache to the memory cache + if err := leaseCache.Restore(ctx, ps); err != nil { + c.UI.Error(fmt.Sprintf("Error restoring in-memory cache from persisted file: %v", err)) + if config.Cache.Persist.ExitOnErr { + return 1 + } + } + cacheLogger.Info("loaded memcache from persistent storage") + + // Check for previous auto-auth token + oldTokenBytes, err := ps.GetAutoAuthToken(ctx) + if err != nil { + c.UI.Error(fmt.Sprintf("Error in fetching previous auto-auth token: %s", err)) + if config.Cache.Persist.ExitOnErr { + return 1 + } + } + if len(oldTokenBytes) > 0 { + oldToken, err := cachememdb.Deserialize(oldTokenBytes) + if err != nil { + c.UI.Error(fmt.Sprintf("Error in deserializing previous auto-auth token cache entry: %s", err)) + if config.Cache.Persist.ExitOnErr { + return 1 + } + } + previousToken = oldToken.Token + } + + // If keep_after_import true, set persistent storage layer in + // leaseCache, else remove db file + if config.Cache.Persist.KeepAfterImport { + defer ps.Close() + leaseCache.SetPersistentStorage(ps) + } else { + if err := ps.Close(); err != nil { + c.UI.Warn(fmt.Sprintf("failed to close persistent cache file: %s", err)) + } + dbFile := filepath.Join(config.Cache.Persist.Path, cacheboltdb.DatabaseFileName) + if err := os.Remove(dbFile); err != nil { + c.UI.Error(fmt.Sprintf("failed to remove persistent storage file %s: %s", dbFile, err)) + if config.Cache.Persist.ExitOnErr { + return 1 + } + } + } + } else { + km, err := keymanager.NewPassthroughKeyManager(ctx, nil) + if err != nil { + c.UI.Error(fmt.Sprintf("failed to configure persistence encryption for cache: %s", err)) + return 1 + } + ps, err := cacheboltdb.NewBoltStorage(&cacheboltdb.BoltStorageConfig{ + Path: config.Cache.Persist.Path, + Logger: cacheLogger.Named("cacheboltdb"), + Wrapper: km.Wrapper(), + AAD: aad, + }) + if err != nil { + c.UI.Error(fmt.Sprintf("Error creating persistent cache: %v", err)) + return 1 + } + cacheLogger.Info("configured persistent storage", "path", config.Cache.Persist.Path) + + // Stash the key material in bolt + token, err := km.RetrievalToken(ctx) + if err != nil { + c.UI.Error(fmt.Sprintf("Error getting persistent key: %s", err)) + return 1 + } + if err := ps.StoreRetrievalToken(token); err != nil { + c.UI.Error(fmt.Sprintf("Error setting key in persistent cache: %v", err)) + return 1 + } + + defer ps.Close() + leaseCache.SetPersistentStorage(ps) + } + } + } + + var listeners []net.Listener + + // Ensure we've added all the reload funcs for TLS before anyone triggers a reload. + c.tlsReloadFuncsLock.Lock() + + for i, lnConfig := range config.Listeners { + var ln net.Listener + var tlsCfg *tls.Config + + if lnConfig.Type == listenerutil.BufConnType { + inProcListener := bufconn.Listen(1024 * 1024) + if config.Cache != nil { + config.Cache.InProcDialer = listenerutil.NewBufConnWrapper(inProcListener) + } + ln = inProcListener + } else { + lnBundle, err := cache.StartListener(lnConfig) + if err != nil { + c.UI.Error(fmt.Sprintf("Error starting listener: %v", err)) + return 1 + } + + tlsCfg = lnBundle.TLSConfig + ln = lnBundle.Listener + + // Track the reload func, so we can reload later if needed. + c.tlsReloadFuncs = append(c.tlsReloadFuncs, lnBundle.TLSReloadFunc) + } + + listeners = append(listeners, ln) + + proxyVaultToken := true + var inmemSink sink.Sink + if config.APIProxy != nil { + if config.APIProxy.UseAutoAuthToken { + apiProxyLogger.Debug("auto-auth token is allowed to be used; configuring inmem sink") + inmemSink, err = inmem.New(&sink.SinkConfig{ + Logger: apiProxyLogger, + }, leaseCache) + if err != nil { + c.UI.Error(fmt.Sprintf("Error creating inmem sink for cache: %v", err)) + return 1 + } + sinks = append(sinks, &sink.SinkConfig{ + Logger: apiProxyLogger, + Sink: inmemSink, + }) + } + proxyVaultToken = !config.APIProxy.ForceAutoAuthToken + } + + var muxHandler http.Handler + if leaseCache != nil { + muxHandler = cache.ProxyHandler(ctx, apiProxyLogger, leaseCache, inmemSink, proxyVaultToken) + } else { + muxHandler = cache.ProxyHandler(ctx, apiProxyLogger, apiProxy, inmemSink, proxyVaultToken) + } + + // Parse 'require_request_header' listener config option, and wrap + // the request handler if necessary + if lnConfig.RequireRequestHeader && ("metrics_only" != lnConfig.Role) { + muxHandler = verifyRequestHeader(muxHandler) + } + + // Create a muxer and add paths relevant for the lease cache layer + mux := http.NewServeMux() + quitEnabled := lnConfig.ProxyAPI != nil && lnConfig.ProxyAPI.EnableQuit + + mux.Handle(consts.ProxyPathMetrics, c.handleMetrics()) + if "metrics_only" != lnConfig.Role { + mux.Handle(consts.ProxyPathCacheClear, leaseCache.HandleCacheClear(ctx)) + mux.Handle(consts.ProxyPathQuit, c.handleQuit(quitEnabled)) + mux.Handle("/", muxHandler) + } + + scheme := "https://" + if tlsCfg == nil { + scheme = "http://" + } + if ln.Addr().Network() == "unix" { + scheme = "unix://" + } + + infoKey := fmt.Sprintf("api address %d", i+1) + info[infoKey] = scheme + ln.Addr().String() + infoKeys = append(infoKeys, infoKey) + + server := &http.Server{ + Addr: ln.Addr().String(), + TLSConfig: tlsCfg, + Handler: mux, + ReadHeaderTimeout: 10 * time.Second, + ReadTimeout: 30 * time.Second, + IdleTimeout: 5 * time.Minute, + ErrorLog: apiProxyLogger.StandardLogger(nil), + } + + go server.Serve(ln) + } + + c.tlsReloadFuncsLock.Unlock() + + // Ensure that listeners are closed at all the exits + listenerCloseFunc := func() { + for _, ln := range listeners { + ln.Close() + } + } + defer c.cleanupGuard.Do(listenerCloseFunc) + + // Inform any tests that the server is ready + if c.startedCh != nil { + close(c.startedCh) + } + + var g run.Group + + g.Add(func() error { + for { + select { + case <-c.SighupCh: + c.UI.Output("==> Vault Proxy config reload triggered") + err := c.reloadConfig(c.flagConfigs) + if err != nil { + c.outputErrors(err) + } + // Send the 'reloaded' message on the relevant channel + select { + case c.reloadedCh <- struct{}{}: + default: + } + case <-ctx.Done(): + return nil + } + } + }, func(error) { + cancelFunc() + }) + + // This run group watches for signal termination + g.Add(func() error { + for { + select { + case <-c.ShutdownCh: + c.UI.Output("==> Vault Proxy shutdown triggered") + // Notify systemd that the server is shutting down + // Let the lease cache know this is a shutdown; no need to evict everything + if leaseCache != nil { + leaseCache.SetShuttingDown(true) + } + return nil + case <-ctx.Done(): + return nil + case <-winsvc.ShutdownChannel(): + return nil + } + } + }, func(error) {}) + + // Start auto-auth and sink servers + if method != nil { + // Auth Handler is going to set its own retry values, so we want to + // work on a copy of the client to not affect other subsystems. + ahClient, err := c.client.CloneWithHeaders() + if err != nil { + c.UI.Error(fmt.Sprintf("Error cloning client for auth handler: %v", err)) + return 1 + } + + if config.DisableIdleConnsAutoAuth { + ahClient.SetMaxIdleConnections(-1) + } + + if config.DisableKeepAlivesAutoAuth { + ahClient.SetDisableKeepAlives(true) + } + + ah := auth.NewAuthHandler(&auth.AuthHandlerConfig{ + Logger: c.logger.Named("auth.handler"), + Client: ahClient, + WrapTTL: config.AutoAuth.Method.WrapTTL, + MinBackoff: config.AutoAuth.Method.MinBackoff, + MaxBackoff: config.AutoAuth.Method.MaxBackoff, + EnableReauthOnNewCredentials: config.AutoAuth.EnableReauthOnNewCredentials, + Token: previousToken, + ExitOnError: config.AutoAuth.Method.ExitOnError, + UserAgent: useragent.ProxyAutoAuthString(), + MetricsSignifier: "proxy", + }) + + ss := sink.NewSinkServer(&sink.SinkServerConfig{ + Logger: c.logger.Named("sink.server"), + Client: ahClient, + ExitAfterAuth: config.ExitAfterAuth, + }) + + g.Add(func() error { + return ah.Run(ctx, method) + }, func(error) { + // Let the lease cache know this is a shutdown; no need to evict + // everything + if leaseCache != nil { + leaseCache.SetShuttingDown(true) + } + cancelFunc() + }) + + g.Add(func() error { + err := ss.Run(ctx, ah.OutputCh, sinks) + c.logger.Info("sinks finished, exiting") + + // Start goroutine to drain from ah.OutputCh from this point onward + // to prevent ah.Run from being blocked. + go func() { + for { + select { + case <-ctx.Done(): + return + case <-ah.OutputCh: + } + } + }() + + return err + }, func(error) { + // Let the lease cache know this is a shutdown; no need to evict + // everything + if leaseCache != nil { + leaseCache.SetShuttingDown(true) + } + cancelFunc() + }) + } + + // Server configuration output + padding := 24 + sort.Strings(infoKeys) + caser := cases.Title(language.English) + c.UI.Output("==> Vault Proxy configuration:\n") + for _, k := range infoKeys { + c.UI.Output(fmt.Sprintf( + "%s%s: %s", + strings.Repeat(" ", padding-len(k)), + caser.String(k), + info[k])) + } + c.UI.Output("") + + // Release the log gate. + c.logGate.Flush() + + // Write out the PID to the file now that server has successfully started + if err := c.storePidFile(config.PidFile); err != nil { + c.UI.Error(fmt.Sprintf("Error storing PID: %s", err)) + return 1 + } + + // Notify systemd that the server is ready (if applicable) + c.notifySystemd(systemd.SdNotifyReady) + + defer func() { + if err := c.removePidFile(config.PidFile); err != nil { + c.UI.Error(fmt.Sprintf("Error deleting the PID file: %s", err)) + } + }() + + var exitCode int + if err := g.Run(); err != nil { + c.logger.Error("runtime error encountered", "error", err) + c.UI.Error("Error encountered during run, refer to logs for more details.") + exitCode = 1 + } + c.notifySystemd(systemd.SdNotifyStopping) + return exitCode +} + +// applyConfigOverrides ensures that the config object accurately reflects the desired +// settings as configured by the user. It applies the relevant config setting based +// on the precedence (env var overrides file config, cli overrides env var). +// It mutates the config object supplied. +func (c *ProxyCommand) applyConfigOverrides(f *FlagSets, config *proxyConfig.Config) { + if config.Vault == nil { + config.Vault = &proxyConfig.Vault{} + } + + f.applyLogConfigOverrides(config.SharedConfig) + + f.Visit(func(fl *flag.Flag) { + if fl.Name == flagNameProxyExitAfterAuth { + config.ExitAfterAuth = c.flagExitAfterAuth + } + }) + + c.setStringFlag(f, config.Vault.Address, &StringVar{ + Name: flagNameAddress, + Target: &c.flagAddress, + Default: "https://127.0.0.1:8200", + EnvVar: api.EnvVaultAddress, + }) + config.Vault.Address = c.flagAddress + c.setStringFlag(f, config.Vault.CACert, &StringVar{ + Name: flagNameCACert, + Target: &c.flagCACert, + Default: "", + EnvVar: api.EnvVaultCACert, + }) + config.Vault.CACert = c.flagCACert + c.setStringFlag(f, config.Vault.CAPath, &StringVar{ + Name: flagNameCAPath, + Target: &c.flagCAPath, + Default: "", + EnvVar: api.EnvVaultCAPath, + }) + config.Vault.CAPath = c.flagCAPath + c.setStringFlag(f, config.Vault.ClientCert, &StringVar{ + Name: flagNameClientCert, + Target: &c.flagClientCert, + Default: "", + EnvVar: api.EnvVaultClientCert, + }) + config.Vault.ClientCert = c.flagClientCert + c.setStringFlag(f, config.Vault.ClientKey, &StringVar{ + Name: flagNameClientKey, + Target: &c.flagClientKey, + Default: "", + EnvVar: api.EnvVaultClientKey, + }) + config.Vault.ClientKey = c.flagClientKey + c.setBoolFlag(f, config.Vault.TLSSkipVerify, &BoolVar{ + Name: flagNameTLSSkipVerify, + Target: &c.flagTLSSkipVerify, + Default: false, + EnvVar: api.EnvVaultSkipVerify, + }) + config.Vault.TLSSkipVerify = c.flagTLSSkipVerify + c.setStringFlag(f, config.Vault.TLSServerName, &StringVar{ + Name: flagTLSServerName, + Target: &c.flagTLSServerName, + Default: "", + EnvVar: api.EnvVaultTLSServerName, + }) + config.Vault.TLSServerName = c.flagTLSServerName +} + +func (c *ProxyCommand) notifySystemd(status string) { + sent, err := systemd.SdNotify(false, status) + if err != nil { + c.logger.Error("error notifying systemd", "error", err) + } else { + if sent { + c.logger.Debug("sent systemd notification", "notification", status) + } else { + c.logger.Debug("would have sent systemd notification (systemd not present)", "notification", status) + } + } +} + +func (c *ProxyCommand) setStringFlag(f *FlagSets, configVal string, fVar *StringVar) { + var isFlagSet bool + f.Visit(func(f *flag.Flag) { + if f.Name == fVar.Name { + isFlagSet = true + } + }) + + flagEnvValue, flagEnvSet := os.LookupEnv(fVar.EnvVar) + switch { + case isFlagSet: + // Don't do anything as the flag is already set from the command line + case flagEnvSet: + // Use value from env var + *fVar.Target = flagEnvValue + case configVal != "": + // Use value from config + *fVar.Target = configVal + default: + // Use the default value + *fVar.Target = fVar.Default + } +} + +func (c *ProxyCommand) setBoolFlag(f *FlagSets, configVal bool, fVar *BoolVar) { + var isFlagSet bool + f.Visit(func(f *flag.Flag) { + if f.Name == fVar.Name { + isFlagSet = true + } + }) + + flagEnvValue, flagEnvSet := os.LookupEnv(fVar.EnvVar) + switch { + case isFlagSet: + // Don't do anything as the flag is already set from the command line + case flagEnvSet: + // Use value from env var + *fVar.Target = flagEnvValue != "" + case configVal: + // Use value from config + *fVar.Target = configVal + default: + // Use the default value + *fVar.Target = fVar.Default + } +} + +// storePidFile is used to write out our PID to a file if necessary +func (c *ProxyCommand) storePidFile(pidPath string) error { + // Quit fast if no pidfile + if pidPath == "" { + return nil + } + + // Open the PID file + pidFile, err := os.OpenFile(pidPath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0o600) + if err != nil { + return fmt.Errorf("could not open pid file: %w", err) + } + defer pidFile.Close() + + // Write out the PID + pid := os.Getpid() + _, err = pidFile.WriteString(fmt.Sprintf("%d", pid)) + if err != nil { + return fmt.Errorf("could not write to pid file: %w", err) + } + return nil +} + +// removePidFile is used to cleanup the PID file if necessary +func (c *ProxyCommand) removePidFile(pidPath string) error { + if pidPath == "" { + return nil + } + return os.Remove(pidPath) +} + +func (c *ProxyCommand) handleMetrics() http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodGet { + logical.RespondError(w, http.StatusMethodNotAllowed, nil) + return + } + + if err := r.ParseForm(); err != nil { + logical.RespondError(w, http.StatusBadRequest, err) + return + } + + format := r.Form.Get("format") + if format == "" { + format = metricsutil.FormatFromRequest(&logical.Request{ + Headers: r.Header, + }) + } + + resp := c.metricsHelper.ResponseForFormat(format) + + status := resp.Data[logical.HTTPStatusCode].(int) + w.Header().Set("Content-Type", resp.Data[logical.HTTPContentType].(string)) + switch v := resp.Data[logical.HTTPRawBody].(type) { + case string: + w.WriteHeader(status) + w.Write([]byte(v)) + case []byte: + w.WriteHeader(status) + w.Write(v) + default: + logical.RespondError(w, http.StatusInternalServerError, fmt.Errorf("wrong response returned")) + } + }) +} + +func (c *ProxyCommand) handleQuit(enabled bool) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if !enabled { + w.WriteHeader(http.StatusNotFound) + return + } + + switch r.Method { + case http.MethodPost: + default: + w.WriteHeader(http.StatusMethodNotAllowed) + return + } + + c.logger.Debug("received quit request") + close(c.ShutdownCh) + }) +} + +// newLogger creates a logger based on parsed config field on the Proxy Command struct. +func (c *ProxyCommand) newLogger() (log.InterceptLogger, error) { + if c.config == nil { + return nil, fmt.Errorf("cannot create logger, no config") + } + + var errors error + + // Parse all the log related config + logLevel, err := logging.ParseLogLevel(c.config.LogLevel) + if err != nil { + errors = multierror.Append(errors, err) + } + + logFormat, err := logging.ParseLogFormat(c.config.LogFormat) + if err != nil { + errors = multierror.Append(errors, err) + } + + logRotateDuration, err := parseutil.ParseDurationSecond(c.config.LogRotateDuration) + if err != nil { + errors = multierror.Append(errors, err) + } + + if errors != nil { + return nil, errors + } + + logCfg := &logging.LogConfig{ + Name: "proxy", + LogLevel: logLevel, + LogFormat: logFormat, + LogFilePath: c.config.LogFile, + LogRotateDuration: logRotateDuration, + LogRotateBytes: c.config.LogRotateBytes, + LogRotateMaxFiles: c.config.LogRotateMaxFiles, + } + + l, err := logging.Setup(logCfg, c.logWriter) + if err != nil { + return nil, err + } + + return l, nil +} + +// loadConfig attempts to generate a Proxy config from the file(s) specified. +func (c *ProxyCommand) loadConfig(paths []string) (*proxyConfig.Config, error) { + var errors error + cfg := proxyConfig.NewConfig() + + for _, configPath := range paths { + configFromPath, err := proxyConfig.LoadConfig(configPath) + if err != nil { + errors = multierror.Append(errors, fmt.Errorf("error loading configuration from %s: %w", configPath, err)) + } else { + cfg = cfg.Merge(configFromPath) + } + } + + if errors != nil { + return nil, errors + } + + if err := cfg.ValidateConfig(); err != nil { + return nil, fmt.Errorf("error validating configuration: %w", err) + } + + return cfg, nil +} + +// reloadConfig will attempt to reload the config from file(s) and adjust certain +// config values without requiring a restart of the Vault Proxy. +// If config is retrieved without error it is stored in the config field of the ProxyCommand. +// This operation is not atomic and could result in updated config but partially applied config settings. +// The error returned from this func may be a multierror. +// This function will most likely be called due to Vault Proxy receiving a SIGHUP signal. +// Currently only reloading the following are supported: +// * log level +// * TLS certs for listeners +func (c *ProxyCommand) reloadConfig(paths []string) error { + // Notify systemd that the server is reloading + c.notifySystemd(systemd.SdNotifyReloading) + defer c.notifySystemd(systemd.SdNotifyReady) + + var errors error + + // Reload the config + cfg, err := c.loadConfig(paths) + if err != nil { + // Returning single error as we won't continue with bad config and won't 'commit' it. + return err + } + c.config = cfg + + // Update the log level + err = c.reloadLogLevel() + if err != nil { + errors = multierror.Append(errors, err) + } + + // Update certs + err = c.reloadCerts() + if err != nil { + errors = multierror.Append(errors, err) + } + + return errors +} + +// reloadLogLevel will attempt to update the log level for the logger attached +// to the ProxyCommand struct using the value currently set in config. +func (c *ProxyCommand) reloadLogLevel() error { + logLevel, err := logging.ParseLogLevel(c.config.LogLevel) + if err != nil { + return err + } + + c.logger.SetLevel(logLevel) + + return nil +} + +// reloadCerts will attempt to reload certificates using a reload func which +// was provided when the listeners were configured, only funcs that were appended +// to the ProxyCommand slice will be invoked. +// This function returns a multierror type so that every func can report an error +// if it encounters one. +func (c *ProxyCommand) reloadCerts() error { + var errors error + + c.tlsReloadFuncsLock.RLock() + defer c.tlsReloadFuncsLock.RUnlock() + + for _, reloadFunc := range c.tlsReloadFuncs { + // Non-TLS listeners will have a nil reload func. + if reloadFunc != nil { + err := reloadFunc() + if err != nil { + errors = multierror.Append(errors, err) + } + } + } + + return errors +} + +// outputErrors will take an error or multierror and handle outputting each to the UI +func (c *ProxyCommand) outputErrors(err error) { + if err != nil { + if me, ok := err.(*multierror.Error); ok { + for _, err := range me.Errors { + c.UI.Error(err.Error()) + } + } else { + c.UI.Error(err.Error()) + } + } +} diff --git a/command/proxy/config/config.go b/command/proxy/config/config.go new file mode 100644 index 000000000..4d263fe77 --- /dev/null +++ b/command/proxy/config/config.go @@ -0,0 +1,840 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package config + +import ( + "context" + "errors" + "fmt" + "io" + "net" + "os" + "path/filepath" + "strings" + "time" + + ctconfig "github.com/hashicorp/consul-template/config" + "github.com/hashicorp/go-multierror" + "github.com/hashicorp/go-secure-stdlib/parseutil" + "github.com/hashicorp/hcl" + "github.com/hashicorp/hcl/hcl/ast" + "github.com/hashicorp/vault/helper/namespace" + "github.com/hashicorp/vault/internalshared/configutil" +) + +// Config is the configuration for Vault Proxy. +type Config struct { + *configutil.SharedConfig `hcl:"-"` + + AutoAuth *AutoAuth `hcl:"auto_auth"` + ExitAfterAuth bool `hcl:"exit_after_auth"` + Cache *Cache `hcl:"cache"` + APIProxy *APIProxy `hcl:"api_proxy""` + Vault *Vault `hcl:"vault"` + DisableIdleConns []string `hcl:"disable_idle_connections"` + DisableIdleConnsAPIProxy bool `hcl:"-"` + DisableIdleConnsAutoAuth bool `hcl:"-"` + DisableKeepAlives []string `hcl:"disable_keep_alives"` + DisableKeepAlivesAPIProxy bool `hcl:"-"` + DisableKeepAlivesAutoAuth bool `hcl:"-"` +} + +const ( + DisableIdleConnsEnv = "VAULT_PROXY_DISABLE_IDLE_CONNECTIONS" + DisableKeepAlivesEnv = "VAULT_PROXY_DISABLE_KEEP_ALIVES" +) + +func (c *Config) Prune() { + for _, l := range c.Listeners { + l.RawConfig = nil + l.Profiling.UnusedKeys = nil + l.Telemetry.UnusedKeys = nil + l.CustomResponseHeaders = nil + } + c.FoundKeys = nil + c.UnusedKeys = nil + c.SharedConfig.FoundKeys = nil + c.SharedConfig.UnusedKeys = nil + if c.Telemetry != nil { + c.Telemetry.FoundKeys = nil + c.Telemetry.UnusedKeys = nil + } +} + +type Retry struct { + NumRetries int `hcl:"num_retries"` +} + +// Vault contains configuration for connecting to Vault servers +type Vault struct { + Address string `hcl:"address"` + CACert string `hcl:"ca_cert"` + CAPath string `hcl:"ca_path"` + TLSSkipVerify bool `hcl:"-"` + TLSSkipVerifyRaw interface{} `hcl:"tls_skip_verify"` + ClientCert string `hcl:"client_cert"` + ClientKey string `hcl:"client_key"` + TLSServerName string `hcl:"tls_server_name"` + Retry *Retry `hcl:"retry"` +} + +// transportDialer is an interface that allows passing a custom dialer function +// to an HTTP client's transport config +type transportDialer interface { + // Dial is intended to match https://pkg.go.dev/net#Dialer.Dial + Dial(network, address string) (net.Conn, error) + + // DialContext is intended to match https://pkg.go.dev/net#Dialer.DialContext + DialContext(ctx context.Context, network, address string) (net.Conn, error) +} + +// APIProxy contains any configuration needed for proxy mode +type APIProxy struct { + UseAutoAuthTokenRaw interface{} `hcl:"use_auto_auth_token"` + UseAutoAuthToken bool `hcl:"-"` + ForceAutoAuthToken bool `hcl:"-"` + EnforceConsistency string `hcl:"enforce_consistency"` + WhenInconsistent string `hcl:"when_inconsistent"` +} + +// Cache contains any configuration needed for Cache mode +type Cache struct { + Persist *Persist `hcl:"persist"` + InProcDialer transportDialer `hcl:"-"` +} + +// Persist contains configuration needed for persistent caching +type Persist struct { + Type string + Path string `hcl:"path"` + KeepAfterImport bool `hcl:"keep_after_import"` + ExitOnErr bool `hcl:"exit_on_err"` + ServiceAccountTokenFile string `hcl:"service_account_token_file"` +} + +// AutoAuth is the configured authentication method and sinks +type AutoAuth struct { + Method *Method `hcl:"-"` + Sinks []*Sink `hcl:"sinks"` + + // NOTE: This is unsupported outside of testing and may disappear at any + // time. + EnableReauthOnNewCredentials bool `hcl:"enable_reauth_on_new_credentials"` +} + +// Method represents the configuration for the authentication backend +type Method struct { + Type string + MountPath string `hcl:"mount_path"` + WrapTTLRaw interface{} `hcl:"wrap_ttl"` + WrapTTL time.Duration `hcl:"-"` + MinBackoffRaw interface{} `hcl:"min_backoff"` + MinBackoff time.Duration `hcl:"-"` + MaxBackoffRaw interface{} `hcl:"max_backoff"` + MaxBackoff time.Duration `hcl:"-"` + Namespace string `hcl:"namespace"` + ExitOnError bool `hcl:"exit_on_err"` + Config map[string]interface{} +} + +// Sink defines a location to write the authenticated token +type Sink struct { + Type string + WrapTTLRaw interface{} `hcl:"wrap_ttl"` + WrapTTL time.Duration `hcl:"-"` + DHType string `hcl:"dh_type"` + DeriveKey bool `hcl:"derive_key"` + DHPath string `hcl:"dh_path"` + AAD string `hcl:"aad"` + AADEnvVar string `hcl:"aad_env_var"` + Config map[string]interface{} +} + +func NewConfig() *Config { + return &Config{ + SharedConfig: new(configutil.SharedConfig), + } +} + +// Merge merges two Proxy configurations. +func (c *Config) Merge(c2 *Config) *Config { + if c2 == nil { + return c + } + + result := NewConfig() + + result.SharedConfig = c.SharedConfig + if c2.SharedConfig != nil { + result.SharedConfig = c.SharedConfig.Merge(c2.SharedConfig) + } + + result.AutoAuth = c.AutoAuth + if c2.AutoAuth != nil { + result.AutoAuth = c2.AutoAuth + } + + result.Cache = c.Cache + if c2.Cache != nil { + result.Cache = c2.Cache + } + + result.APIProxy = c.APIProxy + if c2.APIProxy != nil { + result.APIProxy = c2.APIProxy + } + + result.DisableMlock = c.DisableMlock + if c2.DisableMlock { + result.DisableMlock = c2.DisableMlock + } + + // For these, ignore the non-specific one and overwrite them all + result.DisableIdleConnsAutoAuth = c.DisableIdleConnsAutoAuth + if c2.DisableIdleConnsAutoAuth { + result.DisableIdleConnsAutoAuth = c2.DisableIdleConnsAutoAuth + } + + result.DisableIdleConnsAPIProxy = c.DisableIdleConnsAPIProxy + if c2.DisableIdleConnsAPIProxy { + result.DisableIdleConnsAPIProxy = c2.DisableIdleConnsAPIProxy + } + + result.DisableKeepAlivesAutoAuth = c.DisableKeepAlivesAutoAuth + if c2.DisableKeepAlivesAutoAuth { + result.DisableKeepAlivesAutoAuth = c2.DisableKeepAlivesAutoAuth + } + + result.DisableKeepAlivesAPIProxy = c.DisableKeepAlivesAPIProxy + if c2.DisableKeepAlivesAPIProxy { + result.DisableKeepAlivesAPIProxy = c2.DisableKeepAlivesAPIProxy + } + + result.ExitAfterAuth = c.ExitAfterAuth + if c2.ExitAfterAuth { + result.ExitAfterAuth = c2.ExitAfterAuth + } + + result.Vault = c.Vault + if c2.Vault != nil { + result.Vault = c2.Vault + } + + result.PidFile = c.PidFile + if c2.PidFile != "" { + result.PidFile = c2.PidFile + } + + return result +} + +// ValidateConfig validates a Vault configuration after it has been fully merged together, to +// ensure that required combinations of configs are there +func (c *Config) ValidateConfig() error { + if c.Cache != nil { + if len(c.Listeners) < 1 { + return fmt.Errorf("enabling the cache requires at least 1 listener to be defined") + } + } + + if c.APIProxy != nil { + if len(c.Listeners) < 1 { + return fmt.Errorf("configuring the api_proxy requires at least 1 listener to be defined") + } + + if c.APIProxy.UseAutoAuthToken { + if c.AutoAuth == nil { + return fmt.Errorf("api_proxy.use_auto_auth_token is true but auto_auth not configured") + } + if c.AutoAuth != nil && c.AutoAuth.Method != nil && c.AutoAuth.Method.WrapTTL > 0 { + return fmt.Errorf("api_proxy.use_auto_auth_token is true and auto_auth uses wrapping") + } + } + } + + if c.AutoAuth != nil { + if len(c.AutoAuth.Sinks) == 0 && + (c.APIProxy == nil || !c.APIProxy.UseAutoAuthToken) { + return fmt.Errorf("auto_auth requires at least one sink or api_proxy.use_auto_auth_token=true") + } + } + + if c.AutoAuth == nil && c.Cache == nil && len(c.Listeners) == 0 { + return fmt.Errorf("no auto_auth, cache, or listener block found in config") + } + + return nil +} + +// LoadConfig loads the configuration at the given path, regardless if +// it's a file or directory. +func LoadConfig(path string) (*Config, error) { + fi, err := os.Stat(path) + if err != nil { + return nil, err + } + + if fi.IsDir() { + return LoadConfigDir(path) + } + return LoadConfigFile(path) +} + +// LoadConfigDir loads the configuration at the given path if it's a directory +func LoadConfigDir(dir string) (*Config, error) { + f, err := os.Open(dir) + if err != nil { + return nil, err + } + defer f.Close() + + fi, err := f.Stat() + if err != nil { + return nil, err + } + if !fi.IsDir() { + return nil, fmt.Errorf("configuration path must be a directory: %q", dir) + } + + var files []string + err = nil + for err != io.EOF { + var fis []os.FileInfo + fis, err = f.Readdir(128) + if err != nil && err != io.EOF { + return nil, err + } + + for _, fi := range fis { + // Ignore directories + if fi.IsDir() { + continue + } + + // Only care about files that are valid to load. + name := fi.Name() + skip := true + if strings.HasSuffix(name, ".hcl") { + skip = false + } else if strings.HasSuffix(name, ".json") { + skip = false + } + if skip || isTemporaryFile(name) { + continue + } + + path := filepath.Join(dir, name) + files = append(files, path) + } + } + + result := NewConfig() + for _, f := range files { + config, err := LoadConfigFile(f) + if err != nil { + return nil, fmt.Errorf("error loading %q: %w", f, err) + } + + if result == nil { + result = config + } else { + result = result.Merge(config) + } + } + + return result, nil +} + +// isTemporaryFile returns true or false depending on whether the +// provided file name is a temporary file for the following editors: +// emacs or vim. +func isTemporaryFile(name string) bool { + return strings.HasSuffix(name, "~") || // vim + strings.HasPrefix(name, ".#") || // emacs + (strings.HasPrefix(name, "#") && strings.HasSuffix(name, "#")) // emacs +} + +// LoadConfigFile loads the configuration at the given path if it's a file +func LoadConfigFile(path string) (*Config, error) { + fi, err := os.Stat(path) + if err != nil { + return nil, err + } + + if fi.IsDir() { + return nil, fmt.Errorf("location is a directory, not a file") + } + + // Read the file + d, err := os.ReadFile(path) + if err != nil { + return nil, err + } + + // Parse! + obj, err := hcl.Parse(string(d)) + if err != nil { + return nil, err + } + + // Attribute + ast.Walk(obj, func(n ast.Node) (ast.Node, bool) { + if k, ok := n.(*ast.ObjectKey); ok { + k.Token.Pos.Filename = path + } + return n, true + }) + + // Start building the result + result := NewConfig() + if err := hcl.DecodeObject(result, obj); err != nil { + return nil, err + } + + sharedConfig, err := configutil.ParseConfig(string(d)) + if err != nil { + return nil, err + } + + // Pruning custom headers for Vault for now + for _, ln := range sharedConfig.Listeners { + ln.CustomResponseHeaders = nil + } + + result.SharedConfig = sharedConfig + + list, ok := obj.Node.(*ast.ObjectList) + if !ok { + return nil, fmt.Errorf("error parsing: file doesn't contain a root object") + } + + if err := parseAutoAuth(result, list); err != nil { + return nil, fmt.Errorf("error parsing 'auto_auth': %w", err) + } + + if err := parseCache(result, list); err != nil { + return nil, fmt.Errorf("error parsing 'cache':%w", err) + } + + if err := parseAPIProxy(result, list); err != nil { + return nil, fmt.Errorf("error parsing 'api_proxy':%w", err) + } + + err = parseVault(result, list) + if err != nil { + return nil, fmt.Errorf("error parsing 'vault':%w", err) + } + + if result.Vault != nil { + // Set defaults + if result.Vault.Retry == nil { + result.Vault.Retry = &Retry{} + } + switch result.Vault.Retry.NumRetries { + case 0: + result.Vault.Retry.NumRetries = ctconfig.DefaultRetryAttempts + case -1: + result.Vault.Retry.NumRetries = 0 + } + } + + if disableIdleConnsEnv := os.Getenv(DisableIdleConnsEnv); disableIdleConnsEnv != "" { + result.DisableIdleConns, err = parseutil.ParseCommaStringSlice(strings.ToLower(disableIdleConnsEnv)) + if err != nil { + return nil, fmt.Errorf("error parsing environment variable %s: %v", DisableIdleConnsEnv, err) + } + } + + for _, subsystem := range result.DisableIdleConns { + switch subsystem { + case "auto-auth": + result.DisableIdleConnsAutoAuth = true + case "caching", "proxying": + result.DisableIdleConnsAPIProxy = true + case "": + continue + default: + return nil, fmt.Errorf("unknown disable_idle_connections value: %s", subsystem) + } + } + + if disableKeepAlivesEnv := os.Getenv(DisableKeepAlivesEnv); disableKeepAlivesEnv != "" { + result.DisableKeepAlives, err = parseutil.ParseCommaStringSlice(strings.ToLower(disableKeepAlivesEnv)) + if err != nil { + return nil, fmt.Errorf("error parsing environment variable %s: %v", DisableKeepAlivesEnv, err) + } + } + + for _, subsystem := range result.DisableKeepAlives { + switch subsystem { + case "auto-auth": + result.DisableKeepAlivesAutoAuth = true + case "caching", "proxying": + result.DisableKeepAlivesAPIProxy = true + case "": + continue + default: + return nil, fmt.Errorf("unknown disable_keep_alives value: %s", subsystem) + } + } + + return result, nil +} + +func parseVault(result *Config, list *ast.ObjectList) error { + name := "vault" + + vaultList := list.Filter(name) + if len(vaultList.Items) == 0 { + return nil + } + + if len(vaultList.Items) > 1 { + return fmt.Errorf("one and only one %q block is required", name) + } + + item := vaultList.Items[0] + + var v Vault + err := hcl.DecodeObject(&v, item.Val) + if err != nil { + return err + } + + if v.TLSSkipVerifyRaw != nil { + v.TLSSkipVerify, err = parseutil.ParseBool(v.TLSSkipVerifyRaw) + if err != nil { + return err + } + } + + result.Vault = &v + + subs, ok := item.Val.(*ast.ObjectType) + if !ok { + return fmt.Errorf("could not parse %q as an object", name) + } + + if err := parseRetry(result, subs.List); err != nil { + return fmt.Errorf("error parsing 'retry': %w", err) + } + + return nil +} + +func parseRetry(result *Config, list *ast.ObjectList) error { + name := "retry" + + retryList := list.Filter(name) + if len(retryList.Items) == 0 { + return nil + } + + if len(retryList.Items) > 1 { + return fmt.Errorf("one and only one %q block is required", name) + } + + item := retryList.Items[0] + + var r Retry + err := hcl.DecodeObject(&r, item.Val) + if err != nil { + return err + } + + result.Vault.Retry = &r + + return nil +} + +func parseAPIProxy(result *Config, list *ast.ObjectList) error { + name := "api_proxy" + + apiProxyList := list.Filter(name) + if len(apiProxyList.Items) == 0 { + return nil + } + + if len(apiProxyList.Items) > 1 { + return fmt.Errorf("one and only one %q block is required", name) + } + + item := apiProxyList.Items[0] + + var apiProxy APIProxy + err := hcl.DecodeObject(&apiProxy, item.Val) + if err != nil { + return err + } + + if apiProxy.UseAutoAuthTokenRaw != nil { + apiProxy.UseAutoAuthToken, err = parseutil.ParseBool(apiProxy.UseAutoAuthTokenRaw) + if err != nil { + // Could be a value of "force" instead of "true"/"false" + switch apiProxy.UseAutoAuthTokenRaw.(type) { + case string: + v := apiProxy.UseAutoAuthTokenRaw.(string) + + if !strings.EqualFold(v, "force") { + return fmt.Errorf("value of 'use_auto_auth_token' can be either true/false/force, %q is an invalid option", apiProxy.UseAutoAuthTokenRaw) + } + apiProxy.UseAutoAuthToken = true + apiProxy.ForceAutoAuthToken = true + + default: + return err + } + } + } + result.APIProxy = &apiProxy + + return nil +} + +func parseCache(result *Config, list *ast.ObjectList) error { + name := "cache" + + cacheList := list.Filter(name) + if len(cacheList.Items) == 0 { + return nil + } + + if len(cacheList.Items) > 1 { + return fmt.Errorf("one and only one %q block is required", name) + } + + item := cacheList.Items[0] + + var c Cache + err := hcl.DecodeObject(&c, item.Val) + if err != nil { + return err + } + + result.Cache = &c + + subs, ok := item.Val.(*ast.ObjectType) + if !ok { + return fmt.Errorf("could not parse %q as an object", name) + } + subList := subs.List + if err := parsePersist(result, subList); err != nil { + return fmt.Errorf("error parsing persist: %w", err) + } + + return nil +} + +func parsePersist(result *Config, list *ast.ObjectList) error { + name := "persist" + + persistList := list.Filter(name) + if len(persistList.Items) == 0 { + return nil + } + + if len(persistList.Items) > 1 { + return fmt.Errorf("only one %q block is required", name) + } + + item := persistList.Items[0] + + var p Persist + err := hcl.DecodeObject(&p, item.Val) + if err != nil { + return err + } + + if p.Type == "" { + if len(item.Keys) == 1 { + p.Type = strings.ToLower(item.Keys[0].Token.Value().(string)) + } + if p.Type == "" { + return errors.New("persist type must be specified") + } + } + + result.Cache.Persist = &p + + return nil +} + +func parseAutoAuth(result *Config, list *ast.ObjectList) error { + name := "auto_auth" + + autoAuthList := list.Filter(name) + if len(autoAuthList.Items) == 0 { + return nil + } + if len(autoAuthList.Items) > 1 { + return fmt.Errorf("at most one %q block is allowed", name) + } + + // Get our item + item := autoAuthList.Items[0] + + var a AutoAuth + if err := hcl.DecodeObject(&a, item.Val); err != nil { + return err + } + + result.AutoAuth = &a + + subs, ok := item.Val.(*ast.ObjectType) + if !ok { + return fmt.Errorf("could not parse %q as an object", name) + } + subList := subs.List + + if err := parseMethod(result, subList); err != nil { + return fmt.Errorf("error parsing 'method': %w", err) + } + if a.Method == nil { + return fmt.Errorf("no 'method' block found") + } + + if err := parseSinks(result, subList); err != nil { + return fmt.Errorf("error parsing 'sink' stanzas: %w", err) + } + + if result.AutoAuth.Method.WrapTTL > 0 { + if len(result.AutoAuth.Sinks) != 1 { + return fmt.Errorf("error parsing auto_auth: wrapping enabled on auth method and 0 or many sinks defined") + } + + if result.AutoAuth.Sinks[0].WrapTTL > 0 { + return fmt.Errorf("error parsing auto_auth: wrapping enabled both on auth method and sink") + } + } + + if result.AutoAuth.Method.MaxBackoffRaw != nil { + var err error + if result.AutoAuth.Method.MaxBackoff, err = parseutil.ParseDurationSecond(result.AutoAuth.Method.MaxBackoffRaw); err != nil { + return err + } + result.AutoAuth.Method.MaxBackoffRaw = nil + } + + if result.AutoAuth.Method.MinBackoffRaw != nil { + var err error + if result.AutoAuth.Method.MinBackoff, err = parseutil.ParseDurationSecond(result.AutoAuth.Method.MinBackoffRaw); err != nil { + return err + } + result.AutoAuth.Method.MinBackoffRaw = nil + } + + return nil +} + +func parseMethod(result *Config, list *ast.ObjectList) error { + name := "method" + + methodList := list.Filter(name) + if len(methodList.Items) != 1 { + return fmt.Errorf("one and only one %q block is required", name) + } + + // Get our item + item := methodList.Items[0] + + var m Method + if err := hcl.DecodeObject(&m, item.Val); err != nil { + return err + } + + if m.Type == "" { + if len(item.Keys) == 1 { + m.Type = strings.ToLower(item.Keys[0].Token.Value().(string)) + } + if m.Type == "" { + return errors.New("method type must be specified") + } + } + + // Default to Vault's default + if m.MountPath == "" { + m.MountPath = fmt.Sprintf("auth/%s", m.Type) + } + // Standardize on no trailing slash + m.MountPath = strings.TrimSuffix(m.MountPath, "/") + + if m.WrapTTLRaw != nil { + var err error + if m.WrapTTL, err = parseutil.ParseDurationSecond(m.WrapTTLRaw); err != nil { + return err + } + m.WrapTTLRaw = nil + } + + // Canonicalize namespace path if provided + m.Namespace = namespace.Canonicalize(m.Namespace) + + result.AutoAuth.Method = &m + return nil +} + +func parseSinks(result *Config, list *ast.ObjectList) error { + name := "sink" + + sinkList := list.Filter(name) + if len(sinkList.Items) < 1 { + return nil + } + + var ts []*Sink + + for _, item := range sinkList.Items { + var s Sink + if err := hcl.DecodeObject(&s, item.Val); err != nil { + return err + } + + if s.Type == "" { + if len(item.Keys) == 1 { + s.Type = strings.ToLower(item.Keys[0].Token.Value().(string)) + } + if s.Type == "" { + return errors.New("sink type must be specified") + } + } + + if s.WrapTTLRaw != nil { + var err error + if s.WrapTTL, err = parseutil.ParseDurationSecond(s.WrapTTLRaw); err != nil { + return multierror.Prefix(err, fmt.Sprintf("sink.%s", s.Type)) + } + s.WrapTTLRaw = nil + } + + switch s.DHType { + case "": + case "curve25519": + default: + return multierror.Prefix(errors.New("invalid value for 'dh_type'"), fmt.Sprintf("sink.%s", s.Type)) + } + + if s.AADEnvVar != "" { + s.AAD = os.Getenv(s.AADEnvVar) + s.AADEnvVar = "" + } + + switch { + case s.DHPath == "" && s.DHType == "": + if s.AAD != "" { + return multierror.Prefix(errors.New("specifying AAD data without 'dh_type' does not make sense"), fmt.Sprintf("sink.%s", s.Type)) + } + if s.DeriveKey { + return multierror.Prefix(errors.New("specifying 'derive_key' data without 'dh_type' does not make sense"), fmt.Sprintf("sink.%s", s.Type)) + } + case s.DHPath != "" && s.DHType != "": + default: + return multierror.Prefix(errors.New("'dh_type' and 'dh_path' must be specified together"), fmt.Sprintf("sink.%s", s.Type)) + } + + ts = append(ts, &s) + } + + result.AutoAuth.Sinks = ts + return nil +} diff --git a/command/proxy/config/config_test.go b/command/proxy/config/config_test.go new file mode 100644 index 000000000..6a9bd8a6f --- /dev/null +++ b/command/proxy/config/config_test.go @@ -0,0 +1,118 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package config + +import ( + "testing" + + "github.com/go-test/deep" + "github.com/hashicorp/vault/internalshared/configutil" +) + +// TestLoadConfigFile_ProxyCache tests loading a config file containing a cache +// as well as a valid proxy config. +func TestLoadConfigFile_ProxyCache(t *testing.T) { + config, err := LoadConfigFile("./test-fixtures/config-cache.hcl") + if err != nil { + t.Fatal(err) + } + + expected := &Config{ + SharedConfig: &configutil.SharedConfig{ + PidFile: "./pidfile", + Listeners: []*configutil.Listener{ + { + Type: "unix", + Address: "/path/to/socket", + TLSDisable: true, + SocketMode: "configmode", + SocketUser: "configuser", + SocketGroup: "configgroup", + }, + { + Type: "tcp", + Address: "127.0.0.1:8300", + TLSDisable: true, + }, + { + Type: "tcp", + Address: "127.0.0.1:3000", + Role: "metrics_only", + TLSDisable: true, + }, + { + Type: "tcp", + Role: "default", + Address: "127.0.0.1:8400", + TLSKeyFile: "/path/to/cakey.pem", + TLSCertFile: "/path/to/cacert.pem", + }, + }, + }, + AutoAuth: &AutoAuth{ + Method: &Method{ + Type: "aws", + MountPath: "auth/aws", + Config: map[string]interface{}{ + "role": "foobar", + }, + }, + Sinks: []*Sink{ + { + Type: "file", + DHType: "curve25519", + DHPath: "/tmp/file-foo-dhpath", + AAD: "foobar", + Config: map[string]interface{}{ + "path": "/tmp/file-foo", + }, + }, + }, + }, + APIProxy: &APIProxy{ + EnforceConsistency: "always", + WhenInconsistent: "retry", + UseAutoAuthTokenRaw: true, + UseAutoAuthToken: true, + ForceAutoAuthToken: false, + }, + Cache: &Cache{ + Persist: &Persist{ + Type: "kubernetes", + Path: "/vault/agent-cache/", + KeepAfterImport: true, + ExitOnErr: true, + ServiceAccountTokenFile: "/tmp/serviceaccount/token", + }, + }, + Vault: &Vault{ + Address: "http://127.0.0.1:1111", + CACert: "config_ca_cert", + CAPath: "config_ca_path", + TLSSkipVerifyRaw: interface{}("true"), + TLSSkipVerify: true, + ClientCert: "config_client_cert", + ClientKey: "config_client_key", + Retry: &Retry{ + NumRetries: 12, + }, + }, + } + + config.Prune() + if diff := deep.Equal(config, expected); diff != nil { + t.Fatal(diff) + } + + config, err = LoadConfigFile("./test-fixtures/config-cache-embedded-type.hcl") + if err != nil { + t.Fatal(err) + } + expected.Vault.TLSSkipVerifyRaw = interface{}(true) + + config.Prune() + if diff := deep.Equal(config, expected); diff != nil { + t.Fatal(diff) + } +} diff --git a/command/proxy/config/test-fixtures/config-cache-embedded-type.hcl b/command/proxy/config/test-fixtures/config-cache-embedded-type.hcl new file mode 100644 index 000000000..a7d8ef44c --- /dev/null +++ b/command/proxy/config/test-fixtures/config-cache-embedded-type.hcl @@ -0,0 +1,77 @@ +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: MPL-2.0 + +pid_file = "./pidfile" + +auto_auth { + method { + type = "aws" + config = { + role = "foobar" + } + } + + sink { + type = "file" + config = { + path = "/tmp/file-foo" + } + aad = "foobar" + dh_type = "curve25519" + dh_path = "/tmp/file-foo-dhpath" + } +} + +api_proxy { + use_auto_auth_token = true + enforce_consistency = "always" + when_inconsistent = "retry" +} + +cache { + persist "kubernetes" { + path = "/vault/agent-cache/" + keep_after_import = true + exit_on_err = true + service_account_token_file = "/tmp/serviceaccount/token" + } +} + +listener { + type = "unix" + address = "/path/to/socket" + tls_disable = true + socket_mode = "configmode" + socket_user = "configuser" + socket_group = "configgroup" +} + +listener { + type = "tcp" + address = "127.0.0.1:8300" + tls_disable = true +} + +listener { + type = "tcp" + address = "127.0.0.1:3000" + tls_disable = true + role = "metrics_only" +} + +listener { + type = "tcp" + role = "default" + address = "127.0.0.1:8400" + tls_key_file = "/path/to/cakey.pem" + tls_cert_file = "/path/to/cacert.pem" +} + +vault { + address = "http://127.0.0.1:1111" + ca_cert = "config_ca_cert" + ca_path = "config_ca_path" + tls_skip_verify = true + client_cert = "config_client_cert" + client_key = "config_client_key" +} diff --git a/command/proxy/config/test-fixtures/config-cache.hcl b/command/proxy/config/test-fixtures/config-cache.hcl new file mode 100644 index 000000000..d770391fe --- /dev/null +++ b/command/proxy/config/test-fixtures/config-cache.hcl @@ -0,0 +1,75 @@ +# Copyright (c) HashiCorp, Inc. +# SPDX-License-Identifier: MPL-2.0 + +pid_file = "./pidfile" + +auto_auth { + method { + type = "aws" + config = { + role = "foobar" + } + } + + sink { + type = "file" + config = { + path = "/tmp/file-foo" + } + aad = "foobar" + dh_type = "curve25519" + dh_path = "/tmp/file-foo-dhpath" + } +} + +api_proxy { + use_auto_auth_token = true + enforce_consistency = "always" + when_inconsistent = "retry" +} + +cache { + persist = { + type = "kubernetes" + path = "/vault/agent-cache/" + keep_after_import = true + exit_on_err = true + service_account_token_file = "/tmp/serviceaccount/token" + } +} + +listener "unix" { + address = "/path/to/socket" + tls_disable = true + socket_mode = "configmode" + socket_user = "configuser" + socket_group = "configgroup" +} + +listener "tcp" { + address = "127.0.0.1:8300" + tls_disable = true +} + +listener { + type = "tcp" + address = "127.0.0.1:3000" + tls_disable = true + role = "metrics_only" +} + +listener "tcp" { + role = "default" + address = "127.0.0.1:8400" + tls_key_file = "/path/to/cakey.pem" + tls_cert_file = "/path/to/cacert.pem" +} + +vault { + address = "http://127.0.0.1:1111" + ca_cert = "config_ca_cert" + ca_path = "config_ca_path" + tls_skip_verify = "true" + client_cert = "config_client_cert" + client_key = "config_client_key" +} diff --git a/command/proxy_test.go b/command/proxy_test.go new file mode 100644 index 000000000..4897602fe --- /dev/null +++ b/command/proxy_test.go @@ -0,0 +1,678 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package command + +import ( + "fmt" + "net/http" + "os" + "sync" + "testing" + "time" + + "github.com/hashicorp/go-hclog" + vaultjwt "github.com/hashicorp/vault-plugin-auth-jwt" + "github.com/hashicorp/vault/api" + credAppRole "github.com/hashicorp/vault/builtin/credential/approle" + "github.com/hashicorp/vault/command/agent" + "github.com/hashicorp/vault/helper/useragent" + vaulthttp "github.com/hashicorp/vault/http" + "github.com/hashicorp/vault/sdk/helper/logging" + "github.com/hashicorp/vault/sdk/logical" + "github.com/hashicorp/vault/vault" + "github.com/mitchellh/cli" +) + +func testProxyCommand(tb testing.TB, logger hclog.Logger) (*cli.MockUi, *ProxyCommand) { + tb.Helper() + + ui := cli.NewMockUi() + return ui, &ProxyCommand{ + BaseCommand: &BaseCommand{ + UI: ui, + }, + ShutdownCh: MakeShutdownCh(), + SighupCh: MakeSighupCh(), + logger: logger, + startedCh: make(chan struct{}, 5), + reloadedCh: make(chan struct{}, 5), + } +} + +// TestProxy_ExitAfterAuth tests the exit_after_auth flag, provided both +// as config and via -exit-after-auth. +func TestProxy_ExitAfterAuth(t *testing.T) { + t.Run("via_config", func(t *testing.T) { + testProxyExitAfterAuth(t, false) + }) + + t.Run("via_flag", func(t *testing.T) { + testProxyExitAfterAuth(t, true) + }) +} + +func testProxyExitAfterAuth(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) + } + + dir := t.TempDir() + inf, err := os.CreateTemp(dir, "auth.jwt.test.") + if err != nil { + t.Fatal(err) + } + in := inf.Name() + inf.Close() + // We remove these files in this test since we don't need the files, we just need + // a non-conflicting file name for the config. + os.Remove(in) + t.Logf("input: %s", in) + + sink1f, err := os.CreateTemp(dir, "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(dir, "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(dir, "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 := testProxyCommand(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 proxy:\n%s", ui.OutputWriter.String()) + t.Logf("error from proxy:\n%s", ui.ErrorWriter.String()) + } + close(doneCh) + }() + + select { + case <-doneCh: + break + case <-time.After(1 * time.Minute): + t.Fatal("timeout reached while waiting for proxy 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") + } +} + +// TestProxy_AutoAuth_UserAgent tests that the User-Agent sent +// to Vault by Vault Proxy is correct when performing Auto-Auth. +// Uses the custom handler userAgentHandler (defined above) so +// that Vault validates the User-Agent on requests sent by Proxy. +func TestProxy_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.ProxyAutoAuthString() + 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 + 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(`{ + "secret_id_num_uses": "10", + "secret_id_ttl": "1m", + "token_max_ttl": "1m", + "token_num_uses": "10", + "token_ttl": "1m", + "policies": "default" + }`) + 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") + defer os.Remove(roleIDPath) + defer os.Remove(secretIDPath) + + 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 proxy picks up the right test + // cluster address + defer os.Setenv(api.EnvVaultAddress, os.Getenv(api.EnvVaultAddress)) + os.Unsetenv(api.EnvVaultAddress) + + // Start proxy + _, cmd := testProxyCommand(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 + proxyClient, err := api.NewClient(conf) + if err != nil { + t.Fatalf("err: %s", err) + } + + proxyClient.SetToken("") + err = proxyClient.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 = proxyClient.NewRequest("GET", "/v1/auth/token/lookup-self") + body = request(t, proxyClient, req, 200) + + close(cmd.ShutdownCh) + wg.Wait() +} + +// TestProxy_APIProxyWithoutCache_UserAgent tests that the User-Agent sent +// to Vault by Vault Proxy 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 Proxy. +func TestProxy_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.ProxyStringWithProxiedUserAgent(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 proxy 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 := testProxyCommand(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") + } + + proxyClient, err := api.NewClient(api.DefaultConfig()) + if err != nil { + t.Fatal(err) + } + proxyClient.AddHeader("User-Agent", userAgentForProxiedClient) + proxyClient.SetToken(serverClient.Token()) + proxyClient.SetMaxRetries(0) + err = proxyClient.SetAddress("http://" + listenAddr) + if err != nil { + t.Fatal(err) + } + + _, err = proxyClient.Auth().Token().LookupSelf() + if err != nil { + t.Fatal(err) + } + + close(cmd.ShutdownCh) + wg.Wait() +} + +// TestProxy_APIProxyWithCache_UserAgent tests that the User-Agent sent +// to Vault by Vault Proxy 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 Proxy. +func TestProxy_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.ProxyStringWithProxiedUserAgent(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 proxy 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 := testProxyCommand(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") + } + + proxyClient, err := api.NewClient(api.DefaultConfig()) + if err != nil { + t.Fatal(err) + } + proxyClient.AddHeader("User-Agent", userAgentForProxiedClient) + proxyClient.SetToken(serverClient.Token()) + proxyClient.SetMaxRetries(0) + err = proxyClient.SetAddress("http://" + listenAddr) + if err != nil { + t.Fatal(err) + } + + _, err = proxyClient.Auth().Token().LookupSelf() + if err != nil { + t.Fatal(err) + } + + close(cmd.ShutdownCh) + wg.Wait() +} + +// TestProxy_Cache_DynamicSecret Tests that the cache successfully caches a dynamic secret +// going through the Proxy, +func TestProxy_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 proxy + _, cmd := testProxyCommand(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") + } + + proxyClient, err := api.NewClient(api.DefaultConfig()) + if err != nil { + t.Fatal(err) + } + proxyClient.SetToken(serverClient.Token()) + proxyClient.SetMaxRetries(0) + err = proxyClient.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 := proxyClient.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 = proxyClient.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() +} diff --git a/command/server/config_test_helpers.go b/command/server/config_test_helpers.go index c171fdbe6..f5136449c 100644 --- a/command/server/config_test_helpers.go +++ b/command/server/config_test_helpers.go @@ -851,6 +851,9 @@ listener "tcp" { agent_api { enable_quit = true } + proxy_api { + enable_quit = true + } }`)) config := Config{ @@ -891,6 +894,9 @@ listener "tcp" { AgentAPI: &configutil.AgentAPI{ EnableQuit: true, }, + ProxyAPI: &configutil.ProxyAPI{ + EnableQuit: true, + }, CustomResponseHeaders: DefaultCustomHeaders, }, }, diff --git a/helper/useragent/useragent.go b/helper/useragent/useragent.go index bc5565a76..f1aa63650 100644 --- a/helper/useragent/useragent.go +++ b/helper/useragent/useragent.go @@ -74,3 +74,29 @@ func AgentAutoAuthString() string { return fmt.Sprintf("Vault Agent Auto-Auth/%s (+%s; %s)", versionFunc(), projectURL, rt) } + +// ProxyString returns the consistent user-agent string for Vault Proxy API Proxying. +// +// e.g. Vault Proxy API Proxy/0.10.4 (+https://www.vaultproject.io/; go1.10.1) +func ProxyString() string { + return fmt.Sprintf("Vault Proxy API Proxy/%s (+%s; %s)", + versionFunc(), projectURL, rt) +} + +// ProxyStringWithProxiedUserAgent returns the consistent user-agent +// string for Vault Proxy API Proxying, keeping the User-Agent of the proxied +// client as an extension to this UserAgent +// +// e.g. Vault Proxy API Proxy/0.10.4 (+https://www.vaultproject.io/; go1.10.1); proxiedUserAgent +func ProxyStringWithProxiedUserAgent(proxiedUserAgent string) string { + return fmt.Sprintf("Vault Proxy API Proxy/%s (+%s; %s); %s", + versionFunc(), projectURL, rt, proxiedUserAgent) +} + +// ProxyAutoAuthString returns the consistent user-agent string for Vault Agent Auto-Auth. +// +// e.g. Vault Proxy Auto-Auth/0.10.4 (+https://www.vaultproject.io/; go1.10.1) +func ProxyAutoAuthString() string { + return fmt.Sprintf("Vault Proxy Auto-Auth/%s (+%s; %s)", + versionFunc(), projectURL, rt) +} diff --git a/internalshared/configutil/listener.go b/internalshared/configutil/listener.go index 5418f6643..5e9373a16 100644 --- a/internalshared/configutil/listener.go +++ b/internalshared/configutil/listener.go @@ -100,6 +100,8 @@ type Listener struct { AgentAPI *AgentAPI `hcl:"agent_api"` + ProxyAPI *ProxyAPI `hcl:"proxy_api"` + Telemetry ListenerTelemetry `hcl:"telemetry"` Profiling ListenerProfiling `hcl:"profiling"` InFlightRequestLogging ListenerInFlightRequestLogging `hcl:"inflight_requests_logging"` @@ -123,6 +125,11 @@ type AgentAPI struct { EnableQuit bool `hcl:"enable_quit"` } +// ProxyAPI allows users to select which parts of the Vault Proxy API they want enabled. +type ProxyAPI struct { + EnableQuit bool `hcl:"enable_quit"` +} + func (l *Listener) GoString() string { return fmt.Sprintf("*%#v", *l) } diff --git a/sdk/helper/consts/proxy.go b/sdk/helper/consts/proxy.go new file mode 100644 index 000000000..0fc4117cc --- /dev/null +++ b/sdk/helper/consts/proxy.go @@ -0,0 +1,15 @@ +// Copyright (c) HashiCorp, Inc. +// SPDX-License-Identifier: MPL-2.0 + +package consts + +// ProxyPathCacheClear is the path that the proxy will use as its cache-clear +// endpoint. +const ProxyPathCacheClear = "/proxy/v1/cache-clear" + +// ProxyPathMetrics is the path the proxy will use to expose its internal +// metrics. +const ProxyPathMetrics = "/proxy/v1/metrics" + +// ProxyPathQuit is the path that the proxy will use to trigger stopping it. +const ProxyPathQuit = "/proxy/v1/quit"