envoy: log mtls failures

This implements limited listener-based access logging for downstream
transport failures, only enabled when downstream_mtls.enforcement is
set to 'reject_connection'. Client certificate details and the error
message will be logged.

Additionally, the new key 'client-certificate' can be set in the
access_log_fields list in the configuration, which will add peer
certificate properties (issuer, subject, SANs) to the existing
per-request http logs.
This commit is contained in:
Joe Kralicky 2024-08-07 10:15:28 -04:00
parent d07cb616fd
commit 783b1127c7
No known key found for this signature in database
GPG key ID: 75C4875F34A9FB79
3 changed files with 44 additions and 20 deletions

View file

@ -139,15 +139,10 @@ func listenerAccessLog() []*envoy_config_accesslog_v3.AccessLog {
} }
tcp := marshalAny( tcp := marshalAny(
&envoy_extensions_access_loggers_grpc_v3.TcpGrpcAccessLogConfig{CommonConfig: cc}) &envoy_extensions_access_loggers_grpc_v3.TcpGrpcAccessLogConfig{CommonConfig: cc})
http := marshalAny(
&envoy_extensions_access_loggers_grpc_v3.HttpGrpcAccessLogConfig{CommonConfig: cc})
return []*envoy_config_accesslog_v3.AccessLog{ return []*envoy_config_accesslog_v3.AccessLog{
{ {
Name: "envoy.access_loggers.tcp_grpc", Name: "envoy.access_loggers.tcp_grpc",
ConfigType: &envoy_config_accesslog_v3.AccessLog_TypedConfig{TypedConfig: tcp}, ConfigType: &envoy_config_accesslog_v3.AccessLog_TypedConfig{TypedConfig: tcp},
}, {
Name: "envoy.access_loggers.http_grpc",
ConfigType: &envoy_config_accesslog_v3.AccessLog_TypedConfig{TypedConfig: http},
}, },
} }
} }
@ -162,7 +157,9 @@ func (b *Builder) buildMainListener(
li.ListenerFilters = append(li.ListenerFilters, ProxyProtocolFilter()) li.ListenerFilters = append(li.ListenerFilters, ProxyProtocolFilter())
} }
li.AccessLog = listenerAccessLog() // XXX if cfg.Options.DownstreamMTLS.Enforcement == config.MTLSEnforcementRejectConnection {
li.AccessLog = listenerAccessLog()
}
if cfg.Options.InsecureServer { if cfg.Options.InsecureServer {
li.Address = buildAddress(cfg.Options.Addr, 80) li.Address = buildAddress(cfg.Options.Addr, 80)

View file

@ -2,13 +2,11 @@ package controlplane
import ( import (
"context" "context"
"encoding/json"
"strings" "strings"
envoy_data_accesslog_v3 "github.com/envoyproxy/go-control-plane/envoy/data/accesslog/v3" envoy_data_accesslog_v3 "github.com/envoyproxy/go-control-plane/envoy/data/accesslog/v3"
envoy_service_accesslog_v3 "github.com/envoyproxy/go-control-plane/envoy/service/accesslog/v3" envoy_service_accesslog_v3 "github.com/envoyproxy/go-control-plane/envoy/service/accesslog/v3"
"github.com/rs/zerolog" "github.com/rs/zerolog"
"google.golang.org/protobuf/encoding/protojson"
"github.com/pomerium/pomerium/internal/log" "github.com/pomerium/pomerium/internal/log"
) )
@ -43,18 +41,18 @@ func accessLogListener(
ctx context.Context, msg *envoy_service_accesslog_v3.StreamAccessLogsMessage, ctx context.Context, msg *envoy_service_accesslog_v3.StreamAccessLogsMessage,
) { ) {
for _, entry := range msg.GetTcpLogs().GetLogEntry() { for _, entry := range msg.GetTcpLogs().GetLogEntry() {
e, _ := protojson.Marshal(entry) failure := entry.GetCommonProperties().GetDownstreamTransportFailureReason()
log.Info(ctx). if failure == "" {
Str("service", "envoy"). continue
Interface("log", json.RawMessage(e)). }
Msg("listener connect (TCP log)") e := log.Info(ctx).Str("service", "envoy")
} dict := zerolog.Dict()
for _, entry := range msg.GetHttpLogs().GetLogEntry() { populateCertEventDict(entry.GetCommonProperties().GetTlsProperties().GetPeerCertificateProperties(), dict)
e, _ := protojson.Marshal(entry) e.Dict("client-certificate", dict)
log.Info(ctx). e.Str("ip", entry.GetCommonProperties().GetDownstreamRemoteAddress().GetSocketAddress().GetAddress())
Str("service", "envoy"). e.Str("tls-sni-hostname", entry.GetCommonProperties().GetTlsProperties().GetTlsSniHostname())
Interface("log", json.RawMessage(e)). e.Str("downstream-transport-failure-reason", failure)
Msg("listener connect (HTTP log)") e.Msg("listener connection failure")
} }
} }
@ -121,7 +119,34 @@ func populateLogEvent(
return evt.Str(string(field), entry.GetCommonProperties().GetUpstreamCluster()) return evt.Str(string(field), entry.GetCommonProperties().GetUpstreamCluster())
case log.AccessLogFieldUserAgent: case log.AccessLogFieldUserAgent:
return evt.Str(string(field), entry.GetRequest().GetUserAgent()) return evt.Str(string(field), entry.GetRequest().GetUserAgent())
case log.AccessLogFieldClientCertificate:
dict := zerolog.Dict()
populateCertEventDict(entry.GetCommonProperties().GetTlsProperties().GetPeerCertificateProperties(), dict)
return evt.Dict(string(field), dict)
default: default:
return evt return evt
} }
} }
func populateCertEventDict(cert *envoy_data_accesslog_v3.TLSProperties_CertificateProperties, dict *zerolog.Event) {
if cert.Issuer != "" {
dict.Str("issuer", cert.Issuer)
}
if cert.Subject != "" {
dict.Str("subject", cert.Subject)
}
if len(cert.SubjectAltName) > 0 {
arr := zerolog.Arr()
for _, san := range cert.SubjectAltName {
// follow openssl GENERAL_NAME_print formatting
// envoy only provides dns and uri SANs at the moment
switch san := san.GetSan().(type) {
case *envoy_data_accesslog_v3.TLSProperties_CertificateProperties_SubjectAltName_Dns:
arr.Str("DNS:" + san.Dns)
case *envoy_data_accesslog_v3.TLSProperties_CertificateProperties_SubjectAltName_Uri:
arr.Str("URI:" + san.Uri)
}
}
dict.Array("subjectAltName", arr)
}
}

View file

@ -24,6 +24,7 @@ const (
AccessLogFieldSize AccessLogField = "size" AccessLogFieldSize AccessLogField = "size"
AccessLogFieldUpstreamCluster AccessLogField = "upstream-cluster" AccessLogFieldUpstreamCluster AccessLogField = "upstream-cluster"
AccessLogFieldUserAgent AccessLogField = "user-agent" AccessLogFieldUserAgent AccessLogField = "user-agent"
AccessLogFieldClientCertificate AccessLogField = "client-certificate"
) )
var defaultAccessLogFields = []AccessLogField{ var defaultAccessLogFields = []AccessLogField{
@ -64,6 +65,7 @@ var accessLogFieldLookup = map[AccessLogField]struct{}{
AccessLogFieldSize: {}, AccessLogFieldSize: {},
AccessLogFieldUpstreamCluster: {}, AccessLogFieldUpstreamCluster: {},
AccessLogFieldUserAgent: {}, AccessLogFieldUserAgent: {},
AccessLogFieldClientCertificate: {},
} }
// Validate returns an error if the access log field is invalid. // Validate returns an error if the access log field is invalid.