mirror of
https://github.com/traefik/traefik.git
synced 2026-05-04 20:06:21 +02:00
Add maxResponseBodySize configuration to forwardAuth middleware
This commit is contained in:
parent
288e4e2e2b
commit
4595c7a920
@ -637,4 +637,65 @@ http:
|
||||
[http.middlewares.test-auth.forwardAuth.tls]
|
||||
insecureSkipVerify: true
|
||||
```
|
||||
|
||||
### `maxResponseBodySize`
|
||||
|
||||
_Optional, Default=-1_
|
||||
|
||||
The `maxResponseBodySize` option defines the maximum allowed response body size in bytes from the authentication server.
|
||||
If the response body exceeds the configured limit, the request is rejected with a 401 (Unauthorized) status.
|
||||
If left unset, the request body size is unrestricted which can have performance or security implications.
|
||||
|
||||
```yaml tab="Docker"
|
||||
labels:
|
||||
- "traefik.http.middlewares.test-auth.forwardauth.maxResponseBodySize=10000"
|
||||
```
|
||||
|
||||
```yaml tab="Kubernetes"
|
||||
apiVersion: traefik.io/v1alpha1
|
||||
kind: Middleware
|
||||
metadata:
|
||||
name: test-auth
|
||||
spec:
|
||||
forwardAuth:
|
||||
address: https://example.com/auth
|
||||
maxResponseBodySize: 10000
|
||||
```
|
||||
|
||||
```yaml tab="Consul Catalog"
|
||||
- "traefik.http.middlewares.test-auth.forwardauth.maxResponseBodySize=10000"
|
||||
```
|
||||
|
||||
```json tab="Marathon"
|
||||
"labels": {
|
||||
"traefik.http.middlewares.test-auth.forwardauth.maxResponseBodySize": "10000"
|
||||
}
|
||||
```
|
||||
|
||||
```yaml tab="Rancher"
|
||||
labels:
|
||||
- "traefik.http.middlewares.test-auth.forwardauth.maxResponseBodySize=10000"
|
||||
```
|
||||
|
||||
```yaml tab="File (YAML)"
|
||||
http:
|
||||
middlewares:
|
||||
test-auth:
|
||||
forwardAuth:
|
||||
address: "https://example.com/auth"
|
||||
maxResponseBodySize: 10000
|
||||
```
|
||||
|
||||
```toml tab="File (TOML)"
|
||||
[http.middlewares]
|
||||
[http.middlewares.test-auth.forwardAuth]
|
||||
address = "https://example.com/auth"
|
||||
maxResponseBodySize = 10000
|
||||
```
|
||||
|
||||
!!! warning
|
||||
|
||||
It is strongly recommended to set this option to a suitable value.
|
||||
Not setting it (or setting it to `-1`) allows unlimited response body sizes which can lead to DoS attacks and memory exhaustion.
|
||||
|
||||
{% include-markdown "includes/traefik-for-business-applications.md" %}
|
||||
|
||||
@ -764,3 +764,14 @@ in [RFC3986 section-3](https://datatracker.ietf.org/doc/html/rfc3986#section-3).
|
||||
|
||||
Please check out the entrypoint [encodedCharacters option](../routing/entrypoints.md#encoded-characters) documentation
|
||||
for more details.
|
||||
|
||||
## v2.11.38
|
||||
|
||||
### `maxResponseBodySize` configuration on ForwardAuth middleware
|
||||
|
||||
In `v2.11.38`, a new `maxResponseBodySize` option has been added to the ForwardAuth middleware configuration.
|
||||
The default value for this option is -1, which means there is no limit to the response body size.
|
||||
However, it is strongly recommended to set this option to a suitable value to avoid performance and security issues,
|
||||
such as DoS attacks and memory exhaustion.
|
||||
|
||||
Please check out the [ForwardAuth](../middlewares/http/forwardauth.md#maxresponsebodysize) middleware documentation for more details.
|
||||
|
||||
@ -32,6 +32,7 @@
|
||||
- "traefik.http.middlewares.middleware10.forwardauth.authrequestheaders=foobar, foobar"
|
||||
- "traefik.http.middlewares.middleware10.forwardauth.authresponseheaders=foobar, foobar"
|
||||
- "traefik.http.middlewares.middleware10.forwardauth.authresponseheadersregex=foobar"
|
||||
- "traefik.http.middlewares.middleware10.forwardauth.maxresponsebodysize=42"
|
||||
- "traefik.http.middlewares.middleware10.forwardauth.tls.ca=foobar"
|
||||
- "traefik.http.middlewares.middleware10.forwardauth.tls.caoptional=true"
|
||||
- "traefik.http.middlewares.middleware10.forwardauth.tls.cert=foobar"
|
||||
|
||||
@ -155,6 +155,7 @@
|
||||
authResponseHeaders = ["foobar", "foobar"]
|
||||
authResponseHeadersRegex = "foobar"
|
||||
authRequestHeaders = ["foobar", "foobar"]
|
||||
maxResponseBodySize = 42
|
||||
[http.middlewares.Middleware10.forwardAuth.tls]
|
||||
ca = "foobar"
|
||||
caOptional = true
|
||||
|
||||
@ -176,6 +176,7 @@ http:
|
||||
authRequestHeaders:
|
||||
- foobar
|
||||
- foobar
|
||||
maxResponseBodySize: 42
|
||||
Middleware11:
|
||||
headers:
|
||||
customRequestHeaders:
|
||||
|
||||
@ -1001,6 +1001,11 @@ spec:
|
||||
AuthResponseHeadersRegex defines the regex to match headers to copy from the authentication server response and set on forwarded request, after stripping all headers that match the regex.
|
||||
More info: https://doc.traefik.io/traefik/v2.11/middlewares/http/forwardauth/#authresponseheadersregex
|
||||
type: string
|
||||
maxResponseBodySize:
|
||||
description: MaxResponseBodySize defines the maximum body size
|
||||
in bytes allowed in the response from the authentication server.
|
||||
format: int64
|
||||
type: integer
|
||||
tls:
|
||||
description: TLS defines the configuration used to secure the
|
||||
connection to the authentication server.
|
||||
|
||||
@ -43,6 +43,7 @@ THIS FILE MUST NOT BE EDITED BY HAND
|
||||
| `traefik/http/middlewares/Middleware10/forwardAuth/authResponseHeaders/0` | `foobar` |
|
||||
| `traefik/http/middlewares/Middleware10/forwardAuth/authResponseHeaders/1` | `foobar` |
|
||||
| `traefik/http/middlewares/Middleware10/forwardAuth/authResponseHeadersRegex` | `foobar` |
|
||||
| `traefik/http/middlewares/Middleware10/forwardAuth/maxResponseBodySize` | `42` |
|
||||
| `traefik/http/middlewares/Middleware10/forwardAuth/tls/ca` | `foobar` |
|
||||
| `traefik/http/middlewares/Middleware10/forwardAuth/tls/caOptional` | `true` |
|
||||
| `traefik/http/middlewares/Middleware10/forwardAuth/tls/cert` | `foobar` |
|
||||
|
||||
@ -32,6 +32,7 @@
|
||||
"traefik.http.middlewares.middleware10.forwardauth.authrequestheaders": "foobar, foobar",
|
||||
"traefik.http.middlewares.middleware10.forwardauth.authresponseheaders": "foobar, foobar",
|
||||
"traefik.http.middlewares.middleware10.forwardauth.authresponseheadersregex": "foobar",
|
||||
"traefik.http.middlewares.middleware10.forwardauth.maxresponsebodysize": "42",
|
||||
"traefik.http.middlewares.middleware10.forwardauth.tls.ca": "foobar",
|
||||
"traefik.http.middlewares.middleware10.forwardauth.tls.caoptional": "true",
|
||||
"traefik.http.middlewares.middleware10.forwardauth.tls.cert": "foobar",
|
||||
|
||||
@ -386,6 +386,11 @@ spec:
|
||||
AuthResponseHeadersRegex defines the regex to match headers to copy from the authentication server response and set on forwarded request, after stripping all headers that match the regex.
|
||||
More info: https://doc.traefik.io/traefik/v2.11/middlewares/http/forwardauth/#authresponseheadersregex
|
||||
type: string
|
||||
maxResponseBodySize:
|
||||
description: MaxResponseBodySize defines the maximum body size
|
||||
in bytes allowed in the response from the authentication server.
|
||||
format: int64
|
||||
type: integer
|
||||
tls:
|
||||
description: TLS defines the configuration used to secure the
|
||||
connection to the authentication server.
|
||||
|
||||
@ -1001,6 +1001,11 @@ spec:
|
||||
AuthResponseHeadersRegex defines the regex to match headers to copy from the authentication server response and set on forwarded request, after stripping all headers that match the regex.
|
||||
More info: https://doc.traefik.io/traefik/v2.11/middlewares/http/forwardauth/#authresponseheadersregex
|
||||
type: string
|
||||
maxResponseBodySize:
|
||||
description: MaxResponseBodySize defines the maximum body size
|
||||
in bytes allowed in the response from the authentication server.
|
||||
format: int64
|
||||
type: integer
|
||||
tls:
|
||||
description: TLS defines the configuration used to secure the
|
||||
connection to the authentication server.
|
||||
|
||||
@ -216,6 +216,8 @@ type ForwardAuth struct {
|
||||
// AuthRequestHeaders defines the list of the headers to copy from the request to the authentication server.
|
||||
// If not set or empty then all request headers are passed.
|
||||
AuthRequestHeaders []string `json:"authRequestHeaders,omitempty" toml:"authRequestHeaders,omitempty" yaml:"authRequestHeaders,omitempty" export:"true"`
|
||||
// MaxResponseBodySize defines the maximum body size in bytes allowed in the response from the authentication server.
|
||||
MaxResponseBodySize *int64 `json:"maxResponseBodySize,omitempty" toml:"maxResponseBodySize,omitempty" yaml:"maxResponseBodySize,omitempty" export:"true"`
|
||||
}
|
||||
|
||||
// +k8s:deepcopy-gen=true
|
||||
|
||||
@ -324,6 +324,11 @@ func (in *ForwardAuth) DeepCopyInto(out *ForwardAuth) {
|
||||
*out = make([]string, len(*in))
|
||||
copy(*out, *in)
|
||||
}
|
||||
if in.MaxResponseBodySize != nil {
|
||||
in, out := &in.MaxResponseBodySize, &out.MaxResponseBodySize
|
||||
*out = new(int64)
|
||||
**out = **in
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@ -50,6 +50,7 @@ func TestDecodeConfiguration(t *testing.T) {
|
||||
"traefik.http.middlewares.Middleware7.forwardauth.tls.insecureskipverify": "true",
|
||||
"traefik.http.middlewares.Middleware7.forwardauth.tls.key": "foobar",
|
||||
"traefik.http.middlewares.Middleware7.forwardauth.trustforwardheader": "true",
|
||||
"traefik.http.middlewares.Middleware7.forwardauth.maxresponsebodysize": "42",
|
||||
"traefik.http.middlewares.Middleware8.headers.accesscontrolallowcredentials": "true",
|
||||
"traefik.http.middlewares.Middleware8.headers.allowedhosts": "foobar, fiibar",
|
||||
"traefik.http.middlewares.Middleware8.headers.accesscontrolallowheaders": "X-foobar, X-fiibar",
|
||||
@ -547,6 +548,7 @@ func TestDecodeConfiguration(t *testing.T) {
|
||||
"foobar",
|
||||
"fiibar",
|
||||
},
|
||||
MaxResponseBodySize: pointer[int64](42),
|
||||
},
|
||||
},
|
||||
"Middleware8": {
|
||||
@ -1060,6 +1062,7 @@ func TestEncodeConfiguration(t *testing.T) {
|
||||
"foobar",
|
||||
"fiibar",
|
||||
},
|
||||
MaxResponseBodySize: pointer[int64](42),
|
||||
},
|
||||
},
|
||||
"Middleware8": {
|
||||
@ -1259,6 +1262,7 @@ func TestEncodeConfiguration(t *testing.T) {
|
||||
"traefik.HTTP.Middlewares.Middleware7.ForwardAuth.TLS.InsecureSkipVerify": "true",
|
||||
"traefik.HTTP.Middlewares.Middleware7.ForwardAuth.TLS.Key": "foobar",
|
||||
"traefik.HTTP.Middlewares.Middleware7.ForwardAuth.TrustForwardHeader": "true",
|
||||
"traefik.HTTP.Middlewares.Middleware7.ForwardAuth.MaxResponseBodySize": "42",
|
||||
"traefik.HTTP.Middlewares.Middleware8.Headers.AccessControlAllowCredentials": "true",
|
||||
"traefik.HTTP.Middlewares.Middleware8.Headers.AccessControlAllowHeaders": "X-foobar, X-fiibar",
|
||||
"traefik.HTTP.Middlewares.Middleware8.Headers.AccessControlAllowMethods": "GET, PUT",
|
||||
|
||||
@ -47,11 +47,13 @@ type forwardAuth struct {
|
||||
client http.Client
|
||||
trustForwardHeader bool
|
||||
authRequestHeaders []string
|
||||
maxResponseBodySize int64
|
||||
}
|
||||
|
||||
// NewForward creates a forward auth middleware.
|
||||
func NewForward(ctx context.Context, next http.Handler, config dynamic.ForwardAuth, name string) (http.Handler, error) {
|
||||
log.FromContext(middlewares.GetLoggerCtx(ctx, name, forwardedTypeName)).Debug("Creating middleware")
|
||||
logger := log.FromContext(middlewares.GetLoggerCtx(ctx, name, forwardedTypeName))
|
||||
logger.Debug("Creating middleware")
|
||||
|
||||
fa := &forwardAuth{
|
||||
address: config.Address,
|
||||
@ -62,6 +64,13 @@ func NewForward(ctx context.Context, next http.Handler, config dynamic.ForwardAu
|
||||
authRequestHeaders: config.AuthRequestHeaders,
|
||||
}
|
||||
|
||||
if config.MaxResponseBodySize != nil {
|
||||
fa.maxResponseBodySize = *config.MaxResponseBodySize
|
||||
} else {
|
||||
fa.maxResponseBodySize = -1
|
||||
logger.Warn("ForwardAuth 'maxResponseBodySize' is not configured, allowing unlimited response body size which can lead to DoS attacks and memory exhaustion. Please set an appropriate limit.")
|
||||
}
|
||||
|
||||
// Ensure our request client does not follow redirects
|
||||
fa.client = http.Client{
|
||||
CheckRedirect: func(r *http.Request, via []*http.Request) error {
|
||||
@ -125,9 +134,16 @@ func (fa *forwardAuth) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
|
||||
}
|
||||
defer forwardResponse.Body.Close()
|
||||
|
||||
body, readError := io.ReadAll(forwardResponse.Body)
|
||||
body, readError := fa.readResponseBodyBytes(forwardResponse)
|
||||
if readError != nil {
|
||||
logger.Debugf("Error reading body %s. Cause: %s", fa.address, readError)
|
||||
if errors.Is(readError, errResponseBodyTooLarge) {
|
||||
logger.Debugf("Response body is too large, maxResponseBodySize: %d", fa.maxResponseBodySize)
|
||||
|
||||
tracing.SetErrorWithEvent(req, "Response body is too large, maxResponseBodySize: %d", fa.maxResponseBodySize)
|
||||
rw.WriteHeader(http.StatusUnauthorized)
|
||||
return
|
||||
}
|
||||
logger.Debugf("Error reading body %s", fa.address)
|
||||
tracing.SetErrorWithEvent(req, "Error reading body %s. Cause: %s", fa.address, readError)
|
||||
|
||||
rw.WriteHeader(http.StatusInternalServerError)
|
||||
@ -193,6 +209,27 @@ func (fa *forwardAuth) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
|
||||
fa.next.ServeHTTP(rw, req)
|
||||
}
|
||||
|
||||
var errResponseBodyTooLarge = errors.New("response body too large")
|
||||
|
||||
func (fa *forwardAuth) readResponseBodyBytes(res *http.Response) ([]byte, error) {
|
||||
if fa.maxResponseBodySize < 0 {
|
||||
return io.ReadAll(res.Body)
|
||||
}
|
||||
|
||||
body := make([]byte, fa.maxResponseBodySize+1)
|
||||
n, err := io.ReadFull(res.Body, body)
|
||||
if errors.Is(err, io.EOF) {
|
||||
return nil, nil
|
||||
}
|
||||
if err != nil && !errors.Is(err, io.ErrUnexpectedEOF) {
|
||||
return nil, fmt.Errorf("reading response body bytes: %w", err)
|
||||
}
|
||||
if errors.Is(err, io.ErrUnexpectedEOF) {
|
||||
return body[:n], nil
|
||||
}
|
||||
return nil, errResponseBodyTooLarge
|
||||
}
|
||||
|
||||
func writeHeader(req, forwardReq *http.Request, trustForwardHeader bool, allowedHeaders []string) {
|
||||
utils.CopyHeaders(forwardReq.Header, req.Header)
|
||||
|
||||
|
||||
@ -482,6 +482,89 @@ func TestForwardAuthUsesTracing(t *testing.T) {
|
||||
assert.Equal(t, http.StatusOK, res.StatusCode)
|
||||
}
|
||||
|
||||
func Test_ForwardAuthMaxResponseBodySize(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
maxResponseBodySize int64
|
||||
status int
|
||||
body string
|
||||
expectedStatus int
|
||||
expectedBody string
|
||||
}{
|
||||
{
|
||||
name: "auth failure, unlimited response body",
|
||||
maxResponseBodySize: -1,
|
||||
status: http.StatusForbidden,
|
||||
body: "Forbidden",
|
||||
expectedStatus: http.StatusForbidden,
|
||||
expectedBody: "Forbidden",
|
||||
},
|
||||
{
|
||||
name: "auth failure, response body exceeds the limit",
|
||||
maxResponseBodySize: 1,
|
||||
status: http.StatusForbidden,
|
||||
body: "Forbidden",
|
||||
expectedStatus: http.StatusUnauthorized,
|
||||
expectedBody: "",
|
||||
},
|
||||
{
|
||||
name: "auth success within limit",
|
||||
maxResponseBodySize: 100,
|
||||
status: http.StatusOK,
|
||||
body: "ok",
|
||||
expectedStatus: http.StatusOK,
|
||||
expectedBody: "traefik\n",
|
||||
},
|
||||
{
|
||||
name: "auth success body exceeds limit",
|
||||
maxResponseBodySize: 1,
|
||||
status: http.StatusOK,
|
||||
body: "large auth response",
|
||||
expectedStatus: http.StatusUnauthorized,
|
||||
expectedBody: "",
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range testCases {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(test.status)
|
||||
fmt.Fprint(w, test.body)
|
||||
}))
|
||||
t.Cleanup(server.Close)
|
||||
|
||||
next := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
fmt.Fprintln(w, "traefik")
|
||||
}))
|
||||
|
||||
maxResponseBodySize := test.maxResponseBodySize
|
||||
auth := dynamic.ForwardAuth{
|
||||
Address: server.URL,
|
||||
MaxResponseBodySize: &maxResponseBodySize,
|
||||
}
|
||||
|
||||
middleware, err := NewForward(t.Context(), next, auth, "maxResponseBodySizeTest")
|
||||
require.NoError(t, err)
|
||||
|
||||
ts := httptest.NewServer(middleware)
|
||||
t.Cleanup(ts.Close)
|
||||
|
||||
req := testhelpers.MustNewRequest(http.MethodGet, ts.URL, nil)
|
||||
res, err := http.DefaultClient.Do(req)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, test.expectedStatus, res.StatusCode)
|
||||
|
||||
body, err := io.ReadAll(res.Body)
|
||||
require.NoError(t, err)
|
||||
err = res.Body.Close()
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, test.expectedBody, string(body))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
type mockBackend struct {
|
||||
opentracing.Tracer
|
||||
}
|
||||
|
||||
@ -652,6 +652,10 @@ func createForwardAuthMiddleware(k8sClient Client, namespace string, auth *traef
|
||||
AuthRequestHeaders: auth.AuthRequestHeaders,
|
||||
}
|
||||
|
||||
if auth.MaxResponseBodySize != nil {
|
||||
forwardAuth.MaxResponseBodySize = auth.MaxResponseBodySize
|
||||
}
|
||||
|
||||
if auth.TLS == nil {
|
||||
return forwardAuth, nil
|
||||
}
|
||||
|
||||
@ -155,6 +155,8 @@ type ForwardAuth struct {
|
||||
AuthRequestHeaders []string `json:"authRequestHeaders,omitempty"`
|
||||
// TLS defines the configuration used to secure the connection to the authentication server.
|
||||
TLS *ClientTLS `json:"tls,omitempty"`
|
||||
// MaxResponseBodySize defines the maximum body size in bytes allowed in the response from the authentication server.
|
||||
MaxResponseBodySize *int64 `json:"maxResponseBodySize,omitempty"`
|
||||
}
|
||||
|
||||
// ClientTLS holds the client TLS configuration.
|
||||
|
||||
@ -215,6 +215,11 @@ func (in *ForwardAuth) DeepCopyInto(out *ForwardAuth) {
|
||||
*out = new(ClientTLS)
|
||||
**out = **in
|
||||
}
|
||||
if in.MaxResponseBodySize != nil {
|
||||
in, out := &in.MaxResponseBodySize, &out.MaxResponseBodySize
|
||||
*out = new(int64)
|
||||
**out = **in
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@ -83,6 +83,7 @@ func Test_buildConfiguration(t *testing.T) {
|
||||
"traefik/http/middlewares/Middleware08/forwardAuth/tls/cert": "foobar",
|
||||
"traefik/http/middlewares/Middleware08/forwardAuth/address": "foobar",
|
||||
"traefik/http/middlewares/Middleware08/forwardAuth/trustForwardHeader": "true",
|
||||
"traefik/http/middlewares/Middleware08/forwardAuth/maxResponseBodySize": "42",
|
||||
"traefik/http/middlewares/Middleware15/redirectScheme/scheme": "foobar",
|
||||
"traefik/http/middlewares/Middleware15/redirectScheme/port": "foobar",
|
||||
"traefik/http/middlewares/Middleware15/redirectScheme/permanent": "true",
|
||||
@ -427,6 +428,7 @@ func Test_buildConfiguration(t *testing.T) {
|
||||
"foobar",
|
||||
"foobar",
|
||||
},
|
||||
MaxResponseBodySize: pointer[int64](42),
|
||||
},
|
||||
},
|
||||
"Middleware06": {
|
||||
|
||||
@ -287,6 +287,7 @@ func init() {
|
||||
AuthResponseHeaders: []string{"foo"},
|
||||
AuthResponseHeadersRegex: "foo",
|
||||
AuthRequestHeaders: []string{"foo"},
|
||||
MaxResponseBodySize: pointer[int64](42),
|
||||
},
|
||||
InFlightReq: &dynamic.InFlightReq{
|
||||
Amount: 42,
|
||||
|
||||
@ -247,7 +247,8 @@
|
||||
"authResponseHeadersRegex": "foo",
|
||||
"authRequestHeaders": [
|
||||
"foo"
|
||||
]
|
||||
],
|
||||
"maxResponseBodySize": 42
|
||||
},
|
||||
"inFlightReq": {
|
||||
"amount": 42,
|
||||
|
||||
@ -250,7 +250,8 @@
|
||||
"authResponseHeadersRegex": "foo",
|
||||
"authRequestHeaders": [
|
||||
"foo"
|
||||
]
|
||||
],
|
||||
"maxResponseBodySize": 42
|
||||
},
|
||||
"inFlightReq": {
|
||||
"amount": 42,
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user