pomerium/config/envoyconfig/tls.go
Kenneth Jenkins 04585af9ef
config: generate fallback cert only as last resort (#5250)
Currently Pomerium will always generate a wildcard certificate for use 
as a fallback certificate.

If any other certificate is configured, this fallback certificate will 
not normally be presented, except in the case of a TLS connection where 
the client does not include the Server Name Indication (SNI) extension.
All modern browsers support SNI, so in practice this certificate should
never be presented to end users.

However, some network scanning tools will probe connections by IP 
addresses (without SNI), and so this fallback certificate may be
presented. The presence of this certificate may be flagged as a problem
in some automated vulnerability scans.

Let's avoid generating this fallback certificate if Pomerium has any 
other certificate configured (unless specifically requested by the Auto
TLS option). This should prevent false positive reports from these
particular vulnerability scans.
2024-12-19 09:46:59 -08:00

353 lines
11 KiB
Go

package envoyconfig
import (
"bytes"
"context"
"crypto/tls"
"crypto/x509"
"encoding/asn1"
"encoding/base64"
"encoding/pem"
"fmt"
"net/netip"
"net/url"
"regexp"
"strings"
envoy_config_core_v3 "github.com/envoyproxy/go-control-plane/envoy/config/core/v3"
envoy_extensions_transport_sockets_tls_v3 "github.com/envoyproxy/go-control-plane/envoy/extensions/transport_sockets/tls/v3"
envoy_type_matcher_v3 "github.com/envoyproxy/go-control-plane/envoy/type/matcher/v3"
"google.golang.org/protobuf/types/known/wrapperspb"
"github.com/pomerium/pomerium/config"
"github.com/pomerium/pomerium/internal/log"
)
var (
tlsDownstreamParams = &envoy_extensions_transport_sockets_tls_v3.TlsParameters{
CipherSuites: []string{
"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: envoy_extensions_transport_sockets_tls_v3.TlsParameters_TLSv1_2,
TlsMaximumProtocolVersion: envoy_extensions_transport_sockets_tls_v3.TlsParameters_TLSv1_3,
}
tlsUpstreamParams = &envoy_extensions_transport_sockets_tls_v3.TlsParameters{
CipherSuites: []string{
"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",
"ECDHE-ECDSA-AES128-SHA",
"ECDHE-RSA-AES128-SHA",
"AES128-GCM-SHA256",
"AES128-SHA",
"ECDHE-ECDSA-AES256-SHA",
"ECDHE-RSA-AES256-SHA",
"AES256-GCM-SHA384",
"AES256-SHA",
},
EcdhCurves: []string{
"X25519",
"P-256",
"P-384",
"P-521",
},
TlsMinimumProtocolVersion: envoy_extensions_transport_sockets_tls_v3.TlsParameters_TLSv1_2,
TlsMaximumProtocolVersion: envoy_extensions_transport_sockets_tls_v3.TlsParameters_TLSv1_3,
}
)
var oidMustStaple = asn1.ObjectIdentifier{1, 3, 6, 1, 5, 5, 7, 1, 24}
func (b *Builder) buildSubjectAltNameMatcher(
dst *url.URL,
overrideName string,
) *envoy_extensions_transport_sockets_tls_v3.SubjectAltNameMatcher {
sni := dst.Hostname()
if overrideName != "" {
sni = overrideName
}
if ip, err := netip.ParseAddr(sni); err == nil {
// Strip off any IPv6 zone.
if ip.Zone() != "" {
ip = ip.WithZone("")
}
return &envoy_extensions_transport_sockets_tls_v3.SubjectAltNameMatcher{
SanType: envoy_extensions_transport_sockets_tls_v3.SubjectAltNameMatcher_IP_ADDRESS,
Matcher: &envoy_type_matcher_v3.StringMatcher{
MatchPattern: &envoy_type_matcher_v3.StringMatcher_Exact{
Exact: ip.String(),
},
},
}
}
if strings.Contains(sni, "*") {
pattern := regexp.QuoteMeta(sni)
pattern = strings.Replace(pattern, "\\*", ".*", -1)
return &envoy_extensions_transport_sockets_tls_v3.SubjectAltNameMatcher{
SanType: envoy_extensions_transport_sockets_tls_v3.SubjectAltNameMatcher_DNS,
Matcher: &envoy_type_matcher_v3.StringMatcher{
MatchPattern: &envoy_type_matcher_v3.StringMatcher_SafeRegex{
SafeRegex: &envoy_type_matcher_v3.RegexMatcher{
EngineType: &envoy_type_matcher_v3.RegexMatcher_GoogleRe2{
GoogleRe2: &envoy_type_matcher_v3.RegexMatcher_GoogleRE2{},
},
Regex: pattern,
},
},
},
}
}
return &envoy_extensions_transport_sockets_tls_v3.SubjectAltNameMatcher{
SanType: envoy_extensions_transport_sockets_tls_v3.SubjectAltNameMatcher_DNS,
Matcher: &envoy_type_matcher_v3.StringMatcher{
MatchPattern: &envoy_type_matcher_v3.StringMatcher_Exact{
Exact: sni,
},
},
}
}
func (b *Builder) buildSubjectNameIndication(
dst *url.URL,
overrideName string,
) string {
sni := dst.Hostname()
if overrideName != "" {
sni = overrideName
}
sni = strings.Replace(sni, "*", "example", -1)
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)
}
// Generate a fallback certificate only if explicitly requested, or if there
// are no other available certificates.
if cfg.Options.DeriveInternalDomainCert != nil || len(allCertificates) == 0 {
wc, err := cfg.GenerateCatchAllCertificate()
if err != nil {
return nil, fmt.Errorf("error generating wildcard certificate: %w", err)
}
allCertificates = append(allCertificates, *wc)
}
return allCertificates, nil
}
// validateCertificate validates that a certificate can be used with Envoy's TLS stack.
func validateCertificate(cert *tls.Certificate) error {
if len(cert.Certificate) == 0 {
return nil
}
// parse the x509 certificate because leaf isn't always filled in
x509cert, err := x509.ParseCertificate(cert.Certificate[0])
if err != nil {
return err
}
// check to make sure that if we require an OCSP staple that its available.
if len(cert.OCSPStaple) == 0 && hasMustStaple(x509cert) {
return fmt.Errorf("certificate requires OCSP stapling but has no OCSP staple response")
}
return nil
}
func hasMustStaple(cert *x509.Certificate) bool {
for _, ext := range cert.Extensions {
if ext.Id.Equal(oidMustStaple) {
return true
}
}
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,
}
}