Skip to content

Commit

Permalink
Config/Annotations: Add ssl-forbid-http and force-ssl-forbid-http
Browse files Browse the repository at this point in the history
  • Loading branch information
gavinkflam committed Nov 19, 2024
1 parent 6f868cb commit 5657bd6
Show file tree
Hide file tree
Showing 13 changed files with 304 additions and 1 deletion.
2 changes: 2 additions & 0 deletions docs/user-guide/nginx-configuration/annotations-risk.md
Original file line number Diff line number Diff line change
Expand Up @@ -107,9 +107,11 @@
| Redirect | temporal-redirect | Medium | location |
| Redirect | temporal-redirect-code | Low | location |
| Rewrite | app-root | Medium | location |
| Rewrite | force-ssl-forbid-http | Medium | location |
| Rewrite | force-ssl-redirect | Medium | location |
| Rewrite | preserve-trailing-slash | Medium | location |
| Rewrite | rewrite-target | Medium | ingress |
| Rewrite | ssl-forbid-http | Low | location |
| Rewrite | ssl-redirect | Low | location |
| Rewrite | use-regex | Low | location |
| SSLCipher | ssl-ciphers | Low | ingress |
Expand Down
16 changes: 16 additions & 0 deletions docs/user-guide/nginx-configuration/annotations.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ You can add these Kubernetes annotations to specific Ingress objects to customiz
|[nginx.ingress.kubernetes.io/cors-expose-headers](#enable-cors)|string|
|[nginx.ingress.kubernetes.io/cors-allow-credentials](#enable-cors)|"true" or "false"|
|[nginx.ingress.kubernetes.io/cors-max-age](#enable-cors)|number|
|[nginx.ingress.kubernetes.io/force-ssl-forbid-http](#server-side-https-enforcement-through-forbidden-errors)|"true" or "false"|
|[nginx.ingress.kubernetes.io/force-ssl-redirect](#server-side-https-enforcement-through-redirect)|"true" or "false"|
|[nginx.ingress.kubernetes.io/from-to-www-redirect](#redirect-fromto-www)|"true" or "false"|
|[nginx.ingress.kubernetes.io/http2-push-preload](#http2-push-preload)|"true" or "false"|
Expand Down Expand Up @@ -104,6 +105,7 @@ You can add these Kubernetes annotations to specific Ingress objects to customiz
|[nginx.ingress.kubernetes.io/session-cookie-path](#cookie-affinity)|string|
|[nginx.ingress.kubernetes.io/session-cookie-samesite](#cookie-affinity)|string|"None", "Lax" or "Strict"|
|[nginx.ingress.kubernetes.io/session-cookie-secure](#cookie-affinity)|string|
|[nginx.ingress.kubernetes.io/ssl-forbid-http](#server-side-https-enforcement-through-forbidden-errors)|"true" or "false"|
|[nginx.ingress.kubernetes.io/ssl-redirect](#server-side-https-enforcement-through-redirect)|"true" or "false"|
|[nginx.ingress.kubernetes.io/ssl-passthrough](#ssl-passthrough)|"true" or "false"|
|[nginx.ingress.kubernetes.io/stream-snippet](#stream-snippet)|string|
Expand Down Expand Up @@ -621,6 +623,20 @@ This can be achieved by using the `nginx.ingress.kubernetes.io/force-ssl-redirec
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.

### Server-side HTTPS enforcement through forbidden errors

In certain scenarios, you might prefer to return a 403 Forbidden Error response instead of redirecting traffic to the HTTPS port.
This approach helps prevent misconfigured clients from inadvertently leaking sensitive data over unencrypted connections.

This can be enabled globally using `ssl-forbid-http: "true"` in the NGINX [ConfigMap][./configmap.md#ssl-forbid-http].
To configure this feature for specific ingress resources, you can use the `nginx.ingress.kubernetes.io/ssl-forbid-http: "true"`
annotation in the particular resource.

When using SSL offloading outside of cluster (e.g. AWS ELB) it may be useful to enforce forbidden errors to HTTP requests
even when there is no TLS certificate available.
This can be achieved by using the `nginx.ingress.kubernetes.io/force-ssl-forbid-http: "true"` annotation in the particular resource.
### Redirect from/to www
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.
Expand Down
12 changes: 12 additions & 0 deletions docs/user-guide/nginx-configuration/configmap.md
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,8 @@ The following table shows a configuration option's name, type, and the default v
| [proxy-request-buffering](#proxy-request-buffering) | string | "on" | |
| [ssl-redirect](#ssl-redirect) | bool | "true" | |
| [force-ssl-redirect](#force-ssl-redirect) | bool | "false" | |
| [ssl-forbid-http](#ssl-forbid-http) | bool | "false" | |
| [force-ssl-forbid-http](#force-ssl-forbid-http) | bool | "false" | |
| [denylist-source-range](#denylist-source-range) | []string | []string{} | |
| [whitelist-source-range](#whitelist-source-range) | []string | []string{} | |
| [skip-access-log-urls](#skip-access-log-urls) | []string | []string{} | |
Expand Down Expand Up @@ -1149,6 +1151,16 @@ _**default:**_ "true"
Sets the global value of redirects (308) to HTTPS if the server has a default TLS certificate (defined in extra-args).
_**default:**_ "false"

## ssl-forbid-http

Sets the global value of forbidden errors (403) to HTTP if the server has a TLS certificate (defined in an Ingress rule).
_**default:**_ "false"

## force-ssl-forbid-http

Sets the global value of forbidden errors (403) to HTTP if the server has a default TLS certificate (defined in extra-args).
_**default:**_ "false"

## denylist-source-range

Sets the default denylisted IPs for each `server` block. This can be overwritten by an annotation on an Ingress rule.
Expand Down
12 changes: 11 additions & 1 deletion docs/user-guide/tls.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@ HSTS is enabled by default.

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

## Server-side HTTPS enforcement through redirect
## Server-side HTTPS enforcement

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

In certain scenarios, you might prefer to return a 403 Forbidden Error response instead of redirecting traffic to the HTTPS port.
This approach helps prevent misconfigured clients from inadvertently leaking sensitive data over unencrypted connections.

This can be enabled globally using `ssl-forbid-http: "true"` in the NGINX [config map][ConfigMap],
or per-Ingress with the `nginx.ingress.kubernetes.io/ssl-forbid-http: "true"` annotation in the particular resource.

!!! tip
When using SSL offloading outside of cluster (e.g. AWS ELB) it may be useful to enforce a
redirect to HTTPS even when there is no TLS certificate available.
This can be achieved by using the `nginx.ingress.kubernetes.io/force-ssl-redirect: "true"`
annotation in the particular resource.

Similarly, you can enforce forbidden errors to HTTP requests using the
`nginx.ingress.kubernetes.io/force-ssl-forbid-http: "true"` annotation in the particular
resource.

## Automated Certificate Management with cert-manager

[cert-manager] automatically requests missing or expired certificates from a range of
Expand Down
40 changes: 40 additions & 0 deletions internal/ingress/annotations/rewrite/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ const (
sslRedirectAnnotation = "ssl-redirect"
preserveTrailingSlashAnnotation = "preserve-trailing-slash"
forceSSLRedirectAnnotation = "force-ssl-redirect"
sslForbidHttpAnnotation = "ssl-forbid-http"

Check failure on line 35 in internal/ingress/annotations/rewrite/main.go

View workflow job for this annotation

GitHub Actions / lint

var-naming: const sslForbidHttpAnnotation should be sslForbidHTTPAnnotation (revive)
forceSSLForbidHttpAnnotation = "force-ssl-forbid-http"

Check failure on line 36 in internal/ingress/annotations/rewrite/main.go

View workflow job for this annotation

GitHub Actions / lint

var-naming: const forceSSLForbidHttpAnnotation should be forceSSLForbidHTTPAnnotation (revive)
useRegexAnnotation = "use-regex"
appRootAnnotation = "app-root"
)
Expand Down Expand Up @@ -64,6 +66,18 @@ var rewriteAnnotations = parser.Annotation{
Risk: parser.AnnotationRiskMedium,
Documentation: `This annotation forces the redirection to HTTPS even if the Ingress is not TLS Enabled`,
},
sslForbidHttpAnnotation: {
Validator: parser.ValidateBool,
Scope: parser.AnnotationScopeLocation,
Risk: parser.AnnotationRiskLow,
Documentation: `This annotation defines if the location section should forbid HTTP requests`,
},
forceSSLForbidHttpAnnotation: {
Validator: parser.ValidateBool,
Scope: parser.AnnotationScopeLocation,
Risk: parser.AnnotationRiskMedium,
Documentation: `This annotation forces the forbidden error to HTTP even if the Ingress is not TLS Enabled`,
},
useRegexAnnotation: {
Validator: parser.ValidateBool,
Scope: parser.AnnotationScopeLocation,
Expand All @@ -88,6 +102,10 @@ type Config struct {
SSLRedirect bool `json:"sslRedirect"`
// ForceSSLRedirect indicates if the location section is accessible SSL only
ForceSSLRedirect bool `json:"forceSSLRedirect"`
// SSLForbidHttp indicates if the location section should forbid HTTP
SSLForbidHttp bool `json:"sslForbidHttp"`

Check failure on line 106 in internal/ingress/annotations/rewrite/main.go

View workflow job for this annotation

GitHub Actions / lint

var-naming: struct field SSLForbidHttp should be SSLForbidHTTP (revive)
// ForceSSLForbidHttp indicates if the location section should forbid HTTP
ForceSSLForbidHttp bool `json:"forceSSLForbidHttp"`

Check failure on line 108 in internal/ingress/annotations/rewrite/main.go

View workflow job for this annotation

GitHub Actions / lint

var-naming: struct field ForceSSLForbidHttp should be ForceSSLForbidHTTP (revive)
// PreserveTrailingSlash indicates if the trailing slash should be kept during a tls redirect
PreserveTrailingSlash bool `json:"preserveTrailingSlash"`
// AppRoot defines the Application Root that the Controller must redirect if it's in '/' context
Expand All @@ -113,6 +131,12 @@ func (r1 *Config) Equal(r2 *Config) bool {
if r1.ForceSSLRedirect != r2.ForceSSLRedirect {
return false
}
if r1.SSLForbidHttp != r2.SSLForbidHttp {
return false
}
if r1.ForceSSLForbidHttp != r2.ForceSSLForbidHttp {
return false
}
if r1.AppRoot != r2.AppRoot {
return false
}
Expand Down Expand Up @@ -172,6 +196,22 @@ func (a rewrite) Parse(ing *networking.Ingress) (interface{}, error) {
config.ForceSSLRedirect = a.r.GetDefaultBackend().ForceSSLRedirect
}

config.SSLForbidHttp, err = parser.GetBoolAnnotation(sslForbidHttpAnnotation, ing, a.annotationConfig.Annotations)
if err != nil {
if errors.IsValidationError(err) {
klog.Warningf("%s is invalid, defaulting to '%t'", sslForbidHttpAnnotation, a.r.GetDefaultBackend().SSLForbidHttp)
}
config.SSLForbidHttp = a.r.GetDefaultBackend().SSLForbidHttp
}

config.ForceSSLForbidHttp, err = parser.GetBoolAnnotation(forceSSLForbidHttpAnnotation, ing, a.annotationConfig.Annotations)
if err != nil {
if errors.IsValidationError(err) {
klog.Warningf("%s is invalid, defaulting to '%t'", forceSSLForbidHttpAnnotation, a.r.GetDefaultBackend().ForceSSLForbidHttp)
}
config.ForceSSLForbidHttp = a.r.GetDefaultBackend().ForceSSLForbidHttp
}

config.UseRegex, err = parser.GetBoolAnnotation(useRegexAnnotation, ing, a.annotationConfig.Annotations)
if err != nil {
if errors.IsValidationError(err) {
Expand Down
64 changes: 64 additions & 0 deletions internal/ingress/annotations/rewrite/main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,70 @@ func TestForceSSLRedirect(t *testing.T) {
}
}

func TestSSLForbidHttp(t *testing.T) {
ing := buildIngress()

i, err := NewParser(mockBackend{}).Parse(ing)
if err != nil {
t.Errorf("unexpected error: %v", err)
}
forbid, ok := i.(*Config)
if !ok {
t.Errorf("expected a Forbid type")
}
if forbid.SSLForbidHttp {
t.Errorf("Expected false but returned true")
}

data := map[string]string{}
data[parser.GetAnnotationWithPrefix("ssl-forbid-http")] = "true"
ing.SetAnnotations(data)

i, err = NewParser(mockBackend{}).Parse(ing)
if err != nil {
t.Errorf("unexpected error: %v", err)
}
forbid, ok = i.(*Config)
if !ok {
t.Errorf("expected a Forbid type")
}
if !forbid.SSLForbidHttp {
t.Errorf("Expected true but returned false")
}
}

func TestForceSSLForbidHttp(t *testing.T) {
ing := buildIngress()

i, err := NewParser(mockBackend{}).Parse(ing)
if err != nil {
t.Errorf("unexpected error: %v", err)
}
forbid, ok := i.(*Config)
if !ok {
t.Errorf("expected a Forbid type")
}
if forbid.ForceSSLForbidHttp {
t.Errorf("Expected false but returned true")
}

data := map[string]string{}
data[parser.GetAnnotationWithPrefix("force-ssl-forbid-http")] = "true"
ing.SetAnnotations(data)

i, err = NewParser(mockBackend{}).Parse(ing)
if err != nil {
t.Errorf("unexpected error: %v", err)
}
forbid, ok = i.(*Config)
if !ok {
t.Errorf("expected a Forbid type")
}
if !forbid.ForceSSLForbidHttp {
t.Errorf("Expected true but returned false")
}
}

func TestAppRoot(t *testing.T) {
ap := NewParser(mockBackend{redirect: true})

Expand Down
6 changes: 6 additions & 0 deletions internal/ingress/controller/template/template.go
Original file line number Diff line number Diff line change
Expand Up @@ -435,6 +435,8 @@ func locationConfigForLua(l, a interface{}) string {
force_ssl_redirect = string_to_bool(ngx.var.force_ssl_redirect),
ssl_redirect = string_to_bool(ngx.var.ssl_redirect),
force_no_ssl_redirect = string_to_bool(ngx.var.force_no_ssl_redirect),
force_ssl_forbid_http = string_to_bool(ngx.var.force_ssl_forbid_http),
ssl_forbid_http = string_to_bool(ngx.var.ssl_forbid_http),
preserve_trailing_slash = string_to_bool(ngx.var.preserve_trailing_slash),
use_port_in_redirects = string_to_bool(ngx.var.use_port_in_redirects),
*/
Expand All @@ -443,12 +445,16 @@ func locationConfigForLua(l, a interface{}) string {
set $force_ssl_redirect "%t";
set $ssl_redirect "%t";
set $force_no_ssl_redirect "%t";
set $force_ssl_forbid_http "%t";
set $ssl_forbid_http "%t";
set $preserve_trailing_slash "%t";
set $use_port_in_redirects "%t";
`,
location.Rewrite.ForceSSLRedirect,
location.Rewrite.SSLRedirect,
isLocationInLocationList(l, all.Cfg.NoTLSRedirectLocations),
location.Rewrite.ForceSSLForbidHttp,
location.Rewrite.SSLForbidHttp,
location.Rewrite.PreserveTrailingSlash,
location.UsePortInRedirects,
)
Expand Down
7 changes: 7 additions & 0 deletions internal/ingress/defaults/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,13 @@ type Backend struct {
// This is useful if doing SSL offloading outside of cluster eg AWS ELB
ForceSSLRedirect bool `json:"force-ssl-redirect"`

// Enables or disables forbidden errors (403) to HTTP
SSLForbidHttp bool `json:"ssl-forbid-http"`

Check failure on line 125 in internal/ingress/defaults/main.go

View workflow job for this annotation

GitHub Actions / lint

var-naming: struct field SSLForbidHttp should be SSLForbidHTTP (revive)

// Enables or disables forbidden errors (403) to HTTP even without TLS cert
// This is useful if doing SSL offloading outside of cluster eg AWS ELB
ForceSSLForbidHttp bool `json:"force-ssl-forbid-http"`

Check failure on line 129 in internal/ingress/defaults/main.go

View workflow job for this annotation

GitHub Actions / lint

var-naming: struct field ForceSSLForbidHttp should be ForceSSLForbidHTTP (revive)

// Enables or disables the specification of port in redirects
// Default: false
UsePortInRedirects bool `json:"use-port-in-redirects"`
Expand Down
18 changes: 18 additions & 0 deletions rootfs/etc/nginx/lua/lua_ingress.lua
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,18 @@ local function randomseed()
math.randomseed(seed)
end

local function forbid_http(location_config)
if location_config.force_ssl_forbid_http and ngx.var.pass_access_scheme == "http" then
return true
end

if ngx.var.pass_access_scheme ~= "http" then
return false
end

return location_config.ssl_forbid_http and certificate_configured_for_current_request()
end

local function redirect_to_https(location_config)
if location_config.force_no_ssl_redirect then
return false
Expand Down Expand Up @@ -115,6 +127,8 @@ function _M.rewrite()
force_ssl_redirect = string_to_bool(ngx.var.force_ssl_redirect),
ssl_redirect = string_to_bool(ngx.var.ssl_redirect),
force_no_ssl_redirect = string_to_bool(ngx.var.force_no_ssl_redirect),
force_ssl_forbid_http = string_to_bool(ngx.var.force_ssl_forbid_http),
ssl_forbid_http = string_to_bool(ngx.var.ssl_forbid_http),
preserve_trailing_slash = string_to_bool(ngx.var.preserve_trailing_slash),
use_port_in_redirects = string_to_bool(ngx.var.use_port_in_redirects),
}
Expand Down Expand Up @@ -154,6 +168,10 @@ function _M.rewrite()
ngx.var.pass_port = 443
end

if forbid_http(location_config) then
ngx.exit(ngx.HTTP_FORBIDDEN)
end

if redirect_to_https(location_config) then
local request_uri = ngx.var.request_uri
-- do not append a trailing slash on redirects unless enabled by annotations
Expand Down
2 changes: 2 additions & 0 deletions test/data/cleanConf.expected.conf
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,8 @@ http {
force_ssl_redirect = false,
ssl_redirect = false,
force_no_ssl_redirect = false,
force_ssl_forbid_http = false,
ssl_forbid_http = false,
use_port_in_redirects = false,
})
balancer.rewrite()
Expand Down
2 changes: 2 additions & 0 deletions test/data/cleanConf.src.conf
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,8 @@ lua_shared_dict ocsp_response_cache 5M;
force_ssl_redirect = false,
ssl_redirect = false,
force_no_ssl_redirect = false,
force_ssl_forbid_http = false,
ssl_forbid_http = false,
use_port_in_redirects = false,
})
balancer.rewrite()
Expand Down
Loading

0 comments on commit 5657bd6

Please sign in to comment.