diff --git a/docs/content/middlewares/http/ipallowlist.md b/docs/content/middlewares/http/ipallowlist.md index 703b9f89be..19e1c86bc2 100644 --- a/docs/content/middlewares/http/ipallowlist.md +++ b/docs/content/middlewares/http/ipallowlist.md @@ -207,3 +207,45 @@ http: [http.middlewares.test-ipallowlist.ipAllowList.ipStrategy] excludedIPs = ["127.0.0.1/32", "192.168.1.7"] ``` + +### `rejectStatusCode` + +The `rejectStatusCode` option sets HTTP status code for refused requests. If not set, the default is 403 (Forbidden). + +```yaml tab="Docker & Swarm" +# Reject requests with a 404 rather than a 403 +labels: + - "traefik.http.middlewares.test-ipallowlist.ipallowlist.rejectstatuscode=404" +``` + +```yaml tab="Kubernetes" +# Reject requests with a 404 rather than a 403 +apiVersion: traefik.io/v1alpha1 +kind: Middleware +metadata: + name: test-ipallowlist +spec: + ipAllowList: + rejectStatusCode: 404 +``` + +```yaml tab="Consul Catalog" +# Reject requests with a 404 rather than a 403 +- "traefik.http.middlewares.test-ipallowlist.ipallowlist.rejectstatuscode=404" +``` + +```yaml tab="File (YAML)" +# Reject requests with a 404 rather than a 403 +http: + middlewares: + test-ipallowlist: + ipAllowList: + rejectStatusCode: 404 +``` + +```toml tab="File (TOML)" +# Reject requests with a 404 rather than a 403 +[http.middlewares] + [http.middlewares.test-ipallowlist.ipAllowList] + rejectStatusCode = 404 +``` diff --git a/docs/content/reference/dynamic-configuration/docker-labels.yml b/docs/content/reference/dynamic-configuration/docker-labels.yml index 26ef425c7a..4f7ef62c71 100644 --- a/docs/content/reference/dynamic-configuration/docker-labels.yml +++ b/docs/content/reference/dynamic-configuration/docker-labels.yml @@ -68,6 +68,7 @@ - "traefik.http.middlewares.middleware11.ipallowlist.ipstrategy.depth=42" - "traefik.http.middlewares.middleware11.ipallowlist.ipstrategy.excludedips=foobar, foobar" - "traefik.http.middlewares.middleware11.ipallowlist.sourcerange=foobar, foobar" +- "traefik.http.middlewares.middleware11.ipallowlist.rejectstatuscode=404" - "traefik.http.middlewares.middleware12.inflightreq.amount=42" - "traefik.http.middlewares.middleware12.inflightreq.sourcecriterion.ipstrategy.depth=42" - "traefik.http.middlewares.middleware12.inflightreq.sourcecriterion.ipstrategy.excludedips=foobar, foobar" diff --git a/docs/content/reference/dynamic-configuration/file.toml b/docs/content/reference/dynamic-configuration/file.toml index 2e72be2aa9..4c1e5c593e 100644 --- a/docs/content/reference/dynamic-configuration/file.toml +++ b/docs/content/reference/dynamic-configuration/file.toml @@ -199,6 +199,7 @@ [http.middlewares.Middleware11] [http.middlewares.Middleware11.ipAllowList] sourceRange = ["foobar", "foobar"] + rejectStatusCode = 404 [http.middlewares.Middleware11.ipAllowList.ipStrategy] depth = 42 excludedIPs = ["foobar", "foobar"] diff --git a/docs/content/reference/dynamic-configuration/file.yaml b/docs/content/reference/dynamic-configuration/file.yaml index aa61082de4..368ea3d5b1 100644 --- a/docs/content/reference/dynamic-configuration/file.yaml +++ b/docs/content/reference/dynamic-configuration/file.yaml @@ -225,6 +225,7 @@ http: isDevelopment: true Middleware11: ipAllowList: + rejectStatusCode: 404 sourceRange: - foobar - foobar 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 66e5ca1b0d..504c5cd9c2 100644 --- a/docs/content/reference/dynamic-configuration/kubernetes-crd-definition-v1.yml +++ b/docs/content/reference/dynamic-configuration/kubernetes-crd-definition-v1.yml @@ -1181,6 +1181,10 @@ spec: type: string type: array type: object + rejectStatusCode: + description: RejectStatusCode defines the HTTP status code used + for refused requests. If not set, the default is 403 (Forbidden). + type: integer sourceRange: description: SourceRange defines the set of allowed IPs (or ranges of allowed IPs by using CIDR notation). diff --git a/docs/content/reference/dynamic-configuration/kv-ref.md b/docs/content/reference/dynamic-configuration/kv-ref.md index ce15fb1f27..ef5a7154f9 100644 --- a/docs/content/reference/dynamic-configuration/kv-ref.md +++ b/docs/content/reference/dynamic-configuration/kv-ref.md @@ -81,6 +81,7 @@ | `traefik/http/middlewares/Middleware11/ipAllowList/ipStrategy/depth` | `42` | | `traefik/http/middlewares/Middleware11/ipAllowList/ipStrategy/excludedIPs/0` | `foobar` | | `traefik/http/middlewares/Middleware11/ipAllowList/ipStrategy/excludedIPs/1` | `foobar` | +| `traefik/http/middlewares/Middleware11/ipAllowList/rejectStatusCode` | `404` | | `traefik/http/middlewares/Middleware11/ipAllowList/sourceRange/0` | `foobar` | | `traefik/http/middlewares/Middleware11/ipAllowList/sourceRange/1` | `foobar` | | `traefik/http/middlewares/Middleware12/inFlightReq/amount` | `42` | diff --git a/docs/content/reference/dynamic-configuration/traefik.io_middlewares.yaml b/docs/content/reference/dynamic-configuration/traefik.io_middlewares.yaml index 4885a6315c..1290a6163c 100644 --- a/docs/content/reference/dynamic-configuration/traefik.io_middlewares.yaml +++ b/docs/content/reference/dynamic-configuration/traefik.io_middlewares.yaml @@ -606,6 +606,10 @@ spec: type: string type: array type: object + rejectStatusCode: + description: RejectStatusCode defines the HTTP status code used + for refused requests. If not set, the default is 403 (Forbidden). + type: integer sourceRange: description: SourceRange defines the set of allowed IPs (or ranges of allowed IPs by using CIDR notation). diff --git a/integration/fixtures/k8s/01-traefik-crd.yml b/integration/fixtures/k8s/01-traefik-crd.yml index 66e5ca1b0d..504c5cd9c2 100644 --- a/integration/fixtures/k8s/01-traefik-crd.yml +++ b/integration/fixtures/k8s/01-traefik-crd.yml @@ -1181,6 +1181,10 @@ spec: type: string type: array type: object + rejectStatusCode: + description: RejectStatusCode defines the HTTP status code used + for refused requests. If not set, the default is 403 (Forbidden). + type: integer sourceRange: description: SourceRange defines the set of allowed IPs (or ranges of allowed IPs by using CIDR notation). diff --git a/pkg/config/dynamic/middlewares.go b/pkg/config/dynamic/middlewares.go index 6ab6e5a050..a73881a2ac 100644 --- a/pkg/config/dynamic/middlewares.go +++ b/pkg/config/dynamic/middlewares.go @@ -383,6 +383,9 @@ type IPAllowList struct { // SourceRange defines the set of allowed IPs (or ranges of allowed IPs by using CIDR notation). SourceRange []string `json:"sourceRange,omitempty" toml:"sourceRange,omitempty" yaml:"sourceRange,omitempty"` IPStrategy *IPStrategy `json:"ipStrategy,omitempty" toml:"ipStrategy,omitempty" yaml:"ipStrategy,omitempty" label:"allowEmpty" file:"allowEmpty" kv:"allowEmpty" export:"true"` + // RejectStatusCode defines the HTTP status code used for refused requests. + // If not set, the default is 403 (Forbidden). + RejectStatusCode int `json:"rejectStatusCode,omitempty" toml:"rejectStatusCode,omitempty" yaml:"rejectStatusCode,omitempty" label:"allowEmpty" file:"allowEmpty" kv:"allowEmpty" export:"true"` } // +k8s:deepcopy-gen=true diff --git a/pkg/config/label/label_test.go b/pkg/config/label/label_test.go index 334297edce..c55a73c15d 100644 --- a/pkg/config/label/label_test.go +++ b/pkg/config/label/label_test.go @@ -1254,6 +1254,7 @@ func TestEncodeConfiguration(t *testing.T) { "traefik.HTTP.Middlewares.Middleware8.Headers.STSSeconds": "42", "traefik.HTTP.Middlewares.Middleware9.IPAllowList.IPStrategy.Depth": "42", "traefik.HTTP.Middlewares.Middleware9.IPAllowList.IPStrategy.ExcludedIPs": "foobar, fiibar", + "traefik.HTTP.Middlewares.Middleware9.IPAllowList.RejectStatusCode": "0", "traefik.HTTP.Middlewares.Middleware9.IPAllowList.SourceRange": "foobar, fiibar", "traefik.HTTP.Middlewares.Middleware10.InFlightReq.Amount": "42", "traefik.HTTP.Middlewares.Middleware10.InFlightReq.SourceCriterion.IPStrategy.Depth": "42", diff --git a/pkg/middlewares/ipallowlist/ip_allowlist.go b/pkg/middlewares/ipallowlist/ip_allowlist.go index ddd7bfe47a..3841f3f23d 100644 --- a/pkg/middlewares/ipallowlist/ip_allowlist.go +++ b/pkg/middlewares/ipallowlist/ip_allowlist.go @@ -20,10 +20,11 @@ const ( // ipAllowLister is a middleware that provides Checks of the Requesting IP against a set of Allowlists. type ipAllowLister struct { - next http.Handler - allowLister *ip.Checker - strategy ip.Strategy - name string + next http.Handler + allowLister *ip.Checker + strategy ip.Strategy + name string + rejectStatusCode int } // New builds a new IPAllowLister given a list of CIDR-Strings to allow. @@ -35,6 +36,14 @@ func New(ctx context.Context, next http.Handler, config dynamic.IPAllowList, nam return nil, errors.New("sourceRange is empty, IPAllowLister not created") } + rejectStatusCode := config.RejectStatusCode + // If RejectStatusCode is not given, default to Forbidden (403). + if rejectStatusCode == 0 { + rejectStatusCode = http.StatusForbidden + } else if http.StatusText(rejectStatusCode) == "" { + return nil, fmt.Errorf("invalid HTTP status code %d", rejectStatusCode) + } + checker, err := ip.NewChecker(config.SourceRange) if err != nil { return nil, fmt.Errorf("cannot parse CIDRs %s: %w", config.SourceRange, err) @@ -48,10 +57,11 @@ func New(ctx context.Context, next http.Handler, config dynamic.IPAllowList, nam logger.Debug().Msgf("Setting up IPAllowLister with sourceRange: %s", config.SourceRange) return &ipAllowLister{ - strategy: strategy, - allowLister: checker, - next: next, - name: name, + strategy: strategy, + allowLister: checker, + next: next, + name: name, + rejectStatusCode: rejectStatusCode, }, nil } @@ -69,7 +79,7 @@ func (al *ipAllowLister) ServeHTTP(rw http.ResponseWriter, req *http.Request) { msg := fmt.Sprintf("Rejecting IP %s: %v", clientIP, err) logger.Debug().Msg(msg) tracing.SetStatusErrorf(req.Context(), msg) - reject(ctx, rw) + reject(ctx, al.rejectStatusCode, rw) return } logger.Debug().Msgf("Accepting IP %s", clientIP) @@ -77,9 +87,7 @@ func (al *ipAllowLister) ServeHTTP(rw http.ResponseWriter, req *http.Request) { al.next.ServeHTTP(rw, req) } -func reject(ctx context.Context, rw http.ResponseWriter) { - statusCode := http.StatusForbidden - +func reject(ctx context.Context, statusCode int, rw http.ResponseWriter) { rw.WriteHeader(statusCode) _, err := rw.Write([]byte(http.StatusText(statusCode))) if err != nil { diff --git a/pkg/middlewares/ipallowlist/ip_allowlist_test.go b/pkg/middlewares/ipallowlist/ip_allowlist_test.go index 8bd26b3eb5..cea46e4010 100644 --- a/pkg/middlewares/ipallowlist/ip_allowlist_test.go +++ b/pkg/middlewares/ipallowlist/ip_allowlist_test.go @@ -30,6 +30,14 @@ func TestNewIPAllowLister(t *testing.T) { SourceRange: []string{"10.10.10.10"}, }, }, + { + desc: "invalid HTTP status code", + allowList: dynamic.IPAllowList{ + SourceRange: []string{"10.10.10.10"}, + RejectStatusCode: 600, + }, + expectedError: true, + }, } for _, test := range testCases { @@ -73,6 +81,24 @@ func TestIPAllowLister_ServeHTTP(t *testing.T) { remoteAddr: "20.20.20.21:1234", expected: 403, }, + { + desc: "authorized with remote address, reject 404", + allowList: dynamic.IPAllowList{ + SourceRange: []string{"20.20.20.20"}, + RejectStatusCode: 404, + }, + remoteAddr: "20.20.20.20:1234", + expected: 200, + }, + { + desc: "non authorized with remote address, reject 404", + allowList: dynamic.IPAllowList{ + SourceRange: []string{"20.20.20.20"}, + RejectStatusCode: 404, + }, + remoteAddr: "20.20.20.21:1234", + expected: 404, + }, } for _, test := range testCases {