Skip to content

Commit 1d9a00f

Browse files
committed
Config/Annotations: Add ssl-forbid-http and force-ssl-forbid-http
1 parent 5ae018e commit 1d9a00f

File tree

13 files changed

+304
-1
lines changed

13 files changed

+304
-1
lines changed

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

+2
Original file line numberDiff line numberDiff line change
@@ -107,9 +107,11 @@
107107
| Redirect | temporal-redirect | Medium | location |
108108
| Redirect | temporal-redirect-code | Low | location |
109109
| Rewrite | app-root | Medium | location |
110+
| Rewrite | force-ssl-forbid-http | Medium | location |
110111
| Rewrite | force-ssl-redirect | Medium | location |
111112
| Rewrite | preserve-trailing-slash | Medium | location |
112113
| Rewrite | rewrite-target | Medium | ingress |
114+
| Rewrite | ssl-forbid-http | Low | location |
113115
| Rewrite | ssl-redirect | Low | location |
114116
| Rewrite | use-regex | Low | location |
115117
| SSLCipher | ssl-ciphers | Low | ingress |

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

+16
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ You can add these Kubernetes annotations to specific Ingress objects to customiz
5959
|[nginx.ingress.kubernetes.io/cors-expose-headers](#enable-cors)|string|
6060
|[nginx.ingress.kubernetes.io/cors-allow-credentials](#enable-cors)|"true" or "false"|
6161
|[nginx.ingress.kubernetes.io/cors-max-age](#enable-cors)|number|
62+
|[nginx.ingress.kubernetes.io/force-ssl-forbid-http](#server-side-https-enforcement-through-forbidden-errors)|"true" or "false"|
6263
|[nginx.ingress.kubernetes.io/force-ssl-redirect](#server-side-https-enforcement-through-redirect)|"true" or "false"|
6364
|[nginx.ingress.kubernetes.io/from-to-www-redirect](#redirect-fromto-www)|"true" or "false"|
6465
|[nginx.ingress.kubernetes.io/http2-push-preload](#http2-push-preload)|"true" or "false"|
@@ -104,6 +105,7 @@ You can add these Kubernetes annotations to specific Ingress objects to customiz
104105
|[nginx.ingress.kubernetes.io/session-cookie-path](#cookie-affinity)|string|
105106
|[nginx.ingress.kubernetes.io/session-cookie-samesite](#cookie-affinity)|string|"None", "Lax" or "Strict"|
106107
|[nginx.ingress.kubernetes.io/session-cookie-secure](#cookie-affinity)|string|
108+
|[nginx.ingress.kubernetes.io/ssl-forbid-http](#server-side-https-enforcement-through-forbidden-errors)|"true" or "false"|
107109
|[nginx.ingress.kubernetes.io/ssl-redirect](#server-side-https-enforcement-through-redirect)|"true" or "false"|
108110
|[nginx.ingress.kubernetes.io/ssl-passthrough](#ssl-passthrough)|"true" or "false"|
109111
|[nginx.ingress.kubernetes.io/stream-snippet](#stream-snippet)|string|
@@ -621,6 +623,20 @@ This can be achieved by using the `nginx.ingress.kubernetes.io/force-ssl-redirec
621623
622624
To preserve the trailing slash in the URI with `ssl-redirect`, set `nginx.ingress.kubernetes.io/preserve-trailing-slash: "true"` annotation for that particular resource.
623625

626+
### Server-side HTTPS enforcement through forbidden errors
627+
628+
In certain scenarios, you might prefer to return a 403 Forbidden Error response instead of redirecting traffic to the HTTPS port.
629+
This approach helps prevent misconfigured clients from inadvertently leaking sensitive data over unencrypted connections.
630+
631+
This can be enabled globally using `ssl-forbid-http: "true"` in the NGINX [ConfigMap][./configmap.md#ssl-forbid-http].
632+
633+
To configure this feature for specific ingress resources, you can use the `nginx.ingress.kubernetes.io/ssl-forbid-http: "true"`
634+
annotation in the particular resource.
635+
636+
When using SSL offloading outside of cluster (e.g. AWS ELB) it may be useful to enforce forbidden errors to HTTP requests
637+
even when there is no TLS certificate available.
638+
This can be achieved by using the `nginx.ingress.kubernetes.io/force-ssl-forbid-http: "true"` annotation in the particular resource.
639+
624640
### Redirect from/to www
625641
626642
In some scenarios, it is required to redirect from `www.domain.com` to `domain.com` or vice versa, which way the redirect is performed depends on the configured `host` value in the Ingress object.

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

+12
Original file line numberDiff line numberDiff line change
@@ -188,6 +188,8 @@ The following table shows a configuration option's name, type, and the default v
188188
| [proxy-request-buffering](#proxy-request-buffering) | string | "on" | |
189189
| [ssl-redirect](#ssl-redirect) | bool | "true" | |
190190
| [force-ssl-redirect](#force-ssl-redirect) | bool | "false" | |
191+
| [ssl-forbid-http](#ssl-forbid-http) | bool | "false" | |
192+
| [force-ssl-forbid-http](#force-ssl-forbid-http) | bool | "false" | |
191193
| [denylist-source-range](#denylist-source-range) | []string | []string{} | |
192194
| [whitelist-source-range](#whitelist-source-range) | []string | []string{} | |
193195
| [skip-access-log-urls](#skip-access-log-urls) | []string | []string{} | |
@@ -1149,6 +1151,16 @@ _**default:**_ "true"
11491151
Sets the global value of redirects (308) to HTTPS if the server has a default TLS certificate (defined in extra-args).
11501152
_**default:**_ "false"
11511153

1154+
## ssl-forbid-http
1155+
1156+
Sets the global value of forbidden errors (403) to HTTP if the server has a TLS certificate (defined in an Ingress rule).
1157+
_**default:**_ "false"
1158+
1159+
## force-ssl-forbid-http
1160+
1161+
Sets the global value of forbidden errors (403) to HTTP if the server has a default TLS certificate (defined in extra-args).
1162+
_**default:**_ "false"
1163+
11521164
## denylist-source-range
11531165

11541166
Sets the default denylisted IPs for each `server` block. This can be overwritten by an annotation on an Ingress rule.

docs/user-guide/tls.md

+11-1
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ HSTS is enabled by default.
7878

7979
To disable this behavior use `hsts: "false"` in the configuration [ConfigMap][ConfigMap].
8080

81-
## Server-side HTTPS enforcement through redirect
81+
## Server-side HTTPS enforcement
8282

8383
By default the controller redirects HTTP clients to the HTTPS port
8484
443 using a 308 Permanent Redirect response if TLS is enabled for that Ingress.
@@ -87,12 +87,22 @@ This can be disabled globally using `ssl-redirect: "false"` in the NGINX [config
8787
or per-Ingress with the `nginx.ingress.kubernetes.io/ssl-redirect: "false"`
8888
annotation in the particular resource.
8989

90+
In certain scenarios, you might prefer to return a 403 Forbidden Error response instead of redirecting traffic to the HTTPS port.
91+
This approach helps prevent misconfigured clients from inadvertently leaking sensitive data over unencrypted connections.
92+
93+
This can be enabled globally using `ssl-forbid-http: "true"` in the NGINX [config map][ConfigMap],
94+
or per-Ingress with the `nginx.ingress.kubernetes.io/ssl-forbid-http: "true"` annotation in the particular resource.
95+
9096
!!! tip
9197
When using SSL offloading outside of cluster (e.g. AWS ELB) it may be useful to enforce a
9298
redirect to HTTPS even when there is no TLS certificate available.
9399
This can be achieved by using the `nginx.ingress.kubernetes.io/force-ssl-redirect: "true"`
94100
annotation in the particular resource.
95101

102+
Similarly, you can enforce forbidden errors to HTTP requests using the
103+
`nginx.ingress.kubernetes.io/force-ssl-forbid-http: "true"` annotation in the particular
104+
resource.
105+
96106
## Automated Certificate Management with cert-manager
97107

98108
[cert-manager] automatically requests missing or expired certificates from a range of

internal/ingress/annotations/rewrite/main.go

+40
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@ const (
3232
sslRedirectAnnotation = "ssl-redirect"
3333
preserveTrailingSlashAnnotation = "preserve-trailing-slash"
3434
forceSSLRedirectAnnotation = "force-ssl-redirect"
35+
sslForbidHTTPAnnotation = "ssl-forbid-http"
36+
forceSSLForbidHTTPAnnotation = "force-ssl-forbid-http"
3537
useRegexAnnotation = "use-regex"
3638
appRootAnnotation = "app-root"
3739
)
@@ -64,6 +66,18 @@ var rewriteAnnotations = parser.Annotation{
6466
Risk: parser.AnnotationRiskMedium,
6567
Documentation: `This annotation forces the redirection to HTTPS even if the Ingress is not TLS Enabled`,
6668
},
69+
sslForbidHTTPAnnotation: {
70+
Validator: parser.ValidateBool,
71+
Scope: parser.AnnotationScopeLocation,
72+
Risk: parser.AnnotationRiskLow,
73+
Documentation: `This annotation defines if the location section should forbid HTTP requests`,
74+
},
75+
forceSSLForbidHTTPAnnotation: {
76+
Validator: parser.ValidateBool,
77+
Scope: parser.AnnotationScopeLocation,
78+
Risk: parser.AnnotationRiskMedium,
79+
Documentation: `This annotation forces the forbidden error to HTTP even if the Ingress is not TLS Enabled`,
80+
},
6781
useRegexAnnotation: {
6882
Validator: parser.ValidateBool,
6983
Scope: parser.AnnotationScopeLocation,
@@ -88,6 +102,10 @@ type Config struct {
88102
SSLRedirect bool `json:"sslRedirect"`
89103
// ForceSSLRedirect indicates if the location section is accessible SSL only
90104
ForceSSLRedirect bool `json:"forceSSLRedirect"`
105+
// SSLForbidHTTP indicates if the location section is accessible SSL only
106+
SSLForbidHTTP bool `json:"sslForbidHTTP"`
107+
// ForceSSLForbidHTTP indicates if the location section is accessible SSL only
108+
ForceSSLForbidHTTP bool `json:"forceSSLForbidHTTP"`
91109
// PreserveTrailingSlash indicates if the trailing slash should be kept during a tls redirect
92110
PreserveTrailingSlash bool `json:"preserveTrailingSlash"`
93111
// AppRoot defines the Application Root that the Controller must redirect if it's in '/' context
@@ -113,6 +131,12 @@ func (r1 *Config) Equal(r2 *Config) bool {
113131
if r1.ForceSSLRedirect != r2.ForceSSLRedirect {
114132
return false
115133
}
134+
if r1.SSLForbidHTTP != r2.SSLForbidHTTP {
135+
return false
136+
}
137+
if r1.ForceSSLForbidHTTP != r2.ForceSSLForbidHTTP {
138+
return false
139+
}
116140
if r1.AppRoot != r2.AppRoot {
117141
return false
118142
}
@@ -172,6 +196,22 @@ func (a rewrite) Parse(ing *networking.Ingress) (interface{}, error) {
172196
config.ForceSSLRedirect = a.r.GetDefaultBackend().ForceSSLRedirect
173197
}
174198

199+
config.SSLForbidHTTP, err = parser.GetBoolAnnotation(sslForbidHTTPAnnotation, ing, a.annotationConfig.Annotations)
200+
if err != nil {
201+
if errors.IsValidationError(err) {
202+
klog.Warningf("%s is invalid, defaulting to '%t'", sslForbidHTTPAnnotation, a.r.GetDefaultBackend().SSLForbidHTTP)
203+
}
204+
config.SSLForbidHTTP = a.r.GetDefaultBackend().SSLForbidHTTP
205+
}
206+
207+
config.ForceSSLForbidHTTP, err = parser.GetBoolAnnotation(forceSSLForbidHTTPAnnotation, ing, a.annotationConfig.Annotations)
208+
if err != nil {
209+
if errors.IsValidationError(err) {
210+
klog.Warningf("%s is invalid, defaulting to '%t'", forceSSLForbidHTTPAnnotation, a.r.GetDefaultBackend().ForceSSLForbidHTTP)
211+
}
212+
config.ForceSSLForbidHTTP = a.r.GetDefaultBackend().ForceSSLForbidHTTP
213+
}
214+
175215
config.UseRegex, err = parser.GetBoolAnnotation(useRegexAnnotation, ing, a.annotationConfig.Annotations)
176216
if err != nil {
177217
if errors.IsValidationError(err) {

internal/ingress/annotations/rewrite/main_test.go

+64
Original file line numberDiff line numberDiff line change
@@ -213,6 +213,70 @@ func TestForceSSLRedirect(t *testing.T) {
213213
}
214214
}
215215

216+
func TestSSLForbidHTTP(t *testing.T) {
217+
ing := buildIngress()
218+
219+
i, err := NewParser(mockBackend{}).Parse(ing)
220+
if err != nil {
221+
t.Errorf("unexpected error: %v", err)
222+
}
223+
forbid, ok := i.(*Config)
224+
if !ok {
225+
t.Errorf("expected a Forbid type")
226+
}
227+
if forbid.SSLForbidHTTP {
228+
t.Errorf("Expected false but returned true")
229+
}
230+
231+
data := map[string]string{}
232+
data[parser.GetAnnotationWithPrefix("ssl-forbid-http")] = "true"
233+
ing.SetAnnotations(data)
234+
235+
i, err = NewParser(mockBackend{}).Parse(ing)
236+
if err != nil {
237+
t.Errorf("unexpected error: %v", err)
238+
}
239+
forbid, ok = i.(*Config)
240+
if !ok {
241+
t.Errorf("expected a Forbid type")
242+
}
243+
if !forbid.SSLForbidHTTP {
244+
t.Errorf("Expected true but returned false")
245+
}
246+
}
247+
248+
func TestForceSSLForbidHTTP(t *testing.T) {
249+
ing := buildIngress()
250+
251+
i, err := NewParser(mockBackend{}).Parse(ing)
252+
if err != nil {
253+
t.Errorf("unexpected error: %v", err)
254+
}
255+
forbid, ok := i.(*Config)
256+
if !ok {
257+
t.Errorf("expected a Forbid type")
258+
}
259+
if forbid.ForceSSLForbidHTTP {
260+
t.Errorf("Expected false but returned true")
261+
}
262+
263+
data := map[string]string{}
264+
data[parser.GetAnnotationWithPrefix("force-ssl-forbid-http")] = "true"
265+
ing.SetAnnotations(data)
266+
267+
i, err = NewParser(mockBackend{}).Parse(ing)
268+
if err != nil {
269+
t.Errorf("unexpected error: %v", err)
270+
}
271+
forbid, ok = i.(*Config)
272+
if !ok {
273+
t.Errorf("expected a Forbid type")
274+
}
275+
if !forbid.ForceSSLForbidHTTP {
276+
t.Errorf("Expected true but returned false")
277+
}
278+
}
279+
216280
func TestAppRoot(t *testing.T) {
217281
ap := NewParser(mockBackend{redirect: true})
218282

internal/ingress/controller/template/template.go

+6
Original file line numberDiff line numberDiff line change
@@ -435,6 +435,8 @@ func locationConfigForLua(l, a interface{}) string {
435435
force_ssl_redirect = string_to_bool(ngx.var.force_ssl_redirect),
436436
ssl_redirect = string_to_bool(ngx.var.ssl_redirect),
437437
force_no_ssl_redirect = string_to_bool(ngx.var.force_no_ssl_redirect),
438+
force_ssl_forbid_http = string_to_bool(ngx.var.force_ssl_forbid_http),
439+
ssl_forbid_http = string_to_bool(ngx.var.ssl_forbid_http),
438440
preserve_trailing_slash = string_to_bool(ngx.var.preserve_trailing_slash),
439441
use_port_in_redirects = string_to_bool(ngx.var.use_port_in_redirects),
440442
*/
@@ -443,12 +445,16 @@ func locationConfigForLua(l, a interface{}) string {
443445
set $force_ssl_redirect "%t";
444446
set $ssl_redirect "%t";
445447
set $force_no_ssl_redirect "%t";
448+
set $force_ssl_forbid_http "%t";
449+
set $ssl_forbid_http "%t";
446450
set $preserve_trailing_slash "%t";
447451
set $use_port_in_redirects "%t";
448452
`,
449453
location.Rewrite.ForceSSLRedirect,
450454
location.Rewrite.SSLRedirect,
451455
isLocationInLocationList(l, all.Cfg.NoTLSRedirectLocations),
456+
location.Rewrite.ForceSSLForbidHTTP,
457+
location.Rewrite.SSLForbidHTTP,
452458
location.Rewrite.PreserveTrailingSlash,
453459
location.UsePortInRedirects,
454460
)

internal/ingress/defaults/main.go

+7
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,13 @@ type Backend struct {
121121
// This is useful if doing SSL offloading outside of cluster eg AWS ELB
122122
ForceSSLRedirect bool `json:"force-ssl-redirect"`
123123

124+
// Enables or disables forbidden errors (403) to HTTP
125+
SSLForbidHTTP bool `json:"ssl-forbid-http"`
126+
127+
// Enables or disables forbidden errors (403) to HTTP even without TLS cert
128+
// This is useful if doing SSL offloading outside of cluster eg AWS ELB
129+
ForceSSLForbidHTTP bool `json:"force-ssl-forbid-http"`
130+
124131
// Enables or disables the specification of port in redirects
125132
// Default: false
126133
UsePortInRedirects bool `json:"use-port-in-redirects"`

rootfs/etc/nginx/lua/lua_ingress.lua

+18
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,18 @@ local function randomseed()
6262
math.randomseed(seed)
6363
end
6464

65+
local function forbid_http(location_config)
66+
if location_config.force_ssl_forbid_http and ngx.var.pass_access_scheme == "http" then
67+
return true
68+
end
69+
70+
if ngx.var.pass_access_scheme ~= "http" then
71+
return false
72+
end
73+
74+
return location_config.ssl_forbid_http and certificate_configured_for_current_request()
75+
end
76+
6577
local function redirect_to_https(location_config)
6678
if location_config.force_no_ssl_redirect then
6779
return false
@@ -115,6 +127,8 @@ function _M.rewrite()
115127
force_ssl_redirect = string_to_bool(ngx.var.force_ssl_redirect),
116128
ssl_redirect = string_to_bool(ngx.var.ssl_redirect),
117129
force_no_ssl_redirect = string_to_bool(ngx.var.force_no_ssl_redirect),
130+
force_ssl_forbid_http = string_to_bool(ngx.var.force_ssl_forbid_http),
131+
ssl_forbid_http = string_to_bool(ngx.var.ssl_forbid_http),
118132
preserve_trailing_slash = string_to_bool(ngx.var.preserve_trailing_slash),
119133
use_port_in_redirects = string_to_bool(ngx.var.use_port_in_redirects),
120134
}
@@ -154,6 +168,10 @@ function _M.rewrite()
154168
ngx.var.pass_port = 443
155169
end
156170

171+
if forbid_http(location_config) then
172+
ngx.exit(ngx.HTTP_FORBIDDEN)
173+
end
174+
157175
if redirect_to_https(location_config) then
158176
local request_uri = ngx.var.request_uri
159177
-- do not append a trailing slash on redirects unless enabled by annotations

test/data/cleanConf.expected.conf

+2
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,8 @@ http {
115115
force_ssl_redirect = false,
116116
ssl_redirect = false,
117117
force_no_ssl_redirect = false,
118+
force_ssl_forbid_http = false,
119+
ssl_forbid_http = false,
118120
use_port_in_redirects = false,
119121
})
120122
balancer.rewrite()

test/data/cleanConf.src.conf

+2
Original file line numberDiff line numberDiff line change
@@ -158,6 +158,8 @@ lua_shared_dict ocsp_response_cache 5M;
158158
force_ssl_redirect = false,
159159
ssl_redirect = false,
160160
force_no_ssl_redirect = false,
161+
force_ssl_forbid_http = false,
162+
ssl_forbid_http = false,
161163
use_port_in_redirects = false,
162164
})
163165
balancer.rewrite()

0 commit comments

Comments
 (0)