mirror of
https://github.com/pomerium/pomerium.git
synced 2025-04-28 18:06:34 +02:00
This also replaces instances where we manually write "return ctx.Err()" with "return context.Cause(ctx)" which is functionally identical, but will also correctly propagate cause errors if present.
273 lines
8.3 KiB
Go
273 lines
8.3 KiB
Go
// Package envoyconfig contains a Builder for building Envoy configuration from Pomerium configuration.
|
|
package envoyconfig
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"crypto/tls"
|
|
"crypto/x509"
|
|
"encoding/pem"
|
|
"errors"
|
|
"fmt"
|
|
"net"
|
|
"net/netip"
|
|
"net/url"
|
|
"os"
|
|
"strconv"
|
|
"sync"
|
|
"time"
|
|
|
|
envoy_config_accesslog_v3 "github.com/envoyproxy/go-control-plane/envoy/config/accesslog/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_extensions_access_loggers_grpc_v3 "github.com/envoyproxy/go-control-plane/envoy/extensions/access_loggers/grpc/v3"
|
|
envoy_extensions_transport_sockets_tls_v3 "github.com/envoyproxy/go-control-plane/envoy/extensions/transport_sockets/tls/v3"
|
|
"github.com/martinlindhe/base36"
|
|
"golang.org/x/net/nettest"
|
|
"google.golang.org/protobuf/proto"
|
|
"google.golang.org/protobuf/types/known/anypb"
|
|
"google.golang.org/protobuf/types/known/durationpb"
|
|
"google.golang.org/protobuf/types/known/wrapperspb"
|
|
|
|
"github.com/pomerium/pomerium/config"
|
|
"github.com/pomerium/pomerium/internal/fileutil"
|
|
"github.com/pomerium/pomerium/internal/httputil"
|
|
"github.com/pomerium/pomerium/internal/log"
|
|
"github.com/pomerium/pomerium/pkg/cryptutil"
|
|
)
|
|
|
|
var (
|
|
errNoEndpoints = errors.New("cluster must have endpoints")
|
|
defaultConnectionTimeout = durationpb.New(time.Second * 10)
|
|
)
|
|
|
|
// An Endpoint is a URL with its corresponding Transport Socket.
|
|
type Endpoint struct {
|
|
url url.URL
|
|
transportSocket *envoy_config_core_v3.TransportSocket
|
|
loadBalancerWeight *wrapperspb.UInt32Value
|
|
}
|
|
|
|
// NewEndpoint creates a new Endpoint.
|
|
func NewEndpoint(u *url.URL, ts *envoy_config_core_v3.TransportSocket, weight uint32) Endpoint {
|
|
var w *wrapperspb.UInt32Value
|
|
if weight > 0 {
|
|
w = &wrapperspb.UInt32Value{Value: weight}
|
|
}
|
|
return Endpoint{url: *u, transportSocket: ts, loadBalancerWeight: w}
|
|
}
|
|
|
|
// TransportSocketName return the name for this endpoint.
|
|
func (e Endpoint) TransportSocketName() string {
|
|
if e.transportSocket == nil {
|
|
return ""
|
|
}
|
|
h := cryptutil.HashProto(e.transportSocket)
|
|
return "ts-" + base36.EncodeBytes(h)
|
|
}
|
|
|
|
// newDefaultEnvoyClusterConfig creates envoy cluster with certain default values
|
|
func newDefaultEnvoyClusterConfig() *envoy_config_cluster_v3.Cluster {
|
|
return &envoy_config_cluster_v3.Cluster{
|
|
ConnectTimeout: defaultConnectionTimeout,
|
|
RespectDnsTtl: true,
|
|
DnsLookupFamily: envoy_config_cluster_v3.Cluster_V4_PREFERRED,
|
|
PerConnectionBufferLimitBytes: wrapperspb.UInt32(connectionBufferLimit),
|
|
}
|
|
}
|
|
|
|
func buildAccessLogs(options *config.Options) []*envoy_config_accesslog_v3.AccessLog {
|
|
lvl := options.ProxyLogLevel
|
|
if lvl == "" {
|
|
lvl = options.LogLevel
|
|
}
|
|
if lvl == "" {
|
|
lvl = config.LogLevelDebug
|
|
}
|
|
|
|
switch lvl {
|
|
case config.LogLevelTrace, config.LogLevelDebug, config.LogLevelInfo:
|
|
default:
|
|
// don't log access requests for levels > info
|
|
return nil
|
|
}
|
|
|
|
var additionalRequestHeaders []string
|
|
for _, field := range options.AccessLogFields {
|
|
if headerName, ok := log.GetHeaderField(field); ok {
|
|
additionalRequestHeaders = append(additionalRequestHeaders, httputil.CanonicalHeaderKey(headerName))
|
|
}
|
|
}
|
|
|
|
tc := marshalAny(&envoy_extensions_access_loggers_grpc_v3.HttpGrpcAccessLogConfig{
|
|
CommonConfig: &envoy_extensions_access_loggers_grpc_v3.CommonGrpcAccessLogConfig{
|
|
LogName: "ingress-http",
|
|
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,
|
|
},
|
|
AdditionalRequestHeadersToLog: additionalRequestHeaders,
|
|
})
|
|
return []*envoy_config_accesslog_v3.AccessLog{{
|
|
Name: "envoy.access_loggers.http_grpc",
|
|
ConfigType: &envoy_config_accesslog_v3.AccessLog_TypedConfig{TypedConfig: tc},
|
|
}}
|
|
}
|
|
|
|
func buildAddress(hostport string, defaultPort uint32) *envoy_config_core_v3.Address {
|
|
host, strport, err := net.SplitHostPort(hostport)
|
|
if err != nil {
|
|
host = hostport
|
|
strport = fmt.Sprint(defaultPort)
|
|
}
|
|
port := defaultPort
|
|
if p, err := strconv.ParseUint(strport, 10, 32); err == nil {
|
|
port = uint32(p)
|
|
}
|
|
if host == "" {
|
|
if nettest.SupportsIPv6() {
|
|
host = "::"
|
|
} else {
|
|
host = "0.0.0.0"
|
|
}
|
|
}
|
|
|
|
is4in6 := false
|
|
if addr, err := netip.ParseAddr(host); err == nil {
|
|
is4in6 = addr.Is4In6()
|
|
}
|
|
|
|
return &envoy_config_core_v3.Address{
|
|
Address: &envoy_config_core_v3.Address_SocketAddress{SocketAddress: &envoy_config_core_v3.SocketAddress{
|
|
Address: host,
|
|
PortSpecifier: &envoy_config_core_v3.SocketAddress_PortValue{PortValue: port},
|
|
Ipv4Compat: host == "::" || is4in6,
|
|
}},
|
|
}
|
|
}
|
|
|
|
func (b *Builder) envoyTLSCertificateFromGoTLSCertificate(
|
|
ctx context.Context,
|
|
cert *tls.Certificate,
|
|
) *envoy_extensions_transport_sockets_tls_v3.TlsCertificate {
|
|
envoyCert := &envoy_extensions_transport_sockets_tls_v3.TlsCertificate{}
|
|
var chain bytes.Buffer
|
|
for _, cbs := range cert.Certificate {
|
|
_ = pem.Encode(&chain, &pem.Block{
|
|
Type: "CERTIFICATE",
|
|
Bytes: cbs,
|
|
})
|
|
}
|
|
envoyCert.CertificateChain = b.filemgr.BytesDataSource("tls-crt.pem", chain.Bytes())
|
|
if cert.OCSPStaple != nil {
|
|
envoyCert.OcspStaple = b.filemgr.BytesDataSource("ocsp-staple", cert.OCSPStaple)
|
|
}
|
|
if bs, err := x509.MarshalPKCS8PrivateKey(cert.PrivateKey); err == nil {
|
|
envoyCert.PrivateKey = b.filemgr.BytesDataSource("tls-key.pem", pem.EncodeToMemory(
|
|
&pem.Block{
|
|
Type: "PRIVATE KEY",
|
|
Bytes: bs,
|
|
},
|
|
))
|
|
} else {
|
|
log.Ctx(ctx).Error().Err(err).Msg("failed to marshal private key for tls config")
|
|
}
|
|
for _, scts := range cert.SignedCertificateTimestamps {
|
|
envoyCert.SignedCertificateTimestamp = append(envoyCert.SignedCertificateTimestamp,
|
|
b.filemgr.BytesDataSource("signed-certificate-timestamp", scts))
|
|
}
|
|
return envoyCert
|
|
}
|
|
|
|
var rootCABundle struct {
|
|
sync.Once
|
|
value string
|
|
}
|
|
|
|
func getRootCertificateAuthority(ctx context.Context) (string, error) {
|
|
rootCABundle.Do(func() {
|
|
// from https://www.envoyproxy.io/docs/envoy/latest/intro/arch_overview/security/ssl#arch-overview-ssl-enabling-verification
|
|
knownRootLocations := []string{
|
|
"/etc/ssl/certs/ca-certificates.crt",
|
|
"/etc/pki/ca-trust/extracted/pem/tls-ca-bundle.pem",
|
|
"/etc/pki/tls/certs/ca-bundle.crt",
|
|
"/etc/ssl/ca-bundle.pem",
|
|
"/usr/local/etc/ssl/cert.pem",
|
|
"/etc/ssl/cert.pem",
|
|
}
|
|
for _, path := range knownRootLocations {
|
|
if _, err := os.Stat(path); err == nil {
|
|
rootCABundle.value = path
|
|
break
|
|
}
|
|
}
|
|
if rootCABundle.value == "" {
|
|
log.Ctx(ctx).Error().Strs("known-locations", knownRootLocations).
|
|
Msgf("no root certificates were found in any of the known locations")
|
|
} else {
|
|
log.Ctx(ctx).Info().Msgf("using %s as the system root certificate authority bundle", rootCABundle.value)
|
|
}
|
|
})
|
|
if rootCABundle.value == "" {
|
|
return "", fmt.Errorf("root certificates not found")
|
|
}
|
|
return rootCABundle.value, nil
|
|
}
|
|
|
|
func getCombinedCertificateAuthority(ctx context.Context, cfg *config.Config) ([]byte, error) {
|
|
rootFile, err := getRootCertificateAuthority(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var buf bytes.Buffer
|
|
if err := fileutil.CopyFileUpTo(&buf, rootFile, 5<<20); err != nil {
|
|
return nil, fmt.Errorf("error reading root certificates: %w", err)
|
|
}
|
|
buf.WriteRune('\n')
|
|
|
|
all, err := cfg.AllCertificateAuthoritiesPEM()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("get all CA: %w", err)
|
|
}
|
|
buf.Write(all)
|
|
|
|
return buf.Bytes(), nil
|
|
}
|
|
|
|
func marshalAny(msg proto.Message) *anypb.Any {
|
|
data := new(anypb.Any)
|
|
_ = anypb.MarshalFrom(data, msg, proto.MarshalOptions{
|
|
AllowPartial: true,
|
|
Deterministic: true,
|
|
})
|
|
return data
|
|
}
|
|
|
|
// parseAddress parses a string address into an envoy address.
|
|
func parseAddress(raw string) (*envoy_config_core_v3.Address, error) {
|
|
if host, portstr, err := net.SplitHostPort(raw); err == nil {
|
|
if host == "localhost" {
|
|
host = "127.0.0.1"
|
|
}
|
|
|
|
if port, err := strconv.ParseUint(portstr, 10, 32); err == nil {
|
|
return &envoy_config_core_v3.Address{
|
|
Address: &envoy_config_core_v3.Address_SocketAddress{
|
|
SocketAddress: &envoy_config_core_v3.SocketAddress{
|
|
Address: host,
|
|
PortSpecifier: &envoy_config_core_v3.SocketAddress_PortValue{
|
|
PortValue: uint32(port),
|
|
},
|
|
},
|
|
},
|
|
}, nil
|
|
}
|
|
}
|
|
return nil, fmt.Errorf("unknown address format: %s", raw)
|
|
}
|