From 993a1ae9f2f645d579e6e54809d4d33927fdb1e9 Mon Sep 17 00:00:00 2001 From: Michel Vocks Date: Fri, 4 Oct 2019 09:29:51 +0200 Subject: [PATCH] Add config parameter to allow unauthenticated metrics access (#7550) * Implement config parameter to allow unathenticated metricss access * Add unit test for unauthenticated metrics access parameter * go mod tidy --- command/server.go | 58 ++++++++++++++++++++++++++------------- go.mod | 1 - http/handler.go | 5 ++++ http/sys_metrics.go | 32 +++++++++++++++++++++ http/sys_metrics_test.go | 50 +++++++++++++++++++++++++++++++++ vault/core.go | 6 ++++ vault/request_handling.go | 9 +++--- vault/testing.go | 1 + 8 files changed, 138 insertions(+), 24 deletions(-) create mode 100644 http/sys_metrics.go create mode 100644 http/sys_metrics_test.go diff --git a/command/server.go b/command/server.go index 279a643ce62b..8cf7b49600f8 100644 --- a/command/server.go +++ b/command/server.go @@ -20,25 +20,22 @@ import ( "sync" "time" - "golang.org/x/net/http/httpproxy" - - "github.com/hashicorp/vault/helper/metricsutil" - monitoring "cloud.google.com/go/monitoring/apiv3" - metrics "github.com/armon/go-metrics" + "github.com/armon/go-metrics" "github.com/armon/go-metrics/circonus" "github.com/armon/go-metrics/datadog" "github.com/armon/go-metrics/prometheus" stackdriver "github.com/google/go-metrics-stackdriver" "github.com/hashicorp/errwrap" log "github.com/hashicorp/go-hclog" - multierror "github.com/hashicorp/go-multierror" - sockaddr "github.com/hashicorp/go-sockaddr" + "github.com/hashicorp/go-multierror" + "github.com/hashicorp/go-sockaddr" "github.com/hashicorp/vault/audit" "github.com/hashicorp/vault/command/server" serverseal "github.com/hashicorp/vault/command/server/seal" "github.com/hashicorp/vault/helper/builtinplugins" gatedwriter "github.com/hashicorp/vault/helper/gated-writer" + "github.com/hashicorp/vault/helper/metricsutil" "github.com/hashicorp/vault/helper/namespace" "github.com/hashicorp/vault/helper/reload" vaulthttp "github.com/hashicorp/vault/http" @@ -54,8 +51,9 @@ import ( vaultseal "github.com/hashicorp/vault/vault/seal" shamirseal "github.com/hashicorp/vault/vault/seal/shamir" "github.com/mitchellh/cli" - testing "github.com/mitchellh/go-testing-interface" + "github.com/mitchellh/go-testing-interface" "github.com/posener/complete" + "golang.org/x/net/http/httpproxy" "google.golang.org/api/option" "google.golang.org/grpc/grpclog" ) @@ -125,9 +123,10 @@ type ServerCommand struct { type ServerListener struct { net.Listener - config map[string]interface{} - maxRequestSize int64 - maxRequestDuration time.Duration + config map[string]interface{} + maxRequestSize int64 + maxRequestDuration time.Duration + unauthenticatedMetricsAccess bool } func (c *ServerCommand) Synopsis() string { @@ -988,11 +987,31 @@ CLUSTER_SYNTHESIS_COMPLETE: } props["max_request_duration"] = fmt.Sprintf("%s", maxRequestDuration.String()) + var unauthenticatedMetricsAccess bool + if telemetryRaw, ok := lnConfig.Config["telemetry"]; ok { + telemetry, ok := telemetryRaw.([]map[string]interface{}) + if !ok { + c.UI.Error(fmt.Sprintf("Could not parse telemetry sink value %v", telemetryRaw)) + return 1 + } + + for _, item := range telemetry { + if valRaw, ok := item["unauthenticated_metrics_access"]; ok { + unauthenticatedMetricsAccess, err = parseutil.ParseBool(valRaw) + if err != nil { + c.UI.Error(fmt.Sprintf("Could not parse unauthenticated_metrics_access value %v", valRaw)) + return 1 + } + } + } + } + lns = append(lns, ServerListener{ - Listener: ln, - config: lnConfig.Config, - maxRequestSize: maxRequestSize, - maxRequestDuration: maxRequestDuration, + Listener: ln, + config: lnConfig.Config, + maxRequestSize: maxRequestSize, + maxRequestDuration: maxRequestDuration, + unauthenticatedMetricsAccess: unauthenticatedMetricsAccess, }) // Store the listener props for output later @@ -1229,10 +1248,11 @@ CLUSTER_SYNTHESIS_COMPLETE: // Initialize the HTTP servers for _, ln := range lns { handler := vaulthttp.Handler(&vault.HandlerProperties{ - Core: core, - MaxRequestSize: ln.maxRequestSize, - MaxRequestDuration: ln.maxRequestDuration, - DisablePrintableCheck: config.DisablePrintableCheck, + Core: core, + MaxRequestSize: ln.maxRequestSize, + MaxRequestDuration: ln.maxRequestDuration, + DisablePrintableCheck: config.DisablePrintableCheck, + UnauthenticatedMetricsAccess: ln.unauthenticatedMetricsAccess, }) // We perform validation on the config earlier, we can just cast here diff --git a/go.mod b/go.mod index 015fb8b90a86..1eb02c7d165c 100644 --- a/go.mod +++ b/go.mod @@ -91,7 +91,6 @@ require ( github.com/joyent/triton-go v0.0.0-20190112182421-51ffac552869 github.com/keybase/go-crypto v0.0.0-20190403132359-d65b6b94177f github.com/kr/pretty v0.1.0 - github.com/kr/pty v1.1.3 // indirect github.com/kr/text v0.1.0 github.com/lib/pq v1.2.0 github.com/mattn/go-colorable v0.1.2 diff --git a/http/handler.go b/http/handler.go index 83cc087df4cd..11ee6a88085c 100644 --- a/http/handler.go +++ b/http/handler.go @@ -147,6 +147,11 @@ func Handler(props *vault.HandlerProperties) http.Handler { mux.Handle("/", handleUIRedirect()) } + // Register metrics path without authentication if enabled + if props.UnauthenticatedMetricsAccess { + mux.Handle("/v1/sys/metrics", handleMetricsUnauthenticated(core)) + } + additionalRoutes(mux, core) // Wrap the handler in another handler to trigger all help paths. diff --git a/http/sys_metrics.go b/http/sys_metrics.go new file mode 100644 index 000000000000..2cfce5eda2fb --- /dev/null +++ b/http/sys_metrics.go @@ -0,0 +1,32 @@ +package http + +import ( + "net/http" + + "github.com/hashicorp/vault/helper/metricsutil" + "github.com/hashicorp/vault/sdk/logical" + "github.com/hashicorp/vault/vault" +) + +func handleMetricsUnauthenticated(core *vault.Core) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + req := &logical.Request{Headers: r.Header} + format := r.Form.Get("format") + if format == "" { + format = metricsutil.FormatFromRequest(req) + } + + // Define response + resp, err := core.MetricsHelper().ResponseForFormat(format) + if err != nil { + w.WriteHeader(http.StatusBadRequest) + w.Write([]byte(err.Error())) + return + } + + // Manually extract the logical response and send back the information + w.WriteHeader(resp.Data[logical.HTTPStatusCode].(int)) + w.Header().Set("Content-Type", resp.Data[logical.HTTPContentType].(string)) + w.Write(resp.Data[logical.HTTPRawBody].([]byte)) + }) +} diff --git a/http/sys_metrics_test.go b/http/sys_metrics_test.go new file mode 100644 index 000000000000..fda7430aab1e --- /dev/null +++ b/http/sys_metrics_test.go @@ -0,0 +1,50 @@ +package http + +import ( + "testing" + "time" + + "github.com/armon/go-metrics" + "github.com/hashicorp/vault/helper/metricsutil" + "github.com/hashicorp/vault/vault" +) + +func TestSysMetricsUnauthenticated(t *testing.T) { + inm := metrics.NewInmemSink(10*time.Second, time.Minute) + metrics.DefaultInmemSignal(inm) + conf := &vault.CoreConfig{ + BuiltinRegistry: vault.NewMockBuiltinRegistry(), + MetricsHelper: metricsutil.NewMetricsHelper(inm, false), + } + core, _, token := vault.TestCoreUnsealedWithConfig(t, conf) + ln, addr := TestServer(t, core) + TestServerAuth(t, addr, token) + + // Default: Only authenticated access + resp := testHttpGet(t, "", addr+"/v1/sys/metrics") + testResponseStatus(t, resp, 400) + resp = testHttpGet(t, token, addr+"/v1/sys/metrics") + testResponseStatus(t, resp, 200) + + // Close listener + ln.Close() + + // Setup new custom listener with unauthenticated metrics access + ln, addr = TestListener(t) + props := &vault.HandlerProperties{ + Core: core, + MaxRequestSize: DefaultMaxRequestSize, + UnauthenticatedMetricsAccess: true, + } + TestServerWithListenerAndProperties(t, ln, addr, core, props) + defer ln.Close() + TestServerAuth(t, addr, token) + + // Test without token + resp = testHttpGet(t, "", addr+"/v1/sys/metrics") + testResponseStatus(t, resp, 200) + + // Should also work with token + resp = testHttpGet(t, token, addr+"/v1/sys/metrics") + testResponseStatus(t, resp, 200) +} diff --git a/vault/core.go b/vault/core.go index 17f8c9dc8154..2223a59c80fa 100644 --- a/vault/core.go +++ b/vault/core.go @@ -1979,6 +1979,12 @@ func (c *Core) SetLogLevel(level log.Level) { } } +// MetricsHelper returns the global metrics helper which allows external +// packages to access Vault's internal metrics. +func (c *Core) MetricsHelper() *metricsutil.MetricsHelper { + return c.metricsHelper +} + // BuiltinRegistry is an interface that allows the "vault" package to use // the registry of builtin plugins without getting an import cycle. It // also allows for mocking the registry easily. diff --git a/vault/request_handling.go b/vault/request_handling.go index 0e69ab709c2c..cd67765ac375 100644 --- a/vault/request_handling.go +++ b/vault/request_handling.go @@ -39,10 +39,11 @@ var ( // HandlerProperties is used to seed configuration into a vaulthttp.Handler. // It's in this package to avoid a circular dependency type HandlerProperties struct { - Core *Core - MaxRequestSize int64 - MaxRequestDuration time.Duration - DisablePrintableCheck bool + Core *Core + MaxRequestSize int64 + MaxRequestDuration time.Duration + DisablePrintableCheck bool + UnauthenticatedMetricsAccess bool } // fetchEntityAndDerivedPolicies returns the entity object for the given entity diff --git a/vault/testing.go b/vault/testing.go index 050230950673..e9d6302d07ff 100644 --- a/vault/testing.go +++ b/vault/testing.go @@ -156,6 +156,7 @@ func TestCoreWithSealAndUI(t testing.T, opts *CoreConfig) *Core { conf.Seal = opts.Seal conf.LicensingConfig = opts.LicensingConfig conf.DisableKeyEncodingChecks = opts.DisableKeyEncodingChecks + conf.MetricsHelper = opts.MetricsHelper if opts.Logger != nil { conf.Logger = opts.Logger