From 20a9be891f5566ce60bd8ef9352eb1aadb40d9bf Mon Sep 17 00:00:00 2001 From: Caleb Doxsey Date: Mon, 18 Nov 2024 09:50:23 -0700 Subject: [PATCH] envoyconfig: cleanup (#5350) * envoyconfig: cleanup * remove listener access log for mtls for insecure server which can't use mtls * use new functions * rename method * refactor common code --- config/envoyconfig/envoyconfig.go | 37 - config/envoyconfig/listeners.go | 649 +----------------- config/envoyconfig/listeners_envoy_admin.go | 20 +- config/envoyconfig/listeners_grpc.go | 114 +++ config/envoyconfig/listeners_main.go | 216 ++++++ config/envoyconfig/listeners_main_test.go | 39 ++ config/envoyconfig/listeners_metrics.go | 139 ++++ config/envoyconfig/listeners_test.go | 471 ------------- config/envoyconfig/outbound.go | 20 +- config/envoyconfig/route_configurations.go | 67 +- .../envoyconfig/route_configurations_test.go | 117 ++++ config/envoyconfig/tls.go | 198 ++++++ config/envoyconfig/tls_test.go | 322 +++++++++ 13 files changed, 1227 insertions(+), 1182 deletions(-) create mode 100644 config/envoyconfig/listeners_grpc.go create mode 100644 config/envoyconfig/listeners_main.go create mode 100644 config/envoyconfig/listeners_main_test.go create mode 100644 config/envoyconfig/listeners_metrics.go diff --git a/config/envoyconfig/envoyconfig.go b/config/envoyconfig/envoyconfig.go index 77fcf9589..494fd5fc8 100644 --- a/config/envoyconfig/envoyconfig.go +++ b/config/envoyconfig/envoyconfig.go @@ -4,9 +4,6 @@ package envoyconfig import ( "bytes" "context" - "crypto/tls" - "crypto/x509" - "encoding/pem" "errors" "fmt" "net" @@ -21,7 +18,6 @@ import ( envoy_config_cluster_v3 "github.com/envoyproxy/go-control-plane/envoy/config/cluster/v3" envoy_config_core_v3 "github.com/envoyproxy/go-control-plane/envoy/config/core/v3" envoy_extensions_access_loggers_grpc_v3 "github.com/envoyproxy/go-control-plane/envoy/extensions/access_loggers/grpc/v3" - envoy_extensions_transport_sockets_tls_v3 "github.com/envoyproxy/go-control-plane/envoy/extensions/transport_sockets/tls/v3" "github.com/martinlindhe/base36" "golang.org/x/net/nettest" "google.golang.org/protobuf/proto" @@ -151,39 +147,6 @@ func buildAddress(hostport string, defaultPort uint32) *envoy_config_core_v3.Add } } -func (b *Builder) envoyTLSCertificateFromGoTLSCertificate( - ctx context.Context, - cert *tls.Certificate, -) *envoy_extensions_transport_sockets_tls_v3.TlsCertificate { - envoyCert := &envoy_extensions_transport_sockets_tls_v3.TlsCertificate{} - var chain bytes.Buffer - for _, cbs := range cert.Certificate { - _ = pem.Encode(&chain, &pem.Block{ - Type: "CERTIFICATE", - Bytes: cbs, - }) - } - envoyCert.CertificateChain = b.filemgr.BytesDataSource("tls-crt.pem", chain.Bytes()) - if cert.OCSPStaple != nil { - envoyCert.OcspStaple = b.filemgr.BytesDataSource("ocsp-staple", cert.OCSPStaple) - } - if bs, err := x509.MarshalPKCS8PrivateKey(cert.PrivateKey); err == nil { - envoyCert.PrivateKey = b.filemgr.BytesDataSource("tls-key.pem", pem.EncodeToMemory( - &pem.Block{ - Type: "PRIVATE KEY", - Bytes: bs, - }, - )) - } else { - log.Ctx(ctx).Error().Err(err).Msg("failed to marshal private key for tls config") - } - for _, scts := range cert.SignedCertificateTimestamps { - envoyCert.SignedCertificateTimestamp = append(envoyCert.SignedCertificateTimestamp, - b.filemgr.BytesDataSource("signed-certificate-timestamp", scts)) - } - return envoyCert -} - var rootCABundle struct { sync.Once value string diff --git a/config/envoyconfig/listeners.go b/config/envoyconfig/listeners.go index d5a631ddb..75bab50a3 100644 --- a/config/envoyconfig/listeners.go +++ b/config/envoyconfig/listeners.go @@ -1,36 +1,14 @@ package envoyconfig import ( - "bytes" - "cmp" "context" - "crypto/tls" - "encoding/base64" - "fmt" - "net" - "net/url" "runtime" - "strings" - "time" - envoy_config_accesslog_v3 "github.com/envoyproxy/go-control-plane/envoy/config/accesslog/v3" - envoy_config_core_v3 "github.com/envoyproxy/go-control-plane/envoy/config/core/v3" envoy_config_listener_v3 "github.com/envoyproxy/go-control-plane/envoy/config/listener/v3" - envoy_config_route_v3 "github.com/envoyproxy/go-control-plane/envoy/config/route/v3" - envoy_extensions_access_loggers_grpc_v3 "github.com/envoyproxy/go-control-plane/envoy/extensions/access_loggers/grpc/v3" - envoy_http_connection_manager "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/http_connection_manager/v3" - envoy_extensions_transport_sockets_tls_v3 "github.com/envoyproxy/go-control-plane/envoy/extensions/transport_sockets/tls/v3" - envoy_type_v3 "github.com/envoyproxy/go-control-plane/envoy/type/v3" - "github.com/hashicorp/go-set/v3" - "google.golang.org/protobuf/types/known/durationpb" "google.golang.org/protobuf/types/known/wrapperspb" "github.com/pomerium/pomerium/config" - "github.com/pomerium/pomerium/internal/hashutil" - "github.com/pomerium/pomerium/internal/log" - "github.com/pomerium/pomerium/internal/telemetry/metrics" "github.com/pomerium/pomerium/internal/telemetry/trace" - "github.com/pomerium/pomerium/internal/urlutil" ) const listenerBufferLimit uint32 = 32 * 1024 @@ -62,7 +40,7 @@ func (b *Builder) BuildListeners( listeners = append(listeners, li) } - if cfg.Options.MetricsAddr != "" { + if shouldStartMetricsListener(cfg.Options) { li, err := b.buildMetricsListener(cfg) if err != nil { return nil, err @@ -70,7 +48,7 @@ func (b *Builder) BuildListeners( listeners = append(listeners, li) } - if cfg.Options.EnvoyAdminAddress != "" { + if shouldStartEnvoyAdminListener(cfg.Options) { li, err := b.buildEnvoyAdminListener(ctx, cfg) if err != nil { return nil, err @@ -87,615 +65,8 @@ func (b *Builder) BuildListeners( return listeners, nil } -func getAllCertificates(cfg *config.Config) ([]tls.Certificate, error) { - allCertificates, err := cfg.AllCertificates() - if err != nil { - return nil, fmt.Errorf("error collecting all certificates: %w", err) - } - - wc, err := cfg.GenerateCatchAllCertificate() - if err != nil { - return nil, fmt.Errorf("error getting wildcard certificate: %w", err) - } - - return append(allCertificates, *wc), nil -} - -func (b *Builder) buildTLSSocket(ctx context.Context, cfg *config.Config, certs []tls.Certificate) (*envoy_config_core_v3.TransportSocket, error) { - tlsContext, err := b.buildDownstreamTLSContextMulti(ctx, cfg, certs) - if err != nil { - return nil, err - } - return &envoy_config_core_v3.TransportSocket{ - Name: "tls", - ConfigType: &envoy_config_core_v3.TransportSocket_TypedConfig{ - TypedConfig: marshalAny(tlsContext), - }, - }, nil -} - -func listenerAccessLog() []*envoy_config_accesslog_v3.AccessLog { - cc := &envoy_extensions_access_loggers_grpc_v3.CommonGrpcAccessLogConfig{ - LogName: "ingress-http-listener", - GrpcService: &envoy_config_core_v3.GrpcService{ - TargetSpecifier: &envoy_config_core_v3.GrpcService_EnvoyGrpc_{ - EnvoyGrpc: &envoy_config_core_v3.GrpcService_EnvoyGrpc{ - ClusterName: "pomerium-control-plane-grpc", - }, - }, - }, - TransportApiVersion: envoy_config_core_v3.ApiVersion_V3, - } - tcp := marshalAny( - &envoy_extensions_access_loggers_grpc_v3.TcpGrpcAccessLogConfig{CommonConfig: cc}) - return []*envoy_config_accesslog_v3.AccessLog{ - { - Name: "envoy.access_loggers.tcp_grpc", - ConfigType: &envoy_config_accesslog_v3.AccessLog_TypedConfig{TypedConfig: tcp}, - }, - } -} - -func (b *Builder) buildMainListener( - ctx context.Context, - cfg *config.Config, - fullyStatic bool, -) (*envoy_config_listener_v3.Listener, error) { - li := newEnvoyListener("http-ingress") - if cfg.Options.UseProxyProtocol { - li.ListenerFilters = append(li.ListenerFilters, ProxyProtocolFilter()) - } - - if cfg.Options.DownstreamMTLS.Enforcement == config.MTLSEnforcementRejectConnection { - li.AccessLog = listenerAccessLog() - } - - if cfg.Options.InsecureServer { - li.Address = buildAddress(cfg.Options.Addr, 80) - - filter, err := b.buildMainHTTPConnectionManagerFilter(ctx, cfg, fullyStatic) - if err != nil { - return nil, err - } - - li.FilterChains = []*envoy_config_listener_v3.FilterChain{{ - Filters: []*envoy_config_listener_v3.Filter{ - filter, - }, - }} - } else { - li.Address = buildAddress(cfg.Options.Addr, 443) - li.ListenerFilters = append(li.ListenerFilters, TLSInspectorFilter()) - - li.FilterChains = append(li.FilterChains, b.buildACMETLSALPNFilterChain()) - - allCertificates, err := getAllCertificates(cfg) - if err != nil { - return nil, err - } - - filter, err := b.buildMainHTTPConnectionManagerFilter(ctx, cfg, fullyStatic) - if err != nil { - return nil, err - } - filterChain := &envoy_config_listener_v3.FilterChain{ - Filters: []*envoy_config_listener_v3.Filter{filter}, - } - li.FilterChains = append(li.FilterChains, filterChain) - - sock, err := b.buildTLSSocket(ctx, cfg, allCertificates) - if err != nil { - return nil, fmt.Errorf("error building TLS socket: %w", err) - } - filterChain.TransportSocket = sock - } - return li, nil -} - -func (b *Builder) buildMetricsListener(cfg *config.Config) (*envoy_config_listener_v3.Listener, error) { - filter, err := b.buildMetricsHTTPConnectionManagerFilter() - if err != nil { - return nil, err - } - - filterChain := &envoy_config_listener_v3.FilterChain{ - Filters: []*envoy_config_listener_v3.Filter{ - filter, - }, - } - - cert, err := cfg.Options.GetMetricsCertificate() - if err != nil { - return nil, err - } - if cert != nil { - dtc := &envoy_extensions_transport_sockets_tls_v3.DownstreamTlsContext{ - CommonTlsContext: &envoy_extensions_transport_sockets_tls_v3.CommonTlsContext{ - TlsParams: tlsDownstreamParams, - TlsCertificates: []*envoy_extensions_transport_sockets_tls_v3.TlsCertificate{ - b.envoyTLSCertificateFromGoTLSCertificate(context.TODO(), cert), - }, - AlpnProtocols: []string{"h2", "http/1.1"}, - }, - } - - if cfg.Options.MetricsClientCA != "" { - bs, err := base64.StdEncoding.DecodeString(cfg.Options.MetricsClientCA) - if err != nil { - return nil, fmt.Errorf("xds: invalid metrics_client_ca: %w", err) - } - - dtc.RequireClientCertificate = wrapperspb.Bool(true) - dtc.CommonTlsContext.ValidationContextType = &envoy_extensions_transport_sockets_tls_v3.CommonTlsContext_ValidationContext{ - ValidationContext: &envoy_extensions_transport_sockets_tls_v3.CertificateValidationContext{ - TrustChainVerification: envoy_extensions_transport_sockets_tls_v3.CertificateValidationContext_VERIFY_TRUST_CHAIN, - TrustedCa: b.filemgr.BytesDataSource("metrics_client_ca.pem", bs), - }, - } - } else if cfg.Options.MetricsClientCAFile != "" { - dtc.RequireClientCertificate = wrapperspb.Bool(true) - dtc.CommonTlsContext.ValidationContextType = &envoy_extensions_transport_sockets_tls_v3.CommonTlsContext_ValidationContext{ - ValidationContext: &envoy_extensions_transport_sockets_tls_v3.CertificateValidationContext{ - TrustChainVerification: envoy_extensions_transport_sockets_tls_v3.CertificateValidationContext_VERIFY_TRUST_CHAIN, - TrustedCa: b.filemgr.FileDataSource(cfg.Options.MetricsClientCAFile), - }, - } - } - - tc := marshalAny(dtc) - filterChain.TransportSocket = &envoy_config_core_v3.TransportSocket{ - Name: "tls", - ConfigType: &envoy_config_core_v3.TransportSocket_TypedConfig{ - TypedConfig: tc, - }, - } - } - - // we ignore the host part of the address, only binding to - host, port, err := net.SplitHostPort(cfg.Options.MetricsAddr) - if err != nil { - return nil, fmt.Errorf("metrics_addr %s: %w", cfg.Options.MetricsAddr, err) - } - if port == "" { - return nil, fmt.Errorf("metrics_addr %s: port is required", cfg.Options.MetricsAddr) - } - // unless an explicit IP address was provided, and bind to all interfaces if hostname was provided - if net.ParseIP(host) == nil { - host = "" - } - - addr := buildAddress(net.JoinHostPort(host, port), 9902) - li := newEnvoyListener(fmt.Sprintf("metrics-ingress-%d", hashutil.MustHash(addr))) - li.Address = addr - li.FilterChains = []*envoy_config_listener_v3.FilterChain{filterChain} - return li, nil -} - -func (b *Builder) buildMainHTTPConnectionManagerFilter( - ctx context.Context, - cfg *config.Config, - fullyStatic bool, -) (*envoy_config_listener_v3.Filter, error) { - var grpcClientTimeout *durationpb.Duration - if cfg.Options.GRPCClientTimeout != 0 { - grpcClientTimeout = durationpb.New(cfg.Options.GRPCClientTimeout) - } else { - grpcClientTimeout = durationpb.New(30 * time.Second) - } - - filters := []*envoy_http_connection_manager.HttpFilter{ - LuaFilter(luascripts.RemoveImpersonateHeaders), - LuaFilter(luascripts.SetClientCertificateMetadata), - ExtAuthzFilter(grpcClientTimeout), - LuaFilter(luascripts.ExtAuthzSetCookie), - LuaFilter(luascripts.CleanUpstream), - LuaFilter(luascripts.RewriteHeaders), - } - filters = append(filters, HTTPRouterFilter()) - - var maxStreamDuration *durationpb.Duration - if cfg.Options.WriteTimeout > 0 { - maxStreamDuration = durationpb.New(cfg.Options.WriteTimeout) - } - - tracingProvider, err := buildTracingHTTP(cfg.Options) - if err != nil { - return nil, err - } - - localReply, err := b.buildLocalReplyConfig(cfg.Options) - if err != nil { - return nil, err - } - - mgr := &envoy_http_connection_manager.HttpConnectionManager{ - AlwaysSetRequestIdInResponse: true, - CodecType: cfg.Options.GetCodecType().ToEnvoy(), - StatPrefix: "ingress", - HttpFilters: filters, - AccessLog: buildAccessLogs(cfg.Options), - CommonHttpProtocolOptions: &envoy_config_core_v3.HttpProtocolOptions{ - IdleTimeout: durationpb.New(cfg.Options.IdleTimeout), - MaxStreamDuration: maxStreamDuration, - }, - HttpProtocolOptions: http1ProtocolOptions, - RequestTimeout: durationpb.New(cfg.Options.ReadTimeout), - Tracing: &envoy_http_connection_manager.HttpConnectionManager_Tracing{ - RandomSampling: &envoy_type_v3.Percent{Value: cfg.Options.TracingSampleRate * 100}, - Provider: tracingProvider, - }, - // See https://www.envoyproxy.io/docs/envoy/latest/configuration/http/http_conn_man/headers#x-forwarded-for - UseRemoteAddress: &wrapperspb.BoolValue{Value: true}, - SkipXffAppend: cfg.Options.SkipXffAppend, - XffNumTrustedHops: cfg.Options.XffNumTrustedHops, - LocalReplyConfig: localReply, - NormalizePath: wrapperspb.Bool(true), - } - - if fullyStatic { - routeConfiguration, err := b.buildMainRouteConfiguration(ctx, cfg) - if err != nil { - return nil, err - } - mgr.RouteSpecifier = &envoy_http_connection_manager.HttpConnectionManager_RouteConfig{ - RouteConfig: routeConfiguration, - } - } else { - mgr.RouteSpecifier = &envoy_http_connection_manager.HttpConnectionManager_Rds{ - Rds: &envoy_http_connection_manager.Rds{ - ConfigSource: &envoy_config_core_v3.ConfigSource{ - ResourceApiVersion: envoy_config_core_v3.ApiVersion_V3, - ConfigSourceSpecifier: &envoy_config_core_v3.ConfigSource_Ads{}, - }, - RouteConfigName: "main", - }, - } - } - - return HTTPConnectionManagerFilter(mgr), nil -} - -func (b *Builder) buildMetricsHTTPConnectionManagerFilter() (*envoy_config_listener_v3.Filter, error) { - rc, err := b.buildRouteConfiguration("metrics", []*envoy_config_route_v3.VirtualHost{{ - Name: "metrics", - Domains: []string{"*"}, - Routes: []*envoy_config_route_v3.Route{ - { - Name: "envoy-metrics", - Match: &envoy_config_route_v3.RouteMatch{ - PathSpecifier: &envoy_config_route_v3.RouteMatch_Prefix{Prefix: metrics.EnvoyMetricsPath}, - }, - Action: &envoy_config_route_v3.Route_Route{ - Route: &envoy_config_route_v3.RouteAction{ - ClusterSpecifier: &envoy_config_route_v3.RouteAction_Cluster{ - Cluster: envoyAdminClusterName, - }, - PrefixRewrite: "/stats/prometheus", - }, - }, - }, - { - Name: "metrics", - Match: &envoy_config_route_v3.RouteMatch{ - PathSpecifier: &envoy_config_route_v3.RouteMatch_Prefix{Prefix: "/"}, - }, - Action: &envoy_config_route_v3.Route_Route{ - Route: &envoy_config_route_v3.RouteAction{ - ClusterSpecifier: &envoy_config_route_v3.RouteAction_Cluster{ - Cluster: "pomerium-control-plane-metrics", - }, - }, - }, - }, - }, - }}) - if err != nil { - return nil, err - } - - return HTTPConnectionManagerFilter(&envoy_http_connection_manager.HttpConnectionManager{ - CodecType: envoy_http_connection_manager.HttpConnectionManager_AUTO, - StatPrefix: "metrics", - RouteSpecifier: &envoy_http_connection_manager.HttpConnectionManager_RouteConfig{ - RouteConfig: rc, - }, - HttpFilters: []*envoy_http_connection_manager.HttpFilter{ - HTTPRouterFilter(), - }, - }), nil -} - -func (b *Builder) buildGRPCListener(ctx context.Context, cfg *config.Config) (*envoy_config_listener_v3.Listener, error) { - filter, err := b.buildGRPCHTTPConnectionManagerFilter() - if err != nil { - return nil, err - } - - filterChain := envoy_config_listener_v3.FilterChain{ - Filters: []*envoy_config_listener_v3.Filter{filter}, - } - - li := newEnvoyListener("grpc-ingress") - li.FilterChains = []*envoy_config_listener_v3.FilterChain{&filterChain} - - if cfg.Options.GetGRPCInsecure() { - li.Address = buildAddress(cfg.Options.GetGRPCAddr(), 80) - return li, nil - } - - li.Address = buildAddress(cfg.Options.GetGRPCAddr(), 443) - li.ListenerFilters = []*envoy_config_listener_v3.ListenerFilter{ - TLSInspectorFilter(), - } - - allCertificates, err := getAllCertificates(cfg) - if err != nil { - return nil, err - } - envoyCerts, err := b.envoyCertificates(ctx, allCertificates) - if err != nil { - return nil, err - } - tlsContext := &envoy_extensions_transport_sockets_tls_v3.DownstreamTlsContext{ - CommonTlsContext: &envoy_extensions_transport_sockets_tls_v3.CommonTlsContext{ - TlsParams: tlsDownstreamParams, - TlsCertificates: envoyCerts, - AlpnProtocols: []string{"h2"}, // gRPC requires HTTP/2 - }, - } - filterChain.TransportSocket = &envoy_config_core_v3.TransportSocket{ - Name: "tls", - ConfigType: &envoy_config_core_v3.TransportSocket_TypedConfig{ - TypedConfig: marshalAny(tlsContext), - }, - } - return li, nil -} - -func (b *Builder) buildGRPCHTTPConnectionManagerFilter() (*envoy_config_listener_v3.Filter, error) { - allow := []string{ - "envoy.service.auth.v3.Authorization", - "databroker.DataBrokerService", - "registry.Registry", - "grpc.health.v1.Health", - } - routes := make([]*envoy_config_route_v3.Route, 0, len(allow)) - for _, svc := range allow { - routes = append(routes, &envoy_config_route_v3.Route{ - Name: "grpc", - Match: &envoy_config_route_v3.RouteMatch{ - PathSpecifier: &envoy_config_route_v3.RouteMatch_Prefix{Prefix: fmt.Sprintf("/%s/", svc)}, - Grpc: &envoy_config_route_v3.RouteMatch_GrpcRouteMatchOptions{}, - }, - Action: &envoy_config_route_v3.Route_Route{ - Route: &envoy_config_route_v3.RouteAction{ - ClusterSpecifier: &envoy_config_route_v3.RouteAction_Cluster{ - Cluster: "pomerium-control-plane-grpc", - }, - // disable the timeout to support grpc streaming - Timeout: &durationpb.Duration{ - Seconds: 0, - }, - IdleTimeout: &durationpb.Duration{ - Seconds: 0, - }, - }, - }, - }) - } - rc, err := b.buildRouteConfiguration("grpc", []*envoy_config_route_v3.VirtualHost{{ - Name: "grpc", - Domains: []string{"*"}, - Routes: routes, - }}) - if err != nil { - return nil, err - } - - return HTTPConnectionManagerFilter(&envoy_http_connection_manager.HttpConnectionManager{ - CodecType: envoy_http_connection_manager.HttpConnectionManager_AUTO, - StatPrefix: "grpc_ingress", - // limit request first byte to last byte time - RequestTimeout: &durationpb.Duration{ - Seconds: 15, - }, - RouteSpecifier: &envoy_http_connection_manager.HttpConnectionManager_RouteConfig{ - RouteConfig: rc, - }, - HttpFilters: []*envoy_http_connection_manager.HttpFilter{ - HTTPRouterFilter(), - }, - }), nil -} - -func (b *Builder) buildRouteConfiguration(name string, virtualHosts []*envoy_config_route_v3.VirtualHost) (*envoy_config_route_v3.RouteConfiguration, error) { - return &envoy_config_route_v3.RouteConfiguration{ - Name: name, - VirtualHosts: virtualHosts, - // disable cluster validation since the order of LDS/CDS updates isn't guaranteed - ValidateClusters: &wrapperspb.BoolValue{Value: false}, - }, nil -} - -func (b *Builder) envoyCertificates(ctx context.Context, certs []tls.Certificate) ( - []*envoy_extensions_transport_sockets_tls_v3.TlsCertificate, error, -) { - envoyCerts := make([]*envoy_extensions_transport_sockets_tls_v3.TlsCertificate, 0, len(certs)) - for i := range certs { - cert := &certs[i] - if err := validateCertificate(cert); err != nil { - return nil, fmt.Errorf("invalid certificate for domain %s: %w", - cert.Leaf.Subject.CommonName, err) - } - envoyCert := b.envoyTLSCertificateFromGoTLSCertificate(ctx, cert) - envoyCerts = append(envoyCerts, envoyCert) - } - return envoyCerts, nil -} - -func (b *Builder) buildDownstreamTLSContextMulti( - ctx context.Context, - cfg *config.Config, - certs []tls.Certificate, -) ( - *envoy_extensions_transport_sockets_tls_v3.DownstreamTlsContext, - error, -) { - envoyCerts, err := b.envoyCertificates(ctx, certs) - if err != nil { - return nil, err - } - dtc := &envoy_extensions_transport_sockets_tls_v3.DownstreamTlsContext{ - CommonTlsContext: &envoy_extensions_transport_sockets_tls_v3.CommonTlsContext{ - TlsParams: tlsDownstreamParams, - TlsCertificates: envoyCerts, - AlpnProtocols: getALPNProtos(cfg.Options), - }, - } - b.buildDownstreamValidationContext(ctx, dtc, cfg) - return dtc, nil -} - -func getALPNProtos(opts *config.Options) []string { - switch opts.GetCodecType() { - case config.CodecTypeHTTP1: - return []string{"http/1.1"} - case config.CodecTypeHTTP2: - return []string{"h2"} - default: - return []string{"h2", "http/1.1"} - } -} - -func (b *Builder) buildDownstreamValidationContext( - ctx context.Context, - dtc *envoy_extensions_transport_sockets_tls_v3.DownstreamTlsContext, - cfg *config.Config, -) { - clientCA := clientCABundle(ctx, cfg) - if len(clientCA) == 0 { - return - } - - vc := &envoy_extensions_transport_sockets_tls_v3.CertificateValidationContext{ - TrustedCa: b.filemgr.BytesDataSource("client-ca.pem", clientCA), - MatchTypedSubjectAltNames: make([]*envoy_extensions_transport_sockets_tls_v3.SubjectAltNameMatcher, - 0, len(cfg.Options.DownstreamMTLS.MatchSubjectAltNames)), - OnlyVerifyLeafCertCrl: true, - } - for i := range cfg.Options.DownstreamMTLS.MatchSubjectAltNames { - vc.MatchTypedSubjectAltNames = append(vc.MatchTypedSubjectAltNames, - cfg.Options.DownstreamMTLS.MatchSubjectAltNames[i].ToEnvoyProto()) - } - - if d := cfg.Options.DownstreamMTLS.GetMaxVerifyDepth(); d > 0 { - vc.MaxVerifyDepth = wrapperspb.UInt32(d) - } - - if cfg.Options.DownstreamMTLS.GetEnforcement() == config.MTLSEnforcementRejectConnection { - dtc.RequireClientCertificate = wrapperspb.Bool(true) - } else { - vc.TrustChainVerification = envoy_extensions_transport_sockets_tls_v3.CertificateValidationContext_ACCEPT_UNTRUSTED - } - - if crl := cfg.Options.DownstreamMTLS.CRL; crl != "" { - bs, err := base64.StdEncoding.DecodeString(crl) - if err != nil { - log.Ctx(ctx).Error().Err(err).Msg("invalid client CRL") - } else { - vc.Crl = b.filemgr.BytesDataSource("client-crl.pem", bs) - } - } else if crlf := cfg.Options.DownstreamMTLS.CRLFile; crlf != "" { - vc.Crl = b.filemgr.FileDataSource(crlf) - } - - dtc.CommonTlsContext.ValidationContextType = &envoy_extensions_transport_sockets_tls_v3.CommonTlsContext_ValidationContext{ - ValidationContext: vc, - } -} - -// clientCABundle returns a bundle of the globally configured client CA and any -// per-route client CAs. -func clientCABundle(ctx context.Context, cfg *config.Config) []byte { - var bundle bytes.Buffer - ca, _ := cfg.Options.DownstreamMTLS.GetCA() - addCAToBundle(&bundle, ca) - for p := range cfg.Options.GetAllPolicies() { - // We don't need to check TLSDownstreamClientCAFile here because - // Policy.Validate() will populate TLSDownstreamClientCA when - // TLSDownstreamClientCAFile is set. - if p.TLSDownstreamClientCA == "" { - continue - } - ca, err := base64.StdEncoding.DecodeString(p.TLSDownstreamClientCA) - if err != nil { - log.Ctx(ctx).Error().Stringer("policy", p).Err(err).Msg("invalid client CA") - continue - } - addCAToBundle(&bundle, ca) - } - return bundle.Bytes() -} - -func addCAToBundle(bundle *bytes.Buffer, ca []byte) { - if len(ca) == 0 { - return - } - bundle.Write(ca) - // Make sure each CA is separated by a newline. - if ca[len(ca)-1] != '\n' { - bundle.WriteByte('\n') - } -} - -func getAllRouteableHosts(options *config.Options, addr string) ([]string, error) { - allHosts := set.NewTreeSet(cmp.Compare[string]) - - if addr == options.Addr { - hosts, err := options.GetAllRouteableHTTPHosts() - if err != nil { - return nil, err - } - allHosts.InsertSlice(hosts) - } - - if addr == options.GetGRPCAddr() { - hosts, err := options.GetAllRouteableGRPCHosts() - if err != nil { - return nil, err - } - allHosts.InsertSlice(hosts) - } - - var filtered []string - for host := range allHosts.Items() { - if !strings.Contains(host, "*") { - filtered = append(filtered, host) - } - } - return filtered, nil -} - -func urlsMatchHost(urls []*url.URL, host string) bool { - for _, u := range urls { - if urlMatchesHost(u, host) { - return true - } - } - return false -} - -func urlMatchesHost(u *url.URL, host string) bool { - for _, h := range urlutil.GetDomainsForURL(u, true) { - if h == host { - return true - } - } - return false -} - -// newEnvoyListener creates envoy listener with certain default values -func newEnvoyListener(name string) *envoy_config_listener_v3.Listener { +// newListener creates envoy listener with certain default values +func newListener(name string) *envoy_config_listener_v3.Listener { return &envoy_config_listener_v3.Listener{ Name: name, PerConnectionBufferLimitBytes: wrapperspb.UInt32(listenerBufferLimit), @@ -706,15 +77,3 @@ func newEnvoyListener(name string) *envoy_config_listener_v3.Listener { EnableReusePort: wrapperspb.Bool(runtime.GOOS == "linux"), } } - -func shouldStartMainListener(options *config.Options) bool { - return config.IsAuthenticate(options.Services) || config.IsProxy(options.Services) -} - -func shouldStartGRPCListener(options *config.Options) bool { - if options.GetGRPCAddr() == "" { - return false - } - - return config.IsAuthorize(options.Services) || config.IsDataBroker(options.Services) -} diff --git a/config/envoyconfig/listeners_envoy_admin.go b/config/envoyconfig/listeners_envoy_admin.go index 5b0a8050b..f84205e57 100644 --- a/config/envoyconfig/listeners_envoy_admin.go +++ b/config/envoyconfig/listeners_envoy_admin.go @@ -12,10 +12,7 @@ import ( ) func (b *Builder) buildEnvoyAdminListener(_ context.Context, cfg *config.Config) (*envoy_config_listener_v3.Listener, error) { - filter, err := b.buildEnvoyAdminHTTPConnectionManagerFilter() - if err != nil { - return nil, err - } + filter := b.buildEnvoyAdminHTTPConnectionManagerFilter() filterChain := &envoy_config_listener_v3.FilterChain{ Filters: []*envoy_config_listener_v3.Filter{ @@ -28,14 +25,14 @@ func (b *Builder) buildEnvoyAdminListener(_ context.Context, cfg *config.Config) return nil, fmt.Errorf("envoy_admin_addr %s: %w", cfg.Options.EnvoyAdminAddress, err) } - li := newEnvoyListener("envoy-admin") + li := newListener("envoy-admin") li.Address = addr li.FilterChains = []*envoy_config_listener_v3.FilterChain{filterChain} return li, nil } -func (b *Builder) buildEnvoyAdminHTTPConnectionManagerFilter() (*envoy_config_listener_v3.Filter, error) { - rc, err := b.buildRouteConfiguration("envoy-admin", []*envoy_config_route_v3.VirtualHost{{ +func (b *Builder) buildEnvoyAdminHTTPConnectionManagerFilter() *envoy_config_listener_v3.Filter { + rc := newRouteConfiguration("envoy-admin", []*envoy_config_route_v3.VirtualHost{{ Name: "envoy-admin", Domains: []string{"*"}, Routes: []*envoy_config_route_v3.Route{ @@ -54,9 +51,6 @@ func (b *Builder) buildEnvoyAdminHTTPConnectionManagerFilter() (*envoy_config_li }, }, }}) - if err != nil { - return nil, err - } return HTTPConnectionManagerFilter(&envoy_http_connection_manager.HttpConnectionManager{ CodecType: envoy_http_connection_manager.HttpConnectionManager_AUTO, @@ -67,5 +61,9 @@ func (b *Builder) buildEnvoyAdminHTTPConnectionManagerFilter() (*envoy_config_li HttpFilters: []*envoy_http_connection_manager.HttpFilter{ HTTPRouterFilter(), }, - }), nil + }) +} + +func shouldStartEnvoyAdminListener(options *config.Options) bool { + return options.EnvoyAdminAddress != "" } diff --git a/config/envoyconfig/listeners_grpc.go b/config/envoyconfig/listeners_grpc.go new file mode 100644 index 000000000..69eff2965 --- /dev/null +++ b/config/envoyconfig/listeners_grpc.go @@ -0,0 +1,114 @@ +package envoyconfig + +import ( + "context" + "fmt" + + envoy_config_listener_v3 "github.com/envoyproxy/go-control-plane/envoy/config/listener/v3" + envoy_config_route_v3 "github.com/envoyproxy/go-control-plane/envoy/config/route/v3" + envoy_http_connection_manager "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/http_connection_manager/v3" + envoy_extensions_transport_sockets_tls_v3 "github.com/envoyproxy/go-control-plane/envoy/extensions/transport_sockets/tls/v3" + "google.golang.org/protobuf/types/known/durationpb" + + "github.com/pomerium/pomerium/config" +) + +func (b *Builder) buildGRPCListener(ctx context.Context, cfg *config.Config) (*envoy_config_listener_v3.Listener, error) { + filter := b.buildGRPCHTTPConnectionManagerFilter() + + filterChain := envoy_config_listener_v3.FilterChain{ + Filters: []*envoy_config_listener_v3.Filter{filter}, + } + + li := newListener("grpc-ingress") + li.FilterChains = []*envoy_config_listener_v3.FilterChain{&filterChain} + + if cfg.Options.GetGRPCInsecure() { + li.Address = buildAddress(cfg.Options.GetGRPCAddr(), 80) + return li, nil + } + + li.Address = buildAddress(cfg.Options.GetGRPCAddr(), 443) + li.ListenerFilters = []*envoy_config_listener_v3.ListenerFilter{ + TLSInspectorFilter(), + } + + allCertificates, err := getAllCertificates(cfg) + if err != nil { + return nil, err + } + envoyCerts, err := b.envoyTLSCertificatesFromGoTLSCertificates(ctx, allCertificates) + if err != nil { + return nil, err + } + tlsContext := &envoy_extensions_transport_sockets_tls_v3.DownstreamTlsContext{ + CommonTlsContext: &envoy_extensions_transport_sockets_tls_v3.CommonTlsContext{ + TlsParams: tlsDownstreamParams, + TlsCertificates: envoyCerts, + AlpnProtocols: []string{"h2"}, // gRPC requires HTTP/2 + }, + } + filterChain.TransportSocket = newDownstreamTLSTransportSocket(tlsContext) + return li, nil +} + +func (b *Builder) buildGRPCHTTPConnectionManagerFilter() *envoy_config_listener_v3.Filter { + allow := []string{ + "envoy.service.auth.v3.Authorization", + "databroker.DataBrokerService", + "registry.Registry", + "grpc.health.v1.Health", + } + routes := make([]*envoy_config_route_v3.Route, 0, len(allow)) + for _, svc := range allow { + routes = append(routes, &envoy_config_route_v3.Route{ + Name: "grpc", + Match: &envoy_config_route_v3.RouteMatch{ + PathSpecifier: &envoy_config_route_v3.RouteMatch_Prefix{Prefix: fmt.Sprintf("/%s/", svc)}, + Grpc: &envoy_config_route_v3.RouteMatch_GrpcRouteMatchOptions{}, + }, + Action: &envoy_config_route_v3.Route_Route{ + Route: &envoy_config_route_v3.RouteAction{ + ClusterSpecifier: &envoy_config_route_v3.RouteAction_Cluster{ + Cluster: "pomerium-control-plane-grpc", + }, + // disable the timeout to support grpc streaming + Timeout: &durationpb.Duration{ + Seconds: 0, + }, + IdleTimeout: &durationpb.Duration{ + Seconds: 0, + }, + }, + }, + }) + } + rc := newRouteConfiguration("grpc", []*envoy_config_route_v3.VirtualHost{{ + Name: "grpc", + Domains: []string{"*"}, + Routes: routes, + }}) + + return HTTPConnectionManagerFilter(&envoy_http_connection_manager.HttpConnectionManager{ + CodecType: envoy_http_connection_manager.HttpConnectionManager_AUTO, + StatPrefix: "grpc_ingress", + // limit request first byte to last byte time + RequestTimeout: &durationpb.Duration{ + Seconds: 15, + }, + RouteSpecifier: &envoy_http_connection_manager.HttpConnectionManager_RouteConfig{ + RouteConfig: rc, + }, + HttpFilters: []*envoy_http_connection_manager.HttpFilter{ + HTTPRouterFilter(), + }, + }) +} + +func shouldStartGRPCListener(options *config.Options) bool { + if options.GetGRPCAddr() == "" { + return false + } + + return config.IsAuthorize(options.Services) || config.IsDataBroker(options.Services) +} diff --git a/config/envoyconfig/listeners_main.go b/config/envoyconfig/listeners_main.go new file mode 100644 index 000000000..0bbd14134 --- /dev/null +++ b/config/envoyconfig/listeners_main.go @@ -0,0 +1,216 @@ +package envoyconfig + +import ( + "context" + "time" + + envoy_config_accesslog_v3 "github.com/envoyproxy/go-control-plane/envoy/config/accesslog/v3" + envoy_config_core_v3 "github.com/envoyproxy/go-control-plane/envoy/config/core/v3" + envoy_config_listener_v3 "github.com/envoyproxy/go-control-plane/envoy/config/listener/v3" + envoy_extensions_access_loggers_grpc_v3 "github.com/envoyproxy/go-control-plane/envoy/extensions/access_loggers/grpc/v3" + envoy_extensions_filters_network_http_connection_manager "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/http_connection_manager/v3" + envoy_type_v3 "github.com/envoyproxy/go-control-plane/envoy/type/v3" + "google.golang.org/protobuf/types/known/durationpb" + "google.golang.org/protobuf/types/known/wrapperspb" + + "github.com/pomerium/pomerium/config" +) + +func (b *Builder) buildMainListener( + ctx context.Context, + cfg *config.Config, + fullyStatic bool, +) (*envoy_config_listener_v3.Listener, error) { + if cfg.Options.InsecureServer { + return b.buildMainInsecureListener(ctx, cfg, fullyStatic) + } + return b.buildMainTLSListener(ctx, cfg, fullyStatic) +} + +func (b *Builder) buildMainInsecureListener( + ctx context.Context, + cfg *config.Config, + fullyStatic bool, +) (*envoy_config_listener_v3.Listener, error) { + li := newListener("http-ingress") + li.Address = buildAddress(cfg.Options.Addr, 80) + + // listener filters + if cfg.Options.UseProxyProtocol { + li.ListenerFilters = append(li.ListenerFilters, ProxyProtocolFilter()) + } + + filterChain, err := b.buildMainHTTPConnectionManagerFilterChain(ctx, cfg, fullyStatic, nil) + if err != nil { + return nil, err + } + li.FilterChains = append(li.FilterChains, filterChain) + + return li, nil +} + +func (b *Builder) buildMainTLSListener( + ctx context.Context, + cfg *config.Config, + fullyStatic bool, +) (*envoy_config_listener_v3.Listener, error) { + li := newListener("https-ingress") + li.Address = buildAddress(cfg.Options.Addr, 443) + + // listener filters + if cfg.Options.UseProxyProtocol { + li.ListenerFilters = append(li.ListenerFilters, ProxyProtocolFilter()) + } + li.ListenerFilters = append(li.ListenerFilters, TLSInspectorFilter()) + + // access log + if cfg.Options.DownstreamMTLS.Enforcement == config.MTLSEnforcementRejectConnection { + li.AccessLog = append(li.AccessLog, newListenerAccessLog()) + } + + // filter chains + li.FilterChains = append(li.FilterChains, b.buildACMETLSALPNFilterChain()) + + allCertificates, err := getAllCertificates(cfg) + if err != nil { + return nil, err + } + + tlsContext, err := b.buildDownstreamTLSContextMulti(ctx, cfg, allCertificates) + if err != nil { + return nil, err + } + + filterChain, err := b.buildMainHTTPConnectionManagerFilterChain(ctx, cfg, fullyStatic, newDownstreamTLSTransportSocket(tlsContext)) + if err != nil { + return nil, err + } + li.FilterChains = append(li.FilterChains, filterChain) + + return li, nil +} + +func (b *Builder) buildMainHTTPConnectionManagerFilterChain( + ctx context.Context, + cfg *config.Config, + fullyStatic bool, + transportSocket *envoy_config_core_v3.TransportSocket, +) (*envoy_config_listener_v3.FilterChain, error) { + filter, err := b.buildMainHTTPConnectionManagerFilter(ctx, cfg, fullyStatic) + if err != nil { + return nil, err + } + return &envoy_config_listener_v3.FilterChain{ + Filters: []*envoy_config_listener_v3.Filter{filter}, + TransportSocket: transportSocket, + }, nil +} + +func (b *Builder) buildMainHTTPConnectionManagerFilter( + ctx context.Context, + cfg *config.Config, + fullyStatic bool, +) (*envoy_config_listener_v3.Filter, error) { + var grpcClientTimeout *durationpb.Duration + if cfg.Options.GRPCClientTimeout != 0 { + grpcClientTimeout = durationpb.New(cfg.Options.GRPCClientTimeout) + } else { + grpcClientTimeout = durationpb.New(30 * time.Second) + } + + filters := []*envoy_extensions_filters_network_http_connection_manager.HttpFilter{ + LuaFilter(luascripts.RemoveImpersonateHeaders), + LuaFilter(luascripts.SetClientCertificateMetadata), + ExtAuthzFilter(grpcClientTimeout), + LuaFilter(luascripts.ExtAuthzSetCookie), + LuaFilter(luascripts.CleanUpstream), + LuaFilter(luascripts.RewriteHeaders), + } + filters = append(filters, HTTPRouterFilter()) + + var maxStreamDuration *durationpb.Duration + if cfg.Options.WriteTimeout > 0 { + maxStreamDuration = durationpb.New(cfg.Options.WriteTimeout) + } + + tracingProvider, err := buildTracingHTTP(cfg.Options) + if err != nil { + return nil, err + } + + localReply, err := b.buildLocalReplyConfig(cfg.Options) + if err != nil { + return nil, err + } + + mgr := &envoy_extensions_filters_network_http_connection_manager.HttpConnectionManager{ + AlwaysSetRequestIdInResponse: true, + CodecType: cfg.Options.GetCodecType().ToEnvoy(), + StatPrefix: "ingress", + HttpFilters: filters, + AccessLog: buildAccessLogs(cfg.Options), + CommonHttpProtocolOptions: &envoy_config_core_v3.HttpProtocolOptions{ + IdleTimeout: durationpb.New(cfg.Options.IdleTimeout), + MaxStreamDuration: maxStreamDuration, + }, + HttpProtocolOptions: http1ProtocolOptions, + RequestTimeout: durationpb.New(cfg.Options.ReadTimeout), + Tracing: &envoy_extensions_filters_network_http_connection_manager.HttpConnectionManager_Tracing{ + RandomSampling: &envoy_type_v3.Percent{Value: cfg.Options.TracingSampleRate * 100}, + Provider: tracingProvider, + }, + // See https://www.envoyproxy.io/docs/envoy/latest/configuration/http/http_conn_man/headers#x-forwarded-for + UseRemoteAddress: &wrapperspb.BoolValue{Value: true}, + SkipXffAppend: cfg.Options.SkipXffAppend, + XffNumTrustedHops: cfg.Options.XffNumTrustedHops, + LocalReplyConfig: localReply, + NormalizePath: wrapperspb.Bool(true), + } + + if fullyStatic { + routeConfiguration, err := b.buildMainRouteConfiguration(ctx, cfg) + if err != nil { + return nil, err + } + mgr.RouteSpecifier = &envoy_extensions_filters_network_http_connection_manager.HttpConnectionManager_RouteConfig{ + RouteConfig: routeConfiguration, + } + } else { + mgr.RouteSpecifier = &envoy_extensions_filters_network_http_connection_manager.HttpConnectionManager_Rds{ + Rds: &envoy_extensions_filters_network_http_connection_manager.Rds{ + ConfigSource: &envoy_config_core_v3.ConfigSource{ + ResourceApiVersion: envoy_config_core_v3.ApiVersion_V3, + ConfigSourceSpecifier: &envoy_config_core_v3.ConfigSource_Ads{}, + }, + RouteConfigName: "main", + }, + } + } + + return HTTPConnectionManagerFilter(mgr), nil +} + +func newListenerAccessLog() *envoy_config_accesslog_v3.AccessLog { + return &envoy_config_accesslog_v3.AccessLog{ + Name: "envoy.access_loggers.tcp_grpc", + ConfigType: &envoy_config_accesslog_v3.AccessLog_TypedConfig{ + TypedConfig: marshalAny(&envoy_extensions_access_loggers_grpc_v3.TcpGrpcAccessLogConfig{ + CommonConfig: &envoy_extensions_access_loggers_grpc_v3.CommonGrpcAccessLogConfig{ + LogName: "ingress-http-listener", + GrpcService: &envoy_config_core_v3.GrpcService{ + TargetSpecifier: &envoy_config_core_v3.GrpcService_EnvoyGrpc_{ + EnvoyGrpc: &envoy_config_core_v3.GrpcService_EnvoyGrpc{ + ClusterName: "pomerium-control-plane-grpc", + }, + }, + }, + TransportApiVersion: envoy_config_core_v3.ApiVersion_V3, + }, + }), + }, + } +} + +func shouldStartMainListener(options *config.Options) bool { + return config.IsAuthenticate(options.Services) || config.IsProxy(options.Services) +} diff --git a/config/envoyconfig/listeners_main_test.go b/config/envoyconfig/listeners_main_test.go new file mode 100644 index 000000000..af5d18ae3 --- /dev/null +++ b/config/envoyconfig/listeners_main_test.go @@ -0,0 +1,39 @@ +package envoyconfig + +import ( + "context" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/pomerium/pomerium/config" + "github.com/pomerium/pomerium/internal/testutil" +) + +func Test_requireProxyProtocol(t *testing.T) { + 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, + InsecureServer: true, + }}, false) + require.NoError(t, err) + testutil.AssertProtoJSONEqual(t, `[ + { + "name": "envoy.filters.listener.proxy_protocol", + "typedConfig": { + "@type": "type.googleapis.com/envoy.extensions.filters.listener.proxy_protocol.v3.ProxyProtocol" + } + } + ]`, li.GetListenerFilters()) + }) + t.Run("not required", func(t *testing.T) { + li, err := b.buildMainListener(context.Background(), &config.Config{Options: &config.Options{ + UseProxyProtocol: false, + InsecureServer: true, + }}, false) + require.NoError(t, err) + assert.Len(t, li.GetListenerFilters(), 0) + }) +} diff --git a/config/envoyconfig/listeners_metrics.go b/config/envoyconfig/listeners_metrics.go new file mode 100644 index 000000000..1abf7d1af --- /dev/null +++ b/config/envoyconfig/listeners_metrics.go @@ -0,0 +1,139 @@ +package envoyconfig + +import ( + "context" + "encoding/base64" + "fmt" + "net" + + envoy_config_listener_v3 "github.com/envoyproxy/go-control-plane/envoy/config/listener/v3" + envoy_config_route_v3 "github.com/envoyproxy/go-control-plane/envoy/config/route/v3" + envoy_http_connection_manager "github.com/envoyproxy/go-control-plane/envoy/extensions/filters/network/http_connection_manager/v3" + envoy_extensions_transport_sockets_tls_v3 "github.com/envoyproxy/go-control-plane/envoy/extensions/transport_sockets/tls/v3" + "google.golang.org/protobuf/types/known/wrapperspb" + + "github.com/pomerium/pomerium/config" + "github.com/pomerium/pomerium/internal/hashutil" + "github.com/pomerium/pomerium/internal/telemetry/metrics" +) + +func (b *Builder) buildMetricsListener(cfg *config.Config) (*envoy_config_listener_v3.Listener, error) { + filter := b.buildMetricsHTTPConnectionManagerFilter() + + filterChain := &envoy_config_listener_v3.FilterChain{ + Filters: []*envoy_config_listener_v3.Filter{ + filter, + }, + } + + cert, err := cfg.Options.GetMetricsCertificate() + if err != nil { + return nil, err + } + if cert != nil { + dtc := &envoy_extensions_transport_sockets_tls_v3.DownstreamTlsContext{ + CommonTlsContext: &envoy_extensions_transport_sockets_tls_v3.CommonTlsContext{ + TlsParams: tlsDownstreamParams, + TlsCertificates: []*envoy_extensions_transport_sockets_tls_v3.TlsCertificate{ + b.envoyTLSCertificateFromGoTLSCertificate(context.TODO(), cert), + }, + AlpnProtocols: []string{"h2", "http/1.1"}, + }, + } + + if cfg.Options.MetricsClientCA != "" { + bs, err := base64.StdEncoding.DecodeString(cfg.Options.MetricsClientCA) + if err != nil { + return nil, fmt.Errorf("xds: invalid metrics_client_ca: %w", err) + } + + dtc.RequireClientCertificate = wrapperspb.Bool(true) + dtc.CommonTlsContext.ValidationContextType = &envoy_extensions_transport_sockets_tls_v3.CommonTlsContext_ValidationContext{ + ValidationContext: &envoy_extensions_transport_sockets_tls_v3.CertificateValidationContext{ + TrustChainVerification: envoy_extensions_transport_sockets_tls_v3.CertificateValidationContext_VERIFY_TRUST_CHAIN, + TrustedCa: b.filemgr.BytesDataSource("metrics_client_ca.pem", bs), + }, + } + } else if cfg.Options.MetricsClientCAFile != "" { + dtc.RequireClientCertificate = wrapperspb.Bool(true) + dtc.CommonTlsContext.ValidationContextType = &envoy_extensions_transport_sockets_tls_v3.CommonTlsContext_ValidationContext{ + ValidationContext: &envoy_extensions_transport_sockets_tls_v3.CertificateValidationContext{ + TrustChainVerification: envoy_extensions_transport_sockets_tls_v3.CertificateValidationContext_VERIFY_TRUST_CHAIN, + TrustedCa: b.filemgr.FileDataSource(cfg.Options.MetricsClientCAFile), + }, + } + } + + filterChain.TransportSocket = newDownstreamTLSTransportSocket(dtc) + } + + // we ignore the host part of the address, only binding to + host, port, err := net.SplitHostPort(cfg.Options.MetricsAddr) + if err != nil { + return nil, fmt.Errorf("metrics_addr %s: %w", cfg.Options.MetricsAddr, err) + } + if port == "" { + return nil, fmt.Errorf("metrics_addr %s: port is required", cfg.Options.MetricsAddr) + } + // unless an explicit IP address was provided, and bind to all interfaces if hostname was provided + if net.ParseIP(host) == nil { + host = "" + } + + addr := buildAddress(net.JoinHostPort(host, port), 9902) + li := newListener(fmt.Sprintf("metrics-ingress-%d", hashutil.MustHash(addr))) + li.Address = addr + li.FilterChains = []*envoy_config_listener_v3.FilterChain{filterChain} + return li, nil +} + +func (b *Builder) buildMetricsHTTPConnectionManagerFilter() *envoy_config_listener_v3.Filter { + rc := newRouteConfiguration("metrics", []*envoy_config_route_v3.VirtualHost{{ + Name: "metrics", + Domains: []string{"*"}, + Routes: []*envoy_config_route_v3.Route{ + { + Name: "envoy-metrics", + Match: &envoy_config_route_v3.RouteMatch{ + PathSpecifier: &envoy_config_route_v3.RouteMatch_Prefix{Prefix: metrics.EnvoyMetricsPath}, + }, + Action: &envoy_config_route_v3.Route_Route{ + Route: &envoy_config_route_v3.RouteAction{ + ClusterSpecifier: &envoy_config_route_v3.RouteAction_Cluster{ + Cluster: envoyAdminClusterName, + }, + PrefixRewrite: "/stats/prometheus", + }, + }, + }, + { + Name: "metrics", + Match: &envoy_config_route_v3.RouteMatch{ + PathSpecifier: &envoy_config_route_v3.RouteMatch_Prefix{Prefix: "/"}, + }, + Action: &envoy_config_route_v3.Route_Route{ + Route: &envoy_config_route_v3.RouteAction{ + ClusterSpecifier: &envoy_config_route_v3.RouteAction_Cluster{ + Cluster: "pomerium-control-plane-metrics", + }, + }, + }, + }, + }, + }}) + + return HTTPConnectionManagerFilter(&envoy_http_connection_manager.HttpConnectionManager{ + CodecType: envoy_http_connection_manager.HttpConnectionManager_AUTO, + StatPrefix: "metrics", + RouteSpecifier: &envoy_http_connection_manager.HttpConnectionManager_RouteConfig{ + RouteConfig: rc, + }, + HttpFilters: []*envoy_http_connection_manager.HttpFilter{ + HTTPRouterFilter(), + }, + }) +} + +func shouldStartMetricsListener(options *config.Options) bool { + return options.MetricsAddr != "" +} diff --git a/config/envoyconfig/listeners_test.go b/config/envoyconfig/listeners_test.go index 6ddcc97bc..b968ce946 100644 --- a/config/envoyconfig/listeners_test.go +++ b/config/envoyconfig/listeners_test.go @@ -4,21 +4,18 @@ import ( "bytes" "context" "embed" - "encoding/base64" "os" "path/filepath" "runtime" "testing" "text/template" - envoy_config_route_v3 "github.com/envoyproxy/go-control-plane/envoy/config/route/v3" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/pomerium/pomerium/config" "github.com/pomerium/pomerium/config/envoyconfig/filemgr" "github.com/pomerium/pomerium/internal/testutil" - "github.com/pomerium/pomerium/pkg/cryptutil" ) const ( @@ -109,471 +106,3 @@ func Test_buildMainHTTPConnectionManagerFilter(t *testing.T) { require.NoError(t, err) testutil.AssertProtoJSONEqual(t, testData(t, "main_http_connection_manager_filter.json", nil), filter) } - -func Test_buildDownstreamTLSContext(t *testing.T) { - b := New("local-grpc", "local-http", "local-metrics", filemgr.NewManager(), nil) - - cacheDir, _ := os.UserCacheDir() - clientCAFileName := filepath.Join(cacheDir, "pomerium", "envoy", "files", "client-ca-313754424855313435355a5348.pem") - - t.Run("no-validation", func(t *testing.T) { - downstreamTLSContext, err := b.buildDownstreamTLSContextMulti(context.Background(), &config.Config{Options: &config.Options{}}, nil) - require.NoError(t, err) - testutil.AssertProtoJSONEqual(t, `{ - "commonTlsContext": { - "tlsParams": { - "cipherSuites": [ - "ECDHE-ECDSA-AES256-GCM-SHA384", - "ECDHE-RSA-AES256-GCM-SHA384", - "ECDHE-ECDSA-AES128-GCM-SHA256", - "ECDHE-RSA-AES128-GCM-SHA256", - "ECDHE-ECDSA-CHACHA20-POLY1305", - "ECDHE-RSA-CHACHA20-POLY1305" - ], - "tlsMinimumProtocolVersion": "TLSv1_2", - "tlsMaximumProtocolVersion": "TLSv1_3" - }, - "alpnProtocols": ["h2", "http/1.1"] - } - }`, downstreamTLSContext) - }) - t.Run("client-ca", func(t *testing.T) { - downstreamTLSContext, err := b.buildDownstreamTLSContextMulti(context.Background(), &config.Config{Options: &config.Options{ - DownstreamMTLS: config.DownstreamMTLSSettings{ - CA: "VEVTVAo=", // "TEST\n" (with a trailing newline) - }, - }}, nil) - require.NoError(t, err) - testutil.AssertProtoJSONEqual(t, `{ - "commonTlsContext": { - "tlsParams": { - "cipherSuites": [ - "ECDHE-ECDSA-AES256-GCM-SHA384", - "ECDHE-RSA-AES256-GCM-SHA384", - "ECDHE-ECDSA-AES128-GCM-SHA256", - "ECDHE-RSA-AES128-GCM-SHA256", - "ECDHE-ECDSA-CHACHA20-POLY1305", - "ECDHE-RSA-CHACHA20-POLY1305" - ], - "tlsMinimumProtocolVersion": "TLSv1_2", - "tlsMaximumProtocolVersion": "TLSv1_3" - }, - "alpnProtocols": ["h2", "http/1.1"], - "validationContext": { - "maxVerifyDepth": 1, - "onlyVerifyLeafCertCrl": true, - "trustChainVerification": "ACCEPT_UNTRUSTED", - "trustedCa": { - "filename": "`+clientCAFileName+`" - } - } - } - }`, downstreamTLSContext) - }) - t.Run("client-ca-strict", func(t *testing.T) { - downstreamTLSContext, err := b.buildDownstreamTLSContextMulti(context.Background(), &config.Config{Options: &config.Options{ - DownstreamMTLS: config.DownstreamMTLSSettings{ - CA: "VEVTVAo=", // "TEST\n" (with a trailing newline) - Enforcement: config.MTLSEnforcementRejectConnection, - }, - }}, nil) - require.NoError(t, err) - testutil.AssertProtoJSONEqual(t, `{ - "commonTlsContext": { - "tlsParams": { - "cipherSuites": [ - "ECDHE-ECDSA-AES256-GCM-SHA384", - "ECDHE-RSA-AES256-GCM-SHA384", - "ECDHE-ECDSA-AES128-GCM-SHA256", - "ECDHE-RSA-AES128-GCM-SHA256", - "ECDHE-ECDSA-CHACHA20-POLY1305", - "ECDHE-RSA-CHACHA20-POLY1305" - ], - "tlsMinimumProtocolVersion": "TLSv1_2", - "tlsMaximumProtocolVersion": "TLSv1_3" - }, - "alpnProtocols": ["h2", "http/1.1"], - "validationContext": { - "maxVerifyDepth": 1, - "onlyVerifyLeafCertCrl": true, - "trustedCa": { - "filename": "`+clientCAFileName+`" - } - } - }, - "requireClientCertificate": true - }`, downstreamTLSContext) - }) - t.Run("policy-client-ca", func(t *testing.T) { - downstreamTLSContext, err := b.buildDownstreamTLSContextMulti(context.Background(), &config.Config{Options: &config.Options{ - Policies: []config.Policy{ - { - From: "https://a.example.com:1234", - TLSDownstreamClientCA: "VEVTVA==", // "TEST" (no trailing newline) - }, - }, - }}, nil) - require.NoError(t, err) - - testutil.AssertProtoJSONEqual(t, `{ - "commonTlsContext": { - "tlsParams": { - "cipherSuites": [ - "ECDHE-ECDSA-AES256-GCM-SHA384", - "ECDHE-RSA-AES256-GCM-SHA384", - "ECDHE-ECDSA-AES128-GCM-SHA256", - "ECDHE-RSA-AES128-GCM-SHA256", - "ECDHE-ECDSA-CHACHA20-POLY1305", - "ECDHE-RSA-CHACHA20-POLY1305" - ], - "tlsMinimumProtocolVersion": "TLSv1_2", - "tlsMaximumProtocolVersion": "TLSv1_3" - }, - "alpnProtocols": ["h2", "http/1.1"], - "validationContext": { - "maxVerifyDepth": 1, - "onlyVerifyLeafCertCrl": true, - "trustChainVerification": "ACCEPT_UNTRUSTED", - "trustedCa": { - "filename": "`+clientCAFileName+`" - } - } - } - }`, downstreamTLSContext) - }) - t.Run("client-ca-max-verify-depth", func(t *testing.T) { - var maxVerifyDepth uint32 - config := &config.Config{Options: &config.Options{ - DownstreamMTLS: config.DownstreamMTLSSettings{ - MaxVerifyDepth: &maxVerifyDepth, - CA: "VEVTVAo=", // "TEST\n" - }, - }} - - maxVerifyDepth = 10 - downstreamTLSContext, err := b.buildDownstreamTLSContextMulti(context.Background(), config, nil) - require.NoError(t, err) - testutil.AssertProtoJSONEqual(t, `{ - "maxVerifyDepth": 10, - "onlyVerifyLeafCertCrl": true, - "trustChainVerification": "ACCEPT_UNTRUSTED", - "trustedCa": { - "filename": "`+clientCAFileName+`" - } - }`, downstreamTLSContext.GetCommonTlsContext().GetValidationContext()) - - maxVerifyDepth = 0 - downstreamTLSContext, err = b.buildDownstreamTLSContextMulti(context.Background(), config, nil) - require.NoError(t, err) - testutil.AssertProtoJSONEqual(t, `{ - "onlyVerifyLeafCertCrl": true, - "trustChainVerification": "ACCEPT_UNTRUSTED", - "trustedCa": { - "filename": "`+clientCAFileName+`" - } - }`, downstreamTLSContext.GetCommonTlsContext().GetValidationContext()) - }) - t.Run("client-ca-san-matchers", func(t *testing.T) { - config := &config.Config{Options: &config.Options{ - DownstreamMTLS: config.DownstreamMTLSSettings{ - CA: "VEVTVAo=", // "TEST\n" - MatchSubjectAltNames: []config.SANMatcher{ - {Type: config.SANTypeDNS, Pattern: `.*\.corp\.example\.com`}, - {Type: config.SANTypeEmail, Pattern: `.*@example\.com`}, - {Type: config.SANTypeIPAddress, Pattern: `10\.10\.42\..*`}, - {Type: config.SANTypeURI, Pattern: `spiffe://example\.com/.*`}, - {Type: config.SANTypeUserPrincipalName, Pattern: `^device-id$`}, - }, - }, - }} - downstreamTLSContext, err := b.buildDownstreamTLSContextMulti(context.Background(), config, nil) - require.NoError(t, err) - testutil.AssertProtoJSONEqual(t, `{ - "maxVerifyDepth": 1, - "matchTypedSubjectAltNames": [ - { - "matcher": { - "safeRegex": { - "googleRe2": {}, - "regex": ".*\\.corp\\.example\\.com" - } - }, - "sanType": "DNS" - }, - { - "matcher": { - "safeRegex": { - "googleRe2": {}, - "regex": ".*@example\\.com" - } - }, - "sanType": "EMAIL" - }, - { - "matcher": { - "safeRegex": { - "googleRe2": {}, - "regex": "10\\.10\\.42\\..*" - } - }, - "sanType": "IP_ADDRESS" - }, - { - "matcher": { - "safeRegex": { - "googleRe2": {}, - "regex": "spiffe://example\\.com/.*" - } - }, - "sanType": "URI" - }, - { - "matcher": { - "safeRegex": { - "googleRe2": {}, - "regex": "^device-id$" - } - }, - "sanType": "OTHER_NAME", - "oid": "1.3.6.1.4.1.311.20.2.3" - } - ], - "onlyVerifyLeafCertCrl": true, - "trustChainVerification": "ACCEPT_UNTRUSTED", - "trustedCa": { - "filename": "`+clientCAFileName+`" - } - }`, downstreamTLSContext.GetCommonTlsContext().GetValidationContext()) - }) - t.Run("http1", func(t *testing.T) { - downstreamTLSContext, err := b.buildDownstreamTLSContextMulti(context.Background(), &config.Config{Options: &config.Options{ - Cert: aExampleComCert, - Key: aExampleComKey, - CodecType: config.CodecTypeHTTP1, - }}, nil) - require.NoError(t, err) - - testutil.AssertProtoJSONEqual(t, `{ - "commonTlsContext": { - "tlsParams": { - "cipherSuites": [ - "ECDHE-ECDSA-AES256-GCM-SHA384", - "ECDHE-RSA-AES256-GCM-SHA384", - "ECDHE-ECDSA-AES128-GCM-SHA256", - "ECDHE-RSA-AES128-GCM-SHA256", - "ECDHE-ECDSA-CHACHA20-POLY1305", - "ECDHE-RSA-CHACHA20-POLY1305" - ], - "tlsMinimumProtocolVersion": "TLSv1_2", - "tlsMaximumProtocolVersion": "TLSv1_3" - }, - "alpnProtocols": ["http/1.1"] - } - }`, downstreamTLSContext) - }) - t.Run("http2", func(t *testing.T) { - downstreamTLSContext, err := b.buildDownstreamTLSContextMulti(context.Background(), &config.Config{Options: &config.Options{ - Cert: aExampleComCert, - Key: aExampleComKey, - CodecType: config.CodecTypeHTTP2, - }}, nil) - require.NoError(t, err) - - testutil.AssertProtoJSONEqual(t, `{ - "commonTlsContext": { - "tlsParams": { - "cipherSuites": [ - "ECDHE-ECDSA-AES256-GCM-SHA384", - "ECDHE-RSA-AES256-GCM-SHA384", - "ECDHE-ECDSA-AES128-GCM-SHA256", - "ECDHE-RSA-AES128-GCM-SHA256", - "ECDHE-ECDSA-CHACHA20-POLY1305", - "ECDHE-RSA-CHACHA20-POLY1305" - ], - "tlsMinimumProtocolVersion": "TLSv1_2", - "tlsMaximumProtocolVersion": "TLSv1_3" - }, - "alpnProtocols": ["h2"] - } - }`, downstreamTLSContext) - }) -} - -func Test_clientCABundle(t *testing.T) { - // Make sure multiple bundled CAs are separated by newlines. - clientCA1 := []byte("client CA 1") - clientCA2 := []byte("client CA 2") - clientCA3 := []byte("client CA 3") - - b64 := base64.StdEncoding.EncodeToString - cfg := &config.Config{Options: &config.Options{ - DownstreamMTLS: config.DownstreamMTLSSettings{ - CA: b64(clientCA3), - }, - Policies: []config.Policy{ - { - From: "https://foo.example.com", - TLSDownstreamClientCA: b64(clientCA2), - }, - { - From: "https://bar.example.com", - TLSDownstreamClientCA: b64(clientCA1), - }, - }, - }} - expected := []byte("client CA 3\nclient CA 2\nclient CA 1\n") - actual := clientCABundle(context.Background(), cfg) - assert.Equal(t, expected, actual) -} - -func Test_getAllDomains(t *testing.T) { - cert, err := cryptutil.GenerateCertificate(nil, "*.unknown.example.com") - require.NoError(t, err) - certPEM, keyPEM, err := cryptutil.EncodeCertificate(cert) - require.NoError(t, err) - - options := &config.Options{ - Addr: "127.0.0.1:9000", - GRPCAddr: "127.0.0.1:9001", - Services: "all", - AuthenticateURLString: "https://authenticate.example.com", - AuthenticateInternalURLString: "https://authenticate.int.example.com", - AuthorizeURLString: "https://authorize.example.com:9001", - DataBrokerURLString: "https://cache.example.com:9001", - Policies: []config.Policy{ - {From: "http://a.example.com"}, - {From: "https://b.example.com"}, - {From: "https://c.example.com"}, - {From: "https://d.unknown.example.com"}, - }, - Cert: base64.StdEncoding.EncodeToString(certPEM), - Key: base64.StdEncoding.EncodeToString(keyPEM), - } - t.Run("routable", func(t *testing.T) { - t.Run("http", func(t *testing.T) { - actual, err := getAllRouteableHosts(options, "127.0.0.1:9000") - require.NoError(t, err) - expect := []string{ - "a.example.com", - "a.example.com:80", - "authenticate.example.com", - "authenticate.example.com:443", - "authenticate.int.example.com", - "authenticate.int.example.com:443", - "b.example.com", - "b.example.com:443", - "c.example.com", - "c.example.com:443", - "d.unknown.example.com", - "d.unknown.example.com:443", - } - assert.Equal(t, expect, actual) - }) - t.Run("grpc", func(t *testing.T) { - actual, err := getAllRouteableHosts(options, "127.0.0.1:9001") - require.NoError(t, err) - expect := []string{ - "authorize.example.com:9001", - "cache.example.com:9001", - } - assert.Equal(t, expect, actual) - }) - t.Run("both", func(t *testing.T) { - newOptions := *options - newOptions.GRPCAddr = newOptions.Addr - actual, err := getAllRouteableHosts(&newOptions, "127.0.0.1:9000") - require.NoError(t, err) - expect := []string{ - "a.example.com", - "a.example.com:80", - "authenticate.example.com", - "authenticate.example.com:443", - "authenticate.int.example.com", - "authenticate.int.example.com:443", - "authorize.example.com:9001", - "b.example.com", - "b.example.com:443", - "c.example.com", - "c.example.com:443", - "cache.example.com:9001", - "d.unknown.example.com", - "d.unknown.example.com:443", - } - assert.Equal(t, expect, actual) - }) - }) - - t.Run("exclude default authenticate", func(t *testing.T) { - options := config.NewDefaultOptions() - options.Policies = []config.Policy{ - {From: "https://a.example.com"}, - } - actual, err := getAllRouteableHosts(options, ":443") - require.NoError(t, err) - assert.Equal(t, []string{"a.example.com"}, actual) - }) -} - -func Test_urlMatchesHost(t *testing.T) { - t.Parallel() - - for _, tc := range []struct { - name string - sourceURL string - host string - matches bool - }{ - {"no port", "http://example.com", "example.com", true}, - {"host http port", "http://example.com", "example.com:80", true}, - {"host https port", "https://example.com", "example.com:443", true}, - {"with port", "https://example.com:443", "example.com:443", true}, - {"url port", "https://example.com:443", "example.com", true}, - {"non standard port", "http://example.com:81", "example.com", false}, - {"non standard host port", "http://example.com:81", "example.com:80", false}, - } { - tc := tc - t.Run(tc.name, func(t *testing.T) { - t.Parallel() - - assert.Equal(t, tc.matches, urlMatchesHost(mustParseURL(t, tc.sourceURL), tc.host), - "urlMatchesHost(%s,%s)", tc.sourceURL, tc.host) - }) - } -} - -func Test_buildRouteConfiguration(t *testing.T) { - 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) - assert.Equal(t, "test-route-configuration", routeConfig.GetName()) - assert.Equal(t, virtualHosts, routeConfig.GetVirtualHosts()) - assert.False(t, routeConfig.GetValidateClusters().GetValue()) -} - -func Test_requireProxyProtocol(t *testing.T) { - 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, - InsecureServer: true, - }}, false) - require.NoError(t, err) - testutil.AssertProtoJSONEqual(t, `[ - { - "name": "envoy.filters.listener.proxy_protocol", - "typedConfig": { - "@type": "type.googleapis.com/envoy.extensions.filters.listener.proxy_protocol.v3.ProxyProtocol" - } - } - ]`, li.GetListenerFilters()) - }) - t.Run("not required", func(t *testing.T) { - li, err := b.buildMainListener(context.Background(), &config.Config{Options: &config.Options{ - UseProxyProtocol: false, - InsecureServer: true, - }}, false) - require.NoError(t, err) - assert.Len(t, li.GetListenerFilters(), 0) - }) -} diff --git a/config/envoyconfig/outbound.go b/config/envoyconfig/outbound.go index 2c44f95c2..42c51c03d 100644 --- a/config/envoyconfig/outbound.go +++ b/config/envoyconfig/outbound.go @@ -20,12 +20,9 @@ func (b *Builder) buildOutboundListener(cfg *config.Config) (*envoy_config_liste return nil, fmt.Errorf("invalid outbound port %v: %w", cfg.OutboundPort, err) } - filter, err := b.buildOutboundHTTPConnectionManager() - if err != nil { - return nil, fmt.Errorf("error building outbound http connection manager filter: %w", err) - } + filter := b.buildOutboundHTTPConnectionManager() - li := newEnvoyListener("outbound-ingress") + li := newListener("outbound-ingress") li.Address = &envoy_config_core_v3.Address{ Address: &envoy_config_core_v3.Address_SocketAddress{ SocketAddress: &envoy_config_core_v3.SocketAddress{ @@ -43,11 +40,8 @@ func (b *Builder) buildOutboundListener(cfg *config.Config) (*envoy_config_liste return li, nil } -func (b *Builder) buildOutboundHTTPConnectionManager() (*envoy_config_listener_v3.Filter, error) { - rc, err := b.buildOutboundRouteConfiguration() - if err != nil { - return nil, err - } +func (b *Builder) buildOutboundHTTPConnectionManager() *envoy_config_listener_v3.Filter { + rc := b.buildOutboundRouteConfiguration() tc := marshalAny(&envoy_http_connection_manager.HttpConnectionManager{ CodecType: envoy_http_connection_manager.HttpConnectionManager_AUTO, @@ -69,11 +63,11 @@ func (b *Builder) buildOutboundHTTPConnectionManager() (*envoy_config_listener_v ConfigType: &envoy_config_listener_v3.Filter_TypedConfig{ TypedConfig: tc, }, - }, nil + } } -func (b *Builder) buildOutboundRouteConfiguration() (*envoy_config_route_v3.RouteConfiguration, error) { - return b.buildRouteConfiguration("grpc", []*envoy_config_route_v3.VirtualHost{{ +func (b *Builder) buildOutboundRouteConfiguration() *envoy_config_route_v3.RouteConfiguration { + return newRouteConfiguration("grpc", []*envoy_config_route_v3.VirtualHost{{ Name: "grpc", Domains: []string{"*"}, Routes: b.buildOutboundRoutes(), diff --git a/config/envoyconfig/route_configurations.go b/config/envoyconfig/route_configurations.go index d20e7263f..4757b1a74 100644 --- a/config/envoyconfig/route_configurations.go +++ b/config/envoyconfig/route_configurations.go @@ -1,12 +1,18 @@ package envoyconfig import ( + "cmp" "context" + "net/url" + "strings" envoy_config_route_v3 "github.com/envoyproxy/go-control-plane/envoy/config/route/v3" + "github.com/hashicorp/go-set/v3" + "google.golang.org/protobuf/types/known/wrapperspb" "github.com/pomerium/pomerium/config" "github.com/pomerium/pomerium/internal/telemetry/trace" + "github.com/pomerium/pomerium/internal/urlutil" ) // BuildRouteConfigurations builds the route configurations for the RDS service. @@ -96,10 +102,61 @@ func (b *Builder) buildMainRouteConfiguration( virtualHosts = append(virtualHosts, vh) - rc, err := b.buildRouteConfiguration("main", virtualHosts) - if err != nil { - return nil, err - } - + rc := newRouteConfiguration("main", virtualHosts) return rc, nil } + +func getAllRouteableHosts(options *config.Options, addr string) ([]string, error) { + allHosts := set.NewTreeSet(cmp.Compare[string]) + + if addr == options.Addr { + hosts, err := options.GetAllRouteableHTTPHosts() + if err != nil { + return nil, err + } + allHosts.InsertSlice(hosts) + } + + if addr == options.GetGRPCAddr() { + hosts, err := options.GetAllRouteableGRPCHosts() + if err != nil { + return nil, err + } + allHosts.InsertSlice(hosts) + } + + var filtered []string + for host := range allHosts.Items() { + if !strings.Contains(host, "*") { + filtered = append(filtered, host) + } + } + return filtered, nil +} + +func newRouteConfiguration(name string, virtualHosts []*envoy_config_route_v3.VirtualHost) *envoy_config_route_v3.RouteConfiguration { + return &envoy_config_route_v3.RouteConfiguration{ + Name: name, + VirtualHosts: virtualHosts, + // disable cluster validation since the order of LDS/CDS updates isn't guaranteed + ValidateClusters: &wrapperspb.BoolValue{Value: false}, + } +} + +func urlsMatchHost(urls []*url.URL, host string) bool { + for _, u := range urls { + if urlMatchesHost(u, host) { + return true + } + } + return false +} + +func urlMatchesHost(u *url.URL, host string) bool { + for _, h := range urlutil.GetDomainsForURL(u, true) { + if h == host { + return true + } + } + return false +} diff --git a/config/envoyconfig/route_configurations_test.go b/config/envoyconfig/route_configurations_test.go index bb0bfa3c0..d7e97eb6a 100644 --- a/config/envoyconfig/route_configurations_test.go +++ b/config/envoyconfig/route_configurations_test.go @@ -2,10 +2,12 @@ package envoyconfig import ( "context" + "encoding/base64" "testing" "time" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "google.golang.org/protobuf/encoding/protojson" "github.com/pomerium/pomerium/config" @@ -159,3 +161,118 @@ func TestBuilder_buildMainRouteConfiguration(t *testing.T) { }`, routeConfiguration) } + +func Test_getAllDomains(t *testing.T) { + cert, err := cryptutil.GenerateCertificate(nil, "*.unknown.example.com") + require.NoError(t, err) + certPEM, keyPEM, err := cryptutil.EncodeCertificate(cert) + require.NoError(t, err) + + options := &config.Options{ + Addr: "127.0.0.1:9000", + GRPCAddr: "127.0.0.1:9001", + Services: "all", + AuthenticateURLString: "https://authenticate.example.com", + AuthenticateInternalURLString: "https://authenticate.int.example.com", + AuthorizeURLString: "https://authorize.example.com:9001", + DataBrokerURLString: "https://cache.example.com:9001", + Policies: []config.Policy{ + {From: "http://a.example.com"}, + {From: "https://b.example.com"}, + {From: "https://c.example.com"}, + {From: "https://d.unknown.example.com"}, + }, + Cert: base64.StdEncoding.EncodeToString(certPEM), + Key: base64.StdEncoding.EncodeToString(keyPEM), + } + t.Run("routable", func(t *testing.T) { + t.Run("http", func(t *testing.T) { + actual, err := getAllRouteableHosts(options, "127.0.0.1:9000") + require.NoError(t, err) + expect := []string{ + "a.example.com", + "a.example.com:80", + "authenticate.example.com", + "authenticate.example.com:443", + "authenticate.int.example.com", + "authenticate.int.example.com:443", + "b.example.com", + "b.example.com:443", + "c.example.com", + "c.example.com:443", + "d.unknown.example.com", + "d.unknown.example.com:443", + } + assert.Equal(t, expect, actual) + }) + t.Run("grpc", func(t *testing.T) { + actual, err := getAllRouteableHosts(options, "127.0.0.1:9001") + require.NoError(t, err) + expect := []string{ + "authorize.example.com:9001", + "cache.example.com:9001", + } + assert.Equal(t, expect, actual) + }) + t.Run("both", func(t *testing.T) { + newOptions := *options + newOptions.GRPCAddr = newOptions.Addr + actual, err := getAllRouteableHosts(&newOptions, "127.0.0.1:9000") + require.NoError(t, err) + expect := []string{ + "a.example.com", + "a.example.com:80", + "authenticate.example.com", + "authenticate.example.com:443", + "authenticate.int.example.com", + "authenticate.int.example.com:443", + "authorize.example.com:9001", + "b.example.com", + "b.example.com:443", + "c.example.com", + "c.example.com:443", + "cache.example.com:9001", + "d.unknown.example.com", + "d.unknown.example.com:443", + } + assert.Equal(t, expect, actual) + }) + }) + + t.Run("exclude default authenticate", func(t *testing.T) { + options := config.NewDefaultOptions() + options.Policies = []config.Policy{ + {From: "https://a.example.com"}, + } + actual, err := getAllRouteableHosts(options, ":443") + require.NoError(t, err) + assert.Equal(t, []string{"a.example.com"}, actual) + }) +} + +func Test_urlMatchesHost(t *testing.T) { + t.Parallel() + + for _, tc := range []struct { + name string + sourceURL string + host string + matches bool + }{ + {"no port", "http://example.com", "example.com", true}, + {"host http port", "http://example.com", "example.com:80", true}, + {"host https port", "https://example.com", "example.com:443", true}, + {"with port", "https://example.com:443", "example.com:443", true}, + {"url port", "https://example.com:443", "example.com", true}, + {"non standard port", "http://example.com:81", "example.com", false}, + {"non standard host port", "http://example.com:81", "example.com:80", false}, + } { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + assert.Equal(t, tc.matches, urlMatchesHost(mustParseURL(t, tc.sourceURL), tc.host), + "urlMatchesHost(%s,%s)", tc.sourceURL, tc.host) + }) + } +} diff --git a/config/envoyconfig/tls.go b/config/envoyconfig/tls.go index 77a960b23..abf1d51c7 100644 --- a/config/envoyconfig/tls.go +++ b/config/envoyconfig/tls.go @@ -1,17 +1,26 @@ package envoyconfig import ( + "bytes" + "context" "crypto/tls" "crypto/x509" "encoding/asn1" + "encoding/base64" + "encoding/pem" "fmt" "net/netip" "net/url" "regexp" "strings" + envoy_config_core_v3 "github.com/envoyproxy/go-control-plane/envoy/config/core/v3" envoy_extensions_transport_sockets_tls_v3 "github.com/envoyproxy/go-control-plane/envoy/extensions/transport_sockets/tls/v3" envoy_type_matcher_v3 "github.com/envoyproxy/go-control-plane/envoy/type/matcher/v3" + "google.golang.org/protobuf/types/known/wrapperspb" + + "github.com/pomerium/pomerium/config" + "github.com/pomerium/pomerium/internal/log" ) var ( @@ -121,6 +130,103 @@ func (b *Builder) buildSubjectNameIndication( return sni } +func (b *Builder) envoyTLSCertificateFromGoTLSCertificate( + ctx context.Context, + cert *tls.Certificate, +) *envoy_extensions_transport_sockets_tls_v3.TlsCertificate { + envoyCert := &envoy_extensions_transport_sockets_tls_v3.TlsCertificate{} + var chain bytes.Buffer + for _, cbs := range cert.Certificate { + _ = pem.Encode(&chain, &pem.Block{ + Type: "CERTIFICATE", + Bytes: cbs, + }) + } + envoyCert.CertificateChain = b.filemgr.BytesDataSource("tls-crt.pem", chain.Bytes()) + if cert.OCSPStaple != nil { + envoyCert.OcspStaple = b.filemgr.BytesDataSource("ocsp-staple", cert.OCSPStaple) + } + if bs, err := x509.MarshalPKCS8PrivateKey(cert.PrivateKey); err == nil { + envoyCert.PrivateKey = b.filemgr.BytesDataSource("tls-key.pem", pem.EncodeToMemory( + &pem.Block{ + Type: "PRIVATE KEY", + Bytes: bs, + }, + )) + } else { + log.Ctx(ctx).Error().Err(err).Msg("failed to marshal private key for tls config") + } + for _, scts := range cert.SignedCertificateTimestamps { + envoyCert.SignedCertificateTimestamp = append(envoyCert.SignedCertificateTimestamp, + b.filemgr.BytesDataSource("signed-certificate-timestamp", scts)) + } + return envoyCert +} + +func (b *Builder) envoyTLSCertificatesFromGoTLSCertificates(ctx context.Context, certs []tls.Certificate) ( + []*envoy_extensions_transport_sockets_tls_v3.TlsCertificate, error, +) { + envoyCerts := make([]*envoy_extensions_transport_sockets_tls_v3.TlsCertificate, 0, len(certs)) + for i := range certs { + cert := &certs[i] + if err := validateCertificate(cert); err != nil { + return nil, fmt.Errorf("invalid certificate for domain %s: %w", + cert.Leaf.Subject.CommonName, err) + } + envoyCert := b.envoyTLSCertificateFromGoTLSCertificate(ctx, cert) + envoyCerts = append(envoyCerts, envoyCert) + } + return envoyCerts, nil +} + +// clientCABundle returns a bundle of the globally configured client CA and any +// per-route client CAs. +func clientCABundle(ctx context.Context, cfg *config.Config) []byte { + var bundle bytes.Buffer + ca, _ := cfg.Options.DownstreamMTLS.GetCA() + addCAToBundle(&bundle, ca) + for p := range cfg.Options.GetAllPolicies() { + // We don't need to check TLSDownstreamClientCAFile here because + // Policy.Validate() will populate TLSDownstreamClientCA when + // TLSDownstreamClientCAFile is set. + if p.TLSDownstreamClientCA == "" { + continue + } + ca, err := base64.StdEncoding.DecodeString(p.TLSDownstreamClientCA) + if err != nil { + log.Ctx(ctx).Error().Stringer("policy", p).Err(err).Msg("invalid client CA") + continue + } + addCAToBundle(&bundle, ca) + } + return bundle.Bytes() +} + +func addCAToBundle(bundle *bytes.Buffer, ca []byte) { + if len(ca) == 0 { + return + } + bundle.Write(ca) + // Make sure each CA is separated by a newline. + if ca[len(ca)-1] != '\n' { + bundle.WriteByte('\n') + } +} + +func getAllCertificates(cfg *config.Config) ([]tls.Certificate, error) { + allCertificates, err := cfg.AllCertificates() + if err != nil { + return nil, fmt.Errorf("error collecting all certificates: %w", err) + } + + wc, err := cfg.GenerateCatchAllCertificate() + if err != nil { + return nil, fmt.Errorf("error getting wildcard certificate: %w", err) + } + + return append(allCertificates, *wc), nil +} + // validateCertificate validates that a certificate can be used with Envoy's TLS stack. func validateCertificate(cert *tls.Certificate) error { if len(cert.Certificate) == 0 { @@ -149,3 +255,95 @@ func hasMustStaple(cert *x509.Certificate) bool { } return false } + +func newDownstreamTLSTransportSocket( + downstreamTLSContext *envoy_extensions_transport_sockets_tls_v3.DownstreamTlsContext, +) *envoy_config_core_v3.TransportSocket { + return &envoy_config_core_v3.TransportSocket{ + Name: "tls", + ConfigType: &envoy_config_core_v3.TransportSocket_TypedConfig{ + TypedConfig: marshalAny(downstreamTLSContext), + }, + } +} + +func (b *Builder) buildDownstreamTLSContextMulti( + ctx context.Context, + cfg *config.Config, + certs []tls.Certificate, +) ( + *envoy_extensions_transport_sockets_tls_v3.DownstreamTlsContext, + error, +) { + envoyCerts, err := b.envoyTLSCertificatesFromGoTLSCertificates(ctx, certs) + if err != nil { + return nil, err + } + dtc := &envoy_extensions_transport_sockets_tls_v3.DownstreamTlsContext{ + CommonTlsContext: &envoy_extensions_transport_sockets_tls_v3.CommonTlsContext{ + TlsParams: tlsDownstreamParams, + TlsCertificates: envoyCerts, + AlpnProtocols: getALPNProtos(cfg.Options), + }, + } + b.buildDownstreamValidationContext(ctx, dtc, cfg) + return dtc, nil +} + +func getALPNProtos(opts *config.Options) []string { + switch opts.GetCodecType() { + case config.CodecTypeHTTP1: + return []string{"http/1.1"} + case config.CodecTypeHTTP2: + return []string{"h2"} + default: + return []string{"h2", "http/1.1"} + } +} + +func (b *Builder) buildDownstreamValidationContext( + ctx context.Context, + dtc *envoy_extensions_transport_sockets_tls_v3.DownstreamTlsContext, + cfg *config.Config, +) { + clientCA := clientCABundle(ctx, cfg) + if len(clientCA) == 0 { + return + } + + vc := &envoy_extensions_transport_sockets_tls_v3.CertificateValidationContext{ + TrustedCa: b.filemgr.BytesDataSource("client-ca.pem", clientCA), + MatchTypedSubjectAltNames: make([]*envoy_extensions_transport_sockets_tls_v3.SubjectAltNameMatcher, + 0, len(cfg.Options.DownstreamMTLS.MatchSubjectAltNames)), + OnlyVerifyLeafCertCrl: true, + } + for i := range cfg.Options.DownstreamMTLS.MatchSubjectAltNames { + vc.MatchTypedSubjectAltNames = append(vc.MatchTypedSubjectAltNames, + cfg.Options.DownstreamMTLS.MatchSubjectAltNames[i].ToEnvoyProto()) + } + + if d := cfg.Options.DownstreamMTLS.GetMaxVerifyDepth(); d > 0 { + vc.MaxVerifyDepth = wrapperspb.UInt32(d) + } + + if cfg.Options.DownstreamMTLS.GetEnforcement() == config.MTLSEnforcementRejectConnection { + dtc.RequireClientCertificate = wrapperspb.Bool(true) + } else { + vc.TrustChainVerification = envoy_extensions_transport_sockets_tls_v3.CertificateValidationContext_ACCEPT_UNTRUSTED + } + + if crl := cfg.Options.DownstreamMTLS.CRL; crl != "" { + bs, err := base64.StdEncoding.DecodeString(crl) + if err != nil { + log.Ctx(ctx).Error().Err(err).Msg("invalid client CRL") + } else { + vc.Crl = b.filemgr.BytesDataSource("client-crl.pem", bs) + } + } else if crlf := cfg.Options.DownstreamMTLS.CRLFile; crlf != "" { + vc.Crl = b.filemgr.FileDataSource(crlf) + } + + dtc.CommonTlsContext.ValidationContextType = &envoy_extensions_transport_sockets_tls_v3.CommonTlsContext_ValidationContext{ + ValidationContext: vc, + } +} diff --git a/config/envoyconfig/tls_test.go b/config/envoyconfig/tls_test.go index 95ca632e2..e8bd264ab 100644 --- a/config/envoyconfig/tls_test.go +++ b/config/envoyconfig/tls_test.go @@ -1,14 +1,20 @@ package envoyconfig import ( + "context" "crypto/x509" "crypto/x509/pkix" + "encoding/base64" "net/url" + "os" + "path/filepath" "testing" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "github.com/pomerium/pomerium/config" + "github.com/pomerium/pomerium/config/envoyconfig/filemgr" "github.com/pomerium/pomerium/internal/testutil" "github.com/pomerium/pomerium/pkg/cryptutil" ) @@ -74,3 +80,319 @@ func TestValidateCertificate(t *testing.T) { assert.Error(t, validateCertificate(cert), "should return an error for a must-staple TLS certificate that has no stapled OCSP response") } + +func Test_buildDownstreamTLSContext(t *testing.T) { + b := New("local-grpc", "local-http", "local-metrics", filemgr.NewManager(), nil) + + cacheDir, _ := os.UserCacheDir() + clientCAFileName := filepath.Join(cacheDir, "pomerium", "envoy", "files", "client-ca-313754424855313435355a5348.pem") + + t.Run("no-validation", func(t *testing.T) { + downstreamTLSContext, err := b.buildDownstreamTLSContextMulti(context.Background(), &config.Config{Options: &config.Options{}}, nil) + require.NoError(t, err) + testutil.AssertProtoJSONEqual(t, `{ + "commonTlsContext": { + "tlsParams": { + "cipherSuites": [ + "ECDHE-ECDSA-AES256-GCM-SHA384", + "ECDHE-RSA-AES256-GCM-SHA384", + "ECDHE-ECDSA-AES128-GCM-SHA256", + "ECDHE-RSA-AES128-GCM-SHA256", + "ECDHE-ECDSA-CHACHA20-POLY1305", + "ECDHE-RSA-CHACHA20-POLY1305" + ], + "tlsMinimumProtocolVersion": "TLSv1_2", + "tlsMaximumProtocolVersion": "TLSv1_3" + }, + "alpnProtocols": ["h2", "http/1.1"] + } + }`, downstreamTLSContext) + }) + t.Run("client-ca", func(t *testing.T) { + downstreamTLSContext, err := b.buildDownstreamTLSContextMulti(context.Background(), &config.Config{Options: &config.Options{ + DownstreamMTLS: config.DownstreamMTLSSettings{ + CA: "VEVTVAo=", // "TEST\n" (with a trailing newline) + }, + }}, nil) + require.NoError(t, err) + testutil.AssertProtoJSONEqual(t, `{ + "commonTlsContext": { + "tlsParams": { + "cipherSuites": [ + "ECDHE-ECDSA-AES256-GCM-SHA384", + "ECDHE-RSA-AES256-GCM-SHA384", + "ECDHE-ECDSA-AES128-GCM-SHA256", + "ECDHE-RSA-AES128-GCM-SHA256", + "ECDHE-ECDSA-CHACHA20-POLY1305", + "ECDHE-RSA-CHACHA20-POLY1305" + ], + "tlsMinimumProtocolVersion": "TLSv1_2", + "tlsMaximumProtocolVersion": "TLSv1_3" + }, + "alpnProtocols": ["h2", "http/1.1"], + "validationContext": { + "maxVerifyDepth": 1, + "onlyVerifyLeafCertCrl": true, + "trustChainVerification": "ACCEPT_UNTRUSTED", + "trustedCa": { + "filename": "`+clientCAFileName+`" + } + } + } + }`, downstreamTLSContext) + }) + t.Run("client-ca-strict", func(t *testing.T) { + downstreamTLSContext, err := b.buildDownstreamTLSContextMulti(context.Background(), &config.Config{Options: &config.Options{ + DownstreamMTLS: config.DownstreamMTLSSettings{ + CA: "VEVTVAo=", // "TEST\n" (with a trailing newline) + Enforcement: config.MTLSEnforcementRejectConnection, + }, + }}, nil) + require.NoError(t, err) + testutil.AssertProtoJSONEqual(t, `{ + "commonTlsContext": { + "tlsParams": { + "cipherSuites": [ + "ECDHE-ECDSA-AES256-GCM-SHA384", + "ECDHE-RSA-AES256-GCM-SHA384", + "ECDHE-ECDSA-AES128-GCM-SHA256", + "ECDHE-RSA-AES128-GCM-SHA256", + "ECDHE-ECDSA-CHACHA20-POLY1305", + "ECDHE-RSA-CHACHA20-POLY1305" + ], + "tlsMinimumProtocolVersion": "TLSv1_2", + "tlsMaximumProtocolVersion": "TLSv1_3" + }, + "alpnProtocols": ["h2", "http/1.1"], + "validationContext": { + "maxVerifyDepth": 1, + "onlyVerifyLeafCertCrl": true, + "trustedCa": { + "filename": "`+clientCAFileName+`" + } + } + }, + "requireClientCertificate": true + }`, downstreamTLSContext) + }) + t.Run("policy-client-ca", func(t *testing.T) { + downstreamTLSContext, err := b.buildDownstreamTLSContextMulti(context.Background(), &config.Config{Options: &config.Options{ + Policies: []config.Policy{ + { + From: "https://a.example.com:1234", + TLSDownstreamClientCA: "VEVTVA==", // "TEST" (no trailing newline) + }, + }, + }}, nil) + require.NoError(t, err) + + testutil.AssertProtoJSONEqual(t, `{ + "commonTlsContext": { + "tlsParams": { + "cipherSuites": [ + "ECDHE-ECDSA-AES256-GCM-SHA384", + "ECDHE-RSA-AES256-GCM-SHA384", + "ECDHE-ECDSA-AES128-GCM-SHA256", + "ECDHE-RSA-AES128-GCM-SHA256", + "ECDHE-ECDSA-CHACHA20-POLY1305", + "ECDHE-RSA-CHACHA20-POLY1305" + ], + "tlsMinimumProtocolVersion": "TLSv1_2", + "tlsMaximumProtocolVersion": "TLSv1_3" + }, + "alpnProtocols": ["h2", "http/1.1"], + "validationContext": { + "maxVerifyDepth": 1, + "onlyVerifyLeafCertCrl": true, + "trustChainVerification": "ACCEPT_UNTRUSTED", + "trustedCa": { + "filename": "`+clientCAFileName+`" + } + } + } + }`, downstreamTLSContext) + }) + t.Run("client-ca-max-verify-depth", func(t *testing.T) { + var maxVerifyDepth uint32 + config := &config.Config{Options: &config.Options{ + DownstreamMTLS: config.DownstreamMTLSSettings{ + MaxVerifyDepth: &maxVerifyDepth, + CA: "VEVTVAo=", // "TEST\n" + }, + }} + + maxVerifyDepth = 10 + downstreamTLSContext, err := b.buildDownstreamTLSContextMulti(context.Background(), config, nil) + require.NoError(t, err) + testutil.AssertProtoJSONEqual(t, `{ + "maxVerifyDepth": 10, + "onlyVerifyLeafCertCrl": true, + "trustChainVerification": "ACCEPT_UNTRUSTED", + "trustedCa": { + "filename": "`+clientCAFileName+`" + } + }`, downstreamTLSContext.GetCommonTlsContext().GetValidationContext()) + + maxVerifyDepth = 0 + downstreamTLSContext, err = b.buildDownstreamTLSContextMulti(context.Background(), config, nil) + require.NoError(t, err) + testutil.AssertProtoJSONEqual(t, `{ + "onlyVerifyLeafCertCrl": true, + "trustChainVerification": "ACCEPT_UNTRUSTED", + "trustedCa": { + "filename": "`+clientCAFileName+`" + } + }`, downstreamTLSContext.GetCommonTlsContext().GetValidationContext()) + }) + t.Run("client-ca-san-matchers", func(t *testing.T) { + config := &config.Config{Options: &config.Options{ + DownstreamMTLS: config.DownstreamMTLSSettings{ + CA: "VEVTVAo=", // "TEST\n" + MatchSubjectAltNames: []config.SANMatcher{ + {Type: config.SANTypeDNS, Pattern: `.*\.corp\.example\.com`}, + {Type: config.SANTypeEmail, Pattern: `.*@example\.com`}, + {Type: config.SANTypeIPAddress, Pattern: `10\.10\.42\..*`}, + {Type: config.SANTypeURI, Pattern: `spiffe://example\.com/.*`}, + {Type: config.SANTypeUserPrincipalName, Pattern: `^device-id$`}, + }, + }, + }} + downstreamTLSContext, err := b.buildDownstreamTLSContextMulti(context.Background(), config, nil) + require.NoError(t, err) + testutil.AssertProtoJSONEqual(t, `{ + "maxVerifyDepth": 1, + "matchTypedSubjectAltNames": [ + { + "matcher": { + "safeRegex": { + "googleRe2": {}, + "regex": ".*\\.corp\\.example\\.com" + } + }, + "sanType": "DNS" + }, + { + "matcher": { + "safeRegex": { + "googleRe2": {}, + "regex": ".*@example\\.com" + } + }, + "sanType": "EMAIL" + }, + { + "matcher": { + "safeRegex": { + "googleRe2": {}, + "regex": "10\\.10\\.42\\..*" + } + }, + "sanType": "IP_ADDRESS" + }, + { + "matcher": { + "safeRegex": { + "googleRe2": {}, + "regex": "spiffe://example\\.com/.*" + } + }, + "sanType": "URI" + }, + { + "matcher": { + "safeRegex": { + "googleRe2": {}, + "regex": "^device-id$" + } + }, + "sanType": "OTHER_NAME", + "oid": "1.3.6.1.4.1.311.20.2.3" + } + ], + "onlyVerifyLeafCertCrl": true, + "trustChainVerification": "ACCEPT_UNTRUSTED", + "trustedCa": { + "filename": "`+clientCAFileName+`" + } + }`, downstreamTLSContext.GetCommonTlsContext().GetValidationContext()) + }) + t.Run("http1", func(t *testing.T) { + downstreamTLSContext, err := b.buildDownstreamTLSContextMulti(context.Background(), &config.Config{Options: &config.Options{ + Cert: aExampleComCert, + Key: aExampleComKey, + CodecType: config.CodecTypeHTTP1, + }}, nil) + require.NoError(t, err) + + testutil.AssertProtoJSONEqual(t, `{ + "commonTlsContext": { + "tlsParams": { + "cipherSuites": [ + "ECDHE-ECDSA-AES256-GCM-SHA384", + "ECDHE-RSA-AES256-GCM-SHA384", + "ECDHE-ECDSA-AES128-GCM-SHA256", + "ECDHE-RSA-AES128-GCM-SHA256", + "ECDHE-ECDSA-CHACHA20-POLY1305", + "ECDHE-RSA-CHACHA20-POLY1305" + ], + "tlsMinimumProtocolVersion": "TLSv1_2", + "tlsMaximumProtocolVersion": "TLSv1_3" + }, + "alpnProtocols": ["http/1.1"] + } + }`, downstreamTLSContext) + }) + t.Run("http2", func(t *testing.T) { + downstreamTLSContext, err := b.buildDownstreamTLSContextMulti(context.Background(), &config.Config{Options: &config.Options{ + Cert: aExampleComCert, + Key: aExampleComKey, + CodecType: config.CodecTypeHTTP2, + }}, nil) + require.NoError(t, err) + + testutil.AssertProtoJSONEqual(t, `{ + "commonTlsContext": { + "tlsParams": { + "cipherSuites": [ + "ECDHE-ECDSA-AES256-GCM-SHA384", + "ECDHE-RSA-AES256-GCM-SHA384", + "ECDHE-ECDSA-AES128-GCM-SHA256", + "ECDHE-RSA-AES128-GCM-SHA256", + "ECDHE-ECDSA-CHACHA20-POLY1305", + "ECDHE-RSA-CHACHA20-POLY1305" + ], + "tlsMinimumProtocolVersion": "TLSv1_2", + "tlsMaximumProtocolVersion": "TLSv1_3" + }, + "alpnProtocols": ["h2"] + } + }`, downstreamTLSContext) + }) +} + +func Test_clientCABundle(t *testing.T) { + // Make sure multiple bundled CAs are separated by newlines. + clientCA1 := []byte("client CA 1") + clientCA2 := []byte("client CA 2") + clientCA3 := []byte("client CA 3") + + b64 := base64.StdEncoding.EncodeToString + cfg := &config.Config{Options: &config.Options{ + DownstreamMTLS: config.DownstreamMTLSSettings{ + CA: b64(clientCA3), + }, + Policies: []config.Policy{ + { + From: "https://foo.example.com", + TLSDownstreamClientCA: b64(clientCA2), + }, + { + From: "https://bar.example.com", + TLSDownstreamClientCA: b64(clientCA1), + }, + }, + }} + expected := []byte("client CA 3\nclient CA 2\nclient CA 1\n") + actual := clientCABundle(context.Background(), cfg) + assert.Equal(t, expected, actual) +}