Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,9 @@ Minions cannot contain the following annotations:
- nginx.org/proxy-hide-headers
- nginx.org/proxy-pass-headers
- nginx.org/redirect-to-https
- ingress.kubernetes.io/ssl-redirect (deprecated, use nginx.org/ssl-redirect instead)
- nginx.org/ssl-redirect
- ingress.kubernetes.io/ssl-redirect (deprecated, use nginx.org/ssl-redirect instead)
- nginx.org/http-redirect-code
- nginx.org/hsts
- nginx.org/hsts-max-age
- nginx.org/hsts-include-subdomains
Expand Down
19 changes: 17 additions & 2 deletions internal/configs/annotations.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,12 @@ const UseClusterIPAnnotation = "nginx.org/use-cluster-ip"
// SSLRedirectAnnotation is the annotation where the SSL redirect boolean is specified.
const SSLRedirectAnnotation = "nginx.org/ssl-redirect"

// HTTPRedirectCodeAnnotation is the annotation where the HTTP redirect code is specified.
const HTTPRedirectCodeAnnotation = "nginx.org/http-redirect-code"

// RedirectToHTTPSAnnotation is the annotation where the redirect-to-https boolean is specified.
const RedirectToHTTPSAnnotation = "nginx.org/redirect-to-https"

// AppProtectPolicyAnnotation is where the NGINX App Protect policy is specified
const AppProtectPolicyAnnotation = "appprotect.f5.com/app-protect-policy"

Expand Down Expand Up @@ -63,9 +69,10 @@ var masterDenylist = map[string]bool{
var minionDenylist = map[string]bool{
"nginx.org/proxy-hide-headers": true,
"nginx.org/proxy-pass-headers": true,
"nginx.org/redirect-to-https": true,
RedirectToHTTPSAnnotation: true,
"ingress.kubernetes.io/ssl-redirect": true,
SSLRedirectAnnotation: true,
HTTPRedirectCodeAnnotation: true,
"nginx.org/hsts": true,
"nginx.org/hsts-max-age": true,
"nginx.org/hsts-include-subdomains": true,
Expand Down Expand Up @@ -259,7 +266,7 @@ func parseAnnotations(ingEx *IngressEx, baseCfgParams *ConfigParams, isPlus bool
cfgParams.ClientBodyBufferSize = size
}

if redirectToHTTPS, exists, err := GetMapKeyAsBool(ingEx.Ingress.Annotations, "nginx.org/redirect-to-https", ingEx.Ingress); exists {
if redirectToHTTPS, exists, err := GetMapKeyAsBool(ingEx.Ingress.Annotations, RedirectToHTTPSAnnotation, ingEx.Ingress); exists {
if err != nil {
nl.Error(l, err)
} else {
Expand All @@ -281,6 +288,14 @@ func parseAnnotations(ingEx *IngressEx, baseCfgParams *ConfigParams, isPlus bool
}
}

if httpRedirectCode, exists := ingEx.Ingress.Annotations[HTTPRedirectCodeAnnotation]; exists {
if code, err := ParseHTTPRedirectCode(httpRedirectCode); err != nil {
nl.Errorf(l, "Ingress %s/%s: Invalid value for nginx.org/http-redirect-code: %q: %v", ingEx.Ingress.GetNamespace(), ingEx.Ingress.GetName(), httpRedirectCode, err)
} else {
cfgParams.HTTPRedirectCode = code
}
}

if sslCiphers, exists := ingEx.Ingress.Annotations[SSLCiphersAnnotation]; exists {
cfgParams.ServerSSLCiphers = sslCiphers
}
Expand Down
63 changes: 63 additions & 0 deletions internal/configs/annotations_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1011,3 +1011,66 @@ func TestSSLRedirectAnnotations(t *testing.T) {
})
}
}

func TestHTTPRedirectCodeAnnotationBehavior(t *testing.T) {
t.Parallel()

tests := []struct {
name string
annotations map[string]string
expectedCode int
}{
{
name: "redirect code applied when SSL redirect enabled",
annotations: map[string]string{
"nginx.org/ssl-redirect": "true",
"nginx.org/http-redirect-code": "307",
},
expectedCode: 307,
},
{
name: "redirect code applied when SSL redirect disabled",
annotations: map[string]string{
"nginx.org/ssl-redirect": "false",
"nginx.org/http-redirect-code": "307",
},
expectedCode: 307,
},
{
name: "redirect code applied with redirect-to-https",
annotations: map[string]string{
"nginx.org/redirect-to-https": "true",
"nginx.org/http-redirect-code": "302",
},
expectedCode: 302,
},
{
name: "redirect code applied without any redirect settings",
annotations: map[string]string{
"nginx.org/http-redirect-code": "308",
},
expectedCode: 308,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ingEx := &IngressEx{
Ingress: &networking.Ingress{
ObjectMeta: metav1.ObjectMeta{
Name: "test-ingress",
Namespace: "default",
Annotations: tt.annotations,
},
},
}

baseCfgParams := NewDefaultConfigParams(context.Background(), false)
result := parseAnnotations(ingEx, baseCfgParams, false, false, false, false, false)

if result.HTTPRedirectCode != tt.expectedCode {
t.Errorf("Test %q: expected HTTPRedirectCode %d, got %d", tt.name, tt.expectedCode, result.HTTPRedirectCode)
}
})
}
}
2 changes: 2 additions & 0 deletions internal/configs/config_params.go
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,7 @@ type ConfigParams struct {
ProxyReadTimeout string
ProxySendTimeout string
RedirectToHTTPS bool
HTTPRedirectCode int
ResolverAddresses []string
ResolverIPV6 bool
ResolverTimeout string
Expand Down Expand Up @@ -245,6 +246,7 @@ func NewDefaultConfigParams(ctx context.Context, isPlus bool) *ConfigParams {
ProxySendTimeout: "60s",
ClientMaxBodySize: "1m",
SSLRedirect: true,
HTTPRedirectCode: 301,
MainAccessLog: "/dev/stdout main",
MainServerNamesHashBucketSize: "256",
MainServerNamesHashMaxSize: "1024",
Expand Down
34 changes: 34 additions & 0 deletions internal/configs/configmaps.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"fmt"
"log/slog"
"os"
"strconv"
"strings"
"time"

Expand Down Expand Up @@ -160,6 +161,17 @@ func ParseConfigMap(ctx context.Context, cfgm *v1.ConfigMap, nginxPlus bool, has
}
}

if httpRedirectCode, exists := cfgm.Data["http-redirect-code"]; exists {
if code, err := ParseHTTPRedirectCode(httpRedirectCode); err != nil {
errorText := fmt.Sprintf("ConfigMap %s/%s: Invalid value for 'http-redirect-code': %q: %v, ignoring", cfgm.GetNamespace(), cfgm.GetName(), httpRedirectCode, err)
nl.Error(l, errorText)
eventLog.Event(cfgm, v1.EventTypeWarning, nl.EventReasonInvalidValue, errorText)
configOk = false
} else {
cfgParams.HTTPRedirectCode = code
}
}

if hsts, exists, err := GetMapKeyAsBool(cfgm.Data, "hsts", cfgm); exists {
if err != nil {
nl.Error(l, err)
Expand Down Expand Up @@ -955,6 +967,28 @@ func parseConfigMapOpenTelemetry(l *slog.Logger, cfgm *v1.ConfigMap, cfgParams *
return cfgParams, nil
}

// ParseHTTPRedirectCode parses and validates an HTTP redirect code.
func ParseHTTPRedirectCode(code string) (int, error) {
redirectCode, err := strconv.Atoi(code)
if err != nil {
return 0, fmt.Errorf("invalid redirect code: %w", err)
}

// Validate that the code is one of the allowed redirect codes: 301, 302, 307, 308
validCodes := map[int]bool{
301: true,
302: true,
307: true,
308: true,
}

if _, valid := validCodes[redirectCode]; !valid {
return 0, fmt.Errorf("status code out of accepted range. accepted values are '301', '302', '307', '308'")
}

return redirectCode, nil
}

// ParseMGMTConfigMap parses the mgmt block ConfigMap into MGMTConfigParams.
//
//nolint:gocyclo
Expand Down
162 changes: 162 additions & 0 deletions internal/configs/configmaps_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2712,6 +2712,168 @@ func TestParseErrorLogLevelToVirtualServer(t *testing.T) {
}
}

func TestParseHTTPRedirectCode(t *testing.T) {
t.Parallel()
tests := []struct {
code string
expect int
isValid bool
msg string
}{
{
code: "301",
expect: 301,
isValid: true,
msg: "valid code 301",
},
{
code: "302",
expect: 302,
isValid: true,
msg: "valid code 302",
},
{
code: "307",
expect: 307,
isValid: true,
msg: "valid code 307",
},
{
code: "308",
expect: 308,
isValid: true,
msg: "valid code 308",
},
{
code: "200",
expect: 0,
isValid: false,
msg: "invalid code 200",
},
{
code: "404",
expect: 0,
isValid: false,
msg: "invalid code 404",
},
{
code: "invalid",
expect: 0,
isValid: false,
msg: "non-numeric code",
},
{
code: "",
expect: 0,
isValid: false,
msg: "empty code",
},
}

for _, test := range tests {
result, err := ParseHTTPRedirectCode(test.code)
if test.isValid {
assert.NoError(t, err, test.msg)
assert.Equal(t, test.expect, result, test.msg)
} else {
assert.Error(t, err, test.msg)
}
}
}

func TestParseConfigMapWithHTTPRedirectCode(t *testing.T) {
t.Parallel()
nginxPlus := false
hasAppProtect := false
hasAppProtectDos := false
hasTLSPassthrough := false
directiveAutoadjustEnabled := false

tests := []struct {
configMap map[string]string
expected int
expectError bool
msg string
}{
{
configMap: map[string]string{
"http-redirect-code": "301",
},
expected: 301,
expectError: false,
msg: "valid redirect code 301",
},
{
configMap: map[string]string{
"http-redirect-code": "302",
},
expected: 302,
expectError: false,
msg: "valid redirect code 302",
},
{
configMap: map[string]string{
"http-redirect-code": "307",
},
expected: 307,
expectError: false,
msg: "valid redirect code 307",
},
{
configMap: map[string]string{
"http-redirect-code": "308",
},
expected: 308,
expectError: false,
msg: "valid redirect code 308",
},
{
configMap: map[string]string{
"http-redirect-code": "200",
},
expected: 301, // should fallback to default
expectError: true,
msg: "invalid redirect code 200",
},
{
configMap: map[string]string{
"http-redirect-code": "invalid",
},
expected: 301, // should fallback to default
expectError: true,
msg: "non-numeric redirect code",
},
{
configMap: map[string]string{},
expected: 301, // default value
expectError: false,
msg: "no redirect code specified",
},
}

for _, test := range tests {
t.Run(test.msg, func(t *testing.T) {
configMap := &v1.ConfigMap{
ObjectMeta: metav1.ObjectMeta{
Name: "nginx-config",
Namespace: "nginx-ingress",
},
Data: test.configMap,
}

result, configOK := ParseConfigMap(context.Background(), configMap, nginxPlus, hasAppProtect, hasAppProtectDos, hasTLSPassthrough, directiveAutoadjustEnabled, makeEventLogger())

if test.expectError {
assert.False(t, configOK, test.msg)
} else {
assert.True(t, configOK, test.msg)
}

assert.Equal(t, test.expected, result.HTTPRedirectCode, test.msg)
})
}
}

func makeEventLogger() record.EventRecorder {
return record.NewFakeRecorder(1024)
}
1 change: 1 addition & 0 deletions internal/configs/ingress.go
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,7 @@ func generateNginxCfg(p NginxCfgParams) (version1.IngressNginxConfig, Warnings)
HTTP2: cfgParams.HTTP2,
RedirectToHTTPS: cfgParams.RedirectToHTTPS,
SSLRedirect: cfgParams.SSLRedirect,
HTTPRedirectCode: cfgParams.HTTPRedirectCode,
SSLCiphers: cfgParams.ServerSSLCiphers,
SSLPreferServerCiphers: cfgParams.ServerSSLPreferServerCiphers,
ProxyProtocol: cfgParams.ProxyProtocol,
Expand Down
Loading
Loading