From 817ac8f256a3ccf89801072b957d7d9f28f8c2f3 Mon Sep 17 00:00:00 2001 From: Eric Date: Wed, 28 Jul 2021 16:42:09 +0100 Subject: [PATCH] Add organizationalUnit to passtlscert middleware --- .../middlewares/http/passtlsclientcert.md | 62 ++++--- .../dynamic-configuration/docker-labels.yml | 1 + .../reference/dynamic-configuration/file.toml | 1 + .../reference/dynamic-configuration/file.yaml | 1 + .../reference/dynamic-configuration/kv-ref.md | 1 + .../marathon-labels.json | 1 + .../traefik.containo.us_middlewares.yaml | 12 +- integration/fixtures/k8s/01-traefik-crd.yml | 12 +- pkg/anonymize/anonymize_config_test.go | 19 +-- .../testdata/anonymized-dynamic-config.json | 1 + pkg/config/dynamic/fixtures/sample.toml | 1 + pkg/config/dynamic/middlewares.go | 31 +++- pkg/config/dynamic/zz_generated.deepcopy.go | 28 +++- pkg/config/label/label_test.go | 40 ++--- .../passtlsclientcert/pass_tls_client_cert.go | 101 ++++++++++-- .../pass_tls_client_cert_test.go | 152 ++++++++++-------- pkg/provider/kv/kv_test.go | 20 +-- .../components/_commons/PanelMiddlewares.vue | 12 +- 18 files changed, 339 insertions(+), 157 deletions(-) diff --git a/docs/content/middlewares/http/passtlsclientcert.md b/docs/content/middlewares/http/passtlsclientcert.md index fa65553f6..89a802a13 100644 --- a/docs/content/middlewares/http/passtlsclientcert.md +++ b/docs/content/middlewares/http/passtlsclientcert.md @@ -76,6 +76,7 @@ http: - "traefik.http.middlewares.test-passtlsclientcert.passtlsclientcert.info.subject.domaincomponent=true" - "traefik.http.middlewares.test-passtlsclientcert.passtlsclientcert.info.subject.locality=true" - "traefik.http.middlewares.test-passtlsclientcert.passtlsclientcert.info.subject.organization=true" + - "traefik.http.middlewares.test-passtlsclientcert.passtlsclientcert.info.subject.organizationalunit=true" - "traefik.http.middlewares.test-passtlsclientcert.passtlsclientcert.info.subject.province=true" - "traefik.http.middlewares.test-passtlsclientcert.passtlsclientcert.info.subject.serialnumber=true" - "traefik.http.middlewares.test-passtlsclientcert.passtlsclientcert.info.issuer.commonname=true" @@ -104,6 +105,7 @@ http: province: true locality: true organization: true + organizationalUnit: true commonName: true serialNumber: true domainComponent: true @@ -127,6 +129,7 @@ http: - "traefik.http.middlewares.test-passtlsclientcert.passtlsclientcert.info.subject.domaincomponent=true" - "traefik.http.middlewares.test-passtlsclientcert.passtlsclientcert.info.subject.locality=true" - "traefik.http.middlewares.test-passtlsclientcert.passtlsclientcert.info.subject.organization=true" + - "traefik.http.middlewares.test-passtlsclientcert.passtlsclientcert.info.subject.organizationalunit=true" - "traefik.http.middlewares.test-passtlsclientcert.passtlsclientcert.info.subject.province=true" - "traefik.http.middlewares.test-passtlsclientcert.passtlsclientcert.info.subject.serialnumber=true" - "traefik.http.middlewares.test-passtlsclientcert.passtlsclientcert.info.issuer.commonname=true" @@ -148,6 +151,7 @@ http: "traefik.http.middlewares.test-passtlsclientcert.passtlsclientcert.info.subject.domaincomponent": "true", "traefik.http.middlewares.test-passtlsclientcert.passtlsclientcert.info.subject.locality": "true", "traefik.http.middlewares.test-passtlsclientcert.passtlsclientcert.info.subject.organization": "true", + "traefik.http.middlewares.test-passtlsclientcert.passtlsclientcert.info.subject.organizationalunit": "true", "traefik.http.middlewares.test-passtlsclientcert.passtlsclientcert.info.subject.province": "true", "traefik.http.middlewares.test-passtlsclientcert.passtlsclientcert.info.subject.serialnumber": "true", "traefik.http.middlewares.test-passtlsclientcert.passtlsclientcert.info.issuer.commonname": "true", @@ -171,6 +175,7 @@ http: - "traefik.http.middlewares.test-passtlsclientcert.passtlsclientcert.info.subject.domaincomponent=true" - "traefik.http.middlewares.test-passtlsclientcert.passtlsclientcert.info.subject.locality=true" - "traefik.http.middlewares.test-passtlsclientcert.passtlsclientcert.info.subject.organization=true" + - "traefik.http.middlewares.test-passtlsclientcert.passtlsclientcert.info.subject.organizationalunit=true" - "traefik.http.middlewares.test-passtlsclientcert.passtlsclientcert.info.subject.province=true" - "traefik.http.middlewares.test-passtlsclientcert.passtlsclientcert.info.subject.serialnumber=true" - "traefik.http.middlewares.test-passtlsclientcert.passtlsclientcert.info.issuer.commonname=true" @@ -197,6 +202,7 @@ http: province: true locality: true organization: true + organizationalUnit: true commonName: true serialNumber: true domainComponent: true @@ -223,6 +229,7 @@ http: province = true locality = true organization = true + organizationalUnit = true commonName = true serialNumber = true domainComponent = true @@ -247,7 +254,7 @@ PassTLSClientCert can add two headers to the request: !!! info - * The headers are filled with escaped string so it can be safely placed inside a URL query. + * Each header value is a string that has been escaped in order to be a valid URL query. * These options only work accordingly to the [MutualTLS configuration](../../https/tls.md#client-authentication-mtls). That is to say, only the certificates that match the `clientAuth.clientAuthType` policy are passed. @@ -412,15 +419,18 @@ In the example, it is the part between `-----BEGIN CERTIFICATE-----` and `-----E !!! warning "`X-Forwarded-Tls-Client-Cert` value could exceed the web server header size limit" The header size limit of web servers is commonly between 4kb and 8kb. - You could change the server configuration to allow bigger header or use the `info` option with the needed field(s). + If that turns out to be a problem, and if reconfiguring the server to allow larger headers is not an option, + one can alleviate the problem by selecting only the interesting parts of the cert, + through the use of the `info` options described below. (And by setting `pem` to false). ### `info` The `info` option selects the specific client certificate details you want to add to the `X-Forwarded-Tls-Client-Cert-Info` header. The value of the header is an escaped concatenation of all the selected certificate details. +But in the following, unless specified otherwise, all the header values examples are shown unescaped, for readability. -The following example shows an unescaped result that uses all the available fields: +The following example shows such a concatenation, when all the available fields are selected: ```text Subject="DC=org,DC=cheese,C=FR,C=US,ST=Cheese org state,ST=Cheese com state,L=TOULOUSE,L=LYON,O=Cheese,O=Cheese 2,CN=*.example.com";Issuer="DC=org,DC=cheese,C=FR,C=US,ST=Signing State,ST=Signing State 2,L=TOULOUSE,L=LYON,O=Cheese,O=Cheese 2,CN=Simple Signing CA 2";NB="1544094616";NA="1607166616";SAN="*.example.org,*.example.net,*.example.com,test@example.org,test@example.net,10.0.1.0,10.0.1.2" @@ -441,7 +451,7 @@ The data is taken from the following certificate part: Not After : Dec 5 11:10:16 2020 GMT ``` -The escaped `notAfter` info part is formatted as below: +And it is formatted as follows in the header: ```text NA="1607166616" @@ -458,7 +468,7 @@ Validity Not Before: Dec 6 11:10:16 2018 GMT ``` -The escaped `notBefore` info part is formatted as below: +And it is formatted as follows in the header: ```text NB="1544094616" @@ -475,7 +485,7 @@ The data is taken from the following certificate part: DNS:*.example.org, DNS:*.example.net, DNS:*.example.com, IP Address:10.0.1.0, IP Address:10.0.1.2, email:test@example.org, email:test@example.net ``` -The escape SANs info part is formatted as below: +And it is formatted as follows in the header: ```text SAN="*.example.org,*.example.net,*.example.com,test@example.org,test@example.net,10.0.1.0,10.0.1.2" @@ -501,7 +511,7 @@ Set the `info.subject.country` option to `true` to add the `country` information The data is taken from the subject part with the `C` key. -The escape country info in the subject part is formatted as below: +And it is formatted as follows in the header: ```text C=FR,C=US @@ -513,7 +523,7 @@ Set the `info.subject.province` option to `true` to add the `province` informati The data is taken from the subject part with the `ST` key. -The escape province info in the subject part is formatted as below: +And it is formatted as follows in the header: ```text ST=Cheese org state,ST=Cheese com state @@ -525,7 +535,7 @@ Set the `info.subject.locality` option to `true` to add the `locality` informati The data is taken from the subject part with the `L` key. -The escape locality info in the subject part is formatted as below: +And it is formatted as follows in the header: ```text L=TOULOUSE,L=LYON @@ -537,19 +547,31 @@ Set the `info.subject.organization` option to `true` to add the `organization` i The data is taken from the subject part with the `O` key. -The escape organization info in the subject part is formatted as below: +And it is formatted as follows in the header: ```text O=Cheese,O=Cheese 2 ``` +##### `info.subject.organizationalUnit` + +Set the `info.subject.organizationalUnit` option to `true` to add the `organizationalUnit` information into the subject. + +The data is taken from the subject part with the `OU` key. + +And it is formatted as follows in the header: + +```text +OU=Cheese Section,OU=Cheese Section 2 +``` + ##### `info.subject.commonName` Set the `info.subject.commonName` option to `true` to add the `commonName` information into the subject. The data is taken from the subject part with the `CN` key. -The escape common name info in the subject part is formatted as below: +And it is formatted as follows in the header: ```text CN=*.example.com @@ -561,7 +583,7 @@ Set the `info.subject.serialNumber` option to `true` to add the `serialNumber` i The data is taken from the subject part with the `SN` key. -The escape serial number info in the subject part is formatted as below: +And it is formatted as follows in the header: ```text SN=1234567890 @@ -573,7 +595,7 @@ Set the `info.subject.domainComponent` option to `true` to add the `domainCompon The data is taken from the subject part with the `DC` key. -The escape domain component info in the subject part is formatted as below: +And it is formatted as follows in the header: ```text DC=org,DC=cheese @@ -595,7 +617,7 @@ Set the `info.issuer.country` option to `true` to add the `country` information The data is taken from the issuer part with the `C` key. -The escape country info in the issuer part is formatted as below: +And it is formatted as follows in the header: ```text C=FR,C=US @@ -607,7 +629,7 @@ Set the `info.issuer.province` option to `true` to add the `province` informatio The data is taken from the issuer part with the `ST` key. -The escape province info in the issuer part is formatted as below: +And it is formatted as follows in the header: ```text ST=Signing State,ST=Signing State 2 @@ -619,7 +641,7 @@ Set the `info.issuer.locality` option to `true` to add the `locality` informatio The data is taken from the issuer part with the `L` key. -The escape locality info in the issuer part is formatted as below: +And it is formatted as follows in the header: ```text L=TOULOUSE,L=LYON @@ -631,7 +653,7 @@ Set the `info.issuer.organization` option to `true` to add the `organization` in The data is taken from the issuer part with the `O` key. -The escape organization info in the issuer part is formatted as below: +And it is formatted as follows in the header: ```text O=Cheese,O=Cheese 2 @@ -643,7 +665,7 @@ Set the `info.issuer.commonName` option to `true` to add the `commonName` inform The data is taken from the issuer part with the `CN` key. -The escape common name info in the issuer part is formatted as below: +And it is formatted as follows in the header: ```text CN=Simple Signing CA 2 @@ -655,7 +677,7 @@ Set the `info.issuer.serialNumber` option to `true` to add the `serialNumber` in The data is taken from the issuer part with the `SN` key. -The escape serial number info in the issuer part is formatted as below: +And it is formatted as follows in the header: ```text SN=1234567890 @@ -667,7 +689,7 @@ Set the `info.issuer.domainComponent` option to `true` to add the `domainCompone The data is taken from the issuer part with the `DC` key. -The escape domain component info in the issuer part is formatted as below: +And it is formatted as follows in the header: ```text DC=org,DC=cheese diff --git a/docs/content/reference/dynamic-configuration/docker-labels.yml b/docs/content/reference/dynamic-configuration/docker-labels.yml index cd4f59243..82574efdd 100644 --- a/docs/content/reference/dynamic-configuration/docker-labels.yml +++ b/docs/content/reference/dynamic-configuration/docker-labels.yml @@ -90,6 +90,7 @@ - "traefik.http.middlewares.middleware13.passtlsclientcert.info.subject.domaincomponent=true" - "traefik.http.middlewares.middleware13.passtlsclientcert.info.subject.locality=true" - "traefik.http.middlewares.middleware13.passtlsclientcert.info.subject.organization=true" +- "traefik.http.middlewares.middleware13.passtlsclientcert.info.subject.organizationalunit=true" - "traefik.http.middlewares.middleware13.passtlsclientcert.info.subject.province=true" - "traefik.http.middlewares.middleware13.passtlsclientcert.info.subject.serialnumber=true" - "traefik.http.middlewares.middleware13.passtlsclientcert.pem=true" diff --git a/docs/content/reference/dynamic-configuration/file.toml b/docs/content/reference/dynamic-configuration/file.toml index d531f81a1..a7ac27b77 100644 --- a/docs/content/reference/dynamic-configuration/file.toml +++ b/docs/content/reference/dynamic-configuration/file.toml @@ -217,6 +217,7 @@ province = true locality = true organization = true + organizationalUnit = true commonName = true serialNumber = true domainComponent = true diff --git a/docs/content/reference/dynamic-configuration/file.yaml b/docs/content/reference/dynamic-configuration/file.yaml index 515dba07c..3a124cbfe 100644 --- a/docs/content/reference/dynamic-configuration/file.yaml +++ b/docs/content/reference/dynamic-configuration/file.yaml @@ -250,6 +250,7 @@ http: province: true locality: true organization: true + organizationalUnit: true commonName: true serialNumber: true domainComponent: true diff --git a/docs/content/reference/dynamic-configuration/kv-ref.md b/docs/content/reference/dynamic-configuration/kv-ref.md index b4c81d76b..d06c339e0 100644 --- a/docs/content/reference/dynamic-configuration/kv-ref.md +++ b/docs/content/reference/dynamic-configuration/kv-ref.md @@ -106,6 +106,7 @@ | `traefik/http/middlewares/Middleware13/passTLSClientCert/info/subject/domainComponent` | `true` | | `traefik/http/middlewares/Middleware13/passTLSClientCert/info/subject/locality` | `true` | | `traefik/http/middlewares/Middleware13/passTLSClientCert/info/subject/organization` | `true` | +| `traefik/http/middlewares/Middleware13/passTLSClientCert/info/subject/organizationalUnit` | `true` | | `traefik/http/middlewares/Middleware13/passTLSClientCert/info/subject/province` | `true` | | `traefik/http/middlewares/Middleware13/passTLSClientCert/info/subject/serialNumber` | `true` | | `traefik/http/middlewares/Middleware13/passTLSClientCert/pem` | `true` | diff --git a/docs/content/reference/dynamic-configuration/marathon-labels.json b/docs/content/reference/dynamic-configuration/marathon-labels.json index 65f2c74c4..93aa9c6d1 100644 --- a/docs/content/reference/dynamic-configuration/marathon-labels.json +++ b/docs/content/reference/dynamic-configuration/marathon-labels.json @@ -90,6 +90,7 @@ "traefik.http.middlewares.middleware13.passtlsclientcert.info.subject.domaincomponent": "true", "traefik.http.middlewares.middleware13.passtlsclientcert.info.subject.locality": "true", "traefik.http.middlewares.middleware13.passtlsclientcert.info.subject.organization": "true", +"traefik.http.middlewares.middleware13.passtlsclientcert.info.subject.organizationalunit": "true", "traefik.http.middlewares.middleware13.passtlsclientcert.info.subject.province": "true", "traefik.http.middlewares.middleware13.passtlsclientcert.info.subject.serialnumber": "true", "traefik.http.middlewares.middleware13.passtlsclientcert.pem": "true", diff --git a/docs/content/reference/dynamic-configuration/traefik.containo.us_middlewares.yaml b/docs/content/reference/dynamic-configuration/traefik.containo.us_middlewares.yaml index 4ce827116..c85effec0 100644 --- a/docs/content/reference/dynamic-configuration/traefik.containo.us_middlewares.yaml +++ b/docs/content/reference/dynamic-configuration/traefik.containo.us_middlewares.yaml @@ -398,8 +398,9 @@ spec: info configuration. properties: issuer: - description: TLSCLientCertificateDNInfo holds the client TLS - certificate distinguished name info configuration. cf https://tools.ietf.org/html/rfc3739 + description: TLSCLientCertificateIssuerDNInfo holds the client + TLS certificate distinguished name info configuration. cf + https://tools.ietf.org/html/rfc3739 properties: commonName: type: boolean @@ -425,8 +426,9 @@ spec: serialNumber: type: boolean subject: - description: TLSCLientCertificateDNInfo holds the client TLS - certificate distinguished name info configuration. cf https://tools.ietf.org/html/rfc3739 + description: TLSCLientCertificateSubjectDNInfo holds the client + TLS certificate distinguished name info configuration. cf + https://tools.ietf.org/html/rfc3739 properties: commonName: type: boolean @@ -438,6 +440,8 @@ spec: type: boolean organization: type: boolean + organizationalUnit: + type: boolean province: type: boolean serialNumber: diff --git a/integration/fixtures/k8s/01-traefik-crd.yml b/integration/fixtures/k8s/01-traefik-crd.yml index 84ca0e756..543bb577a 100644 --- a/integration/fixtures/k8s/01-traefik-crd.yml +++ b/integration/fixtures/k8s/01-traefik-crd.yml @@ -840,8 +840,9 @@ spec: info configuration. properties: issuer: - description: TLSCLientCertificateDNInfo holds the client TLS - certificate distinguished name info configuration. cf https://tools.ietf.org/html/rfc3739 + description: TLSCLientCertificateIssuerDNInfo holds the client + TLS certificate distinguished name info configuration. cf + https://tools.ietf.org/html/rfc3739 properties: commonName: type: boolean @@ -867,8 +868,9 @@ spec: serialNumber: type: boolean subject: - description: TLSCLientCertificateDNInfo holds the client TLS - certificate distinguished name info configuration. cf https://tools.ietf.org/html/rfc3739 + description: TLSCLientCertificateSubjectDNInfo holds the client + TLS certificate distinguished name info configuration. cf + https://tools.ietf.org/html/rfc3739 properties: commonName: type: boolean @@ -880,6 +882,8 @@ spec: type: boolean organization: type: boolean + organizationalUnit: + type: boolean province: type: boolean serialNumber: diff --git a/pkg/anonymize/anonymize_config_test.go b/pkg/anonymize/anonymize_config_test.go index 8a1133fca..831bdea65 100644 --- a/pkg/anonymize/anonymize_config_test.go +++ b/pkg/anonymize/anonymize_config_test.go @@ -315,16 +315,17 @@ func TestDo_dynamicConfiguration(t *testing.T) { NotAfter: true, NotBefore: true, Sans: true, - Subject: &dynamic.TLSCLientCertificateDNInfo{ - Country: true, - Province: true, - Locality: true, - Organization: true, - CommonName: true, - SerialNumber: true, - DomainComponent: true, + Subject: &dynamic.TLSCLientCertificateSubjectDNInfo{ + Country: true, + Province: true, + Locality: true, + Organization: true, + OrganizationalUnit: true, + CommonName: true, + SerialNumber: true, + DomainComponent: true, }, - Issuer: &dynamic.TLSCLientCertificateDNInfo{ + Issuer: &dynamic.TLSCLientCertificateIssuerDNInfo{ Country: true, Province: true, Locality: true, diff --git a/pkg/anonymize/testdata/anonymized-dynamic-config.json b/pkg/anonymize/testdata/anonymized-dynamic-config.json index 917fca5d7..c71b0cb0a 100644 --- a/pkg/anonymize/testdata/anonymized-dynamic-config.json +++ b/pkg/anonymize/testdata/anonymized-dynamic-config.json @@ -288,6 +288,7 @@ "province": true, "locality": true, "organization": true, + "organizationalUnit": true, "commonName": true, "serialNumber": true, "domainComponent": true diff --git a/pkg/config/dynamic/fixtures/sample.toml b/pkg/config/dynamic/fixtures/sample.toml index 6bacc6f80..1c2defb0a 100644 --- a/pkg/config/dynamic/fixtures/sample.toml +++ b/pkg/config/dynamic/fixtures/sample.toml @@ -330,6 +330,7 @@ province = true locality = true organization = true + organizationalUnit = true commonName = true serialNumber = true domainComponent = true diff --git a/pkg/config/dynamic/middlewares.go b/pkg/config/dynamic/middlewares.go index 0b3452f0a..136901af7 100644 --- a/pkg/config/dynamic/middlewares.go +++ b/pkg/config/dynamic/middlewares.go @@ -399,19 +399,19 @@ type StripPrefixRegex struct { // TLSClientCertificateInfo holds the client TLS certificate info configuration. type TLSClientCertificateInfo struct { - NotAfter bool `json:"notAfter,omitempty" toml:"notAfter,omitempty" yaml:"notAfter,omitempty" export:"true"` - NotBefore bool `json:"notBefore,omitempty" toml:"notBefore,omitempty" yaml:"notBefore,omitempty" export:"true"` - Sans bool `json:"sans,omitempty" toml:"sans,omitempty" yaml:"sans,omitempty" export:"true"` - Subject *TLSCLientCertificateDNInfo `json:"subject,omitempty" toml:"subject,omitempty" yaml:"subject,omitempty" export:"true"` - Issuer *TLSCLientCertificateDNInfo `json:"issuer,omitempty" toml:"issuer,omitempty" yaml:"issuer,omitempty" export:"true"` - SerialNumber bool `json:"serialNumber,omitempty" toml:"serialNumber,omitempty" yaml:"serialNumber,omitempty" export:"true"` + NotAfter bool `json:"notAfter,omitempty" toml:"notAfter,omitempty" yaml:"notAfter,omitempty" export:"true"` + NotBefore bool `json:"notBefore,omitempty" toml:"notBefore,omitempty" yaml:"notBefore,omitempty" export:"true"` + Sans bool `json:"sans,omitempty" toml:"sans,omitempty" yaml:"sans,omitempty" export:"true"` + Subject *TLSCLientCertificateSubjectDNInfo `json:"subject,omitempty" toml:"subject,omitempty" yaml:"subject,omitempty" export:"true"` + Issuer *TLSCLientCertificateIssuerDNInfo `json:"issuer,omitempty" toml:"issuer,omitempty" yaml:"issuer,omitempty" export:"true"` + SerialNumber bool `json:"serialNumber,omitempty" toml:"serialNumber,omitempty" yaml:"serialNumber,omitempty" export:"true"` } // +k8s:deepcopy-gen=true -// TLSCLientCertificateDNInfo holds the client TLS certificate distinguished name info configuration. +// TLSCLientCertificateIssuerDNInfo holds the client TLS certificate distinguished name info configuration. // cf https://tools.ietf.org/html/rfc3739 -type TLSCLientCertificateDNInfo struct { +type TLSCLientCertificateIssuerDNInfo struct { Country bool `json:"country,omitempty" toml:"country,omitempty" yaml:"country,omitempty" export:"true"` Province bool `json:"province,omitempty" toml:"province,omitempty" yaml:"province,omitempty" export:"true"` Locality bool `json:"locality,omitempty" toml:"locality,omitempty" yaml:"locality,omitempty" export:"true"` @@ -423,6 +423,21 @@ type TLSCLientCertificateDNInfo struct { // +k8s:deepcopy-gen=true +// TLSCLientCertificateSubjectDNInfo holds the client TLS certificate distinguished name info configuration. +// cf https://tools.ietf.org/html/rfc3739 +type TLSCLientCertificateSubjectDNInfo struct { + Country bool `json:"country,omitempty" toml:"country,omitempty" yaml:"country,omitempty" export:"true"` + Province bool `json:"province,omitempty" toml:"province,omitempty" yaml:"province,omitempty" export:"true"` + Locality bool `json:"locality,omitempty" toml:"locality,omitempty" yaml:"locality,omitempty" export:"true"` + Organization bool `json:"organization,omitempty" toml:"organization,omitempty" yaml:"organization,omitempty" export:"true"` + OrganizationalUnit bool `json:"organizationalUnit,omitempty" toml:"organizationalUnit,omitempty" yaml:"organizationalUnit,omitempty" export:"true"` + CommonName bool `json:"commonName,omitempty" toml:"commonName,omitempty" yaml:"commonName,omitempty" export:"true"` + SerialNumber bool `json:"serialNumber,omitempty" toml:"serialNumber,omitempty" yaml:"serialNumber,omitempty" export:"true"` + DomainComponent bool `json:"domainComponent,omitempty" toml:"domainComponent,omitempty" yaml:"domainComponent,omitempty" export:"true"` +} + +// +k8s:deepcopy-gen=true + // Users holds a list of users. type Users []string diff --git a/pkg/config/dynamic/zz_generated.deepcopy.go b/pkg/config/dynamic/zz_generated.deepcopy.go index 742e59c65..44293e7ad 100644 --- a/pkg/config/dynamic/zz_generated.deepcopy.go +++ b/pkg/config/dynamic/zz_generated.deepcopy.go @@ -1535,17 +1535,33 @@ func (in *TCPWeightedRoundRobin) DeepCopy() *TCPWeightedRoundRobin { } // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *TLSCLientCertificateDNInfo) DeepCopyInto(out *TLSCLientCertificateDNInfo) { +func (in *TLSCLientCertificateIssuerDNInfo) DeepCopyInto(out *TLSCLientCertificateIssuerDNInfo) { *out = *in return } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TLSCLientCertificateDNInfo. -func (in *TLSCLientCertificateDNInfo) DeepCopy() *TLSCLientCertificateDNInfo { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TLSCLientCertificateIssuerDNInfo. +func (in *TLSCLientCertificateIssuerDNInfo) DeepCopy() *TLSCLientCertificateIssuerDNInfo { if in == nil { return nil } - out := new(TLSCLientCertificateDNInfo) + out := new(TLSCLientCertificateIssuerDNInfo) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *TLSCLientCertificateSubjectDNInfo) DeepCopyInto(out *TLSCLientCertificateSubjectDNInfo) { + *out = *in + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new TLSCLientCertificateSubjectDNInfo. +func (in *TLSCLientCertificateSubjectDNInfo) DeepCopy() *TLSCLientCertificateSubjectDNInfo { + if in == nil { + return nil + } + out := new(TLSCLientCertificateSubjectDNInfo) in.DeepCopyInto(out) return out } @@ -1555,12 +1571,12 @@ func (in *TLSClientCertificateInfo) DeepCopyInto(out *TLSClientCertificateInfo) *out = *in if in.Subject != nil { in, out := &in.Subject, &out.Subject - *out = new(TLSCLientCertificateDNInfo) + *out = new(TLSCLientCertificateSubjectDNInfo) **out = **in } if in.Issuer != nil { in, out := &in.Issuer, &out.Issuer - *out = new(TLSCLientCertificateDNInfo) + *out = new(TLSCLientCertificateIssuerDNInfo) **out = **in } return diff --git a/pkg/config/label/label_test.go b/pkg/config/label/label_test.go index 3abbd57ea..eac688246 100644 --- a/pkg/config/label/label_test.go +++ b/pkg/config/label/label_test.go @@ -95,6 +95,7 @@ func TestDecodeConfiguration(t *testing.T) { "traefik.http.middlewares.Middleware11.passtlsclientcert.info.subject.domaincomponent": "true", "traefik.http.middlewares.Middleware11.passtlsclientcert.info.subject.locality": "true", "traefik.http.middlewares.Middleware11.passtlsclientcert.info.subject.organization": "true", + "traefik.http.middlewares.Middleware11.passtlsclientcert.info.subject.organizationalunit": "true", "traefik.http.middlewares.Middleware11.passtlsclientcert.info.subject.province": "true", "traefik.http.middlewares.Middleware11.passtlsclientcert.info.subject.serialnumber": "true", "traefik.http.middlewares.Middleware11.passtlsclientcert.info.issuer.commonname": "true", @@ -366,16 +367,17 @@ func TestDecodeConfiguration(t *testing.T) { NotAfter: true, NotBefore: true, SerialNumber: true, - Subject: &dynamic.TLSCLientCertificateDNInfo{ - Country: true, - Province: true, - Locality: true, - Organization: true, - CommonName: true, - SerialNumber: true, - DomainComponent: true, + Subject: &dynamic.TLSCLientCertificateSubjectDNInfo{ + Country: true, + Province: true, + Locality: true, + Organization: true, + OrganizationalUnit: true, + CommonName: true, + SerialNumber: true, + DomainComponent: true, }, - Issuer: &dynamic.TLSCLientCertificateDNInfo{ + Issuer: &dynamic.TLSCLientCertificateIssuerDNInfo{ Country: true, Province: true, Locality: true, @@ -844,16 +846,17 @@ func TestEncodeConfiguration(t *testing.T) { NotAfter: true, NotBefore: true, SerialNumber: true, - Subject: &dynamic.TLSCLientCertificateDNInfo{ - Country: true, - Province: true, - Locality: true, - Organization: true, - CommonName: true, - SerialNumber: true, - DomainComponent: true, + Subject: &dynamic.TLSCLientCertificateSubjectDNInfo{ + Country: true, + Province: true, + Locality: true, + Organization: true, + OrganizationalUnit: true, + CommonName: true, + SerialNumber: true, + DomainComponent: true, }, - Issuer: &dynamic.TLSCLientCertificateDNInfo{ + Issuer: &dynamic.TLSCLientCertificateIssuerDNInfo{ Country: true, Province: true, Locality: true, @@ -1234,6 +1237,7 @@ func TestEncodeConfiguration(t *testing.T) { "traefik.HTTP.Middlewares.Middleware11.PassTLSClientCert.Info.Subject.Province": "true", "traefik.HTTP.Middlewares.Middleware11.PassTLSClientCert.Info.Subject.Locality": "true", "traefik.HTTP.Middlewares.Middleware11.PassTLSClientCert.Info.Subject.Organization": "true", + "traefik.HTTP.Middlewares.Middleware11.PassTLSClientCert.Info.Subject.OrganizationalUnit": "true", "traefik.HTTP.Middlewares.Middleware11.PassTLSClientCert.Info.Subject.CommonName": "true", "traefik.HTTP.Middlewares.Middleware11.PassTLSClientCert.Info.Subject.SerialNumber": "true", "traefik.HTTP.Middlewares.Middleware11.PassTLSClientCert.Info.Subject.DomainComponent": "true", diff --git a/pkg/middlewares/passtlsclientcert/pass_tls_client_cert.go b/pkg/middlewares/passtlsclientcert/pass_tls_client_cert.go index 682836d56..3089f02d5 100644 --- a/pkg/middlewares/passtlsclientcert/pass_tls_client_cert.go +++ b/pkg/middlewares/passtlsclientcert/pass_tls_client_cert.go @@ -35,8 +35,10 @@ var attributeTypeNames = map[string]string{ "0.9.2342.19200300.100.1.25": "DC", // Domain component OID - RFC 2247 } -// DistinguishedNameOptions is a struct for specifying the configuration for the distinguished name info. -type DistinguishedNameOptions struct { +// 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 @@ -46,12 +48,12 @@ type DistinguishedNameOptions struct { StateOrProvinceName bool } -func newDistinguishedNameOptions(info *dynamic.TLSCLientCertificateDNInfo) *DistinguishedNameOptions { +func newIssuerDistinguishedNameOptions(info *dynamic.TLSCLientCertificateIssuerDNInfo) *IssuerDistinguishedNameOptions { if info == nil { return nil } - return &DistinguishedNameOptions{ + return &IssuerDistinguishedNameOptions{ CommonName: info.CommonName, CountryName: info.Country, DomainComponent: info.DomainComponent, @@ -62,13 +64,44 @@ func newDistinguishedNameOptions(info *dynamic.TLSCLientCertificateDNInfo) *Dist } } +// 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 +} + +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 *DistinguishedNameOptions - issuer *DistinguishedNameOptions + subject *SubjectDistinguishedNameOptions + issuer *IssuerDistinguishedNameOptions serialNumber bool } @@ -78,10 +111,10 @@ func newTLSClientCertificateInfo(info *dynamic.TLSClientCertificateInfo) *tlsCli } return &tlsClientCertificateInfo{ - issuer: newDistinguishedNameOptions(info.Issuer), + issuer: newIssuerDistinguishedNameOptions(info.Issuer), notAfter: info.NotAfter, notBefore: info.NotBefore, - subject: newDistinguishedNameOptions(info.Subject), + subject: newSubjectDistinguishedNameOptions(info.Subject), serialNumber: info.SerialNumber, sans: info.Sans, } @@ -147,12 +180,12 @@ func (p *passTLSClientCert) getCertInfo(ctx context.Context, certs []*x509.Certi var values []string if p.info != nil { - subject := getDNInfo(ctx, p.info.subject, &peerCert.Subject) + subject := getSubjectDNInfo(ctx, p.info.subject, &peerCert.Subject) if subject != "" { values = append(values, fmt.Sprintf(`Subject="%s"`, strings.TrimSuffix(subject, subFieldSeparator))) } - issuer := getDNInfo(ctx, p.info.issuer, &peerCert.Issuer) + issuer := getIssuerDNInfo(ctx, p.info.issuer, &peerCert.Issuer) if issuer != "" { values = append(values, fmt.Sprintf(`Issuer="%s"`, strings.TrimSuffix(issuer, subFieldSeparator))) } @@ -187,7 +220,7 @@ func (p *passTLSClientCert) getCertInfo(ctx context.Context, certs []*x509.Certi return strings.Join(headerValues, certSeparator) } -func getDNInfo(ctx context.Context, options *DistinguishedNameOptions, cs *pkix.Name) string { +func getIssuerDNInfo(ctx context.Context, options *IssuerDistinguishedNameOptions, cs *pkix.Name) string { if options == nil { return "" } @@ -229,6 +262,52 @@ func getDNInfo(ctx context.Context, options *DistinguishedNameOptions, cs *pkix. 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)) + } + } + + 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.OrganizationalUnitName { + writeParts(ctx, content, cs.OrganizationalUnit, "OU") + } + + if options.SerialNumber { + writePart(ctx, content, cs.SerialNumber, "SN") + } + + if options.CommonName { + writePart(ctx, content, cs.CommonName, "CN") + } + + return content.String() +} + func writeParts(ctx context.Context, content io.StringWriter, entries []string, prefix string) { for _, entry := range entries { writePart(ctx, content, entry, prefix) diff --git a/pkg/middlewares/passtlsclientcert/pass_tls_client_cert_test.go b/pkg/middlewares/passtlsclientcert/pass_tls_client_cert_test.go index 934dbff1a..b4a56d208 100644 --- a/pkg/middlewares/passtlsclientcert/pass_tls_client_cert_test.go +++ b/pkg/middlewares/passtlsclientcert/pass_tls_client_cert_test.go @@ -310,22 +310,22 @@ func TestPassTLSClientCert_PEM(t *testing.T) { } for _, test := range testCases { - tlsClientHeaders, err := New(context.Background(), next, test.config, "foo") - require.NoError(t, err) - - res := httptest.NewRecorder() - req := testhelpers.MustNewRequest(http.MethodGet, "http://example.com/foo", nil) - - if test.certContents != nil && len(test.certContents) > 0 { - req.TLS = buildTLSWith(test.certContents) - } - - tlsClientHeaders.ServeHTTP(res, req) - test := test t.Run(test.desc, func(t *testing.T) { t.Parallel() + tlsClientHeaders, err := New(context.Background(), next, test.config, "foo") + require.NoError(t, err) + + res := httptest.NewRecorder() + req := testhelpers.MustNewRequest(http.MethodGet, "http://example.com/foo", nil) + + if test.certContents != nil && len(test.certContents) > 0 { + req.TLS = buildTLSWith(test.certContents) + } + + tlsClientHeaders.ServeHTTP(res, req) + assert.Equal(t, http.StatusOK, res.Code, "Http Status should be OK") assert.Equal(t, "bar", res.Body.String(), "Should be the expected body") @@ -351,7 +351,7 @@ func TestPassTLSClientCert_certInfo(t *testing.T) { }, fieldSeparator) completeCertAllInfo := strings.Join([]string{ - `Subject="DC=org,DC=cheese,C=FR,C=US,ST=Cheese org state,ST=Cheese com state,L=TOULOUSE,L=LYON,O=Cheese,O=Cheese 2,CN=*.cheese.com"`, + `Subject="DC=org,DC=cheese,C=FR,C=US,ST=Cheese org state,ST=Cheese com state,L=TOULOUSE,L=LYON,O=Cheese,O=Cheese 2,OU=Simple Signing Section,OU=Simple Signing Section 2,CN=*.cheese.com"`, `Issuer="DC=org,DC=cheese,C=FR,C=US,ST=Signing State,ST=Signing State 2,L=TOULOUSE,L=LYON,O=Cheese,O=Cheese 2,CN=Simple Signing CA 2"`, `SerialNumber="1"`, `NB="1544094616"`, @@ -376,13 +376,14 @@ func TestPassTLSClientCert_certInfo(t *testing.T) { desc: "No TLS, with subject info", config: dynamic.PassTLSClientCert{ Info: &dynamic.TLSClientCertificateInfo{ - Subject: &dynamic.TLSCLientCertificateDNInfo{ - CommonName: true, - Organization: true, - Locality: true, - Province: true, - Country: true, - SerialNumber: true, + Subject: &dynamic.TLSCLientCertificateSubjectDNInfo{ + CommonName: true, + Organization: true, + OrganizationalUnit: true, + Locality: true, + Province: true, + Country: true, + SerialNumber: true, }, }, }, @@ -392,7 +393,7 @@ func TestPassTLSClientCert_certInfo(t *testing.T) { config: dynamic.PassTLSClientCert{ PEM: false, Info: &dynamic.TLSClientCertificateInfo{ - Subject: &dynamic.TLSCLientCertificateDNInfo{}, + Subject: &dynamic.TLSCLientCertificateSubjectDNInfo{}, }, }, }, @@ -405,16 +406,17 @@ func TestPassTLSClientCert_certInfo(t *testing.T) { NotBefore: true, Sans: true, SerialNumber: true, - Subject: &dynamic.TLSCLientCertificateDNInfo{ - CommonName: true, - Country: true, - DomainComponent: true, - Locality: true, - Organization: true, - Province: true, - SerialNumber: true, + Subject: &dynamic.TLSCLientCertificateSubjectDNInfo{ + CommonName: true, + Country: true, + DomainComponent: true, + Locality: true, + Organization: true, + OrganizationalUnit: true, + Province: true, + SerialNumber: true, }, - Issuer: &dynamic.TLSCLientCertificateDNInfo{ + Issuer: &dynamic.TLSCLientCertificateIssuerDNInfo{ CommonName: true, Country: true, DomainComponent: true, @@ -434,10 +436,30 @@ func TestPassTLSClientCert_certInfo(t *testing.T) { Info: &dynamic.TLSClientCertificateInfo{ NotAfter: true, Sans: true, - Subject: &dynamic.TLSCLientCertificateDNInfo{ + Subject: &dynamic.TLSCLientCertificateSubjectDNInfo{ Organization: true, }, - Issuer: &dynamic.TLSCLientCertificateDNInfo{ + Issuer: &dynamic.TLSCLientCertificateIssuerDNInfo{ + Country: true, + }, + }, + }, + expectedHeader: `Subject="O=Cheese";Issuer="C=FR,C=US";NA="1632568236"`, + }, + { + desc: "TLS with simple certificate, requesting non-existent info", + certContents: []string{minimalCheeseCrt}, + config: dynamic.PassTLSClientCert{ + Info: &dynamic.TLSClientCertificateInfo{ + NotAfter: true, + Sans: true, + Subject: &dynamic.TLSCLientCertificateSubjectDNInfo{ + Organization: true, + // OrganizationalUnit is not set on this example certificate, + // so even though it's requested, it will be absent. + OrganizationalUnit: true, + }, + Issuer: &dynamic.TLSCLientCertificateIssuerDNInfo{ Country: true, }, }, @@ -453,16 +475,17 @@ func TestPassTLSClientCert_certInfo(t *testing.T) { NotBefore: true, Sans: true, SerialNumber: true, - Subject: &dynamic.TLSCLientCertificateDNInfo{ - Country: true, - Province: true, - Locality: true, - Organization: true, - CommonName: true, - SerialNumber: true, - DomainComponent: true, + Subject: &dynamic.TLSCLientCertificateSubjectDNInfo{ + Country: true, + Province: true, + Locality: true, + Organization: true, + OrganizationalUnit: true, + CommonName: true, + SerialNumber: true, + DomainComponent: true, }, - Issuer: &dynamic.TLSCLientCertificateDNInfo{ + Issuer: &dynamic.TLSCLientCertificateIssuerDNInfo{ Country: true, Province: true, Locality: true, @@ -484,16 +507,17 @@ func TestPassTLSClientCert_certInfo(t *testing.T) { NotBefore: true, Sans: true, SerialNumber: true, - Subject: &dynamic.TLSCLientCertificateDNInfo{ - Country: true, - Province: true, - Locality: true, - Organization: true, - CommonName: true, - SerialNumber: true, - DomainComponent: true, + Subject: &dynamic.TLSCLientCertificateSubjectDNInfo{ + Country: true, + Province: true, + Locality: true, + Organization: true, + OrganizationalUnit: true, + CommonName: true, + SerialNumber: true, + DomainComponent: true, }, - Issuer: &dynamic.TLSCLientCertificateDNInfo{ + Issuer: &dynamic.TLSCLientCertificateIssuerDNInfo{ Country: true, Province: true, Locality: true, @@ -509,22 +533,22 @@ func TestPassTLSClientCert_certInfo(t *testing.T) { } for _, test := range testCases { - tlsClientHeaders, err := New(context.Background(), next, test.config, "foo") - require.NoError(t, err) - - res := httptest.NewRecorder() - req := testhelpers.MustNewRequest(http.MethodGet, "http://example.com/foo", nil) - - if test.certContents != nil && len(test.certContents) > 0 { - req.TLS = buildTLSWith(test.certContents) - } - - tlsClientHeaders.ServeHTTP(res, req) - test := test t.Run(test.desc, func(t *testing.T) { t.Parallel() + tlsClientHeaders, err := New(context.Background(), next, test.config, "foo") + require.NoError(t, err) + + res := httptest.NewRecorder() + req := testhelpers.MustNewRequest(http.MethodGet, "http://example.com/foo", nil) + + if test.certContents != nil && len(test.certContents) > 0 { + req.TLS = buildTLSWith(test.certContents) + } + + tlsClientHeaders.ServeHTTP(res, req) + assert.Equal(t, http.StatusOK, res.Code, "Http Status should be OK") assert.Equal(t, "bar", res.Body.String(), "Should be the expected body") @@ -621,12 +645,12 @@ func Test_getSANs(t *testing.T) { } for _, test := range testCases { - sans := getSANs(test.cert) - test := test t.Run(test.desc, func(t *testing.T) { t.Parallel() + sans := getSANs(test.cert) + if len(test.expected) > 0 { for i, expected := range test.expected { assert.Equal(t, expected, sans[i]) diff --git a/pkg/provider/kv/kv_test.go b/pkg/provider/kv/kv_test.go index 38896001a..a04093f18 100644 --- a/pkg/provider/kv/kv_test.go +++ b/pkg/provider/kv/kv_test.go @@ -156,6 +156,7 @@ func Test_buildConfiguration(t *testing.T) { "traefik/http/middlewares/Middleware12/passTLSClientCert/info/subject/province": "true", "traefik/http/middlewares/Middleware12/passTLSClientCert/info/subject/locality": "true", "traefik/http/middlewares/Middleware12/passTLSClientCert/info/subject/organization": "true", + "traefik/http/middlewares/Middleware12/passTLSClientCert/info/subject/organizationalunit": "true", "traefik/http/middlewares/Middleware12/passTLSClientCert/info/subject/commonName": "true", "traefik/http/middlewares/Middleware12/passTLSClientCert/info/subject/serialNumber": "true", "traefik/http/middlewares/Middleware12/passTLSClientCert/info/subject/domainComponent": "true", @@ -478,16 +479,17 @@ func Test_buildConfiguration(t *testing.T) { NotAfter: true, NotBefore: true, Sans: true, - Subject: &dynamic.TLSCLientCertificateDNInfo{ - Country: true, - Province: true, - Locality: true, - Organization: true, - CommonName: true, - SerialNumber: true, - DomainComponent: true, + Subject: &dynamic.TLSCLientCertificateSubjectDNInfo{ + Country: true, + Province: true, + Locality: true, + Organization: true, + OrganizationalUnit: true, + CommonName: true, + SerialNumber: true, + DomainComponent: true, }, - Issuer: &dynamic.TLSCLientCertificateDNInfo{ + Issuer: &dynamic.TLSCLientCertificateIssuerDNInfo{ Country: true, Province: true, Locality: true, diff --git a/webui/src/components/_commons/PanelMiddlewares.vue b/webui/src/components/_commons/PanelMiddlewares.vue index 17de1d43e..6bf637bcc 100644 --- a/webui/src/components/_commons/PanelMiddlewares.vue +++ b/webui/src/components/_commons/PanelMiddlewares.vue @@ -866,17 +866,21 @@
-
Common Name
- +
Organizational Unit
+
-
Serial Number
- +
Common Name
+
+
+
Serial Number
+ +
Domain Component