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(
&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{
{
Name: "envoy.access_loggers.tcp_grpc",
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.AccessLog = listenerAccessLog() // XXX
if cfg.Options.DownstreamMTLS.Enforcement == config.MTLSEnforcementRejectConnection {
li.AccessLog = listenerAccessLog()
}
if cfg.Options.InsecureServer {
li.Address = buildAddress(cfg.Options.Addr, 80)

View file

@ -2,13 +2,11 @@ package controlplane
import (
"context"
"encoding/json"
"strings"
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"
"github.com/rs/zerolog"
"google.golang.org/protobuf/encoding/protojson"
"github.com/pomerium/pomerium/internal/log"
)
@ -43,18 +41,18 @@ func accessLogListener(
ctx context.Context, msg *envoy_service_accesslog_v3.StreamAccessLogsMessage,
) {
for _, entry := range msg.GetTcpLogs().GetLogEntry() {
e, _ := protojson.Marshal(entry)
log.Info(ctx).
Str("service", "envoy").
Interface("log", json.RawMessage(e)).
Msg("listener connect (TCP log)")
}
for _, entry := range msg.GetHttpLogs().GetLogEntry() {
e, _ := protojson.Marshal(entry)
log.Info(ctx).
Str("service", "envoy").
Interface("log", json.RawMessage(e)).
Msg("listener connect (HTTP log)")
failure := entry.GetCommonProperties().GetDownstreamTransportFailureReason()
if failure == "" {
continue
}
e := log.Info(ctx).Str("service", "envoy")
dict := zerolog.Dict()
populateCertEventDict(entry.GetCommonProperties().GetTlsProperties().GetPeerCertificateProperties(), dict)
e.Dict("client-certificate", dict)
e.Str("ip", entry.GetCommonProperties().GetDownstreamRemoteAddress().GetSocketAddress().GetAddress())
e.Str("tls-sni-hostname", entry.GetCommonProperties().GetTlsProperties().GetTlsSniHostname())
e.Str("downstream-transport-failure-reason", failure)
e.Msg("listener connection failure")
}
}
@ -121,7 +119,34 @@ func populateLogEvent(
return evt.Str(string(field), entry.GetCommonProperties().GetUpstreamCluster())
case log.AccessLogFieldUserAgent:
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:
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"
AccessLogFieldUpstreamCluster AccessLogField = "upstream-cluster"
AccessLogFieldUserAgent AccessLogField = "user-agent"
AccessLogFieldClientCertificate AccessLogField = "client-certificate"
)
var defaultAccessLogFields = []AccessLogField{
@ -64,6 +65,7 @@ var accessLogFieldLookup = map[AccessLogField]struct{}{
AccessLogFieldSize: {},
AccessLogFieldUpstreamCluster: {},
AccessLogFieldUserAgent: {},
AccessLogFieldClientCertificate: {},
}
// Validate returns an error if the access log field is invalid.