mirror of
https://github.com/traefik/traefik.git
synced 2026-06-27 20:14:26 +00:00
Compare commits
28 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1268620453 | |||
| b5e7a48bcd | |||
| ec80d1145c | |||
| 2391520b50 | |||
| 6cc3dd8d40 | |||
| bcf768ee09 | |||
| 51b9a37615 | |||
| 26c96a3935 | |||
| cb9e8ab510 | |||
| b46e795f41 | |||
| e53a37b869 | |||
| ad1c1fc2f2 | |||
| 0209f984eb | |||
| e043982244 | |||
| 149e62d6db | |||
| 4ef4c09300 | |||
| d5ad3eb63b | |||
| 8447bfc71e | |||
| dc4b6fe2c6 | |||
| 15ecff2bbd | |||
| 8773d7ead4 | |||
| 29406d4289 | |||
| eec68dce06 | |||
| edd7d2eb33 | |||
| f7c0fdea57 | |||
| 9893e89628 | |||
| 786f7192e1 | |||
| 174e5d8111 |
@@ -8,7 +8,7 @@ on:
|
||||
jobs:
|
||||
sync:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 15
|
||||
timeout-minutes: 30
|
||||
permissions:
|
||||
packages: write
|
||||
contents: read
|
||||
|
||||
@@ -1,3 +1,30 @@
|
||||
## [v3.7.5](https://github.com/traefik/traefik/tree/v3.7.5) (2026-06-10)
|
||||
[All Commits](https://github.com/traefik/traefik/compare/v3.7.4...v3.7.5)
|
||||
|
||||
**Bug fixes:**
|
||||
- **[k8s/ingress-nginx]** Skip ingress when auth-secret resolution fails ([#13323](https://github.com/traefik/traefik/pull/13323) @gndz07)
|
||||
- **[k8s/ingress-nginx]** Pass endpointslice fencing on ingress-nginx provider ([#13290](https://github.com/traefik/traefik/pull/13290) @Learloj)
|
||||
- **[k8s/gatewayapi]** Reject cross-provider references with backendRefs.namespace ([#13322](https://github.com/traefik/traefik/pull/13322) @youkoulayley)
|
||||
- **[server]** Bump to github.com/pires/go-proxyproto v0.12.0 ([#13313](https://github.com/traefik/traefik/pull/13313) @timschumi)
|
||||
- **[tls]** Fix routers with same host, different tlsoptions on different entryPoint ([#13329](https://github.com/traefik/traefik/pull/13329) @juliens)
|
||||
- **[tls]** Fix snicheck for routers with no hosts ([#13333](https://github.com/traefik/traefik/pull/13333) @rtribotte)
|
||||
|
||||
## [v3.6.21](https://github.com/traefik/traefik/tree/v3.6.21) (2026-06-10)
|
||||
[All Commits](https://github.com/traefik/traefik/compare/v3.6.20...v3.6.21)
|
||||
|
||||
**Bug fixes:**
|
||||
- **[k8s/gatewayapi]** Reject cross-provider references with backendRefs.namespace ([#13322](https://github.com/traefik/traefik/pull/13322) @youkoulayley)
|
||||
- **[server]** Bump to github.com/pires/go-proxyproto v0.12.0 ([#13313](https://github.com/traefik/traefik/pull/13313) @timschumi)
|
||||
- **[tls]** Fix routers with same host, different tlsoptions on different entryPoint ([#13329](https://github.com/traefik/traefik/pull/13329) @juliens)
|
||||
- **[tls]** Fix snicheck for routers with no hosts ([#13333](https://github.com/traefik/traefik/pull/13333) @rtribotte)
|
||||
|
||||
## [v2.11.50](https://github.com/traefik/traefik/tree/v2.11.50) (2026-06-10)
|
||||
[All Commits](https://github.com/traefik/traefik/compare/v2.11.49...v2.11.50)
|
||||
|
||||
**Bug fixes:**
|
||||
- **[tls]** Fix routers with same host, different tlsoptions on different entryPoint ([#13329](https://github.com/traefik/traefik/pull/13329) @juliens)
|
||||
- **[tls]** Fix snicheck for routers with no hosts ([#13333](https://github.com/traefik/traefik/pull/13333) @rtribotte)
|
||||
|
||||
## [v3.7.4](https://github.com/traefik/traefik/tree/v3.7.4) (2026-06-05)
|
||||
[All Commits](https://github.com/traefik/traefik/compare/v3.7.3...v3.7.4)
|
||||
|
||||
|
||||
@@ -93,6 +93,12 @@ For a complete list of supported annotations and behavioral differences, see the
|
||||
|
||||
The Kubernetes Ingress NGINX provider requires **Traefik v3.6.2 or later**.
|
||||
|
||||
!!! info "Legacy Scheme Headers"
|
||||
|
||||
If your applications still depend on ingress-nginx's legacy `X-Forwarded-Scheme` or `X-Scheme` headers,
|
||||
enable `entryPoints.<name>.forwardedHeaders.addXForwardedSchemeHeaders=true` on the entrypoints that receive this traffic.
|
||||
This keeps `X-Forwarded-Proto` unchanged and restores the compatibility headers at the entrypoint level for every provider.
|
||||
|
||||
---
|
||||
|
||||
## Prerequisites
|
||||
|
||||
@@ -9,6 +9,25 @@ This guide provides detailed migration steps for upgrading between different Tra
|
||||
|
||||
---
|
||||
|
||||
## v3.8.0
|
||||
|
||||
### `peerCertURI` option deprecation
|
||||
|
||||
Starting with `v3.8.0`, the `peerCertURI` option is deprecated in the `ServersTransport` and `ServersTransportTCP` configurations and will be removed in the next major version.
|
||||
The new `peerCertSANs` option replaces it and supports multiple Subject Alternative Names (SANs) of type `URI` or `DNSName`.
|
||||
|
||||
Please check out the [ServersTransport](../reference/routing-configuration/http/load-balancing/serverstransport.md#opt-peerCertSANs) and [ServersTransportTCP](../reference/routing-configuration/tcp/serverstransport.md#opt-serverstransport-tls-peerCertSANs) documentation for more details.
|
||||
|
||||
#### Kubernetes CRD Provider
|
||||
|
||||
To use the new `peerCertSANs` field on `ServersTransport` and `ServersTransportTCP` resources with the Kubernetes CRD provider, you need to update your CRDs.
|
||||
|
||||
**Apply Updated CRDs:**
|
||||
|
||||
```shell
|
||||
kubectl apply -f https://raw.githubusercontent.com/traefik/traefik/v3.8/docs/content/reference/dynamic-configuration/kubernetes-crd-definition-v1.yml
|
||||
```
|
||||
|
||||
## v3.7.3
|
||||
|
||||
### Kubernetes Gateway API Provider
|
||||
|
||||
@@ -2493,9 +2493,25 @@ spec:
|
||||
description: MinVersion defines the minimum TLS version to use when
|
||||
contacting backend servers.
|
||||
type: string
|
||||
peerCertSANs:
|
||||
description: PeerCertSANs defines the peer cert Subject Alternative
|
||||
Names used to match against SAN during the peer certificate verification.
|
||||
items:
|
||||
description: SAN represents a Subject Alternative Name.
|
||||
properties:
|
||||
type:
|
||||
description: SANType is the type of the Subject Alternative
|
||||
Name.
|
||||
type: string
|
||||
value:
|
||||
type: string
|
||||
type: object
|
||||
type: array
|
||||
peerCertURI:
|
||||
description: PeerCertURI defines the peer cert URI used to match against
|
||||
SAN URI during the peer certificate verification.
|
||||
description: |-
|
||||
PeerCertURI defines the peer cert URI used to match against SAN URI during the peer certificate verification.
|
||||
|
||||
Deprecated: PeerCertURI is deprecated, please use the PeerCertSANs option instead.
|
||||
type: string
|
||||
rootCAs:
|
||||
description: RootCAs defines a list of CA certificate Secrets or ConfigMaps
|
||||
@@ -2647,10 +2663,26 @@ spec:
|
||||
insecureSkipVerify:
|
||||
description: InsecureSkipVerify disables TLS certificate verification.
|
||||
type: boolean
|
||||
peerCertSANs:
|
||||
description: PeerCertSANs defines the peer cert Subject Alternative
|
||||
Names used to match against SAN during the peer certificate
|
||||
verification.
|
||||
items:
|
||||
description: SAN represents a Subject Alternative Name.
|
||||
properties:
|
||||
type:
|
||||
description: SANType is the type of the Subject Alternative
|
||||
Name.
|
||||
type: string
|
||||
value:
|
||||
type: string
|
||||
type: object
|
||||
type: array
|
||||
peerCertURI:
|
||||
description: |-
|
||||
MaxIdleConnsPerHost controls the maximum idle (keep-alive) to keep per-host.
|
||||
PeerCertURI defines the peer cert URI used to match against SAN URI during the peer certificate verification.
|
||||
|
||||
Deprecated: PeerCertURI is deprecated, please use the PeerCertSANs option instead.
|
||||
type: string
|
||||
rootCAs:
|
||||
description: RootCAs defines a list of CA certificate Secrets
|
||||
|
||||
@@ -123,9 +123,25 @@ spec:
|
||||
description: MinVersion defines the minimum TLS version to use when
|
||||
contacting backend servers.
|
||||
type: string
|
||||
peerCertSANs:
|
||||
description: PeerCertSANs defines the peer cert Subject Alternative
|
||||
Names used to match against SAN during the peer certificate verification.
|
||||
items:
|
||||
description: SAN represents a Subject Alternative Name.
|
||||
properties:
|
||||
type:
|
||||
description: SANType is the type of the Subject Alternative
|
||||
Name.
|
||||
type: string
|
||||
value:
|
||||
type: string
|
||||
type: object
|
||||
type: array
|
||||
peerCertURI:
|
||||
description: PeerCertURI defines the peer cert URI used to match against
|
||||
SAN URI during the peer certificate verification.
|
||||
description: |-
|
||||
PeerCertURI defines the peer cert URI used to match against SAN URI during the peer certificate verification.
|
||||
|
||||
Deprecated: PeerCertURI is deprecated, please use the PeerCertSANs option instead.
|
||||
type: string
|
||||
rootCAs:
|
||||
description: RootCAs defines a list of CA certificate Secrets or ConfigMaps
|
||||
|
||||
@@ -93,10 +93,26 @@ spec:
|
||||
insecureSkipVerify:
|
||||
description: InsecureSkipVerify disables TLS certificate verification.
|
||||
type: boolean
|
||||
peerCertSANs:
|
||||
description: PeerCertSANs defines the peer cert Subject Alternative
|
||||
Names used to match against SAN during the peer certificate
|
||||
verification.
|
||||
items:
|
||||
description: SAN represents a Subject Alternative Name.
|
||||
properties:
|
||||
type:
|
||||
description: SANType is the type of the Subject Alternative
|
||||
Name.
|
||||
type: string
|
||||
value:
|
||||
type: string
|
||||
type: object
|
||||
type: array
|
||||
peerCertURI:
|
||||
description: |-
|
||||
MaxIdleConnsPerHost controls the maximum idle (keep-alive) to keep per-host.
|
||||
PeerCertURI defines the peer cert URI used to match against SAN URI during the peer certificate verification.
|
||||
|
||||
Deprecated: PeerCertURI is deprecated, please use the PeerCertSANs option instead.
|
||||
type: string
|
||||
rootCAs:
|
||||
description: RootCAs defines a list of CA certificate Secrets
|
||||
|
||||
@@ -85,6 +85,7 @@ THIS FILE MUST NOT BE EDITED BY HAND
|
||||
| <a id="opt-entrypoints-name-address" href="#opt-entrypoints-name-address" title="#opt-entrypoints-name-address">entrypoints._name_.address</a> | Entry point address. | |
|
||||
| <a id="opt-entrypoints-name-allowacmebypass" href="#opt-entrypoints-name-allowacmebypass" title="#opt-entrypoints-name-allowacmebypass">entrypoints._name_.allowacmebypass</a> | Enables handling of ACME TLS and HTTP challenges with custom routers. | false |
|
||||
| <a id="opt-entrypoints-name-asdefault" href="#opt-entrypoints-name-asdefault" title="#opt-entrypoints-name-asdefault">entrypoints._name_.asdefault</a> | Adds this EntryPoint to the list of default EntryPoints to be used on routers that don't have any Entrypoint defined. | false |
|
||||
| <a id="opt-entrypoints-name-forwardedheaders-addxforwardedschemeheaders" href="#opt-entrypoints-name-forwardedheaders-addxforwardedschemeheaders" title="#opt-entrypoints-name-forwardedheaders-addxforwardedschemeheaders">entrypoints._name_.forwardedheaders.addxforwardedschemeheaders</a> | Add the X-Forwarded-Scheme and X-Scheme headers. | false |
|
||||
| <a id="opt-entrypoints-name-forwardedheaders-connection" href="#opt-entrypoints-name-forwardedheaders-connection" title="#opt-entrypoints-name-forwardedheaders-connection">entrypoints._name_.forwardedheaders.connection</a> | List of Connection headers that are allowed to pass through the middleware chain before being removed. | |
|
||||
| <a id="opt-entrypoints-name-forwardedheaders-insecure" href="#opt-entrypoints-name-forwardedheaders-insecure" title="#opt-entrypoints-name-forwardedheaders-insecure">entrypoints._name_.forwardedheaders.insecure</a> | Trust all forwarded headers. | false |
|
||||
| <a id="opt-entrypoints-name-forwardedheaders-notappendxforwardedfor" href="#opt-entrypoints-name-forwardedheaders-notappendxforwardedfor" title="#opt-entrypoints-name-forwardedheaders-notappendxforwardedfor">entrypoints._name_.forwardedheaders.notappendxforwardedfor</a> | Disable appending RemoteAddr to X-Forwarded-For header. Defaults to false (appending is enabled). | false |
|
||||
@@ -395,6 +396,7 @@ THIS FILE MUST NOT BE EDITED BY HAND
|
||||
| <a id="opt-providers-kubernetesingress-labelselector" href="#opt-providers-kubernetesingress-labelselector" title="#opt-providers-kubernetesingress-labelselector">providers.kubernetesingress.labelselector</a> | Kubernetes Ingress label selector to use. | |
|
||||
| <a id="opt-providers-kubernetesingress-namespaces" href="#opt-providers-kubernetesingress-namespaces" title="#opt-providers-kubernetesingress-namespaces">providers.kubernetesingress.namespaces</a> | Kubernetes namespaces. | |
|
||||
| <a id="opt-providers-kubernetesingress-nativelbbydefault" href="#opt-providers-kubernetesingress-nativelbbydefault" title="#opt-providers-kubernetesingress-nativelbbydefault">providers.kubernetesingress.nativelbbydefault</a> | Defines whether to use Native Kubernetes load-balancing mode by default. | false |
|
||||
| <a id="opt-providers-kubernetesingress-reportnodeinternalips" href="#opt-providers-kubernetesingress-reportnodeinternalips" title="#opt-providers-kubernetesingress-reportnodeinternalips">providers.kubernetesingress.reportnodeinternalips</a> | Report node internal IPs in Ingress status. | false |
|
||||
| <a id="opt-providers-kubernetesingress-strictprefixmatching" href="#opt-providers-kubernetesingress-strictprefixmatching" title="#opt-providers-kubernetesingress-strictprefixmatching">providers.kubernetesingress.strictprefixmatching</a> | Make prefix matching strictly comply with the Kubernetes Ingress specification (path-element-wise matching instead of character-by-character string matching). | false |
|
||||
| <a id="opt-providers-kubernetesingress-throttleduration" href="#opt-providers-kubernetesingress-throttleduration" title="#opt-providers-kubernetesingress-throttleduration">providers.kubernetesingress.throttleduration</a> | Ingress refresh throttle duration | 0 |
|
||||
| <a id="opt-providers-kubernetesingress-token" href="#opt-providers-kubernetesingress-token" title="#opt-providers-kubernetesingress-token">providers.kubernetesingress.token</a> | Kubernetes bearer token (not needed for in-cluster client). It accepts either a token value or a file path to the token. | |
|
||||
|
||||
@@ -89,6 +89,7 @@ additionalArguments:
|
||||
| <a id="opt-asDefault" href="#opt-asDefault" title="#opt-asDefault">`asDefault`</a> | Mark the `entryPoint` to be in the list of default `entryPoints`.<br /> `entryPoints`in this list are used (by default) on HTTP and TCP routers that do not define their own `entryPoints` option.<br /> More information [here](#asdefault). | false | No |
|
||||
| <a id="opt-allowACMEByPass" href="#opt-allowACMEByPass" title="#opt-allowACMEByPass">`allowACMEByPass`</a> | Enables handling of ACME TLS and HTTP challenges with custom routers instead of the internal ACME router. | false | No |
|
||||
| <a id="opt-forwardedHeaders-connection" href="#opt-forwardedHeaders-connection" title="#opt-forwardedHeaders-connection">`forwardedHeaders.`<br />`connection`</a> | List of Connection headers that are allowed to pass through the middleware chain before being removed. | false | No |
|
||||
| <a id="opt-forwardedHeaders-addXForwardedSchemeHeaders" href="#opt-forwardedHeaders-addXForwardedSchemeHeaders" title="#opt-forwardedHeaders-addXForwardedSchemeHeaders">`forwardedHeaders.`<br />`addXForwardedSchemeHeaders`</a> | Add the compatibility headers `X-Forwarded-Scheme` and `X-Scheme`. | false | No |
|
||||
| <a id="opt-forwardedHeaders-insecure" href="#opt-forwardedHeaders-insecure" title="#opt-forwardedHeaders-insecure">`forwardedHeaders.`<br />`insecure`</a> | Set the insecure mode to always trust the forwarded headers information (`X-Forwarded-*`).<br />We recommend to use this option only for tests purposes, not in production. | false | No |
|
||||
| <a id="opt-forwardedHeaders-trustedIPs" href="#opt-forwardedHeaders-trustedIPs" title="#opt-forwardedHeaders-trustedIPs">`forwardedHeaders.`<br />`trustedIPs`</a> | Set the IPs or CIDR from where Traefik trusts the forwarded headers information (`X-Forwarded-*`). | - | No |
|
||||
| <a id="opt-forwardedHeaders-notAppendXForwardedFor" href="#opt-forwardedHeaders-notAppendXForwardedFor" title="#opt-forwardedHeaders-notAppendXForwardedFor">`forwardedHeaders.`<br />`notAppendXForwardedFor`</a> | When set to `true`, Traefik will not append the client's `RemoteAddr` to the `X-Forwarded-For` header. The existing header is preserved as-is. If no `X-Forwarded-For` header exists, none will be added. | false | No |
|
||||
@@ -392,6 +393,37 @@ You can configure Traefik to trust the forwarded headers information (`X-Forward
|
||||
--entryPoints.web.forwardedHeaders.connection=foobar
|
||||
```
|
||||
|
||||
??? info "`forwardedHeaders.addXForwardedSchemeHeaders`"
|
||||
|
||||
Add the compatibility headers `X-Forwarded-Scheme` and `X-Scheme` next to `X-Forwarded-Proto`.
|
||||
This is primarily useful when migrating from ingress-nginx and your applications still rely on these legacy headers.
|
||||
When enabled, these compatibility headers follow the same value as `X-Forwarded-Proto`.
|
||||
|
||||
```yaml tab="File (YAML)"
|
||||
## Static configuration
|
||||
entryPoints:
|
||||
websecure:
|
||||
address: ":443"
|
||||
forwardedHeaders:
|
||||
addXForwardedSchemeHeaders: true
|
||||
```
|
||||
|
||||
```toml tab="File (TOML)"
|
||||
## Static configuration
|
||||
[entryPoints]
|
||||
[entryPoints.websecure]
|
||||
address = ":443"
|
||||
|
||||
[entryPoints.websecure.forwardedHeaders]
|
||||
addXForwardedSchemeHeaders = true
|
||||
```
|
||||
|
||||
```bash tab="CLI"
|
||||
## Static configuration
|
||||
--entryPoints.websecure.address=:443
|
||||
--entryPoints.websecure.forwardedHeaders.addXForwardedSchemeHeaders=true
|
||||
```
|
||||
|
||||
### HTTP3
|
||||
|
||||
As HTTP/3 actually uses UDP, when Traefik is configured with a TCP `entryPoint`
|
||||
|
||||
+27
-1
@@ -58,12 +58,13 @@ which in turn creates the resulting routers, services, handlers, etc.
|
||||
| <a id="opt-providers-kubernetesIngress-ingressEndpoint-hostname" href="#opt-providers-kubernetesIngress-ingressEndpoint-hostname" title="#opt-providers-kubernetesIngress-ingressEndpoint-hostname">`providers.kubernetesIngress.`<br />`ingressEndpoint.hostname`</a> | Hostname used for Kubernetes Ingress endpoints. | "" | No |
|
||||
| <a id="opt-providers-kubernetesIngress-ingressEndpoint-ip" href="#opt-providers-kubernetesIngress-ingressEndpoint-ip" title="#opt-providers-kubernetesIngress-ingressEndpoint-ip">`providers.kubernetesIngress.`<br />`ingressEndpoint.ip`</a> | This IP will get copied to the Ingress `status.loadbalancer.ip`, and currently only supports one IP value (IPv4 or IPv6). | "" | No |
|
||||
| <a id="opt-providers-kubernetesIngress-ingressEndpoint-publishedService" href="#opt-providers-kubernetesIngress-ingressEndpoint-publishedService" title="#opt-providers-kubernetesIngress-ingressEndpoint-publishedService">`providers.kubernetesIngress.`<br />`ingressEndpoint.publishedService`</a> | The Kubernetes service to copy status from.<br />More information [here](#ingressendpointpublishedservice). | "" | No |
|
||||
| <a id="opt-providers-kubernetesIngress-reportNodeInternalIPs" href="#opt-providers-kubernetesIngress-reportNodeInternalIPs" title="#opt-providers-kubernetesIngress-reportNodeInternalIPs">`providers.kubernetesIngress.reportNodeInternalIPs`</a> | Report node internal IPs in Ingress status.<br />Incompatible with `ingressEndpoint` and `disableClusterScopeResources`.<br />More information [here](#reportnodeinternalips). | false | No |
|
||||
| <a id="opt-providers-kubernetesIngress-throttleDuration" href="#opt-providers-kubernetesIngress-throttleDuration" title="#opt-providers-kubernetesIngress-throttleDuration">`providers.kubernetesIngress.throttleDuration`</a> | Minimum amount of time to wait between two Kubernetes events before producing a new configuration.<br />This prevents a Kubernetes cluster that updates many times per second from continuously changing your Traefik configuration.<br />If empty, every event is caught. | 0s | No |
|
||||
| <a id="opt-providers-kubernetesIngress-allowEmptyServices" href="#opt-providers-kubernetesIngress-allowEmptyServices" title="#opt-providers-kubernetesIngress-allowEmptyServices">`providers.kubernetesIngress.allowEmptyServices`</a> | Allows creating a route to reach a service that has no endpoint available.<br />It allows Traefik to handle the requests and responses targeting this service (applying middleware or observability operations) before returning a `503` HTTP Status. | false | No |
|
||||
| <a id="opt-providers-kubernetesIngress-allowExternalNameServices" href="#opt-providers-kubernetesIngress-allowExternalNameServices" title="#opt-providers-kubernetesIngress-allowExternalNameServices">`providers.kubernetesIngress.allowExternalNameServices`</a> | Allows the `Ingress` to reference ExternalName services. | false | No |
|
||||
| <a id="opt-providers-kubernetesIngress-crossProviderNamespaces" href="#opt-providers-kubernetesIngress-crossProviderNamespaces" title="#opt-providers-kubernetesIngress-crossProviderNamespaces">`providers.kubernetesIngress.crossProviderNamespaces`</a> | List of namespaces from which Ingresses or Services are allowed to use `traefik.ingress.kubernetes.io/router.middlewares`, `traefik.ingress.kubernetes.io/router.tls.options`, or `traefik.ingress.kubernetes.io/service.serverstransport` annotations.<br />When unset, all namespaces are allowed. When set to `[]`, every cross-provider reference is rejected. | [] | No |
|
||||
| <a id="opt-providers-kubernetesIngress-nativeLBByDefault" href="#opt-providers-kubernetesIngress-nativeLBByDefault" title="#opt-providers-kubernetesIngress-nativeLBByDefault">`providers.kubernetesIngress.nativeLBByDefault`</a> | Allow using the Kubernetes Service load balancing between the pods instead of the one provided by Traefik for every `Ingress` by default.<br />It can be overridden in the [`Service`](../../../../reference/routing-configuration/kubernetes/crd/http/service.md#opt-nativeLB) | false | No |
|
||||
| <a id="opt-providers-kubernetesIngress-disableClusterScopeResources" href="#opt-providers-kubernetesIngress-disableClusterScopeResources" title="#opt-providers-kubernetesIngress-disableClusterScopeResources">`providers.kubernetesIngress.disableClusterScopeResources`</a> | Prevent from discovering cluster scope resources (`IngressClass` and `Nodes`).<br />By doing so, it alleviates the requirement of giving Traefik the rights to look up for cluster resources.<br />Furthermore, Traefik will not handle Ingresses with IngressClass references, therefore such Ingresses will be ignored (please note that annotations are not affected by this option).<br />This will also prevent from using the `NodePortLB` options on services. | false | No |
|
||||
| <a id="opt-providers-kubernetesIngress-disableClusterScopeResources" href="#opt-providers-kubernetesIngress-disableClusterScopeResources" title="#opt-providers-kubernetesIngress-disableClusterScopeResources">`providers.kubernetesIngress.disableClusterScopeResources`</a> | Prevent from discovering cluster scope resources (`IngressClass` and `Nodes`).<br />By doing so, it alleviates the requirement of giving Traefik the rights to look up for cluster resources.<br />Furthermore, Traefik will not handle Ingresses with IngressClass references, therefore such Ingresses will be ignored (please note that annotations are not affected by this option).<br />This will also prevent from using the `NodePortLB` options on services and is incompatible with `reportNodeInternalIPs`. | false | No |
|
||||
| <a id="opt-providers-kubernetesIngress-strictPrefixMatching" href="#opt-providers-kubernetesIngress-strictPrefixMatching" title="#opt-providers-kubernetesIngress-strictPrefixMatching">`providers.kubernetesIngress.strictPrefixMatching`</a> | Make prefix matching strictly comply with the Kubernetes Ingress specification (path-element-wise matching instead of character-by-character string matching). For example, a PathPrefix of `/foo` will match `/foo`, `/foo/`, and `/foo/bar` but not `/foobar`. | false | No |
|
||||
|
||||
<!-- markdownlint-enable MD013 -->
|
||||
@@ -138,6 +139,31 @@ providers:
|
||||
--providers.kubernetesingress.ingressendpoint.publishedservice=namespace/foo-service
|
||||
```
|
||||
|
||||
### `reportNodeInternalIPs`
|
||||
|
||||
When set to `true`, Traefik reports the internal IPs of all nodes in the cluster into the `status.loadBalancer.ingress` field of each managed Ingress resource.
|
||||
|
||||
This is the equivalent of ingress-nginx's `--report-node-internal-ip-address` flag and is the recommended approach for bare-metal Kubernetes deployments where Traefik runs as a DaemonSet without a cloud LoadBalancer or MetalLB.
|
||||
|
||||
This option requires cluster-scope access to Node resources and is mutually exclusive with `ingressEndpoint` and `disableClusterScopeResources`.
|
||||
|
||||
```yaml tab="File (YAML)"
|
||||
providers:
|
||||
kubernetesIngress:
|
||||
reportNodeInternalIPs: true
|
||||
# ...
|
||||
```
|
||||
|
||||
```toml tab="File (TOML)"
|
||||
[providers.kubernetesIngress]
|
||||
reportNodeInternalIPs = true
|
||||
# ...
|
||||
```
|
||||
|
||||
```bash tab="CLI"
|
||||
--providers.kubernetesingress.reportnodeinternalips=true
|
||||
```
|
||||
|
||||
## Routing Configuration
|
||||
|
||||
See the dedicated section in [routing](../../../../reference/routing-configuration/kubernetes/ingress.md).
|
||||
|
||||
+12
-6
@@ -23,7 +23,11 @@ http:
|
||||
- "/path/to/rootca2.pem"
|
||||
maxIdleConnsPerHost: 100
|
||||
disableHTTP2: true
|
||||
peerCertURI: "spiffe://example.org/peer"
|
||||
peerCertSANs:
|
||||
- type: DNSName
|
||||
value: foo.com
|
||||
- type: URI
|
||||
value: spiffe://example.org/peer
|
||||
forwardingTimeouts:
|
||||
dialTimeout: "30s"
|
||||
responseHeaderTimeout: "10s"
|
||||
@@ -50,7 +54,7 @@ http:
|
||||
rootcas = ["/path/to/rootca1.pem", "/path/to/rootca2.pem"]
|
||||
maxIdleConnsPerHost = 100
|
||||
disableHTTP2 = true
|
||||
peerCertURI = "spiffe://example.org/peer"
|
||||
peerCertSANs = [{type = "DNSName", value = "foo.com"}, {type = "URI", value = "spiffe://example.org/peer"}]
|
||||
cipherSuites = ["TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256","TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384"]
|
||||
minVersion = "VersionTLS12"
|
||||
maxVersion = "VersionTLS12"
|
||||
@@ -108,12 +112,14 @@ labels:
|
||||
| <a id="opt-certificates" href="#opt-certificates" title="#opt-certificates">`certificates`</a> | Defines the list of certificates (as file paths, or data bytes) that will be set as client certificates for mTLS. | [] | No |
|
||||
| <a id="opt-insecureSkipVerify" href="#opt-insecureSkipVerify" title="#opt-insecureSkipVerify">`insecureSkipVerify`</a> | Controls whether the server's certificate chain and host name is verified. | false | No |
|
||||
| <a id="opt-rootcas" href="#opt-rootcas" title="#opt-rootcas">`rootcas`</a> | Set of root certificate authorities to use when verifying server certificates. (for mTLS connections). | [] | No |
|
||||
| <a id="opt-cipherSuites" href="#opt-cipherSuites" title="#opt-cipherSuites">`cipherSuites`</a> | Defines the cipher suites to use when contacting backend servers. | [] | No |
|
||||
| <a id="opt-minVersion" href="#opt-minVersion" title="#opt-minVersion">`minVersion`</a> | Defines the minimum TLS version to use when contacting backend servers. | "" | No |
|
||||
| <a id="opt-maxVersion" href="#opt-maxVersion" title="#opt-maxVersion">`maxVersion`</a> | Defines the maximum TLS version to use when contacting backend servers. | "" | No |
|
||||
| <a id="opt-cipherSuites" href="#opt-cipherSuites" title="#opt-cipherSuites">`cipherSuites`</a> | Defines the cipher suites to use when contacting backend servers. | [] | No |
|
||||
| <a id="opt-minVersion" href="#opt-minVersion" title="#opt-minVersion">`minVersion`</a> | Defines the minimum TLS version to use when contacting backend servers. | "" | No |
|
||||
| <a id="opt-maxVersion" href="#opt-maxVersion" title="#opt-maxVersion">`maxVersion`</a> | Defines the maximum TLS version to use when contacting backend servers. | "" | No |
|
||||
| <a id="opt-maxIdleConnsPerHost" href="#opt-maxIdleConnsPerHost" title="#opt-maxIdleConnsPerHost">`maxIdleConnsPerHost`</a> | Maximum idle (keep-alive) connections to keep per-host. | 200 | No |
|
||||
| <a id="opt-disableHTTP2" href="#opt-disableHTTP2" title="#opt-disableHTTP2">`disableHTTP2`</a> | Disables HTTP/2 for connections with servers. | false | No |
|
||||
| <a id="opt-peerCertURI" href="#opt-peerCertURI" title="#opt-peerCertURI">`peerCertURI`</a> | Defines the URI used to match against SAN URIs during the server's certificate verification. | "" | No |
|
||||
| <a id="opt-peerCertSANs" href="#opt-peerCertSANs" title="#opt-peerCertSANs">`peerCertSANs`</a> | Defines the SANs (Subject Alternative Names) used to match against SANs during the peer certificate verification. | [] | No |
|
||||
| <a id="opt-peerCertSANs-type" href="#opt-peerCertSANs-type" title="#opt-peerCertSANs-type">`peerCertSANs[].type`</a> | Defines the SAN type (`URI` or `DNSName`) to match against the peer certificate's Subject Alternative Names. | "" | No |
|
||||
| <a id="opt-peerCertSANs-value" href="#opt-peerCertSANs-value" title="#opt-peerCertSANs-value">`peerCertSANs[].value`</a> | Defines the SAN value to match against the peer certificate's Subject Alternative Names. | "" | No |
|
||||
| <a id="opt-forwardingTimeouts-dialTimeout" href="#opt-forwardingTimeouts-dialTimeout" title="#opt-forwardingTimeouts-dialTimeout">`forwardingTimeouts.dialTimeout`</a> | Amount of time to wait until a connection to a server can be established.<br />0 = no timeout | 30s | No |
|
||||
| <a id="opt-forwardingTimeouts-responseHeaderTimeout" href="#opt-forwardingTimeouts-responseHeaderTimeout" title="#opt-forwardingTimeouts-responseHeaderTimeout">`forwardingTimeouts.responseHeaderTimeout`</a> | Amount of time to wait for a server's response headers after fully writing the request (including its body, if any).<br />0 = no timeout | 0s | No |
|
||||
| <a id="opt-forwardingTimeouts-idleConnTimeout" href="#opt-forwardingTimeouts-idleConnTimeout" title="#opt-forwardingTimeouts-idleConnTimeout">`forwardingTimeouts.idleConnTimeout`</a> | Maximum amount of time an idle (keep-alive) connection will remain idle before closing itself.<br />0 = no timeout | 90s | No |
|
||||
|
||||
+19
-29
@@ -53,35 +53,25 @@ spec:
|
||||
|
||||
## Configuration Options
|
||||
|
||||
| Field | Description | Default | Required |
|
||||
|:------|:----------------------------------------------------------|:---------------------|:---------|
|
||||
| <a id="opt-serverstransport-serverName" href="#opt-serverstransport-serverName" title="#opt-serverstransport-serverName">`serverstransport.`<br />`serverName`</a> | Defines the server name that will be used for SNI. | | No |
|
||||
| <a id="opt-serverstransport-insecureSkipVerify" href="#opt-serverstransport-insecureSkipVerify" title="#opt-serverstransport-insecureSkipVerify">`serverstransport.`<br />`insecureSkipVerify`</a> | Controls whether the server's certificate chain and host name is verified. | false | No |
|
||||
| <a id="opt-serverstransport-rootcas" href="#opt-serverstransport-rootcas" title="#opt-serverstransport-rootcas">`serverstransport.`<br />`rootcas`</a> | Set of root certificate authorities to use when verifying server certificates. (for mTLS connections). | | No |
|
||||
| <a id="opt-serverstransport-certificatesSecrets" href="#opt-serverstransport-certificatesSecrets" title="#opt-serverstransport-certificatesSecrets">`serverstransport.`<br />`certificatesSecrets`</a> | Certificates to present to the server for mTLS. | | No |
|
||||
| <a id="opt-serverstransport-maxIdleConnsPerHost" href="#opt-serverstransport-maxIdleConnsPerHost" title="#opt-serverstransport-maxIdleConnsPerHost">`serverstransport.`<br />`maxIdleConnsPerHost`</a> | Maximum idle (keep-alive) connections to keep per-host. | 200 | No |
|
||||
| <a id="opt-serverstransport-disableHTTP2" href="#opt-serverstransport-disableHTTP2" title="#opt-serverstransport-disableHTTP2">`serverstransport.`<br />`disableHTTP2`</a> | Disables HTTP/2 for connections with servers. | false | No |
|
||||
| <a id="opt-serverstransport-peerCertURI" href="#opt-serverstransport-peerCertURI" title="#opt-serverstransport-peerCertURI">`serverstransport.`<br />`peerCertURI`</a> | Defines the URI used to match against SAN URIs during the server's certificate verification. | "" | No |
|
||||
| <a id="opt-serverstransport-forwardingTimeouts-dialTimeout" href="#opt-serverstransport-forwardingTimeouts-dialTimeout" title="#opt-serverstransport-forwardingTimeouts-dialTimeout">`serverstransport.`<br />`forwardingTimeouts.dialTimeout`</a> | Amount of time to wait until a connection to a server can be established.<br />Zero means no timeout. | 30s | No |
|
||||
| <a id="opt-serverstransport-forwardingTimeouts-responseHeaderTimeout" href="#opt-serverstransport-forwardingTimeouts-responseHeaderTimeout" title="#opt-serverstransport-forwardingTimeouts-responseHeaderTimeout">`serverstransport.`<br />`forwardingTimeouts.responseHeaderTimeout`</a> | Amount of time to wait for a server's response headers after fully writing the request (including its body, if any).<br />Zero means no timeout | 0s | No |
|
||||
| <a id="opt-serverstransport-forwardingTimeouts-idleConnTimeout" href="#opt-serverstransport-forwardingTimeouts-idleConnTimeout" title="#opt-serverstransport-forwardingTimeouts-idleConnTimeout">`serverstransport.`<br />`forwardingTimeouts.idleConnTimeout`</a> | Maximum amount of time an idle (keep-alive) connection will remain idle before closing itself.<br />Zero means no timeout. | 90s | No |
|
||||
| <a id="opt-serverstransport-spiffe-ids" href="#opt-serverstransport-spiffe-ids" title="#opt-serverstransport-spiffe-ids">`serverstransport.`<br />`spiffe.ids`</a> | Allow SPIFFE IDs.<br />This takes precedence over the SPIFFE TrustDomain. | | No |
|
||||
| <a id="opt-serverstransport-spiffe-trustDomain" href="#opt-serverstransport-spiffe-trustDomain" title="#opt-serverstransport-spiffe-trustDomain">`serverstransport.`<br />`spiffe.trustDomain`</a> | Allow SPIFFE trust domain. | "" | No |
|
||||
| <a id="opt-serverstransport-serverName-2" href="#opt-serverstransport-serverName-2" title="#opt-serverstransport-serverName-2">`serverstransport.`<br />`serverName`</a> | Defines the server name that will be used for SNI. | | No |
|
||||
| <a id="opt-serverstransport-insecureSkipVerify-2" href="#opt-serverstransport-insecureSkipVerify-2" title="#opt-serverstransport-insecureSkipVerify-2">`serverstransport.`<br />`insecureSkipVerify`</a> | Controls whether the server's certificate chain and host name is verified. | false | No |
|
||||
| <a id="opt-serverstransport-rootcas-2" href="#opt-serverstransport-rootcas-2" title="#opt-serverstransport-rootcas-2">`serverstransport.`<br />`rootcas`</a> | Set of root certificate authorities to use when verifying server certificates. (for mTLS connections). | | No |
|
||||
| <a id="opt-serverstransport-certificatesSecrets-2" href="#opt-serverstransport-certificatesSecrets-2" title="#opt-serverstransport-certificatesSecrets-2">`serverstransport.`<br />`certificatesSecrets`</a> | Certificates to present to the server for mTLS. | | No |
|
||||
| <a id="opt-serverstransport-cipherSuites" href="#opt-serverstransport-cipherSuites" title="#opt-serverstransport-cipherSuites">`serverstransport.`<br />`cipherSuites`</a> | Defines the cipher suites to use when contacting backend servers. | [] | No |
|
||||
| <a id="opt-serverstransport-minVersion" href="#opt-serverstransport-minVersion" title="#opt-serverstransport-minVersion">`serverstransport.`<br />`minVersion`</a> | Defines the minimum TLS version to use when contacting backend servers. | "" | No |
|
||||
| <a id="opt-serverstransport-maxVersion" href="#opt-serverstransport-maxVersion" title="#opt-serverstransport-maxVersion">`serverstransport.`<br />`maxVersion`</a> | Defines the maximum TLS version to use when contacting backend servers. | "" | No |
|
||||
| <a id="opt-serverstransport-maxIdleConnsPerHost-2" href="#opt-serverstransport-maxIdleConnsPerHost-2" title="#opt-serverstransport-maxIdleConnsPerHost-2">`serverstransport.`<br />`maxIdleConnsPerHost`</a> | Maximum idle (keep-alive) connections to keep per-host. | 200 | No |
|
||||
| <a id="opt-serverstransport-disableHTTP2-2" href="#opt-serverstransport-disableHTTP2-2" title="#opt-serverstransport-disableHTTP2-2">`serverstransport.`<br />`disableHTTP2`</a> | Disables HTTP/2 for connections with servers. | false | No |
|
||||
| <a id="opt-serverstransport-peerCertURI-2" href="#opt-serverstransport-peerCertURI-2" title="#opt-serverstransport-peerCertURI-2">`serverstransport.`<br />`peerCertURI`</a> | Defines the URI used to match against SAN URIs during the server's certificate verification. | "" | No |
|
||||
| <a id="opt-serverstransport-forwardingTimeouts-dialTimeout-2" href="#opt-serverstransport-forwardingTimeouts-dialTimeout-2" title="#opt-serverstransport-forwardingTimeouts-dialTimeout-2">`serverstransport.`<br />`forwardingTimeouts.dialTimeout`</a> | Amount of time to wait until a connection to a server can be established.<br />Zero means no timeout. | 30s | No |
|
||||
| <a id="opt-serverstransport-forwardingTimeouts-responseHeaderTimeout-2" href="#opt-serverstransport-forwardingTimeouts-responseHeaderTimeout-2" title="#opt-serverstransport-forwardingTimeouts-responseHeaderTimeout-2">`serverstransport.`<br />`forwardingTimeouts.responseHeaderTimeout`</a> | Amount of time to wait for a server's response headers after fully writing the request (including its body, if any).<br />Zero means no timeout | 0s | No |
|
||||
| <a id="opt-serverstransport-forwardingTimeouts-idleConnTimeout-2" href="#opt-serverstransport-forwardingTimeouts-idleConnTimeout-2" title="#opt-serverstransport-forwardingTimeouts-idleConnTimeout-2">`serverstransport.`<br />`forwardingTimeouts.idleConnTimeout`</a> | Maximum amount of time an idle (keep-alive) connection will remain idle before closing itself.<br />Zero means no timeout. | 90s | No |
|
||||
| <a id="opt-serverstransport-spiffe-ids-2" href="#opt-serverstransport-spiffe-ids-2" title="#opt-serverstransport-spiffe-ids-2">`serverstransport.`<br />`spiffe.ids`</a> | Allow SPIFFE IDs.<br />This takes precedence over the SPIFFE TrustDomain. | | No |
|
||||
| <a id="opt-serverstransport-spiffe-trustDomain-2" href="#opt-serverstransport-spiffe-trustDomain-2" title="#opt-serverstransport-spiffe-trustDomain-2">`serverstransport.`<br />`spiffe.trustDomain`</a> | Allow SPIFFE trust domain. | "" | No |
|
||||
| Field | Description | Default | Required |
|
||||
|:--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:----------------------------------------------------------|:--------|:---------|
|
||||
| <a id="opt-serverstransport-serverName" href="#opt-serverstransport-serverName" title="#opt-serverstransport-serverName">`serverstransport.`<br />`serverName`</a> | Defines the server name that will be used for SNI. | | No |
|
||||
| <a id="opt-serverstransport-insecureSkipVerify" href="#opt-serverstransport-insecureSkipVerify" title="#opt-serverstransport-insecureSkipVerify">`serverstransport.`<br />`insecureSkipVerify`</a> | Controls whether the server's certificate chain and host name is verified. | false | No |
|
||||
| <a id="opt-serverstransport-rootcas" href="#opt-serverstransport-rootcas" title="#opt-serverstransport-rootcas">`serverstransport.`<br />`rootcas`</a> | Set of root certificate authorities to use when verifying server certificates. (for mTLS connections). | | No |
|
||||
| <a id="opt-serverstransport-certificatesSecrets" href="#opt-serverstransport-certificatesSecrets" title="#opt-serverstransport-certificatesSecrets">`serverstransport.`<br />`certificatesSecrets`</a> | Certificates to present to the server for mTLS. | | No |
|
||||
| <a id="opt-serverstransport-maxIdleConnsPerHost" href="#opt-serverstransport-maxIdleConnsPerHost" title="#opt-serverstransport-maxIdleConnsPerHost">`serverstransport.`<br />`maxIdleConnsPerHost`</a> | Maximum idle (keep-alive) connections to keep per-host. | 200 | No |
|
||||
| <a id="opt-serverstransport-disableHTTP2" href="#opt-serverstransport-disableHTTP2" title="#opt-serverstransport-disableHTTP2">`serverstransport.`<br />`disableHTTP2`</a> | Disables HTTP/2 for connections with servers. | false | No |
|
||||
| <a id="opt-serverstransport-peerCertSANs" href="#opt-serverstransport-peerCertSANs" title="#opt-serverstransport-peerCertSANs">`serverstransport.`<br />`peerCertSANs`</a> | Defines the SANs (Subject Alternative Names) used to match against SANs during the peer certificate verification. | [] | No |
|
||||
| <a id="opt-serverstransport-peerCertSANs-type" href="#opt-serverstransport-peerCertSANs-type" title="#opt-serverstransport-peerCertSANs-type">`serverstransport.`<br />`peerCertSANs[].type`</a> | Defines the SAN type (`URI` or `DNSName`) to match against the peer certificate's Subject Alternative Names. | "" | No |
|
||||
| <a id="opt-serverstransport-peerCertSANs-value" href="#opt-serverstransport-peerCertSANs-value" title="#opt-serverstransport-peerCertSANs-value">`serverstransport.`<br />`peerCertSANs[].value`</a> | Defines the SAN value to match against the peer certificate's Subject Alternative Names. | "" | No |
|
||||
| <a id="opt-serverstransport-forwardingTimeouts-dialTimeout" href="#opt-serverstransport-forwardingTimeouts-dialTimeout" title="#opt-serverstransport-forwardingTimeouts-dialTimeout">`serverstransport.`<br />`forwardingTimeouts.dialTimeout`</a> | Amount of time to wait until a connection to a server can be established.<br />Zero means no timeout. | 30s | No |
|
||||
| <a id="opt-serverstransport-forwardingTimeouts-responseHeaderTimeout" href="#opt-serverstransport-forwardingTimeouts-responseHeaderTimeout" title="#opt-serverstransport-forwardingTimeouts-responseHeaderTimeout">`serverstransport.`<br />`forwardingTimeouts.responseHeaderTimeout`</a> | Amount of time to wait for a server's response headers after fully writing the request (including its body, if any).<br />Zero means no timeout | 0s | No |
|
||||
| <a id="opt-serverstransport-forwardingTimeouts-idleConnTimeout" href="#opt-serverstransport-forwardingTimeouts-idleConnTimeout" title="#opt-serverstransport-forwardingTimeouts-idleConnTimeout">`serverstransport.`<br />`forwardingTimeouts.idleConnTimeout`</a> | Maximum amount of time an idle (keep-alive) connection will remain idle before closing itself.<br />Zero means no timeout. | 90s | No |
|
||||
| <a id="opt-serverstransport-spiffe-ids" href="#opt-serverstransport-spiffe-ids" title="#opt-serverstransport-spiffe-ids">`serverstransport.`<br />`spiffe.ids`</a> | Allow SPIFFE IDs.<br />This takes precedence over the SPIFFE TrustDomain. | | No |
|
||||
| <a id="opt-serverstransport-spiffe-trustDomain" href="#opt-serverstransport-spiffe-trustDomain" title="#opt-serverstransport-spiffe-trustDomain">`serverstransport.`<br />`spiffe.trustDomain`</a> | Allow SPIFFE trust domain. | "" | No |
|
||||
| <a id="opt-serverstransport-cipherSuites" href="#opt-serverstransport-cipherSuites" title="#opt-serverstransport-cipherSuites">`serverstransport.`<br />`cipherSuites`</a> | Defines the cipher suites to use when contacting backend servers. | [] | No |
|
||||
| <a id="opt-serverstransport-minVersion" href="#opt-serverstransport-minVersion" title="#opt-serverstransport-minVersion">`serverstransport.`<br />`minVersion`</a> | Defines the minimum TLS version to use when contacting backend servers. | "" | No |
|
||||
| <a id="opt-serverstransport-maxVersion" href="#opt-serverstransport-maxVersion" title="#opt-serverstransport-maxVersion">`serverstransport.`<br />`maxVersion`</a> | Defines the maximum TLS version to use when contacting backend servers. | "" | No |
|
||||
|
||||
!!! note "CA Secret"
|
||||
The CA secret must contain a base64 encoded certificate under either a tls.ca or a ca.crt key.
|
||||
|
||||
+3
-1
@@ -48,7 +48,9 @@ spec:
|
||||
| <a id="opt-terminationDelay" href="#opt-terminationDelay" title="#opt-terminationDelay">`terminationDelay`</a> | Defines the delay to wait before fully terminating the connection, after one connected peer has closed its writing capability. | 100ms | No |
|
||||
| <a id="opt-tls-serverName" href="#opt-tls-serverName" title="#opt-tls-serverName">`tls.serverName`</a> | ServerName used to contact the server. | "" | No |
|
||||
| <a id="opt-tls-insecureSkipVerify" href="#opt-tls-insecureSkipVerify" title="#opt-tls-insecureSkipVerify">`tls.insecureSkipVerify`</a> | Controls whether the server's certificate chain and host name is verified. | false | No |
|
||||
| <a id="opt-tls-peerCertURI" href="#opt-tls-peerCertURI" title="#opt-tls-peerCertURI">`tls.peerCertURI`</a> | Defines the URI used to match against SAN URIs during the server's certificate verification. | "" | No |
|
||||
| <a id="opt-tls-peerCertSANs" href="#opt-tls-peerCertSANs" title="#opt-tls-peerCertSANs">`tls.peerCertSANs`</a> | Defines the SANs (Subject Alternative Names) used to match against SANs during the peer certificate verification. | [] | No |
|
||||
| <a id="opt-tls-peerCertSANs-type" href="#opt-tls-peerCertSANs-type" title="#opt-tls-peerCertSANs-type">`tls.peerCertSANs[].type`</a> | Defines the SAN type (`URI` or `DNSName`) to match against the peer certificate's Subject Alternative Names. | "" | No |
|
||||
| <a id="opt-tls-peerCertSANs-value" href="#opt-tls-peerCertSANs-value" title="#opt-tls-peerCertSANs-value">`tls.peerCertSANs[].value`</a> | Defines the SAN value to match against the peer certificate's Subject Alternative Names. | "" | No |
|
||||
| <a id="opt-tls-rootCAsSecrets" href="#opt-tls-rootCAsSecrets" title="#opt-tls-rootCAsSecrets">`tls.rootCAsSecrets`</a> | Defines the set of root certificate authorities to use when verifying server certificates.<br />The CA secret must contain a base64 encoded certificate under either a `tls.ca` or a `ca.crt` key. | "" | No |
|
||||
| <a id="opt-tls-certificatesSecrets" href="#opt-tls-certificatesSecrets" title="#opt-tls-certificatesSecrets">`tls.certificatesSecrets`</a> | Certificates to present to the server for mTLS. | "" | No |
|
||||
| <a id="opt-spiffe" href="#opt-spiffe" title="#opt-spiffe">`spiffe`</a> | Configures [SPIFFE](../../../../install-configuration/tls/spiffe.md) options. | "" | No |
|
||||
|
||||
@@ -41,6 +41,7 @@ creating the corresponding routers, services, middlewares, and other components
|
||||
Important differences in default behaviors:
|
||||
|
||||
- **Request buffering**: NGINX enables `proxy-request-buffering` by default, while Traefik requires explicit opt-in via the provider's `proxyRequestBuffering` option.
|
||||
- **Legacy scheme headers**: If your applications depend on `X-Forwarded-Scheme` or `X-Scheme`, enable `entryPoints.<name>.forwardedHeaders.addXForwardedSchemeHeaders=true` on the relevant entrypoints.
|
||||
|
||||
To ensure consistent behavior during migration,
|
||||
review and configure Traefik's provider-level options to match your current NGINX ConfigMap settings.
|
||||
|
||||
@@ -24,7 +24,11 @@ tcp:
|
||||
insecureSkipVerify: true
|
||||
rootcas:
|
||||
- "/path/to/rootca.pem"
|
||||
peerCertURI: "spiffe://example.org/peer"
|
||||
peerCertSANs:
|
||||
- type: DNSName
|
||||
value: foo.com
|
||||
- type: URI
|
||||
value: spiffe://example.org/peer
|
||||
spiffe:
|
||||
ids:
|
||||
- "spiffe://example.org/id1"
|
||||
@@ -43,7 +47,7 @@ tcp:
|
||||
certificates = ["/path/to/cert1.pem", "/path/to/cert2.pem"]
|
||||
insecureSkipVerify = true
|
||||
rootcas = ["/path/to/rootca.pem"]
|
||||
peerCertURI = "spiffe://example.org/peer"
|
||||
peerCertSANs = [{type = "DNSName", value = "foo.com"}, {type = "URI", value = "spiffe://example.org/peer"}]
|
||||
|
||||
[tcp.serversTransports.mytransport.spiffe]
|
||||
ids = ["spiffe://example.org/id1", "spiffe://example.org/id2"]
|
||||
@@ -96,7 +100,9 @@ labels:
|
||||
| <a id="opt-serverstransport-tls-certificates" href="#opt-serverstransport-tls-certificates" title="#opt-serverstransport-tls-certificates">`serverstransport.`<br />`tls`<br />`.certificates`</a> | Defines the list of certificates (as file paths, or data bytes) that will be set as client certificates for mTLS. | | No |
|
||||
| <a id="opt-serverstransport-tls-insecureSkipVerify" href="#opt-serverstransport-tls-insecureSkipVerify" title="#opt-serverstransport-tls-insecureSkipVerify">`serverstransport.`<br />`tls`<br />`.insecureSkipVerify`</a> | Controls whether the server's certificate chain and host name is verified. | false | No |
|
||||
| <a id="opt-serverstransport-tls-rootcas" href="#opt-serverstransport-tls-rootcas" title="#opt-serverstransport-tls-rootcas">`serverstransport.`<br />`tls`<br />`.rootcas`</a> | Defines the root certificate authorities to use when verifying server certificates. (for mTLS connections). | | No |
|
||||
| <a id="opt-serverstransport-tls-peerCertURI" href="#opt-serverstransport-tls-peerCertURI" title="#opt-serverstransport-tls-peerCertURI">`serverstransport.`<br />`tls.`<br />`peerCertURI`</a> | Defines the URI used to match against SAN URIs during the server's certificate verification. | false | No |
|
||||
| <a id="opt-serverstransport-tls-peerCertSANs" href="#opt-serverstransport-tls-peerCertSANs" title="#opt-serverstransport-tls-peerCertSANs">`serverstransport.`<br />`tls.`<br />`peerCertSANs`</a> | Defines the SANs (Subject Alternative Names) used to match against SANs during the peer certificate verification. | [] | No |
|
||||
| <a id="opt-serverstransport-tls-peerCertSANs-type" href="#opt-serverstransport-tls-peerCertSANs-type" title="#opt-serverstransport-tls-peerCertSANs-type">`serverstransport.`<br />`tls.`<br />`peerCertSANs[].type`</a> | Defines the SAN type (`URI` or `DNSName`) to match against the peer certificate's Subject Alternative Names. | "" | No |
|
||||
| <a id="opt-serverstransport-tls-peerCertSANs-value" href="#opt-serverstransport-tls-peerCertSANs-value" title="#opt-serverstransport-tls-peerCertSANs-value">`serverstransport.`<br />`tls.`<br />`peerCertSANs[].value`</a> | Defines the SAN value to match against the peer certificate's Subject Alternative Names. | "" | No |
|
||||
| <a id="opt-serverstransport-spiffe" href="#opt-serverstransport-spiffe" title="#opt-serverstransport-spiffe">`serverstransport.`<br />`spiffe`</a> | Defines the SPIFFE configuration. An empty `spiffe` section enables SPIFFE (that allows any SPIFFE ID). | | No |
|
||||
| <a id="opt-serverstransport-spiffe-ids" href="#opt-serverstransport-spiffe-ids" title="#opt-serverstransport-spiffe-ids">`serverstransport.`<br />`spiffe`<br />`.ids`</a> | Allow SPIFFE IDs.<br />This takes precedence over the SPIFFE TrustDomain. | | No |
|
||||
| <a id="opt-serverstransport-spiffe-trustDomain" href="#opt-serverstransport-spiffe-trustDomain" title="#opt-serverstransport-spiffe-trustDomain">`serverstransport.`<br />`spiffe`<br />`.trustDomain`</a> | Allow SPIFFE trust domain. | "" | No |
|
||||
|
||||
@@ -50,6 +50,7 @@
|
||||
insecure = true
|
||||
trustedIPs = ["foobar", "foobar"]
|
||||
connection = ["foobar", "foobar"]
|
||||
addXForwardedSchemeHeaders = true
|
||||
[entryPoints.EntryPoint0.http]
|
||||
middlewares = ["foobar", "foobar"]
|
||||
encodeQuerySemicolons = true
|
||||
|
||||
@@ -61,6 +61,7 @@ entryPoints:
|
||||
connection:
|
||||
- foobar
|
||||
- foobar
|
||||
addXForwardedSchemeHeaders: true
|
||||
http:
|
||||
redirections:
|
||||
entryPoint:
|
||||
|
||||
Generated
+7
-44
@@ -1,37 +1,16 @@
|
||||
{
|
||||
"nodes": {
|
||||
"flake-utils": {
|
||||
"inputs": {
|
||||
"systems": "systems"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1731533236,
|
||||
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1772963539,
|
||||
"narHash": "sha256-9jVDGZnvCckTGdYT53d/EfznygLskyLQXYwJLKMPsZs=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "9dcb002ca1690658be4a04645215baea8b95f31d",
|
||||
"type": "github"
|
||||
"lastModified": 1778036283,
|
||||
"narHash": "sha256-GW2cEd/cLcVbbCes8iQuoY2qGIeCA7UiaD351hpkXfI=",
|
||||
"rev": "ed67bc86e84e51d4a88e73c7fd36006dc876476f",
|
||||
"type": "tarball",
|
||||
"url": "https://releases.nixos.org/nixpkgs/nixpkgs-26.05pre993032.ed67bc86e84e/nixexprs.tar.xz"
|
||||
},
|
||||
"original": {
|
||||
"owner": "NixOS",
|
||||
"ref": "nixos-unstable",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
"type": "tarball",
|
||||
"url": "https://channels.nixos.org/nixpkgs-unstable/nixexprs.tar.xz"
|
||||
}
|
||||
},
|
||||
"nixpkgs-golangci": {
|
||||
@@ -68,26 +47,10 @@
|
||||
},
|
||||
"root": {
|
||||
"inputs": {
|
||||
"flake-utils": "flake-utils",
|
||||
"nixpkgs": "nixpkgs",
|
||||
"nixpkgs-golangci": "nixpkgs-golangci",
|
||||
"nixpkgs-kct": "nixpkgs-kct"
|
||||
}
|
||||
},
|
||||
"systems": {
|
||||
"locked": {
|
||||
"lastModified": 1681028828,
|
||||
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "nix-systems",
|
||||
"repo": "default",
|
||||
"type": "github"
|
||||
}
|
||||
}
|
||||
},
|
||||
"root": "root",
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
inputs = {
|
||||
# Main nixpkgs (used for gnused)
|
||||
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
|
||||
nixpkgs.url = "https://channels.nixos.org/nixpkgs-unstable/nixexprs.tar.xz";
|
||||
|
||||
# Pinned nixpkgs for kubernetes-controller-tools
|
||||
# Search: https://www.nixhub.io/packages/kubernetes-controller-tools
|
||||
@@ -12,33 +12,34 @@
|
||||
# Pinned nixpkgs for golangci-lint
|
||||
# Search: https://www.nixhub.io/packages/golangci-lint
|
||||
nixpkgs-golangci.url = "github:NixOS/nixpkgs/80d901ec0377e19ac3f7bb8c035201e2e098cc97";
|
||||
|
||||
flake-utils.url = "github:numtide/flake-utils";
|
||||
};
|
||||
|
||||
outputs = { self, nixpkgs, nixpkgs-kct, nixpkgs-golangci, flake-utils }:
|
||||
flake-utils.lib.eachDefaultSystem (system:
|
||||
let
|
||||
pkgs = import nixpkgs {
|
||||
inherit system;
|
||||
};
|
||||
outputs =
|
||||
{
|
||||
nixpkgs,
|
||||
nixpkgs-kct,
|
||||
nixpkgs-golangci,
|
||||
...
|
||||
}:
|
||||
let
|
||||
inherit (nixpkgs.lib) genAttrs;
|
||||
forEachSystem = genAttrs nixpkgs.lib.systems.flakeExposed;
|
||||
|
||||
pkgs-kct = import nixpkgs-kct {
|
||||
inherit system;
|
||||
};
|
||||
|
||||
pkgs-golangci = import nixpkgs-golangci {
|
||||
inherit system;
|
||||
};
|
||||
in
|
||||
{
|
||||
devShells.default = pkgs.mkShell {
|
||||
pkgsForEach = nixpkgs.legacyPackages;
|
||||
pkgsKctForEach = nixpkgs-kct.legacyPackages;
|
||||
pkgsGolangCiForEach = nixpkgs-golangci.legacyPackages;
|
||||
in
|
||||
{
|
||||
devShells = forEachSystem (system: {
|
||||
default = pkgsForEach.${system}.mkShell {
|
||||
packages = [
|
||||
pkgs-kct.kubernetes-controller-tools
|
||||
pkgs.gnused
|
||||
pkgs-golangci.golangci-lint
|
||||
pkgsForEach.${system}.gnused
|
||||
pkgsKctForEach.${system}.kubernetes-controller-tools
|
||||
pkgsGolangCiForEach.${system}.golangci-lint
|
||||
];
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
formatter = forEachSystem (system: pkgsForEach.${system}.nixfmt);
|
||||
};
|
||||
}
|
||||
|
||||
@@ -53,7 +53,7 @@ require (
|
||||
github.com/moby/moby/api v1.54.1
|
||||
github.com/moby/moby/client v0.4.0
|
||||
github.com/patrickmn/go-cache v2.1.0+incompatible
|
||||
github.com/pires/go-proxyproto v0.8.1
|
||||
github.com/pires/go-proxyproto v0.12.0
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // No tag on the repo.
|
||||
github.com/prometheus/client_golang v1.23.2
|
||||
github.com/prometheus/client_model v0.6.2
|
||||
|
||||
@@ -1748,8 +1748,8 @@ github.com/phpdave11/gofpdi v1.0.13/go.mod h1:vBmVV0Do6hSBHC8uKUQ71JGW+ZGQq74llk
|
||||
github.com/pierrec/lz4 v1.0.2-0.20190131084431-473cd7ce01a1/go.mod h1:3/3N9NVKO0jef7pBehbT1qWhCMrIgbYNnFAZCqQ5LRc=
|
||||
github.com/pierrec/lz4 v2.6.1+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY=
|
||||
github.com/pierrec/lz4/v4 v4.1.15/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
|
||||
github.com/pires/go-proxyproto v0.8.1 h1:9KEixbdJfhrbtjpz/ZwCdWDD2Xem0NZ38qMYaASJgp0=
|
||||
github.com/pires/go-proxyproto v0.8.1/go.mod h1:ZKAAyp3cgy5Y5Mo4n9AlScrkCZwUy0g3Jf+slqQVcuU=
|
||||
github.com/pires/go-proxyproto v0.12.0 h1:TTCxD66dU898tahivkqc3hoceZp7P44FnorWyo9d5vM=
|
||||
github.com/pires/go-proxyproto v0.12.0/go.mod h1:qUvfqUMEoX7T8g0q7TQLDnhMjdTrxnG0hvpMn+7ePNI=
|
||||
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
|
||||
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
|
||||
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
[global]
|
||||
checkNewVersion = false
|
||||
sendAnonymousUsage = false
|
||||
|
||||
[log]
|
||||
level = "DEBUG"
|
||||
|
||||
[entryPoints.websecure]
|
||||
address = ":4443"
|
||||
|
||||
[entryPoints.websecure2]
|
||||
address = ":4444"
|
||||
|
||||
[api]
|
||||
insecure = true
|
||||
|
||||
[providers.file]
|
||||
filename = "{{ .SelfFilename }}"
|
||||
|
||||
## dynamic configuration ##
|
||||
|
||||
# --- Same host, same options, same entryPoint: no conflict, the options are applied. ---
|
||||
[http.routers.same-1]
|
||||
rule = "Host(`same.www.snitest.com`)"
|
||||
entryPoints = ["websecure"]
|
||||
service = "service1"
|
||||
[http.routers.same-1.tls]
|
||||
options = "tls12"
|
||||
|
||||
[http.routers.same-2]
|
||||
rule = "Host(`same.www.snitest.com`) && PathPrefix(`/same`)"
|
||||
entryPoints = ["websecure"]
|
||||
service = "service1"
|
||||
[http.routers.same-2.tls]
|
||||
options = "tls12"
|
||||
|
||||
# --- Same host, different options, same entryPoint: conflict, fallback to default options. ---
|
||||
[http.routers.conflict-1]
|
||||
rule = "Host(`conflict.www.snitest.com`)"
|
||||
entryPoints = ["websecure"]
|
||||
service = "service1"
|
||||
[http.routers.conflict-1.tls]
|
||||
options = "tls12"
|
||||
|
||||
[http.routers.conflict-2]
|
||||
rule = "Host(`conflict.www.snitest.com`) && PathPrefix(`/conflict`)"
|
||||
entryPoints = ["websecure"]
|
||||
service = "service1"
|
||||
[http.routers.conflict-2.tls]
|
||||
options = "tls13"
|
||||
|
||||
# --- Same host, different options, different entryPoints: no conflict, each entryPoint keeps its own options. ---
|
||||
[http.routers.cross-ep1]
|
||||
rule = "Host(`cross.www.snitest.com`)"
|
||||
entryPoints = ["websecure"]
|
||||
service = "service1"
|
||||
[http.routers.cross-ep1.tls]
|
||||
options = "tls12"
|
||||
|
||||
[http.routers.cross-ep2]
|
||||
rule = "Host(`cross.www.snitest.com`)"
|
||||
entryPoints = ["websecure2"]
|
||||
service = "service1"
|
||||
[http.routers.cross-ep2.tls]
|
||||
options = "tls13"
|
||||
|
||||
# --- Domain fronting (Host header != SNI): same options follow the header, different options are rejected. ---
|
||||
[http.routers.df-a]
|
||||
rule = "Host(`df-a.www.snitest.com`)"
|
||||
entryPoints = ["websecure"]
|
||||
service = "service1"
|
||||
[http.routers.df-a.tls]
|
||||
options = "tls12"
|
||||
|
||||
[http.routers.df-b]
|
||||
rule = "Host(`df-b.www.snitest.com`)"
|
||||
entryPoints = ["websecure"]
|
||||
service = "service1"
|
||||
[http.routers.df-b.tls]
|
||||
options = "tls12"
|
||||
|
||||
[http.routers.df-c]
|
||||
rule = "Host(`df-c.www.snitest.com`)"
|
||||
entryPoints = ["websecure"]
|
||||
service = "service1"
|
||||
[http.routers.df-c.tls]
|
||||
options = "tls13"
|
||||
|
||||
[http.services.service1]
|
||||
[[http.services.service1.loadBalancer.servers]]
|
||||
url = "http://127.0.0.1:9010"
|
||||
|
||||
[[tls.certificates]]
|
||||
certFile = "fixtures/https/wildcard.www.snitest.com.cert"
|
||||
keyFile = "fixtures/https/wildcard.www.snitest.com.key"
|
||||
|
||||
[tls.options]
|
||||
[tls.options.tls12]
|
||||
maxVersion = "VersionTLS12"
|
||||
[tls.options.tls13]
|
||||
minVersion = "VersionTLS13"
|
||||
@@ -2494,9 +2494,25 @@ spec:
|
||||
description: MinVersion defines the minimum TLS version to use when
|
||||
contacting backend servers.
|
||||
type: string
|
||||
peerCertSANs:
|
||||
description: PeerCertSANs defines the peer cert Subject Alternative
|
||||
Names used to match against SAN during the peer certificate verification.
|
||||
items:
|
||||
description: SAN represents a Subject Alternative Name.
|
||||
properties:
|
||||
type:
|
||||
description: SANType is the type of the Subject Alternative
|
||||
Name.
|
||||
type: string
|
||||
value:
|
||||
type: string
|
||||
type: object
|
||||
type: array
|
||||
peerCertURI:
|
||||
description: PeerCertURI defines the peer cert URI used to match against
|
||||
SAN URI during the peer certificate verification.
|
||||
description: |-
|
||||
PeerCertURI defines the peer cert URI used to match against SAN URI during the peer certificate verification.
|
||||
|
||||
Deprecated: PeerCertURI is deprecated, please use the PeerCertSANs option instead.
|
||||
type: string
|
||||
rootCAs:
|
||||
description: RootCAs defines a list of CA certificate Secrets or ConfigMaps
|
||||
@@ -2648,10 +2664,26 @@ spec:
|
||||
insecureSkipVerify:
|
||||
description: InsecureSkipVerify disables TLS certificate verification.
|
||||
type: boolean
|
||||
peerCertSANs:
|
||||
description: PeerCertSANs defines the peer cert Subject Alternative
|
||||
Names used to match against SAN during the peer certificate
|
||||
verification.
|
||||
items:
|
||||
description: SAN represents a Subject Alternative Name.
|
||||
properties:
|
||||
type:
|
||||
description: SANType is the type of the Subject Alternative
|
||||
Name.
|
||||
type: string
|
||||
value:
|
||||
type: string
|
||||
type: object
|
||||
type: array
|
||||
peerCertURI:
|
||||
description: |-
|
||||
MaxIdleConnsPerHost controls the maximum idle (keep-alive) to keep per-host.
|
||||
PeerCertURI defines the peer cert URI used to match against SAN URI during the peer certificate verification.
|
||||
|
||||
Deprecated: PeerCertURI is deprecated, please use the PeerCertSANs option instead.
|
||||
type: string
|
||||
rootCAs:
|
||||
description: RootCAs defines a list of CA certificate Secrets
|
||||
|
||||
+2
-2
@@ -30,10 +30,11 @@ profiles:
|
||||
result: success
|
||||
statistics:
|
||||
Failed: 0
|
||||
Passed: 20
|
||||
Passed: 21
|
||||
Skipped: 0
|
||||
supportedFeatures:
|
||||
- BackendTLSPolicy
|
||||
- BackendTLSPolicySANValidation
|
||||
- GatewayPort8080
|
||||
- HTTPRouteBackendProtocolH2C
|
||||
- HTTPRouteBackendProtocolWebSocket
|
||||
@@ -48,7 +49,6 @@ profiles:
|
||||
- HTTPRouteResponseHeaderModification
|
||||
- HTTPRouteSchemeRedirect
|
||||
unsupportedFeatures:
|
||||
- BackendTLSPolicySANValidation
|
||||
- GatewayAddressEmpty
|
||||
- GatewayBackendClientCertificate
|
||||
- GatewayFrontendClientCertificateValidation
|
||||
|
||||
+148
-1
@@ -415,7 +415,7 @@ func (s *HTTPSSuite) TestWithConflictingTLSOptions() {
|
||||
assert.ErrorContains(s.T(), err, "tls: no supported versions satisfy MinVersion and MaxVersion")
|
||||
|
||||
// with unknown tls option
|
||||
err = try.GetRequest("http://127.0.0.1:8080/api/rawdata", 1*time.Second, try.BodyContains("found different TLS options for routers on the same host, so using the default TLS options instead"))
|
||||
err = try.GetRequest("http://127.0.0.1:8080/api/rawdata", 1*time.Second, try.BodyContains("router's TLSOptions configuration is conflicting with other routers on the same entrypoint and host, default TLS options will be used instead"))
|
||||
require.NoError(s.T(), err)
|
||||
}
|
||||
|
||||
@@ -1262,6 +1262,153 @@ func (s *HTTPSSuite) TestWithDomainFronting() {
|
||||
}
|
||||
}
|
||||
|
||||
// TestWithTLSOptionsConflict checks how TLS options are resolved when several routers
|
||||
// target the same host (SNI), across the different conflict situations:
|
||||
// - same options on the same entryPoint: no conflict, the options are applied;
|
||||
// - different options on the same entryPoint: conflict, fallback to the default options;
|
||||
// - different options on different entryPoints: no conflict, each entryPoint keeps its
|
||||
// own options (they are selected independently on each listener);
|
||||
// - domain fronting (Host header != SNI): allowed when both resolve to the same options,
|
||||
// rejected with a 421 otherwise.
|
||||
//
|
||||
// The effective TLS options are probed through the negotiated TLS version: the "tls12"
|
||||
// options cap the version to TLS 1.2, while the "tls13" options require at least TLS 1.3.
|
||||
func (s *HTTPSSuite) TestWithTLSOptionsConflict() {
|
||||
backend := startTestServer("9010", http.StatusOK, "server1")
|
||||
defer backend.Close()
|
||||
|
||||
file := s.adaptFile("fixtures/https/https_tls_options_conflict.toml", struct{}{})
|
||||
s.traefikCmd(withConfigFile(file))
|
||||
|
||||
// wait for Traefik
|
||||
err := try.GetRequest("http://127.0.0.1:8080/api/rawdata", 1*time.Second, try.BodyContains("Host(`cross.www.snitest.com`)"))
|
||||
require.NoError(s.T(), err)
|
||||
|
||||
testCases := []struct {
|
||||
desc string
|
||||
addr string // entryPoint address to reach
|
||||
hostHeader string
|
||||
serverName string // SNI
|
||||
minVersion uint16 // 0 means the crypto/tls library default
|
||||
maxVersion uint16 // 0 means the crypto/tls library default
|
||||
// expectHandshakeError is set when the TLS handshake itself is expected to fail
|
||||
// (i.e. the probed options reject the client's TLS version). Otherwise
|
||||
// expectedStatusCode is asserted on the HTTP response.
|
||||
expectHandshakeError bool
|
||||
expectedStatusCode int
|
||||
}{
|
||||
// Same host, same options, same entryPoint: no conflict, the "tls12" options are applied.
|
||||
{
|
||||
desc: "same options / same entryPoint: TLS 1.2 client is accepted",
|
||||
addr: "127.0.0.1:4443",
|
||||
hostHeader: "same.www.snitest.com",
|
||||
serverName: "same.www.snitest.com",
|
||||
maxVersion: tls.VersionTLS12,
|
||||
expectedStatusCode: http.StatusOK,
|
||||
},
|
||||
{
|
||||
desc: "same options / same entryPoint: TLS 1.3 client is rejected (maxVersion TLS1.2 enforced)",
|
||||
addr: "127.0.0.1:4443",
|
||||
hostHeader: "same.www.snitest.com",
|
||||
serverName: "same.www.snitest.com",
|
||||
minVersion: tls.VersionTLS13,
|
||||
expectHandshakeError: true,
|
||||
},
|
||||
|
||||
// Same host, different options, same entryPoint: conflict, both routers fall back to the default options.
|
||||
{
|
||||
desc: "conflicting options / same entryPoint: TLS 1.3 client is accepted (default options used)",
|
||||
addr: "127.0.0.1:4443",
|
||||
hostHeader: "conflict.www.snitest.com",
|
||||
serverName: "conflict.www.snitest.com",
|
||||
minVersion: tls.VersionTLS13,
|
||||
expectedStatusCode: http.StatusOK,
|
||||
},
|
||||
{
|
||||
desc: "conflicting options / same entryPoint: TLS 1.2 client is accepted (default options used)",
|
||||
addr: "127.0.0.1:4443",
|
||||
hostHeader: "conflict.www.snitest.com",
|
||||
serverName: "conflict.www.snitest.com",
|
||||
maxVersion: tls.VersionTLS12,
|
||||
expectedStatusCode: http.StatusOK,
|
||||
},
|
||||
|
||||
// Same host, different options, different entryPoints: no conflict, each entryPoint keeps its own options.
|
||||
{
|
||||
desc: "different entryPoints: websecure keeps tls12, TLS 1.2 client is accepted",
|
||||
addr: "127.0.0.1:4443",
|
||||
hostHeader: "cross.www.snitest.com",
|
||||
serverName: "cross.www.snitest.com",
|
||||
maxVersion: tls.VersionTLS12,
|
||||
expectedStatusCode: http.StatusOK,
|
||||
},
|
||||
{
|
||||
desc: "different entryPoints: websecure keeps tls12, TLS 1.3 client is rejected",
|
||||
addr: "127.0.0.1:4443",
|
||||
hostHeader: "cross.www.snitest.com",
|
||||
serverName: "cross.www.snitest.com",
|
||||
minVersion: tls.VersionTLS13,
|
||||
expectHandshakeError: true,
|
||||
},
|
||||
{
|
||||
desc: "different entryPoints: websecure2 keeps tls13, TLS 1.3 client is accepted",
|
||||
addr: "127.0.0.1:4444",
|
||||
hostHeader: "cross.www.snitest.com",
|
||||
serverName: "cross.www.snitest.com",
|
||||
minVersion: tls.VersionTLS13,
|
||||
expectedStatusCode: http.StatusOK,
|
||||
},
|
||||
{
|
||||
desc: "different entryPoints: websecure2 keeps tls13, TLS 1.2 client is rejected",
|
||||
addr: "127.0.0.1:4444",
|
||||
hostHeader: "cross.www.snitest.com",
|
||||
serverName: "cross.www.snitest.com",
|
||||
maxVersion: tls.VersionTLS12,
|
||||
expectHandshakeError: true,
|
||||
},
|
||||
|
||||
// Domain fronting (Host header != SNI) on the same entryPoint.
|
||||
{
|
||||
desc: "domain fronting / same options: request follows the Host header (200)",
|
||||
addr: "127.0.0.1:4443",
|
||||
hostHeader: "df-a.www.snitest.com",
|
||||
serverName: "df-b.www.snitest.com",
|
||||
maxVersion: tls.VersionTLS12,
|
||||
expectedStatusCode: http.StatusOK,
|
||||
},
|
||||
{
|
||||
desc: "domain fronting / different options: request is misdirected (421)",
|
||||
addr: "127.0.0.1:4443",
|
||||
hostHeader: "df-a.www.snitest.com",
|
||||
serverName: "df-c.www.snitest.com",
|
||||
minVersion: tls.VersionTLS13,
|
||||
expectedStatusCode: http.StatusMisdirectedRequest,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range testCases {
|
||||
tlsConfig := &tls.Config{
|
||||
InsecureSkipVerify: true,
|
||||
ServerName: test.serverName,
|
||||
MinVersion: test.minVersion,
|
||||
MaxVersion: test.maxVersion,
|
||||
}
|
||||
|
||||
req, err := http.NewRequest(http.MethodGet, "https://"+test.addr+"/", nil)
|
||||
require.NoError(s.T(), err)
|
||||
req.Host = test.hostHeader
|
||||
|
||||
if test.expectHandshakeError {
|
||||
_, err = (&http.Client{Transport: &http.Transport{TLSClientConfig: tlsConfig}}).Do(req)
|
||||
assert.ErrorContains(s.T(), err, "tls:", "test %q should fail the TLS handshake", test.desc)
|
||||
continue
|
||||
}
|
||||
|
||||
err = try.RequestWithTransport(req, 2*time.Second, &http.Transport{TLSClientConfig: tlsConfig}, try.StatusCodeIs(test.expectedStatusCode))
|
||||
assert.NoError(s.T(), err, "test %q failed with: %v", test.desc, err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestWithInvalidTLSOption verifies the behavior when using an invalid tlsOption configuration.
|
||||
func (s *HTTPSSuite) TestWithInvalidTLSOption() {
|
||||
backend := startTestServer("9010", http.StatusOK, "server1")
|
||||
|
||||
@@ -949,7 +949,7 @@ func (s *SimpleSuite) TestRouterConfigErrors() {
|
||||
s.traefikCmd(withConfigFile(file))
|
||||
|
||||
// All errors
|
||||
err := try.GetRequest("http://127.0.0.1:8080/api/http/routers", 1000*time.Millisecond, try.BodyContains(`["middleware \"unknown@file\" does not exist","found different TLS options for routers on the same host, so using the default TLS options instead"]`))
|
||||
err := try.GetRequest("http://127.0.0.1:8080/api/http/routers", 1000*time.Millisecond, try.BodyContains(`["middleware \"unknown@file\" does not exist","router's TLSOptions configuration is conflicting with other routers on the same entrypoint and host, default TLS options will be used instead"]`))
|
||||
require.NoError(s.T(), err)
|
||||
|
||||
// router3 has an error because it uses an unknown entrypoint
|
||||
@@ -957,11 +957,11 @@ func (s *SimpleSuite) TestRouterConfigErrors() {
|
||||
require.NoError(s.T(), err)
|
||||
|
||||
// router4 is enabled, but in warning state because its tls options conf was messed up
|
||||
err = try.GetRequest("http://127.0.0.1:8080/api/http/routers/router4@file", 1000*time.Millisecond, try.BodyContains(`"status":"warning"`))
|
||||
err = try.GetRequest("http://127.0.0.1:8080/api/http/routers/websecure-conflicted-router4@file", 1000*time.Millisecond, try.BodyContains(`"status":"warning"`))
|
||||
require.NoError(s.T(), err)
|
||||
|
||||
// router5 is disabled because its middleware conf is broken
|
||||
err = try.GetRequest("http://127.0.0.1:8080/api/http/routers/router5@file", 1000*time.Millisecond, try.BodyContains())
|
||||
err = try.GetRequest("http://127.0.0.1:8080/api/http/routers/websecure-conflicted-router5@file", 1000*time.Millisecond, try.BodyContains())
|
||||
require.NoError(s.T(), err)
|
||||
}
|
||||
|
||||
|
||||
@@ -537,8 +537,10 @@ type ServersTransport struct {
|
||||
MaxIdleConnsPerHost int `description:"If non-zero, controls the maximum idle (keep-alive) to keep per-host. If zero, DefaultMaxIdleConnsPerHost is used. If negative, disables connection reuse." json:"maxIdleConnsPerHost,omitempty" toml:"maxIdleConnsPerHost,omitempty" yaml:"maxIdleConnsPerHost,omitempty" export:"true"`
|
||||
ForwardingTimeouts *ForwardingTimeouts `description:"Defines the timeouts for requests forwarded to the backend servers." json:"forwardingTimeouts,omitempty" toml:"forwardingTimeouts,omitempty" yaml:"forwardingTimeouts,omitempty" export:"true"`
|
||||
DisableHTTP2 bool `description:"Disables HTTP/2 for connections with backend servers." json:"disableHTTP2,omitempty" toml:"disableHTTP2,omitempty" yaml:"disableHTTP2,omitempty" export:"true"`
|
||||
PeerCertURI string `description:"Defines the URI used to match against SAN URI during the peer certificate verification." json:"peerCertURI,omitempty" toml:"peerCertURI,omitempty" yaml:"peerCertURI,omitempty" export:"true"`
|
||||
Spiffe *Spiffe `description:"Defines the SPIFFE configuration." json:"spiffe,omitempty" toml:"spiffe,omitempty" yaml:"spiffe,omitempty" label:"allowEmpty" file:"allowEmpty" export:"true"`
|
||||
// Deprecated: PeerCertURI is deprecated, please use the PeerCertSANs option instead.
|
||||
PeerCertURI string `description:"Defines the URI used to match against SAN URI during the peer certificate verification." json:"peerCertURI,omitempty" toml:"peerCertURI,omitempty" yaml:"peerCertURI,omitempty"`
|
||||
PeerCertSANs []traefiktls.SAN `description:"Defines the SANs (Subject Alternative Names) used to match against SANs during the peer certificate verification." json:"peerCertSANs,omitempty" toml:"peerCertSANs,omitempty" yaml:"peerCertSANs,omitempty"`
|
||||
Spiffe *Spiffe `description:"Defines the SPIFFE configuration." json:"spiffe,omitempty" toml:"spiffe,omitempty" yaml:"spiffe,omitempty" label:"allowEmpty" file:"allowEmpty" export:"true"`
|
||||
}
|
||||
|
||||
// +k8s:deepcopy-gen=true
|
||||
|
||||
@@ -207,8 +207,10 @@ type TLSClientConfig struct {
|
||||
InsecureSkipVerify bool `description:"Disables SSL certificate verification." json:"insecureSkipVerify,omitempty" toml:"insecureSkipVerify,omitempty" yaml:"insecureSkipVerify,omitempty" export:"true"`
|
||||
RootCAs []types.FileOrContent `description:"Defines a list of CA certificates used to validate server certificates." json:"rootCAs,omitempty" toml:"rootCAs,omitempty" yaml:"rootCAs,omitempty"`
|
||||
Certificates traefiktls.Certificates `description:"Defines a list of client certificates for mTLS." json:"certificates,omitempty" toml:"certificates,omitempty" yaml:"certificates,omitempty" export:"true"`
|
||||
PeerCertURI string `description:"Defines the URI used to match against SAN URI during the peer certificate verification." json:"peerCertURI,omitempty" toml:"peerCertURI,omitempty" yaml:"peerCertURI,omitempty" export:"true"`
|
||||
Spiffe *Spiffe `description:"Defines the SPIFFE TLS configuration." json:"spiffe,omitempty" toml:"spiffe,omitempty" yaml:"spiffe,omitempty" label:"allowEmpty" file:"allowEmpty" export:"true"`
|
||||
// Deprecated: PeerCertURI is deprecated, please use the PeerCertSANs option instead.
|
||||
PeerCertURI string `description:"Defines the URI used to match against SAN URI during the peer certificate verification." json:"peerCertURI,omitempty" toml:"peerCertURI,omitempty" yaml:"peerCertURI,omitempty"`
|
||||
PeerCertSANs []traefiktls.SAN `description:"Defines the SANs (Subject Alternative Names) used to match against SANs during the peer certificate verification." json:"peerCertSANs,omitempty" toml:"peerCertSANs,omitempty" yaml:"peerCertSANs,omitempty"`
|
||||
Spiffe *Spiffe `description:"Defines the SPIFFE TLS configuration." json:"spiffe,omitempty" toml:"spiffe,omitempty" yaml:"spiffe,omitempty" label:"allowEmpty" file:"allowEmpty" export:"true"`
|
||||
}
|
||||
|
||||
// +k8s:deepcopy-gen=true
|
||||
|
||||
@@ -1855,6 +1855,11 @@ func (in *ServersTransport) DeepCopyInto(out *ServersTransport) {
|
||||
*out = new(ForwardingTimeouts)
|
||||
**out = **in
|
||||
}
|
||||
if in.PeerCertSANs != nil {
|
||||
in, out := &in.PeerCertSANs, &out.PeerCertSANs
|
||||
*out = make([]tls.SAN, len(*in))
|
||||
copy(*out, *in)
|
||||
}
|
||||
if in.Spiffe != nil {
|
||||
in, out := &in.Spiffe, &out.Spiffe
|
||||
*out = new(Spiffe)
|
||||
@@ -2522,6 +2527,11 @@ func (in *TLSClientConfig) DeepCopyInto(out *TLSClientConfig) {
|
||||
*out = make(tls.Certificates, len(*in))
|
||||
copy(*out, *in)
|
||||
}
|
||||
if in.PeerCertSANs != nil {
|
||||
in, out := &in.PeerCertSANs, &out.PeerCertSANs
|
||||
*out = make([]tls.SAN, len(*in))
|
||||
copy(*out, *in)
|
||||
}
|
||||
if in.Spiffe != nil {
|
||||
in, out := &in.Spiffe, &out.Spiffe
|
||||
*out = new(Spiffe)
|
||||
|
||||
@@ -150,10 +150,11 @@ type TLSConfig struct {
|
||||
|
||||
// ForwardedHeaders Trust client forwarding headers.
|
||||
type ForwardedHeaders struct {
|
||||
Insecure bool `description:"Trust all forwarded headers." json:"insecure,omitempty" toml:"insecure,omitempty" yaml:"insecure,omitempty" export:"true"`
|
||||
TrustedIPs []string `description:"Trust only forwarded headers from selected IPs." json:"trustedIPs,omitempty" toml:"trustedIPs,omitempty" yaml:"trustedIPs,omitempty"`
|
||||
Connection []string `description:"List of Connection headers that are allowed to pass through the middleware chain before being removed." json:"connection,omitempty" toml:"connection,omitempty" yaml:"connection,omitempty"`
|
||||
NotAppendXForwardedFor bool `description:"Disable appending RemoteAddr to X-Forwarded-For header. Defaults to false (appending is enabled)." json:"notAppendXForwardedFor,omitempty" toml:"notAppendXForwardedFor,omitempty" yaml:"notAppendXForwardedFor,omitempty" label:"allowEmpty" file:"allowEmpty" export:"true"`
|
||||
Insecure bool `description:"Trust all forwarded headers." json:"insecure,omitempty" toml:"insecure,omitempty" yaml:"insecure,omitempty" export:"true"`
|
||||
TrustedIPs []string `description:"Trust only forwarded headers from selected IPs." json:"trustedIPs,omitempty" toml:"trustedIPs,omitempty" yaml:"trustedIPs,omitempty"`
|
||||
Connection []string `description:"List of Connection headers that are allowed to pass through the middleware chain before being removed." json:"connection,omitempty" toml:"connection,omitempty" yaml:"connection,omitempty"`
|
||||
NotAppendXForwardedFor bool `description:"Disable appending RemoteAddr to X-Forwarded-For header. Defaults to false (appending is enabled)." json:"notAppendXForwardedFor,omitempty" toml:"notAppendXForwardedFor,omitempty" yaml:"notAppendXForwardedFor,omitempty" label:"allowEmpty" file:"allowEmpty" export:"true"`
|
||||
AddXForwardedSchemeHeaders bool `description:"Add the X-Forwarded-Scheme and X-Scheme headers." json:"addXForwardedSchemeHeaders,omitempty" toml:"addXForwardedSchemeHeaders,omitempty" yaml:"addXForwardedSchemeHeaders,omitempty" export:"true"`
|
||||
}
|
||||
|
||||
// ProxyProtocol contains Proxy-Protocol configuration.
|
||||
|
||||
@@ -18,12 +18,14 @@ const (
|
||||
XForwardedFor = "X-Forwarded-For"
|
||||
XForwardedHost = "X-Forwarded-Host"
|
||||
XForwardedPort = "X-Forwarded-Port"
|
||||
xForwardedScheme = "X-Forwarded-Scheme"
|
||||
xForwardedServer = "X-Forwarded-Server"
|
||||
XForwardedURI = "X-Forwarded-Uri"
|
||||
XForwardedMethod = "X-Forwarded-Method"
|
||||
XForwardedPrefix = "X-Forwarded-Prefix"
|
||||
xForwardedTLSClientCert = "X-Forwarded-Tls-Client-Cert"
|
||||
xForwardedTLSClientCertInfo = "X-Forwarded-Tls-Client-Cert-Info"
|
||||
xScheme = "X-Scheme"
|
||||
xRealIP = "X-Real-Ip"
|
||||
connection = "Connection"
|
||||
upgrade = "Upgrade"
|
||||
@@ -34,6 +36,7 @@ const (
|
||||
// that Go's HTTP server preserves (e.g. X_Forwarded_Proto).
|
||||
var XHeadersSet = map[string]struct{}{
|
||||
XForwardedProto: {},
|
||||
xForwardedScheme: {},
|
||||
XForwardedFor: {},
|
||||
XForwardedHost: {},
|
||||
XForwardedPort: {},
|
||||
@@ -43,6 +46,7 @@ var XHeadersSet = map[string]struct{}{
|
||||
XForwardedPrefix: {},
|
||||
xForwardedTLSClientCert: {},
|
||||
xForwardedTLSClientCertInfo: {},
|
||||
xScheme: {},
|
||||
xRealIP: {},
|
||||
}
|
||||
|
||||
@@ -70,17 +74,18 @@ func isManagedXHeader(key string) bool {
|
||||
// Unless insecure is set,
|
||||
// it first removes all the existing values for those headers if the remote address is not one of the trusted ones.
|
||||
type XForwarded struct {
|
||||
insecure bool
|
||||
trustedIPs []string
|
||||
connectionHeaders []string
|
||||
notAppendXForwardedFor bool
|
||||
ipChecker *ip.Checker
|
||||
next http.Handler
|
||||
hostname string
|
||||
insecure bool
|
||||
trustedIPs []string
|
||||
connectionHeaders []string
|
||||
notAppendXForwardedFor bool
|
||||
addXForwardedSchemeHeaders bool
|
||||
ipChecker *ip.Checker
|
||||
next http.Handler
|
||||
hostname string
|
||||
}
|
||||
|
||||
// NewXForwarded creates a new XForwarded.
|
||||
func NewXForwarded(insecure bool, trustedIPs []string, connectionHeaders []string, notAppendXForwardedFor bool, next http.Handler) (*XForwarded, error) {
|
||||
func NewXForwarded(insecure bool, trustedIPs []string, connectionHeaders []string, notAppendXForwardedFor bool, addXForwardedSchemeHeaders bool, next http.Handler) (*XForwarded, error) {
|
||||
var ipChecker *ip.Checker
|
||||
if len(trustedIPs) > 0 {
|
||||
var err error
|
||||
@@ -101,13 +106,14 @@ func NewXForwarded(insecure bool, trustedIPs []string, connectionHeaders []strin
|
||||
}
|
||||
|
||||
return &XForwarded{
|
||||
insecure: insecure,
|
||||
trustedIPs: trustedIPs,
|
||||
connectionHeaders: canonicalConnectionHeaders,
|
||||
notAppendXForwardedFor: notAppendXForwardedFor,
|
||||
ipChecker: ipChecker,
|
||||
next: next,
|
||||
hostname: hostname,
|
||||
insecure: insecure,
|
||||
trustedIPs: trustedIPs,
|
||||
connectionHeaders: canonicalConnectionHeaders,
|
||||
notAppendXForwardedFor: notAppendXForwardedFor,
|
||||
addXForwardedSchemeHeaders: addXForwardedSchemeHeaders,
|
||||
ipChecker: ipChecker,
|
||||
next: next,
|
||||
hostname: hostname,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -168,6 +174,12 @@ func (x *XForwarded) rewrite(outreq *http.Request) {
|
||||
unsafeHeader(outreq.Header).Set(XForwardedPort, forwardedPort(outreq))
|
||||
}
|
||||
|
||||
if x.addXForwardedSchemeHeaders {
|
||||
scheme := unsafeHeader(outreq.Header).Get(XForwardedProto)
|
||||
unsafeHeader(outreq.Header).Set(xForwardedScheme, scheme)
|
||||
unsafeHeader(outreq.Header).Set(xScheme, scheme)
|
||||
}
|
||||
|
||||
if xfHost := unsafeHeader(outreq.Header).Get(XForwardedHost); xfHost == "" && outreq.Host != "" {
|
||||
unsafeHeader(outreq.Header).Set(XForwardedHost, outreq.Host)
|
||||
}
|
||||
|
||||
@@ -17,12 +17,14 @@ func TestServeHTTP(t *testing.T) {
|
||||
insecure bool
|
||||
trustedIps []string
|
||||
connectionHeaders []string
|
||||
addSchemeHeaders bool
|
||||
incomingHeaders map[string][]string
|
||||
remoteAddr string
|
||||
expectedHeaders map[string]string
|
||||
tls bool
|
||||
websocket bool
|
||||
host string
|
||||
absentHeaders []string
|
||||
}{
|
||||
{
|
||||
desc: "all Empty",
|
||||
@@ -230,6 +232,24 @@ func TestServeHTTP(t *testing.T) {
|
||||
XForwardedProto: "https",
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "xForwardedScheme headers with tls",
|
||||
tls: true,
|
||||
addSchemeHeaders: true,
|
||||
expectedHeaders: map[string]string{
|
||||
XForwardedProto: "https",
|
||||
xForwardedScheme: "https",
|
||||
xScheme: "https",
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "xForwardedScheme headers disabled keeps legacy headers absent",
|
||||
tls: true,
|
||||
expectedHeaders: map[string]string{
|
||||
XForwardedProto: "https",
|
||||
},
|
||||
absentHeaders: []string{xForwardedScheme, xScheme},
|
||||
},
|
||||
{
|
||||
desc: "xForwardedProto with websocket",
|
||||
tls: false,
|
||||
@@ -238,6 +258,16 @@ func TestServeHTTP(t *testing.T) {
|
||||
XForwardedProto: "ws",
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "xForwardedScheme headers with websocket",
|
||||
websocket: true,
|
||||
addSchemeHeaders: true,
|
||||
expectedHeaders: map[string]string{
|
||||
XForwardedProto: "ws",
|
||||
xForwardedScheme: "ws",
|
||||
xScheme: "ws",
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "xForwardedProto with websocket and tls",
|
||||
tls: true,
|
||||
@@ -246,6 +276,17 @@ func TestServeHTTP(t *testing.T) {
|
||||
XForwardedProto: "wss",
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "xForwardedScheme headers with websocket and tls",
|
||||
tls: true,
|
||||
websocket: true,
|
||||
addSchemeHeaders: true,
|
||||
expectedHeaders: map[string]string{
|
||||
XForwardedProto: "wss",
|
||||
xForwardedScheme: "wss",
|
||||
xScheme: "wss",
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "xForwardedProto with websocket and tls and already x-forwarded-proto with wss",
|
||||
tls: true,
|
||||
@@ -257,6 +298,21 @@ func TestServeHTTP(t *testing.T) {
|
||||
XForwardedProto: "wss",
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "xForwardedScheme headers overwrite in insecure mode",
|
||||
insecure: true,
|
||||
addSchemeHeaders: true,
|
||||
incomingHeaders: map[string][]string{
|
||||
XForwardedProto: {"https"},
|
||||
xForwardedScheme: {"external-https"},
|
||||
xScheme: {"external-https"},
|
||||
},
|
||||
expectedHeaders: map[string]string{
|
||||
XForwardedProto: "https",
|
||||
xForwardedScheme: "https",
|
||||
xScheme: "https",
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "xForwardedPort with explicit port",
|
||||
host: "foo.com:8080",
|
||||
@@ -643,7 +699,7 @@ func TestServeHTTP(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
m, err := NewXForwarded(test.insecure, test.trustedIps, test.connectionHeaders, false,
|
||||
m, err := NewXForwarded(test.insecure, test.trustedIps, test.connectionHeaders, false, test.addSchemeHeaders,
|
||||
http.HandlerFunc(func(_ http.ResponseWriter, _ *http.Request) {}))
|
||||
require.NoError(t, err)
|
||||
|
||||
@@ -656,6 +712,10 @@ func TestServeHTTP(t *testing.T) {
|
||||
for k, v := range test.expectedHeaders {
|
||||
assert.Equal(t, v, req.Header.Get(k))
|
||||
}
|
||||
|
||||
for _, header := range test.absentHeaders {
|
||||
assert.NotContains(t, req.Header, http.CanonicalHeaderKey(header))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -782,7 +842,7 @@ func TestConnection(t *testing.T) {
|
||||
t.Run(test.desc, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
forwarded, err := NewXForwarded(true, nil, test.connectionHeaders, false, nil)
|
||||
forwarded, err := NewXForwarded(true, nil, test.connectionHeaders, false, false, nil)
|
||||
require.NoError(t, err)
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "https://localhost", nil)
|
||||
|
||||
@@ -462,7 +462,12 @@ func Test_buildConfiguration(t *testing.T) {
|
||||
KeyFile: "key",
|
||||
},
|
||||
},
|
||||
PeerCertURI: "spiffe:///ns/ns/dc/dc1/svc/dev/Test",
|
||||
PeerCertSANs: []tls.SAN{
|
||||
{
|
||||
Type: tls.SANURIType,
|
||||
Value: "spiffe:///ns/ns/dc/dc1/svc/dev/Test",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -557,7 +562,12 @@ func Test_buildConfiguration(t *testing.T) {
|
||||
KeyFile: "key",
|
||||
},
|
||||
},
|
||||
PeerCertURI: "spiffe:///ns/ns/dc/dc1/svc/dev/Test",
|
||||
PeerCertSANs: []tls.SAN{
|
||||
{
|
||||
Type: tls.SANURIType,
|
||||
Value: "spiffe:///ns/ns/dc/dc1/svc/dev/Test",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -2626,7 +2636,12 @@ func Test_buildConfiguration(t *testing.T) {
|
||||
KeyFile: "key",
|
||||
},
|
||||
},
|
||||
PeerCertURI: "spiffe:///ns/ns/dc/dc1/svc/Test",
|
||||
PeerCertSANs: []tls.SAN{
|
||||
{
|
||||
Type: tls.SANURIType,
|
||||
Value: "spiffe:///ns/ns/dc/dc1/svc/Test",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -3279,7 +3294,12 @@ func Test_buildConfiguration(t *testing.T) {
|
||||
KeyFile: "key",
|
||||
},
|
||||
},
|
||||
PeerCertURI: "spiffe:///ns/ns/dc/dc1/svc/Test",
|
||||
PeerCertSANs: []tls.SAN{
|
||||
{
|
||||
Type: tls.SANURIType,
|
||||
Value: "spiffe:///ns/ns/dc/dc1/svc/Test",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -69,7 +69,12 @@ func (c *connectCert) serversTransport(item itemData) *dynamic.ServersTransport
|
||||
Certificates: traefiktls.Certificates{
|
||||
c.getLeaf(),
|
||||
},
|
||||
PeerCertURI: spiffeID,
|
||||
PeerCertSANs: []traefiktls.SAN{
|
||||
{
|
||||
Type: traefiktls.SANURIType,
|
||||
Value: spiffeID,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -91,7 +96,12 @@ func (c *connectCert) tcpServersTransport(item itemData) *dynamic.TCPServersTran
|
||||
Certificates: traefiktls.Certificates{
|
||||
c.getLeaf(),
|
||||
},
|
||||
PeerCertURI: spiffeID,
|
||||
PeerCertSANs: []traefiktls.SAN{
|
||||
{
|
||||
Type: traefiktls.SANURIType,
|
||||
Value: spiffeID,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -177,7 +177,7 @@ func (c *clientWrapper) WatchAll(namespaces []string, stopCh <-chan struct{}) (<
|
||||
}
|
||||
|
||||
for _, ns := range namespaces {
|
||||
factoryCrd := traefikinformers.NewSharedInformerFactoryWithOptions(c.csCrd, resyncPeriod, traefikinformers.WithNamespace(ns), traefikinformers.WithTweakListOptions(matchesLabelSelector))
|
||||
factoryCrd := traefikinformers.NewSharedInformerFactoryWithOptions(c.csCrd, resyncPeriod, traefikinformers.WithNamespace(ns), traefikinformers.WithTweakListOptions(matchesLabelSelector), traefikinformers.WithTransform(k8s.StripManagedFields))
|
||||
_, err := factoryCrd.Traefik().V1alpha1().IngressRoutes().Informer().AddEventHandler(eventHandler)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -219,7 +219,7 @@ func (c *clientWrapper) WatchAll(namespaces []string, stopCh <-chan struct{}) (<
|
||||
return nil, err
|
||||
}
|
||||
|
||||
factoryKube := kinformers.NewSharedInformerFactoryWithOptions(c.csKube, resyncPeriod, kinformers.WithNamespace(ns))
|
||||
factoryKube := kinformers.NewSharedInformerFactoryWithOptions(c.csKube, resyncPeriod, kinformers.WithNamespace(ns), kinformers.WithTransform(k8s.StripManagedFields))
|
||||
_, err = factoryKube.Core().V1().Services().Informer().AddEventHandler(eventHandler)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -233,7 +233,7 @@ func (c *clientWrapper) WatchAll(namespaces []string, stopCh <-chan struct{}) (<
|
||||
return nil, err
|
||||
}
|
||||
|
||||
factorySecret := kinformers.NewSharedInformerFactoryWithOptions(c.csKube, resyncPeriod, kinformers.WithNamespace(ns), kinformers.WithTweakListOptions(notOwnedByHelm))
|
||||
factorySecret := kinformers.NewSharedInformerFactoryWithOptions(c.csKube, resyncPeriod, kinformers.WithNamespace(ns), kinformers.WithTweakListOptions(notOwnedByHelm), kinformers.WithTransform(k8s.StripManagedFields))
|
||||
_, err = factorySecret.Core().V1().Secrets().Informer().AddEventHandler(eventHandler)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -271,7 +271,7 @@ func (c *clientWrapper) WatchAll(namespaces []string, stopCh <-chan struct{}) (<
|
||||
}
|
||||
|
||||
if !c.disableClusterScopeInformer {
|
||||
c.clusterScopeFactory = kinformers.NewSharedInformerFactory(c.csKube, resyncPeriod)
|
||||
c.clusterScopeFactory = kinformers.NewSharedInformerFactoryWithOptions(c.csKube, resyncPeriod, kinformers.WithTransform(k8s.StripManagedFields))
|
||||
_, err := c.clusterScopeFactory.Core().V1().Nodes().Informer().AddEventHandler(eventHandler)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -141,6 +141,11 @@ spec:
|
||||
maxIdleConnsPerHost: 42
|
||||
disableHTTP2: true
|
||||
peerCertURI: foo://bar
|
||||
peerCertSANs:
|
||||
- type: DNSName
|
||||
value: foo.com
|
||||
- type: URI
|
||||
value: foo://bar
|
||||
rootCAsSecrets:
|
||||
- root-ca0
|
||||
- root-ca1
|
||||
|
||||
+15
@@ -28,6 +28,7 @@ package v1alpha1
|
||||
|
||||
import (
|
||||
dynamic "github.com/traefik/traefik/v3/pkg/config/dynamic"
|
||||
tls "github.com/traefik/traefik/v3/pkg/tls"
|
||||
)
|
||||
|
||||
// ServersTransportSpecApplyConfiguration represents a declarative configuration of the ServersTransportSpec type for use
|
||||
@@ -60,7 +61,11 @@ type ServersTransportSpecApplyConfiguration struct {
|
||||
// DisableHTTP2 disables HTTP/2 for connections with backend servers.
|
||||
DisableHTTP2 *bool `json:"disableHTTP2,omitempty"`
|
||||
// PeerCertURI defines the peer cert URI used to match against SAN URI during the peer certificate verification.
|
||||
//
|
||||
// Deprecated: PeerCertURI is deprecated, please use the PeerCertSANs option instead.
|
||||
PeerCertURI *string `json:"peerCertURI,omitempty"`
|
||||
// PeerCertSANs defines the peer cert Subject Alternative Names used to match against SAN during the peer certificate verification.
|
||||
PeerCertSANs []tls.SAN `json:"peerCertSANs,omitempty"`
|
||||
// Spiffe defines the SPIFFE configuration.
|
||||
Spiffe *dynamic.Spiffe `json:"spiffe,omitempty"`
|
||||
}
|
||||
@@ -178,6 +183,16 @@ func (b *ServersTransportSpecApplyConfiguration) WithPeerCertURI(value string) *
|
||||
return b
|
||||
}
|
||||
|
||||
// WithPeerCertSANs adds the given value to the PeerCertSANs field in the declarative configuration
|
||||
// and returns the receiver, so that objects can be build by chaining "With" function invocations.
|
||||
// If called multiple times, values provided by each call will be appended to the PeerCertSANs field.
|
||||
func (b *ServersTransportSpecApplyConfiguration) WithPeerCertSANs(values ...tls.SAN) *ServersTransportSpecApplyConfiguration {
|
||||
for i := range values {
|
||||
b.PeerCertSANs = append(b.PeerCertSANs, values[i])
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
// WithSpiffe sets the Spiffe field in the declarative configuration to the given value
|
||||
// and returns the receiver, so that objects can be built by chaining "With" function invocations.
|
||||
// If called multiple times, the Spiffe field is set to the value of the last call.
|
||||
|
||||
+15
-1
@@ -28,6 +28,7 @@ package v1alpha1
|
||||
|
||||
import (
|
||||
dynamic "github.com/traefik/traefik/v3/pkg/config/dynamic"
|
||||
tls "github.com/traefik/traefik/v3/pkg/tls"
|
||||
)
|
||||
|
||||
// TLSClientConfigApplyConfiguration represents a declarative configuration of the TLSClientConfig type for use
|
||||
@@ -47,9 +48,12 @@ type TLSClientConfigApplyConfiguration struct {
|
||||
RootCAsSecrets []string `json:"rootCAsSecrets,omitempty"`
|
||||
// CertificatesSecrets defines a list of secret storing client certificates for mTLS.
|
||||
CertificatesSecrets []string `json:"certificatesSecrets,omitempty"`
|
||||
// MaxIdleConnsPerHost controls the maximum idle (keep-alive) to keep per-host.
|
||||
// PeerCertURI defines the peer cert URI used to match against SAN URI during the peer certificate verification.
|
||||
//
|
||||
// Deprecated: PeerCertURI is deprecated, please use the PeerCertSANs option instead.
|
||||
PeerCertURI *string `json:"peerCertURI,omitempty"`
|
||||
// PeerCertSANs defines the peer cert Subject Alternative Names used to match against SAN during the peer certificate verification.
|
||||
PeerCertSANs []tls.SAN `json:"peerCertSANs,omitempty"`
|
||||
// Spiffe defines the SPIFFE configuration.
|
||||
Spiffe *dynamic.Spiffe `json:"spiffe,omitempty"`
|
||||
}
|
||||
@@ -117,6 +121,16 @@ func (b *TLSClientConfigApplyConfiguration) WithPeerCertURI(value string) *TLSCl
|
||||
return b
|
||||
}
|
||||
|
||||
// WithPeerCertSANs adds the given value to the PeerCertSANs field in the declarative configuration
|
||||
// and returns the receiver, so that objects can be build by chaining "With" function invocations.
|
||||
// If called multiple times, values provided by each call will be appended to the PeerCertSANs field.
|
||||
func (b *TLSClientConfigApplyConfiguration) WithPeerCertSANs(values ...tls.SAN) *TLSClientConfigApplyConfiguration {
|
||||
for i := range values {
|
||||
b.PeerCertSANs = append(b.PeerCertSANs, values[i])
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
// WithSpiffe sets the Spiffe field in the declarative configuration to the given value
|
||||
// and returns the receiver, so that objects can be built by chaining "With" function invocations.
|
||||
// If called multiple times, the Spiffe field is set to the value of the last call.
|
||||
|
||||
@@ -541,6 +541,7 @@ func (p *Provider) loadConfigurationFromCRD(ctx context.Context, client Client)
|
||||
MaxIdleConnsPerHost: serversTransport.Spec.MaxIdleConnsPerHost,
|
||||
ForwardingTimeouts: forwardingTimeout,
|
||||
PeerCertURI: serversTransport.Spec.PeerCertURI,
|
||||
PeerCertSANs: serversTransport.Spec.PeerCertSANs,
|
||||
Spiffe: serversTransport.Spec.Spiffe,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5472,6 +5472,16 @@ func TestLoadIngressRoutes(t *testing.T) {
|
||||
PingTimeout: ptypes.Duration(42 * time.Second),
|
||||
},
|
||||
PeerCertURI: "foo://bar",
|
||||
PeerCertSANs: []tls.SAN{
|
||||
{
|
||||
Type: tls.SANDNSNameType,
|
||||
Value: "foo.com",
|
||||
},
|
||||
{
|
||||
Type: tls.SANURIType,
|
||||
Value: "foo://bar",
|
||||
},
|
||||
},
|
||||
Spiffe: &dynamic.Spiffe{
|
||||
IDs: []string{
|
||||
"spiffe://foo/buz",
|
||||
|
||||
@@ -2,6 +2,7 @@ package v1alpha1
|
||||
|
||||
import (
|
||||
"github.com/traefik/traefik/v3/pkg/config/dynamic"
|
||||
traefiktls "github.com/traefik/traefik/v3/pkg/tls"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/util/intstr"
|
||||
)
|
||||
@@ -53,7 +54,11 @@ type ServersTransportSpec struct {
|
||||
// DisableHTTP2 disables HTTP/2 for connections with backend servers.
|
||||
DisableHTTP2 bool `json:"disableHTTP2,omitempty"`
|
||||
// PeerCertURI defines the peer cert URI used to match against SAN URI during the peer certificate verification.
|
||||
//
|
||||
// Deprecated: PeerCertURI is deprecated, please use the PeerCertSANs option instead.
|
||||
PeerCertURI string `json:"peerCertURI,omitempty"`
|
||||
// PeerCertSANs defines the peer cert Subject Alternative Names used to match against SAN during the peer certificate verification.
|
||||
PeerCertSANs []traefiktls.SAN `json:"peerCertSANs,omitempty"`
|
||||
// Spiffe defines the SPIFFE configuration.
|
||||
Spiffe *dynamic.Spiffe `json:"spiffe,omitempty"`
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package v1alpha1
|
||||
|
||||
import (
|
||||
"github.com/traefik/traefik/v3/pkg/config/dynamic"
|
||||
traefiktls "github.com/traefik/traefik/v3/pkg/tls"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/util/intstr"
|
||||
)
|
||||
@@ -59,9 +60,12 @@ type TLSClientConfig struct {
|
||||
RootCAsSecrets []string `json:"rootCAsSecrets,omitempty"`
|
||||
// CertificatesSecrets defines a list of secret storing client certificates for mTLS.
|
||||
CertificatesSecrets []string `json:"certificatesSecrets,omitempty"`
|
||||
// MaxIdleConnsPerHost controls the maximum idle (keep-alive) to keep per-host.
|
||||
// PeerCertURI defines the peer cert URI used to match against SAN URI during the peer certificate verification.
|
||||
//
|
||||
// Deprecated: PeerCertURI is deprecated, please use the PeerCertSANs option instead.
|
||||
PeerCertURI string `json:"peerCertURI,omitempty"`
|
||||
// PeerCertSANs defines the peer cert Subject Alternative Names used to match against SAN during the peer certificate verification.
|
||||
PeerCertSANs []traefiktls.SAN `json:"peerCertSANs,omitempty"`
|
||||
// Spiffe defines the SPIFFE configuration.
|
||||
Spiffe *dynamic.Spiffe `json:"spiffe,omitempty"`
|
||||
}
|
||||
|
||||
@@ -1618,6 +1618,11 @@ func (in *ServersTransportSpec) DeepCopyInto(out *ServersTransportSpec) {
|
||||
*out = new(ForwardingTimeouts)
|
||||
(*in).DeepCopyInto(*out)
|
||||
}
|
||||
if in.PeerCertSANs != nil {
|
||||
in, out := &in.PeerCertSANs, &out.PeerCertSANs
|
||||
*out = make([]tls.SAN, len(*in))
|
||||
copy(*out, *in)
|
||||
}
|
||||
if in.Spiffe != nil {
|
||||
in, out := &in.Spiffe, &out.Spiffe
|
||||
*out = new(dynamic.Spiffe)
|
||||
@@ -1871,6 +1876,11 @@ func (in *TLSClientConfig) DeepCopyInto(out *TLSClientConfig) {
|
||||
*out = make([]string, len(*in))
|
||||
copy(*out, *in)
|
||||
}
|
||||
if in.PeerCertSANs != nil {
|
||||
in, out := &in.PeerCertSANs, &out.PeerCertSANs
|
||||
*out = make([]tls.SAN, len(*in))
|
||||
copy(*out, *in)
|
||||
}
|
||||
if in.Spiffe != nil {
|
||||
in, out := &in.Spiffe, &out.Spiffe
|
||||
*out = new(dynamic.Spiffe)
|
||||
|
||||
@@ -148,20 +148,20 @@ func (c *clientWrapper) WatchAll(namespaces []string, stopCh <-chan struct{}) (<
|
||||
options.LabelSelector = c.labelSelector
|
||||
}
|
||||
|
||||
c.factoryNamespace = kinformers.NewSharedInformerFactory(c.csKube, resyncPeriod)
|
||||
c.factoryNamespace = kinformers.NewSharedInformerFactoryWithOptions(c.csKube, resyncPeriod, kinformers.WithTransform(k8s.StripManagedFields))
|
||||
_, err := c.factoryNamespace.Core().V1().Namespaces().Informer().AddEventHandler(eventHandler)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
c.factoryGatewayClass = gateinformers.NewSharedInformerFactoryWithOptions(c.csGateway, resyncPeriod, gateinformers.WithTweakListOptions(labelSelectorOptions))
|
||||
c.factoryGatewayClass = gateinformers.NewSharedInformerFactoryWithOptions(c.csGateway, resyncPeriod, gateinformers.WithTweakListOptions(labelSelectorOptions), gateinformers.WithTransform(k8s.StripManagedFields))
|
||||
_, err = c.factoryGatewayClass.Gateway().V1().GatewayClasses().Informer().AddEventHandler(eventHandler)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for _, ns := range namespaces {
|
||||
factoryKube := kinformers.NewSharedInformerFactoryWithOptions(c.csKube, resyncPeriod, kinformers.WithNamespace(ns))
|
||||
factoryKube := kinformers.NewSharedInformerFactoryWithOptions(c.csKube, resyncPeriod, kinformers.WithNamespace(ns), kinformers.WithTransform(k8s.StripManagedFields))
|
||||
_, err = factoryKube.Core().V1().Services().Informer().AddEventHandler(eventHandler)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -171,7 +171,7 @@ func (c *clientWrapper) WatchAll(namespaces []string, stopCh <-chan struct{}) (<
|
||||
return nil, err
|
||||
}
|
||||
|
||||
factoryGateway := gateinformers.NewSharedInformerFactoryWithOptions(c.csGateway, resyncPeriod, gateinformers.WithNamespace(ns))
|
||||
factoryGateway := gateinformers.NewSharedInformerFactoryWithOptions(c.csGateway, resyncPeriod, gateinformers.WithNamespace(ns), gateinformers.WithTransform(k8s.StripManagedFields))
|
||||
_, err = factoryGateway.Gateway().V1().Gateways().Informer().AddEventHandler(eventHandler)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -209,7 +209,7 @@ func (c *clientWrapper) WatchAll(namespaces []string, stopCh <-chan struct{}) (<
|
||||
}
|
||||
}
|
||||
|
||||
factorySecret := kinformers.NewSharedInformerFactoryWithOptions(c.csKube, resyncPeriod, kinformers.WithNamespace(ns), kinformers.WithTweakListOptions(notOwnedByHelm))
|
||||
factorySecret := kinformers.NewSharedInformerFactoryWithOptions(c.csKube, resyncPeriod, kinformers.WithNamespace(ns), kinformers.WithTweakListOptions(notOwnedByHelm), kinformers.WithTransform(k8s.StripManagedFields))
|
||||
_, err = factorySecret.Core().V1().Secrets().Informer().AddEventHandler(eventHandler)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -727,14 +727,6 @@ func (c *clientWrapper) UpdateBackendTLSPolicyStatus(ctx context.Context, policy
|
||||
ancestorStatuses = append(ancestorStatuses, ancestorStatus)
|
||||
continue
|
||||
}
|
||||
|
||||
// Keep statuses added by Traefik for other ancestors.
|
||||
// A BackendTLSPolicy can target services attached to different listeners.
|
||||
if !slices.ContainsFunc(status.Ancestors, func(s gatev1.PolicyAncestorStatus) bool {
|
||||
return reflect.DeepEqual(s.AncestorRef, ancestorStatus.AncestorRef)
|
||||
}) {
|
||||
ancestorStatuses = append(ancestorStatuses, ancestorStatus)
|
||||
}
|
||||
}
|
||||
|
||||
if len(ancestorStatuses) > 16 {
|
||||
|
||||
@@ -15,6 +15,7 @@ var SupportedFeatures = sync.OnceValue(func() []features.FeatureName {
|
||||
Insert(features.HTTPRouteExtendedFeatures.Intersection(extendedHTTPRouteFeatures()).UnsortedList()...).
|
||||
Insert(features.ReferenceGrantCoreFeatures.UnsortedList()...).
|
||||
Insert(features.BackendTLSPolicyCoreFeatures.UnsortedList()...).
|
||||
Insert(features.BackendTLSPolicyExtendedFeatures.Intersection(extendedBackendTLSPolicyFeatures()).UnsortedList()...).
|
||||
Insert(features.GRPCRouteCoreFeatures.UnsortedList()...).
|
||||
Insert(features.TLSRouteCoreFeatures.UnsortedList()...).
|
||||
Insert(features.TLSRouteExtendedFeatures.Intersection(extendedTLSRouteFeatures()).UnsortedList()...)
|
||||
@@ -56,3 +57,10 @@ func extendedHTTPRouteFeatures() sets.Set[features.Feature] {
|
||||
features.HTTPRouteBackendRequestHeaderModificationFeature,
|
||||
)
|
||||
}
|
||||
|
||||
// extendedBackendTLSPolicyFeatures returns the supported extended BackendTLSPolicy features.
|
||||
func extendedBackendTLSPolicyFeatures() sets.Set[features.Feature] {
|
||||
return sets.New(
|
||||
features.BackendTLSPolicySanValidationFeature,
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
---
|
||||
kind: GatewayClass
|
||||
apiVersion: gateway.networking.k8s.io/v1
|
||||
metadata:
|
||||
name: my-gateway-class
|
||||
spec:
|
||||
controllerName: traefik.io/gateway-controller
|
||||
|
||||
---
|
||||
kind: Gateway
|
||||
apiVersion: gateway.networking.k8s.io/v1
|
||||
metadata:
|
||||
name: my-gateway
|
||||
namespace: default
|
||||
spec:
|
||||
gatewayClassName: my-gateway-class
|
||||
listeners: # Use GatewayClass defaults for listener definition.
|
||||
- name: web
|
||||
protocol: HTTP
|
||||
port: 80
|
||||
allowedRoutes:
|
||||
kinds:
|
||||
- kind: GRPCRoute
|
||||
group: gateway.networking.k8s.io
|
||||
namespaces:
|
||||
from: Same
|
||||
|
||||
---
|
||||
kind: GRPCRoute
|
||||
apiVersion: gateway.networking.k8s.io/v1
|
||||
metadata:
|
||||
name: grpc-app-1
|
||||
namespace: default
|
||||
spec:
|
||||
parentRefs:
|
||||
- name: my-gateway
|
||||
kind: Gateway
|
||||
group: gateway.networking.k8s.io
|
||||
hostnames:
|
||||
- foo.com
|
||||
rules:
|
||||
- backendRefs:
|
||||
- name: whoami
|
||||
port: 80
|
||||
weight: 1
|
||||
|
||||
---
|
||||
kind: BackendTLSPolicy
|
||||
apiVersion: gateway.networking.k8s.io/v1
|
||||
metadata:
|
||||
name: backend-tls-policy
|
||||
namespace: default
|
||||
spec:
|
||||
targetRefs:
|
||||
- group: ""
|
||||
kind: Service
|
||||
name: whoami
|
||||
validation:
|
||||
hostname: whoami
|
||||
caCertificateRefs:
|
||||
- group: ""
|
||||
kind: ConfigMap
|
||||
name: ca-file
|
||||
- group: core
|
||||
kind: ConfigMap
|
||||
name: ca-file-2
|
||||
- group: ""
|
||||
kind: Secret
|
||||
name: ca-file
|
||||
- group: core
|
||||
kind: Secret
|
||||
name: ca-file-2
|
||||
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: ca-file
|
||||
namespace: default
|
||||
data:
|
||||
ca.crt: "CA1"
|
||||
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: ca-file-2
|
||||
namespace: default
|
||||
data:
|
||||
ca.crt: "CA2"
|
||||
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: ca-file
|
||||
namespace: default
|
||||
data:
|
||||
ca.crt: Q0ExLXNlY3JldA==
|
||||
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: ca-file-2
|
||||
namespace: default
|
||||
data:
|
||||
ca.crt: Q0EyLXNlY3JldA==
|
||||
@@ -0,0 +1,64 @@
|
||||
---
|
||||
kind: GatewayClass
|
||||
apiVersion: gateway.networking.k8s.io/v1
|
||||
metadata:
|
||||
name: my-gateway-class
|
||||
spec:
|
||||
controllerName: traefik.io/gateway-controller
|
||||
|
||||
---
|
||||
kind: Gateway
|
||||
apiVersion: gateway.networking.k8s.io/v1
|
||||
metadata:
|
||||
name: my-gateway
|
||||
namespace: default
|
||||
spec:
|
||||
gatewayClassName: my-gateway-class
|
||||
listeners: # Use GatewayClass defaults for listener definition.
|
||||
- name: web
|
||||
protocol: HTTP
|
||||
port: 80
|
||||
allowedRoutes:
|
||||
kinds:
|
||||
- kind: GRPCRoute
|
||||
group: gateway.networking.k8s.io
|
||||
namespaces:
|
||||
from: Same
|
||||
|
||||
---
|
||||
kind: GRPCRoute
|
||||
apiVersion: gateway.networking.k8s.io/v1
|
||||
metadata:
|
||||
name: grpc-app-1
|
||||
namespace: default
|
||||
spec:
|
||||
parentRefs:
|
||||
- name: my-gateway
|
||||
kind: Gateway
|
||||
group: gateway.networking.k8s.io
|
||||
hostnames:
|
||||
- foo.com
|
||||
rules:
|
||||
- backendRefs:
|
||||
- name: whoami
|
||||
port: 80
|
||||
weight: 1
|
||||
|
||||
---
|
||||
kind: BackendTLSPolicy
|
||||
apiVersion: gateway.networking.k8s.io/v1
|
||||
metadata:
|
||||
name: backend-tls-policy
|
||||
namespace: default
|
||||
spec:
|
||||
targetRefs:
|
||||
- group: ""
|
||||
kind: Service
|
||||
name: whoami
|
||||
validation:
|
||||
hostname: whoami
|
||||
subjectAltNames:
|
||||
- type: Hostname
|
||||
hostname: whoami.default.svc.cluster.local
|
||||
- type: URI
|
||||
uri: spiffe://cluster.local/ns/default/sa/whoami
|
||||
@@ -0,0 +1,60 @@
|
||||
---
|
||||
kind: GatewayClass
|
||||
apiVersion: gateway.networking.k8s.io/v1
|
||||
metadata:
|
||||
name: my-gateway-class
|
||||
spec:
|
||||
controllerName: traefik.io/gateway-controller
|
||||
|
||||
---
|
||||
kind: Gateway
|
||||
apiVersion: gateway.networking.k8s.io/v1
|
||||
metadata:
|
||||
name: my-gateway
|
||||
namespace: default
|
||||
spec:
|
||||
gatewayClassName: my-gateway-class
|
||||
listeners: # Use GatewayClass defaults for listener definition.
|
||||
- name: http
|
||||
protocol: HTTP
|
||||
port: 80
|
||||
allowedRoutes:
|
||||
kinds:
|
||||
- kind: GRPCRoute
|
||||
group: gateway.networking.k8s.io
|
||||
namespaces:
|
||||
from: Same
|
||||
|
||||
---
|
||||
kind: GRPCRoute
|
||||
apiVersion: gateway.networking.k8s.io/v1
|
||||
metadata:
|
||||
name: grpc-app-1
|
||||
namespace: default
|
||||
spec:
|
||||
parentRefs:
|
||||
- name: my-gateway
|
||||
kind: Gateway
|
||||
group: gateway.networking.k8s.io
|
||||
hostnames:
|
||||
- foo.com
|
||||
rules:
|
||||
- backendRefs:
|
||||
- name: whoami
|
||||
port: 80
|
||||
weight: 1
|
||||
|
||||
---
|
||||
kind: BackendTLSPolicy
|
||||
apiVersion: gateway.networking.k8s.io/v1
|
||||
metadata:
|
||||
name: backend-tls-policy
|
||||
namespace: default
|
||||
spec:
|
||||
targetRefs:
|
||||
- group: core
|
||||
kind: Service
|
||||
name: whoami
|
||||
validation:
|
||||
hostname: whoami
|
||||
wellKnownCACertificates: System
|
||||
@@ -0,0 +1,55 @@
|
||||
---
|
||||
kind: GatewayClass
|
||||
apiVersion: gateway.networking.k8s.io/v1
|
||||
metadata:
|
||||
name: my-gateway-class
|
||||
spec:
|
||||
controllerName: traefik.io/gateway-controller
|
||||
|
||||
---
|
||||
kind: Gateway
|
||||
apiVersion: gateway.networking.k8s.io/v1
|
||||
metadata:
|
||||
name: my-gateway
|
||||
namespace: default
|
||||
spec:
|
||||
gatewayClassName: my-gateway-class
|
||||
listeners: # Use GatewayClass defaults for listener definition.
|
||||
- name: http
|
||||
protocol: HTTP
|
||||
port: 80
|
||||
allowedRoutes:
|
||||
namespaces:
|
||||
from: Same
|
||||
|
||||
---
|
||||
kind: HTTPRoute
|
||||
apiVersion: gateway.networking.k8s.io/v1
|
||||
metadata:
|
||||
name: http-app-1
|
||||
namespace: default
|
||||
spec:
|
||||
parentRefs:
|
||||
- name: my-gateway
|
||||
kind: Gateway
|
||||
group: gateway.networking.k8s.io
|
||||
hostnames:
|
||||
- "foo.com"
|
||||
rules:
|
||||
- matches:
|
||||
- path:
|
||||
type: Exact
|
||||
value: /bar
|
||||
backendRefs:
|
||||
- weight: 1
|
||||
group: traefik.io
|
||||
kind: TraefikService
|
||||
name: service@file
|
||||
namespace: bar
|
||||
port: 80
|
||||
|
||||
- name: whoami
|
||||
port: 80
|
||||
weight: 1
|
||||
group: ""
|
||||
kind: Service
|
||||
@@ -0,0 +1,70 @@
|
||||
---
|
||||
kind: GatewayClass
|
||||
apiVersion: gateway.networking.k8s.io/v1
|
||||
metadata:
|
||||
name: my-gateway-class
|
||||
spec:
|
||||
controllerName: traefik.io/gateway-controller
|
||||
|
||||
---
|
||||
kind: Gateway
|
||||
apiVersion: gateway.networking.k8s.io/v1
|
||||
metadata:
|
||||
name: my-gateway
|
||||
namespace: default
|
||||
spec:
|
||||
gatewayClassName: my-gateway-class
|
||||
listeners: # Use GatewayClass defaults for listener definition.
|
||||
- name: http
|
||||
protocol: HTTP
|
||||
port: 80
|
||||
allowedRoutes:
|
||||
kinds:
|
||||
- kind: HTTPRoute
|
||||
group: gateway.networking.k8s.io
|
||||
namespaces:
|
||||
from: Same
|
||||
|
||||
---
|
||||
kind: HTTPRoute
|
||||
apiVersion: gateway.networking.k8s.io/v1
|
||||
metadata:
|
||||
name: http-app-1
|
||||
namespace: default
|
||||
spec:
|
||||
parentRefs:
|
||||
- name: my-gateway
|
||||
kind: Gateway
|
||||
group: gateway.networking.k8s.io
|
||||
hostnames:
|
||||
- "foo.com"
|
||||
rules:
|
||||
- matches:
|
||||
- path:
|
||||
type: Exact
|
||||
value: /bar
|
||||
backendRefs:
|
||||
- name: whoami
|
||||
port: 80
|
||||
weight: 1
|
||||
kind: Service
|
||||
group: ""
|
||||
|
||||
---
|
||||
kind: BackendTLSPolicy
|
||||
apiVersion: gateway.networking.k8s.io/v1
|
||||
metadata:
|
||||
name: policy-1
|
||||
namespace: default
|
||||
spec:
|
||||
targetRefs:
|
||||
- group: ""
|
||||
kind: Service
|
||||
name: whoami
|
||||
validation:
|
||||
hostname: whoami
|
||||
subjectAltNames:
|
||||
- type: Hostname
|
||||
hostname: whoami.default.svc.cluster.local
|
||||
- type: URI
|
||||
uri: spiffe://cluster.local/ns/default/sa/whoami
|
||||
@@ -0,0 +1,51 @@
|
||||
---
|
||||
kind: GatewayClass
|
||||
apiVersion: gateway.networking.k8s.io/v1
|
||||
metadata:
|
||||
name: my-gateway-class
|
||||
spec:
|
||||
controllerName: traefik.io/gateway-controller
|
||||
|
||||
---
|
||||
kind: Gateway
|
||||
apiVersion: gateway.networking.k8s.io/v1
|
||||
metadata:
|
||||
name: my-gateway
|
||||
namespace: default
|
||||
spec:
|
||||
gatewayClassName: my-gateway-class
|
||||
listeners: # Use GatewayClass defaults for listener definition.
|
||||
- name: tcp
|
||||
protocol: TCP
|
||||
port: 9000
|
||||
allowedRoutes:
|
||||
kinds:
|
||||
- kind: TCPRoute
|
||||
group: gateway.networking.k8s.io
|
||||
namespaces:
|
||||
from: Same
|
||||
|
||||
---
|
||||
kind: TCPRoute
|
||||
apiVersion: gateway.networking.k8s.io/v1alpha2
|
||||
metadata:
|
||||
name: tcp-app-1
|
||||
namespace: default
|
||||
spec:
|
||||
parentRefs:
|
||||
- name: my-gateway
|
||||
kind: Gateway
|
||||
group: gateway.networking.k8s.io
|
||||
rules:
|
||||
- backendRefs:
|
||||
- weight: 1
|
||||
group: traefik.io
|
||||
kind: TraefikService
|
||||
name: service@file
|
||||
namespace: bar
|
||||
port: 9000
|
||||
- name: whoamitcp
|
||||
port: 9000
|
||||
weight: 1
|
||||
group: ""
|
||||
kind: Service
|
||||
@@ -0,0 +1,67 @@
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: supersecret
|
||||
namespace: default
|
||||
|
||||
data:
|
||||
tls.crt: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUJxRENDQVU2Z0F3SUJBZ0lVWU9zcjBRZ0hPQnE0a1lSQ0w1K1REZFZ0NmJRd0NnWUlLb1pJemowRUF3SXcKRmpFVU1CSUdBMVVFQXd3TFpYaGhiWEJzWlM1amIyMHdIaGNOTWpVeE1ERXdNRGN4TnpNd1doY05NelV4TURBNApNRGN4TnpNd1dqQVdNUlF3RWdZRFZRUUREQXRsZUdGdGNHeGxMbU52YlRCWk1CTUdCeXFHU000OUFnRUdDQ3FHClNNNDlBd0VIQTBJQUJET3JpdzNaUTd3SWhXcmJQUzZKRlFUM2JUb05DRjAwdlNWNWZhYjZUYlh5TDh0bHNHcmUKVFJJRjJFd2dzdGVNT2t4R0tLU2xEdnVhRHdxOHAvcVYrMHVqZWpCNE1CMEdBMVVkRGdRV0JCUk1Fa3VleFhRaApVdERnUmcxS0J2NzJDRHErRXpBZkJnTlZIU01FR0RBV2dCUk1Fa3VleFhRaFV0RGdSZzFLQnY3MkNEcStFekFQCkJnTlZIUk1CQWY4RUJUQURBUUgvTUNVR0ExVWRFUVFlTUJ5Q0MyVjRZVzF3YkdVdVkyOXRnZzBxTG1WNFlXMXcKYkdVdVkyOXRNQW9HQ0NxR1NNNDlCQU1DQTBnQU1FVUNJUURzODdWazBzd0E2SGdPSmpST3llMW14RDgzcWNHeQpwZUZnb3hWOTNEeStjd0lnVjBNTUVKSmJWc1R5WkszRVErK1hjNXJFTDc4bnJKK1lJRVYrckNVV2o1VT0KLS0tLS1FTkQgQ0VSVElGSUNBVEUtLS0tLQ==
|
||||
tls.key: LS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0tCk1JR0hBZ0VBTUJNR0J5cUdTTTQ5QWdFR0NDcUdTTTQ5QXdFSEJHMHdhd0lCQVFRZ253Z0w1RFk0VUIxNHNNNmYKRGlrUWR0cWgyUVcxQXJmRjRmYzFVRnppZmRHaFJBTkNBQVF6cTRzTjJVTzhDSVZxMnowdWlSVUU5MjA2RFFoZApOTDBsZVgybStrMjE4aS9MWmJCcTNrMFNCZGhNSUxMWGpEcE1SaWlrcFE3N21nOEt2S2Y2bGZ0TAotLS0tLUVORCBQUklWQVRFIEtFWS0tLS0t
|
||||
|
||||
---
|
||||
kind: GatewayClass
|
||||
apiVersion: gateway.networking.k8s.io/v1
|
||||
metadata:
|
||||
name: my-gateway-class
|
||||
spec:
|
||||
controllerName: traefik.io/gateway-controller
|
||||
|
||||
---
|
||||
kind: Gateway
|
||||
apiVersion: gateway.networking.k8s.io/v1
|
||||
metadata:
|
||||
name: my-gateway
|
||||
namespace: default
|
||||
spec:
|
||||
gatewayClassName: my-gateway-class
|
||||
listeners: # Use GatewayClass defaults for listener definition.
|
||||
- name: tls
|
||||
protocol: TLS
|
||||
port: 9000
|
||||
tls:
|
||||
certificateRefs:
|
||||
- kind: Secret
|
||||
name: supersecret
|
||||
group: ""
|
||||
allowedRoutes:
|
||||
kinds:
|
||||
- kind: TLSRoute
|
||||
group: gateway.networking.k8s.io
|
||||
namespaces:
|
||||
from: Same
|
||||
|
||||
---
|
||||
kind: TLSRoute
|
||||
apiVersion: gateway.networking.k8s.io/v1
|
||||
metadata:
|
||||
name: tls-app-1
|
||||
namespace: default
|
||||
spec:
|
||||
parentRefs:
|
||||
- name: my-gateway
|
||||
kind: Gateway
|
||||
group: gateway.networking.k8s.io
|
||||
rules:
|
||||
- backendRefs:
|
||||
- weight: 1
|
||||
group: traefik.io
|
||||
kind: TraefikService
|
||||
name: service@file
|
||||
namespace: bar
|
||||
port: 9000
|
||||
- name: whoamitcp
|
||||
port: 9000
|
||||
weight: 1
|
||||
kind: Service
|
||||
group: ""
|
||||
@@ -0,0 +1,124 @@
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: supersecret
|
||||
namespace: default
|
||||
|
||||
data:
|
||||
tls.crt: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUJxRENDQVU2Z0F3SUJBZ0lVWU9zcjBRZ0hPQnE0a1lSQ0w1K1REZFZ0NmJRd0NnWUlLb1pJemowRUF3SXcKRmpFVU1CSUdBMVVFQXd3TFpYaGhiWEJzWlM1amIyMHdIaGNOTWpVeE1ERXdNRGN4TnpNd1doY05NelV4TURBNApNRGN4TnpNd1dqQVdNUlF3RWdZRFZRUUREQXRsZUdGdGNHeGxMbU52YlRCWk1CTUdCeXFHU000OUFnRUdDQ3FHClNNNDlBd0VIQTBJQUJET3JpdzNaUTd3SWhXcmJQUzZKRlFUM2JUb05DRjAwdlNWNWZhYjZUYlh5TDh0bHNHcmUKVFJJRjJFd2dzdGVNT2t4R0tLU2xEdnVhRHdxOHAvcVYrMHVqZWpCNE1CMEdBMVVkRGdRV0JCUk1Fa3VleFhRaApVdERnUmcxS0J2NzJDRHErRXpBZkJnTlZIU01FR0RBV2dCUk1Fa3VleFhRaFV0RGdSZzFLQnY3MkNEcStFekFQCkJnTlZIUk1CQWY4RUJUQURBUUgvTUNVR0ExVWRFUVFlTUJ5Q0MyVjRZVzF3YkdVdVkyOXRnZzBxTG1WNFlXMXcKYkdVdVkyOXRNQW9HQ0NxR1NNNDlCQU1DQTBnQU1FVUNJUURzODdWazBzd0E2SGdPSmpST3llMW14RDgzcWNHeQpwZUZnb3hWOTNEeStjd0lnVjBNTUVKSmJWc1R5WkszRVErK1hjNXJFTDc4bnJKK1lJRVYrckNVV2o1VT0KLS0tLS1FTkQgQ0VSVElGSUNBVEUtLS0tLQ==
|
||||
tls.key: LS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0tCk1JR0hBZ0VBTUJNR0J5cUdTTTQ5QWdFR0NDcUdTTTQ5QXdFSEJHMHdhd0lCQVFRZ253Z0w1RFk0VUIxNHNNNmYKRGlrUWR0cWgyUVcxQXJmRjRmYzFVRnppZmRHaFJBTkNBQVF6cTRzTjJVTzhDSVZxMnowdWlSVUU5MjA2RFFoZApOTDBsZVgybStrMjE4aS9MWmJCcTNrMFNCZGhNSUxMWGpEcE1SaWlrcFE3N21nOEt2S2Y2bGZ0TAotLS0tLUVORCBQUklWQVRFIEtFWS0tLS0t
|
||||
|
||||
---
|
||||
kind: GatewayClass
|
||||
apiVersion: gateway.networking.k8s.io/v1
|
||||
metadata:
|
||||
name: my-gateway-class
|
||||
spec:
|
||||
controllerName: traefik.io/gateway-controller
|
||||
|
||||
---
|
||||
kind: Gateway
|
||||
apiVersion: gateway.networking.k8s.io/v1
|
||||
metadata:
|
||||
name: my-gateway
|
||||
namespace: default
|
||||
spec:
|
||||
gatewayClassName: my-gateway-class
|
||||
listeners: # Use GatewayClass defaults for listener definition.
|
||||
- name: tls
|
||||
protocol: TLS
|
||||
port: 9001
|
||||
hostname: foo.com
|
||||
tls:
|
||||
mode: Terminate # Default mode
|
||||
certificateRefs:
|
||||
- kind: Secret
|
||||
name: supersecret
|
||||
group: ""
|
||||
allowedRoutes:
|
||||
kinds:
|
||||
- kind: TLSRoute
|
||||
group: gateway.networking.k8s.io
|
||||
namespaces:
|
||||
from: Same
|
||||
|
||||
---
|
||||
kind: TLSRoute
|
||||
apiVersion: gateway.networking.k8s.io/v1
|
||||
metadata:
|
||||
name: tls-app-1
|
||||
namespace: default
|
||||
spec:
|
||||
parentRefs:
|
||||
- name: my-gateway
|
||||
kind: Gateway
|
||||
group: gateway.networking.k8s.io
|
||||
rules:
|
||||
- backendRefs:
|
||||
- name: whoami
|
||||
port: 80
|
||||
weight: 1
|
||||
|
||||
---
|
||||
kind: BackendTLSPolicy
|
||||
apiVersion: gateway.networking.k8s.io/v1
|
||||
metadata:
|
||||
name: policy-1
|
||||
namespace: default
|
||||
spec:
|
||||
targetRefs:
|
||||
- group: ""
|
||||
kind: Service
|
||||
name: whoami
|
||||
validation:
|
||||
hostname: whoami
|
||||
caCertificateRefs:
|
||||
- group: ""
|
||||
kind: ConfigMap
|
||||
name: ca-file
|
||||
- group: core
|
||||
kind: ConfigMap
|
||||
name: ca-file-2
|
||||
- group: ""
|
||||
kind: Secret
|
||||
name: ca-file
|
||||
- group: core
|
||||
kind: Secret
|
||||
name: ca-file-2
|
||||
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: ca-file
|
||||
namespace: default
|
||||
data:
|
||||
ca.crt: "CA1"
|
||||
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: ca-file-2
|
||||
namespace: default
|
||||
data:
|
||||
ca.crt: "CA2"
|
||||
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: ca-file
|
||||
namespace: default
|
||||
data:
|
||||
ca.crt: Q0ExLXNlY3JldA==
|
||||
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: ca-file-2
|
||||
namespace: default
|
||||
data:
|
||||
ca.crt: Q0EyLXNlY3JldA==
|
||||
@@ -0,0 +1,80 @@
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: supersecret
|
||||
namespace: default
|
||||
|
||||
data:
|
||||
tls.crt: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUJxRENDQVU2Z0F3SUJBZ0lVWU9zcjBRZ0hPQnE0a1lSQ0w1K1REZFZ0NmJRd0NnWUlLb1pJemowRUF3SXcKRmpFVU1CSUdBMVVFQXd3TFpYaGhiWEJzWlM1amIyMHdIaGNOTWpVeE1ERXdNRGN4TnpNd1doY05NelV4TURBNApNRGN4TnpNd1dqQVdNUlF3RWdZRFZRUUREQXRsZUdGdGNHeGxMbU52YlRCWk1CTUdCeXFHU000OUFnRUdDQ3FHClNNNDlBd0VIQTBJQUJET3JpdzNaUTd3SWhXcmJQUzZKRlFUM2JUb05DRjAwdlNWNWZhYjZUYlh5TDh0bHNHcmUKVFJJRjJFd2dzdGVNT2t4R0tLU2xEdnVhRHdxOHAvcVYrMHVqZWpCNE1CMEdBMVVkRGdRV0JCUk1Fa3VleFhRaApVdERnUmcxS0J2NzJDRHErRXpBZkJnTlZIU01FR0RBV2dCUk1Fa3VleFhRaFV0RGdSZzFLQnY3MkNEcStFekFQCkJnTlZIUk1CQWY4RUJUQURBUUgvTUNVR0ExVWRFUVFlTUJ5Q0MyVjRZVzF3YkdVdVkyOXRnZzBxTG1WNFlXMXcKYkdVdVkyOXRNQW9HQ0NxR1NNNDlCQU1DQTBnQU1FVUNJUURzODdWazBzd0E2SGdPSmpST3llMW14RDgzcWNHeQpwZUZnb3hWOTNEeStjd0lnVjBNTUVKSmJWc1R5WkszRVErK1hjNXJFTDc4bnJKK1lJRVYrckNVV2o1VT0KLS0tLS1FTkQgQ0VSVElGSUNBVEUtLS0tLQ==
|
||||
tls.key: LS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0tCk1JR0hBZ0VBTUJNR0J5cUdTTTQ5QWdFR0NDcUdTTTQ5QXdFSEJHMHdhd0lCQVFRZ253Z0w1RFk0VUIxNHNNNmYKRGlrUWR0cWgyUVcxQXJmRjRmYzFVRnppZmRHaFJBTkNBQVF6cTRzTjJVTzhDSVZxMnowdWlSVUU5MjA2RFFoZApOTDBsZVgybStrMjE4aS9MWmJCcTNrMFNCZGhNSUxMWGpEcE1SaWlrcFE3N21nOEt2S2Y2bGZ0TAotLS0tLUVORCBQUklWQVRFIEtFWS0tLS0t
|
||||
|
||||
---
|
||||
kind: GatewayClass
|
||||
apiVersion: gateway.networking.k8s.io/v1
|
||||
metadata:
|
||||
name: my-gateway-class
|
||||
spec:
|
||||
controllerName: traefik.io/gateway-controller
|
||||
|
||||
---
|
||||
kind: Gateway
|
||||
apiVersion: gateway.networking.k8s.io/v1
|
||||
metadata:
|
||||
name: my-gateway
|
||||
namespace: default
|
||||
spec:
|
||||
gatewayClassName: my-gateway-class
|
||||
listeners: # Use GatewayClass defaults for listener definition.
|
||||
- name: tls
|
||||
protocol: TLS
|
||||
port: 9001
|
||||
hostname: foo.com
|
||||
tls:
|
||||
mode: Terminate # Default mode
|
||||
certificateRefs:
|
||||
- kind: Secret
|
||||
name: supersecret
|
||||
group: ""
|
||||
allowedRoutes:
|
||||
kinds:
|
||||
- kind: TLSRoute
|
||||
group: gateway.networking.k8s.io
|
||||
namespaces:
|
||||
from: Same
|
||||
|
||||
---
|
||||
kind: TLSRoute
|
||||
apiVersion: gateway.networking.k8s.io/v1
|
||||
metadata:
|
||||
name: tls-app-1
|
||||
namespace: default
|
||||
spec:
|
||||
parentRefs:
|
||||
- name: my-gateway
|
||||
kind: Gateway
|
||||
group: gateway.networking.k8s.io
|
||||
rules:
|
||||
- backendRefs:
|
||||
- name: whoami
|
||||
port: 80
|
||||
weight: 1
|
||||
|
||||
---
|
||||
kind: BackendTLSPolicy
|
||||
apiVersion: gateway.networking.k8s.io/v1
|
||||
metadata:
|
||||
name: policy-1
|
||||
namespace: default
|
||||
spec:
|
||||
targetRefs:
|
||||
- group: ""
|
||||
kind: Service
|
||||
name: whoami
|
||||
validation:
|
||||
hostname: whoami
|
||||
subjectAltNames:
|
||||
- type: Hostname
|
||||
hostname: whoami.default.svc.cluster.local
|
||||
- type: URI
|
||||
uri: spiffe://cluster.local/ns/default/sa/whoami
|
||||
@@ -0,0 +1,78 @@
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Secret
|
||||
metadata:
|
||||
name: supersecret
|
||||
namespace: default
|
||||
|
||||
data:
|
||||
tls.crt: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUJxRENDQVU2Z0F3SUJBZ0lVWU9zcjBRZ0hPQnE0a1lSQ0w1K1REZFZ0NmJRd0NnWUlLb1pJemowRUF3SXcKRmpFVU1CSUdBMVVFQXd3TFpYaGhiWEJzWlM1amIyMHdIaGNOTWpVeE1ERXdNRGN4TnpNd1doY05NelV4TURBNApNRGN4TnpNd1dqQVdNUlF3RWdZRFZRUUREQXRsZUdGdGNHeGxMbU52YlRCWk1CTUdCeXFHU000OUFnRUdDQ3FHClNNNDlBd0VIQTBJQUJET3JpdzNaUTd3SWhXcmJQUzZKRlFUM2JUb05DRjAwdlNWNWZhYjZUYlh5TDh0bHNHcmUKVFJJRjJFd2dzdGVNT2t4R0tLU2xEdnVhRHdxOHAvcVYrMHVqZWpCNE1CMEdBMVVkRGdRV0JCUk1Fa3VleFhRaApVdERnUmcxS0J2NzJDRHErRXpBZkJnTlZIU01FR0RBV2dCUk1Fa3VleFhRaFV0RGdSZzFLQnY3MkNEcStFekFQCkJnTlZIUk1CQWY4RUJUQURBUUgvTUNVR0ExVWRFUVFlTUJ5Q0MyVjRZVzF3YkdVdVkyOXRnZzBxTG1WNFlXMXcKYkdVdVkyOXRNQW9HQ0NxR1NNNDlCQU1DQTBnQU1FVUNJUURzODdWazBzd0E2SGdPSmpST3llMW14RDgzcWNHeQpwZUZnb3hWOTNEeStjd0lnVjBNTUVKSmJWc1R5WkszRVErK1hjNXJFTDc4bnJKK1lJRVYrckNVV2o1VT0KLS0tLS1FTkQgQ0VSVElGSUNBVEUtLS0tLQ==
|
||||
tls.key: LS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0tCk1JR0hBZ0VBTUJNR0J5cUdTTTQ5QWdFR0NDcUdTTTQ5QXdFSEJHMHdhd0lCQVFRZ253Z0w1RFk0VUIxNHNNNmYKRGlrUWR0cWgyUVcxQXJmRjRmYzFVRnppZmRHaFJBTkNBQVF6cTRzTjJVTzhDSVZxMnowdWlSVUU5MjA2RFFoZApOTDBsZVgybStrMjE4aS9MWmJCcTNrMFNCZGhNSUxMWGpEcE1SaWlrcFE3N21nOEt2S2Y2bGZ0TAotLS0tLUVORCBQUklWQVRFIEtFWS0tLS0t
|
||||
|
||||
---
|
||||
kind: GatewayClass
|
||||
apiVersion: gateway.networking.k8s.io/v1
|
||||
metadata:
|
||||
name: my-gateway-class
|
||||
spec:
|
||||
controllerName: traefik.io/gateway-controller
|
||||
|
||||
---
|
||||
kind: Gateway
|
||||
apiVersion: gateway.networking.k8s.io/v1
|
||||
metadata:
|
||||
name: my-gateway
|
||||
namespace: default
|
||||
spec:
|
||||
gatewayClassName: my-gateway-class
|
||||
listeners: # Use GatewayClass defaults for listener definition.
|
||||
- name: tls
|
||||
protocol: TLS
|
||||
port: 9001
|
||||
hostname: foo.com
|
||||
tls:
|
||||
mode: Terminate # Default mode
|
||||
certificateRefs:
|
||||
- kind: Secret
|
||||
name: supersecret
|
||||
group: ""
|
||||
allowedRoutes:
|
||||
kinds:
|
||||
- kind: TLSRoute
|
||||
group: gateway.networking.k8s.io
|
||||
namespaces:
|
||||
from: Same
|
||||
|
||||
---
|
||||
kind: TLSRoute
|
||||
apiVersion: gateway.networking.k8s.io/v1
|
||||
metadata:
|
||||
name: tls-app-1
|
||||
namespace: default
|
||||
spec:
|
||||
parentRefs:
|
||||
- name: my-gateway
|
||||
kind: Gateway
|
||||
group: gateway.networking.k8s.io
|
||||
rules:
|
||||
- backendRefs:
|
||||
- name: whoami
|
||||
port: 80
|
||||
weight: 1
|
||||
kind: Service
|
||||
group: ""
|
||||
|
||||
---
|
||||
kind: BackendTLSPolicy
|
||||
apiVersion: gateway.networking.k8s.io/v1
|
||||
metadata:
|
||||
name: policy-1
|
||||
namespace: default
|
||||
spec:
|
||||
targetRefs:
|
||||
- group: core
|
||||
kind: Service
|
||||
name: whoami
|
||||
validation:
|
||||
hostname: whoami
|
||||
wellKnownCACertificates: System
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"fmt"
|
||||
"net"
|
||||
"regexp"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
@@ -21,7 +22,7 @@ import (
|
||||
)
|
||||
|
||||
// TODO: as described in the specification https://gateway-api.sigs.k8s.io/reference/spec/#gateway.networking.k8s.io%2fv1.GRPCRoute, we should check for hostname conflicts between HTTP and GRPC routes.
|
||||
func (p *Provider) loadGRPCRoutes(ctx context.Context, gatewayListeners []gatewayListener, conf *dynamic.Configuration) {
|
||||
func (p *Provider) loadGRPCRoutes(ctx context.Context, gatewayListeners []gatewayListener, conf *dynamic.Configuration, statusReport *statusReport) {
|
||||
routes, err := p.client.ListGRPCRoutes()
|
||||
if err != nil {
|
||||
log.Ctx(ctx).Error().Err(err).Msg("Unable to list GRPCRoutes")
|
||||
@@ -39,9 +40,8 @@ func (p *Provider) loadGRPCRoutes(ctx context.Context, gatewayListeners []gatewa
|
||||
continue
|
||||
}
|
||||
|
||||
var parentStatuses []gatev1.RouteParentStatus
|
||||
for _, parentRef := range route.Spec.ParentRefs {
|
||||
parentStatus := &gatev1.RouteParentStatus{
|
||||
parentStatus := gatev1.RouteParentStatus{
|
||||
ParentRef: parentRef,
|
||||
ControllerName: controllerName,
|
||||
Conditions: []metav1.Condition{
|
||||
@@ -77,7 +77,7 @@ func (p *Provider) loadGRPCRoutes(ctx context.Context, gatewayListeners []gatewa
|
||||
}
|
||||
}
|
||||
|
||||
routeConf, resolveRefCondition := p.loadGRPCRoute(logger.WithContext(ctx), listener, route, hostnames)
|
||||
routeConf, resolveRefCondition := p.loadGRPCRoute(logger.WithContext(ctx), listener, route, hostnames, statusReport)
|
||||
if accepted && listener.Attached {
|
||||
mergeHTTPConfiguration(routeConf, conf)
|
||||
}
|
||||
@@ -85,23 +85,12 @@ func (p *Provider) loadGRPCRoutes(ctx context.Context, gatewayListeners []gatewa
|
||||
parentStatus.Conditions = upsertRouteConditionResolvedRefs(parentStatus.Conditions, resolveRefCondition)
|
||||
}
|
||||
|
||||
parentStatuses = append(parentStatuses, *parentStatus)
|
||||
}
|
||||
|
||||
status := gatev1.GRPCRouteStatus{
|
||||
RouteStatus: gatev1.RouteStatus{
|
||||
Parents: parentStatuses,
|
||||
},
|
||||
}
|
||||
if err := p.client.UpdateGRPCRouteStatus(ctx, ktypes.NamespacedName{Namespace: route.Namespace, Name: route.Name}, status); err != nil {
|
||||
logger.Warn().
|
||||
Err(err).
|
||||
Msg("Unable to update GRPCRoute status")
|
||||
statusReport.RecordGRPCRouteStatus(ktypes.NamespacedName{Namespace: route.Namespace, Name: route.Name}, parentStatus)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Provider) loadGRPCRoute(ctx context.Context, listener gatewayListener, route *gatev1.GRPCRoute, hostnames []gatev1.Hostname) (*dynamic.Configuration, metav1.Condition) {
|
||||
func (p *Provider) loadGRPCRoute(ctx context.Context, listener gatewayListener, route *gatev1.GRPCRoute, hostnames []gatev1.Hostname, statusReport *statusReport) (*dynamic.Configuration, metav1.Condition) {
|
||||
conf := &dynamic.Configuration{
|
||||
HTTP: &dynamic.HTTPConfiguration{
|
||||
Routers: make(map[string]*dynamic.Router),
|
||||
@@ -168,7 +157,7 @@ func (p *Provider) loadGRPCRoute(ctx context.Context, listener gatewayListener,
|
||||
|
||||
default:
|
||||
var serviceCondition *metav1.Condition
|
||||
router.Service, serviceCondition = p.loadGRPCService(conf, routerName, routeRule, route)
|
||||
router.Service, serviceCondition = p.loadGRPCService(listener, conf, routerName, routeRule, route, statusReport)
|
||||
if serviceCondition != nil {
|
||||
condition = *serviceCondition
|
||||
}
|
||||
@@ -181,7 +170,7 @@ func (p *Provider) loadGRPCRoute(ctx context.Context, listener gatewayListener,
|
||||
return conf, condition
|
||||
}
|
||||
|
||||
func (p *Provider) loadGRPCService(conf *dynamic.Configuration, routeKey string, routeRule gatev1.GRPCRouteRule, route *gatev1.GRPCRoute) (string, *metav1.Condition) {
|
||||
func (p *Provider) loadGRPCService(listener gatewayListener, conf *dynamic.Configuration, routeKey string, routeRule gatev1.GRPCRouteRule, route *gatev1.GRPCRoute, statusReport *statusReport) (string, *metav1.Condition) {
|
||||
name := routeKey + "-wrr"
|
||||
if _, ok := conf.HTTP.Services[name]; ok {
|
||||
return name, nil
|
||||
@@ -190,7 +179,7 @@ func (p *Provider) loadGRPCService(conf *dynamic.Configuration, routeKey string,
|
||||
var wrr dynamic.WeightedRoundRobin
|
||||
var condition *metav1.Condition
|
||||
for _, backendRef := range routeRule.BackendRefs {
|
||||
svcName, svc, errCondition := p.loadGRPCBackendRef(route, backendRef)
|
||||
svcName, svc, errCondition := p.loadGRPCBackendRef(listener, conf, route, backendRef, statusReport)
|
||||
weight := ptr.To(int(ptr.Deref(backendRef.Weight, 1)))
|
||||
if errCondition != nil {
|
||||
condition = errCondition
|
||||
@@ -219,7 +208,7 @@ func (p *Provider) loadGRPCService(conf *dynamic.Configuration, routeKey string,
|
||||
return name, condition
|
||||
}
|
||||
|
||||
func (p *Provider) loadGRPCBackendRef(route *gatev1.GRPCRoute, backendRef gatev1.GRPCBackendRef) (string, *dynamic.Service, *metav1.Condition) {
|
||||
func (p *Provider) loadGRPCBackendRef(listener gatewayListener, conf *dynamic.Configuration, route *gatev1.GRPCRoute, backendRef gatev1.GRPCBackendRef, statusReport *statusReport) (string, *dynamic.Service, *metav1.Condition) {
|
||||
kind := ptr.Deref(backendRef.Kind, kindService)
|
||||
|
||||
group := groupCore
|
||||
@@ -271,11 +260,16 @@ func (p *Provider) loadGRPCBackendRef(route *gatev1.GRPCRoute, backendRef gatev1
|
||||
portStr := strconv.FormatInt(int64(port), 10)
|
||||
serviceName = provider.Normalize(serviceName + "-" + portStr + "-grpc")
|
||||
|
||||
lb, errCondition := p.loadGRPCServers(namespace, route, backendRef)
|
||||
lb, st, errCondition := p.loadGRPCServers(namespace, route, backendRef, listener, statusReport)
|
||||
if errCondition != nil {
|
||||
return serviceName, nil, errCondition
|
||||
}
|
||||
|
||||
if st != nil {
|
||||
lb.ServersTransport = serviceName
|
||||
conf.HTTP.ServersTransports[serviceName] = st
|
||||
}
|
||||
|
||||
return serviceName, &dynamic.Service{LoadBalancer: lb}, nil
|
||||
}
|
||||
|
||||
@@ -325,10 +319,10 @@ func (p *Provider) loadGRPCMiddlewares(conf *dynamic.Configuration, namespace, r
|
||||
return middlewareNames, nil
|
||||
}
|
||||
|
||||
func (p *Provider) loadGRPCServers(namespace string, route *gatev1.GRPCRoute, backendRef gatev1.GRPCBackendRef) (*dynamic.ServersLoadBalancer, *metav1.Condition) {
|
||||
func (p *Provider) loadGRPCServers(namespace string, route *gatev1.GRPCRoute, backendRef gatev1.GRPCBackendRef, listener gatewayListener, statusReport *statusReport) (*dynamic.ServersLoadBalancer, *dynamic.ServersTransport, *metav1.Condition) {
|
||||
backendAddresses, svcPort, err := p.getBackendAddresses(namespace, backendRef.BackendRef)
|
||||
if err != nil {
|
||||
return nil, &metav1.Condition{
|
||||
return nil, nil, &metav1.Condition{
|
||||
Type: string(gatev1.RouteConditionResolvedRefs),
|
||||
Status: metav1.ConditionFalse,
|
||||
ObservedGeneration: route.Generation,
|
||||
@@ -338,26 +332,128 @@ func (p *Provider) loadGRPCServers(namespace string, route *gatev1.GRPCRoute, ba
|
||||
}
|
||||
}
|
||||
|
||||
if svcPort.Protocol != corev1.ProtocolTCP {
|
||||
return nil, &metav1.Condition{
|
||||
backendTLSPolicies, err := p.client.ListBackendTLSPoliciesForService(namespace, string(backendRef.Name))
|
||||
if err != nil {
|
||||
return nil, nil, &metav1.Condition{
|
||||
Type: string(gatev1.RouteConditionResolvedRefs),
|
||||
Status: metav1.ConditionFalse,
|
||||
ObservedGeneration: route.Generation,
|
||||
LastTransitionTime: metav1.Now(),
|
||||
Reason: string(gatev1.RouteReasonUnsupportedProtocol),
|
||||
Message: fmt.Sprintf("Cannot load GRPCBackendRef %s/%s: only TCP protocol is supported", namespace, backendRef.Name),
|
||||
Reason: string(gatev1.RouteReasonRefNotPermitted),
|
||||
Message: fmt.Sprintf("Cannot list BackendTLSPolicies for Service %s/%s: %s", namespace, string(backendRef.Name), err),
|
||||
}
|
||||
}
|
||||
|
||||
protocol, err := getGRPCServiceProtocol(svcPort)
|
||||
if err != nil {
|
||||
return nil, &metav1.Condition{
|
||||
Type: string(gatev1.RouteConditionResolvedRefs),
|
||||
Status: metav1.ConditionFalse,
|
||||
ObservedGeneration: route.Generation,
|
||||
LastTransitionTime: metav1.Now(),
|
||||
Reason: string(gatev1.RouteReasonUnsupportedProtocol),
|
||||
Message: fmt.Sprintf("Cannot load GRPCBackendRef %s/%s: only \"kubernetes.io/h2c\" and \"https\" appProtocol is supported", namespace, backendRef.Name),
|
||||
// Sort BackendTLSPolicies by creation timestamp, then by name to match the BackendTLSPolicy requirements.
|
||||
slices.SortStableFunc(backendTLSPolicies, func(a, b *gatev1.BackendTLSPolicy) int {
|
||||
cmpTime := a.CreationTimestamp.Time.Compare(b.CreationTimestamp.Time)
|
||||
if cmpTime == 0 {
|
||||
return strings.Compare(a.Name, b.Name)
|
||||
}
|
||||
return cmpTime
|
||||
})
|
||||
|
||||
var serversTransport *dynamic.ServersTransport
|
||||
for _, policy := range backendTLSPolicies {
|
||||
for _, targetRef := range policy.Spec.TargetRefs {
|
||||
// Skip targetRefs that doesn't match the backendRef,
|
||||
// since a BackendTLSPolicy can select multiple services.
|
||||
if targetRef.Name != backendRef.Name {
|
||||
continue
|
||||
}
|
||||
// Skip the targetRef if the sectionName doesn't match the backendRef port.
|
||||
if targetRef.SectionName != nil && svcPort.Name != string(*targetRef.SectionName) {
|
||||
continue
|
||||
}
|
||||
|
||||
policyAncestorStatus := gatev1.PolicyAncestorStatus{
|
||||
AncestorRef: gatev1.ParentReference{
|
||||
Group: ptr.To(gatev1.Group(groupGateway)),
|
||||
Kind: ptr.To(gatev1.Kind(kindGateway)),
|
||||
Namespace: ptr.To(gatev1.Namespace(namespace)),
|
||||
Name: gatev1.ObjectName(listener.GWName),
|
||||
SectionName: ptr.To(gatev1.SectionName(listener.Name)),
|
||||
},
|
||||
ControllerName: controllerName,
|
||||
}
|
||||
|
||||
// Multiple BackendTLSPolicies can match the same service port, meaning that there is a conflict.
|
||||
if serversTransport != nil {
|
||||
policyAncestorStatus.Conditions = append(policyAncestorStatus.Conditions,
|
||||
metav1.Condition{
|
||||
Type: string(gatev1.BackendTLSPolicyConditionResolvedRefs),
|
||||
Status: metav1.ConditionFalse,
|
||||
ObservedGeneration: policy.Generation,
|
||||
LastTransitionTime: metav1.Now(),
|
||||
Reason: string(gatev1.BackendTLSPolicyReasonResolvedRefs),
|
||||
},
|
||||
metav1.Condition{
|
||||
Type: string(gatev1.PolicyConditionAccepted),
|
||||
Status: metav1.ConditionFalse,
|
||||
ObservedGeneration: policy.Generation,
|
||||
LastTransitionTime: metav1.Now(),
|
||||
Reason: string(gatev1.PolicyReasonConflicted),
|
||||
},
|
||||
)
|
||||
|
||||
statusReport.RecordBackendTLSPolicyStatus(ktypes.NamespacedName{Namespace: policy.Namespace, Name: policy.Name}, policyAncestorStatus)
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
var resolvedRefCondition metav1.Condition
|
||||
serversTransport, resolvedRefCondition = p.loadServersTransport(namespace, policy)
|
||||
|
||||
policyAncestorStatus.Conditions = append(policyAncestorStatus.Conditions, resolvedRefCondition)
|
||||
if resolvedRefCondition.Status == metav1.ConditionFalse {
|
||||
policyAncestorStatus.Conditions = append(policyAncestorStatus.Conditions, metav1.Condition{
|
||||
Type: string(gatev1.PolicyConditionAccepted),
|
||||
Status: metav1.ConditionFalse,
|
||||
ObservedGeneration: policy.Generation,
|
||||
LastTransitionTime: metav1.Now(),
|
||||
Reason: string(gatev1.BackendTLSPolicyReasonNoValidCACertificate),
|
||||
})
|
||||
} else {
|
||||
policyAncestorStatus.Conditions = append(policyAncestorStatus.Conditions, metav1.Condition{
|
||||
Type: string(gatev1.PolicyConditionAccepted),
|
||||
Status: metav1.ConditionTrue,
|
||||
ObservedGeneration: policy.Generation,
|
||||
LastTransitionTime: metav1.Now(),
|
||||
Reason: string(gatev1.PolicyReasonAccepted),
|
||||
})
|
||||
}
|
||||
|
||||
statusReport.RecordBackendTLSPolicyStatus(ktypes.NamespacedName{Namespace: policy.Namespace, Name: policy.Name}, policyAncestorStatus)
|
||||
|
||||
// When something went wrong during the loading of a ServersTransport,
|
||||
// we stop here and return a route condition error.
|
||||
if resolvedRefCondition.Status == metav1.ConditionFalse {
|
||||
return nil, nil, &metav1.Condition{
|
||||
Type: string(gatev1.RouteConditionResolvedRefs),
|
||||
Status: metav1.ConditionFalse,
|
||||
ObservedGeneration: route.Generation,
|
||||
LastTransitionTime: metav1.Now(),
|
||||
Reason: string(gatev1.RouteReasonRefNotPermitted),
|
||||
Message: fmt.Sprintf("Cannot apply BackendTLSPolicy for Service %s/%s: %s", namespace, string(backendRef.Name), resolvedRefCondition.Message),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If a ServersTransport is set, it means a BackendTLSPolicy matched the service port, and we can safely assume the protocol is HTTPS.
|
||||
// When no ServersTransport is set, we need to determine the protocol based on the service port.
|
||||
protocol := "https"
|
||||
if serversTransport == nil {
|
||||
protocol, err = getGRPCServiceProtocol(svcPort)
|
||||
if err != nil {
|
||||
return nil, nil, &metav1.Condition{
|
||||
Type: string(gatev1.RouteConditionResolvedRefs),
|
||||
Status: metav1.ConditionFalse,
|
||||
ObservedGeneration: route.Generation,
|
||||
LastTransitionTime: metav1.Now(),
|
||||
Reason: string(gatev1.RouteReasonUnsupportedProtocol),
|
||||
Message: fmt.Sprintf("Cannot load GRPCBackendRef %s/%s: %s", namespace, backendRef.Name, err),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -369,7 +465,8 @@ func (p *Provider) loadGRPCServers(namespace string, route *gatev1.GRPCRoute, ba
|
||||
URL: fmt.Sprintf("%s://%s", protocol, net.JoinHostPort(ba.IP, strconv.Itoa(int(ba.Port)))),
|
||||
})
|
||||
}
|
||||
return lb, nil
|
||||
|
||||
return lb, serversTransport, nil
|
||||
}
|
||||
|
||||
func buildGRPCMatchRule(hostnames []gatev1.Hostname, match gatev1.GRPCRouteMatch) (string, int) {
|
||||
|
||||
@@ -15,6 +15,7 @@ import (
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/traefik/traefik/v3/pkg/config/dynamic"
|
||||
"github.com/traefik/traefik/v3/pkg/provider"
|
||||
"github.com/traefik/traefik/v3/pkg/tls"
|
||||
"github.com/traefik/traefik/v3/pkg/types"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
@@ -23,7 +24,7 @@ import (
|
||||
gatev1 "sigs.k8s.io/gateway-api/apis/v1"
|
||||
)
|
||||
|
||||
func (p *Provider) loadHTTPRoutes(ctx context.Context, gatewayListeners []gatewayListener, conf *dynamic.Configuration) {
|
||||
func (p *Provider) loadHTTPRoutes(ctx context.Context, gatewayListeners []gatewayListener, conf *dynamic.Configuration, statusReport *statusReport) {
|
||||
routes, err := p.client.ListHTTPRoutes()
|
||||
if err != nil {
|
||||
log.Ctx(ctx).Error().Err(err).Msg("Unable to list HTTPRoutes")
|
||||
@@ -41,9 +42,8 @@ func (p *Provider) loadHTTPRoutes(ctx context.Context, gatewayListeners []gatewa
|
||||
continue
|
||||
}
|
||||
|
||||
var parentStatuses []gatev1.RouteParentStatus
|
||||
for _, parentRef := range route.Spec.ParentRefs {
|
||||
parentStatus := &gatev1.RouteParentStatus{
|
||||
parentStatus := gatev1.RouteParentStatus{
|
||||
ParentRef: parentRef,
|
||||
ControllerName: controllerName,
|
||||
Conditions: []metav1.Condition{
|
||||
@@ -79,7 +79,7 @@ func (p *Provider) loadHTTPRoutes(ctx context.Context, gatewayListeners []gatewa
|
||||
}
|
||||
}
|
||||
|
||||
routeConf, resolveRefCondition := p.loadHTTPRoute(logger.WithContext(ctx), listener, route, hostnames)
|
||||
routeConf, resolveRefCondition := p.loadHTTPRoute(logger.WithContext(ctx), listener, route, hostnames, statusReport)
|
||||
if accepted && listener.Attached {
|
||||
mergeHTTPConfiguration(routeConf, conf)
|
||||
}
|
||||
@@ -87,23 +87,12 @@ func (p *Provider) loadHTTPRoutes(ctx context.Context, gatewayListeners []gatewa
|
||||
parentStatus.Conditions = upsertRouteConditionResolvedRefs(parentStatus.Conditions, resolveRefCondition)
|
||||
}
|
||||
|
||||
parentStatuses = append(parentStatuses, *parentStatus)
|
||||
}
|
||||
|
||||
status := gatev1.HTTPRouteStatus{
|
||||
RouteStatus: gatev1.RouteStatus{
|
||||
Parents: parentStatuses,
|
||||
},
|
||||
}
|
||||
if err := p.client.UpdateHTTPRouteStatus(ctx, ktypes.NamespacedName{Namespace: route.Namespace, Name: route.Name}, status); err != nil {
|
||||
logger.Warn().
|
||||
Err(err).
|
||||
Msg("Unable to update HTTPRoute status")
|
||||
statusReport.RecordHTTPRouteStatus(ktypes.NamespacedName{Namespace: route.Namespace, Name: route.Name}, parentStatus)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Provider) loadHTTPRoute(ctx context.Context, listener gatewayListener, route *gatev1.HTTPRoute, hostnames []gatev1.Hostname) (*dynamic.Configuration, metav1.Condition) {
|
||||
func (p *Provider) loadHTTPRoute(ctx context.Context, listener gatewayListener, route *gatev1.HTTPRoute, hostnames []gatev1.Hostname, statusReport *statusReport) (*dynamic.Configuration, metav1.Condition) {
|
||||
conf := &dynamic.Configuration{
|
||||
HTTP: &dynamic.HTTPConfiguration{
|
||||
Routers: make(map[string]*dynamic.Router),
|
||||
@@ -176,7 +165,7 @@ func (p *Provider) loadHTTPRoute(ctx context.Context, listener gatewayListener,
|
||||
|
||||
default:
|
||||
var serviceCondition *metav1.Condition
|
||||
router.Service, serviceCondition = p.loadWRRService(ctx, listener, conf, routerName, routeRule, route, match.Path)
|
||||
router.Service, serviceCondition = p.loadWRRService(ctx, listener, conf, routerName, routeRule, route, match.Path, statusReport)
|
||||
if serviceCondition != nil {
|
||||
condition = *serviceCondition
|
||||
}
|
||||
@@ -191,7 +180,7 @@ func (p *Provider) loadHTTPRoute(ctx context.Context, listener gatewayListener,
|
||||
return conf, condition
|
||||
}
|
||||
|
||||
func (p *Provider) loadWRRService(ctx context.Context, listener gatewayListener, conf *dynamic.Configuration, routeKey string, routeRule gatev1.HTTPRouteRule, route *gatev1.HTTPRoute, pathMatch *gatev1.HTTPPathMatch) (string, *metav1.Condition) {
|
||||
func (p *Provider) loadWRRService(ctx context.Context, listener gatewayListener, conf *dynamic.Configuration, routeKey string, routeRule gatev1.HTTPRouteRule, route *gatev1.HTTPRoute, pathMatch *gatev1.HTTPPathMatch, statusReport *statusReport) (string, *metav1.Condition) {
|
||||
name := routeKey + "-wrr"
|
||||
if _, ok := conf.HTTP.Services[name]; ok {
|
||||
return name, nil
|
||||
@@ -202,7 +191,7 @@ func (p *Provider) loadWRRService(ctx context.Context, listener gatewayListener,
|
||||
for _, backendRef := range routeRule.BackendRefs {
|
||||
// TODO in loadService we need to always return a non-nil serviceName even when there is an error which is not the
|
||||
// usual defacto.
|
||||
svcName, errCondition := p.loadService(ctx, listener, conf, route, backendRef, pathMatch)
|
||||
svcName, errCondition := p.loadService(listener, conf, route, backendRef, pathMatch, statusReport)
|
||||
weight := ptr.To(int(ptr.Deref(backendRef.Weight, 1)))
|
||||
if errCondition != nil {
|
||||
log.Ctx(ctx).Error().
|
||||
@@ -229,7 +218,7 @@ func (p *Provider) loadWRRService(ctx context.Context, listener gatewayListener,
|
||||
|
||||
// loadService returns a dynamic.Service config corresponding to the given gatev1.HTTPBackendRef.
|
||||
// Note that the returned dynamic.Service config can be nil (for cross-provider, internal services, and backendFunc).
|
||||
func (p *Provider) loadService(ctx context.Context, listener gatewayListener, conf *dynamic.Configuration, route *gatev1.HTTPRoute, backendRef gatev1.HTTPBackendRef, pathMatch *gatev1.HTTPPathMatch) (string, *metav1.Condition) {
|
||||
func (p *Provider) loadService(listener gatewayListener, conf *dynamic.Configuration, route *gatev1.HTTPRoute, backendRef gatev1.HTTPBackendRef, pathMatch *gatev1.HTTPPathMatch, statusReport *statusReport) (string, *metav1.Condition) {
|
||||
kind := ptr.Deref(backendRef.Kind, kindService)
|
||||
|
||||
group := groupCore
|
||||
@@ -240,6 +229,17 @@ func (p *Provider) loadService(ctx context.Context, listener gatewayListener, co
|
||||
namespace := route.Namespace
|
||||
if backendRef.Namespace != nil && *backendRef.Namespace != "" {
|
||||
namespace = string(*backendRef.Namespace)
|
||||
|
||||
if strings.Contains(string(backendRef.Name), "@") {
|
||||
return provider.Normalize(namespace + "-" + string(backendRef.Name) + "-http"), &metav1.Condition{
|
||||
Type: string(gatev1.RouteConditionResolvedRefs),
|
||||
Status: metav1.ConditionFalse,
|
||||
ObservedGeneration: route.Generation,
|
||||
LastTransitionTime: metav1.Now(),
|
||||
Reason: string(gatev1.RouteReasonRefNotPermitted),
|
||||
Message: fmt.Sprintf("Cannot load HTTPBackendRef %s/%s/%s/%s: namespace is not allowed with a cross-provider reference", group, kind, namespace, backendRef.Name),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
serviceName := provider.Normalize(namespace + "-" + string(backendRef.Name) + "-http")
|
||||
@@ -304,7 +304,7 @@ func (p *Provider) loadService(ctx context.Context, listener gatewayListener, co
|
||||
portStr := strconv.FormatInt(int64(port), 10)
|
||||
serviceName = provider.Normalize(serviceName + "-" + portStr)
|
||||
|
||||
lb, st, errCondition := p.loadHTTPServers(ctx, namespace, route, backendRef, listener)
|
||||
lb, st, errCondition := p.loadHTTPServers(namespace, route, backendRef, listener, statusReport)
|
||||
if errCondition != nil {
|
||||
return serviceName, errCondition
|
||||
}
|
||||
@@ -431,7 +431,7 @@ func (p *Provider) loadHTTPRouteFilterExtensionRef(namespace string, extensionRe
|
||||
return filterFunc(string(extensionRef.Name), namespace)
|
||||
}
|
||||
|
||||
func (p *Provider) loadHTTPServers(ctx context.Context, namespace string, route *gatev1.HTTPRoute, backendRef gatev1.HTTPBackendRef, listener gatewayListener) (*dynamic.ServersLoadBalancer, *dynamic.ServersTransport, *metav1.Condition) {
|
||||
func (p *Provider) loadHTTPServers(namespace string, route *gatev1.HTTPRoute, backendRef gatev1.HTTPBackendRef, listener gatewayListener, statusReport *statusReport) (*dynamic.ServersLoadBalancer, *dynamic.ServersTransport, *metav1.Condition) {
|
||||
backendAddresses, svcPort, err := p.getBackendAddresses(namespace, backendRef.BackendRef)
|
||||
if err != nil {
|
||||
return nil, nil, &metav1.Condition{
|
||||
@@ -508,12 +508,7 @@ func (p *Provider) loadHTTPServers(ctx context.Context, namespace string, route
|
||||
},
|
||||
)
|
||||
|
||||
status := gatev1.PolicyStatus{
|
||||
Ancestors: []gatev1.PolicyAncestorStatus{policyAncestorStatus},
|
||||
}
|
||||
if err := p.client.UpdateBackendTLSPolicyStatus(ctx, ktypes.NamespacedName{Namespace: policy.Namespace, Name: policy.Name}, status); err != nil {
|
||||
log.Ctx(ctx).Warn().Err(err).Msg("Unable to update conflicting BackendTLSPolicy status")
|
||||
}
|
||||
statusReport.RecordBackendTLSPolicyStatus(ktypes.NamespacedName{Namespace: policy.Namespace, Name: policy.Name}, policyAncestorStatus)
|
||||
|
||||
continue
|
||||
}
|
||||
@@ -540,12 +535,7 @@ func (p *Provider) loadHTTPServers(ctx context.Context, namespace string, route
|
||||
})
|
||||
}
|
||||
|
||||
status := gatev1.PolicyStatus{
|
||||
Ancestors: []gatev1.PolicyAncestorStatus{policyAncestorStatus},
|
||||
}
|
||||
if err := p.client.UpdateBackendTLSPolicyStatus(ctx, ktypes.NamespacedName{Namespace: policy.Namespace, Name: policy.Name}, status); err != nil {
|
||||
log.Ctx(ctx).Warn().Err(err).Msg("Unable to update BackendTLSPolicy status")
|
||||
}
|
||||
statusReport.RecordBackendTLSPolicyStatus(ktypes.NamespacedName{Namespace: policy.Namespace, Name: policy.Name}, policyAncestorStatus)
|
||||
|
||||
// When something wen wrong during the loading of a ServersTransport,
|
||||
// we stop here and return a route condition error.
|
||||
@@ -595,6 +585,37 @@ func (p *Provider) loadServersTransport(namespace string, policy *gatev1.Backend
|
||||
ServerName: string(policy.Spec.Validation.Hostname),
|
||||
}
|
||||
|
||||
if len(policy.Spec.Validation.SubjectAltNames) > 0 {
|
||||
// Per the Gateway API specification the Hostname should only be used for authentication
|
||||
// and not for certificate validation. Thus, if SubjectAltNames is specified, we ignore
|
||||
// the Hostname validation by setting the InsecureSkipVerify option to true.
|
||||
st.InsecureSkipVerify = true
|
||||
|
||||
for _, san := range policy.Spec.Validation.SubjectAltNames {
|
||||
switch san.Type {
|
||||
case gatev1.URISubjectAltNameType:
|
||||
st.PeerCertSANs = append(st.PeerCertSANs, tls.SAN{
|
||||
Type: tls.SANURIType,
|
||||
Value: string(san.URI),
|
||||
})
|
||||
case gatev1.HostnameSubjectAltNameType:
|
||||
st.PeerCertSANs = append(st.PeerCertSANs, tls.SAN{
|
||||
Type: tls.SANDNSNameType,
|
||||
Value: string(san.Hostname),
|
||||
})
|
||||
default:
|
||||
return nil, metav1.Condition{
|
||||
Type: string(gatev1.BackendTLSPolicyConditionResolvedRefs),
|
||||
Status: metav1.ConditionFalse,
|
||||
ObservedGeneration: policy.Generation,
|
||||
LastTransitionTime: metav1.Now(),
|
||||
Reason: string(gatev1.BackendTLSPolicyReasonInvalidKind),
|
||||
Message: fmt.Sprintf("Unsupported SubjectAltName type %q; only URI and Hostname types are supported", san.Type),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if policy.Spec.Validation.WellKnownCACertificates != nil {
|
||||
return st, metav1.Condition{
|
||||
Type: string(gatev1.BackendTLSPolicyConditionResolvedRefs),
|
||||
@@ -606,7 +627,7 @@ func (p *Provider) loadServersTransport(namespace string, policy *gatev1.Backend
|
||||
}
|
||||
|
||||
for _, caCertRef := range policy.Spec.Validation.CACertificateRefs {
|
||||
if (caCertRef.Group != "" && caCertRef.Group != groupCore) || (caCertRef.Kind != "ConfigMap" && caCertRef.Kind != "Secret") {
|
||||
if (caCertRef.Group != "" && caCertRef.Group != groupCore) || (caCertRef.Kind != kindConfigMap && caCertRef.Kind != kindSecret) {
|
||||
return nil, metav1.Condition{
|
||||
Type: string(gatev1.BackendTLSPolicyConditionResolvedRefs),
|
||||
Status: metav1.ConditionFalse,
|
||||
@@ -619,7 +640,7 @@ func (p *Provider) loadServersTransport(namespace string, policy *gatev1.Backend
|
||||
|
||||
var caCRT string
|
||||
switch caCertRef.Kind {
|
||||
case "ConfigMap":
|
||||
case kindConfigMap:
|
||||
configmap, err := p.client.GetConfigMap(namespace, string(caCertRef.Name))
|
||||
if err != nil {
|
||||
return nil, metav1.Condition{
|
||||
@@ -632,7 +653,7 @@ func (p *Provider) loadServersTransport(namespace string, policy *gatev1.Backend
|
||||
}
|
||||
}
|
||||
caCRT = configmap.Data["ca.crt"]
|
||||
case "Secret":
|
||||
case kindSecret:
|
||||
secret, err := p.client.GetSecret(namespace, string(caCertRef.Name))
|
||||
if err != nil {
|
||||
return nil, metav1.Condition{
|
||||
|
||||
@@ -49,6 +49,8 @@ const (
|
||||
kindTCPRoute = "TCPRoute"
|
||||
kindTLSRoute = "TLSRoute"
|
||||
kindService = "Service"
|
||||
kindConfigMap = "ConfigMap"
|
||||
kindSecret = "Secret"
|
||||
|
||||
appProtocolHTTP = "http"
|
||||
appProtocolHTTPS = "https"
|
||||
@@ -222,20 +224,29 @@ func (p *Provider) Provide(configurationChan chan<- dynamic.Message, pool *safe.
|
||||
// Note that event is the *first* event that came in during this throttling interval -- if we're hitting our throttle, we may have dropped events.
|
||||
// This is fine, because we don't treat different event types differently.
|
||||
// But if we do in the future, we'll need to track more information about the dropped events.
|
||||
conf := p.loadConfigurationFromGateways(ctxLog)
|
||||
|
||||
confHash, err := hashstructure.Hash(conf, nil)
|
||||
switch {
|
||||
case err != nil:
|
||||
logger.Error().Msg("Unable to hash the configuration")
|
||||
case p.lastConfiguration.Get() == confHash:
|
||||
logger.Debug().Msgf("Skipping Kubernetes event kind %T", event)
|
||||
default:
|
||||
p.lastConfiguration.Set(confHash)
|
||||
configurationChan <- dynamic.Message{
|
||||
ProviderName: ProviderName,
|
||||
Configuration: conf,
|
||||
conf, statusReport, err := p.loadConfigurationFromGateways(ctxLog)
|
||||
if err != nil {
|
||||
logger.Error().Err(err).Msg("Unable to load configuration from Gateways")
|
||||
} else {
|
||||
confHash, err := hashstructure.Hash(conf, nil)
|
||||
switch {
|
||||
case err != nil:
|
||||
logger.Error().Msg("Unable to hash the configuration")
|
||||
case p.lastConfiguration.Get() == confHash:
|
||||
logger.Debug().Msgf("Skipping Kubernetes event kind %T", event)
|
||||
default:
|
||||
p.lastConfiguration.Set(confHash)
|
||||
configurationChan <- dynamic.Message{
|
||||
ProviderName: ProviderName,
|
||||
Configuration: conf,
|
||||
}
|
||||
}
|
||||
|
||||
// Flush regardless of whether the dynamic configuration changed: the
|
||||
// statusReport is independent of confHash and may carry writes even
|
||||
// when the data plane has nothing new to consume (e.g. a GatewayClass
|
||||
// that's now Accepted but has no Gateway pointing at it yet).
|
||||
statusReport.Flush(ctxLog, p.client)
|
||||
}
|
||||
|
||||
// If we're throttling,
|
||||
@@ -302,7 +313,8 @@ func (p *Provider) newK8sClient(ctx context.Context) (*clientWrapper, error) {
|
||||
}
|
||||
|
||||
// TODO Handle errors and update resources statuses (gatewayClass, gateway).
|
||||
func (p *Provider) loadConfigurationFromGateways(ctx context.Context) *dynamic.Configuration {
|
||||
func (p *Provider) loadConfigurationFromGateways(ctx context.Context) (*dynamic.Configuration, *statusReport, error) {
|
||||
statusReport := newStatusReport()
|
||||
conf := &dynamic.Configuration{
|
||||
HTTP: &dynamic.HTTPConfiguration{
|
||||
Routers: map[string]*dynamic.Router{},
|
||||
@@ -325,14 +337,12 @@ func (p *Provider) loadConfigurationFromGateways(ctx context.Context) *dynamic.C
|
||||
|
||||
addresses, err := p.gatewayAddresses()
|
||||
if err != nil {
|
||||
log.Ctx(ctx).Error().Err(err).Msg("Unable to get Gateway status addresses")
|
||||
return nil
|
||||
return nil, nil, fmt.Errorf("getting gateway addresses: %w", err)
|
||||
}
|
||||
|
||||
gatewayClasses, err := p.client.ListGatewayClasses()
|
||||
if err != nil {
|
||||
log.Ctx(ctx).Error().Err(err).Msg("Unable to list GatewayClasses")
|
||||
return nil
|
||||
return nil, nil, fmt.Errorf("listing gateway classes: %w", err)
|
||||
}
|
||||
|
||||
var supportedFeatures []gatev1.SupportedFeature
|
||||
@@ -363,13 +373,7 @@ func (p *Provider) loadConfigurationFromGateways(ctx context.Context) *dynamic.C
|
||||
SupportedFeatures: supportedFeatures,
|
||||
}
|
||||
|
||||
if err := p.client.UpdateGatewayClassStatus(ctx, gatewayClass.Name, status); err != nil {
|
||||
log.Ctx(ctx).
|
||||
Warn().
|
||||
Err(err).
|
||||
Str("gateway_class", gatewayClass.Name).
|
||||
Msg("Unable to update GatewayClass status")
|
||||
}
|
||||
statusReport.RecordGatewayClassStatus(gatewayClass.Name, status)
|
||||
}
|
||||
|
||||
var gateways []*gatev1.Gateway
|
||||
@@ -390,14 +394,14 @@ func (p *Provider) loadConfigurationFromGateways(ctx context.Context) *dynamic.C
|
||||
gatewayListeners = append(gatewayListeners, p.loadGatewayListeners(logger.WithContext(ctx), gateway, conf)...)
|
||||
}
|
||||
|
||||
p.loadHTTPRoutes(ctx, gatewayListeners, conf)
|
||||
p.loadHTTPRoutes(ctx, gatewayListeners, conf, statusReport)
|
||||
|
||||
p.loadGRPCRoutes(ctx, gatewayListeners, conf)
|
||||
p.loadGRPCRoutes(ctx, gatewayListeners, conf, statusReport)
|
||||
|
||||
p.loadTLSRoutes(ctx, gatewayListeners, conf)
|
||||
p.loadTLSRoutes(ctx, gatewayListeners, conf, statusReport)
|
||||
|
||||
if p.ExperimentalChannel {
|
||||
p.loadTCPRoutes(ctx, gatewayListeners, conf)
|
||||
p.loadTCPRoutes(ctx, gatewayListeners, conf, statusReport)
|
||||
}
|
||||
|
||||
for _, gateway := range gateways {
|
||||
@@ -428,14 +432,10 @@ func (p *Provider) loadConfigurationFromGateways(ctx context.Context) *dynamic.C
|
||||
Msg("Gateway Not Accepted")
|
||||
}
|
||||
|
||||
if err = p.client.UpdateGatewayStatus(ctx, ktypes.NamespacedName{Name: gateway.Name, Namespace: gateway.Namespace}, gatewayStatus); err != nil {
|
||||
logger.Warn().
|
||||
Err(err).
|
||||
Msg("Unable to update Gateway status")
|
||||
}
|
||||
statusReport.RecordGatewayStatus(ktypes.NamespacedName{Name: gateway.Name, Namespace: gateway.Namespace}, gatewayStatus)
|
||||
}
|
||||
|
||||
return conf
|
||||
return conf, statusReport, nil
|
||||
}
|
||||
|
||||
func (p *Provider) loadGatewayListeners(ctx context.Context, gateway *gatev1.Gateway, conf *dynamic.Configuration) []gatewayListener {
|
||||
@@ -591,7 +591,7 @@ func (p *Provider) loadGatewayListeners(ctx context.Context, gateway *gatev1.Gat
|
||||
var errCertConditions []metav1.Condition
|
||||
listenerTLSCerts := make(map[string]*tls.CertAndStores)
|
||||
for _, certificateRef := range listener.TLS.CertificateRefs {
|
||||
if certificateRef.Kind == nil || *certificateRef.Kind != "Secret" || certificateRef.Group == nil || (*certificateRef.Group != "" && *certificateRef.Group != groupCore) {
|
||||
if certificateRef.Kind == nil || *certificateRef.Kind != kindSecret || certificateRef.Group == nil || (*certificateRef.Group != "" && *certificateRef.Group != groupCore) {
|
||||
errCertConditions = append(errCertConditions, metav1.Condition{
|
||||
Type: string(gatev1.ListenerConditionResolvedRefs),
|
||||
Status: metav1.ConditionFalse,
|
||||
@@ -604,7 +604,7 @@ func (p *Provider) loadGatewayListeners(ctx context.Context, gateway *gatev1.Gat
|
||||
}
|
||||
|
||||
certificateNamespace := string(ptr.Deref(certificateRef.Namespace, gatev1.Namespace(gateway.Namespace)))
|
||||
if err := p.isReferenceGranted(kindGateway, gateway.Namespace, groupCore, "Secret", string(certificateRef.Name), certificateNamespace); err != nil {
|
||||
if err := p.isReferenceGranted(kindGateway, gateway.Namespace, groupCore, kindSecret, string(certificateRef.Name), certificateNamespace); err != nil {
|
||||
errCertConditions = append(errCertConditions, metav1.Condition{
|
||||
Type: string(gatev1.ListenerConditionResolvedRefs),
|
||||
Status: metav1.ConditionFalse,
|
||||
|
||||
@@ -95,7 +95,10 @@ func TestGatewayClassLabelSelector(t *testing.T) {
|
||||
client: client,
|
||||
}
|
||||
|
||||
_ = p.loadConfigurationFromGateways(t.Context())
|
||||
_, statusReport, err := p.loadConfigurationFromGateways(t.Context())
|
||||
require.NoError(t, err)
|
||||
|
||||
statusReport.Flush(t.Context(), p.client)
|
||||
|
||||
gw, err := gwClient.GatewayV1().Gateways("default").Get(t.Context(), "traefik-external", metav1.GetOptions{})
|
||||
require.NoError(t, err)
|
||||
@@ -2471,6 +2474,78 @@ func TestLoadHTTPRoutes(t *testing.T) {
|
||||
TLS: &dynamic.TLSConfiguration{},
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "Simple HTTPRoute and BackendTLSPolicy with subjectAltNames",
|
||||
paths: []string{"services.yml", "httproute/with_backend_tls_policy_san.yml"},
|
||||
entryPoints: map[string]Entrypoint{"web": {
|
||||
Address: ":80",
|
||||
}},
|
||||
expected: &dynamic.Configuration{
|
||||
UDP: &dynamic.UDPConfiguration{
|
||||
Routers: map[string]*dynamic.UDPRouter{},
|
||||
Services: map[string]*dynamic.UDPService{},
|
||||
},
|
||||
TCP: &dynamic.TCPConfiguration{
|
||||
Routers: map[string]*dynamic.TCPRouter{},
|
||||
Middlewares: map[string]*dynamic.TCPMiddleware{},
|
||||
Services: map[string]*dynamic.TCPService{},
|
||||
ServersTransports: map[string]*dynamic.TCPServersTransport{},
|
||||
},
|
||||
HTTP: &dynamic.HTTPConfiguration{
|
||||
Routers: map[string]*dynamic.Router{
|
||||
"httproute-default-http-app-1-gw-default-my-gateway-ep-web-0-af329269dd38031b03e3": {
|
||||
EntryPoints: []string{"web"},
|
||||
Service: "httproute-default-http-app-1-gw-default-my-gateway-ep-web-0-af329269dd38031b03e3-wrr",
|
||||
Rule: `Host("foo.com") && Path("/bar")`,
|
||||
Priority: 100008,
|
||||
RuleSyntax: "default",
|
||||
},
|
||||
},
|
||||
Middlewares: map[string]*dynamic.Middleware{},
|
||||
Services: map[string]*dynamic.Service{
|
||||
"httproute-default-http-app-1-gw-default-my-gateway-ep-web-0-af329269dd38031b03e3-wrr": {
|
||||
Weighted: &dynamic.WeightedRoundRobin{
|
||||
Services: []dynamic.WRRService{
|
||||
{
|
||||
Name: "default-whoami-http-80",
|
||||
Weight: ptr.To(1),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"default-whoami-http-80": {
|
||||
LoadBalancer: &dynamic.ServersLoadBalancer{
|
||||
Strategy: dynamic.BalancerStrategyWRR,
|
||||
Servers: []dynamic.Server{
|
||||
{
|
||||
URL: "https://10.10.0.1:80",
|
||||
},
|
||||
{
|
||||
URL: "https://10.10.0.2:80",
|
||||
},
|
||||
},
|
||||
PassHostHeader: ptr.To(true),
|
||||
ResponseForwarding: &dynamic.ResponseForwarding{
|
||||
FlushInterval: ptypes.Duration(100 * time.Millisecond),
|
||||
},
|
||||
ServersTransport: "default-whoami-http-80",
|
||||
},
|
||||
},
|
||||
},
|
||||
ServersTransports: map[string]*dynamic.ServersTransport{
|
||||
"default-whoami-http-80": {
|
||||
ServerName: "whoami",
|
||||
InsecureSkipVerify: true,
|
||||
PeerCertSANs: []tls.SAN{
|
||||
{Type: tls.SANDNSNameType, Value: "whoami.default.svc.cluster.local"},
|
||||
{Type: tls.SANURIType, Value: "spiffe://cluster.local/ns/default/sa/whoami"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
TLS: &dynamic.TLSConfiguration{},
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "Simple HTTPRoute and BackendTLSPolicy with System CA",
|
||||
paths: []string{"services.yml", "httproute/with_backend_tls_policy_system.yml"},
|
||||
@@ -2753,7 +2828,9 @@ func TestLoadHTTPRoutes(t *testing.T) {
|
||||
client: client,
|
||||
}
|
||||
|
||||
conf := p.loadConfigurationFromGateways(t.Context())
|
||||
conf, _, err := p.loadConfigurationFromGateways(t.Context())
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, test.expected, conf)
|
||||
})
|
||||
}
|
||||
@@ -3216,7 +3293,9 @@ func TestLoadHTTPRoutes_backendExtensionRef(t *testing.T) {
|
||||
p.RegisterBackendFuncs(group, kind, backendFunc)
|
||||
}
|
||||
}
|
||||
conf := p.loadConfigurationFromGateways(t.Context())
|
||||
conf, _, err := p.loadConfigurationFromGateways(t.Context())
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, test.expected, conf)
|
||||
})
|
||||
}
|
||||
@@ -3502,7 +3581,265 @@ func TestLoadHTTPRoutes_filterExtensionRef(t *testing.T) {
|
||||
p.RegisterFilterFuncs(group, kind, filterFunc)
|
||||
}
|
||||
}
|
||||
conf := p.loadConfigurationFromGateways(t.Context())
|
||||
conf, _, err := p.loadConfigurationFromGateways(t.Context())
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.Equal(t, test.expected, conf)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadGRPCRoutes(t *testing.T) {
|
||||
testCases := []struct {
|
||||
desc string
|
||||
paths []string
|
||||
expected *dynamic.Configuration
|
||||
entryPoints map[string]Entrypoint
|
||||
}{
|
||||
{
|
||||
desc: "Simple GRPCRoute and BackendTLSPolicy with CA certificate",
|
||||
paths: []string{"services.yml", "grpcroute/with_backend_tls_policy.yml"},
|
||||
entryPoints: map[string]Entrypoint{"web": {
|
||||
Address: ":80",
|
||||
}},
|
||||
expected: &dynamic.Configuration{
|
||||
UDP: &dynamic.UDPConfiguration{
|
||||
Routers: map[string]*dynamic.UDPRouter{},
|
||||
Services: map[string]*dynamic.UDPService{},
|
||||
},
|
||||
TCP: &dynamic.TCPConfiguration{
|
||||
Routers: map[string]*dynamic.TCPRouter{},
|
||||
Middlewares: map[string]*dynamic.TCPMiddleware{},
|
||||
Services: map[string]*dynamic.TCPService{},
|
||||
ServersTransports: map[string]*dynamic.TCPServersTransport{},
|
||||
},
|
||||
HTTP: &dynamic.HTTPConfiguration{
|
||||
Routers: map[string]*dynamic.Router{
|
||||
"grpcroute-default-grpc-app-1-gw-default-my-gateway-ep-web-0-6a1e0890d475642f7c64": {
|
||||
EntryPoints: []string{"web"},
|
||||
Service: "grpcroute-default-grpc-app-1-gw-default-my-gateway-ep-web-0-6a1e0890d475642f7c64-wrr",
|
||||
Rule: `Host("foo.com") && PathPrefix("/")`,
|
||||
Priority: 22,
|
||||
RuleSyntax: "default",
|
||||
},
|
||||
},
|
||||
Middlewares: map[string]*dynamic.Middleware{},
|
||||
Services: map[string]*dynamic.Service{
|
||||
"grpcroute-default-grpc-app-1-gw-default-my-gateway-ep-web-0-6a1e0890d475642f7c64-wrr": {
|
||||
Weighted: &dynamic.WeightedRoundRobin{
|
||||
Services: []dynamic.WRRService{
|
||||
{
|
||||
Name: "default-whoami-80-grpc",
|
||||
Weight: ptr.To(1),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"default-whoami-80-grpc": {
|
||||
LoadBalancer: &dynamic.ServersLoadBalancer{
|
||||
Strategy: dynamic.BalancerStrategyWRR,
|
||||
Servers: []dynamic.Server{
|
||||
{
|
||||
URL: "https://10.10.0.1:80",
|
||||
},
|
||||
{
|
||||
URL: "https://10.10.0.2:80",
|
||||
},
|
||||
},
|
||||
PassHostHeader: ptr.To(true),
|
||||
ResponseForwarding: &dynamic.ResponseForwarding{
|
||||
FlushInterval: ptypes.Duration(100 * time.Millisecond),
|
||||
},
|
||||
ServersTransport: "default-whoami-80-grpc",
|
||||
},
|
||||
},
|
||||
},
|
||||
ServersTransports: map[string]*dynamic.ServersTransport{
|
||||
"default-whoami-80-grpc": {
|
||||
ServerName: "whoami",
|
||||
RootCAs: []types.FileOrContent{
|
||||
"CA1",
|
||||
"CA2",
|
||||
"CA1-secret",
|
||||
"CA2-secret",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
TLS: &dynamic.TLSConfiguration{},
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "Simple GRPCRoute and BackendTLSPolicy with subjectAltNames",
|
||||
paths: []string{"services.yml", "grpcroute/with_backend_tls_policy_san.yml"},
|
||||
entryPoints: map[string]Entrypoint{"web": {
|
||||
Address: ":80",
|
||||
}},
|
||||
expected: &dynamic.Configuration{
|
||||
UDP: &dynamic.UDPConfiguration{
|
||||
Routers: map[string]*dynamic.UDPRouter{},
|
||||
Services: map[string]*dynamic.UDPService{},
|
||||
},
|
||||
TCP: &dynamic.TCPConfiguration{
|
||||
Routers: map[string]*dynamic.TCPRouter{},
|
||||
Middlewares: map[string]*dynamic.TCPMiddleware{},
|
||||
Services: map[string]*dynamic.TCPService{},
|
||||
ServersTransports: map[string]*dynamic.TCPServersTransport{},
|
||||
},
|
||||
HTTP: &dynamic.HTTPConfiguration{
|
||||
Routers: map[string]*dynamic.Router{
|
||||
"grpcroute-default-grpc-app-1-gw-default-my-gateway-ep-web-0-6a1e0890d475642f7c64": {
|
||||
EntryPoints: []string{"web"},
|
||||
Service: "grpcroute-default-grpc-app-1-gw-default-my-gateway-ep-web-0-6a1e0890d475642f7c64-wrr",
|
||||
Rule: `Host("foo.com") && PathPrefix("/")`,
|
||||
Priority: 22,
|
||||
RuleSyntax: "default",
|
||||
},
|
||||
},
|
||||
Middlewares: map[string]*dynamic.Middleware{},
|
||||
Services: map[string]*dynamic.Service{
|
||||
"grpcroute-default-grpc-app-1-gw-default-my-gateway-ep-web-0-6a1e0890d475642f7c64-wrr": {
|
||||
Weighted: &dynamic.WeightedRoundRobin{
|
||||
Services: []dynamic.WRRService{
|
||||
{
|
||||
Name: "default-whoami-80-grpc",
|
||||
Weight: ptr.To(1),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"default-whoami-80-grpc": {
|
||||
LoadBalancer: &dynamic.ServersLoadBalancer{
|
||||
Strategy: dynamic.BalancerStrategyWRR,
|
||||
Servers: []dynamic.Server{
|
||||
{
|
||||
URL: "https://10.10.0.1:80",
|
||||
},
|
||||
{
|
||||
URL: "https://10.10.0.2:80",
|
||||
},
|
||||
},
|
||||
PassHostHeader: ptr.To(true),
|
||||
ResponseForwarding: &dynamic.ResponseForwarding{
|
||||
FlushInterval: ptypes.Duration(100 * time.Millisecond),
|
||||
},
|
||||
ServersTransport: "default-whoami-80-grpc",
|
||||
},
|
||||
},
|
||||
},
|
||||
ServersTransports: map[string]*dynamic.ServersTransport{
|
||||
"default-whoami-80-grpc": {
|
||||
ServerName: "whoami",
|
||||
InsecureSkipVerify: true,
|
||||
PeerCertSANs: []tls.SAN{
|
||||
{Type: tls.SANDNSNameType, Value: "whoami.default.svc.cluster.local"},
|
||||
{Type: tls.SANURIType, Value: "spiffe://cluster.local/ns/default/sa/whoami"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
TLS: &dynamic.TLSConfiguration{},
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "Simple GRPCRoute and BackendTLSPolicy with System CA",
|
||||
paths: []string{"services.yml", "grpcroute/with_backend_tls_policy_system.yml"},
|
||||
entryPoints: map[string]Entrypoint{"web": {
|
||||
Address: ":80",
|
||||
}},
|
||||
expected: &dynamic.Configuration{
|
||||
UDP: &dynamic.UDPConfiguration{
|
||||
Routers: map[string]*dynamic.UDPRouter{},
|
||||
Services: map[string]*dynamic.UDPService{},
|
||||
},
|
||||
TCP: &dynamic.TCPConfiguration{
|
||||
Routers: map[string]*dynamic.TCPRouter{},
|
||||
Middlewares: map[string]*dynamic.TCPMiddleware{},
|
||||
Services: map[string]*dynamic.TCPService{},
|
||||
ServersTransports: map[string]*dynamic.TCPServersTransport{},
|
||||
},
|
||||
HTTP: &dynamic.HTTPConfiguration{
|
||||
Routers: map[string]*dynamic.Router{
|
||||
"grpcroute-default-grpc-app-1-gw-default-my-gateway-ep-web-0-6a1e0890d475642f7c64": {
|
||||
EntryPoints: []string{"web"},
|
||||
Service: "grpcroute-default-grpc-app-1-gw-default-my-gateway-ep-web-0-6a1e0890d475642f7c64-wrr",
|
||||
Rule: `Host("foo.com") && PathPrefix("/")`,
|
||||
Priority: 22,
|
||||
RuleSyntax: "default",
|
||||
},
|
||||
},
|
||||
Middlewares: map[string]*dynamic.Middleware{},
|
||||
Services: map[string]*dynamic.Service{
|
||||
"grpcroute-default-grpc-app-1-gw-default-my-gateway-ep-web-0-6a1e0890d475642f7c64-wrr": {
|
||||
Weighted: &dynamic.WeightedRoundRobin{
|
||||
Services: []dynamic.WRRService{
|
||||
{
|
||||
Name: "default-whoami-80-grpc",
|
||||
Weight: ptr.To(1),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"default-whoami-80-grpc": {
|
||||
LoadBalancer: &dynamic.ServersLoadBalancer{
|
||||
Strategy: dynamic.BalancerStrategyWRR,
|
||||
Servers: []dynamic.Server{
|
||||
{
|
||||
URL: "https://10.10.0.1:80",
|
||||
},
|
||||
{
|
||||
URL: "https://10.10.0.2:80",
|
||||
},
|
||||
},
|
||||
PassHostHeader: ptr.To(true),
|
||||
ResponseForwarding: &dynamic.ResponseForwarding{
|
||||
FlushInterval: ptypes.Duration(100 * time.Millisecond),
|
||||
},
|
||||
ServersTransport: "default-whoami-80-grpc",
|
||||
},
|
||||
},
|
||||
},
|
||||
ServersTransports: map[string]*dynamic.ServersTransport{
|
||||
"default-whoami-80-grpc": {
|
||||
ServerName: "whoami",
|
||||
},
|
||||
},
|
||||
},
|
||||
TLS: &dynamic.TLSConfiguration{},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range testCases {
|
||||
t.Run(test.desc, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
if test.expected == nil {
|
||||
return
|
||||
}
|
||||
|
||||
k8sObjects, gwObjects := readResources(t, test.paths)
|
||||
|
||||
kubeClient := kubefake.NewClientset(k8sObjects...)
|
||||
gwClient := newGatewaySimpleClientSet(t, gwObjects...)
|
||||
|
||||
client := newClientImpl(kubeClient, gwClient)
|
||||
|
||||
eventCh, err := client.WatchAll(nil, make(chan struct{}))
|
||||
require.NoError(t, err)
|
||||
|
||||
if len(k8sObjects) > 0 || len(gwObjects) > 0 {
|
||||
<-eventCh
|
||||
}
|
||||
|
||||
p := Provider{
|
||||
EntryPoints: test.entryPoints,
|
||||
client: client,
|
||||
}
|
||||
|
||||
conf, _, err := p.loadConfigurationFromGateways(t.Context())
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, test.expected, conf)
|
||||
})
|
||||
}
|
||||
@@ -3794,7 +4131,9 @@ func TestLoadGRPCRoutes_filterExtensionRef(t *testing.T) {
|
||||
p.RegisterFilterFuncs(group, kind, filterFunc)
|
||||
}
|
||||
}
|
||||
conf := p.loadConfigurationFromGateways(t.Context())
|
||||
conf, _, err := p.loadConfigurationFromGateways(t.Context())
|
||||
assert.NoError(t, err)
|
||||
|
||||
assert.Equal(t, test.expected, conf)
|
||||
})
|
||||
}
|
||||
@@ -4715,7 +5054,9 @@ func TestLoadTCPRoutes(t *testing.T) {
|
||||
client: client,
|
||||
}
|
||||
|
||||
conf := p.loadConfigurationFromGateways(t.Context())
|
||||
conf, _, err := p.loadConfigurationFromGateways(t.Context())
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, test.expected, conf)
|
||||
})
|
||||
}
|
||||
@@ -5976,6 +6317,266 @@ func TestLoadTLSRoutes(t *testing.T) {
|
||||
TLS: &dynamic.TLSConfiguration{},
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "Simple TLSRoute and BackendTLSPolicy with CA certificate",
|
||||
paths: []string{"services.yml", "tlsroute/with_backend_tls_policy.yml"},
|
||||
entryPoints: map[string]Entrypoint{"tls": {
|
||||
Address: ":9001",
|
||||
}},
|
||||
expected: &dynamic.Configuration{
|
||||
UDP: &dynamic.UDPConfiguration{
|
||||
Routers: map[string]*dynamic.UDPRouter{},
|
||||
Services: map[string]*dynamic.UDPService{},
|
||||
},
|
||||
TCP: &dynamic.TCPConfiguration{
|
||||
Routers: map[string]*dynamic.TCPRouter{
|
||||
"deny-unknown-host": {
|
||||
Rule: "HostSNI(`*`) && !ALPN(`h2`) && !ALPN(`http/1.1`)",
|
||||
Priority: 1,
|
||||
Service: "deny-unknown-host",
|
||||
TLS: &dynamic.RouterTCPTLSConfig{},
|
||||
},
|
||||
"tlsroute-default-tls-app-1-gw-default-my-gateway-ep-tls-0-e3b0c44298fc1c149afb": {
|
||||
EntryPoints: []string{"tls"},
|
||||
Service: "tlsroute-default-tls-app-1-gw-default-my-gateway-ep-tls-0-e3b0c44298fc1c149afb-wrr",
|
||||
Rule: `HostSNI("foo.com")`,
|
||||
Priority: 7,
|
||||
RuleSyntax: "default",
|
||||
TLS: &dynamic.RouterTCPTLSConfig{},
|
||||
},
|
||||
},
|
||||
Middlewares: map[string]*dynamic.TCPMiddleware{},
|
||||
Services: map[string]*dynamic.TCPService{
|
||||
"deny-unknown-host": {
|
||||
LoadBalancer: &dynamic.TCPServersLoadBalancer{},
|
||||
},
|
||||
"tlsroute-default-tls-app-1-gw-default-my-gateway-ep-tls-0-e3b0c44298fc1c149afb-wrr": {
|
||||
Weighted: &dynamic.TCPWeightedRoundRobin{
|
||||
Services: []dynamic.TCPWRRService{
|
||||
{
|
||||
Name: "default-whoami-80",
|
||||
Weight: ptr.To(1),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"default-whoami-80": {
|
||||
LoadBalancer: &dynamic.TCPServersLoadBalancer{
|
||||
Servers: []dynamic.TCPServer{
|
||||
{
|
||||
Address: "10.10.0.1:80",
|
||||
},
|
||||
{
|
||||
Address: "10.10.0.2:80",
|
||||
},
|
||||
},
|
||||
ServersTransport: "default-whoami-80",
|
||||
},
|
||||
},
|
||||
},
|
||||
ServersTransports: map[string]*dynamic.TCPServersTransport{
|
||||
"default-whoami-80": {
|
||||
TLS: &dynamic.TLSClientConfig{
|
||||
ServerName: "whoami",
|
||||
RootCAs: []types.FileOrContent{
|
||||
"CA1",
|
||||
"CA2",
|
||||
"CA1-secret",
|
||||
"CA2-secret",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
HTTP: &dynamic.HTTPConfiguration{
|
||||
Routers: map[string]*dynamic.Router{},
|
||||
Middlewares: map[string]*dynamic.Middleware{},
|
||||
Services: map[string]*dynamic.Service{},
|
||||
ServersTransports: map[string]*dynamic.ServersTransport{},
|
||||
},
|
||||
TLS: &dynamic.TLSConfiguration{
|
||||
Certificates: []*tls.CertAndStores{
|
||||
{
|
||||
Certificate: tls.Certificate{
|
||||
CertFile: types.FileOrContent(listenerCert),
|
||||
KeyFile: types.FileOrContent(listenerKey),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "Simple TLSRoute and BackendTLSPolicy with subjectAltNames",
|
||||
paths: []string{"services.yml", "tlsroute/with_backend_tls_policy_san.yml"},
|
||||
entryPoints: map[string]Entrypoint{"tls": {
|
||||
Address: ":9001",
|
||||
}},
|
||||
expected: &dynamic.Configuration{
|
||||
UDP: &dynamic.UDPConfiguration{
|
||||
Routers: map[string]*dynamic.UDPRouter{},
|
||||
Services: map[string]*dynamic.UDPService{},
|
||||
},
|
||||
TCP: &dynamic.TCPConfiguration{
|
||||
Routers: map[string]*dynamic.TCPRouter{
|
||||
"deny-unknown-host": {
|
||||
Rule: "HostSNI(`*`) && !ALPN(`h2`) && !ALPN(`http/1.1`)",
|
||||
Priority: 1,
|
||||
Service: "deny-unknown-host",
|
||||
TLS: &dynamic.RouterTCPTLSConfig{},
|
||||
},
|
||||
"tlsroute-default-tls-app-1-gw-default-my-gateway-ep-tls-0-e3b0c44298fc1c149afb": {
|
||||
EntryPoints: []string{"tls"},
|
||||
Service: "tlsroute-default-tls-app-1-gw-default-my-gateway-ep-tls-0-e3b0c44298fc1c149afb-wrr",
|
||||
Rule: `HostSNI("foo.com")`,
|
||||
Priority: 7,
|
||||
RuleSyntax: "default",
|
||||
TLS: &dynamic.RouterTCPTLSConfig{},
|
||||
},
|
||||
},
|
||||
Middlewares: map[string]*dynamic.TCPMiddleware{},
|
||||
Services: map[string]*dynamic.TCPService{
|
||||
"deny-unknown-host": {
|
||||
LoadBalancer: &dynamic.TCPServersLoadBalancer{},
|
||||
},
|
||||
"tlsroute-default-tls-app-1-gw-default-my-gateway-ep-tls-0-e3b0c44298fc1c149afb-wrr": {
|
||||
Weighted: &dynamic.TCPWeightedRoundRobin{
|
||||
Services: []dynamic.TCPWRRService{
|
||||
{
|
||||
Name: "default-whoami-80",
|
||||
Weight: ptr.To(1),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"default-whoami-80": {
|
||||
LoadBalancer: &dynamic.TCPServersLoadBalancer{
|
||||
Servers: []dynamic.TCPServer{
|
||||
{
|
||||
Address: "10.10.0.1:80",
|
||||
},
|
||||
{
|
||||
Address: "10.10.0.2:80",
|
||||
},
|
||||
},
|
||||
ServersTransport: "default-whoami-80",
|
||||
},
|
||||
},
|
||||
},
|
||||
ServersTransports: map[string]*dynamic.TCPServersTransport{
|
||||
"default-whoami-80": {
|
||||
TLS: &dynamic.TLSClientConfig{
|
||||
ServerName: "whoami",
|
||||
InsecureSkipVerify: true,
|
||||
PeerCertSANs: []tls.SAN{
|
||||
{Type: tls.SANDNSNameType, Value: "whoami.default.svc.cluster.local"},
|
||||
{Type: tls.SANURIType, Value: "spiffe://cluster.local/ns/default/sa/whoami"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
HTTP: &dynamic.HTTPConfiguration{
|
||||
Routers: map[string]*dynamic.Router{},
|
||||
Middlewares: map[string]*dynamic.Middleware{},
|
||||
Services: map[string]*dynamic.Service{},
|
||||
ServersTransports: map[string]*dynamic.ServersTransport{},
|
||||
},
|
||||
TLS: &dynamic.TLSConfiguration{
|
||||
Certificates: []*tls.CertAndStores{
|
||||
{
|
||||
Certificate: tls.Certificate{
|
||||
CertFile: types.FileOrContent(listenerCert),
|
||||
KeyFile: types.FileOrContent(listenerKey),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "Simple TLSRoute and BackendTLSPolicy with System CA",
|
||||
paths: []string{"services.yml", "tlsroute/with_backend_tls_policy_system.yml"},
|
||||
entryPoints: map[string]Entrypoint{"tls": {
|
||||
Address: ":9001",
|
||||
}},
|
||||
expected: &dynamic.Configuration{
|
||||
UDP: &dynamic.UDPConfiguration{
|
||||
Routers: map[string]*dynamic.UDPRouter{},
|
||||
Services: map[string]*dynamic.UDPService{},
|
||||
},
|
||||
TCP: &dynamic.TCPConfiguration{
|
||||
Routers: map[string]*dynamic.TCPRouter{
|
||||
"deny-unknown-host": {
|
||||
Rule: "HostSNI(`*`) && !ALPN(`h2`) && !ALPN(`http/1.1`)",
|
||||
Priority: 1,
|
||||
Service: "deny-unknown-host",
|
||||
TLS: &dynamic.RouterTCPTLSConfig{},
|
||||
},
|
||||
"tlsroute-default-tls-app-1-gw-default-my-gateway-ep-tls-0-e3b0c44298fc1c149afb": {
|
||||
EntryPoints: []string{"tls"},
|
||||
Service: "tlsroute-default-tls-app-1-gw-default-my-gateway-ep-tls-0-e3b0c44298fc1c149afb-wrr",
|
||||
Rule: `HostSNI("foo.com")`,
|
||||
Priority: 7,
|
||||
RuleSyntax: "default",
|
||||
TLS: &dynamic.RouterTCPTLSConfig{},
|
||||
},
|
||||
},
|
||||
Middlewares: map[string]*dynamic.TCPMiddleware{},
|
||||
Services: map[string]*dynamic.TCPService{
|
||||
"deny-unknown-host": {
|
||||
LoadBalancer: &dynamic.TCPServersLoadBalancer{},
|
||||
},
|
||||
"tlsroute-default-tls-app-1-gw-default-my-gateway-ep-tls-0-e3b0c44298fc1c149afb-wrr": {
|
||||
Weighted: &dynamic.TCPWeightedRoundRobin{
|
||||
Services: []dynamic.TCPWRRService{
|
||||
{
|
||||
Name: "default-whoami-80",
|
||||
Weight: ptr.To(1),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"default-whoami-80": {
|
||||
LoadBalancer: &dynamic.TCPServersLoadBalancer{
|
||||
Servers: []dynamic.TCPServer{
|
||||
{
|
||||
Address: "10.10.0.1:80",
|
||||
},
|
||||
{
|
||||
Address: "10.10.0.2:80",
|
||||
},
|
||||
},
|
||||
ServersTransport: "default-whoami-80",
|
||||
},
|
||||
},
|
||||
},
|
||||
ServersTransports: map[string]*dynamic.TCPServersTransport{
|
||||
"default-whoami-80": {
|
||||
TLS: &dynamic.TLSClientConfig{
|
||||
ServerName: "whoami",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
HTTP: &dynamic.HTTPConfiguration{
|
||||
Routers: map[string]*dynamic.Router{},
|
||||
Middlewares: map[string]*dynamic.Middleware{},
|
||||
Services: map[string]*dynamic.Service{},
|
||||
ServersTransports: map[string]*dynamic.ServersTransport{},
|
||||
},
|
||||
TLS: &dynamic.TLSConfiguration{
|
||||
Certificates: []*tls.CertAndStores{
|
||||
{
|
||||
Certificate: tls.Certificate{
|
||||
CertFile: types.FileOrContent(listenerCert),
|
||||
KeyFile: types.FileOrContent(listenerKey),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range testCases {
|
||||
@@ -6009,7 +6610,9 @@ func TestLoadTLSRoutes(t *testing.T) {
|
||||
client: client,
|
||||
}
|
||||
|
||||
conf := p.loadConfigurationFromGateways(t.Context())
|
||||
conf, _, err := p.loadConfigurationFromGateways(t.Context())
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, test.expected, conf)
|
||||
})
|
||||
}
|
||||
@@ -6999,7 +7602,9 @@ func TestLoadMixedRoutes(t *testing.T) {
|
||||
client: client,
|
||||
}
|
||||
|
||||
conf := p.loadConfigurationFromGateways(t.Context())
|
||||
conf, _, err := p.loadConfigurationFromGateways(t.Context())
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, test.expected, conf)
|
||||
})
|
||||
}
|
||||
@@ -7297,7 +7902,9 @@ func TestLoadRoutesWithReferenceGrants(t *testing.T) {
|
||||
client: client,
|
||||
}
|
||||
|
||||
conf := p.loadConfigurationFromGateways(t.Context())
|
||||
conf, _, err := p.loadConfigurationFromGateways(t.Context())
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, test.expected, conf)
|
||||
})
|
||||
}
|
||||
@@ -8425,20 +9032,22 @@ func Test_isCrossProviderNamespaceAllowed(t *testing.T) {
|
||||
func TestCrossProviderNamespaces_HTTPRoute(t *testing.T) {
|
||||
testCases := []struct {
|
||||
desc string
|
||||
fixture string
|
||||
crossProviderNamespaces []string
|
||||
wantError bool
|
||||
}{
|
||||
{desc: "nil: cross-provider TraefikService backendRefs accepted (backward compatible)", crossProviderNamespaces: nil, wantError: false},
|
||||
{desc: "empty list: cross-provider TraefikService backendRefs are rejected, route dropped", crossProviderNamespaces: []string{}, wantError: true},
|
||||
{desc: "namespace allowed: cross-provider TraefikService backendRefs accepted", crossProviderNamespaces: []string{"default"}, wantError: false},
|
||||
{desc: "namespace not allowed: cross-provider TraefikService backendRefs rejected, route dropped", crossProviderNamespaces: []string{"other"}, wantError: true},
|
||||
{desc: "nil: cross-provider TraefikService backendRefs accepted (backward compatible)", fixture: "httproute/simple_cross_provider.yml", crossProviderNamespaces: nil, wantError: false},
|
||||
{desc: "empty list: cross-provider TraefikService backendRefs are rejected, route dropped", fixture: "httproute/simple_cross_provider.yml", crossProviderNamespaces: []string{}, wantError: true},
|
||||
{desc: "namespace allowed: cross-provider TraefikService backendRefs accepted", fixture: "httproute/simple_cross_provider.yml", crossProviderNamespaces: []string{"default"}, wantError: false},
|
||||
{desc: "namespace not allowed: cross-provider TraefikService backendRefs rejected, route dropped", fixture: "httproute/simple_cross_provider.yml", crossProviderNamespaces: []string{"other"}, wantError: true},
|
||||
{desc: "namespace provided with cross-provider backendRef, route dropped", fixture: "httproute/invalid_cross_provider.yml", crossProviderNamespaces: []string{"other"}, wantError: true},
|
||||
}
|
||||
|
||||
for _, test := range testCases {
|
||||
t.Run(test.desc, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
k8sObjects, gwObjects := readResources(t, []string{"services.yml", "httproute/simple_cross_provider.yml"})
|
||||
k8sObjects, gwObjects := readResources(t, []string{"services.yml", test.fixture})
|
||||
|
||||
kubeClient := kubefake.NewClientset(k8sObjects...)
|
||||
gwClient := newGatewaySimpleClientSet(t, gwObjects...)
|
||||
@@ -8459,7 +9068,8 @@ func TestCrossProviderNamespaces_HTTPRoute(t *testing.T) {
|
||||
client: client,
|
||||
}
|
||||
|
||||
conf := p.loadConfigurationFromGateways(t.Context())
|
||||
conf, _, err := p.loadConfigurationFromGateways(t.Context())
|
||||
require.NoError(t, err)
|
||||
|
||||
router, ok := conf.HTTP.Routers["httproute-default-http-app-1-gw-default-my-gateway-ep-web-0-af329269dd38031b03e3"]
|
||||
require.True(t, ok)
|
||||
@@ -8488,20 +9098,22 @@ func TestCrossProviderNamespaces_HTTPRoute(t *testing.T) {
|
||||
func TestCrossProviderNamespaces_TCPRoute(t *testing.T) {
|
||||
testCases := []struct {
|
||||
desc string
|
||||
fixture string
|
||||
crossProviderNamespaces []string
|
||||
wantError bool
|
||||
}{
|
||||
{desc: "nil: cross-provider TraefikService backendRefs accepted (backward compatible)", crossProviderNamespaces: nil, wantError: false},
|
||||
{desc: "empty list: cross-provider TraefikService backendRefs are rejected, route dropped", crossProviderNamespaces: []string{}, wantError: true},
|
||||
{desc: "namespace allowed: cross-provider TraefikService backendRefs accepted", crossProviderNamespaces: []string{"default"}, wantError: false},
|
||||
{desc: "namespace not allowed: cross-provider TraefikService backendRefs rejected, route dropped", crossProviderNamespaces: []string{"other"}, wantError: true},
|
||||
{desc: "nil: cross-provider TraefikService backendRefs accepted (backward compatible)", fixture: "tcproute/simple_cross_provider.yml", crossProviderNamespaces: nil, wantError: false},
|
||||
{desc: "empty list: cross-provider TraefikService backendRefs are rejected, route dropped", fixture: "tcproute/simple_cross_provider.yml", crossProviderNamespaces: []string{}, wantError: true},
|
||||
{desc: "namespace allowed: cross-provider TraefikService backendRefs accepted", fixture: "tcproute/simple_cross_provider.yml", crossProviderNamespaces: []string{"default"}, wantError: false},
|
||||
{desc: "namespace not allowed: cross-provider TraefikService backendRefs rejected, route dropped", fixture: "tcproute/simple_cross_provider.yml", crossProviderNamespaces: []string{"other"}, wantError: true},
|
||||
{desc: "namespace provided with cross-provider backendRef, route dropped", fixture: "tcproute/invalid_cross_provider.yml", crossProviderNamespaces: []string{"other"}, wantError: true},
|
||||
}
|
||||
|
||||
for _, test := range testCases {
|
||||
t.Run(test.desc, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
k8sObjects, gwObjects := readResources(t, []string{"services.yml", "tcproute/simple_cross_provider.yml"})
|
||||
k8sObjects, gwObjects := readResources(t, []string{"services.yml", test.fixture})
|
||||
|
||||
kubeClient := kubefake.NewClientset(k8sObjects...)
|
||||
gwClient := newGatewaySimpleClientSet(t, gwObjects...)
|
||||
@@ -8524,7 +9136,8 @@ func TestCrossProviderNamespaces_TCPRoute(t *testing.T) {
|
||||
ExperimentalChannel: true,
|
||||
}
|
||||
|
||||
conf := p.loadConfigurationFromGateways(t.Context())
|
||||
conf, _, err := p.loadConfigurationFromGateways(t.Context())
|
||||
require.NoError(t, err)
|
||||
|
||||
router, ok := conf.TCP.Routers["tcproute-default-tcp-app-1-gw-default-my-gateway-ep-tcp-0-e3b0c44298fc1c149afb"]
|
||||
require.True(t, ok)
|
||||
@@ -8560,20 +9173,22 @@ func TestCrossProviderNamespaces_TCPRoute(t *testing.T) {
|
||||
func TestCrossProviderNamespaces_TLSRoute(t *testing.T) {
|
||||
testCases := []struct {
|
||||
desc string
|
||||
fixture string
|
||||
crossProviderNamespaces []string
|
||||
wantError bool
|
||||
}{
|
||||
{desc: "nil: cross-provider TraefikService backendRefs accepted (backward compatible)", crossProviderNamespaces: nil, wantError: false},
|
||||
{desc: "empty list: cross-provider TraefikService backendRefs are rejected, route dropped", crossProviderNamespaces: []string{}, wantError: true},
|
||||
{desc: "namespace allowed: cross-provider TraefikService backendRefs accepted", crossProviderNamespaces: []string{"default"}, wantError: false},
|
||||
{desc: "namespace not allowed: cross-provider TraefikService backendRefs rejected, route dropped", crossProviderNamespaces: []string{"other"}, wantError: true},
|
||||
{desc: "nil: cross-provider TraefikService backendRefs accepted (backward compatible)", fixture: "tlsroute/simple_cross_provider.yml", crossProviderNamespaces: nil, wantError: false},
|
||||
{desc: "empty list: cross-provider TraefikService backendRefs are rejected, route dropped", fixture: "tlsroute/simple_cross_provider.yml", crossProviderNamespaces: []string{}, wantError: true},
|
||||
{desc: "namespace allowed: cross-provider TraefikService backendRefs accepted", fixture: "tlsroute/simple_cross_provider.yml", crossProviderNamespaces: []string{"default"}, wantError: false},
|
||||
{desc: "namespace not allowed: cross-provider TraefikService backendRefs rejected, route dropped", fixture: "tlsroute/simple_cross_provider.yml", crossProviderNamespaces: []string{"other"}, wantError: true},
|
||||
{desc: "namespace provided with cross-provider backendRef, route dropped", fixture: "tlsroute/invalid_cross_provider.yml", crossProviderNamespaces: []string{"other"}, wantError: true},
|
||||
}
|
||||
|
||||
for _, test := range testCases {
|
||||
t.Run(test.desc, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
k8sObjects, gwObjects := readResources(t, []string{"services.yml", "tlsroute/simple_cross_provider.yml"})
|
||||
k8sObjects, gwObjects := readResources(t, []string{"services.yml", test.fixture})
|
||||
|
||||
kubeClient := kubefake.NewClientset(k8sObjects...)
|
||||
gwClient := newGatewaySimpleClientSet(t, gwObjects...)
|
||||
@@ -8595,7 +9210,8 @@ func TestCrossProviderNamespaces_TLSRoute(t *testing.T) {
|
||||
client: client,
|
||||
}
|
||||
|
||||
conf := p.loadConfigurationFromGateways(t.Context())
|
||||
conf, _, err := p.loadConfigurationFromGateways(t.Context())
|
||||
assert.NoError(t, err)
|
||||
|
||||
fmt.Println(conf.TCP.Routers)
|
||||
|
||||
|
||||
@@ -0,0 +1,136 @@
|
||||
package gateway
|
||||
|
||||
import (
|
||||
"context"
|
||||
"reflect"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
ktypes "k8s.io/apimachinery/pkg/types"
|
||||
gatev1 "sigs.k8s.io/gateway-api/apis/v1"
|
||||
gatev1alpha2 "sigs.k8s.io/gateway-api/apis/v1alpha2"
|
||||
)
|
||||
|
||||
// statusReport collects the status writes produced by a single rebuild so they
|
||||
// can be flushed to the apiserver after the dynamic configuration has been published.
|
||||
type statusReport struct {
|
||||
gatewayClasses map[string]gatev1.GatewayClassStatus
|
||||
gateways map[ktypes.NamespacedName]gatev1.GatewayStatus
|
||||
httpRoutes map[ktypes.NamespacedName]gatev1.RouteStatus
|
||||
grpcRoutes map[ktypes.NamespacedName]gatev1.RouteStatus
|
||||
tcpRoutes map[ktypes.NamespacedName]gatev1.RouteStatus
|
||||
tlsRoutes map[ktypes.NamespacedName]gatev1.RouteStatus
|
||||
backendTLSPolicies map[ktypes.NamespacedName]gatev1.PolicyStatus
|
||||
}
|
||||
|
||||
func newStatusReport() *statusReport {
|
||||
return &statusReport{
|
||||
gatewayClasses: map[string]gatev1.GatewayClassStatus{},
|
||||
gateways: map[ktypes.NamespacedName]gatev1.GatewayStatus{},
|
||||
httpRoutes: map[ktypes.NamespacedName]gatev1.RouteStatus{},
|
||||
grpcRoutes: map[ktypes.NamespacedName]gatev1.RouteStatus{},
|
||||
tcpRoutes: map[ktypes.NamespacedName]gatev1.RouteStatus{},
|
||||
tlsRoutes: map[ktypes.NamespacedName]gatev1.RouteStatus{},
|
||||
backendTLSPolicies: map[ktypes.NamespacedName]gatev1.PolicyStatus{},
|
||||
}
|
||||
}
|
||||
|
||||
// Flush sends every status write collected during the
|
||||
// routing configuration build to the Kubernetes API server.
|
||||
func (r *statusReport) Flush(ctx context.Context, client *clientWrapper) {
|
||||
logger := log.Ctx(ctx)
|
||||
|
||||
for name, status := range r.gatewayClasses {
|
||||
if err := client.UpdateGatewayClassStatus(ctx, name, status); err != nil {
|
||||
logger.Warn().Err(err).Str("gateway_class", name).Msg("Unable to update GatewayClass status")
|
||||
}
|
||||
}
|
||||
|
||||
for name, status := range r.gateways {
|
||||
if err := client.UpdateGatewayStatus(ctx, name, status); err != nil {
|
||||
logger.Warn().Err(err).Str("gateway", name.Name).Str("namespace", name.Namespace).Msg("Unable to update Gateway status")
|
||||
}
|
||||
}
|
||||
|
||||
for name, routeStatus := range r.httpRoutes {
|
||||
status := gatev1.HTTPRouteStatus{RouteStatus: routeStatus}
|
||||
if err := client.UpdateHTTPRouteStatus(ctx, name, status); err != nil {
|
||||
logger.Warn().Err(err).Str("http_route", name.Name).Str("namespace", name.Namespace).Msg("Unable to update HTTPRoute status")
|
||||
}
|
||||
}
|
||||
|
||||
for name, routeStatus := range r.grpcRoutes {
|
||||
status := gatev1.GRPCRouteStatus{RouteStatus: routeStatus}
|
||||
if err := client.UpdateGRPCRouteStatus(ctx, name, status); err != nil {
|
||||
logger.Warn().Err(err).Str("grpc_route", name.Name).Str("namespace", name.Namespace).Msg("Unable to update GRPCRoute status")
|
||||
}
|
||||
}
|
||||
|
||||
for name, routeStatus := range r.tcpRoutes {
|
||||
status := gatev1alpha2.TCPRouteStatus{RouteStatus: routeStatus}
|
||||
if err := client.UpdateTCPRouteStatus(ctx, name, status); err != nil {
|
||||
logger.Warn().Err(err).Str("tcp_route", name.Name).Str("namespace", name.Namespace).Msg("Unable to update TCPRoute status")
|
||||
}
|
||||
}
|
||||
|
||||
for name, routeStatus := range r.tlsRoutes {
|
||||
status := gatev1.TLSRouteStatus{RouteStatus: routeStatus}
|
||||
if err := client.UpdateTLSRouteStatus(ctx, name, status); err != nil {
|
||||
logger.Warn().Err(err).Str("tls_route", name.Name).Str("namespace", name.Namespace).Msg("Unable to update TLSRoute status")
|
||||
}
|
||||
}
|
||||
|
||||
for name, policyStatus := range r.backendTLSPolicies {
|
||||
if err := client.UpdateBackendTLSPolicyStatus(ctx, name, policyStatus); err != nil {
|
||||
logger.Warn().Err(err).Str("backend_tls_policy", name.Name).Str("namespace", name.Namespace).Msg("Unable to update BackendTLSPolicy status")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (r *statusReport) RecordGatewayClassStatus(gatewayClassName string, status gatev1.GatewayClassStatus) {
|
||||
r.gatewayClasses[gatewayClassName] = status
|
||||
}
|
||||
|
||||
func (r *statusReport) RecordGatewayStatus(gateway ktypes.NamespacedName, status gatev1.GatewayStatus) {
|
||||
r.gateways[gateway] = status
|
||||
}
|
||||
|
||||
func (r *statusReport) RecordHTTPRouteStatus(route ktypes.NamespacedName, status gatev1.RouteParentStatus) {
|
||||
r.httpRoutes[route] = gatev1.RouteStatus{
|
||||
Parents: append(r.httpRoutes[route].Parents, status),
|
||||
}
|
||||
}
|
||||
|
||||
func (r *statusReport) RecordGRPCRouteStatus(route ktypes.NamespacedName, status gatev1.RouteParentStatus) {
|
||||
r.grpcRoutes[route] = gatev1.RouteStatus{
|
||||
Parents: append(r.grpcRoutes[route].Parents, status),
|
||||
}
|
||||
}
|
||||
|
||||
func (r *statusReport) RecordTCPRouteStatus(route ktypes.NamespacedName, status gatev1.RouteParentStatus) {
|
||||
r.tcpRoutes[route] = gatev1.RouteStatus{
|
||||
Parents: append(r.tcpRoutes[route].Parents, status),
|
||||
}
|
||||
}
|
||||
|
||||
func (r *statusReport) RecordTLSRouteStatus(route ktypes.NamespacedName, status gatev1.RouteParentStatus) {
|
||||
r.tlsRoutes[route] = gatev1.RouteStatus{
|
||||
Parents: append(r.tlsRoutes[route].Parents, status),
|
||||
}
|
||||
}
|
||||
|
||||
func (r *statusReport) RecordBackendTLSPolicyStatus(policy ktypes.NamespacedName, status gatev1.PolicyAncestorStatus) {
|
||||
var ancestors []gatev1.PolicyAncestorStatus
|
||||
|
||||
// Keep existing ancestor statuses, except if it matches the status to merge.
|
||||
for _, existing := range r.backendTLSPolicies[policy].Ancestors {
|
||||
if reflect.DeepEqual(existing.AncestorRef, status.AncestorRef) {
|
||||
continue
|
||||
}
|
||||
|
||||
ancestors = append(ancestors, existing)
|
||||
}
|
||||
|
||||
r.backendTLSPolicies[policy] = gatev1.PolicyStatus{
|
||||
Ancestors: append(ancestors, status), // Add the new status to the existing ancestors statuses.
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
package gateway
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
ktypes "k8s.io/apimachinery/pkg/types"
|
||||
gatev1 "sigs.k8s.io/gateway-api/apis/v1"
|
||||
)
|
||||
|
||||
func TestStatusReport_RecordGatewayClassStatus(t *testing.T) {
|
||||
report := newStatusReport()
|
||||
|
||||
accepted := gatev1.GatewayClassStatus{
|
||||
Conditions: []metav1.Condition{{Type: string(gatev1.GatewayClassConditionStatusAccepted)}},
|
||||
}
|
||||
report.RecordGatewayClassStatus("traefik", accepted)
|
||||
assert.Equal(t, accepted, report.gatewayClasses["traefik"])
|
||||
|
||||
// A later record for the same GatewayClass overwrites the previous one.
|
||||
unsupported := gatev1.GatewayClassStatus{
|
||||
Conditions: []metav1.Condition{{Type: string(gatev1.GatewayClassReasonUnsupportedVersion)}},
|
||||
}
|
||||
report.RecordGatewayClassStatus("traefik", unsupported)
|
||||
assert.Equal(t, unsupported, report.gatewayClasses["traefik"])
|
||||
}
|
||||
|
||||
func TestStatusReport_RecordGatewayStatus(t *testing.T) {
|
||||
report := newStatusReport()
|
||||
gateway := ktypes.NamespacedName{Namespace: "default", Name: "my-gateway"}
|
||||
|
||||
accepted := gatev1.GatewayStatus{
|
||||
Conditions: []metav1.Condition{{Type: string(gatev1.GatewayConditionAccepted)}},
|
||||
}
|
||||
report.RecordGatewayStatus(gateway, accepted)
|
||||
assert.Equal(t, accepted, report.gateways[gateway])
|
||||
|
||||
// A later record for the same Gateway overwrites the previous one.
|
||||
programmed := gatev1.GatewayStatus{
|
||||
Conditions: []metav1.Condition{{Type: string(gatev1.GatewayConditionProgrammed)}},
|
||||
}
|
||||
report.RecordGatewayStatus(gateway, programmed)
|
||||
assert.Equal(t, programmed, report.gateways[gateway])
|
||||
}
|
||||
|
||||
func TestStatusReport_RecordHTTPRouteStatus(t *testing.T) {
|
||||
report := newStatusReport()
|
||||
route := ktypes.NamespacedName{Namespace: "default", Name: "my-route"}
|
||||
|
||||
gatewayParent := gatev1.RouteParentStatus{ParentRef: gatev1.ParentReference{Name: "gateway"}}
|
||||
otherParent := gatev1.RouteParentStatus{ParentRef: gatev1.ParentReference{Name: "other-gateway"}}
|
||||
|
||||
report.RecordHTTPRouteStatus(route, gatewayParent)
|
||||
report.RecordHTTPRouteStatus(route, otherParent)
|
||||
|
||||
// Each parentRef accumulates as a distinct parent status.
|
||||
assert.Equal(t, []gatev1.RouteParentStatus{gatewayParent, otherParent}, report.httpRoutes[route].Parents)
|
||||
}
|
||||
|
||||
func TestStatusReport_RecordGRPCRouteStatus(t *testing.T) {
|
||||
report := newStatusReport()
|
||||
route := ktypes.NamespacedName{Namespace: "default", Name: "my-route"}
|
||||
|
||||
gatewayParent := gatev1.RouteParentStatus{ParentRef: gatev1.ParentReference{Name: "gateway"}}
|
||||
otherParent := gatev1.RouteParentStatus{ParentRef: gatev1.ParentReference{Name: "other-gateway"}}
|
||||
|
||||
report.RecordGRPCRouteStatus(route, gatewayParent)
|
||||
report.RecordGRPCRouteStatus(route, otherParent)
|
||||
|
||||
assert.Equal(t, []gatev1.RouteParentStatus{gatewayParent, otherParent}, report.grpcRoutes[route].Parents)
|
||||
}
|
||||
|
||||
func TestStatusReport_RecordTCPRouteStatus(t *testing.T) {
|
||||
report := newStatusReport()
|
||||
route := ktypes.NamespacedName{Namespace: "default", Name: "my-route"}
|
||||
|
||||
gatewayParent := gatev1.RouteParentStatus{ParentRef: gatev1.ParentReference{Name: "gateway"}}
|
||||
otherParent := gatev1.RouteParentStatus{ParentRef: gatev1.ParentReference{Name: "other-gateway"}}
|
||||
|
||||
report.RecordTCPRouteStatus(route, gatewayParent)
|
||||
report.RecordTCPRouteStatus(route, otherParent)
|
||||
|
||||
assert.Equal(t, []gatev1.RouteParentStatus{gatewayParent, otherParent}, report.tcpRoutes[route].Parents)
|
||||
}
|
||||
|
||||
func TestStatusReport_RecordTLSRouteStatus(t *testing.T) {
|
||||
report := newStatusReport()
|
||||
route := ktypes.NamespacedName{Namespace: "default", Name: "my-route"}
|
||||
|
||||
gatewayParent := gatev1.RouteParentStatus{ParentRef: gatev1.ParentReference{Name: "gateway"}}
|
||||
otherParent := gatev1.RouteParentStatus{ParentRef: gatev1.ParentReference{Name: "other-gateway"}}
|
||||
|
||||
report.RecordTLSRouteStatus(route, gatewayParent)
|
||||
report.RecordTLSRouteStatus(route, otherParent)
|
||||
|
||||
assert.Equal(t, []gatev1.RouteParentStatus{gatewayParent, otherParent}, report.tlsRoutes[route].Parents)
|
||||
}
|
||||
|
||||
func TestStatusReport_RecordBackendTLSPolicyStatus(t *testing.T) {
|
||||
gatewayAncestor := gatev1.PolicyAncestorStatus{
|
||||
AncestorRef: gatev1.ParentReference{Name: "gateway"},
|
||||
ControllerName: controllerName,
|
||||
}
|
||||
otherAncestor := gatev1.PolicyAncestorStatus{
|
||||
AncestorRef: gatev1.ParentReference{Name: "other-gateway"},
|
||||
ControllerName: controllerName,
|
||||
}
|
||||
|
||||
testCases := []struct {
|
||||
desc string
|
||||
records []gatev1.PolicyAncestorStatus
|
||||
expected []gatev1.PolicyAncestorStatus
|
||||
}{
|
||||
{
|
||||
desc: "distinct ancestor refs accumulate",
|
||||
records: []gatev1.PolicyAncestorStatus{gatewayAncestor, otherAncestor},
|
||||
expected: []gatev1.PolicyAncestorStatus{gatewayAncestor, otherAncestor},
|
||||
},
|
||||
{
|
||||
desc: "same ancestor ref is replaced, not duplicated",
|
||||
records: []gatev1.PolicyAncestorStatus{
|
||||
gatewayAncestor,
|
||||
{
|
||||
AncestorRef: gatev1.ParentReference{Name: "gateway"},
|
||||
ControllerName: "another.io/controller",
|
||||
},
|
||||
},
|
||||
expected: []gatev1.PolicyAncestorStatus{
|
||||
{
|
||||
AncestorRef: gatev1.ParentReference{Name: "gateway"},
|
||||
ControllerName: "another.io/controller",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range testCases {
|
||||
t.Run(test.desc, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
report := newStatusReport()
|
||||
policy := ktypes.NamespacedName{Namespace: "default", Name: "my-policy"}
|
||||
|
||||
for _, record := range test.records {
|
||||
report.RecordBackendTLSPolicyStatus(policy, record)
|
||||
}
|
||||
|
||||
assert.Equal(t, test.expected, report.backendTLSPolicies[policy].Ancestors)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -19,7 +19,7 @@ import (
|
||||
gatev1alpha2 "sigs.k8s.io/gateway-api/apis/v1alpha2"
|
||||
)
|
||||
|
||||
func (p *Provider) loadTCPRoutes(ctx context.Context, gatewayListeners []gatewayListener, conf *dynamic.Configuration) {
|
||||
func (p *Provider) loadTCPRoutes(ctx context.Context, gatewayListeners []gatewayListener, conf *dynamic.Configuration, statusReport *statusReport) {
|
||||
logger := log.Ctx(ctx)
|
||||
routes, err := p.client.ListTCPRoutes()
|
||||
if err != nil {
|
||||
@@ -28,19 +28,13 @@ func (p *Provider) loadTCPRoutes(ctx context.Context, gatewayListeners []gateway
|
||||
}
|
||||
|
||||
for _, route := range routes {
|
||||
logger := log.Ctx(ctx).With().
|
||||
Str("tcp_route", route.Name).
|
||||
Str("namespace", route.Namespace).
|
||||
Logger()
|
||||
|
||||
routeListeners := matchingGatewayListeners(gatewayListeners, route.Namespace, route.Spec.ParentRefs)
|
||||
if len(routeListeners) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
var parentStatuses []gatev1alpha2.RouteParentStatus
|
||||
for _, parentRef := range route.Spec.ParentRefs {
|
||||
parentStatus := &gatev1alpha2.RouteParentStatus{
|
||||
parentStatus := gatev1alpha2.RouteParentStatus{
|
||||
ParentRef: parentRef,
|
||||
ControllerName: controllerName,
|
||||
Conditions: []metav1.Condition{
|
||||
@@ -77,18 +71,7 @@ func (p *Provider) loadTCPRoutes(ctx context.Context, gatewayListeners []gateway
|
||||
parentStatus.Conditions = upsertRouteConditionResolvedRefs(parentStatus.Conditions, resolveRefCondition)
|
||||
}
|
||||
|
||||
parentStatuses = append(parentStatuses, *parentStatus)
|
||||
}
|
||||
|
||||
routeStatus := gatev1alpha2.TCPRouteStatus{
|
||||
RouteStatus: gatev1alpha2.RouteStatus{
|
||||
Parents: parentStatuses,
|
||||
},
|
||||
}
|
||||
if err := p.client.UpdateTCPRouteStatus(ctx, ktypes.NamespacedName{Namespace: route.Namespace, Name: route.Name}, routeStatus); err != nil {
|
||||
logger.Warn().
|
||||
Err(err).
|
||||
Msg("Unable to update TCPRoute status")
|
||||
statusReport.RecordTCPRouteStatus(ktypes.NamespacedName{Namespace: route.Namespace, Name: route.Name}, parentStatus)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -221,6 +204,17 @@ func (p *Provider) loadTCPService(route *gatev1alpha2.TCPRoute, backendRef gatev
|
||||
namespace := route.Namespace
|
||||
if backendRef.Namespace != nil && *backendRef.Namespace != "" {
|
||||
namespace = string(*backendRef.Namespace)
|
||||
|
||||
if strings.Contains(string(backendRef.Name), "@") {
|
||||
return provider.Normalize(namespace + "-" + string(backendRef.Name)), nil, &metav1.Condition{
|
||||
Type: string(gatev1.RouteConditionResolvedRefs),
|
||||
Status: metav1.ConditionFalse,
|
||||
ObservedGeneration: route.Generation,
|
||||
LastTransitionTime: metav1.Now(),
|
||||
Reason: string(gatev1.RouteReasonRefNotPermitted),
|
||||
Message: fmt.Sprintf("Cannot load TCPRoute BackendRef %s/%s/%s/%s: namespace is not allowed with a cross-provider reference", group, kind, namespace, backendRef.Name),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
serviceName := provider.Normalize(namespace + "-" + string(backendRef.Name))
|
||||
@@ -347,4 +341,9 @@ func mergeTCPConfiguration(from, to *dynamic.Configuration) {
|
||||
to.TCP.Services = map[string]*dynamic.TCPService{}
|
||||
}
|
||||
maps.Copy(to.TCP.Services, from.TCP.Services)
|
||||
|
||||
if to.TCP.ServersTransports == nil {
|
||||
to.TCP.ServersTransports = map[string]*dynamic.TCPServersTransport{}
|
||||
}
|
||||
maps.Copy(to.TCP.ServersTransports, from.TCP.ServersTransports)
|
||||
}
|
||||
|
||||
@@ -5,12 +5,15 @@ import (
|
||||
"fmt"
|
||||
"net"
|
||||
"regexp"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/traefik/traefik/v3/pkg/config/dynamic"
|
||||
"github.com/traefik/traefik/v3/pkg/provider"
|
||||
"github.com/traefik/traefik/v3/pkg/tls"
|
||||
"github.com/traefik/traefik/v3/pkg/types"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
ktypes "k8s.io/apimachinery/pkg/types"
|
||||
@@ -18,7 +21,7 @@ import (
|
||||
gatev1 "sigs.k8s.io/gateway-api/apis/v1"
|
||||
)
|
||||
|
||||
func (p *Provider) loadTLSRoutes(ctx context.Context, gatewayListeners []gatewayListener, conf *dynamic.Configuration) {
|
||||
func (p *Provider) loadTLSRoutes(ctx context.Context, gatewayListeners []gatewayListener, conf *dynamic.Configuration, statusReport *statusReport) {
|
||||
logger := log.Ctx(ctx)
|
||||
routes, err := p.client.ListTLSRoutes()
|
||||
if err != nil {
|
||||
@@ -27,18 +30,13 @@ func (p *Provider) loadTLSRoutes(ctx context.Context, gatewayListeners []gateway
|
||||
}
|
||||
|
||||
for _, route := range routes {
|
||||
logger := log.Ctx(ctx).With().
|
||||
Str("tls_route", route.Name).
|
||||
Str("namespace", route.Namespace).Logger()
|
||||
|
||||
routeListeners := matchingGatewayListeners(gatewayListeners, route.Namespace, route.Spec.ParentRefs)
|
||||
if len(routeListeners) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
var parentStatuses []gatev1.RouteParentStatus
|
||||
for _, parentRef := range route.Spec.ParentRefs {
|
||||
parentStatus := &gatev1.RouteParentStatus{
|
||||
parentStatus := gatev1.RouteParentStatus{
|
||||
ParentRef: parentRef,
|
||||
ControllerName: controllerName,
|
||||
Conditions: []metav1.Condition{
|
||||
@@ -73,14 +71,14 @@ func (p *Provider) loadTLSRoutes(ctx context.Context, gatewayListeners []gateway
|
||||
}
|
||||
}
|
||||
|
||||
routeConf, resolveRefCondition := p.loadTLSRoute(listener, route, hostnames)
|
||||
routeConf, resolveRefCondition := p.loadTLSRoute(listener, route, hostnames, statusReport)
|
||||
if accepted && listener.Attached {
|
||||
mergeTCPConfiguration(routeConf, conf)
|
||||
}
|
||||
parentStatus.Conditions = upsertRouteConditionResolvedRefs(parentStatus.Conditions, resolveRefCondition)
|
||||
}
|
||||
|
||||
parentStatuses = append(parentStatuses, *parentStatus)
|
||||
statusReport.RecordTLSRouteStatus(ktypes.NamespacedName{Namespace: route.Namespace, Name: route.Name}, parentStatus)
|
||||
}
|
||||
|
||||
// When there is at least one TLS listener, we add a default deny-all route to avoid accepting traffic for undefined hosts.
|
||||
@@ -96,21 +94,10 @@ func (p *Provider) loadTLSRoutes(ctx context.Context, gatewayListeners []gateway
|
||||
LoadBalancer: &dynamic.TCPServersLoadBalancer{},
|
||||
}
|
||||
}
|
||||
|
||||
routeStatus := gatev1.TLSRouteStatus{
|
||||
RouteStatus: gatev1.RouteStatus{
|
||||
Parents: parentStatuses,
|
||||
},
|
||||
}
|
||||
if err := p.client.UpdateTLSRouteStatus(ctx, ktypes.NamespacedName{Namespace: route.Namespace, Name: route.Name}, routeStatus); err != nil {
|
||||
logger.Warn().
|
||||
Err(err).
|
||||
Msg("Unable to update TLSRoute status")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Provider) loadTLSRoute(listener gatewayListener, route *gatev1.TLSRoute, hostnames []gatev1.Hostname) (*dynamic.Configuration, metav1.Condition) {
|
||||
func (p *Provider) loadTLSRoute(listener gatewayListener, route *gatev1.TLSRoute, hostnames []gatev1.Hostname, statusReport *statusReport) (*dynamic.Configuration, metav1.Condition) {
|
||||
conf := &dynamic.Configuration{
|
||||
TCP: &dynamic.TCPConfiguration{
|
||||
Routers: make(map[string]*dynamic.TCPRouter),
|
||||
@@ -171,7 +158,7 @@ func (p *Provider) loadTLSRoute(listener gatewayListener, route *gatev1.TLSRoute
|
||||
}
|
||||
|
||||
var serviceCondition *metav1.Condition
|
||||
router.Service, serviceCondition = p.loadTLSWRRService(conf, routerName, routeRule.BackendRefs, route)
|
||||
router.Service, serviceCondition = p.loadTLSWRRService(listener, conf, routerName, routeRule.BackendRefs, route, statusReport)
|
||||
if serviceCondition != nil {
|
||||
condition = *serviceCondition
|
||||
}
|
||||
@@ -183,7 +170,7 @@ func (p *Provider) loadTLSRoute(listener gatewayListener, route *gatev1.TLSRoute
|
||||
}
|
||||
|
||||
// loadTLSWRRService is generating a WRR service, even when there is only one target.
|
||||
func (p *Provider) loadTLSWRRService(conf *dynamic.Configuration, routeKey string, backendRefs []gatev1.BackendRef, route *gatev1.TLSRoute) (string, *metav1.Condition) {
|
||||
func (p *Provider) loadTLSWRRService(listener gatewayListener, conf *dynamic.Configuration, routeKey string, backendRefs []gatev1.BackendRef, route *gatev1.TLSRoute, statusReport *statusReport) (string, *metav1.Condition) {
|
||||
name := routeKey + "-wrr"
|
||||
if _, ok := conf.TCP.Services[name]; ok {
|
||||
return name, nil
|
||||
@@ -192,7 +179,7 @@ func (p *Provider) loadTLSWRRService(conf *dynamic.Configuration, routeKey strin
|
||||
var wrr dynamic.TCPWeightedRoundRobin
|
||||
var condition *metav1.Condition
|
||||
for _, backendRef := range backendRefs {
|
||||
svcName, svc, errCondition := p.loadTLSService(route, backendRef)
|
||||
svcName, svc, errCondition := p.loadTLSService(listener, conf, route, backendRef, statusReport)
|
||||
weight := ptr.To(int(ptr.Deref(backendRef.Weight, 1)))
|
||||
|
||||
if errCondition != nil {
|
||||
@@ -226,7 +213,7 @@ func (p *Provider) loadTLSWRRService(conf *dynamic.Configuration, routeKey strin
|
||||
return name, condition
|
||||
}
|
||||
|
||||
func (p *Provider) loadTLSService(route *gatev1.TLSRoute, backendRef gatev1.BackendRef) (string, *dynamic.TCPService, *metav1.Condition) {
|
||||
func (p *Provider) loadTLSService(listener gatewayListener, conf *dynamic.Configuration, route *gatev1.TLSRoute, backendRef gatev1.BackendRef, statusReport *statusReport) (string, *dynamic.TCPService, *metav1.Condition) {
|
||||
kind := ptr.Deref(backendRef.Kind, kindService)
|
||||
|
||||
group := groupCore
|
||||
@@ -237,6 +224,17 @@ func (p *Provider) loadTLSService(route *gatev1.TLSRoute, backendRef gatev1.Back
|
||||
namespace := route.Namespace
|
||||
if backendRef.Namespace != nil && *backendRef.Namespace != "" {
|
||||
namespace = string(*backendRef.Namespace)
|
||||
|
||||
if strings.Contains(string(backendRef.Name), "@") {
|
||||
return provider.Normalize(namespace + "-" + string(backendRef.Name)), nil, &metav1.Condition{
|
||||
Type: string(gatev1.RouteConditionResolvedRefs),
|
||||
Status: metav1.ConditionFalse,
|
||||
ObservedGeneration: route.Generation,
|
||||
LastTransitionTime: metav1.Now(),
|
||||
Reason: string(gatev1.RouteReasonRefNotPermitted),
|
||||
Message: fmt.Sprintf("Cannot load TLSRoute BackendRef %s/%s/%s/%s: namespace is not allowed with a cross-provider reference", group, kind, namespace, backendRef.Name),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
serviceName := provider.Normalize(namespace + "-" + string(backendRef.Name))
|
||||
@@ -283,18 +281,23 @@ func (p *Provider) loadTLSService(route *gatev1.TLSRoute, backendRef gatev1.Back
|
||||
portStr := strconv.FormatInt(int64(port), 10)
|
||||
serviceName = provider.Normalize(serviceName + "-" + portStr)
|
||||
|
||||
lb, errCondition := p.loadTLSServers(namespace, route, backendRef)
|
||||
lb, st, errCondition := p.loadTLSServers(namespace, route, backendRef, listener, statusReport)
|
||||
if errCondition != nil {
|
||||
return serviceName, nil, errCondition
|
||||
}
|
||||
|
||||
if st != nil {
|
||||
lb.ServersTransport = serviceName
|
||||
conf.TCP.ServersTransports[serviceName] = st
|
||||
}
|
||||
|
||||
return serviceName, &dynamic.TCPService{LoadBalancer: lb}, nil
|
||||
}
|
||||
|
||||
func (p *Provider) loadTLSServers(namespace string, route *gatev1.TLSRoute, backendRef gatev1.BackendRef) (*dynamic.TCPServersLoadBalancer, *metav1.Condition) {
|
||||
func (p *Provider) loadTLSServers(namespace string, route *gatev1.TLSRoute, backendRef gatev1.BackendRef, listener gatewayListener, statusReport *statusReport) (*dynamic.TCPServersLoadBalancer, *dynamic.TCPServersTransport, *metav1.Condition) {
|
||||
backendAddresses, svcPort, err := p.getBackendAddresses(namespace, backendRef)
|
||||
if err != nil {
|
||||
return nil, &metav1.Condition{
|
||||
return nil, nil, &metav1.Condition{
|
||||
Type: string(gatev1.RouteConditionResolvedRefs),
|
||||
Status: metav1.ConditionFalse,
|
||||
ObservedGeneration: route.GetGeneration(),
|
||||
@@ -304,8 +307,116 @@ func (p *Provider) loadTLSServers(namespace string, route *gatev1.TLSRoute, back
|
||||
}
|
||||
}
|
||||
|
||||
backendTLSPolicies, err := p.client.ListBackendTLSPoliciesForService(namespace, string(backendRef.Name))
|
||||
if err != nil {
|
||||
return nil, nil, &metav1.Condition{
|
||||
Type: string(gatev1.RouteConditionResolvedRefs),
|
||||
Status: metav1.ConditionFalse,
|
||||
ObservedGeneration: route.Generation,
|
||||
LastTransitionTime: metav1.Now(),
|
||||
Reason: string(gatev1.RouteReasonRefNotPermitted),
|
||||
Message: fmt.Sprintf("Cannot list BackendTLSPolicies for Service %s/%s: %s", namespace, string(backendRef.Name), err),
|
||||
}
|
||||
}
|
||||
|
||||
// Sort BackendTLSPolicies by creation timestamp, then by name to match the BackendTLSPolicy requirements.
|
||||
slices.SortStableFunc(backendTLSPolicies, func(a, b *gatev1.BackendTLSPolicy) int {
|
||||
cmpTime := a.CreationTimestamp.Time.Compare(b.CreationTimestamp.Time)
|
||||
if cmpTime == 0 {
|
||||
return strings.Compare(a.Name, b.Name)
|
||||
}
|
||||
return cmpTime
|
||||
})
|
||||
|
||||
var serversTransport *dynamic.TCPServersTransport
|
||||
for _, policy := range backendTLSPolicies {
|
||||
for _, targetRef := range policy.Spec.TargetRefs {
|
||||
// Skip targetRefs that doesn't match the backendRef,
|
||||
// since a BackendTLSPolicy can select multiple services.
|
||||
if targetRef.Name != backendRef.Name {
|
||||
continue
|
||||
}
|
||||
// Skip the targetRef if the sectionName doesn't match the backendRef port.
|
||||
if targetRef.SectionName != nil && svcPort.Name != string(*targetRef.SectionName) {
|
||||
continue
|
||||
}
|
||||
|
||||
policyAncestorStatus := gatev1.PolicyAncestorStatus{
|
||||
AncestorRef: gatev1.ParentReference{
|
||||
Group: ptr.To(gatev1.Group(groupGateway)),
|
||||
Kind: ptr.To(gatev1.Kind(kindGateway)),
|
||||
Namespace: ptr.To(gatev1.Namespace(namespace)),
|
||||
Name: gatev1.ObjectName(listener.GWName),
|
||||
SectionName: ptr.To(gatev1.SectionName(listener.Name)),
|
||||
},
|
||||
ControllerName: controllerName,
|
||||
}
|
||||
|
||||
// Multiple BackendTLSPolicies can match the same service port, meaning that there is a conflict.
|
||||
if serversTransport != nil {
|
||||
policyAncestorStatus.Conditions = append(policyAncestorStatus.Conditions,
|
||||
metav1.Condition{
|
||||
Type: string(gatev1.BackendTLSPolicyConditionResolvedRefs),
|
||||
Status: metav1.ConditionFalse,
|
||||
ObservedGeneration: policy.Generation,
|
||||
LastTransitionTime: metav1.Now(),
|
||||
Reason: string(gatev1.BackendTLSPolicyReasonResolvedRefs),
|
||||
},
|
||||
metav1.Condition{
|
||||
Type: string(gatev1.PolicyConditionAccepted),
|
||||
Status: metav1.ConditionFalse,
|
||||
ObservedGeneration: policy.Generation,
|
||||
LastTransitionTime: metav1.Now(),
|
||||
Reason: string(gatev1.PolicyReasonConflicted),
|
||||
},
|
||||
)
|
||||
|
||||
statusReport.RecordBackendTLSPolicyStatus(ktypes.NamespacedName{Namespace: policy.Namespace, Name: policy.Name}, policyAncestorStatus)
|
||||
|
||||
continue
|
||||
}
|
||||
|
||||
var resolvedRefCondition metav1.Condition
|
||||
serversTransport, resolvedRefCondition = p.loadTCPServersTransport(namespace, policy)
|
||||
|
||||
policyAncestorStatus.Conditions = append(policyAncestorStatus.Conditions, resolvedRefCondition)
|
||||
if resolvedRefCondition.Status == metav1.ConditionFalse {
|
||||
policyAncestorStatus.Conditions = append(policyAncestorStatus.Conditions, metav1.Condition{
|
||||
Type: string(gatev1.PolicyConditionAccepted),
|
||||
Status: metav1.ConditionFalse,
|
||||
ObservedGeneration: policy.Generation,
|
||||
LastTransitionTime: metav1.Now(),
|
||||
Reason: string(gatev1.BackendTLSPolicyReasonNoValidCACertificate),
|
||||
})
|
||||
} else {
|
||||
policyAncestorStatus.Conditions = append(policyAncestorStatus.Conditions, metav1.Condition{
|
||||
Type: string(gatev1.PolicyConditionAccepted),
|
||||
Status: metav1.ConditionTrue,
|
||||
ObservedGeneration: policy.Generation,
|
||||
LastTransitionTime: metav1.Now(),
|
||||
Reason: string(gatev1.PolicyReasonAccepted),
|
||||
})
|
||||
}
|
||||
|
||||
statusReport.RecordBackendTLSPolicyStatus(ktypes.NamespacedName{Namespace: policy.Namespace, Name: policy.Name}, policyAncestorStatus)
|
||||
|
||||
// When something went wrong during the loading of a ServersTransport,
|
||||
// we stop here and return a route condition error.
|
||||
if resolvedRefCondition.Status == metav1.ConditionFalse {
|
||||
return nil, nil, &metav1.Condition{
|
||||
Type: string(gatev1.RouteConditionResolvedRefs),
|
||||
Status: metav1.ConditionFalse,
|
||||
ObservedGeneration: route.Generation,
|
||||
LastTransitionTime: metav1.Now(),
|
||||
Reason: string(gatev1.RouteReasonRefNotPermitted),
|
||||
Message: fmt.Sprintf("Cannot apply BackendTLSPolicy for Service %s/%s: %s", namespace, string(backendRef.Name), resolvedRefCondition.Message),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if svcPort.Protocol != corev1.ProtocolTCP {
|
||||
return nil, &metav1.Condition{
|
||||
return nil, nil, &metav1.Condition{
|
||||
Type: string(gatev1.RouteConditionResolvedRefs),
|
||||
Status: metav1.ConditionFalse,
|
||||
ObservedGeneration: route.GetGeneration(),
|
||||
@@ -323,7 +434,120 @@ func (p *Provider) loadTLSServers(namespace string, route *gatev1.TLSRoute, back
|
||||
Address: net.JoinHostPort(ba.IP, strconv.Itoa(int(ba.Port))),
|
||||
})
|
||||
}
|
||||
return lb, nil
|
||||
return lb, serversTransport, nil
|
||||
}
|
||||
|
||||
func (p *Provider) loadTCPServersTransport(namespace string, policy *gatev1.BackendTLSPolicy) (*dynamic.TCPServersTransport, metav1.Condition) {
|
||||
st := &dynamic.TCPServersTransport{
|
||||
TLS: &dynamic.TLSClientConfig{
|
||||
ServerName: string(policy.Spec.Validation.Hostname),
|
||||
},
|
||||
}
|
||||
|
||||
if len(policy.Spec.Validation.SubjectAltNames) > 0 {
|
||||
// Per the Gateway API specification the Hostname should only be used for authentication
|
||||
// and not for certificate validation. Thus, if SubjectAltNames is specified, we ignore
|
||||
// the Hostname validation by setting the InsecureSkipVerify option to true.
|
||||
st.TLS.InsecureSkipVerify = true
|
||||
|
||||
for _, san := range policy.Spec.Validation.SubjectAltNames {
|
||||
switch san.Type {
|
||||
case gatev1.URISubjectAltNameType:
|
||||
st.TLS.PeerCertSANs = append(st.TLS.PeerCertSANs, tls.SAN{
|
||||
Type: tls.SANURIType,
|
||||
Value: string(san.URI),
|
||||
})
|
||||
case gatev1.HostnameSubjectAltNameType:
|
||||
st.TLS.PeerCertSANs = append(st.TLS.PeerCertSANs, tls.SAN{
|
||||
Type: tls.SANDNSNameType,
|
||||
Value: string(san.Hostname),
|
||||
})
|
||||
default:
|
||||
return nil, metav1.Condition{
|
||||
Type: string(gatev1.BackendTLSPolicyConditionResolvedRefs),
|
||||
Status: metav1.ConditionFalse,
|
||||
ObservedGeneration: policy.Generation,
|
||||
LastTransitionTime: metav1.Now(),
|
||||
Reason: string(gatev1.BackendTLSPolicyReasonInvalidKind),
|
||||
Message: fmt.Sprintf("Unsupported SubjectAltName type %q; only URI and Hostname types are supported", san.Type),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if policy.Spec.Validation.WellKnownCACertificates != nil {
|
||||
return st, metav1.Condition{
|
||||
Type: string(gatev1.BackendTLSPolicyConditionResolvedRefs),
|
||||
Status: metav1.ConditionTrue,
|
||||
ObservedGeneration: policy.Generation,
|
||||
LastTransitionTime: metav1.Now(),
|
||||
Reason: string(gatev1.BackendTLSPolicyReasonResolvedRefs),
|
||||
}
|
||||
}
|
||||
|
||||
for _, caCertRef := range policy.Spec.Validation.CACertificateRefs {
|
||||
if (caCertRef.Group != "" && caCertRef.Group != groupCore) || (caCertRef.Kind != kindConfigMap && caCertRef.Kind != kindSecret) {
|
||||
return nil, metav1.Condition{
|
||||
Type: string(gatev1.BackendTLSPolicyConditionResolvedRefs),
|
||||
Status: metav1.ConditionFalse,
|
||||
ObservedGeneration: policy.Generation,
|
||||
LastTransitionTime: metav1.Now(),
|
||||
Reason: string(gatev1.BackendTLSPolicyReasonInvalidKind),
|
||||
Message: "Only ConfigMaps and Secrets are supported",
|
||||
}
|
||||
}
|
||||
|
||||
var caCRT string
|
||||
switch caCertRef.Kind {
|
||||
case kindConfigMap:
|
||||
configmap, err := p.client.GetConfigMap(namespace, string(caCertRef.Name))
|
||||
if err != nil {
|
||||
return nil, metav1.Condition{
|
||||
Type: string(gatev1.BackendTLSPolicyConditionResolvedRefs),
|
||||
Status: metav1.ConditionFalse,
|
||||
ObservedGeneration: policy.Generation,
|
||||
LastTransitionTime: metav1.Now(),
|
||||
Reason: string(gatev1.BackendTLSPolicyReasonInvalidCACertificateRef),
|
||||
Message: fmt.Sprintf("getting configmap %s/%s: %s", namespace, string(caCertRef.Name), err),
|
||||
}
|
||||
}
|
||||
caCRT = configmap.Data["ca.crt"]
|
||||
case kindSecret:
|
||||
secret, err := p.client.GetSecret(namespace, string(caCertRef.Name))
|
||||
if err != nil {
|
||||
return nil, metav1.Condition{
|
||||
Type: string(gatev1.BackendTLSPolicyConditionResolvedRefs),
|
||||
Status: metav1.ConditionFalse,
|
||||
ObservedGeneration: policy.Generation,
|
||||
LastTransitionTime: metav1.Now(),
|
||||
Reason: string(gatev1.BackendTLSPolicyReasonInvalidCACertificateRef),
|
||||
Message: fmt.Sprintf("getting secret %s/%s: %s", namespace, string(caCertRef.Name), err),
|
||||
}
|
||||
}
|
||||
caCRT = string(secret.Data["ca.crt"])
|
||||
}
|
||||
|
||||
if caCRT == "" {
|
||||
return nil, metav1.Condition{
|
||||
Type: string(gatev1.BackendTLSPolicyConditionResolvedRefs),
|
||||
Status: metav1.ConditionFalse,
|
||||
ObservedGeneration: policy.Generation,
|
||||
LastTransitionTime: metav1.Now(),
|
||||
Reason: string(gatev1.BackendTLSPolicyReasonInvalidCACertificateRef),
|
||||
Message: fmt.Sprintf("%s %s/%s does not have a ca.crt", caCertRef.Kind, namespace, string(caCertRef.Name)),
|
||||
}
|
||||
}
|
||||
|
||||
st.TLS.RootCAs = append(st.TLS.RootCAs, types.FileOrContent(caCRT))
|
||||
}
|
||||
|
||||
return st, metav1.Condition{
|
||||
Type: string(gatev1.BackendTLSPolicyConditionResolvedRefs),
|
||||
Status: metav1.ConditionTrue,
|
||||
ObservedGeneration: policy.Generation,
|
||||
LastTransitionTime: metav1.Now(),
|
||||
Reason: string(gatev1.BackendTLSPolicyReasonResolvedRefs),
|
||||
}
|
||||
}
|
||||
|
||||
func hostSNIRule(hostnames []gatev1.Hostname) (string, int) {
|
||||
|
||||
@@ -415,11 +415,13 @@ func (p *Provider) build(ctx context.Context, ingressClasses []*netv1.IngressCla
|
||||
logger.Error().
|
||||
Err(err).
|
||||
Str("ingress", fmt.Sprintf("%s/%s rule-%d path-%d", ing.Namespace, ing.Name, ri, pi)).
|
||||
Msg("Cannot resolve auth secret, skipping auth middleware")
|
||||
} else {
|
||||
loc.BasicAuth = basic
|
||||
loc.DigestAuth = digest
|
||||
Msg("Cannot resolve auth secret, skipping ingress")
|
||||
// Skipping the ingress entirely when auth secret resolution fails,
|
||||
// to match ingress-nginx behavior.
|
||||
continue
|
||||
}
|
||||
loc.BasicAuth = basic
|
||||
loc.DigestAuth = digest
|
||||
}
|
||||
|
||||
// Pre-resolve custom headers ConfigMap.
|
||||
|
||||
@@ -158,7 +158,7 @@ func (c *clientWrapper) WatchAll(ctx context.Context, namespace, namespaceSelect
|
||||
}
|
||||
|
||||
for _, ns := range c.watchedNamespaces {
|
||||
factoryIngress := kinformers.NewSharedInformerFactoryWithOptions(c.clientset, resyncPeriod, kinformers.WithNamespace(ns))
|
||||
factoryIngress := kinformers.NewSharedInformerFactoryWithOptions(c.clientset, resyncPeriod, kinformers.WithNamespace(ns), kinformers.WithTransform(k8s.StripManagedFields))
|
||||
|
||||
_, err := factoryIngress.Networking().V1().Ingresses().Informer().AddEventHandler(eventHandler)
|
||||
if err != nil {
|
||||
@@ -167,7 +167,7 @@ func (c *clientWrapper) WatchAll(ctx context.Context, namespace, namespaceSelect
|
||||
|
||||
c.factoriesIngress[ns] = factoryIngress
|
||||
|
||||
factoryKube := kinformers.NewSharedInformerFactoryWithOptions(c.clientset, resyncPeriod, kinformers.WithNamespace(ns))
|
||||
factoryKube := kinformers.NewSharedInformerFactoryWithOptions(c.clientset, resyncPeriod, kinformers.WithNamespace(ns), kinformers.WithTransform(k8s.StripManagedFields))
|
||||
_, err = factoryKube.Core().V1().Services().Informer().AddEventHandler(eventHandler)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -178,14 +178,14 @@ func (c *clientWrapper) WatchAll(ctx context.Context, namespace, namespaceSelect
|
||||
}
|
||||
c.factoriesKube[ns] = factoryKube
|
||||
|
||||
factorySecret := kinformers.NewSharedInformerFactoryWithOptions(c.clientset, resyncPeriod, kinformers.WithNamespace(ns), kinformers.WithTweakListOptions(notOwnedByHelm))
|
||||
factorySecret := kinformers.NewSharedInformerFactoryWithOptions(c.clientset, resyncPeriod, kinformers.WithNamespace(ns), kinformers.WithTweakListOptions(notOwnedByHelm), kinformers.WithTransform(k8s.StripManagedFields))
|
||||
_, err = factorySecret.Core().V1().Secrets().Informer().AddEventHandler(eventHandler)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
c.factoriesSecret[ns] = factorySecret
|
||||
|
||||
factoryConfigMap := kinformers.NewSharedInformerFactoryWithOptions(c.clientset, resyncPeriod, kinformers.WithNamespace(ns), kinformers.WithTweakListOptions(notOwnedByHelm))
|
||||
factoryConfigMap := kinformers.NewSharedInformerFactoryWithOptions(c.clientset, resyncPeriod, kinformers.WithNamespace(ns), kinformers.WithTweakListOptions(notOwnedByHelm), kinformers.WithTransform(k8s.StripManagedFields))
|
||||
_, err = factoryConfigMap.Core().V1().ConfigMaps().Informer().AddEventHandler(eventHandler)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -226,7 +226,7 @@ func (c *clientWrapper) WatchAll(ctx context.Context, namespace, namespaceSelect
|
||||
}
|
||||
}
|
||||
|
||||
c.clusterScopeFactory = kinformers.NewSharedInformerFactory(c.clientset, resyncPeriod)
|
||||
c.clusterScopeFactory = kinformers.NewSharedInformerFactoryWithOptions(c.clientset, resyncPeriod, kinformers.WithTransform(k8s.StripManagedFields))
|
||||
|
||||
if !c.ignoreIngressClasses {
|
||||
_, err = c.clusterScopeFactory.Networking().V1().IngressClasses().Informer().AddEventHandler(eventHandler)
|
||||
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
---
|
||||
kind: Ingress
|
||||
apiVersion: networking.k8s.io/v1
|
||||
metadata:
|
||||
name: ingress-with-basicauth-secret-missing
|
||||
namespace: default
|
||||
annotations:
|
||||
nginx.ingress.kubernetes.io/auth-type: "basic"
|
||||
nginx.ingress.kubernetes.io/auth-secret-type: "auth-file"
|
||||
nginx.ingress.kubernetes.io/auth-secret: "default/missing-basic-auth"
|
||||
nginx.ingress.kubernetes.io/auth-realm: "Authentication Required"
|
||||
|
||||
spec:
|
||||
ingressClassName: nginx
|
||||
rules:
|
||||
- host: whoami.localhost
|
||||
http:
|
||||
paths:
|
||||
- path: /basicauth
|
||||
pathType: Exact
|
||||
backend:
|
||||
service:
|
||||
name: whoami
|
||||
port:
|
||||
number: 80
|
||||
+69
@@ -0,0 +1,69 @@
|
||||
---
|
||||
kind: Ingress
|
||||
apiVersion: networking.k8s.io/v1
|
||||
metadata:
|
||||
name: ingress-with-endpoint-conditions
|
||||
namespace: default
|
||||
annotations:
|
||||
kubernetes.io/ingress.class: nginx
|
||||
|
||||
spec:
|
||||
rules:
|
||||
- host: whoami.localhost
|
||||
http:
|
||||
paths:
|
||||
- path: /
|
||||
pathType: Prefix
|
||||
backend:
|
||||
service:
|
||||
name: whoami
|
||||
port:
|
||||
number: 80
|
||||
|
||||
---
|
||||
kind: Service
|
||||
apiVersion: v1
|
||||
metadata:
|
||||
name: whoami
|
||||
namespace: default
|
||||
|
||||
spec:
|
||||
clusterIP: 10.10.10.1
|
||||
ports:
|
||||
- name: web
|
||||
protocol: TCP
|
||||
port: 80
|
||||
targetPort: web
|
||||
|
||||
---
|
||||
kind: EndpointSlice
|
||||
apiVersion: discovery.k8s.io/v1
|
||||
metadata:
|
||||
name: whoami-abc
|
||||
namespace: default
|
||||
labels:
|
||||
kubernetes.io/service-name: whoami
|
||||
|
||||
addressType: IPv4
|
||||
ports:
|
||||
- name: web
|
||||
port: 80
|
||||
endpoints:
|
||||
- addresses:
|
||||
- 10.10.0.1
|
||||
conditions:
|
||||
ready: true
|
||||
serving: true
|
||||
terminating: false
|
||||
- addresses:
|
||||
- 10.10.0.2
|
||||
conditions:
|
||||
ready: false
|
||||
serving: true
|
||||
terminating: true
|
||||
- addresses:
|
||||
- 10.10.0.3
|
||||
conditions:
|
||||
ready: false
|
||||
serving: false
|
||||
terminating: true
|
||||
@@ -1329,6 +1329,37 @@ func TestLoadIngresses(t *testing.T) {
|
||||
TLS: &dynamic.TLSConfiguration{},
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "Basic Auth with missing secret — ingress is skipped entirely",
|
||||
paths: []string{
|
||||
"services.yml",
|
||||
"ingressclasses.yml",
|
||||
"ingresses/ingress-with-basicauth-secret-missing.yml",
|
||||
},
|
||||
expected: &dynamic.Configuration{
|
||||
TCP: &dynamic.TCPConfiguration{
|
||||
Routers: map[string]*dynamic.TCPRouter{},
|
||||
Services: map[string]*dynamic.TCPService{},
|
||||
},
|
||||
HTTP: &dynamic.HTTPConfiguration{
|
||||
Routers: map[string]*dynamic.Router{},
|
||||
Middlewares: map[string]*dynamic.Middleware{},
|
||||
Services: map[string]*dynamic.Service{
|
||||
"unavailable-service": {
|
||||
LoadBalancer: &dynamic.ServersLoadBalancer{
|
||||
Strategy: "wrr",
|
||||
PassHostHeader: ptr.To(true),
|
||||
ResponseForwarding: &dynamic.ResponseForwarding{
|
||||
FlushInterval: dynamic.DefaultFlushInterval,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
ServersTransports: map[string]*dynamic.ServersTransport{},
|
||||
},
|
||||
TLS: &dynamic.TLSConfiguration{},
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "Forward Auth",
|
||||
paths: []string{
|
||||
@@ -15656,6 +15687,102 @@ func TestLoadIngresses(t *testing.T) {
|
||||
TLS: &dynamic.TLSConfiguration{},
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "Ingress with endpoint conditions",
|
||||
paths: []string{
|
||||
"ingressclasses.yml",
|
||||
"ingresses/ingress-with-endpoint-conditions.yml",
|
||||
},
|
||||
expected: &dynamic.Configuration{
|
||||
TCP: &dynamic.TCPConfiguration{
|
||||
Routers: map[string]*dynamic.TCPRouter{},
|
||||
Services: map[string]*dynamic.TCPService{},
|
||||
},
|
||||
HTTP: &dynamic.HTTPConfiguration{
|
||||
Routers: map[string]*dynamic.Router{
|
||||
"default-ingress-with-endpoint-conditions-rule-0-path-0": {
|
||||
EntryPoints: []string{"http"},
|
||||
Rule: `Host("whoami.localhost") && PathPrefix("/")`,
|
||||
RuleSyntax: "default",
|
||||
Service: "default-ingress-with-endpoint-conditions-whoami-80",
|
||||
Middlewares: []string{"default-ingress-with-endpoint-conditions-rule-0-path-0-retry"},
|
||||
Observability: &dynamic.RouterObservabilityConfig{
|
||||
Metadata: &dynamic.ObservabilityMetadata{
|
||||
Ingress: &dynamic.KubernetesIngressMetadata{
|
||||
Namespace: "default",
|
||||
IngressName: "ingress-with-endpoint-conditions",
|
||||
ServiceName: "whoami",
|
||||
ServicePort: "80",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
"default-ingress-with-endpoint-conditions-rule-0-path-0-tls": {
|
||||
EntryPoints: []string{"https"},
|
||||
Rule: `Host("whoami.localhost") && PathPrefix("/")`,
|
||||
RuleSyntax: "default",
|
||||
Service: "default-ingress-with-endpoint-conditions-whoami-80",
|
||||
Middlewares: []string{"default-ingress-with-endpoint-conditions-rule-0-path-0-tls-retry"},
|
||||
TLS: &dynamic.RouterTLSConfig{},
|
||||
Observability: &dynamic.RouterObservabilityConfig{
|
||||
Metadata: &dynamic.ObservabilityMetadata{
|
||||
Ingress: &dynamic.KubernetesIngressMetadata{
|
||||
Namespace: "default",
|
||||
IngressName: "ingress-with-endpoint-conditions",
|
||||
ServiceName: "whoami",
|
||||
ServicePort: "80",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
Middlewares: map[string]*dynamic.Middleware{
|
||||
"default-ingress-with-endpoint-conditions-rule-0-path-0-retry": {
|
||||
Retry: &dynamic.Retry{Attempts: 3},
|
||||
},
|
||||
"default-ingress-with-endpoint-conditions-rule-0-path-0-tls-retry": {
|
||||
Retry: &dynamic.Retry{Attempts: 3},
|
||||
},
|
||||
},
|
||||
Services: map[string]*dynamic.Service{
|
||||
"default-ingress-with-endpoint-conditions-whoami-80": {
|
||||
LoadBalancer: &dynamic.ServersLoadBalancer{
|
||||
Servers: []dynamic.Server{
|
||||
{URL: "http://10.10.0.1:80"},
|
||||
{URL: "http://10.10.0.2:80", Fenced: true},
|
||||
},
|
||||
Strategy: dynamic.BalancerStrategyWRR,
|
||||
PassHostHeader: ptr.To(true),
|
||||
ServersTransport: "default-ingress-with-endpoint-conditions",
|
||||
ResponseForwarding: &dynamic.ResponseForwarding{
|
||||
FlushInterval: dynamic.DefaultFlushInterval,
|
||||
},
|
||||
},
|
||||
},
|
||||
"unavailable-service": {
|
||||
LoadBalancer: &dynamic.ServersLoadBalancer{
|
||||
Strategy: dynamic.BalancerStrategyWRR,
|
||||
PassHostHeader: ptr.To(true),
|
||||
ResponseForwarding: &dynamic.ResponseForwarding{
|
||||
FlushInterval: dynamic.DefaultFlushInterval,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
ServersTransports: map[string]*dynamic.ServersTransport{
|
||||
"default-ingress-with-endpoint-conditions": {
|
||||
ForwardingTimeouts: &dynamic.ForwardingTimeouts{
|
||||
DialTimeout: ptypes.Duration(60 * time.Second),
|
||||
ReadTimeout: ptypes.Duration(60 * time.Second),
|
||||
WriteTimeout: ptypes.Duration(60 * time.Second),
|
||||
IdleConnTimeout: ptypes.Duration(60 * time.Second),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
TLS: &dynamic.TLSConfiguration{},
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "Auth TLS secret missing — ingress is skipped entirely",
|
||||
paths: []string{
|
||||
|
||||
@@ -294,7 +294,8 @@ func buildService(backend *backend, serversTransportName string) *dynamic.Servic
|
||||
svc := &dynamic.Service{LoadBalancer: lb}
|
||||
for _, ep := range backend.Endpoints {
|
||||
svc.LoadBalancer.Servers = append(svc.LoadBalancer.Servers, dynamic.Server{
|
||||
URL: fmt.Sprintf("http://%s", ep.Address),
|
||||
URL: fmt.Sprintf("http://%s", ep.Address),
|
||||
Fenced: ep.Fenced,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -320,7 +321,8 @@ func buildServiceWithLocConfig(backend *backend, serversTransportName string, lo
|
||||
svc := &dynamic.Service{LoadBalancer: lb}
|
||||
for _, ep := range backend.Endpoints {
|
||||
svc.LoadBalancer.Servers = append(svc.LoadBalancer.Servers, dynamic.Server{
|
||||
URL: fmt.Sprintf("%s://%s", scheme, ep.Address),
|
||||
URL: fmt.Sprintf("%s://%s", scheme, ep.Address),
|
||||
Fenced: ep.Fenced,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -158,7 +158,7 @@ func (c *clientWrapper) WatchAll(namespaces []string, stopCh <-chan struct{}) (<
|
||||
}
|
||||
|
||||
for _, ns := range namespaces {
|
||||
factoryIngress := kinformers.NewSharedInformerFactoryWithOptions(c.clientset, resyncPeriod, kinformers.WithNamespace(ns), kinformers.WithTweakListOptions(matchesLabelSelector))
|
||||
factoryIngress := kinformers.NewSharedInformerFactoryWithOptions(c.clientset, resyncPeriod, kinformers.WithNamespace(ns), kinformers.WithTweakListOptions(matchesLabelSelector), kinformers.WithTransform(k8s.StripManagedFields))
|
||||
|
||||
_, err := factoryIngress.Networking().V1().Ingresses().Informer().AddEventHandler(eventHandler)
|
||||
if err != nil {
|
||||
@@ -167,7 +167,7 @@ func (c *clientWrapper) WatchAll(namespaces []string, stopCh <-chan struct{}) (<
|
||||
|
||||
c.factoriesIngress[ns] = factoryIngress
|
||||
|
||||
factoryKube := kinformers.NewSharedInformerFactoryWithOptions(c.clientset, resyncPeriod, kinformers.WithNamespace(ns))
|
||||
factoryKube := kinformers.NewSharedInformerFactoryWithOptions(c.clientset, resyncPeriod, kinformers.WithNamespace(ns), kinformers.WithTransform(k8s.StripManagedFields))
|
||||
_, err = factoryKube.Core().V1().Services().Informer().AddEventHandler(eventHandler)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -178,7 +178,7 @@ func (c *clientWrapper) WatchAll(namespaces []string, stopCh <-chan struct{}) (<
|
||||
}
|
||||
c.factoriesKube[ns] = factoryKube
|
||||
|
||||
factorySecret := kinformers.NewSharedInformerFactoryWithOptions(c.clientset, resyncPeriod, kinformers.WithNamespace(ns), kinformers.WithTweakListOptions(notOwnedByHelm))
|
||||
factorySecret := kinformers.NewSharedInformerFactoryWithOptions(c.clientset, resyncPeriod, kinformers.WithNamespace(ns), kinformers.WithTweakListOptions(notOwnedByHelm), kinformers.WithTransform(k8s.StripManagedFields))
|
||||
_, err = factorySecret.Core().V1().Secrets().Informer().AddEventHandler(eventHandler)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -213,7 +213,7 @@ func (c *clientWrapper) WatchAll(namespaces []string, stopCh <-chan struct{}) (<
|
||||
}
|
||||
|
||||
if !c.disableIngressClassInformer || !c.disableClusterScopeInformer {
|
||||
c.clusterScopeFactory = kinformers.NewSharedInformerFactory(c.clientset, resyncPeriod)
|
||||
c.clusterScopeFactory = kinformers.NewSharedInformerFactoryWithOptions(c.clientset, resyncPeriod, kinformers.WithTransform(k8s.StripManagedFields))
|
||||
|
||||
_, err := c.clusterScopeFactory.Networking().V1().IngressClasses().Informer().AddEventHandler(eventHandler)
|
||||
if err != nil {
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
---
|
||||
kind: Node
|
||||
apiVersion: v1
|
||||
metadata:
|
||||
name: node1
|
||||
|
||||
status:
|
||||
addresses:
|
||||
- type: InternalIP
|
||||
address: 10.0.0.1
|
||||
- type: ExternalIP
|
||||
address: 1.2.3.4
|
||||
|
||||
---
|
||||
kind: Node
|
||||
apiVersion: v1
|
||||
metadata:
|
||||
name: node2
|
||||
|
||||
status:
|
||||
addresses:
|
||||
- type: InternalIP
|
||||
address: 10.0.0.2
|
||||
|
||||
---
|
||||
kind: Ingress
|
||||
apiVersion: networking.k8s.io/v1
|
||||
metadata:
|
||||
name: foo
|
||||
namespace: default
|
||||
|
||||
spec:
|
||||
rules:
|
||||
- host: "*.foo.com"
|
||||
http:
|
||||
paths:
|
||||
- path: /
|
||||
pathType: Prefix
|
||||
backend:
|
||||
service:
|
||||
name: service1
|
||||
port:
|
||||
number: 80
|
||||
|
||||
---
|
||||
kind: Service
|
||||
apiVersion: v1
|
||||
metadata:
|
||||
name: service1
|
||||
namespace: default
|
||||
|
||||
spec:
|
||||
ports:
|
||||
- port: 80
|
||||
|
||||
clusterIP: 10.0.0.1
|
||||
|
||||
---
|
||||
kind: EndpointSlice
|
||||
apiVersion: discovery.k8s.io/v1
|
||||
metadata:
|
||||
name: service1-abc
|
||||
namespace: default
|
||||
labels:
|
||||
kubernetes.io/service-name: service1
|
||||
|
||||
addressType: IPv4
|
||||
ports:
|
||||
- port: 8080
|
||||
name: ""
|
||||
endpoints:
|
||||
- addresses:
|
||||
- 10.10.0.1
|
||||
conditions:
|
||||
ready: true
|
||||
@@ -48,6 +48,7 @@ type Provider struct {
|
||||
LabelSelector string `description:"Kubernetes Ingress label selector to use." json:"labelSelector,omitempty" toml:"labelSelector,omitempty" yaml:"labelSelector,omitempty" export:"true"`
|
||||
IngressClass string `description:"Value of kubernetes.io/ingress.class annotation or IngressClass name to watch for." json:"ingressClass,omitempty" toml:"ingressClass,omitempty" yaml:"ingressClass,omitempty" export:"true"`
|
||||
IngressEndpoint *EndpointIngress `description:"Kubernetes Ingress Endpoint." json:"ingressEndpoint,omitempty" toml:"ingressEndpoint,omitempty" yaml:"ingressEndpoint,omitempty" export:"true"`
|
||||
ReportNodeInternalIPs bool `description:"Report node internal IPs in Ingress status." json:"reportNodeInternalIPs,omitempty" toml:"reportNodeInternalIPs,omitempty" yaml:"reportNodeInternalIPs,omitempty" export:"true"`
|
||||
ThrottleDuration ptypes.Duration `description:"Ingress refresh throttle duration" json:"throttleDuration,omitempty" toml:"throttleDuration,omitempty" yaml:"throttleDuration,omitempty" export:"true"`
|
||||
AllowEmptyServices bool `description:"Allow creation of services without endpoints." json:"allowEmptyServices,omitempty" toml:"allowEmptyServices,omitempty" yaml:"allowEmptyServices,omitempty" export:"true"`
|
||||
AllowExternalNameServices bool `description:"Allow ExternalName services." json:"allowExternalNameServices,omitempty" toml:"allowExternalNameServices,omitempty" yaml:"allowExternalNameServices,omitempty" export:"true"`
|
||||
@@ -72,6 +73,14 @@ func (p *Provider) SetRouterTransform(routerTransform k8s.RouterTransform) {
|
||||
|
||||
// Init the provider.
|
||||
func (p *Provider) Init() error {
|
||||
if p.ReportNodeInternalIPs && p.IngressEndpoint != nil {
|
||||
return errors.New("reportNodeInternalIPs and ingressEndpoint are mutually exclusive")
|
||||
}
|
||||
|
||||
if p.ReportNodeInternalIPs && p.DisableClusterScopeResources {
|
||||
return errors.New("reportNodeInternalIPs and disableClusterScopeResources are mutually exclusive")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -246,6 +255,26 @@ func (p *Provider) loadConfigurationFromIngresses(ctx context.Context, client Cl
|
||||
|
||||
ingresses := client.GetIngresses()
|
||||
|
||||
var nodeIngressStatus []netv1.IngressLoadBalancerIngress
|
||||
if p.ReportNodeInternalIPs {
|
||||
nodes, _, err := client.GetNodes()
|
||||
if err != nil {
|
||||
log.Ctx(ctx).Error().Err(err).Msg("Error while getting nodes for ingress status")
|
||||
} else {
|
||||
for _, node := range nodes {
|
||||
for _, address := range node.Status.Addresses {
|
||||
if address.Type == corev1.NodeInternalIP {
|
||||
nodeIngressStatus = append(nodeIngressStatus, netv1.IngressLoadBalancerIngress{IP: address.Address})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(nodeIngressStatus) == 0 {
|
||||
log.Ctx(ctx).Error().Msg("No nodes with internal IP address found for ingress status")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
certConfigs := make(map[string]*tls.CertAndStores)
|
||||
for _, ingress := range ingresses {
|
||||
logger := log.Ctx(ctx).With().Str("ingress", ingress.Name).Str("namespace", ingress.Namespace).Logger()
|
||||
@@ -255,7 +284,7 @@ func (p *Provider) loadConfigurationFromIngresses(ctx context.Context, client Cl
|
||||
continue
|
||||
}
|
||||
|
||||
if err := p.updateIngressStatus(ingress, client); err != nil {
|
||||
if err := p.updateIngressStatus(ingress, client, nodeIngressStatus); err != nil {
|
||||
logger.Error().Err(err).Msg("Error while updating ingress status")
|
||||
}
|
||||
|
||||
@@ -442,7 +471,11 @@ func (p *Provider) loadConfigurationFromIngresses(ctx context.Context, client Cl
|
||||
return conf
|
||||
}
|
||||
|
||||
func (p *Provider) updateIngressStatus(ing *netv1.Ingress, k8sClient Client) error {
|
||||
func (p *Provider) updateIngressStatus(ing *netv1.Ingress, k8sClient Client, nodeIngressStatus []netv1.IngressLoadBalancerIngress) error {
|
||||
if len(nodeIngressStatus) > 0 {
|
||||
return k8sClient.UpdateIngressStatus(ing, nodeIngressStatus)
|
||||
}
|
||||
|
||||
// Only process if an EndpointIngress has been configured.
|
||||
if p.IngressEndpoint == nil {
|
||||
return nil
|
||||
@@ -623,12 +656,12 @@ func (p *Provider) loadService(client Client, namespace string, backend netv1.In
|
||||
return nil, errors.New("nodes lookup is disabled")
|
||||
}
|
||||
|
||||
nodes, nodesExists, nodesErr := client.GetNodes()
|
||||
nodes, _, nodesErr := client.GetNodes()
|
||||
if nodesErr != nil {
|
||||
return nil, nodesErr
|
||||
}
|
||||
|
||||
if !nodesExists || len(nodes) == 0 {
|
||||
if len(nodes) == 0 {
|
||||
return nil, fmt.Errorf("nodes not found in namespace %s", namespace)
|
||||
}
|
||||
|
||||
|
||||
@@ -3094,6 +3094,77 @@ func readResources(t *testing.T, paths []string) []runtime.Object {
|
||||
return k8sObjects
|
||||
}
|
||||
|
||||
func TestProviderInit(t *testing.T) {
|
||||
p := Provider{
|
||||
ReportNodeInternalIPs: true,
|
||||
IngressEndpoint: &EndpointIngress{IP: "1.2.3.4"},
|
||||
}
|
||||
assert.EqualError(t, p.Init(), "reportNodeInternalIPs and ingressEndpoint are mutually exclusive")
|
||||
|
||||
p2 := Provider{
|
||||
ReportNodeInternalIPs: true,
|
||||
DisableClusterScopeResources: true,
|
||||
}
|
||||
assert.EqualError(t, p2.Init(), "reportNodeInternalIPs and disableClusterScopeResources are mutually exclusive")
|
||||
|
||||
p3 := Provider{ReportNodeInternalIPs: true}
|
||||
assert.NoError(t, p3.Init())
|
||||
}
|
||||
|
||||
func TestReportNodeInternalIPs(t *testing.T) {
|
||||
testCases := []struct {
|
||||
desc string
|
||||
client clientMock
|
||||
expectedEmpty bool
|
||||
}{
|
||||
{
|
||||
desc: "nodes present",
|
||||
client: newClientMock(generateTestFilename("Node Internal IP")),
|
||||
},
|
||||
{
|
||||
desc: "GetNodes API error",
|
||||
client: clientMock{apiNodesError: errors.New("api nodes error")},
|
||||
expectedEmpty: true,
|
||||
},
|
||||
{
|
||||
desc: "no nodes found",
|
||||
client: clientMock{nodes: []*corev1.Node{}},
|
||||
expectedEmpty: true,
|
||||
},
|
||||
{
|
||||
desc: "nodes exist but none have an internal IP",
|
||||
client: clientMock{
|
||||
nodes: []*corev1.Node{
|
||||
{
|
||||
Status: corev1.NodeStatus{
|
||||
Addresses: []corev1.NodeAddress{
|
||||
{Type: corev1.NodeExternalIP, Address: "1.2.3.4"},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedEmpty: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range testCases {
|
||||
t.Run(test.desc, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
p := Provider{ReportNodeInternalIPs: true}
|
||||
conf := p.loadConfigurationFromIngresses(t.Context(), test.client)
|
||||
if test.expectedEmpty {
|
||||
assert.Empty(t, conf.HTTP.Routers)
|
||||
assert.Empty(t, conf.HTTP.Services)
|
||||
} else {
|
||||
assert.NotEmpty(t, conf.HTTP.Routers)
|
||||
assert.NotEmpty(t, conf.HTTP.Services)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestStrictPrefixMatchingRule(t *testing.T) {
|
||||
tests := []struct {
|
||||
path string
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
package k8s
|
||||
|
||||
import metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
|
||||
// StripManagedFields drops metadata.managedFields as objects enter the informer cache.
|
||||
// Traefik never reads them, and they inflate the cache footprint and the cost of copying
|
||||
// and comparing cached objects, which matters under heavy resource churn.
|
||||
func StripManagedFields(obj any) (any, error) {
|
||||
object, ok := obj.(metav1.Object)
|
||||
if !ok {
|
||||
return obj, nil
|
||||
}
|
||||
|
||||
object.SetManagedFields(nil)
|
||||
return obj, nil
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package k8s
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
)
|
||||
|
||||
func TestStripManagedFields(t *testing.T) {
|
||||
service := &corev1.Service{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "whoami",
|
||||
ManagedFields: []metav1.ManagedFieldsEntry{
|
||||
{
|
||||
Manager: "kubectl",
|
||||
Operation: metav1.ManagedFieldsOperationApply,
|
||||
APIVersion: "v1",
|
||||
Time: &metav1.Time{Time: time.Now()},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
got, err := StripManagedFields(service)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Same(t, service, got)
|
||||
assert.Nil(t, service.ManagedFields)
|
||||
assert.Equal(t, "whoami", service.Name)
|
||||
}
|
||||
|
||||
func TestStripManagedFieldsIgnoresNonKubernetesObjects(t *testing.T) {
|
||||
obj := struct{ Name string }{Name: "whoami"}
|
||||
|
||||
got, err := StripManagedFields(obj)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, obj, got)
|
||||
}
|
||||
@@ -122,13 +122,13 @@ func (c *clientWrapper) WatchAll(namespaces []string, stopCh <-chan struct{}) (<
|
||||
for _, ns := range namespaces {
|
||||
factory := knativenetworkinginformers.NewSharedInformerFactoryWithOptions(c.csKnativeNetworking, resyncPeriod, knativenetworkinginformers.WithNamespace(ns), knativenetworkinginformers.WithTweakListOptions(func(opts *metav1.ListOptions) {
|
||||
opts.LabelSelector = c.labelSelector
|
||||
}))
|
||||
}), knativenetworkinginformers.WithTransform(k8s.StripManagedFields))
|
||||
_, err := factory.Networking().V1alpha1().Ingresses().Informer().AddEventHandler(eventHandler)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
factoryKube := kinformers.NewSharedInformerFactoryWithOptions(c.csKube, resyncPeriod, kinformers.WithNamespace(ns))
|
||||
factoryKube := kinformers.NewSharedInformerFactoryWithOptions(c.csKube, resyncPeriod, kinformers.WithNamespace(ns), kinformers.WithTransform(k8s.StripManagedFields))
|
||||
_, err = factoryKube.Core().V1().Services().Informer().AddEventHandler(eventHandler)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
+101
-50
@@ -55,7 +55,7 @@ func mergeConfiguration(configurations dynamic.Configurations, defaultEntryPoint
|
||||
Str(logs.RouterName, routerName).
|
||||
Strs(logs.EntryPointName, defaultEntryPoints).
|
||||
Msg("No entryPoint defined for this router, using the default one(s) instead")
|
||||
router.EntryPoints = defaultEntryPoints
|
||||
router.EntryPoints = slices.Clone(defaultEntryPoints)
|
||||
}
|
||||
|
||||
// The `ruleSyntax` option is deprecated.
|
||||
@@ -99,7 +99,7 @@ func mergeConfiguration(configurations dynamic.Configurations, defaultEntryPoint
|
||||
log.Debug().
|
||||
Str(logs.RouterName, routerName).
|
||||
Msgf("No entryPoint defined for this TCP router, using the default one(s) instead: %+v", defaultEntryPoints)
|
||||
router.EntryPoints = defaultEntryPoints
|
||||
router.EntryPoints = slices.Clone(defaultEntryPoints)
|
||||
}
|
||||
conf.TCP.Routers[provider.MakeQualifiedName(pvd, routerName)] = router
|
||||
}
|
||||
@@ -173,80 +173,131 @@ func mergeConfiguration(configurations dynamic.Configurations, defaultEntryPoint
|
||||
return conf
|
||||
}
|
||||
|
||||
func resolveHTTPTLSOptions(cfg dynamic.Configuration) dynamic.Configuration {
|
||||
if cfg.HTTP == nil || len(cfg.HTTP.Routers) == 0 {
|
||||
return cfg
|
||||
// resolveHTTPTLSOptions resolves the TLS options for the given routers, on a per
|
||||
// entryPoint basis.
|
||||
//
|
||||
// TLS options conflicts (i.e. the same host served with different TLS options) can
|
||||
// only be detected and arbitrated within a single TLS listener, that is to say within
|
||||
// a single entryPoint. To honor that, routers are grouped per entryPoint and the
|
||||
// conflict detection is run independently for each entryPoint.
|
||||
//
|
||||
// A router keeps its original name, and its resolved TLS options, for the entryPoints
|
||||
// on which it does not conflict. For each entryPoint on which it conflicts, that
|
||||
// entryPoint is removed from the router and a dedicated copy is emitted, with its
|
||||
// TLSOptions reset to the default one, named following the "ep-conflicted-name@provider" pattern.
|
||||
func resolveHTTPTLSOptions(routers map[string]*dynamic.Router) map[string]*dynamic.Router {
|
||||
if len(routers) == 0 {
|
||||
return routers
|
||||
}
|
||||
|
||||
rts := make(map[string]*dynamic.Router)
|
||||
newRouters := make(map[string]*dynamic.Router)
|
||||
|
||||
// Keyed by domain, then by options reference.
|
||||
// The actual source of truth for what TLS options will actually be used for the connection.
|
||||
// As opposed to tlsOptionsForHost, it keeps track of all the (different) TLS
|
||||
// options that occur for a given host name, so that later on we can set relevant
|
||||
// errors and logging for all the routers concerned (i.e. wrongly configured).
|
||||
tlsOptionsForHostSNI := map[string]map[string][]string{}
|
||||
|
||||
for routerHTTPName, routerHTTPConfig := range cfg.HTTP.Routers {
|
||||
rts[routerHTTPName] = routerHTTPConfig.DeepCopy()
|
||||
|
||||
if routerHTTPConfig.TLS == nil {
|
||||
// Split every router per entryPoint.
|
||||
// Routers always have at least one entryPoint at this stage, as they are
|
||||
// defaulted in mergeConfiguration before applyModel and this resolution run.
|
||||
routersByEntryPoint := map[string]map[string]*dynamic.Router{}
|
||||
for name, router := range routers {
|
||||
if router.TLS == nil {
|
||||
newRouters[name] = router
|
||||
continue
|
||||
}
|
||||
|
||||
ctxRouter := provider.AddInContext(context.Background(), routerHTTPName)
|
||||
logger := log.Ctx(ctxRouter).With().Str(logs.RouterName, routerHTTPName).Logger()
|
||||
|
||||
tlsOptionsName := traefiktls.DefaultTLSConfigName
|
||||
if len(routerHTTPConfig.TLS.Options) > 0 && routerHTTPConfig.TLS.Options != traefiktls.DefaultTLSConfigName {
|
||||
tlsOptionsName = provider.GetQualifiedName(ctxRouter, routerHTTPConfig.TLS.Options)
|
||||
router.TLS.ResolvedOptions = traefiktls.DefaultTLSConfigName
|
||||
if len(router.TLS.Options) > 0 && router.TLS.Options != traefiktls.DefaultTLSConfigName {
|
||||
router.TLS.ResolvedOptions = provider.GetQualifiedName(provider.AddInContext(context.Background(), name), router.TLS.Options)
|
||||
}
|
||||
|
||||
domains, err := httpmuxer.ParseDomains(routerHTTPConfig.Rule)
|
||||
for _, ep := range router.EntryPoints {
|
||||
if routersByEntryPoint[ep] == nil {
|
||||
routersByEntryPoint[ep] = map[string]*dynamic.Router{}
|
||||
}
|
||||
|
||||
routersByEntryPoint[ep][name] = router
|
||||
}
|
||||
}
|
||||
|
||||
// Resolve the TLS options independently for each entryPoint.
|
||||
conflictingRouters := make(map[string][]string, len(routersByEntryPoint))
|
||||
for ep, epRouters := range routersByEntryPoint {
|
||||
conflictingRouters[ep] = findConflictingRouters(ep, epRouters)
|
||||
}
|
||||
|
||||
for name, router := range routers {
|
||||
router.EntryPoints = slices.DeleteFunc(router.EntryPoints, func(ep string) bool {
|
||||
deleted := slices.Contains(conflictingRouters[ep], name)
|
||||
if deleted {
|
||||
rt := router.DeepCopy()
|
||||
rt.TLS.ResolvedOptions = traefiktls.DefaultTLSConfigName
|
||||
rt.EntryPoints = []string{ep}
|
||||
// The new name is not collision free but has very small possibility to collide.
|
||||
// TODO: rework this naming whenever we'll introduce a resource reference mechanism not based on a string.
|
||||
newRouters[ep+"-conflicted-"+name] = rt
|
||||
}
|
||||
|
||||
return deleted
|
||||
})
|
||||
|
||||
if len(router.EntryPoints) > 0 {
|
||||
newRouters[name] = router
|
||||
}
|
||||
}
|
||||
|
||||
return newRouters
|
||||
}
|
||||
|
||||
// findConflictingRouters returns the names of the routers, among the given
|
||||
// single-entryPoint routers, that serve a host (SNI) also served by another router
|
||||
// with a different resolved TLS option. Such routers are arbitrated by falling back
|
||||
// to the default TLS options.
|
||||
func findConflictingRouters(ep string, routers map[string]*dynamic.Router) []string {
|
||||
var conflicting []string
|
||||
|
||||
// For each host (SNI, already lower-cased by the domain parsing), the routers
|
||||
// serving it grouped by their resolved TLS option. A host with more than one
|
||||
// group is served with conflicting TLS options.
|
||||
routersByHostAndOption := map[string]map[string][]string{}
|
||||
|
||||
for name, router := range routers {
|
||||
if router.TLS == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
domains, err := httpmuxer.ParseDomains(router.Rule)
|
||||
if err != nil {
|
||||
logger.Error().Err(err).Msgf("Invalid rule %s", routerHTTPConfig.Rule)
|
||||
continue
|
||||
}
|
||||
|
||||
if len(domains) == 0 {
|
||||
rts[routerHTTPName].TLS.ResolvedOptions = "default"
|
||||
logger.Warn().Msgf("No domain found in rule %v, the TLS options applied for this router will depend on the SNI of each request", routerHTTPConfig.Rule)
|
||||
// The configured TLSOptions on a router without a domain in its rule cannot be selected when evaluating the SNI,
|
||||
// so if it is not the default one, it is a conflict.
|
||||
if len(domains) == 0 && router.TLS.ResolvedOptions != traefiktls.DefaultTLSConfigName {
|
||||
conflicting = append(conflicting, name)
|
||||
continue
|
||||
}
|
||||
|
||||
for _, domain := range domains {
|
||||
// domain is already in lower case thanks to the domain parsing
|
||||
if tlsOptionsForHostSNI[domain] == nil {
|
||||
tlsOptionsForHostSNI[domain] = make(map[string][]string)
|
||||
if routersByHostAndOption[domain] == nil {
|
||||
routersByHostAndOption[domain] = map[string][]string{}
|
||||
}
|
||||
tlsOptionsForHostSNI[domain][tlsOptionsName] = append(tlsOptionsForHostSNI[domain][tlsOptionsName], routerHTTPName)
|
||||
option := router.TLS.ResolvedOptions
|
||||
routersByHostAndOption[domain][option] = append(routersByHostAndOption[domain][option], name)
|
||||
}
|
||||
}
|
||||
|
||||
for hostSNI, tlsConfigs := range tlsOptionsForHostSNI {
|
||||
if len(tlsConfigs) == 1 {
|
||||
for optionsName, v := range tlsConfigs {
|
||||
log.Debug().Msgf("Adding route for %s with TLS options %s", hostSNI, optionsName)
|
||||
for _, s := range v {
|
||||
rts[s].TLS.ResolvedOptions = optionsName
|
||||
}
|
||||
}
|
||||
for domain, routersByOption := range routersByHostAndOption {
|
||||
if len(routersByOption) == 1 {
|
||||
continue
|
||||
}
|
||||
|
||||
// multiple tlsConfigs
|
||||
routers := make([]string, 0, len(tlsConfigs))
|
||||
for _, v := range tlsConfigs {
|
||||
for _, s := range v {
|
||||
rts[s].TLS.ResolvedOptions = traefiktls.DefaultTLSConfigName
|
||||
routers = append(routers, s)
|
||||
}
|
||||
var routersInConflict []string
|
||||
for _, names := range routersByOption {
|
||||
conflicting = append(conflicting, names...)
|
||||
routersInConflict = append(routersInConflict, names...)
|
||||
}
|
||||
|
||||
log.Warn().Msgf("Found different TLS options for routers on the same host %v, so using the default TLS options instead for these routers: %#v", hostSNI, routers)
|
||||
log.Error().Msgf("On EntryPoint %q, Host %q is served by multiple routers with different TLS options, default TLSOptions will be applied for the following routers: %v", ep, domain, routersInConflict)
|
||||
}
|
||||
|
||||
cfg.HTTP.Routers = rts
|
||||
return cfg
|
||||
return conflicting
|
||||
}
|
||||
|
||||
func applyModel(cfg dynamic.Configuration) dynamic.Configuration {
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
|
||||
"github.com/go-acme/lego/v4/challenge/tlsalpn01"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
"github.com/traefik/traefik/v3/pkg/config/dynamic"
|
||||
otypes "github.com/traefik/traefik/v3/pkg/observability/types"
|
||||
"github.com/traefik/traefik/v3/pkg/tls"
|
||||
@@ -1230,3 +1231,113 @@ func Test_applyModel(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_resolveHTTPTLSOptions(t *testing.T) {
|
||||
testCases := []struct {
|
||||
desc string
|
||||
routers map[string]*dynamic.Router
|
||||
expected map[string]string // router name -> ResolvedOptions
|
||||
unexpectedRouters []string
|
||||
}{
|
||||
{
|
||||
desc: "same host, different options, different entryPoints: no conflict",
|
||||
routers: map[string]*dynamic.Router{
|
||||
"router-a@file": {EntryPoints: []string{"ep-a"}, Rule: "Host(`example.com`)", TLS: &dynamic.RouterTLSConfig{Options: "optsA"}},
|
||||
"router-b@file": {EntryPoints: []string{"ep-b"}, Rule: "Host(`example.com`)", TLS: &dynamic.RouterTLSConfig{Options: "optsB"}},
|
||||
},
|
||||
expected: map[string]string{
|
||||
"router-a@file": "optsA@file",
|
||||
"router-b@file": "optsB@file",
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "same host, different options, same entryPoint: conflict falls back to default",
|
||||
routers: map[string]*dynamic.Router{
|
||||
"router-a@file": {EntryPoints: []string{"ep-a"}, Rule: "Host(`example.com`)", TLS: &dynamic.RouterTLSConfig{Options: "optsA"}},
|
||||
"router-b@file": {EntryPoints: []string{"ep-a"}, Rule: "Host(`example.com`)", TLS: &dynamic.RouterTLSConfig{Options: "optsB"}},
|
||||
},
|
||||
expected: map[string]string{
|
||||
"ep-a-conflicted-router-a@file": "default",
|
||||
"ep-a-conflicted-router-b@file": "default",
|
||||
},
|
||||
unexpectedRouters: []string{"router-a@file", "router-b@file"},
|
||||
},
|
||||
{
|
||||
desc: "same host, same options, same entryPoint: keeps the configured options",
|
||||
routers: map[string]*dynamic.Router{
|
||||
"router-a@file": {EntryPoints: []string{"ep-a"}, Rule: "Host(`example.com`)", TLS: &dynamic.RouterTLSConfig{Options: "optsA"}},
|
||||
"router-b@file": {EntryPoints: []string{"ep-a"}, Rule: "Host(`example.com`) && PathPrefix(`/foo`)", TLS: &dynamic.RouterTLSConfig{Options: "optsA"}},
|
||||
},
|
||||
expected: map[string]string{
|
||||
"router-a@file": "optsA@file",
|
||||
"router-b@file": "optsA@file",
|
||||
},
|
||||
},
|
||||
{
|
||||
desc: "router spanning two entryPoints, conflict on one only: router is duplicated",
|
||||
routers: map[string]*dynamic.Router{
|
||||
"shared@file": {EntryPoints: []string{"ep-a", "ep-b"}, Rule: "Host(`example.com`)", TLS: &dynamic.RouterTLSConfig{Options: "optsX"}},
|
||||
"other@file": {EntryPoints: []string{"ep-a"}, Rule: "Host(`example.com`)", TLS: &dynamic.RouterTLSConfig{Options: "optsY"}},
|
||||
},
|
||||
expected: map[string]string{
|
||||
"ep-a-conflicted-shared@file": "default", // conflicts with other@file on ep-a
|
||||
"shared@file": "optsX@file", // alone on ep-b
|
||||
"ep-a-conflicted-other@file": "default",
|
||||
},
|
||||
unexpectedRouters: []string{"other@file"},
|
||||
},
|
||||
{
|
||||
desc: "no domain in rule, non-default options: forced to default and renamed",
|
||||
routers: map[string]*dynamic.Router{
|
||||
"router-a@file": {EntryPoints: []string{"ep-a"}, Rule: "PathPrefix(`/foo`)", TLS: &dynamic.RouterTLSConfig{Options: "optsA"}},
|
||||
},
|
||||
expected: map[string]string{
|
||||
"ep-a-conflicted-router-a@file": "default",
|
||||
},
|
||||
unexpectedRouters: []string{"router-a@file"},
|
||||
},
|
||||
{
|
||||
desc: "no domain in rule, implicit default options: not conflicting, keeps its name",
|
||||
routers: map[string]*dynamic.Router{
|
||||
"router-a@file": {EntryPoints: []string{"ep-a"}, Rule: "PathPrefix(`/foo`)", TLS: &dynamic.RouterTLSConfig{}},
|
||||
},
|
||||
expected: map[string]string{
|
||||
"router-a@file": "default",
|
||||
},
|
||||
unexpectedRouters: []string{"ep-a-conflicted-router-a@file"},
|
||||
},
|
||||
{
|
||||
desc: "no domain in rule, explicit default options: not conflicting, keeps its name",
|
||||
routers: map[string]*dynamic.Router{
|
||||
"router-a@file": {EntryPoints: []string{"ep-a"}, Rule: "PathPrefix(`/foo`)", TLS: &dynamic.RouterTLSConfig{
|
||||
Options: "default",
|
||||
}},
|
||||
},
|
||||
expected: map[string]string{
|
||||
"router-a@file": "default",
|
||||
},
|
||||
unexpectedRouters: []string{"ep-a-conflicted-router-a@file"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range testCases {
|
||||
t.Run(test.desc, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
got := resolveHTTPTLSOptions(test.routers)
|
||||
|
||||
for name, want := range test.expected {
|
||||
rt, ok := got[name]
|
||||
|
||||
require.True(t, ok, "router %q is missing", name)
|
||||
require.NotNil(t, rt.TLS, "router %q has no TLS config", name)
|
||||
assert.Equal(t, want, rt.TLS.ResolvedOptions, "router %q %v", name, rt.EntryPoints)
|
||||
}
|
||||
|
||||
for _, name := range test.unexpectedRouters {
|
||||
_, ok := got[name]
|
||||
require.False(t, ok, "router %q is present", name)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -176,7 +176,9 @@ func (c *ConfigurationWatcher) applyConfigurations(ctx context.Context) {
|
||||
|
||||
conf := mergeConfiguration(newConfigs.DeepCopy(), c.defaultEntryPoints)
|
||||
conf = applyModel(conf)
|
||||
conf = resolveHTTPTLSOptions(conf)
|
||||
if conf.HTTP != nil {
|
||||
conf.HTTP.Routers = resolveHTTPTLSOptions(conf.HTTP.Routers)
|
||||
}
|
||||
|
||||
for _, listener := range c.configurationListeners {
|
||||
listener(conf)
|
||||
|
||||
@@ -173,8 +173,7 @@ func (m *Manager) buildEntryPointHandler(ctx context.Context, configs map[string
|
||||
}
|
||||
|
||||
if len(domains) > 0 && routerHTTPConfig.TLS.ResolvedOptions != tlsOptionsName {
|
||||
logger.Warn().Msg("Found different TLS options for routers on the same host, so using the default TLS options instead.")
|
||||
routerHTTPConfig.AddError(errors.New("found different TLS options for routers on the same host, so using the default TLS options instead"), false)
|
||||
routerHTTPConfig.AddError(errors.New("router's TLSOptions configuration is conflicting with other routers on the same entrypoint and host, default TLS options will be used instead"), false)
|
||||
}
|
||||
|
||||
// Even though the error is seemingly ignored (aside from logging it),
|
||||
|
||||
@@ -643,6 +643,7 @@ func newHTTPServer(ctx context.Context, ln net.Listener, configuration *static.E
|
||||
configuration.ForwardedHeaders.TrustedIPs,
|
||||
configuration.ForwardedHeaders.Connection,
|
||||
configuration.ForwardedHeaders.NotAppendXForwardedFor,
|
||||
configuration.ForwardedHeaders.AddXForwardedSchemeHeaders,
|
||||
next)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -171,7 +171,7 @@ func (t *TransportManager) createTLSConfig(cfg *dynamic.ServersTransport) (*tls.
|
||||
config = tlsconfig.MTLSClientConfig(t.spiffeX509Source, t.spiffeX509Source, spiffeAuthorizer)
|
||||
}
|
||||
|
||||
if cfg.InsecureSkipVerify || len(cfg.RootCAs) > 0 || len(cfg.ServerName) > 0 || len(cfg.Certificates) > 0 || cfg.PeerCertURI != "" || len(cfg.CipherSuites) > 0 || cfg.MaxVersion != "" || cfg.MinVersion != "" {
|
||||
if cfg.InsecureSkipVerify || len(cfg.RootCAs) > 0 || len(cfg.ServerName) > 0 || len(cfg.Certificates) > 0 || cfg.PeerCertURI != "" || len(cfg.PeerCertSANs) > 0 || len(cfg.CipherSuites) > 0 || cfg.MaxVersion != "" || cfg.MinVersion != "" {
|
||||
if config != nil {
|
||||
return nil, errors.New("TLS and SPIFFE configuration cannot be defined at the same time")
|
||||
}
|
||||
@@ -223,9 +223,20 @@ func (t *TransportManager) createTLSConfig(cfg *dynamic.ServersTransport) (*tls.
|
||||
MaxVersion: maxVersion,
|
||||
}
|
||||
|
||||
peerCertSANs := make([]traefiktls.SAN, len(cfg.PeerCertSANs))
|
||||
copy(peerCertSANs, cfg.PeerCertSANs)
|
||||
|
||||
if cfg.PeerCertURI != "" {
|
||||
log.Warn().Msg("PeerCertURI option is deprecated, please use PeerCertSANs instead")
|
||||
peerCertSANs = append(peerCertSANs, traefiktls.SAN{
|
||||
Type: traefiktls.SANURIType,
|
||||
Value: cfg.PeerCertURI,
|
||||
})
|
||||
}
|
||||
|
||||
if len(peerCertSANs) > 0 {
|
||||
config.VerifyPeerCertificate = func(rawCerts [][]byte, _ [][]*x509.Certificate) error {
|
||||
return traefiktls.VerifyPeerCertificate(cfg.PeerCertURI, config, rawCerts)
|
||||
return traefiktls.VerifyPeerCertificate(peerCertSANs, config.RootCAs, rawCerts)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,11 +26,11 @@ import (
|
||||
"github.com/traefik/traefik/v3/pkg/types"
|
||||
)
|
||||
|
||||
// LocalhostCert is a PEM-encoded TLS cert
|
||||
// localhostCert is a PEM-encoded TLS cert
|
||||
// for host example.com, www.example.com
|
||||
// expiring at Jan 29 16:00:00 2084 GMT.
|
||||
// go run $GOROOT/src/crypto/tls/generate_cert.go --rsa-bits 1024 --host example.com,www.example.com --ca --start-date "Jan 1 00:00:00 1970" --duration=1000000h
|
||||
var LocalhostCert = []byte(`-----BEGIN CERTIFICATE-----
|
||||
var localhostCert = []byte(`-----BEGIN CERTIFICATE-----
|
||||
MIICDDCCAXWgAwIBAgIQH20JmcOlcRWHNuf62SYwszANBgkqhkiG9w0BAQsFADAS
|
||||
MRAwDgYDVQQKEwdBY21lIENvMCAXDTcwMDEwMTAwMDAwMFoYDzIwODQwMTI5MTYw
|
||||
MDAwWjASMRAwDgYDVQQKEwdBY21lIENvMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCB
|
||||
@@ -44,8 +44,8 @@ we7mX8lv763u0XuCWPxbHszhclI6FFjoQef0Z1NYLRm8ZRq58QqWDFZ3E6wdDK+B
|
||||
+OWvkW+hRavo6R9LzIZPfbv8yBo4M9PK/DXw8hLqH7VkkI+Gh793iH7Ugd4A7wvT
|
||||
-----END CERTIFICATE-----`)
|
||||
|
||||
// LocalhostKey is the private key for localhostCert.
|
||||
var LocalhostKey = []byte(`-----BEGIN PRIVATE KEY-----
|
||||
// localhostKey is the private key for localhostCert.
|
||||
var localhostKey = []byte(`-----BEGIN PRIVATE KEY-----
|
||||
MIICdwIBADANBgkqhkiG9w0BAQEFAASCAmEwggJdAgEAAoGBALSog3LcXiirq+IO
|
||||
eWkMMTknTyJJEaCCDoTKUkoEpl+mEQba5+ArvwO5+Xf7tvQuURjYB5kfC+Ic4OoL
|
||||
1rqKGPVlhCTT92MCH4546EURa771P9U/yBUVqsWpPwM6nL6GsbtgmK9QjPBvh+Yl
|
||||
@@ -130,7 +130,7 @@ func TestKeepConnectionWhenSameConfiguration(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
cert, err := tls.X509KeyPair(LocalhostCert, LocalhostKey)
|
||||
cert, err := tls.X509KeyPair(localhostCert, localhostKey)
|
||||
require.NoError(t, err)
|
||||
|
||||
srv.TLS = &tls.Config{Certificates: []tls.Certificate{cert}}
|
||||
@@ -141,7 +141,7 @@ func TestKeepConnectionWhenSameConfiguration(t *testing.T) {
|
||||
dynamicConf := map[string]*dynamic.ServersTransport{
|
||||
"test": {
|
||||
ServerName: "example.com",
|
||||
RootCAs: []types.FileOrContent{types.FileOrContent(LocalhostCert)},
|
||||
RootCAs: []types.FileOrContent{types.FileOrContent(localhostCert)},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -164,7 +164,7 @@ func TestKeepConnectionWhenSameConfiguration(t *testing.T) {
|
||||
dynamicConf = map[string]*dynamic.ServersTransport{
|
||||
"test": {
|
||||
ServerName: "www.example.com",
|
||||
RootCAs: []types.FileOrContent{types.FileOrContent(LocalhostCert)},
|
||||
RootCAs: []types.FileOrContent{types.FileOrContent(localhostCert)},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -188,7 +188,7 @@ func TestValidCipherSuites(t *testing.T) {
|
||||
rw.WriteHeader(http.StatusOK)
|
||||
}))
|
||||
|
||||
cert, err := tls.X509KeyPair(LocalhostCert, LocalhostKey)
|
||||
cert, err := tls.X509KeyPair(localhostCert, localhostKey)
|
||||
require.NoError(t, err)
|
||||
|
||||
srv.TLS = &tls.Config{
|
||||
@@ -204,7 +204,7 @@ func TestValidCipherSuites(t *testing.T) {
|
||||
dynamicConf := map[string]*dynamic.ServersTransport{
|
||||
"test": {
|
||||
ServerName: "example.com",
|
||||
RootCAs: []types.FileOrContent{types.FileOrContent(LocalhostCert)},
|
||||
RootCAs: []types.FileOrContent{types.FileOrContent(localhostCert)},
|
||||
CipherSuites: []string{"TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384"},
|
||||
},
|
||||
}
|
||||
@@ -225,7 +225,7 @@ func TestValidTLSVersions(t *testing.T) {
|
||||
rw.WriteHeader(http.StatusOK)
|
||||
}))
|
||||
|
||||
cert, err := tls.X509KeyPair(LocalhostCert, LocalhostKey)
|
||||
cert, err := tls.X509KeyPair(localhostCert, localhostKey)
|
||||
require.NoError(t, err)
|
||||
|
||||
srv.TLS = &tls.Config{
|
||||
@@ -243,7 +243,7 @@ func TestValidTLSVersions(t *testing.T) {
|
||||
dynamicConf := map[string]*dynamic.ServersTransport{
|
||||
"test": {
|
||||
ServerName: "example.com",
|
||||
RootCAs: []types.FileOrContent{types.FileOrContent(LocalhostCert)},
|
||||
RootCAs: []types.FileOrContent{types.FileOrContent(localhostCert)},
|
||||
MaxVersion: "VersionTLS12",
|
||||
MinVersion: "VersionTLS11",
|
||||
CipherSuites: []string{"TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384"},
|
||||
@@ -270,7 +270,7 @@ func TestInvalidTLSConfig(t *testing.T) {
|
||||
desc: "invalid CipherSuite name",
|
||||
transport: &dynamic.ServersTransport{
|
||||
ServerName: "example.com",
|
||||
RootCAs: []types.FileOrContent{types.FileOrContent(LocalhostCert)},
|
||||
RootCAs: []types.FileOrContent{types.FileOrContent(localhostCert)},
|
||||
CipherSuites: []string{"TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA385"},
|
||||
},
|
||||
},
|
||||
@@ -278,7 +278,7 @@ func TestInvalidTLSConfig(t *testing.T) {
|
||||
desc: "invalid MinVersion",
|
||||
transport: &dynamic.ServersTransport{
|
||||
ServerName: "example.com",
|
||||
RootCAs: []types.FileOrContent{types.FileOrContent(LocalhostCert)},
|
||||
RootCAs: []types.FileOrContent{types.FileOrContent(localhostCert)},
|
||||
MinVersion: "VersionTLS09",
|
||||
},
|
||||
},
|
||||
@@ -286,7 +286,7 @@ func TestInvalidTLSConfig(t *testing.T) {
|
||||
desc: "invalid MaxVersion",
|
||||
transport: &dynamic.ServersTransport{
|
||||
ServerName: "example.com",
|
||||
RootCAs: []types.FileOrContent{types.FileOrContent(LocalhostCert)},
|
||||
RootCAs: []types.FileOrContent{types.FileOrContent(localhostCert)},
|
||||
MaxVersion: "VersionTLS16",
|
||||
},
|
||||
},
|
||||
@@ -294,7 +294,7 @@ func TestInvalidTLSConfig(t *testing.T) {
|
||||
desc: "MinVersion above MaxVersion",
|
||||
transport: &dynamic.ServersTransport{
|
||||
ServerName: "example.com",
|
||||
RootCAs: []types.FileOrContent{types.FileOrContent(LocalhostCert)},
|
||||
RootCAs: []types.FileOrContent{types.FileOrContent(localhostCert)},
|
||||
MinVersion: "VersionTLS13",
|
||||
MaxVersion: "VersionTLS12",
|
||||
},
|
||||
@@ -325,7 +325,7 @@ func TestNoCipherSuitesUsesDefaults(t *testing.T) {
|
||||
desc: "no cipher config with ServerName and RootCAs",
|
||||
transport: &dynamic.ServersTransport{
|
||||
ServerName: "example.com",
|
||||
RootCAs: []types.FileOrContent{types.FileOrContent(LocalhostCert)},
|
||||
RootCAs: []types.FileOrContent{types.FileOrContent(localhostCert)},
|
||||
},
|
||||
serverCipher: tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
|
||||
},
|
||||
@@ -338,7 +338,7 @@ func TestNoCipherSuitesUsesDefaults(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
cert, err := tls.X509KeyPair(LocalhostCert, LocalhostKey)
|
||||
cert, err := tls.X509KeyPair(localhostCert, localhostKey)
|
||||
require.NoError(t, err)
|
||||
|
||||
for _, test := range testCases {
|
||||
@@ -375,7 +375,7 @@ func TestMTLS(t *testing.T) {
|
||||
rw.WriteHeader(http.StatusOK)
|
||||
}))
|
||||
|
||||
cert, err := tls.X509KeyPair(LocalhostCert, LocalhostKey)
|
||||
cert, err := tls.X509KeyPair(localhostCert, localhostKey)
|
||||
require.NoError(t, err)
|
||||
|
||||
clientPool := x509.NewCertPool()
|
||||
@@ -397,7 +397,7 @@ func TestMTLS(t *testing.T) {
|
||||
"test": {
|
||||
ServerName: "example.com",
|
||||
// For TLS
|
||||
RootCAs: []types.FileOrContent{types.FileOrContent(LocalhostCert)},
|
||||
RootCAs: []types.FileOrContent{types.FileOrContent(localhostCert)},
|
||||
|
||||
// For mTLS
|
||||
Certificates: traefiktls.Certificates{
|
||||
@@ -629,6 +629,155 @@ func TestDisableHTTP2(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestKerberosRoundTripper(t *testing.T) {
|
||||
testCases := []struct {
|
||||
desc string
|
||||
|
||||
originalRoundTripperHeaders map[string][]string
|
||||
|
||||
expectedStatusCode []int
|
||||
expectedDedicatedCount int
|
||||
expectedOriginalCount int
|
||||
}{
|
||||
{
|
||||
desc: "without special header",
|
||||
expectedStatusCode: []int{http.StatusUnauthorized, http.StatusUnauthorized, http.StatusUnauthorized},
|
||||
expectedOriginalCount: 3,
|
||||
},
|
||||
{
|
||||
desc: "with Negotiate (Kerberos)",
|
||||
originalRoundTripperHeaders: map[string][]string{"Www-Authenticate": {"Negotiate"}},
|
||||
expectedStatusCode: []int{http.StatusUnauthorized, http.StatusOK, http.StatusOK},
|
||||
expectedOriginalCount: 1,
|
||||
expectedDedicatedCount: 2,
|
||||
},
|
||||
{
|
||||
desc: "with NTLM",
|
||||
originalRoundTripperHeaders: map[string][]string{"Www-Authenticate": {"NTLM"}},
|
||||
expectedStatusCode: []int{http.StatusUnauthorized, http.StatusOK, http.StatusOK},
|
||||
expectedOriginalCount: 1,
|
||||
expectedDedicatedCount: 2,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range testCases {
|
||||
t.Run(test.desc, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
origCount := 0
|
||||
dedicatedCount := 0
|
||||
rt := kerberosRoundTripper{
|
||||
new: func() http.RoundTripper {
|
||||
return roundTripperFn(func(req *http.Request) (*http.Response, error) {
|
||||
dedicatedCount++
|
||||
return &http.Response{
|
||||
StatusCode: http.StatusOK,
|
||||
}, nil
|
||||
})
|
||||
},
|
||||
OriginalRoundTripper: roundTripperFn(func(req *http.Request) (*http.Response, error) {
|
||||
origCount++
|
||||
return &http.Response{
|
||||
StatusCode: http.StatusUnauthorized,
|
||||
Header: test.originalRoundTripperHeaders,
|
||||
}, nil
|
||||
}),
|
||||
}
|
||||
|
||||
ctx := AddTransportOnContext(t.Context())
|
||||
for _, expected := range test.expectedStatusCode {
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, "http://127.0.0.1", http.NoBody)
|
||||
require.NoError(t, err)
|
||||
resp, err := rt.RoundTrip(req)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, expected, resp.StatusCode)
|
||||
}
|
||||
|
||||
require.Equal(t, test.expectedOriginalCount, origCount)
|
||||
require.Equal(t, test.expectedDedicatedCount, dedicatedCount)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPeerCertSANs(t *testing.T) {
|
||||
testCases := []struct {
|
||||
desc string
|
||||
serversTransport *dynamic.ServersTransport
|
||||
wantError bool
|
||||
}{
|
||||
{
|
||||
desc: "matches cert SAN",
|
||||
serversTransport: &dynamic.ServersTransport{
|
||||
ServerName: "example.com",
|
||||
RootCAs: []types.FileOrContent{types.FileOrContent(localhostCert)},
|
||||
PeerCertSANs: []traefiktls.SAN{
|
||||
{
|
||||
Type: traefiktls.SANDNSNameType,
|
||||
Value: "www.example.com",
|
||||
},
|
||||
},
|
||||
},
|
||||
wantError: false,
|
||||
},
|
||||
{
|
||||
desc: "does not match cert SAN",
|
||||
serversTransport: &dynamic.ServersTransport{
|
||||
ServerName: "example.com",
|
||||
RootCAs: []types.FileOrContent{types.FileOrContent(localhostCert)},
|
||||
PeerCertSANs: []traefiktls.SAN{
|
||||
{
|
||||
Type: traefiktls.SANDNSNameType,
|
||||
Value: "wrong.example.com",
|
||||
},
|
||||
},
|
||||
},
|
||||
wantError: true,
|
||||
},
|
||||
{
|
||||
desc: "does not match deprecated PeerCertURI",
|
||||
serversTransport: &dynamic.ServersTransport{
|
||||
ServerName: "example.com",
|
||||
RootCAs: []types.FileOrContent{types.FileOrContent(localhostCert)},
|
||||
PeerCertURI: "foo://bar",
|
||||
},
|
||||
wantError: true,
|
||||
},
|
||||
}
|
||||
|
||||
// LocalhostCert is valid for "example.com" — this is the cert the test server serves.
|
||||
// We set ServerName to "other.example.com" to prove SNI and cert auth are decoupled.
|
||||
cert, err := tls.X509KeyPair(localhostCert, localhostKey)
|
||||
require.NoError(t, err)
|
||||
|
||||
for _, test := range testCases {
|
||||
t.Run(test.desc, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
srv := httptest.NewUnstartedServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {}))
|
||||
srv.TLS = &tls.Config{Certificates: []tls.Certificate{cert}}
|
||||
srv.StartTLS()
|
||||
t.Cleanup(srv.Close)
|
||||
|
||||
transportManager := NewTransportManager(nil)
|
||||
transportManager.Update(map[string]*dynamic.ServersTransport{
|
||||
"test": test.serversTransport,
|
||||
})
|
||||
|
||||
tr, err := transportManager.GetRoundTripper("test")
|
||||
require.NoError(t, err)
|
||||
|
||||
client := http.Client{Transport: tr}
|
||||
|
||||
_, err = client.Get(srv.URL)
|
||||
if test.wantError {
|
||||
require.Error(t, err)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// fakeSpiffePKI simulates a SPIFFE aware PKI and allows generating multiple valid SVIDs.
|
||||
type fakeSpiffePKI struct {
|
||||
caPrivateKey *rsa.PrivateKey
|
||||
@@ -744,73 +893,3 @@ type roundTripperFn func(req *http.Request) (*http.Response, error)
|
||||
func (r roundTripperFn) RoundTrip(request *http.Request) (*http.Response, error) {
|
||||
return r(request)
|
||||
}
|
||||
|
||||
func TestKerberosRoundTripper(t *testing.T) {
|
||||
testCases := []struct {
|
||||
desc string
|
||||
|
||||
originalRoundTripperHeaders map[string][]string
|
||||
|
||||
expectedStatusCode []int
|
||||
expectedDedicatedCount int
|
||||
expectedOriginalCount int
|
||||
}{
|
||||
{
|
||||
desc: "without special header",
|
||||
expectedStatusCode: []int{http.StatusUnauthorized, http.StatusUnauthorized, http.StatusUnauthorized},
|
||||
expectedOriginalCount: 3,
|
||||
},
|
||||
{
|
||||
desc: "with Negotiate (Kerberos)",
|
||||
originalRoundTripperHeaders: map[string][]string{"Www-Authenticate": {"Negotiate"}},
|
||||
expectedStatusCode: []int{http.StatusUnauthorized, http.StatusOK, http.StatusOK},
|
||||
expectedOriginalCount: 1,
|
||||
expectedDedicatedCount: 2,
|
||||
},
|
||||
{
|
||||
desc: "with NTLM",
|
||||
originalRoundTripperHeaders: map[string][]string{"Www-Authenticate": {"NTLM"}},
|
||||
expectedStatusCode: []int{http.StatusUnauthorized, http.StatusOK, http.StatusOK},
|
||||
expectedOriginalCount: 1,
|
||||
expectedDedicatedCount: 2,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range testCases {
|
||||
t.Run(test.desc, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
origCount := 0
|
||||
dedicatedCount := 0
|
||||
rt := kerberosRoundTripper{
|
||||
new: func() http.RoundTripper {
|
||||
return roundTripperFn(func(req *http.Request) (*http.Response, error) {
|
||||
dedicatedCount++
|
||||
return &http.Response{
|
||||
StatusCode: http.StatusOK,
|
||||
}, nil
|
||||
})
|
||||
},
|
||||
OriginalRoundTripper: roundTripperFn(func(req *http.Request) (*http.Response, error) {
|
||||
origCount++
|
||||
return &http.Response{
|
||||
StatusCode: http.StatusUnauthorized,
|
||||
Header: test.originalRoundTripperHeaders,
|
||||
}, nil
|
||||
}),
|
||||
}
|
||||
|
||||
ctx := AddTransportOnContext(t.Context())
|
||||
for _, expected := range test.expectedStatusCode {
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, "http://127.0.0.1", http.NoBody)
|
||||
require.NoError(t, err)
|
||||
resp, err := rt.RoundTrip(req)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, expected, resp.StatusCode)
|
||||
}
|
||||
|
||||
require.Equal(t, test.expectedOriginalCount, origCount)
|
||||
require.Equal(t, test.expectedDedicatedCount, dedicatedCount)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
+13
-2
@@ -175,7 +175,7 @@ func (d *DialerManager) Build(config *dynamic.TCPServersLoadBalancer, isTLS bool
|
||||
tlsConfig = tlsconfig.MTLSClientConfig(d.spiffeX509Source, d.spiffeX509Source, authorizer)
|
||||
}
|
||||
|
||||
if st.TLS.InsecureSkipVerify || len(st.TLS.RootCAs) > 0 || len(st.TLS.ServerName) > 0 || len(st.TLS.Certificates) > 0 || st.TLS.PeerCertURI != "" {
|
||||
if st.TLS.InsecureSkipVerify || len(st.TLS.RootCAs) > 0 || len(st.TLS.ServerName) > 0 || len(st.TLS.Certificates) > 0 || st.TLS.PeerCertURI != "" || len(st.TLS.PeerCertSANs) > 0 {
|
||||
if tlsConfig != nil {
|
||||
return nil, errors.New("TLS and SPIFFE configuration cannot be defined at the same time")
|
||||
}
|
||||
@@ -187,9 +187,20 @@ func (d *DialerManager) Build(config *dynamic.TCPServersLoadBalancer, isTLS bool
|
||||
Certificates: st.TLS.Certificates.GetCertificates(),
|
||||
}
|
||||
|
||||
peerCertSANs := make([]traefiktls.SAN, len(st.TLS.PeerCertSANs))
|
||||
copy(peerCertSANs, st.TLS.PeerCertSANs)
|
||||
|
||||
if st.TLS.PeerCertURI != "" {
|
||||
log.Warn().Msg("PeerCertURI option is deprecated, please use PeerCertSANs instead")
|
||||
peerCertSANs = append(peerCertSANs, traefiktls.SAN{
|
||||
Type: traefiktls.SANURIType,
|
||||
Value: st.TLS.PeerCertURI,
|
||||
})
|
||||
}
|
||||
|
||||
if len(peerCertSANs) > 0 {
|
||||
tlsConfig.VerifyPeerCertificate = func(rawCerts [][]byte, _ [][]*x509.Certificate) error {
|
||||
return traefiktls.VerifyPeerCertificate(st.TLS.PeerCertURI, tlsConfig, rawCerts)
|
||||
return traefiktls.VerifyPeerCertificate(peerCertSANs, tlsConfig.RootCAs, rawCerts)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+96
-12
@@ -11,6 +11,8 @@ import (
|
||||
"io"
|
||||
"math/big"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"testing"
|
||||
"time"
|
||||
@@ -27,11 +29,11 @@ import (
|
||||
"github.com/traefik/traefik/v3/pkg/types"
|
||||
)
|
||||
|
||||
// LocalhostCert is a PEM-encoded TLS cert
|
||||
// localhostCert is a PEM-encoded TLS cert
|
||||
// for host example.com, www.example.com
|
||||
// expiring at Jan 29 16:00:00 2084 GMT.
|
||||
// go run $GOROOT/src/crypto/tls/generate_cert.go --rsa-bits 1024 --host example.com,www.example.com --ca --start-date "Jan 1 00:00:00 1970" --duration=1000000h
|
||||
var LocalhostCert = []byte(`-----BEGIN CERTIFICATE-----
|
||||
var localhostCert = []byte(`-----BEGIN CERTIFICATE-----
|
||||
MIICDDCCAXWgAwIBAgIQH20JmcOlcRWHNuf62SYwszANBgkqhkiG9w0BAQsFADAS
|
||||
MRAwDgYDVQQKEwdBY21lIENvMCAXDTcwMDEwMTAwMDAwMFoYDzIwODQwMTI5MTYw
|
||||
MDAwWjASMRAwDgYDVQQKEwdBY21lIENvMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCB
|
||||
@@ -45,8 +47,8 @@ we7mX8lv763u0XuCWPxbHszhclI6FFjoQef0Z1NYLRm8ZRq58QqWDFZ3E6wdDK+B
|
||||
+OWvkW+hRavo6R9LzIZPfbv8yBo4M9PK/DXw8hLqH7VkkI+Gh793iH7Ugd4A7wvT
|
||||
-----END CERTIFICATE-----`)
|
||||
|
||||
// LocalhostKey is the private key for localhostCert.
|
||||
var LocalhostKey = []byte(`-----BEGIN PRIVATE KEY-----
|
||||
// localhostKey is the private key for localhostCert.
|
||||
var localhostKey = []byte(`-----BEGIN PRIVATE KEY-----
|
||||
MIICdwIBADANBgkqhkiG9w0BAQEFAASCAmEwggJdAgEAAoGBALSog3LcXiirq+IO
|
||||
eWkMMTknTyJJEaCCDoTKUkoEpl+mEQba5+ArvwO5+Xf7tvQuURjYB5kfC+Ic4OoL
|
||||
1rqKGPVlhCTT92MCH4546EURa771P9U/yBUVqsWpPwM6nL6GsbtgmK9QjPBvh+Yl
|
||||
@@ -178,7 +180,7 @@ func TestNoTLS(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestTLS(t *testing.T) {
|
||||
cert, err := tls.X509KeyPair(LocalhostCert, LocalhostKey)
|
||||
cert, err := tls.X509KeyPair(localhostCert, localhostKey)
|
||||
require.NoError(t, err)
|
||||
|
||||
backendListener, err := net.Listen("tcp", ":0")
|
||||
@@ -199,7 +201,7 @@ func TestTLS(t *testing.T) {
|
||||
"test": {
|
||||
TLS: &dynamic.TLSClientConfig{
|
||||
ServerName: "example.com",
|
||||
RootCAs: []types.FileOrContent{types.FileOrContent(LocalhostCert)},
|
||||
RootCAs: []types.FileOrContent{types.FileOrContent(localhostCert)},
|
||||
},
|
||||
},
|
||||
}
|
||||
@@ -228,7 +230,7 @@ func TestTLS(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestTLSWithInsecureSkipVerify(t *testing.T) {
|
||||
cert, err := tls.X509KeyPair(LocalhostCert, LocalhostKey)
|
||||
cert, err := tls.X509KeyPair(localhostCert, localhostKey)
|
||||
require.NoError(t, err)
|
||||
|
||||
backendListener, err := net.Listen("tcp", ":0")
|
||||
@@ -249,7 +251,7 @@ func TestTLSWithInsecureSkipVerify(t *testing.T) {
|
||||
"test": {
|
||||
TLS: &dynamic.TLSClientConfig{
|
||||
ServerName: "bad-domain.com",
|
||||
RootCAs: []types.FileOrContent{types.FileOrContent(LocalhostCert)},
|
||||
RootCAs: []types.FileOrContent{types.FileOrContent(localhostCert)},
|
||||
InsecureSkipVerify: true,
|
||||
},
|
||||
},
|
||||
@@ -279,7 +281,7 @@ func TestTLSWithInsecureSkipVerify(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestMTLS(t *testing.T) {
|
||||
cert, err := tls.X509KeyPair(LocalhostCert, LocalhostKey)
|
||||
cert, err := tls.X509KeyPair(localhostCert, localhostKey)
|
||||
require.NoError(t, err)
|
||||
|
||||
clientPool := x509.NewCertPool()
|
||||
@@ -311,7 +313,7 @@ func TestMTLS(t *testing.T) {
|
||||
TLS: &dynamic.TLSClientConfig{
|
||||
ServerName: "example.com",
|
||||
// For TLS
|
||||
RootCAs: []types.FileOrContent{types.FileOrContent(LocalhostCert)},
|
||||
RootCAs: []types.FileOrContent{types.FileOrContent(localhostCert)},
|
||||
|
||||
// For mTLS
|
||||
Certificates: traefiktls.Certificates{
|
||||
@@ -595,7 +597,7 @@ func TestProxyProtocolWithTLS(t *testing.T) {
|
||||
|
||||
for _, test := range testCases {
|
||||
t.Run(test.desc, func(t *testing.T) {
|
||||
cert, err := tls.X509KeyPair(LocalhostCert, LocalhostKey)
|
||||
cert, err := tls.X509KeyPair(localhostCert, localhostKey)
|
||||
require.NoError(t, err)
|
||||
|
||||
backendListener, err := net.Listen("tcp", ":0")
|
||||
@@ -651,7 +653,7 @@ func TestProxyProtocolWithTLS(t *testing.T) {
|
||||
"test": {
|
||||
TLS: &dynamic.TLSClientConfig{
|
||||
ServerName: "example.com",
|
||||
RootCAs: []types.FileOrContent{types.FileOrContent(LocalhostCert)},
|
||||
RootCAs: []types.FileOrContent{types.FileOrContent(localhostCert)},
|
||||
InsecureSkipVerify: true,
|
||||
},
|
||||
ProxyProtocol: &dynamic.ProxyProtocol{
|
||||
@@ -741,6 +743,88 @@ func TestProxyProtocolDisabled(t *testing.T) {
|
||||
assert.Equal(t, "PONG", string(buf[:4]))
|
||||
}
|
||||
|
||||
func TestPeerCertSANs(t *testing.T) {
|
||||
testCases := []struct {
|
||||
desc string
|
||||
serversTransport *dynamic.TCPServersTransport
|
||||
wantError bool
|
||||
}{
|
||||
{
|
||||
desc: "matches cert SAN",
|
||||
serversTransport: &dynamic.TCPServersTransport{
|
||||
TLS: &dynamic.TLSClientConfig{
|
||||
ServerName: "example.com",
|
||||
RootCAs: []types.FileOrContent{types.FileOrContent(localhostCert)},
|
||||
PeerCertSANs: []traefiktls.SAN{
|
||||
{
|
||||
Type: traefiktls.SANDNSNameType,
|
||||
Value: "www.example.com",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
wantError: false,
|
||||
},
|
||||
{
|
||||
desc: "does not match cert SAN",
|
||||
serversTransport: &dynamic.TCPServersTransport{
|
||||
TLS: &dynamic.TLSClientConfig{
|
||||
ServerName: "example.com",
|
||||
RootCAs: []types.FileOrContent{types.FileOrContent(localhostCert)},
|
||||
PeerCertSANs: []traefiktls.SAN{
|
||||
{
|
||||
Type: traefiktls.SANDNSNameType,
|
||||
Value: "wrong.example.com",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
wantError: true,
|
||||
},
|
||||
{
|
||||
desc: "does not match deprecated PeerCertURI",
|
||||
serversTransport: &dynamic.TCPServersTransport{
|
||||
TLS: &dynamic.TLSClientConfig{
|
||||
ServerName: "example.com",
|
||||
RootCAs: []types.FileOrContent{types.FileOrContent(localhostCert)},
|
||||
PeerCertURI: "foo://bar",
|
||||
},
|
||||
},
|
||||
wantError: true,
|
||||
},
|
||||
}
|
||||
|
||||
cert, err := tls.X509KeyPair(localhostCert, localhostKey)
|
||||
require.NoError(t, err)
|
||||
|
||||
for _, test := range testCases {
|
||||
t.Run(test.desc, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
srv := httptest.NewUnstartedServer(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {}))
|
||||
srv.TLS = &tls.Config{Certificates: []tls.Certificate{cert}}
|
||||
srv.StartTLS()
|
||||
t.Cleanup(srv.Close)
|
||||
|
||||
dialerManager := NewDialerManager(nil)
|
||||
dialerManager.Update(map[string]*dynamic.TCPServersTransport{
|
||||
"test": test.serversTransport,
|
||||
})
|
||||
|
||||
dialer, err := dialerManager.Build(&dynamic.TCPServersLoadBalancer{ServersTransport: "test"}, true)
|
||||
require.NoError(t, err)
|
||||
|
||||
conn, err := dialer.Dial("tcp", srv.Listener.Addr().String(), nil)
|
||||
if test.wantError {
|
||||
require.Error(t, err)
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
_ = conn.Close()
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
type fakeClientConn struct {
|
||||
remoteAddr *net.TCPAddr
|
||||
localAddr *net.TCPAddr
|
||||
|
||||
+37
-19
@@ -5,7 +5,9 @@ import (
|
||||
"crypto/x509"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"os"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
@@ -144,34 +146,50 @@ func (f FileOrContent) Read() ([]byte, error) {
|
||||
return content, nil
|
||||
}
|
||||
|
||||
// VerifyPeerCertificate verifies the chain certificates and their URI.
|
||||
func VerifyPeerCertificate(uri string, cfg *tls.Config, rawCerts [][]byte) error {
|
||||
// TODO: Refactor to avoid useless verifyChain (ex: when insecureskipverify is false)
|
||||
cert, err := verifyChain(cfg.RootCAs, rawCerts)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// SANType is the type of the Subject Alternative Name.
|
||||
type SANType string
|
||||
|
||||
if len(uri) > 0 {
|
||||
return verifyServerCertMatchesURI(uri, cert)
|
||||
}
|
||||
const (
|
||||
// SANDNSNameType specifies hostname-based SAN.
|
||||
SANDNSNameType SANType = "DNSName"
|
||||
|
||||
return nil
|
||||
// SANURIType specifies URI-based SAN, e.g. SPIFFE id.
|
||||
SANURIType SANType = "URI"
|
||||
)
|
||||
|
||||
// +k8s:deepcopy-gen=true
|
||||
|
||||
// SAN represents a Subject Alternative Name.
|
||||
type SAN struct {
|
||||
Type SANType `json:"type,omitempty" toml:"type,omitempty" yaml:"type,omitempty"`
|
||||
Value string `json:"value,omitempty" toml:"value,omitempty" yaml:"value,omitempty"`
|
||||
}
|
||||
|
||||
// verifyServerCertMatchesURI verifies that the given certificate contains the specified URI in its SANs.
|
||||
func verifyServerCertMatchesURI(uri string, cert *x509.Certificate) error {
|
||||
if cert == nil {
|
||||
return errors.New("peer certificate mismatch: no peer certificate presented")
|
||||
// VerifyPeerCertificate verifies the chain certificates and their URI.
|
||||
func VerifyPeerCertificate(sans []SAN, rootCAs *x509.CertPool, rawCerts [][]byte) error {
|
||||
// TODO: Refactor to avoid useless verifyChain (ex: when insecureskipverify is false)
|
||||
cert, err := verifyChain(rootCAs, rawCerts)
|
||||
if err != nil {
|
||||
return fmt.Errorf("verifying chain: %w", err)
|
||||
}
|
||||
|
||||
for _, certURI := range cert.URIs {
|
||||
if strings.EqualFold(certURI.String(), uri) {
|
||||
return nil
|
||||
for _, san := range sans {
|
||||
switch san.Type {
|
||||
case SANURIType:
|
||||
if slices.ContainsFunc(cert.URIs, func(uri *url.URL) bool {
|
||||
return strings.EqualFold(san.Value, uri.String())
|
||||
}) {
|
||||
return nil
|
||||
}
|
||||
|
||||
case SANDNSNameType:
|
||||
if err := cert.VerifyHostname(san.Value); err == nil {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return fmt.Errorf("peer certificate mismatch: no SAN URI in peer certificate matches %s", uri)
|
||||
return errors.New("no matching SAN in peer certificate")
|
||||
}
|
||||
|
||||
// verifyChain performs standard TLS verification without enforcing remote hostname matching.
|
||||
|
||||
+173
-26
@@ -1,55 +1,145 @@
|
||||
package tls
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"crypto/rsa"
|
||||
"crypto/x509"
|
||||
"crypto/x509/pkix"
|
||||
"math/big"
|
||||
"net/url"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func Test_verifyServerCertMatchesURI(t *testing.T) {
|
||||
func Test_VerifyPeerCertificate(t *testing.T) {
|
||||
pki := newTestPKI(t)
|
||||
|
||||
tests := []struct {
|
||||
desc string
|
||||
uri string
|
||||
cert *x509.Certificate
|
||||
expErr require.ErrorAssertionFunc
|
||||
desc string
|
||||
sans []SAN
|
||||
rawCerts [][]byte
|
||||
rootCAs *x509.CertPool
|
||||
expErr require.ErrorAssertionFunc
|
||||
}{
|
||||
{
|
||||
desc: "returns error when certificate is nil",
|
||||
uri: "spiffe://foo.com",
|
||||
expErr: require.Error,
|
||||
desc: "returns error when no certificates are provided",
|
||||
sans: []SAN{{Type: SANURIType, Value: "spiffe://foo.com"}},
|
||||
rawCerts: nil,
|
||||
rootCAs: pki.caPool,
|
||||
expErr: require.Error,
|
||||
},
|
||||
{
|
||||
desc: "returns error when certificate has no URIs",
|
||||
uri: "spiffe://foo.com",
|
||||
cert: &x509.Certificate{URIs: nil},
|
||||
expErr: require.Error,
|
||||
desc: "returns error when certificate has no URIs",
|
||||
sans: []SAN{{Type: SANURIType, Value: "spiffe://foo.com"}},
|
||||
rawCerts: [][]byte{pki.newLeafCertDER(t, nil, nil)},
|
||||
rootCAs: pki.caPool,
|
||||
expErr: require.Error,
|
||||
},
|
||||
{
|
||||
desc: "returns error when no URI matches",
|
||||
uri: "spiffe://foo.com",
|
||||
cert: &x509.Certificate{URIs: []*url.URL{
|
||||
sans: []SAN{{Type: SANURIType, Value: "spiffe://foo.com"}},
|
||||
rawCerts: [][]byte{pki.newLeafCertDER(t, nil, []*url.URL{
|
||||
{Scheme: "spiffe", Host: "other.org"},
|
||||
}},
|
||||
expErr: require.Error,
|
||||
})},
|
||||
rootCAs: pki.caPool,
|
||||
expErr: require.Error,
|
||||
},
|
||||
{
|
||||
desc: "returns nil when URI matches",
|
||||
uri: "spiffe://foo.com",
|
||||
cert: &x509.Certificate{URIs: []*url.URL{
|
||||
sans: []SAN{{Type: SANURIType, Value: "spiffe://foo.com"}},
|
||||
rawCerts: [][]byte{pki.newLeafCertDER(t, nil, []*url.URL{
|
||||
{Scheme: "spiffe", Host: "foo.com"},
|
||||
}},
|
||||
expErr: require.NoError,
|
||||
})},
|
||||
rootCAs: pki.caPool,
|
||||
expErr: require.NoError,
|
||||
},
|
||||
{
|
||||
desc: "returns nil when one of the URI matches",
|
||||
uri: "spiffe://foo.com",
|
||||
cert: &x509.Certificate{URIs: []*url.URL{
|
||||
desc: "returns nil when one of the URIs matches",
|
||||
sans: []SAN{{Type: SANURIType, Value: "spiffe://foo.com"}},
|
||||
rawCerts: [][]byte{pki.newLeafCertDER(t, nil, []*url.URL{
|
||||
{Scheme: "spiffe", Host: "example.org"},
|
||||
{Scheme: "spiffe", Host: "foo.com"},
|
||||
}},
|
||||
expErr: require.NoError,
|
||||
})},
|
||||
rootCAs: pki.caPool,
|
||||
expErr: require.NoError,
|
||||
},
|
||||
{
|
||||
desc: "returns error when certificate has no DNS names",
|
||||
sans: []SAN{{Type: SANDNSNameType, Value: "foo.com"}},
|
||||
rawCerts: [][]byte{pki.newLeafCertDER(t, nil, nil)},
|
||||
rootCAs: pki.caPool,
|
||||
expErr: require.Error,
|
||||
},
|
||||
{
|
||||
desc: "returns error when no DNS name matches",
|
||||
sans: []SAN{{Type: SANDNSNameType, Value: "foo.com"}},
|
||||
rawCerts: [][]byte{pki.newLeafCertDER(t, []string{"other.com"}, nil)},
|
||||
rootCAs: pki.caPool,
|
||||
expErr: require.Error,
|
||||
},
|
||||
{
|
||||
desc: "returns nil when DNS name matches",
|
||||
sans: []SAN{{Type: SANDNSNameType, Value: "foo.com"}},
|
||||
rawCerts: [][]byte{pki.newLeafCertDER(t, []string{"foo.com"}, nil)},
|
||||
rootCAs: pki.caPool,
|
||||
expErr: require.NoError,
|
||||
},
|
||||
{
|
||||
desc: "returns nil when DNS name matches a wildcard",
|
||||
sans: []SAN{{Type: SANDNSNameType, Value: "bar.foo.com"}},
|
||||
rawCerts: [][]byte{pki.newLeafCertDER(t, []string{"*.foo.com"}, nil)},
|
||||
rootCAs: pki.caPool,
|
||||
expErr: require.NoError,
|
||||
},
|
||||
{
|
||||
desc: "returns nil when one of the DNS names matches",
|
||||
sans: []SAN{{Type: SANDNSNameType, Value: "foo.com"}},
|
||||
rawCerts: [][]byte{pki.newLeafCertDER(t, []string{"example.com", "foo.com"}, nil)},
|
||||
rootCAs: pki.caPool,
|
||||
expErr: require.NoError,
|
||||
},
|
||||
{
|
||||
desc: "returns nil when DNS name matches case-insensitively",
|
||||
sans: []SAN{{Type: SANDNSNameType, Value: "FOO.COM"}},
|
||||
rawCerts: [][]byte{pki.newLeafCertDER(t, []string{"foo.com"}, nil)},
|
||||
rootCAs: pki.caPool,
|
||||
expErr: require.NoError,
|
||||
},
|
||||
{
|
||||
desc: "returns nil when URI matches in mixed sans",
|
||||
sans: []SAN{
|
||||
{Type: SANURIType, Value: "spiffe://foo.com"},
|
||||
{Type: SANDNSNameType, Value: "foo.com"},
|
||||
},
|
||||
rawCerts: [][]byte{pki.newLeafCertDER(t, nil, []*url.URL{
|
||||
{Scheme: "spiffe", Host: "foo.com"},
|
||||
})},
|
||||
rootCAs: pki.caPool,
|
||||
expErr: require.NoError,
|
||||
},
|
||||
{
|
||||
desc: "returns nil when DNS name matches in mixed sans",
|
||||
sans: []SAN{
|
||||
{Type: SANURIType, Value: "spiffe://foo.com"},
|
||||
{Type: SANDNSNameType, Value: "foo.com"},
|
||||
},
|
||||
rawCerts: [][]byte{pki.newLeafCertDER(t, []string{"foo.com"}, nil)},
|
||||
rootCAs: pki.caPool,
|
||||
expErr: require.NoError,
|
||||
},
|
||||
{
|
||||
desc: "returns error when neither URI nor DNS name matches in mixed sans",
|
||||
sans: []SAN{
|
||||
{Type: SANURIType, Value: "spiffe://foo.com"},
|
||||
{Type: SANDNSNameType, Value: "foo.com"},
|
||||
},
|
||||
rawCerts: [][]byte{pki.newLeafCertDER(t, []string{"other.com"}, []*url.URL{
|
||||
{Scheme: "spiffe", Host: "other.org"},
|
||||
})},
|
||||
rootCAs: pki.caPool,
|
||||
expErr: require.Error,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -57,8 +147,65 @@ func Test_verifyServerCertMatchesURI(t *testing.T) {
|
||||
t.Run(test.desc, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
err := verifyServerCertMatchesURI(test.uri, test.cert)
|
||||
err := VerifyPeerCertificate(test.sans, test.rootCAs, test.rawCerts)
|
||||
test.expErr(t, err)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
type testPKI struct {
|
||||
caPool *x509.CertPool
|
||||
caCert *x509.Certificate
|
||||
caKey *rsa.PrivateKey
|
||||
}
|
||||
|
||||
func newTestPKI(t *testing.T) *testPKI {
|
||||
t.Helper()
|
||||
|
||||
caKey, err := rsa.GenerateKey(rand.Reader, 2048)
|
||||
require.NoError(t, err)
|
||||
|
||||
caTemplate := &x509.Certificate{
|
||||
SerialNumber: big.NewInt(1),
|
||||
Subject: pkix.Name{Organization: []string{"Test CA"}},
|
||||
NotBefore: time.Now(),
|
||||
NotAfter: time.Now().Add(time.Hour),
|
||||
KeyUsage: x509.KeyUsageCertSign | x509.KeyUsageCRLSign,
|
||||
BasicConstraintsValid: true,
|
||||
IsCA: true,
|
||||
}
|
||||
|
||||
caCertDER, err := x509.CreateCertificate(rand.Reader, caTemplate, caTemplate, &caKey.PublicKey, caKey)
|
||||
require.NoError(t, err)
|
||||
|
||||
caCert, err := x509.ParseCertificate(caCertDER)
|
||||
require.NoError(t, err)
|
||||
|
||||
pool := x509.NewCertPool()
|
||||
pool.AddCert(caCert)
|
||||
|
||||
return &testPKI{caPool: pool, caCert: caCert, caKey: caKey}
|
||||
}
|
||||
|
||||
func (p *testPKI) newLeafCertDER(t *testing.T, dnsNames []string, uris []*url.URL) []byte {
|
||||
t.Helper()
|
||||
|
||||
leafKey, err := rsa.GenerateKey(rand.Reader, 2048)
|
||||
require.NoError(t, err)
|
||||
|
||||
template := &x509.Certificate{
|
||||
SerialNumber: big.NewInt(2),
|
||||
Subject: pkix.Name{Organization: []string{"Test Leaf"}},
|
||||
NotBefore: time.Now(),
|
||||
NotAfter: time.Now().Add(time.Hour),
|
||||
KeyUsage: x509.KeyUsageDigitalSignature | x509.KeyUsageKeyEncipherment,
|
||||
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
|
||||
DNSNames: dnsNames,
|
||||
URIs: uris,
|
||||
}
|
||||
|
||||
certDER, err := x509.CreateCertificate(rand.Reader, template, p.caCert, &leafKey.PublicKey, p.caKey)
|
||||
require.NoError(t, err)
|
||||
|
||||
return certDER
|
||||
}
|
||||
|
||||
@@ -134,6 +134,22 @@ func (in *Options) DeepCopy() *Options {
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *SAN) DeepCopyInto(out *SAN) {
|
||||
*out = *in
|
||||
return
|
||||
}
|
||||
|
||||
// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SAN.
|
||||
func (in *SAN) DeepCopy() *SAN {
|
||||
if in == nil {
|
||||
return nil
|
||||
}
|
||||
out := new(SAN)
|
||||
in.DeepCopyInto(out)
|
||||
return out
|
||||
}
|
||||
|
||||
// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil.
|
||||
func (in *Store) DeepCopyInto(out *Store) {
|
||||
*out = *in
|
||||
|
||||
@@ -4,11 +4,11 @@ RepositoryName = "traefik"
|
||||
OutputType = "file"
|
||||
FileName = "traefik_changelog.md"
|
||||
|
||||
# example new bugfix v3.7.4
|
||||
# example new bugfix v3.7.5
|
||||
CurrentRef = "v3.7"
|
||||
PreviousRef = "v3.7.3"
|
||||
PreviousRef = "v3.7.4"
|
||||
BaseBranch = "v3.7"
|
||||
FutureCurrentRefName = "v3.7.4"
|
||||
FutureCurrentRefName = "v3.7.5"
|
||||
|
||||
ThresholdPreviousRef = 10000
|
||||
ThresholdCurrentRef = 10000
|
||||
|
||||
Reference in New Issue
Block a user