CVE-2022-24797 (published 2022-03-31T23:15Z)

Pomerium is an identity-aware access proxy. In distributed service mode, Pomerium's Authenticate service exposes pprof debug and prometheus metrics handlers to untrusted traffic. This can leak potentially sensitive environmental information or lead to limited denial of service conditions. This issue is patched in version v0.17.1 Workarounds: Block access to `/debug` and `/metrics` paths on the authenticate service. This can be done with any L7 proxy, including Pomerium's own proxy service.

Severity: MEDIUM
CVSS v2 base score: 6.4 (AV:N/AC:L/Au:N/C:P/I:N/A:P)
CVSS v3 base score: 9.1, CRITICAL (CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:H)

Weakness information:
cwe_id: NVD-CWE-Other
cwe_name: Other
description: NVD is only using a subset of CWE for mapping instead of the entire CWE, and the weakness type is not covered by that subset.
extended_description: Insufficient Information

Fix information:
repo_url: https://github.com/pomerium/pomerium
commit_details: repo_url: https://github.com/pomerium/pomerium
author: Caleb Doxsey
author_date: 2022-03-30 09:37:37-06:00
msg: authenticate: fix debug and metrics endpoints (#3212)
repository: repo_url: https://github.com/pomerium/pomerium
repo_name: pomerium/pomerium
description: Pomerium is an identity and context-aware access proxy.
repo_language: Go
owner: pomerium
file_changes: filename: config.go
change_type: ModificationType.MODIFY
diff: @@ -18,6 +18,10 @@ type Config struct {
 	HTTPPort string
 	// OutboundPort is the port the outbound gRPC listener is running on.
 	OutboundPort string
+	// MetricsPort is the port the metrics listener is running on.
+	MetricsPort string
+	// DebugPort is the port the debug listener is running on.
+	DebugPort string
 }
 
 // Clone creates a clone of the config.
@@ -32,6 +36,8 @@ func (cfg *Config) Clone() *Config {
 		GRPCPort:     cfg.GRPCPort,
 		HTTPPort:     cfg.HTTPPort,
 		OutboundPort: cfg.OutboundPort,
+		MetricsPort:  cfg.MetricsPort,
+		DebugPort:    cfg.DebugPort,
 	}
 }
 

programming_language: Go
filename: config_source.go
change_type: ModificationType.MODIFY
diff: @@ -110,13 +110,15 @@ func NewFileOrEnvironmentSource(
 		return nil, err
 	}
 
-	ports, err := netutil.AllocatePorts(3)
+	ports, err := netutil.AllocatePorts(5)
 	if err != nil {
 		return nil, err
 	}
 	grpcPort := ports[0]
 	httpPort := ports[1]
 	outboundPort := ports[2]
+	metricsPort := ports[3]
+	debugPort := ports[4]
 
 	cfg := &Config{
 		Options:      options,
@@ -125,6 +127,8 @@ func NewFileOrEnvironmentSource(
 		GRPCPort:     grpcPort,
 		HTTPPort:     httpPort,
 		OutboundPort: outboundPort,
+		MetricsPort:  metricsPort,
+		DebugPort:    debugPort,
 	}
 	metrics.SetConfigInfo(ctx, cfg.Options.Services, "local", cfg.Checksum(), true)
 

programming_language: Go
filename: bootstrap_test.go
change_type: ModificationType.MODIFY
diff: @@ -11,7 +11,7 @@ import (
 )
 
 func TestBuilder_BuildBootstrapAdmin(t *testing.T) {
-	b := New("local-grpc", "local-http", filemgr.NewManager(), nil)
+	b := New("local-grpc", "local-http", "local-metrics", filemgr.NewManager(), nil)
 	t.Run("valid", func(t *testing.T) {
 		adminCfg, err := b.BuildBootstrapAdmin(&config.Config{
 			Options: &config.Options{
@@ -41,7 +41,7 @@ func TestBuilder_BuildBootstrapAdmin(t *testing.T) {
 }
 
 func TestBuilder_BuildBootstrapLayeredRuntime(t *testing.T) {
-	b := New("localhost:1111", "localhost:2222", filemgr.NewManager(), nil)
+	b := New("localhost:1111", "localhost:2222", "localhost:3333", filemgr.NewManager(), nil)
 	staticCfg, err := b.BuildBootstrapLayeredRuntime()
 	assert.NoError(t, err)
 	testutil.AssertProtoJSONEqual(t, `
@@ -58,7 +58,7 @@ func TestBuilder_BuildBootstrapLayeredRuntime(t *testing.T) {
 
 func TestBuilder_BuildBootstrapStaticResources(t *testing.T) {
 	t.Run("valid", func(t *testing.T) {
-		b := New("localhost:1111", "localhost:2222", filemgr.NewManager(), nil)
+		b := New("localhost:1111", "localhost:2222", "localhost:3333", filemgr.NewManager(), nil)
 		staticCfg, err := b.BuildBootstrapStaticResources()
 		assert.NoError(t, err)
 		testutil.AssertProtoJSONEqual(t, `
@@ -90,14 +90,14 @@ func TestBuilder_BuildBootstrapStaticResources(t *testing.T) {
 		`, staticCfg)
 	})
 	t.Run("bad gRPC address", func(t *testing.T) {
-		b := New("xyz:zyx", "localhost:2222", filemgr.NewManager(), nil)
+		b := New("xyz:zyx", "localhost:2222", "localhost:3333", filemgr.NewManager(), nil)
 		_, err := b.BuildBootstrapStaticResources()
 		assert.Error(t, err)
 	})
 }
 
 func TestBuilder_BuildBootstrapStatsConfig(t *testing.T) {
-	b := New("local-grpc", "local-http", filemgr.NewManager(), nil)
+	b := New("local-grpc", "local-http", "local-metrics", filemgr.NewManager(), nil)
 	t.Run("valid", func(t *testing.T) {
 		statsCfg, err := b.BuildBootstrapStatsConfig(&config.Config{
 			Options: &config.Options{

programming_language: Go
filename: builder.go
change_type: ModificationType.MODIFY
diff: @@ -7,23 +7,26 @@ import (
 
 // A Builder builds envoy config from pomerium config.
 type Builder struct {
-	localGRPCAddress string
-	localHTTPAddress string
-	filemgr          *filemgr.Manager
-	reproxy          *reproxy.Handler
+	localGRPCAddress    string
+	localHTTPAddress    string
+	localMetricsAddress string
+	filemgr             *filemgr.Manager
+	reproxy             *reproxy.Handler
 }
 
 // New creates a new Builder.
 func New(
 	localGRPCAddress string,
 	localHTTPAddress string,
+	localMetricsAddress string,
 	fileManager *filemgr.Manager,
 	reproxyHandler *reproxy.Handler,
 ) *Builder {
 	return &Builder{
-		localGRPCAddress: localGRPCAddress,
-		localHTTPAddress: localHTTPAddress,
-		filemgr:          fileManager,
-		reproxy:          reproxyHandler,
+		localGRPCAddress:    localGRPCAddress,
+		localHTTPAddress:    localHTTPAddress,
+		localMetricsAddress: localMetricsAddress,
+		filemgr:             fileManager,
+		reproxy:             reproxyHandler,
 	}
 }

programming_language: Go
filename: clusters.go
change_type: ModificationType.MODIFY
diff: @@ -34,6 +34,10 @@ func (b *Builder) BuildClusters(ctx context.Context, cfg *config.Config) ([]*env
 		Scheme: "http",
 		Host:   b.localHTTPAddress,
 	}
+	metricsURL := &url.URL{
+		Scheme: "http",
+		Host:   b.localMetricsAddress,
+	}
 	authorizeURLs, err := cfg.Options.GetInternalAuthorizeURLs()
 	if err != nil {
 		return nil, err
@@ -53,6 +57,11 @@ func (b *Builder) BuildClusters(ctx context.Context, cfg *config.Config) ([]*env
 		return nil, err
 	}
 
+	controlMetrics, err := b.buildInternalCluster(ctx, cfg.Options, "pomerium-control-plane-metrics", []*url.URL{metricsURL}, upstreamProtocolAuto)
+	if err != nil {
+		return nil, err
+	}
+
 	authorizeCluster, err := b.buildInternalCluster(ctx, cfg.Options, "pomerium-authorize", authorizeURLs, upstreamProtocolHTTP2)
 	if err != nil {
 		return nil, err
@@ -74,6 +83,7 @@ func (b *Builder) BuildClusters(ctx context.Context, cfg *config.Config) ([]*env
 	clusters := []*envoy_config_cluster_v3.Cluster{
 		controlGRPC,
 		controlHTTP,
+		controlMetrics,
 		authorizeCluster,
 		databrokerCluster,
 	}

programming_language: Go
filename: clusters_test.go
change_type: ModificationType.MODIFY
diff: @@ -25,7 +25,7 @@ func Test_buildPolicyTransportSocket(t *testing.T) {
 	cacheDir, _ := os.UserCacheDir()
 	customCA := filepath.Join(cacheDir, "pomerium", "envoy", "files", "custom-ca-32484c314b584447463735303142374c31414145374650305a525539554938594d524855353757313942494d473847535231.pem")
 
-	b := New("local-grpc", "local-http", filemgr.NewManager(), nil)
+	b := New("local-grpc", "local-http", "local-metrics", filemgr.NewManager(), nil)
 	rootCABytes, _ := getCombinedCertificateAuthority("", "")
 	rootCA := b.filemgr.BytesDataSource("ca.pem", rootCABytes).GetFilename()
 
@@ -379,7 +379,7 @@ func Test_buildPolicyTransportSocket(t *testing.T) {
 
 func Test_buildCluster(t *testing.T) {
 	ctx := context.Background()
-	b := New("local-grpc", "local-http", filemgr.NewManager(), nil)
+	b := New("local-grpc", "local-http", "local-metrics", filemgr.NewManager(), nil)
 	rootCABytes, _ := getCombinedCertificateAuthority("", "")
 	rootCA := b.filemgr.BytesDataSource("ca.pem", rootCABytes).GetFilename()
 	o1 := config.NewDefaultOptions()
@@ -858,7 +858,7 @@ func Test_bindConfig(t *testing.T) {
 	ctx, clearTimeout := context.WithTimeout(context.Background(), time.Second*10)
 	defer clearTimeout()
 
-	b := New("local-grpc", "local-http", filemgr.NewManager(), nil)
+	b := New("local-grpc", "local-http", "local-metrics", filemgr.NewManager(), nil)
 	t.Run("no bind config", func(t *testing.T) {
 		cluster, err := b.buildPolicyCluster(ctx, &config.Options{}, &config.Policy{
 			From: "https://from.example.com",

programming_language: Go
filename: listeners.go
change_type: ModificationType.MODIFY
diff: @@ -486,7 +486,7 @@ func (b *Builder) buildMetricsHTTPConnectionManagerFilter() (*envoy_config_liste
 			Action: &envoy_config_route_v3.Route_Route{
 				Route: &envoy_config_route_v3.RouteAction{
 					ClusterSpecifier: &envoy_config_route_v3.RouteAction_Cluster{
-						Cluster: "pomerium-control-plane-http",
+						Cluster: "pomerium-control-plane-metrics",
 					},
 				},
 			},

programming_language: Go
filename: listeners_test.go
change_type: ModificationType.MODIFY
diff: @@ -25,7 +25,7 @@ func Test_buildMetricsHTTPConnectionManagerFilter(t *testing.T) {
 	certFileName := filepath.Join(cacheDir, "pomerium", "envoy", "files", "tls-crt-354e49305a5a39414a545530374e58454e48334148524c4e324258463837364355564c4e4532464b54355139495547514a38.pem")
 	keyFileName := filepath.Join(cacheDir, "pomerium", "envoy", "files", "tls-key-3350415a38414e4e4a4655424e55393430474147324651433949384e485341334b5157364f424b4c5856365a545937383735.pem")
 
-	b := New("local-grpc", "local-http", filemgr.NewManager(), nil)
+	b := New("local-grpc", "local-http", "local-metrics", filemgr.NewManager(), nil)
 	li, err := b.buildMetricsListener(&config.Config{
 		Options: &config.Options{
 			MetricsAddr:           "127.0.0.1:9902",
@@ -65,7 +65,7 @@ func Test_buildMetricsHTTPConnectionManagerFilter(t *testing.T) {
 								"prefix": "/"
 							},
 							"route": {
-								"cluster": "pomerium-control-plane-http"
+								"cluster": "pomerium-control-plane-metrics"
 							}
 						}]
 					}]
@@ -108,7 +108,7 @@ func Test_buildMetricsHTTPConnectionManagerFilter(t *testing.T) {
 }
 
 func Test_buildMainHTTPConnectionManagerFilter(t *testing.T) {
-	b := New("local-grpc", "local-http", nil, nil)
+	b := New("local-grpc", "local-http", "local-metrics", nil, nil)
 
 	options := config.NewDefaultOptions()
 	options.SkipXffAppend = true
@@ -523,7 +523,7 @@ func Test_buildMainHTTPConnectionManagerFilter(t *testing.T) {
 }
 
 func Test_buildDownstreamTLSContext(t *testing.T) {
-	b := New("local-grpc", "local-http", filemgr.NewManager(), nil)
+	b := New("local-grpc", "local-http", "local-metrics", filemgr.NewManager(), nil)
 
 	cacheDir, _ := os.UserCacheDir()
 	certFileName := filepath.Join(cacheDir, "pomerium", "envoy", "files", "tls-crt-354e49305a5a39414a545530374e58454e48334148524c4e324258463837364355564c4e4532464b54355139495547514a38.pem")
@@ -805,7 +805,7 @@ func Test_hostMatchesDomain(t *testing.T) {
 }
 
 func Test_buildRouteConfiguration(t *testing.T) {
-	b := New("local-grpc", "local-http", nil, nil)
+	b := New("local-grpc", "local-http", "local-metrics", nil, nil)
 	virtualHosts := make([]*envoy_config_route_v3.VirtualHost, 10)
 	routeConfig, err := b.buildRouteConfiguration("test-route-configuration", virtualHosts)
 	require.NoError(t, err)
@@ -815,7 +815,7 @@ func Test_buildRouteConfiguration(t *testing.T) {
 }
 
 func Test_requireProxyProtocol(t *testing.T) {
-	b := New("local-grpc", "local-http", nil, nil)
+	b := New("local-grpc", "local-http", "local-metrics", nil, nil)
 	t.Run("required", func(t *testing.T) {
 		li, err := b.buildMainListener(context.Background(), &config.Config{Options: &config.Options{
 			UseProxyProtocol: true,

programming_language: Go
filename: routes.go
change_type: ModificationType.MODIFY
diff: @@ -57,51 +57,20 @@ func (b *Builder) buildPomeriumHTTPRoutes(options *config.Options, domain string
 		return nil, err
 	}
 	if !isFrontingAuthenticate {
-		// enable ext_authz
-		r, err := b.buildControlPlanePathRoute("/.pomerium/jwt", true)
-		if err != nil {
-			return nil, err
-		}
-		routes = append(routes, r)
-
-		// disable ext_authz and passthrough to proxy handlers
-		r, err = b.buildControlPlanePathRoute("/ping", false)
-		if err != nil {
-			return nil, err
-		}
-		routes = append(routes, r)
-		r, err = b.buildControlPlanePathRoute("/healthz", false)
-		if err != nil {
-			return nil, err
-		}
-		routes = append(routes, r)
-		r, err = b.buildControlPlanePathRoute("/.pomerium", false)
-		if err != nil {
-			return nil, err
-		}
-		routes = append(routes, r)
-		r, err = b.buildControlPlanePrefixRoute("/.pomerium/", false)
-		if err != nil {
-			return nil, err
-		}
-		routes = append(routes, r)
-		r, err = b.buildControlPlanePathRoute("/.well-known/pomerium", false)
-		if err != nil {
-			return nil, err
-		}
-		routes = append(routes, r)
-		r, err = b.buildControlPlanePrefixRoute("/.well-known/pomerium/", false)
-		if err != nil {
-			return nil, err
-		}
-		routes = append(routes, r)
+		routes = append(routes,
+			// enable ext_authz
+			b.buildControlPlanePathRoute("/.pomerium/jwt", true),
+			// disable ext_authz and passthrough to proxy handlers
+			b.buildControlPlanePathRoute("/ping", false),
+			b.buildControlPlanePathRoute("/healthz", false),
+			b.buildControlPlanePathRoute("/.pomerium", false),
+			b.buildControlPlanePrefixRoute("/.pomerium/", false),
+			b.buildControlPlanePathRoute("/.well-known/pomerium", false),
+			b.buildControlPlanePrefixRoute("/.well-known/pomerium/", false),
+		)
 		// per #837, only add robots.txt if there are no unauthenticated routes
 		if !hasPublicPolicyMatchingURL(options, url.URL{Scheme: "https", Host: domain, Path: "/robots.txt"}) {
-			r, err := b.buildControlPlanePathRoute("/robots.txt", false)
-			if err != nil {
-				return nil, err
-			}
-			routes = append(routes, r)
+			routes = append(routes, b.buildControlPlanePathRoute("/robots.txt", false))
 		}
 	}
 	// if we're handling authentication, add the oauth2 callback url
@@ -110,11 +79,10 @@ func (b *Builder) buildPomeriumHTTPRoutes(options *config.Options, domain string
 		return nil, err
 	}
 	if config.IsAuthenticate(options.Services) && hostMatchesDomain(authenticateURL, domain) {
-		r, err := b.buildControlPlanePrefixRoute("/", false)
-		if err != nil {
-			return nil, err
-		}
-		routes = append(routes, r)
+		routes = append(routes,
+			b.buildControlPlanePathRoute(options.AuthenticateCallbackPath, false),
+			b.buildControlPlanePathRoute("/", false),
+		)
 	}
 	// if we're the proxy and this is the forward-auth url
 	forwardAuthURL, err := options.GetForwardAuthURL()
@@ -196,7 +164,7 @@ func (b *Builder) buildControlPlanePathAndQueryRoute(path string, queryparams []
 	}, nil
 }
 
-func (b *Builder) buildControlPlanePathRoute(path string, protected bool) (*envoy_config_route_v3.Route, error) {
+func (b *Builder) buildControlPlanePathRoute(path string, protected bool) *envoy_config_route_v3.Route {
 	r := &envoy_config_route_v3.Route{
 		Name: "pomerium-path-" + path,
 		Match: &envoy_config_route_v3.RouteMatch{
@@ -215,10 +183,10 @@ func (b *Builder) buildControlPlanePathRoute(path string, protected bool) (*envo
 			"envoy.filters.http.ext_authz": disableExtAuthz,
 		}
 	}
-	return r, nil
+	return r
 }
 
-func (b *Builder) buildControlPlanePrefixRoute(prefix string, protected bool) (*envoy_config_route_v3.Route, error) {
+func (b *Builder) buildControlPlanePrefixRoute(prefix string, protected bool) *envoy_config_route_v3.Route {
 	r := &envoy_config_route_v3.Route{
 		Name: "pomerium-prefix-" + prefix,
 		Match: &envoy_config_route_v3.RouteMatch{
@@ -237,7 +205,7 @@ func (b *Builder) buildControlPlanePrefixRoute(prefix string, protected bool) (*
 			"envoy.filters.http.ext_authz": disableExtAuthz,
 		}
 	}
-	return r, nil
+	return r
 }
 
 // getClusterID returns a cluster ID

programming_language: Go
filename: routes_test.go
change_type: ModificationType.MODIFY
diff: @@ -96,7 +96,8 @@ func Test_buildPomeriumHTTPRoutes(t *testing.T) {
 			`+routeString("path", "/.well-known/pomerium", false)+`,
 			`+routeString("prefix", "/.well-known/pomerium/", false)+`,
 			`+routeString("path", "/robots.txt", false)+`,
-			`+routeString("prefix", "/", false)+`
+			`+routeString("path", "/oauth2/callback", false)+`,
+			`+routeString("path", "/", false)+`
 		]`, routes)
 	})
 	t.Run("proxy fronting authenticate", func(t *testing.T) {
@@ -167,8 +168,7 @@ func Test_buildPomeriumHTTPRoutes(t *testing.T) {
 
 func Test_buildControlPlanePathRoute(t *testing.T) {
 	b := &Builder{filemgr: filemgr.NewManager()}
-	route, err := b.buildControlPlanePathRoute("/hello/world", false)
-	require.NoError(t, err)
+	route := b.buildControlPlanePathRoute("/hello/world", false)
 	testutil.AssertProtoJSONEqual(t, `
 		{
 			"name": "pomerium-path-/hello/world",
@@ -190,8 +190,7 @@ func Test_buildControlPlanePathRoute(t *testing.T) {
 
 func Test_buildControlPlanePrefixRoute(t *testing.T) {
 	b := &Builder{filemgr: filemgr.NewManager()}
-	route, err := b.buildControlPlanePrefixRoute("/hello/world/", false)
-	require.NoError(t, err)
+	route := b.buildControlPlanePrefixRoute("/hello/world/", false)
 	testutil.AssertProtoJSONEqual(t, `
 		{
 			"name": "pomerium-prefix-/hello/world/",

programming_language: Go
filename: metrics.go
change_type: ModificationType.MODIFY
diff: @@ -3,6 +3,7 @@ package config
 import (
 	"context"
 	"net/http"
+	"net/url"
 	"os"
 	"sync"
 
@@ -16,12 +17,13 @@ import (
 
 // A MetricsManager manages metrics for a given configuration.
 type MetricsManager struct {
-	mu             sync.RWMutex
-	installationID string
-	serviceName    string
-	addr           string
-	basicAuth      string
-	handler        http.Handler
+	mu                sync.RWMutex
+	installationID    string
+	serviceName       string
+	addr              string
+	basicAuth         string
+	envoyAdminAddress string
+	handler           http.Handler
 }
 
 // NewMetricsManager creates a new MetricsManager.
@@ -46,8 +48,8 @@ func (mgr *MetricsManager) OnConfigChange(ctx context.Context, cfg *Config) {
 	mgr.mu.Lock()
 	defer mgr.mu.Unlock()
 
-	mgr.updateInfo(cfg)
-	mgr.updateServer(cfg)
+	mgr.updateInfo(ctx, cfg)
+	mgr.updateServer(ctx, cfg)
 }
 
 func (mgr *MetricsManager) ServeHTTP(w http.ResponseWriter, r *http.Request) {
@@ -61,7 +63,7 @@ func (mgr *MetricsManager) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 	mgr.handler.ServeHTTP(w, r)
 }
 
-func (mgr *MetricsManager) updateInfo(cfg *Config) {
+func (mgr *MetricsManager) updateInfo(ctx context.Context, cfg *Config) {
 	serviceName := telemetry.ServiceName(cfg.Options.Services)
 	if serviceName == mgr.serviceName {
 		return
@@ -69,7 +71,7 @@ func (mgr *MetricsManager) updateInfo(cfg *Config) {
 
 	hostname, err := os.Hostname()
 	if err != nil {
-		log.Error(context.TODO()).Err(err).Msg("telemetry/metrics: failed to get OS hostname")
+		log.Error(ctx).Err(err).Msg("telemetry/metrics: failed to get OS hostname")
 		hostname = "__unknown__"
 	}
 
@@ -77,26 +79,34 @@ func (mgr *MetricsManager) updateInfo(cfg *Config) {
 	mgr.serviceName = serviceName
 }
 
-func (mgr *MetricsManager) updateServer(cfg *Config) {
+func (mgr *MetricsManager) updateServer(ctx context.Context, cfg *Config) {
 	if cfg.Options.MetricsAddr == mgr.addr &&
 		cfg.Options.MetricsBasicAuth == mgr.basicAuth &&
-		cfg.Options.InstallationID == mgr.installationID {
+		cfg.Options.InstallationID == mgr.installationID &&
+		cfg.Options.EnvoyAdminAddress == mgr.envoyAdminAddress {
 		return
 	}
 
 	mgr.addr = cfg.Options.MetricsAddr
 	mgr.basicAuth = cfg.Options.MetricsBasicAuth
 	mgr.installationID = cfg.Options.InstallationID
+	mgr.envoyAdminAddress = cfg.Options.EnvoyAdminAddress
 	mgr.handler = nil
 
 	if mgr.addr == "" {
-		log.Info(context.TODO()).Msg("metrics: http server disabled")
+		log.Info(ctx).Msg("metrics: http server disabled")
 		return
 	}
 
-	handler, err := metrics.PrometheusHandler(EnvoyAdminURL, mgr.installationID)
+	envoyURL, err := url.Parse("http://" + cfg.Options.EnvoyAdminAddress)
 	if err != nil {
-		log.Error(context.TODO()).Err(err).Msg("metrics: failed to create prometheus handler")
+		log.Error(ctx).Err(err).Msg("metrics: invalid envoy admin address, disabling")
+		return
+	}
+
+	handler, err := metrics.PrometheusHandler(envoyURL, mgr.installationID)
+	if err != nil {
+		log.Error(ctx).Err(err).Msg("metrics: failed to create prometheus handler")
 		return
 	}
 

programming_language: Go
filename: options.go
change_type: ModificationType.MODIFY
diff: @@ -49,9 +49,6 @@ const (
 // gRPC server, or is used for healthchecks (authorize only service)
 const DefaultAlternativeAddr = ":5443"
 
-// EnvoyAdminURL indicates where the envoy control plane is listening
-var EnvoyAdminURL = &url.URL{Host: "127.0.0.1:9901", Scheme: "http"}
-
 // The randomSharedKey is used if no shared key is supplied in all-in-one mode.
 var randomSharedKey = cryptutil.NewBase64Key()
 

programming_language: Go
filename: pomerium.go
change_type: ModificationType.MODIFY
diff: @@ -86,6 +86,8 @@ func Run(ctx context.Context, configFile string) error {
 		Str("grpc-port", src.GetConfig().GRPCPort).
 		Str("http-port", src.GetConfig().HTTPPort).
 		Str("outbound-port", src.GetConfig().OutboundPort).
+		Str("metrics-port", src.GetConfig().MetricsPort).
+		Str("debug-port", src.GetConfig().DebugPort).
 		Msg("server started")
 
 	// create envoy server

programming_language: Go
filename: http.go
change_type: ModificationType.MODIFY
diff: @@ -49,12 +49,12 @@ func (srv *Server) addHTTPMiddleware() {
 	root.HandleFunc("/ping", httputil.HealthCheck)
 
 	// pprof
-	root.Path("/debug/pprof/cmdline").HandlerFunc(pprof.Cmdline)
-	root.Path("/debug/pprof/profile").HandlerFunc(pprof.Profile)
-	root.Path("/debug/pprof/symbol").HandlerFunc(pprof.Symbol)
-	root.Path("/debug/pprof/trace").HandlerFunc(pprof.Trace)
-	root.PathPrefix("/debug/pprof/").HandlerFunc(pprof.Index)
+	srv.DebugRouter.Path("/debug/pprof/cmdline").HandlerFunc(pprof.Cmdline)
+	srv.DebugRouter.Path("/debug/pprof/profile").HandlerFunc(pprof.Profile)
+	srv.DebugRouter.Path("/debug/pprof/symbol").HandlerFunc(pprof.Symbol)
+	srv.DebugRouter.Path("/debug/pprof/trace").HandlerFunc(pprof.Trace)
+	srv.DebugRouter.PathPrefix("/debug/pprof/").HandlerFunc(pprof.Index)
 
 	// metrics
-	root.Handle("/metrics", srv.metricsMgr)
+	srv.MetricsRouter.Handle("/metrics", srv.metricsMgr)
 }

programming_language: Go
filename: server.go
change_type: ModificationType.MODIFY
diff: @@ -50,11 +50,15 @@ func (avo *atomicVersionedConfig) Store(cfg versionedConfig) {
 
 // A Server is the control-plane gRPC and HTTP servers.
 type Server struct {
-	GRPCListener net.Listener
-	GRPCServer   *grpc.Server
-	HTTPListener net.Listener
-	HTTPRouter   *mux.Router
-	Builder      *envoyconfig.Builder
+	GRPCListener    net.Listener
+	GRPCServer      *grpc.Server
+	HTTPListener    net.Listener
+	HTTPRouter      *mux.Router
+	MetricsListener net.Listener
+	MetricsRouter   *mux.Router
+	DebugListener   net.Listener
+	DebugRouter     *mux.Router
+	Builder         *envoyconfig.Builder
 
 	currentConfig atomicVersionedConfig
 	name          string
@@ -106,7 +110,25 @@ func NewServer(cfg *config.Config, metricsMgr *config.MetricsManager) (*Server,
 		_ = srv.GRPCListener.Close()
 		return nil, err
 	}
+
+	srv.MetricsListener, err = net.Listen("tcp4", net.JoinHostPort("127.0.0.1", cfg.MetricsPort))
+	if err != nil {
+		_ = srv.GRPCListener.Close()
+		_ = srv.HTTPListener.Close()
+		return nil, err
+	}
+
+	srv.DebugListener, err = net.Listen("tcp4", net.JoinHostPort("127.0.0.1", cfg.DebugPort))
+	if err != nil {
+		_ = srv.GRPCListener.Close()
+		_ = srv.HTTPListener.Close()
+		_ = srv.DebugListener.Close()
+		return nil, err
+	}
+
 	srv.HTTPRouter = mux.NewRouter()
+	srv.DebugRouter = mux.NewRouter()
+	srv.MetricsRouter = mux.NewRouter()
 	srv.addHTTPMiddleware()
 
 	srv.filemgr = filemgr.NewManager()
@@ -115,6 +137,7 @@ func NewServer(cfg *config.Config, metricsMgr *config.MetricsManager) (*Server,
 	srv.Builder = envoyconfig.New(
 		srv.GRPCListener.Addr().String(),
 		srv.HTTPListener.Addr().String(),
+		srv.MetricsListener.Addr().String(),
 		srv.filemgr,
 		srv.reproxy,
 	)
@@ -175,28 +198,41 @@ func (srv *Server) Run(ctx context.Context) error {
 		return nil
 	})
 
-	hsrv := (&http.Server{
-		BaseContext: func(li net.Listener) context.Context {
-			return ctx
-		},
-		Handler: srv.HTTPRouter,
-	})
+	for _, entry := range []struct {
+		Name     string
+		Listener net.Listener
+		Handler  *mux.Router
+	}{
+		{"http", srv.HTTPListener, srv.HTTPRouter},
+		{"debug", srv.DebugListener, srv.DebugRouter},
+		{"metrics", srv.MetricsListener, srv.MetricsRouter},
+	} {
+		entry := entry
+		hsrv := (&http.Server{
+			BaseContext: func(li net.Listener) context.Context {
+				return ctx
+			},
+			Handler: entry.Handler,
+		})
 
-	// start the HTTP server
-	eg.Go(func() error {
-		log.Info(ctx).Str("addr", srv.HTTPListener.Addr().String()).Msg("starting control-plane HTTP server")
-		return hsrv.Serve(srv.HTTPListener)
-	})
+		// start the HTTP server
+		eg.Go(func() error {
+			log.Info(ctx).
+				Str("addr", entry.Listener.Addr().String()).
+				Msgf("starting control-plane %s server", entry.Name)
+			return hsrv.Serve(entry.Listener)
+		})
 
-	// gracefully stop the HTTP server on context cancellation
-	eg.Go(func() error {
-		<-ctx.Done()
+		// gracefully stop the HTTP server on context cancellation
+		eg.Go(func() error {
+			<-ctx.Done()
 
-		ctx, cleanup := context.WithTimeout(ctx, time.Second*5)
-		defer cleanup()
+			ctx, cleanup := context.WithTimeout(ctx, time.Second*5)
+			defer cleanup()
 
-		return hsrv.Shutdown(ctx)
-	})
+			return hsrv.Shutdown(ctx)
+		})
+	}
 
 	return eg.Wait()
 }

programming_language: Go
filename: reporter.go
change_type: ModificationType.MODIFY
diff: @@ -66,6 +66,10 @@ func (r *Reporter) OnConfigChange(ctx context.Context, cfg *config.Config) {
 }
 
 func getReportedServices(cfg *config.Config) ([]*pb.Service, error) {
+	if cfg.Options.MetricsAddr == "" {
+		return nil, nil
+	}
+
 	mu, err := metricsURL(*cfg.Options)
 	if err != nil {
 		return nil, err

programming_language: Go