mirror of
https://github.com/pomerium/pomerium.git
synced 2025-05-02 11:56:02 +02:00
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
This commit is contained in:
parent
3e51b4f905
commit
20a9be891f
13 changed files with 1227 additions and 1182 deletions
|
@ -4,9 +4,6 @@ package envoyconfig
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"crypto/tls"
|
|
||||||
"crypto/x509"
|
|
||||||
"encoding/pem"
|
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net"
|
"net"
|
||||||
|
@ -21,7 +18,6 @@ import (
|
||||||
envoy_config_cluster_v3 "github.com/envoyproxy/go-control-plane/envoy/config/cluster/v3"
|
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_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_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"
|
"github.com/martinlindhe/base36"
|
||||||
"golang.org/x/net/nettest"
|
"golang.org/x/net/nettest"
|
||||||
"google.golang.org/protobuf/proto"
|
"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 {
|
var rootCABundle struct {
|
||||||
sync.Once
|
sync.Once
|
||||||
value string
|
value string
|
||||||
|
|
|
@ -1,36 +1,14 @@
|
||||||
package envoyconfig
|
package envoyconfig
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
|
||||||
"cmp"
|
|
||||||
"context"
|
"context"
|
||||||
"crypto/tls"
|
|
||||||
"encoding/base64"
|
|
||||||
"fmt"
|
|
||||||
"net"
|
|
||||||
"net/url"
|
|
||||||
"runtime"
|
"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_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"
|
"google.golang.org/protobuf/types/known/wrapperspb"
|
||||||
|
|
||||||
"github.com/pomerium/pomerium/config"
|
"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/telemetry/trace"
|
||||||
"github.com/pomerium/pomerium/internal/urlutil"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const listenerBufferLimit uint32 = 32 * 1024
|
const listenerBufferLimit uint32 = 32 * 1024
|
||||||
|
@ -62,7 +40,7 @@ func (b *Builder) BuildListeners(
|
||||||
listeners = append(listeners, li)
|
listeners = append(listeners, li)
|
||||||
}
|
}
|
||||||
|
|
||||||
if cfg.Options.MetricsAddr != "" {
|
if shouldStartMetricsListener(cfg.Options) {
|
||||||
li, err := b.buildMetricsListener(cfg)
|
li, err := b.buildMetricsListener(cfg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
@ -70,7 +48,7 @@ func (b *Builder) BuildListeners(
|
||||||
listeners = append(listeners, li)
|
listeners = append(listeners, li)
|
||||||
}
|
}
|
||||||
|
|
||||||
if cfg.Options.EnvoyAdminAddress != "" {
|
if shouldStartEnvoyAdminListener(cfg.Options) {
|
||||||
li, err := b.buildEnvoyAdminListener(ctx, cfg)
|
li, err := b.buildEnvoyAdminListener(ctx, cfg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
@ -87,615 +65,8 @@ func (b *Builder) BuildListeners(
|
||||||
return listeners, nil
|
return listeners, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func getAllCertificates(cfg *config.Config) ([]tls.Certificate, error) {
|
// newListener creates envoy listener with certain default values
|
||||||
allCertificates, err := cfg.AllCertificates()
|
func newListener(name string) *envoy_config_listener_v3.Listener {
|
||||||
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 {
|
|
||||||
return &envoy_config_listener_v3.Listener{
|
return &envoy_config_listener_v3.Listener{
|
||||||
Name: name,
|
Name: name,
|
||||||
PerConnectionBufferLimitBytes: wrapperspb.UInt32(listenerBufferLimit),
|
PerConnectionBufferLimitBytes: wrapperspb.UInt32(listenerBufferLimit),
|
||||||
|
@ -706,15 +77,3 @@ func newEnvoyListener(name string) *envoy_config_listener_v3.Listener {
|
||||||
EnableReusePort: wrapperspb.Bool(runtime.GOOS == "linux"),
|
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)
|
|
||||||
}
|
|
||||||
|
|
|
@ -12,10 +12,7 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
func (b *Builder) buildEnvoyAdminListener(_ context.Context, cfg *config.Config) (*envoy_config_listener_v3.Listener, error) {
|
func (b *Builder) buildEnvoyAdminListener(_ context.Context, cfg *config.Config) (*envoy_config_listener_v3.Listener, error) {
|
||||||
filter, err := b.buildEnvoyAdminHTTPConnectionManagerFilter()
|
filter := b.buildEnvoyAdminHTTPConnectionManagerFilter()
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
filterChain := &envoy_config_listener_v3.FilterChain{
|
filterChain := &envoy_config_listener_v3.FilterChain{
|
||||||
Filters: []*envoy_config_listener_v3.Filter{
|
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)
|
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.Address = addr
|
||||||
li.FilterChains = []*envoy_config_listener_v3.FilterChain{filterChain}
|
li.FilterChains = []*envoy_config_listener_v3.FilterChain{filterChain}
|
||||||
return li, nil
|
return li, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *Builder) buildEnvoyAdminHTTPConnectionManagerFilter() (*envoy_config_listener_v3.Filter, error) {
|
func (b *Builder) buildEnvoyAdminHTTPConnectionManagerFilter() *envoy_config_listener_v3.Filter {
|
||||||
rc, err := b.buildRouteConfiguration("envoy-admin", []*envoy_config_route_v3.VirtualHost{{
|
rc := newRouteConfiguration("envoy-admin", []*envoy_config_route_v3.VirtualHost{{
|
||||||
Name: "envoy-admin",
|
Name: "envoy-admin",
|
||||||
Domains: []string{"*"},
|
Domains: []string{"*"},
|
||||||
Routes: []*envoy_config_route_v3.Route{
|
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{
|
return HTTPConnectionManagerFilter(&envoy_http_connection_manager.HttpConnectionManager{
|
||||||
CodecType: envoy_http_connection_manager.HttpConnectionManager_AUTO,
|
CodecType: envoy_http_connection_manager.HttpConnectionManager_AUTO,
|
||||||
|
@ -67,5 +61,9 @@ func (b *Builder) buildEnvoyAdminHTTPConnectionManagerFilter() (*envoy_config_li
|
||||||
HttpFilters: []*envoy_http_connection_manager.HttpFilter{
|
HttpFilters: []*envoy_http_connection_manager.HttpFilter{
|
||||||
HTTPRouterFilter(),
|
HTTPRouterFilter(),
|
||||||
},
|
},
|
||||||
}), nil
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func shouldStartEnvoyAdminListener(options *config.Options) bool {
|
||||||
|
return options.EnvoyAdminAddress != ""
|
||||||
}
|
}
|
||||||
|
|
114
config/envoyconfig/listeners_grpc.go
Normal file
114
config/envoyconfig/listeners_grpc.go
Normal file
|
@ -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)
|
||||||
|
}
|
216
config/envoyconfig/listeners_main.go
Normal file
216
config/envoyconfig/listeners_main.go
Normal file
|
@ -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)
|
||||||
|
}
|
39
config/envoyconfig/listeners_main_test.go
Normal file
39
config/envoyconfig/listeners_main_test.go
Normal file
|
@ -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)
|
||||||
|
})
|
||||||
|
}
|
139
config/envoyconfig/listeners_metrics.go
Normal file
139
config/envoyconfig/listeners_metrics.go
Normal file
|
@ -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 != ""
|
||||||
|
}
|
|
@ -4,21 +4,18 @@ import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"embed"
|
"embed"
|
||||||
"encoding/base64"
|
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"runtime"
|
"runtime"
|
||||||
"testing"
|
"testing"
|
||||||
"text/template"
|
"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/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
"github.com/pomerium/pomerium/config"
|
"github.com/pomerium/pomerium/config"
|
||||||
"github.com/pomerium/pomerium/config/envoyconfig/filemgr"
|
"github.com/pomerium/pomerium/config/envoyconfig/filemgr"
|
||||||
"github.com/pomerium/pomerium/internal/testutil"
|
"github.com/pomerium/pomerium/internal/testutil"
|
||||||
"github.com/pomerium/pomerium/pkg/cryptutil"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
@ -109,471 +106,3 @@ func Test_buildMainHTTPConnectionManagerFilter(t *testing.T) {
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
testutil.AssertProtoJSONEqual(t, testData(t, "main_http_connection_manager_filter.json", nil), filter)
|
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)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
|
@ -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)
|
return nil, fmt.Errorf("invalid outbound port %v: %w", cfg.OutboundPort, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
filter, err := b.buildOutboundHTTPConnectionManager()
|
filter := b.buildOutboundHTTPConnectionManager()
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("error building outbound http connection manager filter: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
li := newEnvoyListener("outbound-ingress")
|
li := newListener("outbound-ingress")
|
||||||
li.Address = &envoy_config_core_v3.Address{
|
li.Address = &envoy_config_core_v3.Address{
|
||||||
Address: &envoy_config_core_v3.Address_SocketAddress{
|
Address: &envoy_config_core_v3.Address_SocketAddress{
|
||||||
SocketAddress: &envoy_config_core_v3.SocketAddress{
|
SocketAddress: &envoy_config_core_v3.SocketAddress{
|
||||||
|
@ -43,11 +40,8 @@ func (b *Builder) buildOutboundListener(cfg *config.Config) (*envoy_config_liste
|
||||||
return li, nil
|
return li, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *Builder) buildOutboundHTTPConnectionManager() (*envoy_config_listener_v3.Filter, error) {
|
func (b *Builder) buildOutboundHTTPConnectionManager() *envoy_config_listener_v3.Filter {
|
||||||
rc, err := b.buildOutboundRouteConfiguration()
|
rc := b.buildOutboundRouteConfiguration()
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
tc := marshalAny(&envoy_http_connection_manager.HttpConnectionManager{
|
tc := marshalAny(&envoy_http_connection_manager.HttpConnectionManager{
|
||||||
CodecType: envoy_http_connection_manager.HttpConnectionManager_AUTO,
|
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{
|
ConfigType: &envoy_config_listener_v3.Filter_TypedConfig{
|
||||||
TypedConfig: tc,
|
TypedConfig: tc,
|
||||||
},
|
},
|
||||||
}, nil
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *Builder) buildOutboundRouteConfiguration() (*envoy_config_route_v3.RouteConfiguration, error) {
|
func (b *Builder) buildOutboundRouteConfiguration() *envoy_config_route_v3.RouteConfiguration {
|
||||||
return b.buildRouteConfiguration("grpc", []*envoy_config_route_v3.VirtualHost{{
|
return newRouteConfiguration("grpc", []*envoy_config_route_v3.VirtualHost{{
|
||||||
Name: "grpc",
|
Name: "grpc",
|
||||||
Domains: []string{"*"},
|
Domains: []string{"*"},
|
||||||
Routes: b.buildOutboundRoutes(),
|
Routes: b.buildOutboundRoutes(),
|
||||||
|
|
|
@ -1,12 +1,18 @@
|
||||||
package envoyconfig
|
package envoyconfig
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"cmp"
|
||||||
"context"
|
"context"
|
||||||
|
"net/url"
|
||||||
|
"strings"
|
||||||
|
|
||||||
envoy_config_route_v3 "github.com/envoyproxy/go-control-plane/envoy/config/route/v3"
|
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/config"
|
||||||
"github.com/pomerium/pomerium/internal/telemetry/trace"
|
"github.com/pomerium/pomerium/internal/telemetry/trace"
|
||||||
|
"github.com/pomerium/pomerium/internal/urlutil"
|
||||||
)
|
)
|
||||||
|
|
||||||
// BuildRouteConfigurations builds the route configurations for the RDS service.
|
// BuildRouteConfigurations builds the route configurations for the RDS service.
|
||||||
|
@ -96,10 +102,61 @@ func (b *Builder) buildMainRouteConfiguration(
|
||||||
|
|
||||||
virtualHosts = append(virtualHosts, vh)
|
virtualHosts = append(virtualHosts, vh)
|
||||||
|
|
||||||
rc, err := b.buildRouteConfiguration("main", virtualHosts)
|
rc := newRouteConfiguration("main", virtualHosts)
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return rc, nil
|
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
|
||||||
|
}
|
||||||
|
|
|
@ -2,10 +2,12 @@ package envoyconfig
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"encoding/base64"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
"google.golang.org/protobuf/encoding/protojson"
|
"google.golang.org/protobuf/encoding/protojson"
|
||||||
|
|
||||||
"github.com/pomerium/pomerium/config"
|
"github.com/pomerium/pomerium/config"
|
||||||
|
@ -159,3 +161,118 @@ func TestBuilder_buildMainRouteConfiguration(t *testing.T) {
|
||||||
|
|
||||||
}`, routeConfiguration)
|
}`, 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)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1,17 +1,26 @@
|
||||||
package envoyconfig
|
package envoyconfig
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"crypto/x509"
|
"crypto/x509"
|
||||||
"encoding/asn1"
|
"encoding/asn1"
|
||||||
|
"encoding/base64"
|
||||||
|
"encoding/pem"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/netip"
|
"net/netip"
|
||||||
"net/url"
|
"net/url"
|
||||||
"regexp"
|
"regexp"
|
||||||
"strings"
|
"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_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"
|
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 (
|
var (
|
||||||
|
@ -121,6 +130,103 @@ func (b *Builder) buildSubjectNameIndication(
|
||||||
return sni
|
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.
|
// validateCertificate validates that a certificate can be used with Envoy's TLS stack.
|
||||||
func validateCertificate(cert *tls.Certificate) error {
|
func validateCertificate(cert *tls.Certificate) error {
|
||||||
if len(cert.Certificate) == 0 {
|
if len(cert.Certificate) == 0 {
|
||||||
|
@ -149,3 +255,95 @@ func hasMustStaple(cert *x509.Certificate) bool {
|
||||||
}
|
}
|
||||||
return false
|
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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1,14 +1,20 @@
|
||||||
package envoyconfig
|
package envoyconfig
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"crypto/x509"
|
"crypto/x509"
|
||||||
"crypto/x509/pkix"
|
"crypto/x509/pkix"
|
||||||
|
"encoding/base64"
|
||||||
"net/url"
|
"net/url"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"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/internal/testutil"
|
||||||
"github.com/pomerium/pomerium/pkg/cryptutil"
|
"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")
|
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)
|
||||||
|
}
|
||||||
|
|
Loading…
Add table
Reference in a new issue