Skip to content

Commit 698960e

Browse files
authored
Config/Annotations: Add relative-redirects. (#12161)
1 parent 0207d18 commit 698960e

File tree

8 files changed

+182
-0
lines changed

8 files changed

+182
-0
lines changed

docs/user-guide/nginx-configuration/annotations-risk.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,7 @@
103103
| Redirect | from-to-www-redirect | Low | location |
104104
| Redirect | permanent-redirect | Medium | location |
105105
| Redirect | permanent-redirect-code | Low | location |
106+
| Redirect | relative-redirects | Low | location |
106107
| Redirect | temporal-redirect | Medium | location |
107108
| Redirect | temporal-redirect-code | Low | location |
108109
| Rewrite | app-root | Medium | location |

docs/user-guide/nginx-configuration/configmap.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -223,6 +223,7 @@ The following table shows a configuration option's name, type, and the default v
223223
| [debug-connections](#debug-connections) | []string | "127.0.0.1,1.1.1.1/24" | |
224224
| [strict-validate-path-type](#strict-validate-path-type) | bool | "true" | |
225225
| [grpc-buffer-size-kb](#grpc-buffer-size-kb) | int | 0 | |
226+
| [relative-redirects](#relative-redirects) | bool | false | |
226227

227228
## add-headers
228229

@@ -1382,3 +1383,14 @@ Sets the configuration for the GRPC Buffer Size parameter. If not set it will us
13821383

13831384
_References:_
13841385
[https://nginx.org/en/docs/http/ngx_http_grpc_module.html#grpc_buffer_size](https://nginx.org/en/docs/http/ngx_http_grpc_module.html#grpc_buffer_size)
1386+
1387+
## relative-redirects
1388+
1389+
Use relative redirects instead of absolute redirects. Absolute redirects are the default in nginx. RFC7231 allows relative redirects since 2014.
1390+
Similar to the Ingress rule annotation `nginx.ingress.kubernetes.io/relative-redirects`.
1391+
1392+
_**default:**_ "false"
1393+
1394+
_References:_
1395+
- [https://nginx.org/en/docs/http/ngx_http_core_module.html#absolute_redirect](https://nginx.org/en/docs/http/ngx_http_core_module.html#absolute_redirect)
1396+
- [https://datatracker.ietf.org/doc/html/rfc7231#section-7.1.2](https://datatracker.ietf.org/doc/html/rfc7231#section-7.1.2)

internal/ingress/annotations/redirect/redirect.go

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ type Config struct {
3838
URL string `json:"url"`
3939
Code int `json:"code"`
4040
FromToWWW bool `json:"fromToWWW"`
41+
Relative bool `json:"relative"`
4142
}
4243

4344
const (
@@ -46,6 +47,7 @@ const (
4647
temporalRedirectAnnotationCode = "temporal-redirect-code"
4748
permanentRedirectAnnotation = "permanent-redirect"
4849
permanentRedirectAnnotationCode = "permanent-redirect-code"
50+
relativeRedirectsAnnotation = "relative-redirects"
4951
)
5052

5153
var redirectAnnotations = parser.Annotation{
@@ -83,6 +85,12 @@ var redirectAnnotations = parser.Annotation{
8385
Risk: parser.AnnotationRiskLow, // Low, as it allows just a set of options
8486
Documentation: `This annotation allows you to modify the status code used for permanent redirects.`,
8587
},
88+
relativeRedirectsAnnotation: {
89+
Validator: parser.ValidateBool,
90+
Scope: parser.AnnotationScopeLocation,
91+
Risk: parser.AnnotationRiskLow,
92+
Documentation: `If enabled, redirects issued by nginx will be relative. See https://nginx.org/en/docs/http/ngx_http_core_module.html#absolute_redirect`,
93+
},
8694
},
8795
}
8896

@@ -109,6 +117,11 @@ func (r redirect) Parse(ing *networking.Ingress) (interface{}, error) {
109117
return nil, err
110118
}
111119

120+
rr, err := parser.GetBoolAnnotation(relativeRedirectsAnnotation, ing, r.annotationConfig.Annotations)
121+
if err != nil && !errors.IsMissingAnnotations(err) {
122+
return nil, err
123+
}
124+
112125
tr, err := parser.GetStringAnnotation(temporalRedirectAnnotation, ing, r.annotationConfig.Annotations)
113126
if err != nil && !errors.IsMissingAnnotations(err) {
114127
return nil, err
@@ -132,6 +145,7 @@ func (r redirect) Parse(ing *networking.Ingress) (interface{}, error) {
132145
URL: tr,
133146
Code: trc,
134147
FromToWWW: r3w,
148+
Relative: rr,
135149
}, nil
136150
}
137151

@@ -154,6 +168,13 @@ func (r redirect) Parse(ing *networking.Ingress) (interface{}, error) {
154168
URL: pr,
155169
Code: prc,
156170
FromToWWW: r3w,
171+
Relative: rr,
172+
}, nil
173+
}
174+
175+
if rr {
176+
return &Config{
177+
Relative: rr,
157178
}, nil
158179
}
159180

@@ -177,6 +198,9 @@ func (r1 *Config) Equal(r2 *Config) bool {
177198
if r1.FromToWWW != r2.FromToWWW {
178199
return false
179200
}
201+
if r1.Relative != r2.Relative {
202+
return false
203+
}
180204
return true
181205
}
182206

internal/ingress/annotations/redirect/redirect_test.go

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -193,3 +193,22 @@ func TestIsValidURL(t *testing.T) {
193193
t.Errorf("expected nil but got %v", err)
194194
}
195195
}
196+
197+
func TestParseAnnotations(t *testing.T) {
198+
ing := new(networking.Ingress)
199+
200+
data := map[string]string{}
201+
data[parser.GetAnnotationWithPrefix(relativeRedirectsAnnotation)] = "true"
202+
ing.SetAnnotations(data)
203+
204+
_, err := NewParser(&resolver.Mock{}).Parse(ing)
205+
if err != nil {
206+
t.Errorf("unexpected error: %v", err)
207+
}
208+
209+
// test ingress using the annotation without a TLS section
210+
_, err = NewParser(&resolver.Mock{}).Parse(ing)
211+
if err != nil {
212+
t.Errorf("unexpected error parsing ingress with relative-redirects")
213+
}
214+
}

internal/ingress/controller/config/config.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -549,6 +549,10 @@ type Configuration struct {
549549
// https://nginx.org/en/docs/http/ngx_http_proxy_module.html#proxy_intercept_errors
550550
DisableProxyInterceptErrors bool `json:"disable-proxy-intercept-errors,omitempty"`
551551

552+
// Disable absolute redirects and enables relative redirects.
553+
// https://nginx.org/en/docs/http/ngx_http_core_module.html#absolute_redirect
554+
RelativeRedirects bool `json:"relative-redirects"`
555+
552556
// Sets the ipv4 addresses on which the server will accept requests.
553557
BindAddressIpv4 []string `json:"bind-address-ipv4,omitempty"`
554558

@@ -834,6 +838,7 @@ func NewDefault() Configuration {
834838
VariablesHashMaxSize: 2048,
835839
UseHTTP2: true,
836840
DisableProxyInterceptErrors: false,
841+
RelativeRedirects: false,
837842
ProxyStreamTimeout: "600s",
838843
ProxyStreamNextUpstream: true,
839844
ProxyStreamNextUpstreamTimeout: "600s",
@@ -857,6 +862,7 @@ func NewDefault() Configuration {
857862
SSLRedirect: true,
858863
CustomHTTPErrors: []int{},
859864
DisableProxyInterceptErrors: false,
865+
RelativeRedirects: false,
860866
DenylistSourceRange: []string{},
861867
WhitelistSourceRange: []string{},
862868
SkipAccessLogURLs: []string{},

internal/ingress/defaults/main.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,11 @@ type Backend struct {
125125
// Default: false
126126
UsePortInRedirects bool `json:"use-port-in-redirects"`
127127

128+
// Enables or disables relative redirects. By default nginx uses absolute redirects.
129+
// http://nginx.org/en/docs/http/ngx_http_core_module.html#absolute_redirect
130+
// Default: false
131+
RelativeRedirects bool `json:"relative-redirects"`
132+
128133
// Enable stickiness by client-server mapping based on a NGINX variable, text or a combination of both.
129134
// A consistent hashing method will be used which ensures only a few keys would be remapped to different
130135
// servers on upstream group changes

rootfs/etc/nginx/template/nginx.tmpl

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -459,6 +459,10 @@ http {
459459
proxy_intercept_errors on;
460460
{{ end }}
461461

462+
{{ if $cfg.RelativeRedirects }}
463+
absolute_redirect off;
464+
{{ end }}
465+
462466
{{ range $errCode := $cfg.CustomHTTPErrors }}
463467
error_page {{ $errCode }} = @custom_upstream-default-backend_{{ $errCode }};{{ end }}
464468

@@ -1343,6 +1347,10 @@ stream {
13431347
satisfy {{ $location.Satisfy }};
13441348
{{ end }}
13451349

1350+
{{ if $location.Redirect.Relative }}
1351+
absolute_redirect off;
1352+
{{ end }}
1353+
13461354
{{/* if a location-specific error override is set, add the proxy_intercept here */}}
13471355
{{ if and $location.CustomHTTPErrors (not $location.DisableProxyInterceptErrors) }}
13481356
# Custom error pages per ingress
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
/*
2+
Copyright 2023 The Kubernetes Authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
package annotations
18+
19+
import (
20+
"fmt"
21+
"net/http"
22+
"strings"
23+
24+
"github.com/onsi/ginkgo/v2"
25+
"github.com/stretchr/testify/assert"
26+
"k8s.io/ingress-nginx/test/e2e/framework"
27+
)
28+
29+
const (
30+
relativeRedirectsHostname = "rr.foo.com"
31+
relativeRedirectsRedirectPath = "/something"
32+
relativeRedirectsRelativeRedirectURL = "/new-location"
33+
)
34+
35+
var _ = framework.DescribeAnnotation("relative-redirects", func() {
36+
f := framework.NewDefaultFramework("relative-redirects")
37+
38+
ginkgo.BeforeEach(func() {
39+
f.NewHttpbunDeployment()
40+
f.NewEchoDeployment()
41+
})
42+
43+
ginkgo.It("configures Nginx correctly", func() {
44+
annotations := map[string]string{
45+
"nginx.ingress.kubernetes.io/relative-redirects": "true",
46+
}
47+
48+
ing := framework.NewSingleIngress(relativeRedirectsHostname, "/", relativeRedirectsHostname, f.Namespace, framework.HTTPBunService, 80, annotations)
49+
f.EnsureIngress(ing)
50+
51+
var serverConfig string
52+
f.WaitForNginxServer(relativeRedirectsHostname, func(srvCfg string) bool {
53+
serverConfig = srvCfg
54+
return strings.Contains(serverConfig, fmt.Sprintf("server_name %s", relativeRedirectsHostname))
55+
})
56+
57+
ginkgo.By("turning off absolute_redirect directive")
58+
assert.Contains(ginkgo.GinkgoT(), serverConfig, "absolute_redirect off;")
59+
})
60+
61+
ginkgo.It("should respond with absolute URL in Location", func() {
62+
absoluteRedirectURL := fmt.Sprintf("http://%s%s", relativeRedirectsHostname, relativeRedirectsRelativeRedirectURL)
63+
annotations := map[string]string{
64+
"nginx.ingress.kubernetes.io/permanent-redirect": relativeRedirectsRelativeRedirectURL,
65+
"nginx.ingress.kubernetes.io/relative-redirects": "false",
66+
}
67+
68+
ginkgo.By("setup ingress")
69+
ing := framework.NewSingleIngress(relativeRedirectsHostname, relativeRedirectsRedirectPath, relativeRedirectsHostname, f.Namespace, framework.EchoService, 80, annotations)
70+
f.EnsureIngress(ing)
71+
72+
f.WaitForNginxServer(relativeRedirectsHostname, func(srvCfg string) bool {
73+
return strings.Contains(srvCfg, fmt.Sprintf("server_name %s", relativeRedirectsHostname))
74+
})
75+
76+
ginkgo.By("sending request to redirected URL path")
77+
f.HTTPTestClient().
78+
GET(relativeRedirectsRedirectPath).
79+
WithHeader("Host", relativeRedirectsHostname).
80+
Expect().
81+
Status(http.StatusMovedPermanently).
82+
Header("Location").Equal(absoluteRedirectURL)
83+
})
84+
85+
ginkgo.It("should respond with relative URL in Location", func() {
86+
annotations := map[string]string{
87+
"nginx.ingress.kubernetes.io/permanent-redirect": relativeRedirectsRelativeRedirectURL,
88+
"nginx.ingress.kubernetes.io/relative-redirects": "true",
89+
}
90+
91+
ginkgo.By("setup ingress")
92+
ing := framework.NewSingleIngress(relativeRedirectsHostname, relativeRedirectsRedirectPath, relativeRedirectsHostname, f.Namespace, framework.EchoService, 80, annotations)
93+
f.EnsureIngress(ing)
94+
95+
f.WaitForNginxServer(relativeRedirectsHostname, func(srvCfg string) bool {
96+
return strings.Contains(srvCfg, fmt.Sprintf("server_name %s", relativeRedirectsHostname))
97+
})
98+
99+
ginkgo.By("sending request to redirected URL path")
100+
f.HTTPTestClient().
101+
GET(relativeRedirectsRedirectPath).
102+
WithHeader("Host", relativeRedirectsHostname).
103+
Expect().
104+
Status(http.StatusMovedPermanently).
105+
Header("Location").Equal(relativeRedirectsRelativeRedirectURL)
106+
})
107+
})

0 commit comments

Comments
 (0)