traefik/pkg/middlewares/passtlsclientcert/pass_tls_client_cert.go

376 lines
10 KiB
Go
Raw Normal View History

2018-11-14 09:18:03 +00:00
package passtlsclientcert
import (
"context"
"crypto/x509"
"crypto/x509/pkix"
"encoding/pem"
"fmt"
2019-03-04 15:40:05 +00:00
"io"
2018-11-14 09:18:03 +00:00
"net/http"
"net/url"
"strings"
2022-11-21 17:36:05 +00:00
"github.com/rs/zerolog/log"
2023-02-03 14:24:05 +00:00
"github.com/traefik/traefik/v3/pkg/config/dynamic"
"github.com/traefik/traefik/v3/pkg/middlewares"
2024-01-08 08:10:06 +00:00
"go.opentelemetry.io/otel/trace"
2018-11-14 09:18:03 +00:00
)
const typeName = "PassClientTLSCert"
2018-11-14 09:18:03 +00:00
const (
xForwardedTLSClientCert = "X-Forwarded-Tls-Client-Cert"
xForwardedTLSClientCertInfo = "X-Forwarded-Tls-Client-Cert-Info"
)
const (
certSeparator = ","
fieldSeparator = ";"
subFieldSeparator = ","
2018-11-14 09:18:03 +00:00
)
var attributeTypeNames = map[string]string{
"0.9.2342.19200300.100.1.25": "DC", // Domain component OID - RFC 2247
}
// IssuerDistinguishedNameOptions is a struct for specifying the configuration
// for the distinguished name info of the issuer. This information is defined in
// RFC3739, section 3.1.1.
type IssuerDistinguishedNameOptions struct {
CommonName bool
CountryName bool
DomainComponent bool
LocalityName bool
OrganizationName bool
SerialNumber bool
StateOrProvinceName bool
}
2021-11-08 21:41:43 +00:00
func newIssuerDistinguishedNameOptions(info *dynamic.TLSClientCertificateIssuerDNInfo) *IssuerDistinguishedNameOptions {
if info == nil {
return nil
}
return &IssuerDistinguishedNameOptions{
CommonName: info.CommonName,
CountryName: info.Country,
DomainComponent: info.DomainComponent,
LocalityName: info.Locality,
OrganizationName: info.Organization,
SerialNumber: info.SerialNumber,
StateOrProvinceName: info.Province,
}
}
// SubjectDistinguishedNameOptions is a struct for specifying the configuration
// for the distinguished name info of the subject. This information is defined
// in RFC3739, section 3.1.2.
type SubjectDistinguishedNameOptions struct {
CommonName bool
CountryName bool
DomainComponent bool
LocalityName bool
OrganizationName bool
OrganizationalUnitName bool
SerialNumber bool
StateOrProvinceName bool
}
2021-11-08 21:41:43 +00:00
func newSubjectDistinguishedNameOptions(info *dynamic.TLSClientCertificateSubjectDNInfo) *SubjectDistinguishedNameOptions {
if info == nil {
return nil
}
return &SubjectDistinguishedNameOptions{
CommonName: info.CommonName,
CountryName: info.Country,
DomainComponent: info.DomainComponent,
LocalityName: info.Locality,
OrganizationName: info.Organization,
OrganizationalUnitName: info.OrganizationalUnit,
SerialNumber: info.SerialNumber,
StateOrProvinceName: info.Province,
}
}
// tlsClientCertificateInfo is a struct for specifying the configuration for the passTLSClientCert middleware.
type tlsClientCertificateInfo struct {
notAfter bool
notBefore bool
sans bool
subject *SubjectDistinguishedNameOptions
issuer *IssuerDistinguishedNameOptions
serialNumber bool
2018-11-14 09:18:03 +00:00
}
func newTLSClientCertificateInfo(info *dynamic.TLSClientCertificateInfo) *tlsClientCertificateInfo {
if info == nil {
2018-11-14 09:18:03 +00:00
return nil
}
return &tlsClientCertificateInfo{
issuer: newIssuerDistinguishedNameOptions(info.Issuer),
notAfter: info.NotAfter,
notBefore: info.NotBefore,
subject: newSubjectDistinguishedNameOptions(info.Subject),
serialNumber: info.SerialNumber,
sans: info.Sans,
2018-11-14 09:18:03 +00:00
}
}
// passTLSClientCert is a middleware that helps setup a few tls info features.
type passTLSClientCert struct {
next http.Handler
name string
pem bool // pass the sanitized pem to the backend in a specific header
info *tlsClientCertificateInfo // pass selected information from the client certificate
}
// New constructs a new PassTLSClientCert instance from supplied frontend header struct.
func New(ctx context.Context, next http.Handler, config dynamic.PassTLSClientCert, name string) (http.Handler, error) {
2022-11-21 17:36:05 +00:00
middlewares.GetLogger(ctx, name, typeName).Debug().Msg("Creating middleware")
return &passTLSClientCert{
next: next,
name: name,
pem: config.PEM,
info: newTLSClientCertificateInfo(config.Info),
}, nil
}
2024-01-08 08:10:06 +00:00
func (p *passTLSClientCert) GetTracingInformation() (string, string, trace.SpanKind) {
return p.name, typeName, trace.SpanKindInternal
2018-11-14 09:18:03 +00:00
}
func (p *passTLSClientCert) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
2022-11-21 17:36:05 +00:00
logger := middlewares.GetLogger(req.Context(), p.name, typeName)
ctx := logger.WithContext(req.Context())
if p.pem {
if req.TLS != nil && len(req.TLS.PeerCertificates) > 0 {
req.Header.Set(xForwardedTLSClientCert, getCertificates(ctx, req.TLS.PeerCertificates))
} else {
2022-11-21 17:36:05 +00:00
logger.Warn().Msg("Tried to extract a certificate on a request without mutual TLS")
}
}
if p.info != nil {
if req.TLS != nil && len(req.TLS.PeerCertificates) > 0 {
headerContent := p.getCertInfo(ctx, req.TLS.PeerCertificates)
req.Header.Set(xForwardedTLSClientCertInfo, url.QueryEscape(headerContent))
} else {
2022-11-21 17:36:05 +00:00
logger.Warn().Msg("Tried to extract a certificate on a request without mutual TLS")
}
}
2019-09-13 17:28:04 +00:00
2018-11-14 09:18:03 +00:00
p.next.ServeHTTP(rw, req)
}
2019-09-13 17:28:04 +00:00
// getCertInfo Build a string with the wanted client certificates information
// - the `,` is used to separate certificates
// - the `;` is used to separate root fields
// - the value of root fields is always wrapped by double quote
2020-05-11 10:06:07 +00:00
// - if a field is empty, the field is ignored.
func (p *passTLSClientCert) getCertInfo(ctx context.Context, certs []*x509.Certificate) string {
var headerValues []string
for _, peerCert := range certs {
var values []string
if p.info != nil {
subject := getSubjectDNInfo(ctx, p.info.subject, &peerCert.Subject)
if subject != "" {
values = append(values, fmt.Sprintf(`Subject="%s"`, strings.TrimSuffix(subject, subFieldSeparator)))
}
issuer := getIssuerDNInfo(ctx, p.info.issuer, &peerCert.Issuer)
if issuer != "" {
values = append(values, fmt.Sprintf(`Issuer="%s"`, strings.TrimSuffix(issuer, subFieldSeparator)))
}
if p.info.serialNumber && peerCert.SerialNumber != nil {
sn := peerCert.SerialNumber.String()
if sn != "" {
values = append(values, fmt.Sprintf(`SerialNumber="%s"`, strings.TrimSuffix(sn, subFieldSeparator)))
}
}
if p.info.notBefore {
values = append(values, fmt.Sprintf(`NB="%d"`, uint64(peerCert.NotBefore.Unix())))
}
if p.info.notAfter {
values = append(values, fmt.Sprintf(`NA="%d"`, uint64(peerCert.NotAfter.Unix())))
}
if p.info.sans {
sans := getSANs(peerCert)
if len(sans) > 0 {
values = append(values, fmt.Sprintf(`SAN="%s"`, strings.Join(sans, subFieldSeparator)))
}
}
}
value := strings.Join(values, fieldSeparator)
headerValues = append(headerValues, value)
}
return strings.Join(headerValues, certSeparator)
}
func getIssuerDNInfo(ctx context.Context, options *IssuerDistinguishedNameOptions, cs *pkix.Name) string {
if options == nil {
return ""
}
content := &strings.Builder{}
// Manage non standard attributes
for _, name := range cs.Names {
// Domain Component - RFC 2247
if options.DomainComponent && attributeTypeNames[name.Type.String()] == "DC" {
content.WriteString(fmt.Sprintf("DC=%s%s", name.Value, subFieldSeparator))
}
}
if options.CountryName {
writeParts(ctx, content, cs.Country, "C")
}
if options.StateOrProvinceName {
writeParts(ctx, content, cs.Province, "ST")
}
if options.LocalityName {
writeParts(ctx, content, cs.Locality, "L")
}
if options.OrganizationName {
writeParts(ctx, content, cs.Organization, "O")
}
if options.SerialNumber {
writePart(ctx, content, cs.SerialNumber, "SN")
}
if options.CommonName {
writePart(ctx, content, cs.CommonName, "CN")
}
return content.String()
}
func getSubjectDNInfo(ctx context.Context, options *SubjectDistinguishedNameOptions, cs *pkix.Name) string {
if options == nil {
return ""
}
content := &strings.Builder{}
// Manage non standard attributes
for _, name := range cs.Names {
// Domain Component - RFC 2247
if options.DomainComponent && attributeTypeNames[name.Type.String()] == "DC" {
content.WriteString(fmt.Sprintf("DC=%s%s", name.Value, subFieldSeparator))
}
}
2018-11-14 09:18:03 +00:00
if options.CountryName {
2019-09-13 17:28:04 +00:00
writeParts(ctx, content, cs.Country, "C")
}
2018-11-14 09:18:03 +00:00
if options.StateOrProvinceName {
2019-09-13 17:28:04 +00:00
writeParts(ctx, content, cs.Province, "ST")
}
2018-11-14 09:18:03 +00:00
if options.LocalityName {
2019-09-13 17:28:04 +00:00
writeParts(ctx, content, cs.Locality, "L")
}
2018-11-14 09:18:03 +00:00
if options.OrganizationName {
2019-09-13 17:28:04 +00:00
writeParts(ctx, content, cs.Organization, "O")
}
2018-11-14 09:18:03 +00:00
if options.OrganizationalUnitName {
writeParts(ctx, content, cs.OrganizationalUnit, "OU")
}
if options.SerialNumber {
2019-09-13 17:28:04 +00:00
writePart(ctx, content, cs.SerialNumber, "SN")
}
2018-11-14 09:18:03 +00:00
if options.CommonName {
2019-09-13 17:28:04 +00:00
writePart(ctx, content, cs.CommonName, "CN")
}
2018-11-14 09:18:03 +00:00
return content.String()
}
2018-11-14 09:18:03 +00:00
2019-09-13 17:28:04 +00:00
func writeParts(ctx context.Context, content io.StringWriter, entries []string, prefix string) {
for _, entry := range entries {
2019-09-13 17:28:04 +00:00
writePart(ctx, content, entry, prefix)
2018-11-14 09:18:03 +00:00
}
}
2018-11-14 09:18:03 +00:00
2020-07-07 12:42:03 +00:00
func writePart(ctx context.Context, content io.StringWriter, entry, prefix string) {
if len(entry) > 0 {
_, err := content.WriteString(fmt.Sprintf("%s=%s%s", prefix, entry, subFieldSeparator))
2019-03-04 15:40:05 +00:00
if err != nil {
2022-11-21 17:36:05 +00:00
log.Ctx(ctx).Error().Err(err).Send()
2019-03-04 15:40:05 +00:00
}
}
2018-11-14 09:18:03 +00:00
}
// sanitize As we pass the raw certificates, remove the useless data and make it http request compliant.
func sanitize(cert []byte) string {
return strings.NewReplacer(
"-----BEGIN CERTIFICATE-----", "",
"-----END CERTIFICATE-----", "",
"\n", "",
).Replace(string(cert))
2018-11-14 09:18:03 +00:00
}
// getCertificates Build a string with the client certificates.
func getCertificates(ctx context.Context, certs []*x509.Certificate) string {
var headerValues []string
2019-09-13 17:28:04 +00:00
for _, peerCert := range certs {
headerValues = append(headerValues, extractCertificate(ctx, peerCert))
2018-11-14 09:18:03 +00:00
}
return strings.Join(headerValues, certSeparator)
2018-11-14 09:18:03 +00:00
}
// extractCertificate extract the certificate from the request.
2019-09-13 17:28:04 +00:00
func extractCertificate(ctx context.Context, cert *x509.Certificate) string {
certPEM := pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: cert.Raw})
2018-11-14 09:18:03 +00:00
if certPEM == nil {
2022-11-21 17:36:05 +00:00
log.Ctx(ctx).Error().Msg("Cannot extract the certificate content")
2018-11-14 09:18:03 +00:00
return ""
}
return sanitize(certPEM)
2018-11-14 09:18:03 +00:00
}
// getSANs get the Subject Alternate Name values.
func getSANs(cert *x509.Certificate) []string {
if cert == nil {
return nil
2018-11-14 09:18:03 +00:00
}
var sans []string
2019-02-05 16:10:03 +00:00
sans = append(sans, cert.DNSNames...)
sans = append(sans, cert.EmailAddresses...)
2018-11-14 09:18:03 +00:00
for _, ip := range cert.IPAddresses {
sans = append(sans, ip.String())
2018-11-14 09:18:03 +00:00
}
for _, uri := range cert.URIs {
sans = append(sans, uri.String())
2018-11-14 09:18:03 +00:00
}
return sans
2018-11-14 09:18:03 +00:00
}