mirror of
https://github.com/pomerium/pomerium.git
synced 2025-06-16 01:32:56 +02:00
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.
353 lines
11 KiB
Go
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,
|
|
}
|
|
}
|