diff --git a/cmd/traefik/plugins.go b/cmd/traefik/plugins.go index 2c3d5dd25..ef939d2a0 100644 --- a/cmd/traefik/plugins.go +++ b/cmd/traefik/plugins.go @@ -2,43 +2,62 @@ package main import ( "fmt" + "net/http" + "path/filepath" + "time" + "github.com/hashicorp/go-retryablehttp" + "github.com/rs/zerolog/log" "github.com/traefik/traefik/v3/pkg/config/static" + "github.com/traefik/traefik/v3/pkg/logs" "github.com/traefik/traefik/v3/pkg/plugins" ) const outputDir = "./plugins-storage/" func createPluginBuilder(staticConfiguration *static.Configuration) (*plugins.Builder, error) { - client, plgs, localPlgs, err := initPlugins(staticConfiguration) + manager, plgs, localPlgs, err := initPlugins(staticConfiguration) if err != nil { return nil, err } - return plugins.NewBuilder(client, plgs, localPlgs) + return plugins.NewBuilder(manager, plgs, localPlgs) } -func initPlugins(staticCfg *static.Configuration) (*plugins.Client, map[string]plugins.Descriptor, map[string]plugins.LocalDescriptor, error) { +func initPlugins(staticCfg *static.Configuration) (*plugins.Manager, map[string]plugins.Descriptor, map[string]plugins.LocalDescriptor, error) { err := checkUniquePluginNames(staticCfg.Experimental) if err != nil { return nil, nil, nil, err } - var client *plugins.Client + var manager *plugins.Manager plgs := map[string]plugins.Descriptor{} if hasPlugins(staticCfg) { - opts := plugins.ClientOptions{ + httpClient := retryablehttp.NewClient() + httpClient.Logger = logs.NewRetryableHTTPLogger(log.Logger) + httpClient.HTTPClient = &http.Client{Timeout: 10 * time.Second} + httpClient.RetryMax = 3 + + // Create separate downloader for HTTP operations + archivesPath := filepath.Join(outputDir, "archives") + downloader, err := plugins.NewRegistryDownloader(plugins.RegistryDownloaderOptions{ + HTTPClient: httpClient.HTTPClient, + ArchivesPath: archivesPath, + }) + if err != nil { + return nil, nil, nil, fmt.Errorf("unable to create plugin downloader: %w", err) + } + + opts := plugins.ManagerOptions{ Output: outputDir, } - - var err error - client, err = plugins.NewClient(opts) + manager, err = plugins.NewManager(downloader, opts) if err != nil { - return nil, nil, nil, fmt.Errorf("unable to create plugins client: %w", err) + return nil, nil, nil, fmt.Errorf("unable to create plugins manager: %w", err) } - err = plugins.SetupRemotePlugins(client, staticCfg.Experimental.Plugins) + err = plugins.SetupRemotePlugins(manager, staticCfg.Experimental.Plugins) if err != nil { return nil, nil, nil, fmt.Errorf("unable to set up plugins environment: %w", err) } @@ -57,7 +76,7 @@ func initPlugins(staticCfg *static.Configuration) (*plugins.Client, map[string]p localPlgs = staticCfg.Experimental.LocalPlugins } - return client, plgs, localPlgs, nil + return manager, plgs, localPlgs, nil } func checkUniquePluginNames(e *static.Experimental) error { diff --git a/docs/content/assets/img/secure/oidc-auth-flow.png b/docs/content/assets/img/secure/oidc-auth-flow.png new file mode 100644 index 000000000..c41c32558 Binary files /dev/null and b/docs/content/assets/img/secure/oidc-auth-flow.png differ diff --git a/docs/content/govern/index.md b/docs/content/govern/index.md new file mode 100644 index 000000000..9a574ba90 --- /dev/null +++ b/docs/content/govern/index.md @@ -0,0 +1,59 @@ +--- +title: "Govern Your APIs with Traefik Hub" +description: "Learn how Traefik Hub provides comprehensive API Gateway and API Management capabilities to govern, secure, and manage your APIs at scale." +--- + +# Govern Your APIs with Traefik Hub + +Traefik Hub transforms your API infrastructure by providing enterprise-grade API Gateway and comprehensive API Management capabilities. Built on the foundation of Traefik Proxy, Hub enables organizations to govern, secure, and manage APIs at scale across cloud-native environments. + +## API Gateway and API Management Capabilities + +Traefik Hub offers two complementary approaches to API governance: + +- [Traefik Hub API Gateway](https://traefik.io/solutions/api-gateway/): Enterprise-grade security, distributed features, and advanced access control for cloud-native API gateway scenarios. It includes AI Gateway capabilities for modern machine learning workloads. + +- [Traefik Hub API Management](https://traefik.io/solutions/api-management/): Comprehensive API lifecycle management, developer portals, and organizational features for teams managing multiple APIs across environments. + +## API Management Features + +Traefik Hub API Management provides a complete suite of features designed to streamline API governance and accelerate developer productivity: + +### Comprehensive API Lifecycle Management + +- **Centralized API Catalog**: Comprehensive API inventory providing real-time visibility and control +- **Advanced Versioning**: Utilize the most flexible API versioning and Kubernetes-native labels to logically organize APIs, track their evolution over time, and avoid breaking changes for client applications +- **Quality Control**: Perform error checks and change impact analysis with Traefik Hub's static analyzer to ensure compliance and prevent misconfiguration by catching errors early +- **Resource Management**: Centralize rate limits, quotas, and policy enforcement across groups of APIs through Plans & Bundles, ensuring consistent and efficient resource management +- **Subscription Management**: Create managed and self-serve subscriptions for enhanced ease-of-use, security, and auditability +- **Standards Compliance**: Import, validate, and manage APIs using industry-standard OpenAPI specifications + +### Developer Experience & Self-Service + +- **Customizable Portals**: Easily create one or more dedicated API Portals for developers to discover, understand, and interact with your APIs +- **Interactive API Testing**: Allow developers to test APIs directly within the portal for immediate feedback +- **Branded Experience**: Customize the API portal to house API documentation, clear integration instructions, and personalized content to foster adoption and streamline development +- **Credential Management**: Generate API access credentials, such as API keys, directly from the portal for simplified access and secure integration +- **Auto-generated Documentation**: Automatically generated, interactive API documentation from OpenAPI specifications in the portal + +### Enterprise-Grade Security + +- **Access Control**: Implement fine-grained permissions through detailed API policies and JWT inspection to secure access to API resources +- **Identity Integration**: Leverage existing identity and access management (IAM) solutions, such as Keycloak, Okta, Auth0, etc. through industry-standard authentication protocols, including native OIDC support +- **Data Protection**: Advanced TLS management, supporting Let's Encrypt (ACME) and SPIFFE for robust data protection +- **Threat Prevention**: Native Web Application Firewall (WAF) powered by OWASP Coraza to defend against known vulnerabilities + +### Observability + +- **Open Standards Monitoring**: Gain deep insights into API health and performance with OpenTelemetry integration to provide metrics and tracing capabilities without vendor lock-in +- **Third-party Integrations**: Turnkey integration with Treblle directly on the Traefik Hub Dashboard for real-time, out-of-the-box observability without maintaining a complex monitoring infrastructure +- **Pre-built Grafana Dashboards**: Ready-to-use monitoring dashboards with key API metrics and performance indicators + +## Getting Started with API Governance + +Transform your API infrastructure with Traefik Hub's governance capabilities: + +1. **Start with API Gateway**: Implement enterprise security, authentication, distributed features, and AI Gateway capabilities +2. **Scale with API Management**: Add comprehensive lifecycle management, developer portals, and governance features + +Ready to govern your APIs at scale? Explore [Traefik Hub API Management](https://traefik.io/solutions/api-management/) and discover how it can transform your API strategy. diff --git a/docs/content/reference/dynamic-configuration/kubernetes-crd-definition-v1.yml b/docs/content/reference/dynamic-configuration/kubernetes-crd-definition-v1.yml index afc32d9b9..98268e8e3 100644 --- a/docs/content/reference/dynamic-configuration/kubernetes-crd-definition-v1.yml +++ b/docs/content/reference/dynamic-configuration/kubernetes-crd-definition-v1.yml @@ -2316,7 +2316,7 @@ spec: maxIdleConnsPerHost: description: MaxIdleConnsPerHost controls the maximum idle (keep-alive) to keep per-host. - minimum: 0 + minimum: -1 type: integer peerCertURI: description: PeerCertURI defines the peer cert URI used to match against diff --git a/docs/content/reference/dynamic-configuration/traefik.io_serverstransports.yaml b/docs/content/reference/dynamic-configuration/traefik.io_serverstransports.yaml index 132f71685..f8d29a96a 100644 --- a/docs/content/reference/dynamic-configuration/traefik.io_serverstransports.yaml +++ b/docs/content/reference/dynamic-configuration/traefik.io_serverstransports.yaml @@ -107,7 +107,7 @@ spec: maxIdleConnsPerHost: description: MaxIdleConnsPerHost controls the maximum idle (keep-alive) to keep per-host. - minimum: 0 + minimum: -1 type: integer peerCertURI: description: PeerCertURI defines the peer cert URI used to match against diff --git a/docs/content/reference/install-configuration/configuration-options.md b/docs/content/reference/install-configuration/configuration-options.md index 4c88ae5be..2a0c4a4dc 100644 --- a/docs/content/reference/install-configuration/configuration-options.md +++ b/docs/content/reference/install-configuration/configuration-options.md @@ -130,6 +130,7 @@ THIS FILE MUST NOT BE EDITED BY HAND | experimental.localplugins._name_.settings.mounts | Directory to mount to the wasm guest. | | | experimental.localplugins._name_.settings.useunsafe | Allow the plugin to use unsafe package. | false | | experimental.otlplogs | Enables the OpenTelemetry logs integration. | false | +| experimental.plugins._name_.hash | plugin's hash to validate' | | | experimental.plugins._name_.modulename | plugin's module name. | | | experimental.plugins._name_.settings | Plugin's settings (works only for wasm plugins). | | | experimental.plugins._name_.settings.envs | Environment variables to forward to the wasm guest. | | @@ -441,7 +442,7 @@ THIS FILE MUST NOT BE EDITED BY HAND | serverstransport.forwardingtimeouts.idleconntimeout | The maximum period for which an idle HTTP keep-alive connection will remain open before closing itself | 90 | | serverstransport.forwardingtimeouts.responseheadertimeout | The amount of time to wait for a server's response headers after fully writing the request (including its body, if any). If zero, no timeout exists. | 0 | | serverstransport.insecureskipverify | Disable SSL certificate verification. | false | -| serverstransport.maxidleconnsperhost | If non-zero, controls the maximum idle (keep-alive) to keep per-host. If zero, DefaultMaxIdleConnsPerHost is used | 200 | +| serverstransport.maxidleconnsperhost | If non-zero, controls the maximum idle (keep-alive) to keep per-host. If zero, DefaultMaxIdleConnsPerHost is used. If negative, disables connection reuse. | 200 | | serverstransport.rootcas | Add cert file for self-signed certificate. | | | serverstransport.spiffe | Defines the SPIFFE configuration. | false | | serverstransport.spiffe.ids | Defines the allowed SPIFFE IDs (takes precedence over the SPIFFE TrustDomain). | | diff --git a/docs/content/reference/install-configuration/entrypoints.md b/docs/content/reference/install-configuration/entrypoints.md index 62adb7c1b..e7badffbf 100644 --- a/docs/content/reference/install-configuration/entrypoints.md +++ b/docs/content/reference/install-configuration/entrypoints.md @@ -25,10 +25,11 @@ entryPoints: websecure: address: :443 - tls: {} - middlewares: - - auth@kubernetescrd - - strip@kubernetescrd + http: + tls: {} + middlewares: + - auth@kubernetescrd + - strip@kubernetescrd ``` ```toml tab="File (TOML)" @@ -151,18 +152,20 @@ are applied after the ones declared on the Entrypoint) entryPoints: web: address: :80 - middlewares: - - auth@kubernetescrd - - strip@file + http: + middlewares: + - auth@kubernetescrd + - strip@file ``` ```yaml tab="Helm Chart Values" ports: web: port: :80 - middlewares: - - auth@kubernetescrd - - strip@file + http: + middlewares: + - auth@kubernetescrd + - strip@file ``` ### encodeQuerySemicolons diff --git a/docs/content/reference/routing-configuration/http/middlewares/distributed-ratelimit.md b/docs/content/reference/routing-configuration/http/middlewares/distributed-ratelimit.md index cb4484779..aa718680b 100644 --- a/docs/content/reference/routing-configuration/http/middlewares/distributed-ratelimit.md +++ b/docs/content/reference/routing-configuration/http/middlewares/distributed-ratelimit.md @@ -113,8 +113,6 @@ The `sourceCriterion` option defines what criterion is used to group requests as If several strategies are defined at the same time, an error will be raised. If none are set, the default is to use the request's remote address field (as an `ipStrategy`). -Check out the [OIDC + RateLimit & DistributedRateLimit guide](../../../../secure/middleware/drl-oidc.md) to see this option in action. - ### ipStrategy The `ipStrategy` option defines two parameters that configures how Traefik determines the client IP: `depth`, and `excludedIPs`. diff --git a/docs/content/reference/routing-configuration/http/router/rules-and-priority.md b/docs/content/reference/routing-configuration/http/router/rules-and-priority.md index bf3382fc2..de2ed3eec 100644 --- a/docs/content/reference/routing-configuration/http/router/rules-and-priority.md +++ b/docs/content/reference/routing-configuration/http/router/rules-and-priority.md @@ -83,7 +83,7 @@ Path are always starting with a `/`, except for `PathRegexp`. | Match `/products` as well as everything under `/products`, such as `/products/shoes`, `/products/` but also `/products-for-sale`. | ```PathPrefix(`/products`)``` | | Match both `/products/shoes` and `/products/socks` with and ID like `/products/shoes/31`. | ```PathRegexp(`^/products/(shoes\|socks)/[0-9]+$`)``` | | Match requests with a path ending in either `.jpeg`, `.jpg` or `.png`. | ```PathRegexp(`\.(jpeg\|jpg\|png)$`)``` | -| Match `/products` as well as everything under `/products`, such as `/products/shoes`, `/products/` but also `/products-for-sale`, [case-insensitively](https://en.wikipedia.org/wiki/Case_sensitivity). | ```HostRegexp(`(?i)^/products`)``` | +| Match `/products` as well as everything under `/products`, such as `/products/shoes`, `/products/` but also `/products-for-sale`, [case-insensitively](https://en.wikipedia.org/wiki/Case_sensitivity). | ```PathRegexp(`(?i)^/products`)``` | ### Query and QueryRegexp diff --git a/docs/content/reference/routing-configuration/kubernetes/crd/http/ingressroute.md b/docs/content/reference/routing-configuration/kubernetes/crd/http/ingressroute.md index 29a89e5da..9356189d0 100644 --- a/docs/content/reference/routing-configuration/kubernetes/crd/http/ingressroute.md +++ b/docs/content/reference/routing-configuration/kubernetes/crd/http/ingressroute.md @@ -87,37 +87,8 @@ spec: | `routes[n].`
`observability.`
`accesslogs`
| Defines whether the route will produce [access-logs](../../../../install-configuration/observability/logs-and-accesslogs.md). See [here](../../../http/router/observability.md) for more information. | false | No | | `routes[n].`
`observability.`
`metrics`
| Defines whether the route will produce [metrics](../../../../install-configuration/observability/metrics.md). See [here](../../../http/router/observability.md) for more information. | false | No | | `routes[n].`
`observability.`
`tracing`
| Defines whether the route will produce [traces](../../../../install-configuration/observability/tracing.md). See [here](../../../http/router/observability.md) for more information. | false | No | -| `routes[n].`
`services`
| List of any combination of TraefikService and [Kubernetes service](https://kubernetes.io/docs/concepts/services-networking/service/).
More information [here](#externalname-service). | | No | -| `routes[n].`
`services[m].`
`kind`
| Kind of the service targeted.
Two values allowed:
- **Service**: Kubernetes Service
**TraefikService**: Traefik Service.
More information [here](#externalname-service). | "Service" | No | -| `routes[n].`
`services[m].`
`name`
| Service name.
The character `@` is not authorized.
More information [here](#middleware). | | Yes | -| `routes[n].`
`services[m].`
`namespace`
| Service namespace.
Can be empty if the service belongs to the same namespace as the IngressRoute.
More information [here](#externalname-service). | | No | -| `routes[n].`
`services[m].`
`port`
| Service port (number or port name).
Evaluated only if the kind is **Service**. | | No | -| `routes[n].`
`services[m].`
`responseForwarding.`
`flushInterval`
| Interval, in milliseconds, in between flushes to the client while copying the response body.
A negative value means to flush immediately after each write to the client.
This configuration is ignored when a response is a streaming response; for such responses, writes are flushed to the client immediately.
Evaluated only if the kind is **Service**. | 100ms | No | -| `routes[n].`
`services[m].`
`scheme`
| Scheme to use for the request to the upstream Kubernetes Service.
Evaluated only if the kind is **Service**. | "http"
"https" if `port` is 443 or contains the string *https*. | No | -| `routes[n].`
`services[m].`
`serversTransport`
| Name of ServersTransport resource to use to configure the transport between Traefik and your servers.
Evaluated only if the kind is **Service**. | "" | No | -| `routes[n].`
`services[m].`
`passHostHeader`
| Forward client Host header to server.
Evaluated only if the kind is **Service**. | true | No | -| `routes[n].`
`services[m].`
`healthCheck.scheme`
| Server URL scheme for the health check endpoint.
Evaluated only if the kind is **Service**.
Only for [Kubernetes service](https://kubernetes.io/docs/concepts/services-networking/service/) of type [ExternalName](#externalname-service). | "" | No | -| `routes[n].`
`services[m].`
`healthCheck.mode`
| Health check mode.
If defined to grpc, will use the gRPC health check protocol to probe the server.
Evaluated only if the kind is **Service**.
Only for [Kubernetes service](https://kubernetes.io/docs/concepts/services-networking/service/) of type [ExternalName](#externalname-service). | "http" | No | -| `routes[n].`
`services[m].`
`healthCheck.path`
| Server URL path for the health check endpoint.
Evaluated only if the kind is **Service**.
Only for [Kubernetes service](https://kubernetes.io/docs/concepts/services-networking/service/) of type [ExternalName](#externalname-service). | "" | No | -| `routes[n].`
`services[m].`
`healthCheck.interval`
| Frequency of the health check calls for healthy targets.
Evaluated only if the kind is **Service**.
Only for [Kubernetes service](https://kubernetes.io/docs/concepts/services-networking/service/) of type [ExternalName](#externalname-service). | "100ms" | No | -| `routes[n].`
`services[m].`
`healthCheck.unhealthyInterval`
| Frequency of the health check calls for unhealthy targets.
When not defined, it defaults to the `interval` value.
Evaluated only if the kind is **Service**.
Only for [Kubernetes service](https://kubernetes.io/docs/concepts/services-networking/service/) of type [ExternalName](#externalname-service). | "100ms" | No | -| `routes[n].`
`services[m].`
`healthCheck.method`
| HTTP method for the health check endpoint.
Evaluated only if the kind is **Service**.
Only for [Kubernetes service](https://kubernetes.io/docs/concepts/services-networking/service/) of type [ExternalName](#externalname-service). | "GET" | No | -| `routes[n].`
`services[m].`
`healthCheck.status`
| Expected HTTP status code of the response to the health check request.
Only for [Kubernetes service](https://kubernetes.io/docs/concepts/services-networking/service/) of type ExternalName.
If not set, expect a status between 200 and 399.
Evaluated only if the kind is **Service**. | | No | -| `routes[n].`
`services[m].`
`healthCheck.port`
| URL port for the health check endpoint.
Evaluated only if the kind is **Service**.
Only for [Kubernetes service](https://kubernetes.io/docs/concepts/services-networking/service/) of type [ExternalName](#externalname-service). | | No | -| `routes[n].`
`services[m].`
`healthCheck.timeout`
| Maximum duration to wait before considering the server unhealthy.
Evaluated only if the kind is **Service**.
Only for [Kubernetes service](https://kubernetes.io/docs/concepts/services-networking/service/) of type [ExternalName](#externalname-service). | "5s" | No | -| `routes[n].`
`services[m].`
`healthCheck.hostname`
| Value in the Host header of the health check request.
Evaluated only if the kind is **Service**.
Only for [Kubernetes service](https://kubernetes.io/docs/concepts/services-networking/service/) of type [ExternalName](#externalname-service). | "" | No | -| `routes[n].`
`services[m].`
`healthCheck.`
`followRedirect`
| Follow the redirections during the healtchcheck.
Evaluated only if the kind is **Service**.
Only for [Kubernetes service](https://kubernetes.io/docs/concepts/services-networking/service/) of type [ExternalName](#externalname-service). | true | No | -| `routes[n].`
`services[m].`
`healthCheck.headers`
| Map of header to send to the health check endpoint
Evaluated only if the kind is **Service**.
Only for [Kubernetes service](https://kubernetes.io/docs/concepts/services-networking/service/) of type [ExternalName](#externalname-service)). | | No | -| `routes[n].`
`services[m].`
`sticky.`
`cookie.name`
| Name of the cookie used for the stickiness.
When sticky sessions are enabled, a `Set-Cookie` header is set on the initial response to let the client know which server handles the first response.
On subsequent requests, to keep the session alive with the same server, the client should send the cookie with the value set.
If the server pecified in the cookie becomes unhealthy, the request will be forwarded to a new server (and the cookie will keep track of the new server).
Evaluated only if the kind is **Service**. | "" | No | -| `routes[n].`
`services[m].`
`sticky.`
`cookie.httpOnly`
| Allow the cookie can be accessed by client-side APIs, such as JavaScript.
Evaluated only if the kind is **Service**. | false | No | -| `routes[n].`
`services[m].`
`sticky.`
`cookie.secure`
| Allow the cookie can only be transmitted over an encrypted connection (i.e. HTTPS).
Evaluated only if the kind is **Service**. | false | No | -| `routes[n].`
`services[m].`
`sticky.`
`cookie.sameSite`
| [SameSite](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie/SameSite) policy
Allowed values:
-`none`
-`lax`
`strict`
Evaluated only if the kind is **Service**. | "" | No | -| `routes[n].`
`services[m].`
`sticky.`
`cookie.maxAge`
| Number of seconds until the cookie expires.
Negative number, the cookie expires immediately.
0, the cookie never expires.
Evaluated only if the kind is **Service**. | 0 | No | -| `routes[n].`
`services[m].`
`strategy`
| Load balancing strategy between the servers.
RoundRobin is the only supported value yet.
Evaluated only if the kind is **Service**. | "RoundRobin" | No | -| `routes[n].`
`services[m].`
`weight`
| Service weight.
To use only to refer to WRR TraefikService | "" | No | -| `routes[n].`
`services[m].`
`nativeLB`
| Allow using the Kubernetes Service load balancing between the pods instead of the one provided by Traefik.
Evaluated only if the kind is **Service**. | false | No | -| `routes[n].`
`services[m].`
`nodePortLB`
| Use the nodePort IP address when the service type is NodePort.
It allows services to be reachable when Traefik runs externally from the Kubernetes cluster but within the same network of the nodes.
Evaluated only if the kind is **Service**. | false | No | | `tls` | TLS configuration.
Can be an empty value(`{}`):
A self signed is generated in such a case
(or the [default certificate](tlsstore.md) is used if it is defined.) | | No | +| `routes[n].`
`services`
| List of any combination of [TraefikService](./traefikservice.md) and [Kubernetes service](https://kubernetes.io/docs/concepts/services-networking/service/).
Exhaustive list of option in the [`Service`](./service.md#configuration-options) documentation. | | No | | `tls.secretName` | [Secret](https://kubernetes.io/docs/concepts/configuration/secret/) name used to store the certificate (in the same namesapce as the `IngressRoute`) | "" | No | | `tls.`
`options.name`
| Name of the [`TLSOption`](tlsoption.md) to use.
More information [here](#tls-options). | "" | No | | `tls.`
`options.namespace`
| Namespace of the [`TLSOption`](tlsoption.md) to use. | "" | No | @@ -126,113 +97,6 @@ spec: | `tls.`
`domains[n].main`
| Main domain name | "" | Yes | | `tls.`
`domains[n].sans`
| List of alternative domains (SANs) | | No | -### ExternalName Service - -Traefik backends creation needs a port to be set, however Kubernetes [ExternalName Service](https://kubernetes.io/docs/concepts/services-networking/service/#externalname) could be defined without any port. Accordingly, Traefik supports defining a port in two ways: - -- only on `IngressRoute` service -- on both sides, you'll be warned if the ports don't match, and the `IngressRoute` service port is used - -Thus, in case of two sides port definition, Traefik expects a match between ports. - -=== "Ports defined on Resource" - - ```yaml tab="IngressRoute" - apiVersion: traefik.io/v1alpha1 - kind: IngressRoute - metadata: - name: test.route - namespace: apps - - spec: - entryPoints: - - foo - routes: - - match: Host(`example.net`) - kind: Rule - services: - - name: external-svc - port: 80 - ``` - - ```yaml tab="Service ExternalName" - apiVersion: v1 - kind: Service - metadata: - name: external-svc - namespace: apps - - spec: - externalName: external.domain - type: ExternalName - ``` - -=== "Port defined on the Service" - - ```yaml tab="IngressRoute" - apiVersion: traefik.io/v1alpha1 - kind: IngressRoute - metadata: - name: test.route - namespace: apps - - spec: - entryPoints: - - foo - routes: - - match: Host(`example.net`) - kind: Rule - services: - - name: external-svc - ``` - - ```yaml tab="Service ExternalName" - apiVersion: v1 - kind: Service - metadata: - name: external-svc - namespace: apps - - spec: - externalName: external.domain - type: ExternalName - ports: - - port: 80 - ``` - -=== "Port defined on both sides" - - ```yaml tab="IngressRoute" - apiVersion: traefik.io/v1alpha1 - kind: IngressRoute - metadata: - name: test.route - namespace: apps - - spec: - entryPoints: - - foo - routes: - - match: Host(`example.net`) - kind: Rule - services: - - name: external-svc - port: 80 - ``` - - ```yaml tab="Service ExternalName" - apiVersion: v1 - kind: Service - metadata: - name: external-svc - namespace: apps - - spec: - externalName: external.domain - type: ExternalName - ports: - - port: 80 - ``` ### Middleware @@ -282,109 +146,6 @@ same namespace as the IngressRoute) - `Service` (default value): to reference a [Kubernetes Service](https://kubernetes.io/docs/concepts/services-networking/service/) - `TraefikService`: to reference an object [`TraefikService`](../http/traefikservice.md) -### Port Definition - -Traefik backends creation needs a port to be set, however Kubernetes [ExternalName Service](https://kubernetes.io/docs/concepts/services-networking/service/#externalname) could be defined without any port. Accordingly, Traefik supports defining a port in two ways: - -- only on `IngressRoute` service -- on both sides, you'll be warned if the ports don't match, and the `IngressRoute` service port is used - -Thus, in case of two sides port definition, Traefik expects a match between ports. - -??? example - - ```yaml tab="IngressRoute" - --- - apiVersion: traefik.io/v1alpha1 - kind: IngressRoute - metadata: - name: test.route - namespace: default - - spec: - entryPoints: - - foo - - routes: - - match: Host(`example.net`) - kind: Rule - services: - - name: external-svc - port: 80 - - --- - apiVersion: v1 - kind: Service - metadata: - name: external-svc - namespace: default - spec: - externalName: external.domain - type: ExternalName - ``` - - ```yaml tab="ExternalName Service" - --- - apiVersion: traefik.io/v1alpha1 - kind: IngressRoute - metadata: - name: test.route - namespace: default - - spec: - entryPoints: - - foo - - routes: - - match: Host(`example.net`) - kind: Rule - services: - - name: external-svc - - --- - apiVersion: v1 - kind: Service - metadata: - name: external-svc - namespace: default - spec: - externalName: external.domain - type: ExternalName - ports: - - port: 80 - ``` - - ```yaml tab="Both sides" - --- - apiVersion: traefik.io/v1alpha1 - kind: IngressRoute - metadata: - name: test.route - namespace: default - - spec: - entryPoints: - - foo - - routes: - - match: Host(`example.net`) - kind: Rule - services: - - name: external-svc - port: 80 - - --- - apiVersion: v1 - kind: Service - metadata: - name: external-svc - namespace: default - spec: - externalName: external.domain - type: ExternalName - ports: - - port: 80 - ``` ### TLS Options @@ -455,108 +216,3 @@ TLS options references, a conflict occurs, such as in the example below. If that happens, both mappings are discarded, and the host name (`example.net` in the example) for these routers gets associated with the default TLS options instead. - -### Load Balancing - -You can declare and use Kubernetes Service load balancing as detailed below: - -```yaml tab="IngressRoute" -apiVersion: traefik.io/v1alpha1 -kind: IngressRoute -metadata: - name: ingressroutebar - namespace: default - -spec: - entryPoints: - - web - routes: - - match: Host(`example.com`) && PathPrefix(`/foo`) - kind: Rule - services: - - name: svc1 - namespace: default - - name: svc2 - namespace: default -``` - -```yaml tab="K8s Service" -apiVersion: v1 -kind: Service -metadata: - name: svc1 - namespace: default - -spec: - ports: - - name: http - port: 80 - selector: - app: traefiklabs - task: app1 ---- -apiVersion: v1 -kind: Service -metadata: - name: svc2 - namespace: default - -spec: - ports: - - name: http - port: 80 - selector: - app: traefiklabs - task: app2 -``` - -!!! important "Kubernetes Service Native Load-Balancing" - - To avoid creating the server load-balancer with the pod IPs and use Kubernetes Service clusterIP directly, - one should set the service `NativeLB` option to true. - Please note that, by default, Traefik reuses the established connections to the backends for performance purposes. This can prevent the requests load balancing between the replicas from behaving as one would expect when the option is set. - By default, `NativeLB` is false. - - ??? example "Example" - - ```yaml - --- - apiVersion: traefik.io/v1alpha1 - kind: IngressRoute - metadata: - name: test.route - namespace: default - - spec: - entryPoints: - - foo - - routes: - - match: Host(`example.net`) - kind: Rule - services: - - name: svc - port: 80 - # Here, nativeLB instructs to build the server load-balancer with the Kubernetes Service clusterIP only. - nativeLB: true - - --- - apiVersion: v1 - kind: Service - metadata: - name: svc - namespace: default - spec: - type: ClusterIP - ... - ``` - -### Configuring Backend Protocol - -There are 3 ways to configure the backend protocol for communication between Traefik and your pods: - -- Setting the scheme explicitly (http/https/h2c) -- Configuring the name of the kubernetes service port to start with https (https) -- Setting the kubernetes service port to use port 443 (https) - -If you do not configure the above, Traefik will assume an http connection. diff --git a/docs/content/reference/routing-configuration/kubernetes/crd/http/service.md b/docs/content/reference/routing-configuration/kubernetes/crd/http/service.md new file mode 100644 index 000000000..14de09555 --- /dev/null +++ b/docs/content/reference/routing-configuration/kubernetes/crd/http/service.md @@ -0,0 +1,430 @@ +--- +title: "Kubernetes Service" +description: "A Service is a not Traefik CRD, it allows you to describe the Service option in an IngressRoute or a Traefik Service." +--- + +`Service` is the implementation of a [Traefik HTTP service](../../../http/load-balancing/service.md). + +There is no dedicated CRD, a `Service` is part of: + +- [`IngressRoute`](./ingressroute.md) +- [`TraefikService`](./traefikservice.md) + +Note that, before creating `IngressRoute` or `TraefikService` objects, you need to apply the [Traefik Kubernetes CRDs](https://doc.traefik.io/traefik/reference/dynamic-configuration/kubernetes-crd/#definitions) to your Kubernetes cluster. + +This registers the Traefik-specific resources. + +## Configuration Example + +You can declare a `Service` either as part of an `IngressRoute` or a `TraefikService` as detailed below: + +```yaml tab="IngressRoute" +apiVersion: traefik.io/v1alpha1 +kind: IngressRoute +metadata: + name: test-name + namespace: apps +spec: + entryPoints: + - web + routes: + - kind: Rule + # Rule on the Host + match: Host(`test.example.com`) + services: + # Target a Kubernetes Service + - kind: Service + name: foo + namespace: apps + # Customize the connection between Traefik and the backend + passHostHeader: true + port: 80 + responseForwarding: + flushInterval: 1ms + scheme: https + sticky: + cookie: + httpOnly: true + name: cookie + secure: true + strategy: RoundRobin +``` + +```yaml tab="TraefikService" +apiVersion: traefik.io/v1alpha1 +kind: TraefikService +metadata: + name: wrr1 + namespace: apps + +spec: + weighted: + services: + # Target a Kubernetes Service + - kind: Service + name: foo + namespace: apps + # Customize the connection between Traefik and the backend + passHostHeader: true + port: 80 + responseForwarding: + flushInterval: 1ms + scheme: https + sticky: + cookie: + httpOnly: true + name: cookie + secure: true + strategy: RoundRobin +``` + +## Configuration Options + +| Field | Description | Default | Required | +|:---------------------------------------------------------------------------------|:---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:---------------------------------------------------------------------|:---------| +| `kind` | Kind of the service targeted.
Two values allowed:
- **Service**: Kubernetes Service
**TraefikService**: Traefik Service.
More information [here](#externalname-service). | "Service" | No | +| `name` | Service name.
The character `@` is not authorized.
More information [here](#middleware). | | Yes | +| `namespace` | Service namespace.
Can be empty if the service belongs to the same namespace as the IngressRoute.
More information [here](#externalname-service). | | No | +| `port` | Service port (number or port name).
Evaluated only if the kind is **Service**. | | No | +| `responseForwarding.`
`flushInterval`
| Interval, in milliseconds, in between flushes to the client while copying the response body.
A negative value means to flush immediately after each write to the client.
This configuration is ignored when a response is a streaming response; for such responses, writes are flushed to the client immediately.
Evaluated only if the kind is **Service**. | 100ms | No | +| `scheme` | Scheme to use for the request to the upstream Kubernetes Service.
Evaluated only if the kind is **Service**. | "http"
"https" if `port` is 443 or contains the string *https*. | No | +| `serversTransport` | Name of ServersTransport resource to use to configure the transport between Traefik and your servers.
Evaluated only if the kind is **Service**. | "" | No | +| `passHostHeader` | Forward client Host header to server.
Evaluated only if the kind is **Service**. | true | No | +| `healthCheck.scheme` | Server URL scheme for the health check endpoint.
Evaluated only if the kind is **Service**.
Only for [Kubernetes service](https://kubernetes.io/docs/concepts/services-networking/service/) of type [ExternalName](#externalname-service). | "" | No | +| `healthCheck.mode` | Health check mode.
If defined to grpc, will use the gRPC health check protocol to probe the server.
Evaluated only if the kind is **Service**.
Only for [Kubernetes service](https://kubernetes.io/docs/concepts/services-networking/service/) of type [ExternalName](#externalname-service). | "http" | No | +| `healthCheck.path` | Server URL path for the health check endpoint.
Evaluated only if the kind is **Service**.
Only for [Kubernetes service](https://kubernetes.io/docs/concepts/services-networking/service/) of type [ExternalName](#externalname-service). | "" | No | +| `healthCheck.interval` | Frequency of the health check calls for healthy targets.
Evaluated only if the kind is **Service**.
Only for [Kubernetes service](https://kubernetes.io/docs/concepts/services-networking/service/) of type [ExternalName](#externalname-service). | "100ms" | No | +| `healthCheck.unhealthyInterval` | Frequency of the health check calls for unhealthy targets.
When not defined, it defaults to the `interval` value.
Evaluated only if the kind is **Service**.
Only for [Kubernetes service](https://kubernetes.io/docs/concepts/services-networking/service/) of type [ExternalName](#externalname-service). | "100ms" | No | +| `healthCheck.method` | HTTP method for the health check endpoint.
Evaluated only if the kind is **Service**.
Only for [Kubernetes service](https://kubernetes.io/docs/concepts/services-networking/service/) of type [ExternalName](#externalname-service). | "GET" | No | +| `healthCheck.status` | Expected HTTP status code of the response to the health check request.
Only for [Kubernetes service](https://kubernetes.io/docs/concepts/services-networking/service/) of type ExternalName.
If not set, expect a status between 200 and 399.
Evaluated only if the kind is **Service**. | | No | +| `healthCheck.port` | URL port for the health check endpoint.
Evaluated only if the kind is **Service**.
Only for [Kubernetes service](https://kubernetes.io/docs/concepts/services-networking/service/) of type [ExternalName](#externalname-service). | | No | +| `healthCheck.timeout` | Maximum duration to wait before considering the server unhealthy.
Evaluated only if the kind is **Service**.
Only for [Kubernetes service](https://kubernetes.io/docs/concepts/services-networking/service/) of type [ExternalName](#externalname-service). | "5s" | No | +| `healthCheck.hostname` | Value in the Host header of the health check request.
Evaluated only if the kind is **Service**.
Only for [Kubernetes service](https://kubernetes.io/docs/concepts/services-networking/service/) of type [ExternalName](#externalname-service). | "" | No | +| `healthCheck.`
`followRedirect`
| Follow the redirections during the healtchcheck.
Evaluated only if the kind is **Service**.
Only for [Kubernetes service](https://kubernetes.io/docs/concepts/services-networking/service/) of type [ExternalName](#externalname-service). | true | No | +| `healthCheck.headers` | Map of header to send to the health check endpoint
Evaluated only if the kind is **Service**.
Only for [Kubernetes service](https://kubernetes.io/docs/concepts/services-networking/service/) of type [ExternalName](#externalname-service)). | | No | +| `sticky.`
`cookie.name`
| Name of the cookie used for the stickiness.
When sticky sessions are enabled, a `Set-Cookie` header is set on the initial response to let the client know which server handles the first response.
On subsequent requests, to keep the session alive with the same server, the client should send the cookie with the value set.
If the server pecified in the cookie becomes unhealthy, the request will be forwarded to a new server (and the cookie will keep track of the new server).
Evaluated only if the kind is **Service**. | "" | No | +| `sticky.`
`cookie.httpOnly`
| Allow the cookie can be accessed by client-side APIs, such as JavaScript.
Evaluated only if the kind is **Service**. | false | No | +| `sticky.`
`cookie.secure`
| Allow the cookie can only be transmitted over an encrypted connection (i.e. HTTPS).
Evaluated only if the kind is **Service**. | false | No | +| `sticky.`
`cookie.sameSite`
| [SameSite](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie/SameSite) policy
Allowed values:
-`none`
-`lax`
`strict`
Evaluated only if the kind is **Service**. | "" | No | +| `sticky.`
`cookie.maxAge`
| Number of seconds until the cookie expires.
Negative number, the cookie expires immediately.
0, the cookie never expires.
Evaluated only if the kind is **Service**. | 0 | No | +| `strategy` | Load balancing strategy between the servers.
RoundRobin is the only supported value yet.
Evaluated only if the kind is **Service**. | "RoundRobin" | No | +| `nativeLB` | Allow using the Kubernetes Service load balancing between the pods instead of the one provided by Traefik.
Evaluated only if the kind is **Service**. | false | No | +| `nodePortLB` | Use the nodePort IP address when the service type is NodePort.
It allows services to be reachable when Traefik runs externally from the Kubernetes cluster but within the same network of the nodes.
Evaluated only if the kind is **Service**. | false | No | + + +### ExternalName Service + +Traefik backends creation needs a port to be set, however Kubernetes [ExternalName Service](https://kubernetes.io/docs/concepts/services-networking/service/#externalname) could be defined without any port. Accordingly, Traefik supports defining a port in two ways: + +- only on `IngressRoute` service +- on both sides, you'll be warned if the ports don't match, and the `IngressRoute` service port is used + +Thus, in case of two sides port definition, Traefik expects a match between ports. + +=== "Ports defined on Resource" + + ```yaml tab="IngressRoute" + apiVersion: traefik.io/v1alpha1 + kind: IngressRoute + metadata: + name: test.route + namespace: apps + + spec: + entryPoints: + - foo + routes: + - match: Host(`example.net`) + kind: Rule + services: + - name: external-svc + port: 80 + ``` + + ```yaml tab="Service ExternalName" + apiVersion: v1 + kind: Service + metadata: + name: external-svc + namespace: apps + + spec: + externalName: external.domain + type: ExternalName + ``` + +=== "Port defined on the Service" + + ```yaml tab="IngressRoute" + apiVersion: traefik.io/v1alpha1 + kind: IngressRoute + metadata: + name: test.route + namespace: apps + + spec: + entryPoints: + - foo + routes: + - match: Host(`example.net`) + kind: Rule + services: + - name: external-svc + ``` + + ```yaml tab="Service ExternalName" + apiVersion: v1 + kind: Service + metadata: + name: external-svc + namespace: apps + + spec: + externalName: external.domain + type: ExternalName + ports: + - port: 80 + ``` + +=== "Port defined on both sides" + + ```yaml tab="IngressRoute" + apiVersion: traefik.io/v1alpha1 + kind: IngressRoute + metadata: + name: test.route + namespace: apps + + spec: + entryPoints: + - foo + routes: + - match: Host(`example.net`) + kind: Rule + services: + - name: external-svc + port: 80 + ``` + + ```yaml tab="Service ExternalName" + apiVersion: v1 + kind: Service + metadata: + name: external-svc + namespace: apps + + spec: + externalName: external.domain + type: ExternalName + ports: + - port: 80 + ``` + +### Port Definition + +Traefik backends creation needs a port to be set, however Kubernetes [ExternalName Service](https://kubernetes.io/docs/concepts/services-networking/service/#externalname) could be defined without any port. Accordingly, Traefik supports defining a port in two ways: + +- only on `IngressRoute` service +- on both sides, you'll be warned if the ports don't match, and the `IngressRoute` service port is used + +Thus, in case of two sides port definition, Traefik expects a match between ports. + +??? example + + ```yaml tab="IngressRoute" + --- + apiVersion: traefik.io/v1alpha1 + kind: IngressRoute + metadata: + name: test.route + namespace: default + + spec: + entryPoints: + - foo + + routes: + - match: Host(`example.net`) + kind: Rule + services: + - name: external-svc + port: 80 + + --- + apiVersion: v1 + kind: Service + metadata: + name: external-svc + namespace: default + spec: + externalName: external.domain + type: ExternalName + ``` + + ```yaml tab="ExternalName Service" + --- + apiVersion: traefik.io/v1alpha1 + kind: IngressRoute + metadata: + name: test.route + namespace: default + + spec: + entryPoints: + - foo + + routes: + - match: Host(`example.net`) + kind: Rule + services: + - name: external-svc + + --- + apiVersion: v1 + kind: Service + metadata: + name: external-svc + namespace: default + spec: + externalName: external.domain + type: ExternalName + ports: + - port: 80 + ``` + + ```yaml tab="Both sides" + --- + apiVersion: traefik.io/v1alpha1 + kind: IngressRoute + metadata: + name: test.route + namespace: default + + spec: + entryPoints: + - foo + + routes: + - match: Host(`example.net`) + kind: Rule + services: + - name: external-svc + port: 80 + + --- + apiVersion: v1 + kind: Service + metadata: + name: external-svc + namespace: default + spec: + externalName: external.domain + type: ExternalName + ports: + - port: 80 + ``` + +### Load Balancing + +You can declare and use Kubernetes Service load balancing as detailed below: + +```yaml tab="IngressRoute" +apiVersion: traefik.io/v1alpha1 +kind: IngressRoute +metadata: + name: ingressroutebar + namespace: default + +spec: + entryPoints: + - web + routes: + - match: Host(`example.com`) && PathPrefix(`/foo`) + kind: Rule + services: + - name: svc1 + namespace: default + - name: svc2 + namespace: default +``` + +```yaml tab="K8s Service" +apiVersion: v1 +kind: Service +metadata: + name: svc1 + namespace: default + +spec: + ports: + - name: http + port: 80 + selector: + app: traefiklabs + task: app1 +--- +apiVersion: v1 +kind: Service +metadata: + name: svc2 + namespace: default + +spec: + ports: + - name: http + port: 80 + selector: + app: traefiklabs + task: app2 +``` + +!!! important "Kubernetes Service Native Load-Balancing" + + To avoid creating the server load-balancer with the pod IPs and use Kubernetes Service clusterIP directly, + one should set the service `NativeLB` option to true. + Please note that, by default, Traefik reuses the established connections to the backends for performance purposes. This can prevent the requests load balancing between the replicas from behaving as one would expect when the option is set. + By default, `NativeLB` is false. + + ??? example "Example" + + ```yaml + --- + apiVersion: traefik.io/v1alpha1 + kind: IngressRoute + metadata: + name: test.route + namespace: default + + spec: + entryPoints: + - foo + + routes: + - match: Host(`example.net`) + kind: Rule + services: + - name: svc + port: 80 + # Here, nativeLB instructs to build the server load-balancer with the Kubernetes Service clusterIP only. + nativeLB: true + + --- + apiVersion: v1 + kind: Service + metadata: + name: svc + namespace: default + spec: + type: ClusterIP + ... + ``` + +### Configuring Backend Protocol + +There are 3 ways to configure the backend protocol for communication between Traefik and your pods: + +- Setting the scheme explicitly (http/https/h2c) +- Configuring the name of the kubernetes service port to start with https (https) +- Setting the kubernetes service port to use port 443 (https) + +If you do not configure the above, Traefik will assume an http connection. diff --git a/docs/content/reference/routing-configuration/kubernetes/crd/http/traefikservice.md b/docs/content/reference/routing-configuration/kubernetes/crd/http/traefikservice.md index 8b3f18f3f..c89884777 100644 --- a/docs/content/reference/routing-configuration/kubernetes/crd/http/traefikservice.md +++ b/docs/content/reference/routing-configuration/kubernetes/crd/http/traefikservice.md @@ -151,36 +151,8 @@ data: | Field | Description | Default | Required | |:---------------------------------------------------------------|:---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:---------------------------------------------------------------------|:---------| -| `services` | List of any combination of TraefikService and [Kubernetes service](https://kubernetes.io/docs/concepts/services-networking/service/).
. | | No | -| `services[m].`
`kind`
| Kind of the service targeted.
Two values allowed:
- **Service**: Kubernetes Service
- **TraefikService**: Traefik Service. | "" | No | -| `services[m].`
`name`
| Service name.
The character `@` is not authorized. | "" | Yes | -| `services[m].`
`namespace`
| Service namespace. | "" | No | -| `services[m].`
`port`
| Service port (number or port name).
Evaluated only if the kind is **Service**. | "" | No | -| `services[m].`
`responseForwarding.`
`flushInterval`
| Interval, in milliseconds, in between flushes to the client while copying the response body.
A negative value means to flush immediately after each write to the client.
This configuration is ignored when a response is a streaming response; for such responses, writes are flushed to the client immediately.
Evaluated only if the kind is **Service**. | 100ms | No | -| `services[m].`
`scheme`
| Scheme to use for the request to the upstream Kubernetes Service.
Evaluated only if the kind is **Service**. | "http"
"https" if `port` is 443 or contains the string *https*. | No | -| `services[m].`
`serversTransport`
| Name of ServersTransport resource to use to configure the transport between Traefik and your servers.
Evaluated only if the kind is **Service**. | "" | No | -| `services[m].`
`passHostHeader`
| Forward client Host header to server.
Evaluated only if the kind is **Service**. | true | No | -| `services[m].`
`healthCheck.scheme`
| Server URL scheme for the health check endpoint.
Evaluated only if the kind is **Service**.
Only for [Kubernetes service](https://kubernetes.io/docs/concepts/services-networking/service/) of type `ExternalName`. | "" | No | -| `services[m].`
`healthCheck.mode`
| Health check mode.
If defined to grpc, will use the gRPC health check protocol to probe the server.
Evaluated only if the kind is **Service**.
Only for [Kubernetes service](https://kubernetes.io/docs/concepts/services-networking/service/) of type `ExternalName`. | "http" | No | -| `services[m].`
`healthCheck.path`
| Server URL path for the health check endpoint.
Evaluated only if the kind is **Service**.
Only for [Kubernetes service](https://kubernetes.io/docs/concepts/services-networking/service/) of type `ExternalName`. | "" | No | -| `services[m].`
`healthCheck.interval`
| Frequency of the health check calls for healthy targets.
Evaluated only if the kind is **Service**.
Only for [Kubernetes service](https://kubernetes.io/docs/concepts/services-networking/service/) of type [ExternalName]`ExternalName`. | "100ms" | No | -| `services[m].`
`healthCheck.unhealthyInterval`
| Frequency of the health check calls for unhealthy targets.
When not defined, it defaults to the `interval` value.
Evaluated only if the kind is **Service**.
Only for [Kubernetes service](https://kubernetes.io/docs/concepts/services-networking/service/) of type [ExternalName]`ExternalName`. | "100ms" | No | -| `services[m].`
`healthCheck.method`
| HTTP method for the health check endpoint.
Evaluated only if the kind is **Service**.
Only for [Kubernetes service](https://kubernetes.io/docs/concepts/services-networking/service/) of type `ExternalName`. | "GET" | No | -| `services[m].`
`healthCheck.status`
| Expected HTTP status code of the response to the health check request.
Only for [Kubernetes service](https://kubernetes.io/docs/concepts/services-networking/service/) of type ExternalName.
If not set, expect a status between 200 and 399.
Evaluated only if the kind is **Service**. | | No | -| `services[m].`
`healthCheck.port`
| URL port for the health check endpoint.
Evaluated only if the kind is **Service**.
Only for [Kubernetes service](https://kubernetes.io/docs/concepts/services-networking/service/) of type `ExternalName`. | | No | -| `services[m].`
`healthCheck.timeout`
| Maximum duration to wait before considering the server unhealthy.
Evaluated only if the kind is **Service**.
Only for [Kubernetes service](https://kubernetes.io/docs/concepts/services-networking/service/) of type `ExternalName`. | "5s" | No | -| `services[m].`
`healthCheck.hostname`
| Value in the Host header of the health check request.
Evaluated only if the kind is **Service**.
Only for [Kubernetes service](https://kubernetes.io/docs/concepts/services-networking/service/) of type `ExternalName`. | "" | No | -| `services[m].`
`healthCheck.`
`followRedirect`
| Follow the redirections during the healtchcheck.
Evaluated only if the kind is **Service**.
Only for [Kubernetes service](https://kubernetes.io/docs/concepts/services-networking/service/) of type `ExternalName`. | true | No | -| `services[m].`
`healthCheck.headers`
| Map of header to send to the health check endpoint
Evaluated only if the kind is **Service**.
Only for [Kubernetes service](https://kubernetes.io/docs/concepts/services-networking/service/) of type `ExternalName`. | | No | -| `services[m].`
`sticky.`
`cookie.name`
| Name of the cookie used for the stickiness.
Evaluated only if the kind is **Service**. | Abbreviation of a sha1
(ex: `_1d52e`). | No | -| `services[m].`
`sticky.`
`cookie.httpOnly`
| Allow the cookie can be accessed by client-side APIs, such as JavaScript.
Evaluated only if the kind is **Service**. | false | No | -| `services[m].`
`sticky.`
`cookie.secure`
| Allow the cookie can only be transmitted over an encrypted connection (i.e. HTTPS).
Evaluated only if the kind is **Service**. | false | No | -| `services[m].`
`sticky.`
`cookie.sameSite`
| [SameSite](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie/SameSite) policy.
Allowed values:
-`none`
-`lax`
`strict`
Evaluated only if the kind is **Service**. | "" | No | -| `services[m].`
`sticky.`
`cookie.maxAge`
| Number of seconds until the cookie expires.
Negative number, the cookie expires immediately.
0, the cookie never expires.
Evaluated only if the kind is **Service**. | 0 | No | -| `services[m].`
`strategy`
| Load balancing strategy between the servers.
RoundRobin is the only supported value yet.
Evaluated only if the kind is **Service**. | "RoundRobin" | No | -| `services[m].`
`weight`
| Service weight.
To use only to refer to WRR TraefikService | "" | No | -| `services[m].`
`nativeLB`
| Allow using the Kubernetes Service load balancing between the pods instead of the one provided by Traefik.
Evaluated only if the kind is **Service**. | false | No | -| `services[m].`
`nodePortLB`
| Use the nodePort IP address when the service type is NodePort.
It allows services to be reachable when Traefik runs externally from the Kubernetes cluster but within the same network of the nodes.
Evaluated only if the kind is **Service**. | false | No | +| `services` | List of any combination of TraefikService and [Kubernetes service](https://kubernetes.io/docs/concepts/services-networking/service/).
. Exhaustive list of option in the [`Service`](./service.md#configuration-options) documentation. | | No | +| `services[m].weight` | Service weight. | "" | No | | `sticky.`
`cookie.name`
| Name of the cookie used for the stickiness at the WRR service level.
When sticky sessions are enabled, a `Set-Cookie` header is set on the initial response to let the client know which server handles the first response.
On subsequent requests, to keep the session alive with the same server, the client should send the cookie with the value set.
If the server pecified in the cookie becomes unhealthy, the request will be forwarded to a new server (and the cookie will keep track of the new server).
More information about WRR stickiness [here](#stickiness-on-multiple-levels) | Abbreviation of a sha1
(ex: `_1d52e`). | No | | `sticky.`
`cookie.httpOnly`
| Allow the cookie used for the stickiness at the WRR service level to be accessed by client-side APIs, such as JavaScript.
More information about WRR stickiness [here](#stickiness-on-multiple-levels) | false | No | | `sticky.`
`cookie.secure`
| Allow the cookie used for the stickiness at the WRR service level to be only transmitted over an encrypted connection (i.e. HTTPS).
More information about WRR stickiness [here](#stickiness-on-multiple-levels) | false | No | @@ -443,6 +415,8 @@ spec: mirroring: name: svc1 # svc1 receives 100% of the traffic port: 80 + mirrorBody: true # Set to false by default + maxBodySize: 1M mirrors: - name: svc2 # svc2 receives a copy of 20% of this traffic port: 80 @@ -505,73 +479,31 @@ spec: ### Configuration Options -!!!note "Main and mirrored services" +#### Main Service Options - The main service properties are set as the option root level. +The main service properties are set as the option root level. - The mirrored services properties are set in the `mirrors` list. +The main service provides the same options as a [`Service`](./service.md). + +The exhaustive list of the service options is described in the [`Service`](./service.md#configuration-options) documentation. +The mirror main service dedicated option are described below. | Field | Description | Default | Required | |:--------------------------------------------------------------|:----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:---------------------------------------------------------------------|:---------| -| `kind` | Kind of the main service.
Two values allowed:
- **Service**: Kubernetes Service
- **TraefikService**: Traefik Service.
More information [here](#services) | "" | No | -| `name` | Main service name.
The character `@` is not authorized. | "" | Yes | -| `namespace` | Main service namespace.
More information [here](#services). | "" | No | -| `port` | Main service port (number or port name).
Evaluated only if the kind of the main service is **Service**. | "" | No | -| `responseForwarding.`
`flushInterval`
| Interval, in milliseconds, in between flushes to the client while copying the response body.
A negative value means to flush immediately after each write to the client.
This configuration is ignored when a response is a streaming response; for such responses, writes are flushed to the client immediately.
Evaluated only if the kind of the main service is **Service**. | 100ms | No | -| `scheme` | Scheme to use for the request to the upstream Kubernetes Service.
Evaluated only if the kind of the main service is **Service**. | "http"
"https" if `port` is 443 or contains the string *https*. | No | -| `serversTransport` | Name of ServersTransport resource to use to configure the transport between Traefik and the main service's servers.
Evaluated only if the kind of the main service is **Service**. | "" | No | -| `passHostHeader` | Forward client Host header to main service's server.
Evaluated only if the kind of the main service is **Service**. | true | No | -| `healthCheck.scheme` | Server URL scheme for the health check endpoint.
Evaluated only if the kind of the main service is **Service**.
Only for [Kubernetes service](https://kubernetes.io/docs/concepts/services-networking/service/) of type [ExternalName](#services). | "" | No | -| `healthCheck.mode` | Health check mode.
If defined to grpc, will use the gRPC health check protocol to probe the server.
Evaluated only if the kind of the main service is **Service**.
Only for [Kubernetes service](https://kubernetes.io/docs/concepts/services-networking/service/) of type [ExternalName](#services). | "http" | No | -| `healthCheck.path` | Server URL path for the health check endpoint.
Evaluated only if the kind of the main service is **Service**.
Only for [Kubernetes service](https://kubernetes.io/docs/concepts/services-networking/service/) of type [ExternalName](#services). | "" | No | -| `healthCheck.interval` | Frequency of the health check calls for healthy targets.
Evaluated only if the kind of the main service is **Service**.
Only for [Kubernetes service](https://kubernetes.io/docs/concepts/services-networking/service/) of type [ExternalName](#services). | "100ms" | No | -| `healthCheck.unhealthyInterval` | Frequency of the health check calls for unhealthy targets.
When not defined, it defaults to the `interval` value.
Evaluated only if the kind of the main service is **Service**.
Only for [Kubernetes service](https://kubernetes.io/docs/concepts/services-networking/service/) of type [ExternalName](#services). | "100ms" | No | -| `healthCheck.method` | HTTP method for the health check endpoint.
Evaluated only if the kind of the main service is **Service**.
Only for [Kubernetes service](https://kubernetes.io/docs/concepts/services-networking/service/) of type [ExternalName](#services). | "GET" | No | -| `healthCheck.status` | Expected HTTP status code of the response to the health check request.
Only for [Kubernetes service](https://kubernetes.io/docs/concepts/services-networking/service/) of type ExternalName.
If not set, expect a status between 200 and 399.
Evaluated only if the kind of the main service is **Service**. | | No | -| `healthCheck.port` | URL port for the health check endpoint.
Evaluated only if the kind of the main service is **Service**.
Only for [Kubernetes service](https://kubernetes.io/docs/concepts/services-networking/service/) of type [ExternalName](#services). | | No | -| `healthCheck.timeout` | Maximum duration to wait before considering the server unhealthy.
Evaluated only if the kind of the main service is **Service**.
Only for [Kubernetes service](https://kubernetes.io/docs/concepts/services-networking/service/) of type [ExternalName](#services). | "5s" | No | -| `healthCheck.hostname` | Value in the Host header of the health check request.
Evaluated only if the kind of the main service is **Service**.
Only for [Kubernetes service](https://kubernetes.io/docs/concepts/services-networking/service/) of type [ExternalName](#services). | "" | No | -| `healthCheck.`
`followRedirect`
| Follow the redirections during the healtchcheck.
Evaluated only if the kind of the main service is **Service**.
Only for [Kubernetes service](https://kubernetes.io/docs/concepts/services-networking/service/) of type [ExternalName](#services). | true | No | -| `healthCheck.headers` | Map of header to send to the health check endpoint
Evaluated only if the kind of the main service is **Service**.
Only for [Kubernetes service](https://kubernetes.io/docs/concepts/services-networking/service/) of type [ExternalName](#services). | | No | -| `sticky.`
`cookie.name`
| Name of the cookie used for the stickiness on the main service.
Evaluated only if the kind of the main service is **Service**. | Abbreviation of a sha1
(ex: `_1d52e`). | No | -| `sticky.`
`cookie.httpOnly`
| Allow the cookie can be accessed by client-side APIs, such as JavaScript.
Evaluated only if the kind of the main service is **Service**. | false | No | -| `sticky.`
`cookie.secure`
| Allow the cookie can only be transmitted over an encrypted connection (i.e. HTTPS).
Evaluated only if the kind of the main service is **Service**. | false | No | -| `sticky.`
`cookie.sameSite`
| [SameSite](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie/SameSite) policy.
Allowed values:
-`none`
-`lax`
`strict`
Evaluated only if the kind of the main service is **Service**. | "" | No | -| `sticky.`
`cookie.maxAge`
| Number of seconds until the cookie expires.
Negative number, the cookie expires immediately.
0, the cookie never expires.
Evaluated only if the kind of the main service is **Service**. | 0 | No | -| `strategy` | Load balancing strategy between the main service's servers.
RoundRobin is the only supported value yet.
Evaluated only if the kind of the main service is **Service**. | "RoundRobin" | No | -| `weight` | Service weight.
To use only to refer to WRR TraefikService | "" | No | -| `nativeLB` | Allow using the Kubernetes Service load balancing between the pods instead of the one provided by Traefik.
Evaluated only if the kind of the main service is **Service**. | false | No | -| `nodePortLB` | Use the nodePort IP address when the service type is NodePort.
It allows services to be reachable when Traefik runs externally from the Kubernetes cluster but within the same network of the nodes.
Evaluated only if the kind of the main service is **Service**. | false | No | -| `maxBodySize` | Maximum size allowed for the body of the request.
If the body is larger, the request is not mirrored.
-1 means unlimited size. | -1 | No | -| `mirrors` | List of mirrored services to target.
It can be any combination of TraefikService and [Kubernetes service](https://kubernetes.io/docs/concepts/services-networking/service/).
More information [here](#services). | | No | -| `mirrors[m].`
`kind`
| Kind of the mirrored service targeted.
Two values allowed:
- **Service**: Kubernetes Service
- **TraefikService**: Traefik Service.
More information [here](#services) | "" | No | -| `mirrors[m].`
`name`
| Mirrored service name.
The character `@` is not authorized. | "" | Yes | -| `mirrors[m].`
`namespace`
| Mirrored service namespace.
More information [here](#services). | "" | No | -| `mirrors[m].`
`port`
| Mirrored service port (number or port name).
Evaluated only if the kind of the mirrored service is **Service**. | "" | No | -| `mirrors[m].`
`percent`
| Part of the traffic to mirror in percent (from 0 to 100) | 0 | No | -| `mirrors[m].`
`responseForwarding.`
`flushInterval`
| Interval, in milliseconds, in between flushes to the client while copying the response body.
A negative value means to flush immediately after each write to the client.
This configuration is ignored when a response is a streaming response; for such responses, writes are flushed to the client immediately.
Evaluated only if the kind of the mirrored service is **Service**. | 100ms | No | -| `mirrors[m].`
`scheme`
| Scheme to use for the request to the mirrored service.
Evaluated only if the kind of the mirrored service is **Service**. | "http"
"https" if `port` is 443 or contains the string *https*. | No | -| `mirrors[m].`
`serversTransport`
| Name of ServersTransport resource to use to configure the transport between Traefik and the mirrored service servers.
Evaluated only if the kind of the mirrored service is **Service**. | "" | No | -| `mirrors[m].`
`passHostHeader`
| Forward client Host header to the mirrored service servers.
Evaluated only if the kind of the mirrored service is **Service**. | true | No | -| `mirrors[m].`
`healthCheck.scheme`
| Server URL scheme for the health check endpoint.
Evaluated only if the kind of the mirrored service is **Service**.
Only for [Kubernetes service](https://kubernetes.io/docs/concepts/services-networking/service/) of type [ExternalName](#services). | "" | No | -| `mirrors[m].`
`healthCheck.mode`
| Health check mode.
If defined to grpc, will use the gRPC health check protocol to probe the server.
Evaluated only if the kind of the mirrored service is **Service**.
Only for [Kubernetes service](https://kubernetes.io/docs/concepts/services-networking/service/) of type [ExternalName](#services). | "http" | No | -| `mirrors[m].`
`healthCheck.path`
| Server URL path for the health check endpoint.
Evaluated only if the kind of the mirrored service is **Service**.
Only for [Kubernetes service](https://kubernetes.io/docs/concepts/services-networking/service/) of type [ExternalName](#services). | "" | No | -| `mirrors[m].`
`healthCheck.interval`
| Frequency of the health check calls.
Evaluated only if the kind of the mirrored service is **Service**.
Only for [Kubernetes service](https://kubernetes.io/docs/concepts/services-networking/service/) of type [ExternalName](#services). | "100ms" | No | -| `mirrors[m].`
`healthCheck.unhealthyInterval`
| Frequency of the health check calls for unhealthy targets.
When not defined, it defaults to the `interval` value.
Evaluated only if the kind of the mirrored service is **Service**.
Only for [Kubernetes service](https://kubernetes.io/docs/concepts/services-networking/service/) of type [ExternalName](#services). | "100ms" | No | -| `mirrors[m].`
`healthCheck.method`
| HTTP method for the health check endpoint.
Evaluated only if the kind of the mirrored service is **Service**.
Only for [Kubernetes service](https://kubernetes.io/docs/concepts/services-networking/service/) of type [ExternalName](#services). | "GET" | No | -| `mirrors[m].`
`healthCheck.status`
| Expected HTTP status code of the response to the health check request.
Only for [Kubernetes service](https://kubernetes.io/docs/concepts/services-networking/service/) of type ExternalName.
If not set, expect a status between 200 and 399.
Evaluated only if the kind of the mirrored service is **Service**. | | No | -| `mirrors[m].`
`healthCheck.port`
| URL port for the health check endpoint.
Evaluated only if the kind of the mirrored service is **Service**.
Only for [Kubernetes service](https://kubernetes.io/docs/concepts/services-networking/service/) of type [ExternalName](#services). | | No | -| `mirrors[m].`
`healthCheck.timeout`
| Maximum duration to wait before considering the server unhealthy.
Evaluated only if the kind of the mirrored service is **Service**.
Only for [Kubernetes service](https://kubernetes.io/docs/concepts/services-networking/service/) of type [ExternalName](#services). | "5s" | No | -| `mirrors[m].`
`healthCheck.hostname`
| Value in the Host header of the health check request.
Evaluated only if the kind of the mirrored service is **Service**.
Only for [Kubernetes service](https://kubernetes.io/docs/concepts/services-networking/service/) of type [ExternalName](#services). | "" | No | -| `mirrors[m].`
`healthCheck.`
`followRedirect`
| Follow the redirections during the healtchcheck.
Evaluated only if the kind of the mirrored service is **Service**.
Only for [Kubernetes service](https://kubernetes.io/docs/concepts/services-networking/service/) of type [ExternalName](#services). | true | No | -| `mirrors[m].`
`healthCheck.headers`
| Map of header to send to the health check endpoint
Evaluated only if the kind of the mirrored service is **Service**.
Only for [Kubernetes service](https://kubernetes.io/docs/concepts/services-networking/service/) of type [ExternalName](#services). | | No | -| `mirrors[m].`
`sticky.`
`cookie.name`
| Name of the cookie used for the stickiness.
When sticky sessions are enabled, a `Set-Cookie` header is set on the initial response to let the client know which server handles the first response.
On subsequent requests, to keep the session alive with the same server, the client should send the cookie with the value set.
If the server pecified in the cookie becomes unhealthy, the request will be forwarded to a new server (and the cookie will keep track of the new server).
Evaluated only if the kind of the mirrored service is **Service**. | "" | No | -| `mirrors[m].`
`sticky.`
`cookie.httpOnly`
| Allow the cookie can be accessed by client-side APIs, such as JavaScript.
Evaluated only if the kind of the mirrored service is **Service**. | false | No | -| `mirrors[m].`
`sticky.`
`cookie.secure`
| Allow the cookie can only be transmitted over an encrypted connection (i.e. HTTPS).
Evaluated only if the kind of the mirrored service is **Service**. | false | No | -| `mirrors[m].`
`sticky.`
`cookie.sameSite`
| [SameSite](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie/SameSite) policy.
Allowed values:
-`none`
-`lax`
`strict`
Evaluated only if the kind of the mirrored service is **Service**. | "" | No | -| `mirrors[m].`
`sticky.`
`cookie.maxAge`
| Number of seconds until the cookie expires.
Negative number, the cookie expires immediately.
0, the cookie never expires.
Evaluated only if the kind of the mirrored service is **Service**. | 0 | No | -| `mirrors[m].`
`strategy`
| Load balancing strategy between the servers.
RoundRobin is the only supported value yet.
Evaluated only if the kind of the mirrored service is **Service**. | "RoundRobin" | No | -| `mirrors[m].`
`weight`
| Service weight.
To use only to refer to WRR TraefikService | "" | No | -| `mirrors[m].`
`nativeLB`
| Allow using the Kubernetes Service load balancing between the pods instead of the one provided by Traefik.
Evaluated only if the kind of the mirrored service is **Service**. | false | No | -| `mirrors[m].`
`nodePortLB`
| Use the nodePort IP address when the service type is NodePort.
It allows services to be reachable when Traefik runs externally from the Kubernetes cluster but within the same network of the nodes.
Evaluated only if the kind of the mirrored service is **Service**. | false | No | | `mirrorBody` | Defines whether the request body should be mirrored. | true | No | +| `maxBodySize` | Maximum size allowed for the body of the request.
If the body is larger, the request is not mirrored.
-1 means unlimited size. | -1 | No | +| `mirrors` | List of mirrored services to target.
It can be any combination of TraefikService and [Kubernetes service](https://kubernetes.io/docs/concepts/services-networking/service/).
Exhaustive list of option in the [`Service`](./service.md#configuration-options) documentation. | | Yes | + +#### Mirrored Services Options + +The mirrored services properties are set in the `mirrors` list. + +A mirrored service provides the same options as a [`Service`](./service.md). + +The exhaustive list of the service options is described in the [`Service`](./service.md#configuration-options) documentation. +The mirrorerd service dedicated option are described below. + + +| Field | Description | Default | Required | +|:--------------------------------------------------------------|:----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:---------------------------------------------------------------------|:---------| +| `mirrors[m].percent` | Traffic percentage to route to the service. | 0 | No | diff --git a/docs/content/reference/routing-configuration/kubernetes/crd/tcp/ingressroutetcp.md b/docs/content/reference/routing-configuration/kubernetes/crd/tcp/ingressroutetcp.md index 061ed1615..47d595e1e 100644 --- a/docs/content/reference/routing-configuration/kubernetes/crd/tcp/ingressroutetcp.md +++ b/docs/content/reference/routing-configuration/kubernetes/crd/tcp/ingressroutetcp.md @@ -57,33 +57,6 @@ spec: ## Configuration Options -<<<<<<< HEAD -| Field | Description | Default | Required | -|-----------------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|---------|----------| -| `entryPoints` | List of entrypoints names. | | No | -| `routes` | List of routes. | | Yes | -| `routes[n].match` | Defines the [rule](../../../tcp/router/rules-and-priority.md#rules) of the underlying router. | | Yes | -| `routes[n].priority` | Defines the [priority](../../../tcp/router/rules-and-priority.md#priority) to disambiguate rules of the same length, for route matching. | | No | -| `routes[n].middlewares[n].name` | Defines the [MiddlewareTCP](./middlewaretcp.md) name. | | Yes | -| `routes[n].middlewares[n].namespace` | Defines the [MiddlewareTCP](./middlewaretcp.md) namespace. | "" | No | -| `routes[n].services` | List of [Kubernetes service](https://kubernetes.io/docs/concepts/services-networking/service/) definitions. | | No | -| `routes[n].services[n].name` | Defines the name of a [Kubernetes service](https://kubernetes.io/docs/concepts/services-networking/service/). | | Yes | -| `routes[n].services[n].port` | Defines the port of a [Kubernetes service](https://kubernetes.io/docs/concepts/services-networking/service/). This can be a reference to a named port. | | Yes | -| `routes[n].services[n].weight` | Defines the weight to apply to the server load balancing. | 1 | No | -| `routes[n].services[n].serversTransport` | Defines the [ServersTransportTCP](./serverstransporttcp.md).
The `ServersTransport` namespace is assumed to be the [Kubernetes service](https://kubernetes.io/docs/concepts/services-networking/service/) namespace. | | No | -| `routes[n].services[n].nativeLB` | Controls, when creating the load-balancer, whether the LB's children are directly the pods IPs or if the only child is the Kubernetes Service clusterIP. See [here](#nativelb) for more information. | false | No | -| `routes[n].services[n].nodePortLB` | Controls, when creating the load-balancer, whether the LB's children are directly the nodes internal IPs using the nodePort when the service type is `NodePort`. It allows services to be reachable when Traefik runs externally from the Kubernetes cluster but within the same network of the nodes. | false | No | -| `tls` | Defines [TLS](../../../../install-configuration/tls/certificate-resolvers/overview.md) certificate configuration. | | No | -| `tls.secretName` | Defines the [secret](https://kubernetes.io/docs/concepts/configuration/secret/) name used to store the certificate (in the `IngressRoute` namespace). | "" | No | -| `tls.options` | Defines the reference to a [TLSOption](../http/tlsoption.md). | "" | No | -| `tls.options.name` | Defines the [TLSOption](../http/tlsoption.md) name. | "" | No | -| `tls.options.namespace` | Defines the [TLSOption](../http/tlsoption.md) namespace. | "" | No | -| `tls.certResolver` | Defines the reference to a [CertResolver](../../../../install-configuration/tls/certificate-resolvers/overview.md). | "" | No | -| `tls.domains` | List of domains. | "" | No | -| `tls.domains[n].main` | Defines the main domain name. | "" | No | -| `tls.domains[n].sans` | List of SANs (alternative domains). | "" | No | -| `tls.passthrough` | If `true`, delegates the TLS termination to the backend. | false | No | -======= | Field | Description | Default | Required | |-------------------------------------|-----------------------------|-------------------------------------------|-----------------------| | `entryPoints` | List of entrypoints names. | | No | @@ -111,7 +84,6 @@ spec: | `tls.domains[n].main` | Defines the main domain name. | "" | No | | `tls.domains[n].sans` | List of SANs (alternative domains). | "" | No | | `tls.passthrough` | If `true`, delegates the TLS termination to the backend. | false | No | ->>>>>>> 9c932124f (Add anchors in reference tables) ### ExternalName Service diff --git a/docs/content/secure/secure-api-access-with-jwt.md b/docs/content/secure/secure-api-access-with-jwt.md new file mode 100644 index 000000000..5e853a637 --- /dev/null +++ b/docs/content/secure/secure-api-access-with-jwt.md @@ -0,0 +1,204 @@ +--- +title: 'Secure API Access with JWT' +description: 'Traefik Hub API Gateway - Learn how to configure the JWT Authentication middleware for Ingress management.' +--- + +# Secure API Access with JWT + +!!! info "Traefik Hub Feature" + This middleware is available exclusively in [Traefik Hub](https://traefik.io/traefik-hub/). Learn more about [Traefik Hub's advanced features](https://doc.traefik.io/traefik-hub/api-gateway/intro). + +JSON Web Token (JWT) (defined in the [RFC 7519](https://tools.ietf.org/html/rfc7519)) allows +Traefik Hub API Gateway to secure the API access using a token signed using either a private signing secret or a plublic/private key. + +Traefik Hub API Gateway provides many kinds of sources to perform the token validation: + +- Setting a secret value in the middleware configuration (option `signingSecret`). +- Setting a public key: In that case, users should sign their token using a private key, and the public key can be used to verify the signature (option `publicKey`). +- Setting a [JSON Web Key (JWK)](https://datatracker.ietf.org/doc/html/rfc7517) file to define a set of JWK to be used to verify the signature of the incoming JWT (option `jwksFile`). +- Setting a [JSON Web Key (JWK)](https://datatracker.ietf.org/doc/html/rfc7517) URL to define the URL of the host serving a JWK set (option `jwksUrl`). + +!!! note "One single source" + The JWT middleware does not allow you to set more than one way to validate the incoming tokens. + When a Hub API Gateway receives a request that must be validated using the JWT middleware, it verifies the token using the source configured as described above. + If the token is successfully checked, the request is accepted. + +!!! note "Claim Usage" + A JWT can contain metadata in the form of claims (key-value pairs). + The claims contained in the JWT can be used for advanced use-cases such as adding an Authorization layer using the `claims`. + + More information in the [dedicated section](../reference/routing-configuration/http/middlewares/jwt.md#claims). + +## Verify a JWT with a secret + +To allow the Traefik Hub API Gateway to validate a JWT with a secret value stored in a Kubernetes Secret, apply the following configuration: + +```yaml tab="Middleware JWT" +apiVersion: traefik.io/v1alpha1 +kind: Middleware +metadata: + name: test-jwt + namespace: apps +spec: + plugin: + jwt: + signingSecret: "urn:k8s:secret:jwt:signingSecret" +``` + +```yaml tab="Kubernetes Secret" +apiVersion: v1 +kind: Secret +metadata: + name: jwt + namespace: apps +stringData: + signingSecret: mysuperlongsecret +``` + +```yaml tab="IngressRoute" +apiVersion: traefik.io/v1alpha1 +kind: IngressRoute +metadata: + name: my-app + namespace: apps +spec: + entryPoints: + - websecure + routes: + - match: Path(`/my-app`) + kind: Rule + services: + - name: whoami + port: 80 + middlewares: + - name: test-jwt +``` + +```yaml tab="Service & Deployment" +kind: Deployment +apiVersion: apps/v1 +metadata: + name: whoami + namespace: apps +spec: + replicas: 3 + selector: + matchLabels: + app: whoami + template: + metadata: + labels: + app: whoami + spec: + containers: + - name: whoami + image: traefik/whoami + +--- +apiVersion: v1 +kind: Service +metadata: + name: whoami + namespace: apps +spec: + ports: + - port: 80 + name: whoami + selector: + app: whoami +``` + +## Verify a JWT using an Identity Provider + +To allow the Traefik Hub API Gateway to validate a JWT using an Identity Provider, such as Keycloak and Azure AD in the examples below, apply the following configuration: + +```yaml tab="JWKS with Keycloak URL" +apiVersion: traefik.io/v1alpha1 +kind: Middleware +metadata: + name: test-jwt + namespace: apps +spec: + plugin: + jwt: + # Replace KEYCLOAK_URL and REALM_NAME with your values + jwksUrl: https://KEYCLOAK_URL/realms/REALM_NAME/protocol/openid-connect/certs + # Forward the content of the claim grp in the header Group + forwardHeaders: + Group: grp + # Check the value of the claim grp before sending the request to the backend + claims: Equals(`grp`, `admin`) +``` + +```yaml tab="JWKS with Azure AD URL" +apiVersion: traefik.io/v1alpha1 +kind: Middleware +metadata: + name: test-jwt + namespace: apps +spec: + plugin: + jwt: + jwksUrl: https://login.microsoftonline.com/common/discovery/v2.0/keys +``` + +```yaml tab="IngressRoute" +apiVersion: traefik.io/v1alpha1 +kind: IngressRoute +metadata: + name: my-app + namespace: apps +spec: + entryPoints: + - websecure + routes: + - match: Path(`/my-app`) + kind: Rule + services: + - name: whoami + port: 80 + middlewares: + - name: test-jwt +``` + +```yaml tab="Service & Deployment" +kind: Deployment +apiVersion: apps/v1 +metadata: + name: whoami + namespace: apps +spec: + replicas: 3 + selector: + matchLabels: + app: whoami + template: + metadata: + labels: + app: whoami + spec: + containers: + - name: whoami + image: traefik/whoami + +--- +apiVersion: v1 +kind: Service +metadata: + name: whoami + namespace: apps +spec: + ports: + - port: 80 + name: whoami + selector: + app: whoami +``` + +!!! note "Advanced Configuration" + Advanced options are described in the [reference page](../reference/routing-configuration/http/middlewares/jwt.md). + + For example, the metadata recovered from the Identity Provider can be used to restrict the access to the applications. + To do so, you can use the `claims` option, more information in the [dedicated section](../reference/routing-configuration/http/middlewares/jwt.md#claims). + +{!traefik-for-business-applications.md!} diff --git a/docs/content/secure/secure-api-access-with-oidc.md b/docs/content/secure/secure-api-access-with-oidc.md new file mode 100644 index 000000000..f7b18def9 --- /dev/null +++ b/docs/content/secure/secure-api-access-with-oidc.md @@ -0,0 +1,110 @@ +--- +title: 'Secure API Access with OIDC' +description: 'Traefik Hub API Gateway - The OIDC Authentication middleware secures your applications by delegating the authentication to an external provider.' +--- + +# Secure API Access with OIDC + +!!! info "Traefik Hub Feature" + This middleware is available exclusively in [Traefik Hub](https://traefik.io/traefik-hub/). Learn more about [Traefik Hub's advanced features](https://doc.traefik.io/traefik-hub/api-gateway/intro). + +OpenID Connect Authentication is built on top of the OAuth2 Authorization Code Flow (defined in [OAuth 2.0 RFC 6749, section 4.1](https://tools.ietf.org/html/rfc6749#section-4.1)). +It allows an application to be secured by delegating authentication to an external provider (Keycloak, Okta etc.) +and obtaining the end user's session claims and scopes for authorization purposes. + +To authenticate the user, the middleware redirects through the authentication provider. +Once the authentication is complete, users are redirected back to the middleware before being authorized to access the upstream application, as described in the diagram below: + +![OpenID Connect authentication flow](../assets/img/secure/oidc-auth-flow.png) + +
+ +To allow the OIDC Middleware to use the credentials provided by the requests, apply the following configuration: + +```yaml tab="Middleware OIDC" +apiVersion: traefik.io/v1alpha1 +kind: Middleware +metadata: + name: oidc-login + namespace: apps +spec: + plugin: + oidc: + issuer: MY_ISSUER_URL + clientId: "urn:k8s:secret:oidc-client:client_id" + clientSecret: "urn:k8s:secret:oidc-client:client_secret" + redirectUrl: /oidc/callback +``` + +```yaml tab="Kubernetes Secrets" +apiVersion: v1 +kind: Secret +metadata: + name: oidc-client +stringData: + client_id: my-oauth-client-ID # Set your ClientID here + client_secret: my-oauth-client-secret # Set your client secret here +``` + +```yaml tab="IngressRoute" +apiVersion: traefik.io/v1alpha1 +kind: IngressRoute +metadata: + name: secure-applications-apigateway-oauth2-client-credentials + namespace: apps +spec: + entryPoints: + - websecure + routes: + - match: Path(`/my-app`) + kind: Rule + services: + - name: whoami + port: 80 + middlewares: + - name: oidc-login +``` + +```yaml tab="Service & Deployment" +kind: Deployment +apiVersion: apps/v1 +metadata: + name: whoami + namespace: apps +spec: + replicas: 3 + selector: + matchLabels: + app: whoami + template: + metadata: + labels: + app: whoami + spec: + containers: + - name: whoami + image: traefik/whoami + +--- +apiVersion: v1 +kind: Service +metadata: + name: whoami + namespace: apps +spec: + ports: + - port: 80 + name: whoami + selector: + app: whoami +``` + +!!! note "Advanced Configuration" + + Advanced options are described in the [reference page](../reference/routing-configuration/http/middlewares/oidc.md). + + For example, you can find how to customize the session storage: + - Using a cookie ([Options `session`](../reference/routing-configuration/http/middlewares/oidc.md#configuration-options) (default behavior)) + - Using a [Redis store](../reference/routing-configuration/http/middlewares/oidc.md#sessionstore). + +{!traefik-for-business-applications.md!} diff --git a/docs/content/secure/secure-api-access-with-waf.md b/docs/content/secure/secure-api-access-with-waf.md new file mode 100644 index 000000000..455839b99 --- /dev/null +++ b/docs/content/secure/secure-api-access-with-waf.md @@ -0,0 +1,190 @@ +--- +title: 'Secure API Access with WAF' +description: 'Traefik Hub API Gateway - Learn how to configure the Coraza Web Application Firewall middleware to protect your applications from common web attacks.' +--- + +# Secure API Access with WAF + +!!! info "Traefik Hub Feature" + This middleware is available exclusively in [Traefik Hub](https://traefik.io/traefik-hub/). Learn more about [Traefik Hub's advanced features](https://doc.traefik.io/traefik-hub/api-gateway/intro). + +The [Coraza Web Application Firewall](https://coraza.io/) middleware in Traefik Hub API Gateway provides comprehensive protection against common web application attacks. The middleware supports the Coraza rule syntax and is compatible with [OWASP Core Rule Set (CRS)](https://coreruleset.org/docs/), allowing you to leverage proven security rules maintained by the security community. + +## Basic WAF Protection + +To protect your applications with custom security rules, apply the following configuration: + +```yaml tab="Middleware WAF" +apiVersion: traefik.io/v1alpha1 +kind: Middleware +metadata: + name: waf-protection + namespace: apps +spec: + plugin: + coraza: + directives: + - SecRuleEngine On + - SecRule REQUEST_URI "@streq /admin" "id:101,phase:1,t:lowercase,log,deny" + - SecRule ARGS "@detectSQLi" "id:102,phase:2,block,msg:'SQL Injection Attack Detected',logdata:'Matched Data: %{MATCHED_VAR} found within %{MATCHED_VAR_NAME}'" +``` + +This configuration implements three security directives that work together to protect an application: + +- **SecRuleEngine On**: Activates the WAF engine to begin processing incoming requests. Without this directive, all other rules remain inactive regardless of their configuration. + +- **Admin Path Protection**: The second rule blocks all access to `/admin` paths by examining the request URI. This completely prevents access to administrative interfaces that often contain sensitive functionality like user management, system configuration, or database administration tools. The rule triggers during phase 1 (request headers processing) and applies lowercase transformation to catch variations like `/Admin` or `/ADMIN`. + +- **SQL Injection Detection**: The third rule scans request parameters (query strings and form data) for SQL injection patterns using Coraza's built-in detection engine. The `ARGS` variable covers query string parameters like `?id=1` and form data from POST requests like `username=admin&password=123`, but does not include cookies. SQL injection attacks attempt to manipulate database queries by injecting malicious SQL code through user inputs. When detected, the rule blocks the request and logs detailed information about the attempted attack, including which parameter contained the malicious payload. + +```yaml tab="IngressRoute" +apiVersion: traefik.io/v1alpha1 +kind: IngressRoute +metadata: + name: protected-app + namespace: apps +spec: + entryPoints: + - websecure + routes: + - match: Path(`/my-app`) + kind: Rule + services: + - name: whoami + port: 80 + middlewares: + - name: waf-protection +``` + +```yaml tab="Service & Deployment" +kind: Deployment +apiVersion: apps/v1 +metadata: + name: whoami + namespace: apps +spec: + replicas: 3 + selector: + matchLabels: + app: whoami + template: + metadata: + labels: + app: whoami + spec: + containers: + - name: whoami + image: traefik/whoami + +--- +apiVersion: v1 +kind: Service +metadata: + name: whoami + namespace: apps +spec: + ports: + - port: 80 + name: whoami + selector: + app: whoami +``` + +## Advanced Protection with OWASP Core Rule Set + +To implement comprehensive protection using the OWASP Core Rule Set, which provides battle-tested rules against common attack patterns, apply the following configuration: + +```yaml tab="Middleware WAF with CRS" +apiVersion: traefik.io/v1alpha1 +kind: Middleware +metadata: + name: waf-crs-protection + namespace: apps +spec: + plugin: + coraza: + crsEnabled: true + directives: + - SecRuleEngine On + - SecDefaultAction "phase:1,log,auditlog,deny,status:403" + - SecDefaultAction "phase:2,log,auditlog,deny,status:403" + - SecAction "id:900110, phase:1, pass, t:none, nolog, setvar:tx.inbound_anomaly_score_threshold=5, setvar:tx.outbound_anomaly_score_threshold=4" + - SecAction "id:900200, phase:1, pass, t:none, nolog, setvar:'tx.allowed_methods=GET POST'" + - Include @owasp_crs/REQUEST-911-METHOD-ENFORCEMENT.conf + - Include @owasp_crs/REQUEST-949-BLOCKING-EVALUATION.conf +``` + +This advanced configuration implements [OWASP Core Rule Set (CRS)](https://coreruleset.org/docs/) protection with anomaly scoring: + +- **SecDefaultAction for Phase 1 & 2**: Sets default behavior for request processing phases. Phase 1 processes request headers while Phase 2 processes request body. When rules match, they log the event to both standard and audit logs, then deny the request with a 403 status code. + +- **Anomaly Score Configuration**: The first `SecAction` sets anomaly score thresholds where `inbound_anomaly_score_threshold=5` means requests scoring 5 or higher are blocked, and `outbound_anomaly_score_threshold=4` applies the same logic to responses. This scoring system allows multiple suspicious patterns to accumulate points rather than blocking on first detection, reducing false positives while maintaining security. + +- **Allowed Methods Configuration**: The second `SecAction` restricts HTTP methods to only `GET` and `POST` requests. This prevents potentially dangerous methods like `PUT`, `DELETE`, `PATCH`, or `OPTIONS` that could modify server resources or reveal system information. + +- **METHOD-ENFORCEMENT Rule Set**: The `REQUEST-911-METHOD-ENFORCEMENT.conf` file enforces the allowed HTTP methods policy defined above. It checks incoming requests against the permitted methods and contributes to the anomaly score for disallowed methods. + +- **BLOCKING-EVALUATION Rule Set**: The `REQUEST-949-BLOCKING-EVALUATION.conf` file evaluates the accumulated anomaly score against the configured thresholds. If the total score exceeds the threshold, it triggers the blocking action, preventing the request from reaching your application. + +```yaml tab="IngressRoute" +apiVersion: traefik.io/v1alpha1 +kind: IngressRoute +metadata: + name: crs-protected-app + namespace: apps +spec: + entryPoints: + - websecure + routes: + - match: Path(`/my-app`) + kind: Rule + services: + - name: whoami + port: 80 + middlewares: + - name: waf-crs-protection +``` + +```yaml tab="Service & Deployment" +kind: Deployment +apiVersion: apps/v1 +metadata: + name: whoami + namespace: apps +spec: + replicas: 3 + selector: + matchLabels: + app: whoami + template: + metadata: + labels: + app: whoami + spec: + containers: + - name: whoami + image: traefik/whoami + +--- +apiVersion: v1 +kind: Service +metadata: + name: whoami + namespace: apps +spec: + ports: + - port: 80 + name: whoami + selector: + app: whoami +``` + +!!! warning + Starting with Traefik Hub v3.11.0, Coraza requires read/write permissions to `/tmp`. This requirement stems from upstream changes in the Coraza engine. + +!!! note "Advanced Configuration" + Advanced options and detailed rule configuration are described in the [reference page](../reference/routing-configuration/http/middlewares/waf.md). + + The WAF middleware supports extensive customization through Coraza directives. You can create custom rules, tune detection thresholds, configure logging levels, and integrate with external threat intelligence feeds. For comprehensive rule writing guidance, consult the [Coraza documentation](https://coraza.io/docs/tutorials/introduction/) and [OWASP CRS documentation](https://coreruleset.org/docs/). + +{!traefik-for-business-applications.md!} diff --git a/docs/content/user-guides/docker-compose/acme-tls/index.md b/docs/content/user-guides/docker-compose/acme-tls/index.md index 382244c62..942d17cc1 100644 --- a/docs/content/user-guides/docker-compose/acme-tls/index.md +++ b/docs/content/user-guides/docker-compose/acme-tls/index.md @@ -6,7 +6,7 @@ description: "Learn how to create a certificate with the Let's Encrypt TLS chall # Docker-compose with Let's Encrypt: TLS Challenge This guide aims to demonstrate how to create a certificate with the Let's Encrypt TLS challenge to use https on a simple service exposed with Traefik. -Please also read the [basic example](../basic-example) for details on how to expose such a service. +Please also read the [basic example](../basic-example/) for details on how to expose such a service. ## Prerequisite diff --git a/docs/mkdocs.yml b/docs/mkdocs.yml index e1ccb61ee..addd5ba99 100644 --- a/docs/mkdocs.yml +++ b/docs/mkdocs.yml @@ -129,6 +129,13 @@ plugins: 'middlewares/tcp/inflightconn.md': 'reference/routing-configuration/tcp/middlewares/inflightconn.md' 'middlewares/tcp/ipwhitelist.md': 'reference/routing-configuration/tcp/middlewares/ipallowlist.md' 'middlewares/tcp/ipallowlist.md': 'reference/routing-configuration/tcp/middlewares/ipallowlist.md' + ## User Guides + 'user-guides/crd-acme/index.md': 'expose/kubernetes.md' + 'user-guides/cert-manager.md': 'expose/kubernetes.md' + 'user-guides/docker-compose/basic-example/index.md': 'expose/docker.md' + 'user-guides/docker-compose/acme-tls/index.md': 'expose/docker.md' + 'user-guides/docker-compose/acme-http/index.md': 'expose/docker.md' + 'user-guides/docker-compose/acme-dns/index.md': 'expose/docker.md' # References # Static Configuration 'reference/static-configuration/overview.md': 'reference/install-configuration/configuration-options.md' @@ -193,12 +200,17 @@ nav: - 'Kubernetes': 'expose/kubernetes.md' - 'Docker': 'expose/docker.md' - 'Swarm': 'expose/swarm.md' + - 'Secure': + - 'Secure Access with JWT Traefik Hub API Gateway': 'secure/secure-api-access-with-jwt.md' + - 'Secure Access with OIDC Traefik Hub API Gateway': 'secure/secure-api-access-with-oidc.md' + - 'Secure Access with a WAF Traefik Hub API Gateway': 'secure/secure-api-access-with-waf.md' - 'Observe': - 'Overview': 'observe/overview.md' - 'Logs & Access Logs': 'observe/logs-and-access-logs.md' - 'Metrics': 'observe/metrics.md' - 'Tracing': 'observe/tracing.md' - 'Extend': 'extend/extend-traefik.md' + - 'Govern Traefik Hub API Gateway': 'govern/index.md' - 'Migrate': - 'Traefik v3 minor migrations': 'migrate/v3.md' - 'Traefik v2 to v3': @@ -247,7 +259,7 @@ nav: - 'Health Check (CLI & Ping)': 'reference/install-configuration/observability/healthcheck.md' - 'Options List': 'reference/install-configuration/configuration-options.md' - 'Routing Configuration': - - 'General' : + - 'Common Configuration' : - 'Configuration Methods' : 'reference/routing-configuration/dynamic-configuration-methods.md' - 'HTTP' : - 'Router' : @@ -314,6 +326,7 @@ nav: - 'Kubernetes CRD' : - 'HTTP' : - 'IngressRoute' : 'reference/routing-configuration/kubernetes/crd/http/ingressroute.md' + - 'Service' : 'reference/routing-configuration/kubernetes/crd/http/service.md' - 'TraefikService' : 'reference/routing-configuration/kubernetes/crd/http/traefikservice.md' - 'ServersTransport' : 'reference/routing-configuration/kubernetes/crd/http/serverstransport.md' - 'Middleware' : 'reference/routing-configuration/kubernetes/crd/http/middleware.md' @@ -337,24 +350,16 @@ nav: - 'ECS' : 'reference/routing-configuration/other-providers/ecs.md' - 'KV' : 'reference/routing-configuration/other-providers/kv.md' - 'File' : 'reference/routing-configuration/other-providers/file.md' - - 'Security': - - 'Content-Length': 'security/content-length.md' - - 'TLS in Multi-Tenant Kubernetes': 'security/tls-certs-in-multi-tenant-kubernetes.md' + - 'Security': + - 'Content-Length': 'security/content-length.md' + - 'TLS in Multi-Tenant Kubernetes': 'security/tls-certs-in-multi-tenant-kubernetes.md' + - 'Deprecation Notices': + - 'Releases': 'deprecation/releases.md' + - 'Features': 'deprecation/features.md' - 'User Guides': - 'FastProxy': 'user-guides/fastproxy.md' - - 'Kubernetes and Let''s Encrypt': 'user-guides/crd-acme/index.md' - - 'Kubernetes and cert-manager': 'user-guides/cert-manager.md' - 'gRPC Examples': 'user-guides/grpc.md' - 'WebSocket Examples': 'user-guides/websocket.md' - - 'Docker': - - 'Basic Example': 'user-guides/docker-compose/basic-example/index.md' - - 'HTTPS with Let''s Encrypt': - - 'TLS Challenge': 'user-guides/docker-compose/acme-tls/index.md' - - 'HTTP Challenge': 'user-guides/docker-compose/acme-http/index.md' - - 'DNS Challenge': 'user-guides/docker-compose/acme-dns/index.md' - - 'Deprecation Notices': - - 'Releases': 'deprecation/releases.md' - - 'Features': 'deprecation/features.md' - 'Contributing': - 'Thank You!': 'contributing/thank-you.md' - 'Submitting Issues': 'contributing/submitting-issues.md' @@ -365,4 +370,4 @@ nav: - 'Data Collection': 'contributing/data-collection.md' - 'Advocating': 'contributing/advocating.md' - 'Maintainers': 'contributing/maintainers.md' - - 'Frequently Asked Questions': 'getting-started/faq.md' + - 'FAQ': 'getting-started/faq.md' diff --git a/integration/fixtures/k8s/01-traefik-crd.yml b/integration/fixtures/k8s/01-traefik-crd.yml index afc32d9b9..98268e8e3 100644 --- a/integration/fixtures/k8s/01-traefik-crd.yml +++ b/integration/fixtures/k8s/01-traefik-crd.yml @@ -2316,7 +2316,7 @@ spec: maxIdleConnsPerHost: description: MaxIdleConnsPerHost controls the maximum idle (keep-alive) to keep per-host. - minimum: 0 + minimum: -1 type: integer peerCertURI: description: PeerCertURI defines the peer cert URI used to match against diff --git a/pkg/config/dynamic/http_config.go b/pkg/config/dynamic/http_config.go index b64c444f1..bbda7321c 100644 --- a/pkg/config/dynamic/http_config.go +++ b/pkg/config/dynamic/http_config.go @@ -393,7 +393,7 @@ type ServersTransport 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"` - MaxIdleConnsPerHost int `description:"If non-zero, controls the maximum idle (keep-alive) to keep per-host. If zero, DefaultMaxIdleConnsPerHost is used" json:"maxIdleConnsPerHost,omitempty" toml:"maxIdleConnsPerHost,omitempty" yaml:"maxIdleConnsPerHost,omitempty" export:"true"` + 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"` diff --git a/pkg/config/static/static_config.go b/pkg/config/static/static_config.go index a8a0dbff0..f236da3fb 100644 --- a/pkg/config/static/static_config.go +++ b/pkg/config/static/static_config.go @@ -118,7 +118,7 @@ type Global struct { type ServersTransport struct { InsecureSkipVerify bool `description:"Disable SSL certificate verification." json:"insecureSkipVerify,omitempty" toml:"insecureSkipVerify,omitempty" yaml:"insecureSkipVerify,omitempty" export:"true"` RootCAs []types.FileOrContent `description:"Add cert file for self-signed certificate." json:"rootCAs,omitempty" toml:"rootCAs,omitempty" yaml:"rootCAs,omitempty"` - MaxIdleConnsPerHost int `description:"If non-zero, controls the maximum idle (keep-alive) to keep per-host. If zero, DefaultMaxIdleConnsPerHost is used" json:"maxIdleConnsPerHost,omitempty" toml:"maxIdleConnsPerHost,omitempty" yaml:"maxIdleConnsPerHost,omitempty" export:"true"` + 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:"Timeouts for requests forwarded to the backend servers." json:"forwardingTimeouts,omitempty" toml:"forwardingTimeouts,omitempty" yaml:"forwardingTimeouts,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"` } diff --git a/pkg/plugins/builder.go b/pkg/plugins/builder.go index 8559dec90..96a4bf21e 100644 --- a/pkg/plugins/builder.go +++ b/pkg/plugins/builder.go @@ -28,7 +28,7 @@ type Builder struct { } // NewBuilder creates a new Builder. -func NewBuilder(client *Client, plugins map[string]Descriptor, localPlugins map[string]LocalDescriptor) (*Builder, error) { +func NewBuilder(manager *Manager, plugins map[string]Descriptor, localPlugins map[string]LocalDescriptor) (*Builder, error) { ctx := context.Background() pb := &Builder{ @@ -37,9 +37,9 @@ func NewBuilder(client *Client, plugins map[string]Descriptor, localPlugins map[ } for pName, desc := range plugins { - manifest, err := client.ReadManifest(desc.ModuleName) + manifest, err := manager.ReadManifest(desc.ModuleName) if err != nil { - _ = client.ResetAll() + _ = manager.ResetAll() return nil, fmt.Errorf("%s: failed to read manifest: %w", desc.ModuleName, err) } @@ -52,7 +52,7 @@ func NewBuilder(client *Client, plugins map[string]Descriptor, localPlugins map[ switch manifest.Type { case typeMiddleware: - middleware, err := newMiddlewareBuilder(logCtx, client.GoPath(), manifest, desc.ModuleName, desc.Settings) + middleware, err := newMiddlewareBuilder(logCtx, manager.GoPath(), manifest, desc.ModuleName, desc.Settings) if err != nil { return nil, err } @@ -60,7 +60,7 @@ func NewBuilder(client *Client, plugins map[string]Descriptor, localPlugins map[ pb.middlewareBuilders[pName] = middleware case typeProvider: - pBuilder, err := newProviderBuilder(logCtx, manifest, client.GoPath(), desc.Settings) + pBuilder, err := newProviderBuilder(logCtx, manifest, manager.GoPath(), desc.Settings) if err != nil { return nil, fmt.Errorf("%s: %w", desc.ModuleName, err) } diff --git a/pkg/plugins/downloader.go b/pkg/plugins/downloader.go new file mode 100644 index 000000000..3dce1df4c --- /dev/null +++ b/pkg/plugins/downloader.go @@ -0,0 +1,160 @@ +package plugins + +import ( + "context" + "errors" + "fmt" + "io" + "net/http" + "net/url" + "os" + "path" + "path/filepath" +) + +// PluginDownloader defines the interface for downloading and validating plugins from remote sources. +type PluginDownloader interface { + // Download downloads a plugin archive and returns its hash. + Download(ctx context.Context, pName, pVersion string) (string, error) + // Check checks the plugin archive integrity against a known hash. + Check(ctx context.Context, pName, pVersion, hash string) error +} + +// RegistryDownloaderOptions holds configuration options for creating a RegistryDownloader. +type RegistryDownloaderOptions struct { + HTTPClient *http.Client + ArchivesPath string +} + +// RegistryDownloader implements PluginDownloader for HTTP-based plugin downloads. +type RegistryDownloader struct { + httpClient *http.Client + baseURL *url.URL + archives string +} + +// NewRegistryDownloader creates a new HTTP-based plugin downloader. +func NewRegistryDownloader(opts RegistryDownloaderOptions) (*RegistryDownloader, error) { + baseURL, err := url.Parse(pluginsURL) + if err != nil { + return nil, err + } + + httpClient := opts.HTTPClient + if httpClient == nil { + httpClient = http.DefaultClient + } + + return &RegistryDownloader{ + httpClient: httpClient, + baseURL: baseURL, + archives: opts.ArchivesPath, + }, nil +} + +// Download downloads a plugin archive. +func (d *RegistryDownloader) Download(ctx context.Context, pName, pVersion string) (string, error) { + filename := d.buildArchivePath(pName, pVersion) + + var hash string + _, err := os.Stat(filename) + if err != nil && !os.IsNotExist(err) { + return "", fmt.Errorf("failed to read archive %s: %w", filename, err) + } + + if err == nil { + hash, err = computeHash(filename) + if err != nil { + return "", fmt.Errorf("failed to compute hash: %w", err) + } + } + + endpoint, err := d.baseURL.Parse(path.Join(d.baseURL.Path, "download", pName, pVersion)) + if err != nil { + return "", fmt.Errorf("failed to parse endpoint URL: %w", err) + } + + req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint.String(), nil) + if err != nil { + return "", fmt.Errorf("failed to create request: %w", err) + } + + if hash != "" { + req.Header.Set(hashHeader, hash) + } + + resp, err := d.httpClient.Do(req) + if err != nil { + return "", fmt.Errorf("failed to call service: %w", err) + } + + defer func() { _ = resp.Body.Close() }() + + switch resp.StatusCode { + case http.StatusNotModified: + return hash, nil + case http.StatusOK: + err = os.MkdirAll(filepath.Dir(filename), 0o755) + if err != nil { + return "", fmt.Errorf("failed to create directory: %w", err) + } + + var file *os.File + file, err = os.Create(filename) + if err != nil { + return "", fmt.Errorf("failed to create file %q: %w", filename, err) + } + + defer func() { _ = file.Close() }() + + _, err = io.Copy(file, resp.Body) + if err != nil { + return "", fmt.Errorf("failed to write response: %w", err) + } + + hash, err = computeHash(filename) + if err != nil { + return "", fmt.Errorf("failed to compute hash: %w", err) + } + default: + data, _ := io.ReadAll(resp.Body) + return "", fmt.Errorf("error: %d: %s", resp.StatusCode, string(data)) + } + + return hash, nil +} + +// Check checks the plugin archive integrity. +func (d *RegistryDownloader) Check(ctx context.Context, pName, pVersion, hash string) error { + endpoint, err := d.baseURL.Parse(path.Join(d.baseURL.Path, "validate", pName, pVersion)) + if err != nil { + return fmt.Errorf("failed to parse endpoint URL: %w", err) + } + + req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint.String(), nil) + if err != nil { + return fmt.Errorf("failed to create request: %w", err) + } + + if hash != "" { + req.Header.Set(hashHeader, hash) + } + + resp, err := d.httpClient.Do(req) + if err != nil { + return fmt.Errorf("failed to call service: %w", err) + } + + defer func() { _ = resp.Body.Close() }() + + if resp.StatusCode == http.StatusOK { + return nil + } + + return errors.New("plugin integrity check failed") +} + +// buildArchivePath builds the path to a plugin archive file. +func (d *RegistryDownloader) buildArchivePath(pName, pVersion string) string { + return filepath.Join(d.archives, filepath.FromSlash(pName), pVersion+".zip") +} diff --git a/pkg/plugins/downloader_test.go b/pkg/plugins/downloader_test.go new file mode 100644 index 000000000..bcbd89424 --- /dev/null +++ b/pkg/plugins/downloader_test.go @@ -0,0 +1,159 @@ +package plugins + +import ( + "archive/zip" + "io" + "net/http" + "net/http/httptest" + "net/url" + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestHTTPPluginDownloader_Download(t *testing.T) { + tests := []struct { + name string + serverResponse func(w http.ResponseWriter, r *http.Request) + fileAlreadyExists bool + expectError bool + }{ + { + name: "successful download", + serverResponse: func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/zip") + w.WriteHeader(http.StatusOK) + + require.NoError(t, fillDummyZip(w)) + }, + }, + { + name: "not modified response", + serverResponse: func(w http.ResponseWriter, r *http.Request) { + http.Error(w, "", http.StatusNotModified) + }, + fileAlreadyExists: true, + }, + { + name: "server error", + serverResponse: func(w http.ResponseWriter, r *http.Request) { + http.Error(w, "internal server error", http.StatusInternalServerError) + }, + expectError: true, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(test.serverResponse)) + defer server.Close() + + tempDir := t.TempDir() + archivesPath := filepath.Join(tempDir, "archives") + + if test.fileAlreadyExists { + createDummyZip(t, archivesPath) + } + + baseURL, err := url.Parse(server.URL) + require.NoError(t, err) + + downloader := &RegistryDownloader{ + httpClient: server.Client(), + baseURL: baseURL, + archives: archivesPath, + } + + ctx := t.Context() + hash, err := downloader.Download(ctx, "test/plugin", "v1.0.0") + + if test.expectError { + assert.Error(t, err) + } else { + assert.NoError(t, err) + assert.NotEmpty(t, hash) + + // Check if archive file was created + archivePath := downloader.buildArchivePath("test/plugin", "v1.0.0") + assert.FileExists(t, archivePath) + } + }) + } +} + +func TestHTTPPluginDownloader_Check(t *testing.T) { + tests := []struct { + name string + serverResponse func(w http.ResponseWriter, r *http.Request) + expectError require.ErrorAssertionFunc + }{ + { + name: "successful check", + serverResponse: func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + }, + expectError: require.NoError, + }, + { + name: "failed check", + serverResponse: func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusBadRequest) + }, + expectError: require.Error, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(test.serverResponse)) + defer server.Close() + + tempDir := t.TempDir() + archivesPath := filepath.Join(tempDir, "archives") + + baseURL, err := url.Parse(server.URL) + require.NoError(t, err) + + downloader := &RegistryDownloader{ + httpClient: server.Client(), + baseURL: baseURL, + archives: archivesPath, + } + + ctx := t.Context() + + err = downloader.Check(ctx, "test/plugin", "v1.0.0", "testhash") + test.expectError(t, err) + }) + } +} + +func createDummyZip(t *testing.T, path string) { + t.Helper() + + err := os.MkdirAll(path+"/test/plugin/", 0o755) + require.NoError(t, err) + + zipfile, err := os.Create(path + "/test/plugin/v1.0.0.zip") + require.NoError(t, err) + defer zipfile.Close() + + err = fillDummyZip(zipfile) + require.NoError(t, err) +} + +func fillDummyZip(w io.Writer) error { + writer := zip.NewWriter(w) + + file, err := writer.Create("test.txt") + if err != nil { + return err + } + + _, _ = file.Write([]byte("test content")) + _ = writer.Close() + return nil +} diff --git a/pkg/plugins/client.go b/pkg/plugins/manager.go similarity index 52% rename from pkg/plugins/client.go rename to pkg/plugins/manager.go index 8d119ddec..2bbb9cbe7 100644 --- a/pkg/plugins/client.go +++ b/pkg/plugins/manager.go @@ -9,17 +9,10 @@ import ( "errors" "fmt" "io" - "net/http" - "net/url" "os" - "path" "path/filepath" "strings" - "time" - "github.com/hashicorp/go-retryablehttp" - "github.com/rs/zerolog/log" - "github.com/traefik/traefik/v3/pkg/logs" "golang.org/x/mod/module" "golang.org/x/mod/zip" "gopkg.in/yaml.v3" @@ -39,31 +32,26 @@ const ( hashHeader = "X-Plugin-Hash" ) -// ClientOptions the options of a Traefik plugins client. -type ClientOptions struct { +// ManagerOptions the options of a Traefik plugins manager. +type ManagerOptions struct { Output string } -// Client a Traefik plugins client. -type Client struct { - HTTPClient *http.Client - baseURL *url.URL +// Manager manages Traefik plugins lifecycle operations including storage, and manifest reading. +type Manager struct { + downloader PluginDownloader - archives string stateFile string - goPath string - sources string + + archives string + sources string + goPath string } -// NewClient creates a new Traefik plugins client. -func NewClient(opts ClientOptions) (*Client, error) { - baseURL, err := url.Parse(pluginsURL) - if err != nil { - return nil, err - } - +// NewManager creates a new Traefik plugins manager. +func NewManager(downloader PluginDownloader, opts ManagerOptions) (*Manager, error) { sourcesRootPath := filepath.Join(filepath.FromSlash(opts.Output), sourcesFolder) - err = resetDirectory(sourcesRootPath) + err := resetDirectory(sourcesRootPath) if err != nil { return nil, err } @@ -79,31 +67,48 @@ func NewClient(opts ClientOptions) (*Client, error) { return nil, fmt.Errorf("failed to create archives directory %s: %w", archivesPath, err) } - client := retryablehttp.NewClient() - client.Logger = logs.NewRetryableHTTPLogger(log.Logger) - client.HTTPClient = &http.Client{Timeout: 10 * time.Second} - client.RetryMax = 3 - - return &Client{ - HTTPClient: client.StandardClient(), - baseURL: baseURL, - - archives: archivesPath, - stateFile: filepath.Join(archivesPath, stateFilename), - - goPath: goPath, - sources: filepath.Join(goPath, goPathSrc), + return &Manager{ + downloader: downloader, + stateFile: filepath.Join(archivesPath, stateFilename), + archives: archivesPath, + sources: filepath.Join(goPath, goPathSrc), + goPath: goPath, }, nil } +// InstallPlugin download and unzip the given plugin. +func (m *Manager) InstallPlugin(ctx context.Context, plugin Descriptor) error { + hash, err := m.downloader.Download(ctx, plugin.ModuleName, plugin.Version) + if err != nil { + return fmt.Errorf("unable to download plugin %s: %w", plugin.ModuleName, err) + } + + if plugin.Hash != "" { + if plugin.Hash != hash { + return fmt.Errorf("invalid hash for plugin %s, expected %s, got %s", plugin.ModuleName, plugin.Hash, hash) + } + } else { + err = m.downloader.Check(ctx, plugin.ModuleName, plugin.Version, hash) + if err != nil { + return fmt.Errorf("unable to check archive integrity of the plugin %s: %w", plugin.ModuleName, err) + } + } + + if err = m.unzip(plugin.ModuleName, plugin.Version); err != nil { + return fmt.Errorf("unable to unzip plugin %s: %w", plugin.ModuleName, err) + } + + return nil +} + // GoPath gets the plugins GoPath. -func (c *Client) GoPath() string { - return c.goPath +func (m *Manager) GoPath() string { + return m.goPath } // ReadManifest reads a plugin manifest. -func (c *Client) ReadManifest(moduleName string) (*Manifest, error) { - return ReadManifest(c.goPath, moduleName) +func (m *Manager) ReadManifest(moduleName string) (*Manifest, error) { + return ReadManifest(m.goPath, moduleName) } // ReadManifest reads a plugin manifest. @@ -126,114 +131,74 @@ func ReadManifest(goPath, moduleName string) (*Manifest, error) { return m, nil } -// Download downloads a plugin archive. -func (c *Client) Download(ctx context.Context, pName, pVersion string) (string, error) { - filename := c.buildArchivePath(pName, pVersion) - - var hash string - _, err := os.Stat(filename) - if err != nil && !os.IsNotExist(err) { - return "", fmt.Errorf("failed to read archive %s: %w", filename, err) - } - - if err == nil { - hash, err = computeHash(filename) - if err != nil { - return "", fmt.Errorf("failed to compute hash: %w", err) - } - } - - endpoint, err := c.baseURL.Parse(path.Join(c.baseURL.Path, "download", pName, pVersion)) - if err != nil { - return "", fmt.Errorf("failed to parse endpoint URL: %w", err) - } - - req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint.String(), nil) - if err != nil { - return "", fmt.Errorf("failed to create request: %w", err) - } - - if hash != "" { - req.Header.Set(hashHeader, hash) - } - - resp, err := c.HTTPClient.Do(req) - if err != nil { - return "", fmt.Errorf("failed to call service: %w", err) - } - - defer func() { _ = resp.Body.Close() }() - - switch resp.StatusCode { - case http.StatusNotModified: - // noop - return hash, nil - - case http.StatusOK: - err = os.MkdirAll(filepath.Dir(filename), 0o755) - if err != nil { - return "", fmt.Errorf("failed to create directory: %w", err) - } - - var file *os.File - file, err = os.Create(filename) - if err != nil { - return "", fmt.Errorf("failed to create file %q: %w", filename, err) - } - - defer func() { _ = file.Close() }() - - _, err = io.Copy(file, resp.Body) - if err != nil { - return "", fmt.Errorf("failed to write response: %w", err) - } - - hash, err = computeHash(filename) - if err != nil { - return "", fmt.Errorf("failed to compute hash: %w", err) - } - - return hash, nil - - default: - data, _ := io.ReadAll(resp.Body) - return "", fmt.Errorf("error: %d: %s", resp.StatusCode, string(data)) - } -} - -// Check checks the plugin archive integrity. -func (c *Client) Check(ctx context.Context, pName, pVersion, hash string) error { - endpoint, err := c.baseURL.Parse(path.Join(c.baseURL.Path, "validate", pName, pVersion)) - if err != nil { - return fmt.Errorf("failed to parse endpoint URL: %w", err) - } - - req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint.String(), nil) - if err != nil { - return fmt.Errorf("failed to create request: %w", err) - } - - if hash != "" { - req.Header.Set(hashHeader, hash) - } - - resp, err := c.HTTPClient.Do(req) - if err != nil { - return fmt.Errorf("failed to call service: %w", err) - } - - defer func() { _ = resp.Body.Close() }() - - if resp.StatusCode == http.StatusOK { +// CleanArchives cleans plugins archives. +func (m *Manager) CleanArchives(plugins map[string]Descriptor) error { + if _, err := os.Stat(m.stateFile); os.IsNotExist(err) { return nil } - return errors.New("plugin integrity check failed") + stateFile, err := os.Open(m.stateFile) + if err != nil { + return fmt.Errorf("failed to open state file %s: %w", m.stateFile, err) + } + + previous := make(map[string]string) + err = json.NewDecoder(stateFile).Decode(&previous) + if err != nil { + return fmt.Errorf("failed to decode state file %s: %w", m.stateFile, err) + } + + for pName, pVersion := range previous { + for _, desc := range plugins { + if desc.ModuleName == pName && desc.Version != pVersion { + archivePath := m.buildArchivePath(pName, pVersion) + if err = os.RemoveAll(archivePath); err != nil { + return fmt.Errorf("failed to remove archive %s: %w", archivePath, err) + } + } + } + } + + return nil } -// Unzip unzip a plugin archive. -func (c *Client) Unzip(pName, pVersion string) error { - err := c.unzipModule(pName, pVersion) +// WriteState writes the plugins state files. +func (m *Manager) WriteState(plugins map[string]Descriptor) error { + state := make(map[string]string) + + for _, descriptor := range plugins { + state[descriptor.ModuleName] = descriptor.Version + } + + mp, err := json.MarshalIndent(state, "", " ") + if err != nil { + return fmt.Errorf("unable to marshal plugin state: %w", err) + } + + return os.WriteFile(m.stateFile, mp, 0o600) +} + +// ResetAll resets all plugins related directories. +func (m *Manager) ResetAll() error { + if m.goPath == "" { + return errors.New("goPath is empty") + } + + err := resetDirectory(filepath.Join(m.goPath, "..")) + if err != nil { + return fmt.Errorf("unable to reset plugins GoPath directory %s: %w", m.goPath, err) + } + + err = resetDirectory(m.archives) + if err != nil { + return fmt.Errorf("unable to reset plugins archives directory: %w", err) + } + + return nil +} + +func (m *Manager) unzip(pName, pVersion string) error { + err := m.unzipModule(pName, pVersion) if err == nil { return nil } @@ -241,18 +206,18 @@ func (c *Client) Unzip(pName, pVersion string) error { // Unzip as a generic archive if the module unzip fails. // This is useful for plugins that have vendor directories or other structures. // This is also useful for wasm plugins. - return c.unzipArchive(pName, pVersion) + return m.unzipArchive(pName, pVersion) } -func (c *Client) unzipModule(pName, pVersion string) error { - src := c.buildArchivePath(pName, pVersion) - dest := filepath.Join(c.sources, filepath.FromSlash(pName)) +func (m *Manager) unzipModule(pName, pVersion string) error { + src := m.buildArchivePath(pName, pVersion) + dest := filepath.Join(m.sources, filepath.FromSlash(pName)) return zip.Unzip(dest, module.Version{Path: pName, Version: pVersion}, src) } -func (c *Client) unzipArchive(pName, pVersion string) error { - zipPath := c.buildArchivePath(pName, pVersion) +func (m *Manager) unzipArchive(pName, pVersion string) error { + zipPath := m.buildArchivePath(pName, pVersion) archive, err := zipa.OpenReader(zipPath) if err != nil { @@ -261,10 +226,10 @@ func (c *Client) unzipArchive(pName, pVersion string) error { defer func() { _ = archive.Close() }() - dest := filepath.Join(c.sources, filepath.FromSlash(pName)) + dest := filepath.Join(m.sources, filepath.FromSlash(pName)) for _, f := range archive.File { - err = unzipFile(f, dest) + err = m.unzipFile(f, dest) if err != nil { return fmt.Errorf("unable to unzip %s: %w", f.Name, err) } @@ -273,7 +238,7 @@ func (c *Client) unzipArchive(pName, pVersion string) error { return nil } -func unzipFile(f *zipa.File, dest string) error { +func (m *Manager) unzipFile(f *zipa.File, dest string) error { rc, err := f.Open() if err != nil { return err @@ -341,74 +306,8 @@ func unzipFile(f *zipa.File, dest string) error { return nil } -// CleanArchives cleans plugins archives. -func (c *Client) CleanArchives(plugins map[string]Descriptor) error { - if _, err := os.Stat(c.stateFile); os.IsNotExist(err) { - return nil - } - - stateFile, err := os.Open(c.stateFile) - if err != nil { - return fmt.Errorf("failed to open state file %s: %w", c.stateFile, err) - } - - previous := make(map[string]string) - err = json.NewDecoder(stateFile).Decode(&previous) - if err != nil { - return fmt.Errorf("failed to decode state file %s: %w", c.stateFile, err) - } - - for pName, pVersion := range previous { - for _, desc := range plugins { - if desc.ModuleName == pName && desc.Version != pVersion { - archivePath := c.buildArchivePath(pName, pVersion) - if err = os.RemoveAll(archivePath); err != nil { - return fmt.Errorf("failed to remove archive %s: %w", archivePath, err) - } - } - } - } - - return nil -} - -// WriteState writes the plugins state files. -func (c *Client) WriteState(plugins map[string]Descriptor) error { - m := make(map[string]string) - - for _, descriptor := range plugins { - m[descriptor.ModuleName] = descriptor.Version - } - - mp, err := json.MarshalIndent(m, "", " ") - if err != nil { - return fmt.Errorf("unable to marshal plugin state: %w", err) - } - - return os.WriteFile(c.stateFile, mp, 0o600) -} - -// ResetAll resets all plugins related directories. -func (c *Client) ResetAll() error { - if c.goPath == "" { - return errors.New("goPath is empty") - } - - err := resetDirectory(filepath.Join(c.goPath, "..")) - if err != nil { - return fmt.Errorf("unable to reset plugins GoPath directory %s: %w", c.goPath, err) - } - - err = resetDirectory(c.archives) - if err != nil { - return fmt.Errorf("unable to reset plugins archives directory: %w", err) - } - - return nil -} - -func (c *Client) buildArchivePath(pName, pVersion string) string { - return filepath.Join(c.archives, filepath.FromSlash(pName), pVersion+".zip") +func (m *Manager) buildArchivePath(pName, pVersion string) string { + return filepath.Join(m.archives, filepath.FromSlash(pName), pVersion+".zip") } func resetDirectory(dir string) error { diff --git a/pkg/plugins/manager_test.go b/pkg/plugins/manager_test.go new file mode 100644 index 000000000..5100b2a6b --- /dev/null +++ b/pkg/plugins/manager_test.go @@ -0,0 +1,341 @@ +package plugins + +import ( + zipa "archive/zip" + "context" + "encoding/json" + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "gopkg.in/yaml.v3" +) + +// mockDownloader is a test implementation of PluginDownloader +type mockDownloader struct { + downloadFunc func(ctx context.Context, pName, pVersion string) (string, error) + checkFunc func(ctx context.Context, pName, pVersion, hash string) error +} + +func (m *mockDownloader) Download(ctx context.Context, pName, pVersion string) (string, error) { + if m.downloadFunc != nil { + return m.downloadFunc(ctx, pName, pVersion) + } + return "mockhash", nil +} + +func (m *mockDownloader) Check(ctx context.Context, pName, pVersion, hash string) error { + if m.checkFunc != nil { + return m.checkFunc(ctx, pName, pVersion, hash) + } + return nil +} + +func TestPluginManager_ReadManifest(t *testing.T) { + tempDir := t.TempDir() + opts := ManagerOptions{Output: tempDir} + + downloader := &mockDownloader{} + manager, err := NewManager(downloader, opts) + require.NoError(t, err) + + moduleName := "github.com/test/plugin" + pluginPath := filepath.Join(manager.goPath, "src", moduleName) + err = os.MkdirAll(pluginPath, 0o755) + require.NoError(t, err) + + manifest := &Manifest{ + DisplayName: "Test Plugin", + Type: "middleware", + Import: "github.com/test/plugin", + Summary: "A test plugin", + TestData: map[string]interface{}{ + "test": "data", + }, + } + + manifestPath := filepath.Join(pluginPath, pluginManifest) + manifestData, err := yaml.Marshal(manifest) + require.NoError(t, err) + err = os.WriteFile(manifestPath, manifestData, 0o644) + require.NoError(t, err) + + readManifest, err := manager.ReadManifest(moduleName) + require.NoError(t, err) + assert.Equal(t, manifest.DisplayName, readManifest.DisplayName) + assert.Equal(t, manifest.Type, readManifest.Type) + assert.Equal(t, manifest.Import, readManifest.Import) + assert.Equal(t, manifest.Summary, readManifest.Summary) +} + +func TestPluginManager_ReadManifest_NotFound(t *testing.T) { + tempDir := t.TempDir() + opts := ManagerOptions{Output: tempDir} + + downloader := &mockDownloader{} + manager, err := NewManager(downloader, opts) + require.NoError(t, err) + + _, err = manager.ReadManifest("nonexistent/plugin") + assert.Error(t, err) +} + +func TestPluginManager_CleanArchives(t *testing.T) { + tempDir := t.TempDir() + opts := ManagerOptions{Output: tempDir} + + downloader := &mockDownloader{} + manager, err := NewManager(downloader, opts) + require.NoError(t, err) + + testPlugin1 := "test/plugin1" + testPlugin2 := "test/plugin2" + + archive1Dir := filepath.Join(manager.archives, "test", "plugin1") + archive2Dir := filepath.Join(manager.archives, "test", "plugin2") + err = os.MkdirAll(archive1Dir, 0o755) + require.NoError(t, err) + err = os.MkdirAll(archive2Dir, 0o755) + require.NoError(t, err) + + archive1Old := filepath.Join(archive1Dir, "v1.0.0.zip") + archive1New := filepath.Join(archive1Dir, "v2.0.0.zip") + archive2 := filepath.Join(archive2Dir, "v1.0.0.zip") + + err = os.WriteFile(archive1Old, []byte("old archive"), 0o644) + require.NoError(t, err) + err = os.WriteFile(archive1New, []byte("new archive"), 0o644) + require.NoError(t, err) + err = os.WriteFile(archive2, []byte("archive 2"), 0o644) + require.NoError(t, err) + + state := map[string]string{ + testPlugin1: "v1.0.0", + testPlugin2: "v1.0.0", + } + stateData, err := json.MarshalIndent(state, "", " ") + require.NoError(t, err) + err = os.WriteFile(manager.stateFile, stateData, 0o600) + require.NoError(t, err) + + currentPlugins := map[string]Descriptor{ + "plugin1": { + ModuleName: testPlugin1, + Version: "v2.0.0", + }, + "plugin2": { + ModuleName: testPlugin2, + Version: "v1.0.0", + }, + } + + err = manager.CleanArchives(currentPlugins) + require.NoError(t, err) + + assert.NoFileExists(t, archive1Old) + assert.FileExists(t, archive1New) + assert.FileExists(t, archive2) +} + +func TestPluginManager_WriteState(t *testing.T) { + tempDir := t.TempDir() + opts := ManagerOptions{Output: tempDir} + + downloader := &mockDownloader{} + manager, err := NewManager(downloader, opts) + require.NoError(t, err) + + plugins := map[string]Descriptor{ + "plugin1": { + ModuleName: "test/plugin1", + Version: "v1.0.0", + }, + "plugin2": { + ModuleName: "test/plugin2", + Version: "v2.0.0", + }, + } + + err = manager.WriteState(plugins) + require.NoError(t, err) + + assert.FileExists(t, manager.stateFile) + + data, err := os.ReadFile(manager.stateFile) + require.NoError(t, err) + + var state map[string]string + err = json.Unmarshal(data, &state) + require.NoError(t, err) + + expectedState := map[string]string{ + "test/plugin1": "v1.0.0", + "test/plugin2": "v2.0.0", + } + assert.Equal(t, expectedState, state) +} + +func TestPluginManager_ResetAll(t *testing.T) { + tempDir := t.TempDir() + opts := ManagerOptions{Output: tempDir} + + downloader := &mockDownloader{} + manager, err := NewManager(downloader, opts) + require.NoError(t, err) + + testFile := filepath.Join(manager.GoPath(), "test.txt") + err = os.WriteFile(testFile, []byte("test"), 0o644) + require.NoError(t, err) + + archiveFile := filepath.Join(manager.archives, "test.zip") + err = os.WriteFile(archiveFile, []byte("archive"), 0o644) + require.NoError(t, err) + + err = manager.ResetAll() + require.NoError(t, err) + + assert.DirExists(t, manager.archives) + assert.NoFileExists(t, testFile) + assert.NoFileExists(t, archiveFile) +} + +func TestPluginManager_InstallPlugin(t *testing.T) { + tests := []struct { + name string + plugin Descriptor + downloadFunc func(ctx context.Context, pName, pVersion string) (string, error) + checkFunc func(ctx context.Context, pName, pVersion, hash string) error + setupArchive func(t *testing.T, archivePath string) + expectError bool + errorMsg string + }{ + { + name: "successful installation", + plugin: Descriptor{ + ModuleName: "github.com/test/plugin", + Version: "v1.0.0", + Hash: "expected-hash", + }, + downloadFunc: func(ctx context.Context, pName, pVersion string) (string, error) { + return "expected-hash", nil + }, + checkFunc: func(ctx context.Context, pName, pVersion, hash string) error { + return nil + }, + setupArchive: func(t *testing.T, archivePath string) { + t.Helper() + + // Create a valid zip archive + err := os.MkdirAll(filepath.Dir(archivePath), 0o755) + require.NoError(t, err) + + file, err := os.Create(archivePath) + require.NoError(t, err) + defer file.Close() + + // Write a minimal zip file with a test file + writer := zipa.NewWriter(file) + defer writer.Close() + + fileWriter, err := writer.Create("test-module-v1.0.0/main.go") + require.NoError(t, err) + _, err = fileWriter.Write([]byte("package main\n\nfunc main() {}\n")) + require.NoError(t, err) + }, + expectError: false, + }, + { + name: "download error", + plugin: Descriptor{ + ModuleName: "github.com/test/plugin", + Version: "v1.0.0", + }, + downloadFunc: func(ctx context.Context, pName, pVersion string) (string, error) { + return "", assert.AnError + }, + expectError: true, + errorMsg: "unable to download plugin", + }, + { + name: "check error", + plugin: Descriptor{ + ModuleName: "github.com/test/plugin", + Version: "v1.0.0", + Hash: "expected-hash", + }, + downloadFunc: func(ctx context.Context, pName, pVersion string) (string, error) { + return "actual-hash", nil + }, + checkFunc: func(ctx context.Context, pName, pVersion, hash string) error { + return assert.AnError + }, + expectError: true, + errorMsg: "invalid hash for plugin", + }, + { + name: "unzip error - invalid archive", + plugin: Descriptor{ + ModuleName: "github.com/test/plugin", + Version: "v1.0.0", + }, + downloadFunc: func(ctx context.Context, pName, pVersion string) (string, error) { + return "test-hash", nil + }, + checkFunc: func(ctx context.Context, pName, pVersion, hash string) error { + return nil + }, + setupArchive: func(t *testing.T, archivePath string) { + t.Helper() + + // Create an invalid zip archive + err := os.MkdirAll(filepath.Dir(archivePath), 0o755) + require.NoError(t, err) + err = os.WriteFile(archivePath, []byte("invalid zip content"), 0o644) + require.NoError(t, err) + }, + expectError: true, + errorMsg: "unable to unzip plugin", + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + tempDir := t.TempDir() + opts := ManagerOptions{Output: tempDir} + + downloader := &mockDownloader{ + downloadFunc: test.downloadFunc, + checkFunc: test.checkFunc, + } + + manager, err := NewManager(downloader, opts) + require.NoError(t, err) + + // Setup archive if needed + if test.setupArchive != nil { + archivePath := filepath.Join(manager.archives, + filepath.FromSlash(test.plugin.ModuleName), + test.plugin.Version+".zip") + test.setupArchive(t, archivePath) + } + + ctx := t.Context() + err = manager.InstallPlugin(ctx, test.plugin) + + if test.expectError { + assert.Error(t, err) + if test.errorMsg != "" { + assert.Contains(t, err.Error(), test.errorMsg) + } + } else { + assert.NoError(t, err) + + // Verify that plugin sources were extracted + sourcePath := filepath.Join(manager.sources, filepath.FromSlash(test.plugin.ModuleName)) + assert.DirExists(t, sourcePath) + } + }) + } +} diff --git a/pkg/plugins/plugins.go b/pkg/plugins/plugins.go index 367b6c46c..f7d543154 100644 --- a/pkg/plugins/plugins.go +++ b/pkg/plugins/plugins.go @@ -13,13 +13,13 @@ import ( const localGoPath = "./plugins-local/" // SetupRemotePlugins setup remote plugins environment. -func SetupRemotePlugins(client *Client, plugins map[string]Descriptor) error { +func SetupRemotePlugins(manager *Manager, plugins map[string]Descriptor) error { err := checkRemotePluginsConfiguration(plugins) if err != nil { return fmt.Errorf("invalid configuration: %w", err) } - err = client.CleanArchives(plugins) + err = manager.CleanArchives(plugins) if err != nil { return fmt.Errorf("unable to clean archives: %w", err) } @@ -27,35 +27,20 @@ func SetupRemotePlugins(client *Client, plugins map[string]Descriptor) error { ctx := context.Background() for pAlias, desc := range plugins { - log.Ctx(ctx).Debug().Msgf("Loading of plugin: %s: %s@%s", pAlias, desc.ModuleName, desc.Version) + log.Ctx(ctx).Debug().Msgf("Installing plugin: %s: %s@%s", pAlias, desc.ModuleName, desc.Version) - hash, err := client.Download(ctx, desc.ModuleName, desc.Version) - if err != nil { - _ = client.ResetAll() - return fmt.Errorf("unable to download plugin %s: %w", desc.ModuleName, err) - } - - err = client.Check(ctx, desc.ModuleName, desc.Version, hash) - if err != nil { - _ = client.ResetAll() - return fmt.Errorf("unable to check archive integrity of the plugin %s: %w", desc.ModuleName, err) + if err = manager.InstallPlugin(ctx, desc); err != nil { + _ = manager.ResetAll() + return fmt.Errorf("unable to install plugin %s: %w", pAlias, err) } } - err = client.WriteState(plugins) + err = manager.WriteState(plugins) if err != nil { - _ = client.ResetAll() + _ = manager.ResetAll() return fmt.Errorf("unable to write plugins state: %w", err) } - for _, desc := range plugins { - err = client.Unzip(desc.ModuleName, desc.Version) - if err != nil { - _ = client.ResetAll() - return fmt.Errorf("unable to unzip archive: %w", err) - } - } - return nil } diff --git a/pkg/plugins/types.go b/pkg/plugins/types.go index ccae8dce4..75bb589b3 100644 --- a/pkg/plugins/types.go +++ b/pkg/plugins/types.go @@ -24,6 +24,9 @@ type Descriptor struct { // Version (required) Version string `description:"plugin's version." json:"version,omitempty" toml:"version,omitempty" yaml:"version,omitempty" export:"true"` + // Hash (optional) + Hash string `description:"plugin's hash to validate'" json:"hash,omitempty" toml:"hash,omitempty" yaml:"hash,omitempty" export:"true"` + // Settings (optional) Settings Settings `description:"Plugin's settings (works only for wasm plugins)." json:"settings,omitempty" toml:"settings,omitempty" yaml:"settings,omitempty" export:"true"` } diff --git a/pkg/provider/kubernetes/crd/traefikio/v1alpha1/serverstransport.go b/pkg/provider/kubernetes/crd/traefikio/v1alpha1/serverstransport.go index 08143ed45..2bba208e9 100644 --- a/pkg/provider/kubernetes/crd/traefikio/v1alpha1/serverstransport.go +++ b/pkg/provider/kubernetes/crd/traefikio/v1alpha1/serverstransport.go @@ -39,7 +39,7 @@ type ServersTransportSpec struct { // 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. - // +kubebuilder:validation:Minimum=0 + // +kubebuilder:validation:Minimum=-1 MaxIdleConnsPerHost int `json:"maxIdleConnsPerHost,omitempty"` // ForwardingTimeouts defines the timeouts for requests forwarded to the backend servers. ForwardingTimeouts *ForwardingTimeouts `json:"forwardingTimeouts,omitempty"` diff --git a/pkg/tcp/dialer.go b/pkg/tcp/dialer.go index 2a754d3b4..bc4855b5b 100644 --- a/pkg/tcp/dialer.go +++ b/pkg/tcp/dialer.go @@ -19,12 +19,20 @@ import ( "github.com/traefik/traefik/v3/pkg/config/dynamic" traefiktls "github.com/traefik/traefik/v3/pkg/tls" "github.com/traefik/traefik/v3/pkg/types" - "golang.org/x/net/proxy" ) -type Dialer interface { - proxy.Dialer +// ClientConn is the interface that provides information about the client connection. +type ClientConn interface { + // LocalAddr returns the local network address, if known. + LocalAddr() net.Addr + // RemoteAddr returns the remote network address, if known. + RemoteAddr() net.Addr +} + +// Dialer is an interface to dial a network connection, with support for PROXY protocol and termination delay. +type Dialer interface { + Dial(network, addr string, clientConn ClientConn) (c net.Conn, err error) TerminationDelay() time.Duration } @@ -34,18 +42,20 @@ type tcpDialer struct { proxyProtocol *dynamic.ProxyProtocol } +// TerminationDelay returns the termination delay duration. func (d tcpDialer) TerminationDelay() time.Duration { return d.terminationDelay } -func (d tcpDialer) Dial(network, addr string) (net.Conn, error) { +// Dial dials a network connection and optionally sends a PROXY protocol header. +func (d tcpDialer) Dial(network, addr string, clientConn ClientConn) (net.Conn, error) { conn, err := d.dialer.Dial(network, addr) if err != nil { return nil, err } - if d.proxyProtocol != nil && d.proxyProtocol.Version > 0 && d.proxyProtocol.Version < 3 { - header := proxyproto.HeaderProxyFromAddrs(byte(d.proxyProtocol.Version), conn.RemoteAddr(), conn.LocalAddr()) + if d.proxyProtocol != nil && clientConn != nil && d.proxyProtocol.Version > 0 && d.proxyProtocol.Version < 3 { + header := proxyproto.HeaderProxyFromAddrs(byte(d.proxyProtocol.Version), clientConn.RemoteAddr(), clientConn.LocalAddr()) if _, err := header.WriteTo(conn); err != nil { _ = conn.Close() return nil, fmt.Errorf("writing PROXY Protocol header: %w", err) @@ -60,8 +70,9 @@ type tcpTLSDialer struct { tlsConfig *tls.Config } -func (d tcpTLSDialer) Dial(network, addr string) (net.Conn, error) { - conn, err := d.tcpDialer.Dial(network, addr) +// Dial dials a network connection with the wrapped tcpDialer and performs a TLS handshake. +func (d tcpTLSDialer) Dial(network, addr string, clientConn ClientConn) (net.Conn, error) { + conn, err := d.tcpDialer.Dial(network, addr, clientConn) if err != nil { return nil, err } diff --git a/pkg/tcp/dialer_test.go b/pkg/tcp/dialer_test.go index 62af030db..07d8a7b12 100644 --- a/pkg/tcp/dialer_test.go +++ b/pkg/tcp/dialer_test.go @@ -160,7 +160,7 @@ func TestNoTLS(t *testing.T) { dialer, err := dialerManager.Build(&dynamic.TCPServersLoadBalancer{ServersTransport: "test"}, false) require.NoError(t, err) - conn, err := dialer.Dial("tcp", ":"+port) + conn, err := dialer.Dial("tcp", ":"+port, nil) require.NoError(t, err) _, err = conn.Write([]byte("ping\n")) @@ -209,7 +209,7 @@ func TestTLS(t *testing.T) { dialer, err := dialerManager.Build(&dynamic.TCPServersLoadBalancer{ServersTransport: "test"}, true) require.NoError(t, err) - conn, err := dialer.Dial("tcp", ":"+port) + conn, err := dialer.Dial("tcp", ":"+port, nil) require.NoError(t, err) _, err = conn.Write([]byte("ping\n")) @@ -260,7 +260,7 @@ func TestTLSWithInsecureSkipVerify(t *testing.T) { dialer, err := dialerManager.Build(&dynamic.TCPServersLoadBalancer{ServersTransport: "test"}, true) require.NoError(t, err) - conn, err := dialer.Dial("tcp", ":"+port) + conn, err := dialer.Dial("tcp", ":"+port, nil) require.NoError(t, err) _, err = conn.Write([]byte("ping\n")) @@ -329,7 +329,7 @@ func TestMTLS(t *testing.T) { dialer, err := dialerManager.Build(&dynamic.TCPServersLoadBalancer{ServersTransport: "test"}, true) require.NoError(t, err) - conn, err := dialer.Dial("tcp", ":"+port) + conn, err := dialer.Dial("tcp", ":"+port, nil) require.NoError(t, err) _, err = conn.Write([]byte("ping\n")) @@ -463,7 +463,7 @@ func TestSpiffeMTLS(t *testing.T) { dialer, err := dialerManager.Build(&dynamic.TCPServersLoadBalancer{ServersTransport: "test"}, true) require.NoError(t, err) - conn, err := dialer.Dial("tcp", ":"+port) + conn, err := dialer.Dial("tcp", ":"+port, nil) if test.wantError { require.Error(t, err) @@ -510,10 +510,13 @@ func TestProxyProtocol(t *testing.T) { require.NoError(t, err) var version int + var localAddr, remoteAddr string proxyBackendListener := proxyproto.Listener{ Listener: backendListener, ValidateHeader: func(h *proxyproto.Header) error { version = int(h.Version) + localAddr = h.DestinationAddr.String() + remoteAddr = h.SourceAddr.String() return nil }, Policy: func(upstream net.Addr) (proxyproto.Policy, error) { @@ -544,7 +547,18 @@ func TestProxyProtocol(t *testing.T) { dialer, err := dialerManager.Build(&dynamic.TCPServersLoadBalancer{ServersTransport: "test"}, false) require.NoError(t, err) - conn, err := dialer.Dial("tcp", ":"+port) + clientConn := &fakeClientConn{ + localAddr: &net.TCPAddr{ + IP: net.ParseIP("2.2.2.2"), + Port: 12345, + }, + remoteAddr: &net.TCPAddr{ + IP: net.ParseIP("1.1.1.1"), + Port: 12345, + }, + } + + conn, err := dialer.Dial("tcp", ":"+port, clientConn) require.NoError(t, err) defer conn.Close() @@ -558,6 +572,8 @@ func TestProxyProtocol(t *testing.T) { assert.Equal(t, 4, n) assert.Equal(t, "PONG", string(buf[:4])) assert.Equal(t, test.version, version) + assert.Equal(t, "2.2.2.2:12345", localAddr) + assert.Equal(t, "1.1.1.1:12345", remoteAddr) }) } } @@ -586,10 +602,13 @@ func TestProxyProtocolWithTLS(t *testing.T) { require.NoError(t, err) var version int + var localAddr, remoteAddr string proxyBackendListener := proxyproto.Listener{ Listener: backendListener, ValidateHeader: func(h *proxyproto.Header) error { version = int(h.Version) + localAddr = h.DestinationAddr.String() + remoteAddr = h.SourceAddr.String() return nil }, Policy: func(upstream net.Addr) (proxyproto.Policy, error) { @@ -646,7 +665,18 @@ func TestProxyProtocolWithTLS(t *testing.T) { }, true) require.NoError(t, err) - conn, err := dialer.Dial("tcp", ":"+port) + clientConn := &fakeClientConn{ + localAddr: &net.TCPAddr{ + IP: net.ParseIP("2.2.2.2"), + Port: 12345, + }, + remoteAddr: &net.TCPAddr{ + IP: net.ParseIP("1.1.1.1"), + Port: 12345, + }, + } + + conn, err := dialer.Dial("tcp", ":"+port, clientConn) require.NoError(t, err) defer conn.Close() @@ -660,6 +690,8 @@ func TestProxyProtocolWithTLS(t *testing.T) { assert.Equal(t, 4, n) assert.Equal(t, "PONG", string(buf[:4])) assert.Equal(t, test.version, version) + assert.Equal(t, "2.2.2.2:12345", localAddr) + assert.Equal(t, "1.1.1.1:12345", remoteAddr) }) } } @@ -695,7 +727,7 @@ func TestProxyProtocolDisabled(t *testing.T) { dialer, err := dialerManager.Build(&dynamic.TCPServersLoadBalancer{ServersTransport: "test"}, false) require.NoError(t, err) - conn, err := dialer.Dial("tcp", ":"+port) + conn, err := dialer.Dial("tcp", ":"+port, nil) require.NoError(t, err) _, err = conn.Write([]byte("ping")) @@ -709,6 +741,19 @@ func TestProxyProtocolDisabled(t *testing.T) { assert.Equal(t, "PONG", string(buf[:4])) } +type fakeClientConn struct { + remoteAddr *net.TCPAddr + localAddr *net.TCPAddr +} + +func (f fakeClientConn) LocalAddr() net.Addr { + return f.localAddr +} + +func (f fakeClientConn) RemoteAddr() net.Addr { + return f.remoteAddr +} + // fakeSpiffePKI simulates a SPIFFE aware PKI and allows generating multiple valid SVIDs. type fakeSpiffePKI struct { caPrivateKey *rsa.PrivateKey diff --git a/pkg/tcp/proxy.go b/pkg/tcp/proxy.go index 75bdfcd4d..aa979d303 100644 --- a/pkg/tcp/proxy.go +++ b/pkg/tcp/proxy.go @@ -34,7 +34,7 @@ func (p *Proxy) ServeTCP(conn WriteCloser) { // needed because of e.g. server.trackedConnection defer conn.Close() - connBackend, err := p.dialBackend() + connBackend, err := p.dialBackend(conn) if err != nil { log.Error().Err(err).Msg("Error while dialing backend") return @@ -62,8 +62,10 @@ func (p *Proxy) ServeTCP(conn WriteCloser) { <-errChan } -func (p *Proxy) dialBackend() (WriteCloser, error) { - conn, err := p.dialer.Dial("tcp", p.address) +func (p *Proxy) dialBackend(clientConn net.Conn) (WriteCloser, error) { + // The clientConn is passed to the dialer so that it can use information from it if needed, + // to build a PROXY protocol header. + conn, err := p.dialer.Dial("tcp", p.address, clientConn) if err != nil { return nil, err } diff --git a/webui/public/traefiklabs-hub-button-app/main-v1.js b/webui/public/traefiklabs-hub-button-app/main-v1.js index 9a90cc6b2..e140dab34 100644 --- a/webui/public/traefiklabs-hub-button-app/main-v1.js +++ b/webui/public/traefiklabs-hub-button-app/main-v1.js @@ -1,3 +1,23 @@ /* eslint-disable */ -!function(){var e={110:function(e,t,n){"use strict";var r=n(441),a={childContextTypes:!0,contextType:!0,contextTypes:!0,defaultProps:!0,displayName:!0,getDefaultProps:!0,getDerivedStateFromError:!0,getDerivedStateFromProps:!0,mixins:!0,propTypes:!0,type:!0},l={name:!0,length:!0,prototype:!0,caller:!0,callee:!0,arguments:!0,arity:!0},o={$$typeof:!0,compare:!0,defaultProps:!0,displayName:!0,propTypes:!0,type:!0},i={};function u(e){return r.isMemo(e)?o:i[e.$$typeof]||a}i[r.ForwardRef]={$$typeof:!0,render:!0,defaultProps:!0,displayName:!0,propTypes:!0},i[r.Memo]=o;var s=Object.defineProperty,c=Object.getOwnPropertyNames,f=Object.getOwnPropertySymbols,d=Object.getOwnPropertyDescriptor,p=Object.getPrototypeOf,h=Object.prototype;e.exports=function e(t,n,r){if("string"!==typeof n){if(h){var a=p(n);a&&a!==h&&e(t,a,r)}var o=c(n);f&&(o=o.concat(f(n)));for(var i=u(t),m=u(n),v=0;v