diff --git a/apis/v1alpha1/nginxproxy_types.go b/apis/v1alpha1/nginxproxy_types.go index 6292962ec2..ed4ea9ed3d 100644 --- a/apis/v1alpha1/nginxproxy_types.go +++ b/apis/v1alpha1/nginxproxy_types.go @@ -49,11 +49,21 @@ type NginxProxySpec struct { // // +optional Logging *NginxLogging `json:"logging,omitempty"` + // NginxPlus specifies NGINX Plus additional settings. + // + // +optional + NginxPlus *NginxPlus `json:"nginxPlus,omitempty"` // DisableHTTP2 defines if http2 should be disabled for all servers. // Default is false, meaning http2 will be enabled for all servers. + DisableHTTP2 bool `json:"disableHTTP2,omitempty"` +} + +// NginxPlus specifies NGINX Plus additional settings. These will only be applied if NGINX Plus is being used. +type NginxPlus struct { + // AllowedAddresses specifies IPAddresses or CIDR blocks to the allow list for accessing the NGINX Plus API. // // +optional - DisableHTTP2 bool `json:"disableHTTP2,omitempty"` + AllowedAddresses []NginxPlusAllowAddress `json:"allowedAddresses,omitempty"` } // Telemetry specifies the OpenTelemetry configuration. @@ -149,7 +159,7 @@ type RewriteClientIP struct { // +listType=map // +listMapKey=type // +kubebuilder:validation:MaxItems=16 - TrustedAddresses []Address `json:"trustedAddresses,omitempty"` + TrustedAddresses []RewriteClientIPAddress `json:"trustedAddresses,omitempty"` } // RewriteClientIPModeType defines how NGINX Gateway Fabric will determine the client's original IP address. @@ -183,28 +193,49 @@ const ( IPv6 IPFamilyType = "ipv6" ) -// Address is a struct that specifies address type and value. -type Address struct { +// RewriteClientIPAddress specifies the address type and value for a RewriteClientIP address. +type RewriteClientIPAddress struct { // Type specifies the type of address. - Type AddressType `json:"type"` + Type RewriteClientIPAddressType `json:"type"` // Value specifies the address value. Value string `json:"value"` } -// AddressType specifies the type of address. +// RewriteClientIPAddressType specifies the type of address. // +kubebuilder:validation:Enum=CIDR;IPAddress;Hostname -type AddressType string +type RewriteClientIPAddressType string const ( - // CIDRAddressType specifies that the address is a CIDR block. - CIDRAddressType AddressType = "CIDR" + // RewriteClientIPCIDRAddressType specifies that the address is a CIDR block. + RewriteClientIPCIDRAddressType RewriteClientIPAddressType = "CIDR" + + // RewriteClientIPIPAddressType specifies that the address is an IP address. + RewriteClientIPIPAddressType RewriteClientIPAddressType = "IPAddress" + + // RewriteClientIPHostnameAddressType specifies that the address is a Hostname. + RewriteClientIPHostnameAddressType RewriteClientIPAddressType = "Hostname" +) - // IPAddressType specifies that the address is an IP address. - IPAddressType AddressType = "IPAddress" +// NginxPlusAllowAddress specifies the address type and value for an NginxPlus allow address. +type NginxPlusAllowAddress struct { + // Type specifies the type of address. + Type NginxPlusAllowAddressType `json:"type"` + + // Value specifies the address value. + Value string `json:"value"` +} + +// NginxPlusAllowAddressType specifies the type of address. +// +kubebuilder:validation:Enum=CIDR;IPAddress +type NginxPlusAllowAddressType string + +const ( + // NginxPlusAllowCIDRAddressType specifies that the address is a CIDR block. + NginxPlusAllowCIDRAddressType NginxPlusAllowAddressType = "CIDR" - // HostnameAddressType specifies that the address is a Hostname. - HostnameAddressType AddressType = "Hostname" + // NginxPlusAllowIPAddressType specifies that the address is an IP address. + NginxPlusAllowIPAddressType NginxPlusAllowAddressType = "IPAddress" ) // NginxLogging defines logging related settings for NGINX. diff --git a/apis/v1alpha1/zz_generated.deepcopy.go b/apis/v1alpha1/zz_generated.deepcopy.go index 828faaa612..96100bed3f 100644 --- a/apis/v1alpha1/zz_generated.deepcopy.go +++ b/apis/v1alpha1/zz_generated.deepcopy.go @@ -10,21 +10,6 @@ import ( "sigs.k8s.io/gateway-api/apis/v1alpha2" ) -// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. -func (in *Address) DeepCopyInto(out *Address) { - *out = *in -} - -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new Address. -func (in *Address) DeepCopy() *Address { - if in == nil { - return nil - } - out := new(Address) - in.DeepCopyInto(out) - return out -} - // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *ClientBody) DeepCopyInto(out *ClientBody) { *out = *in @@ -353,6 +338,41 @@ func (in *NginxLogging) DeepCopy() *NginxLogging { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *NginxPlus) DeepCopyInto(out *NginxPlus) { + *out = *in + if in.AllowedAddresses != nil { + in, out := &in.AllowedAddresses, &out.AllowedAddresses + *out = make([]NginxPlusAllowAddress, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NginxPlus. +func (in *NginxPlus) DeepCopy() *NginxPlus { + if in == nil { + return nil + } + out := new(NginxPlus) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *NginxPlusAllowAddress) DeepCopyInto(out *NginxPlusAllowAddress) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NginxPlusAllowAddress. +func (in *NginxPlusAllowAddress) DeepCopy() *NginxPlusAllowAddress { + if in == nil { + return nil + } + out := new(NginxPlusAllowAddress) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *NginxProxy) DeepCopyInto(out *NginxProxy) { *out = *in @@ -434,6 +454,11 @@ func (in *NginxProxySpec) DeepCopyInto(out *NginxProxySpec) { *out = new(NginxLogging) (*in).DeepCopyInto(*out) } + if in.NginxPlus != nil { + in, out := &in.NginxPlus, &out.NginxPlus + *out = new(NginxPlus) + (*in).DeepCopyInto(*out) + } } // DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new NginxProxySpec. @@ -547,7 +572,7 @@ func (in *RewriteClientIP) DeepCopyInto(out *RewriteClientIP) { } if in.TrustedAddresses != nil { in, out := &in.TrustedAddresses, &out.TrustedAddresses - *out = make([]Address, len(*in)) + *out = make([]RewriteClientIPAddress, len(*in)) copy(*out, *in) } } @@ -562,6 +587,21 @@ func (in *RewriteClientIP) DeepCopy() *RewriteClientIP { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *RewriteClientIPAddress) DeepCopyInto(out *RewriteClientIPAddress) { + *out = *in +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new RewriteClientIPAddress. +func (in *RewriteClientIPAddress) DeepCopy() *RewriteClientIPAddress { + if in == nil { + return nil + } + out := new(RewriteClientIPAddress) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *Snippet) DeepCopyInto(out *Snippet) { *out = *in diff --git a/charts/nginx-gateway-fabric/values.schema.json b/charts/nginx-gateway-fabric/values.schema.json index 048e4163cf..73a02d292d 100644 --- a/charts/nginx-gateway-fabric/values.schema.json +++ b/charts/nginx-gateway-fabric/values.schema.json @@ -93,6 +93,34 @@ "required": [], "type": "object" }, + "nginxPlus": { + "description": "NginxPlus specifies NGINX Plus additional settings.", + "properties": { + "allowedAddresses": { + "items": { + "properties": { + "type": { + "enum": [ + "CIDR", + "IPAddress" + ], + "required": [], + "type": "string" + }, + "value": { + "required": [], + "type": "string" + } + }, + "required": [] + }, + "required": [], + "type": "array" + } + }, + "required": [], + "type": "object" + }, "rewriteClientIP": { "description": "RewriteClientIP defines configuration for rewriting the client IP to the original client's IP.", "properties": { diff --git a/charts/nginx-gateway-fabric/values.yaml b/charts/nginx-gateway-fabric/values.yaml index 947212da97..2fe06c966c 100644 --- a/charts/nginx-gateway-fabric/values.yaml +++ b/charts/nginx-gateway-fabric/values.yaml @@ -244,6 +244,21 @@ nginx: # - crit # - alert # - emerg + # nginxPlus: + # type: object + # description: NginxPlus specifies NGINX Plus additional settings. + # properties: + # allowedAddresses: + # type: array + # items: + # properties: + # type: + # type: string + # enum: + # - CIDR + # - IPAddress + # value: + # type: string # @schema # -- The configuration for the data plane that is contained in the NginxProxy resource. config: {} diff --git a/config/crd/bases/gateway.nginx.org_nginxproxies.yaml b/config/crd/bases/gateway.nginx.org_nginxproxies.yaml index b39ac21de3..b842c62214 100644 --- a/config/crd/bases/gateway.nginx.org_nginxproxies.yaml +++ b/config/crd/bases/gateway.nginx.org_nginxproxies.yaml @@ -83,6 +83,31 @@ spec: - emerg type: string type: object + nginxPlus: + description: NginxPlus specifies NGINX Plus additional settings. + properties: + allowedAddresses: + description: AllowedAddresses specifies IPAddresses or CIDR blocks + to the allow list for accessing the NGINX Plus API. + items: + description: NginxPlusAllowAddress specifies the address type + and value for an NginxPlus allow address. + properties: + type: + description: Type specifies the type of address. + enum: + - CIDR + - IPAddress + type: string + value: + description: Value specifies the address value. + type: string + required: + - type + - value + type: object + type: array + type: object rewriteClientIP: description: RewriteClientIP defines configuration for rewriting the client IP to the original client's IP. @@ -122,8 +147,8 @@ spec: Sets NGINX directive set_real_ip_from: https://nginx.org/en/docs/http/ngx_http_realip_module.html#set_real_ip_from This field is required if mode is set. items: - description: Address is a struct that specifies address type - and value. + description: RewriteClientIPAddress specifies the address type + and value for a RewriteClientIP address. properties: type: description: Type specifies the type of address. diff --git a/deploy/crds.yaml b/deploy/crds.yaml index a5ae02e961..f0547580f1 100644 --- a/deploy/crds.yaml +++ b/deploy/crds.yaml @@ -668,6 +668,31 @@ spec: - emerg type: string type: object + nginxPlus: + description: NginxPlus specifies NGINX Plus additional settings. + properties: + allowedAddresses: + description: AllowedAddresses specifies IPAddresses or CIDR blocks + to the allow list for accessing the NGINX Plus API. + items: + description: NginxPlusAllowAddress specifies the address type + and value for an NginxPlus allow address. + properties: + type: + description: Type specifies the type of address. + enum: + - CIDR + - IPAddress + type: string + value: + description: Value specifies the address value. + type: string + required: + - type + - value + type: object + type: array + type: object rewriteClientIP: description: RewriteClientIP defines configuration for rewriting the client IP to the original client's IP. @@ -707,8 +732,8 @@ spec: Sets NGINX directive set_real_ip_from: https://nginx.org/en/docs/http/ngx_http_realip_module.html#set_real_ip_from This field is required if mode is set. items: - description: Address is a struct that specifies address type - and value. + description: RewriteClientIPAddress specifies the address type + and value for a RewriteClientIP address. properties: type: description: Type specifies the type of address. diff --git a/internal/mode/static/handler.go b/internal/mode/static/handler.go index e7be8e5d1d..54dde7ade8 100644 --- a/internal/mode/static/handler.go +++ b/internal/mode/static/handler.go @@ -174,7 +174,7 @@ func (h *eventHandlerImpl) HandleEventBatch(ctx context.Context, logger logr.Log return case state.EndpointsOnlyChange: h.version++ - cfg := dataplane.BuildConfiguration(ctx, gr, h.cfg.serviceResolver, h.version) + cfg := dataplane.BuildConfiguration(ctx, gr, h.cfg.serviceResolver, h.version, h.cfg.plus) depCtx, getErr := h.getDeploymentContext(ctx) if getErr != nil { logger.Error(getErr, "error getting deployment context for usage reporting") @@ -190,7 +190,7 @@ func (h *eventHandlerImpl) HandleEventBatch(ctx context.Context, logger logr.Log } case state.ClusterStateChange: h.version++ - cfg := dataplane.BuildConfiguration(ctx, gr, h.cfg.serviceResolver, h.version) + cfg := dataplane.BuildConfiguration(ctx, gr, h.cfg.serviceResolver, h.version, h.cfg.plus) depCtx, getErr := h.getDeploymentContext(ctx) if getErr != nil { logger.Error(getErr, "error getting deployment context for usage reporting") diff --git a/internal/mode/static/handler_test.go b/internal/mode/static/handler_test.go index 7a315b5c2d..d210509989 100644 --- a/internal/mode/static/handler_test.go +++ b/internal/mode/static/handler_test.go @@ -442,6 +442,7 @@ var _ = Describe("eventHandler", func() { handler.HandleEventBatch(context.Background(), ctlrZap.New(), batch) dcfg := dataplane.GetDefaultConfiguration(&graph.Graph{}, 1) + dcfg.NginxPlus = dataplane.NginxPlus{AllowedAddresses: []string{"127.0.0.1"}} Expect(helpers.Diff(handler.GetLatestConfiguration(), &dcfg)).To(BeEmpty()) Expect(fakeGenerator.GenerateCallCount()).To(Equal(0)) diff --git a/internal/mode/static/nginx/conf/nginx-plus.conf b/internal/mode/static/nginx/conf/nginx-plus.conf index 17ac6de4e3..b1651bb5dc 100644 --- a/internal/mode/static/nginx/conf/nginx-plus.conf +++ b/internal/mode/static/nginx/conf/nginx-plus.conf @@ -27,30 +27,6 @@ http { tcp_nopush on; server_tokens off; - - server { - listen 127.0.0.1:8765; - root /usr/share/nginx/html; - access_log off; - - allow 127.0.0.1; - deny all; - - location = /dashboard.html {} - - location /api { - api write=off; - } - } - - server { - listen unix:/var/run/nginx/nginx-plus-api.sock; - access_log off; - - location /api { - api write=on; - } - } } stream { diff --git a/internal/mode/static/nginx/config/generator.go b/internal/mode/static/nginx/config/generator.go index 13d664c28e..00b9500aea 100644 --- a/internal/mode/static/nginx/config/generator.go +++ b/internal/mode/static/nginx/config/generator.go @@ -58,6 +58,9 @@ const ( // mgmtIncludesFile is the path to the file containing the NGINX Plus mgmt config. mgmtIncludesFile = mainIncludesFolder + "/mgmt.conf" + + // nginxPlusConfigFile is the path to the file containing the NGINX Plus API config. + nginxPlusConfigFile = httpFolder + "/plus-api.conf" ) // ConfigFolders is a list of folders where NGINX configuration files are stored. @@ -199,6 +202,7 @@ func (g GeneratorImpl) getExecuteFuncs( g.executeStreamUpstreams, executeStreamMaps, executeVersion, + executePlusAPI, } } diff --git a/internal/mode/static/nginx/config/generator_test.go b/internal/mode/static/nginx/config/generator_test.go index 435314e881..a33a2b3a81 100644 --- a/internal/mode/static/nginx/config/generator_test.go +++ b/internal/mode/static/nginx/config/generator_test.go @@ -132,6 +132,7 @@ func TestGenerate(t *testing.T) { graph.PlusReportClientSSLCertificate: []byte("cert"), graph.PlusReportClientSSLKey: []byte("key"), }, + NginxPlus: dataplane.NginxPlus{AllowedAddresses: []string{"127.0.0.3", "25.0.0.3", "24.0.0.3/32"}}, } g := NewWithT(t) @@ -144,7 +145,7 @@ func TestGenerate(t *testing.T) { files := generator.Generate(conf) - g.Expect(files).To(HaveLen(17)) + g.Expect(files).To(HaveLen(18)) arrange := func(i, j int) bool { return files[i].Path < files[j].Path } @@ -155,6 +156,7 @@ func TestGenerate(t *testing.T) { /etc/nginx/conf.d/config-version.conf /etc/nginx/conf.d/http.conf /etc/nginx/conf.d/matches.json + /etc/nginx/conf.d/plus-api.conf /etc/nginx/includes/http_snippet1.conf /etc/nginx/includes/http_snippet2.conf /etc/nginx/includes/main_snippet1.conf @@ -196,33 +198,45 @@ func TestGenerate(t *testing.T) { g.Expect(httpCfg).To(ContainSubstring("include /etc/nginx/includes/http_snippet2.conf;")) g.Expect(files[2].Path).To(Equal("/etc/nginx/conf.d/matches.json")) - g.Expect(files[2].Type).To(Equal(file.TypeRegular)) expString := "{}" g.Expect(string(files[2].Content)).To(Equal(expString)) + g.Expect(files[3].Path).To(Equal("/etc/nginx/conf.d/plus-api.conf")) + g.Expect(files[3].Type).To(Equal(file.TypeRegular)) + httpCfg = string(files[3].Content) + g.Expect(httpCfg).To(ContainSubstring("listen unix:/var/run/nginx/nginx-plus-api.sock;")) + g.Expect(httpCfg).To(ContainSubstring("access_log off;")) + g.Expect(httpCfg).To(ContainSubstring("listen 8765;")) + g.Expect(httpCfg).To(ContainSubstring("root /usr/share/nginx/html;")) + g.Expect(httpCfg).To(ContainSubstring("allow 127.0.0.3;")) + g.Expect(httpCfg).To(ContainSubstring("allow 25.0.0.3;")) + g.Expect(httpCfg).To(ContainSubstring("allow 24.0.0.3/32;")) + g.Expect(httpCfg).To(ContainSubstring("deny all;")) + g.Expect(httpCfg).To(ContainSubstring("location = /dashboard.html {}")) + // snippet include files // content is not checked in this test. - g.Expect(files[3].Path).To(Equal("/etc/nginx/includes/http_snippet1.conf")) - g.Expect(files[4].Path).To(Equal("/etc/nginx/includes/http_snippet2.conf")) - g.Expect(files[5].Path).To(Equal("/etc/nginx/includes/main_snippet1.conf")) - g.Expect(files[6].Path).To(Equal("/etc/nginx/includes/main_snippet2.conf")) + g.Expect(files[4].Path).To(Equal("/etc/nginx/includes/http_snippet1.conf")) + g.Expect(files[5].Path).To(Equal("/etc/nginx/includes/http_snippet2.conf")) + g.Expect(files[6].Path).To(Equal("/etc/nginx/includes/main_snippet1.conf")) + g.Expect(files[7].Path).To(Equal("/etc/nginx/includes/main_snippet2.conf")) - g.Expect(files[7].Path).To(Equal("/etc/nginx/main-includes/deployment_ctx.json")) - deploymentCtx := string(files[7].Content) + g.Expect(files[8].Path).To(Equal("/etc/nginx/main-includes/deployment_ctx.json")) + deploymentCtx := string(files[8].Content) g.Expect(deploymentCtx).To(ContainSubstring("\"integration\":\"ngf\"")) g.Expect(deploymentCtx).To(ContainSubstring("\"cluster_id\":\"test-uid\"")) g.Expect(deploymentCtx).To(ContainSubstring("\"installation_id\":\"test-uid-replicaSet\"")) g.Expect(deploymentCtx).To(ContainSubstring("\"cluster_node_count\":1")) - g.Expect(files[8].Path).To(Equal("/etc/nginx/main-includes/main.conf")) - mainConfStr := string(files[8].Content) + g.Expect(files[9].Path).To(Equal("/etc/nginx/main-includes/main.conf")) + mainConfStr := string(files[9].Content) g.Expect(mainConfStr).To(ContainSubstring("load_module modules/ngx_otel_module.so;")) g.Expect(mainConfStr).To(ContainSubstring("include /etc/nginx/includes/main_snippet1.conf;")) g.Expect(mainConfStr).To(ContainSubstring("include /etc/nginx/includes/main_snippet2.conf;")) - g.Expect(files[9].Path).To(Equal("/etc/nginx/main-includes/mgmt.conf")) - mgmtConf := string(files[9].Content) + g.Expect(files[10].Path).To(Equal("/etc/nginx/main-includes/mgmt.conf")) + mgmtConf := string(files[10].Content) g.Expect(mgmtConf).To(ContainSubstring("usage_report endpoint=test-endpoint")) g.Expect(mgmtConf).To(ContainSubstring("license_token /etc/nginx/secrets/license.jwt")) g.Expect(mgmtConf).To(ContainSubstring("deployment_context /etc/nginx/main-includes/deployment_ctx.json")) @@ -230,31 +244,31 @@ func TestGenerate(t *testing.T) { g.Expect(mgmtConf).To(ContainSubstring("ssl_certificate /etc/nginx/secrets/mgmt-tls.crt")) g.Expect(mgmtConf).To(ContainSubstring("ssl_certificate_key /etc/nginx/secrets/mgmt-tls.key")) - g.Expect(files[10].Path).To(Equal("/etc/nginx/secrets/license.jwt")) - g.Expect(string(files[10].Content)).To(Equal("license")) + g.Expect(files[11].Path).To(Equal("/etc/nginx/secrets/license.jwt")) + g.Expect(string(files[11].Content)).To(Equal("license")) - g.Expect(files[11].Path).To(Equal("/etc/nginx/secrets/mgmt-ca.crt")) - g.Expect(string(files[11].Content)).To(Equal("ca")) + g.Expect(files[12].Path).To(Equal("/etc/nginx/secrets/mgmt-ca.crt")) + g.Expect(string(files[12].Content)).To(Equal("ca")) - g.Expect(files[12].Path).To(Equal("/etc/nginx/secrets/mgmt-tls.crt")) - g.Expect(string(files[12].Content)).To(Equal("cert")) + g.Expect(files[13].Path).To(Equal("/etc/nginx/secrets/mgmt-tls.crt")) + g.Expect(string(files[13].Content)).To(Equal("cert")) - g.Expect(files[13].Path).To(Equal("/etc/nginx/secrets/mgmt-tls.key")) - g.Expect(string(files[13].Content)).To(Equal("key")) + g.Expect(files[14].Path).To(Equal("/etc/nginx/secrets/mgmt-tls.key")) + g.Expect(string(files[14].Content)).To(Equal("key")) - g.Expect(files[14].Path).To(Equal("/etc/nginx/secrets/test-certbundle.crt")) - certBundle := string(files[14].Content) + g.Expect(files[15].Path).To(Equal("/etc/nginx/secrets/test-certbundle.crt")) + certBundle := string(files[15].Content) g.Expect(certBundle).To(Equal("test-cert")) - g.Expect(files[15]).To(Equal(file.File{ + g.Expect(files[16]).To(Equal(file.File{ Type: file.TypeSecret, Path: "/etc/nginx/secrets/test-keypair.pem", Content: []byte("test-cert\ntest-key"), })) - g.Expect(files[16].Path).To(Equal("/etc/nginx/stream-conf.d/stream.conf")) - g.Expect(files[16].Type).To(Equal(file.TypeRegular)) - streamCfg := string(files[16].Content) + g.Expect(files[17].Path).To(Equal("/etc/nginx/stream-conf.d/stream.conf")) + g.Expect(files[17].Type).To(Equal(file.TypeRegular)) + streamCfg := string(files[17].Content) g.Expect(streamCfg).To(ContainSubstring("listen unix:/var/run/nginx/app.example.com-443.sock")) g.Expect(streamCfg).To(ContainSubstring("listen 443")) g.Expect(streamCfg).To(ContainSubstring("app.example.com unix:/var/run/nginx/app.example.com-443.sock")) diff --git a/internal/mode/static/nginx/config/plus_api.go b/internal/mode/static/nginx/config/plus_api.go new file mode 100644 index 0000000000..9b1894fe30 --- /dev/null +++ b/internal/mode/static/nginx/config/plus_api.go @@ -0,0 +1,25 @@ +package config + +import ( + gotemplate "text/template" + + "github.com/nginx/nginx-gateway-fabric/internal/framework/helpers" + "github.com/nginx/nginx-gateway-fabric/internal/mode/static/state/dataplane" +) + +var plusAPITemplate = gotemplate.Must(gotemplate.New("plusAPI").Parse(plusAPITemplateText)) + +func executePlusAPI(conf dataplane.Configuration) []executeResult { + result := executeResult{ + dest: nginxPlusConfigFile, + } + // if AllowedAddresses is empty, it means that we are not running on nginx plus, and we don't want this generated + if conf.NginxPlus.AllowedAddresses != nil { + result = executeResult{ + dest: nginxPlusConfigFile, + data: helpers.MustExecuteTemplate(plusAPITemplate, conf.NginxPlus), + } + } + + return []executeResult{result} +} diff --git a/internal/mode/static/nginx/config/plus_api_template.go b/internal/mode/static/nginx/config/plus_api_template.go new file mode 100644 index 0000000000..94fb27ff39 --- /dev/null +++ b/internal/mode/static/nginx/config/plus_api_template.go @@ -0,0 +1,28 @@ +package config + +const plusAPITemplateText = ` +server { + listen unix:/var/run/nginx/nginx-plus-api.sock; + access_log off; + + location /api { + api write=on; + } +} + +server { + listen 8765; + root /usr/share/nginx/html; + access_log off; + {{ range $address := .AllowedAddresses }} + allow {{ $address }}; + {{- end }} + deny all; + + location = /dashboard.html {} + + location /api { + api write=off; + } +} +` diff --git a/internal/mode/static/nginx/config/plus_api_test.go b/internal/mode/static/nginx/config/plus_api_test.go new file mode 100644 index 0000000000..6afb79142a --- /dev/null +++ b/internal/mode/static/nginx/config/plus_api_test.go @@ -0,0 +1,63 @@ +package config + +import ( + "strings" + "testing" + + . "github.com/onsi/gomega" + + "github.com/nginx/nginx-gateway-fabric/internal/mode/static/state/dataplane" +) + +func TestExecutePlusAPI(t *testing.T) { + t.Parallel() + conf := dataplane.Configuration{ + NginxPlus: dataplane.NginxPlus{AllowedAddresses: []string{"127.0.0.1", "25.0.0.3"}}, + } + + g := NewWithT(t) + expSubStrings := map[string]int{ + "listen unix:/var/run/nginx/nginx-plus-api.sock;": 1, + "access_log off;": 2, + "api write=on;": 1, + "listen 8765;": 1, + "root /usr/share/nginx/html;": 1, + "allow 127.0.0.1;": 1, + "allow 25.0.0.3;": 1, + "deny all;": 1, + "location = /dashboard.html {}": 1, + "api write=off;": 1, + } + + for expSubStr, expCount := range expSubStrings { + res := executePlusAPI(conf) + g.Expect(res).To(HaveLen(1)) + g.Expect(expCount).To(Equal(strings.Count(string(res[0].data), expSubStr))) + } +} + +func TestExecutePlusAPI_EmptyNginxPlus(t *testing.T) { + t.Parallel() + conf := dataplane.Configuration{ + NginxPlus: dataplane.NginxPlus{}, + } + + g := NewWithT(t) + expSubStrings := map[string]int{ + "listen unix:/var/run/nginx/nginx-plus-api.sock;": 0, + "access_log off;": 0, + "api write=on;": 0, + "listen 8765;": 0, + "root /usr/share/nginx/html;": 0, + "allow 127.0.0.1;": 0, + "deny all;": 0, + "location = /dashboard.html {}": 0, + "api write=off;": 0, + } + + for expSubStr, expCount := range expSubStrings { + res := executePlusAPI(conf) + g.Expect(res).To(HaveLen(1)) + g.Expect(expCount).To(Equal(strings.Count(string(res[0].data), expSubStr))) + } +} diff --git a/internal/mode/static/state/dataplane/configuration.go b/internal/mode/static/state/dataplane/configuration.go index 33324dbf42..3b4760d331 100644 --- a/internal/mode/static/state/dataplane/configuration.go +++ b/internal/mode/static/state/dataplane/configuration.go @@ -33,9 +33,15 @@ func BuildConfiguration( g *graph.Graph, serviceResolver resolver.ServiceResolver, configVersion int, + plus bool, ) Configuration { if g.GatewayClass == nil || !g.GatewayClass.Valid || g.Gateway == nil { - return GetDefaultConfiguration(g, configVersion) + config := GetDefaultConfiguration(g, configVersion) + if plus { + config.NginxPlus = buildNginxPlus(g) + } + + return config } baseHTTPConfig := buildBaseHTTPConfig(g) @@ -50,6 +56,11 @@ func BuildConfiguration( baseHTTPConfig.IPFamily, ) + var nginxPlus NginxPlus + if plus { + nginxPlus = buildNginxPlus(g) + } + config := Configuration{ HTTPServers: httpServers, SSLServers: sslServers, @@ -63,6 +74,7 @@ func BuildConfiguration( Telemetry: buildTelemetry(g), BaseHTTPConfig: baseHTTPConfig, Logging: buildLogging(g), + NginxPlus: nginxPlus, MainSnippets: buildSnippetsForContext(g.SnippetsFilters, ngfAPIv1alpha1.NginxContextMain), AuxiliarySecrets: buildAuxiliarySecrets(g.PlusSecrets), } @@ -951,7 +963,7 @@ func buildPolicies(graphPolicies []*graph.Policy) []policies.Policy { return finalPolicies } -func convertAddresses(addresses []ngfAPIv1alpha1.Address) []string { +func convertAddresses(addresses []ngfAPIv1alpha1.RewriteClientIPAddress) []string { trustedAddresses := make([]string, len(addresses)) for i, addr := range addresses { trustedAddresses[i] = addr.Value @@ -986,10 +998,29 @@ func buildAuxiliarySecrets( return auxSecrets } +func buildNginxPlus(g *graph.Graph) NginxPlus { + nginxPlusSettings := NginxPlus{AllowedAddresses: []string{"127.0.0.1"}} + + ngfProxy := g.NginxProxy + if ngfProxy != nil && ngfProxy.Source.Spec.NginxPlus != nil { + if ngfProxy.Source.Spec.NginxPlus.AllowedAddresses != nil { + addresses := make([]string, 0, len(ngfProxy.Source.Spec.NginxPlus.AllowedAddresses)) + for _, addr := range ngfProxy.Source.Spec.NginxPlus.AllowedAddresses { + addresses = append(addresses, addr.Value) + } + + nginxPlusSettings.AllowedAddresses = addresses + } + } + + return nginxPlusSettings +} + func GetDefaultConfiguration(g *graph.Graph, configVersion int) Configuration { return Configuration{ Version: configVersion, Logging: buildLogging(g), + NginxPlus: NginxPlus{}, AuxiliarySecrets: buildAuxiliarySecrets(g.PlusSecrets), } } diff --git a/internal/mode/static/state/dataplane/configuration_test.go b/internal/mode/static/state/dataplane/configuration_test.go index a455d5c73e..0117afecdd 100644 --- a/internal/mode/static/state/dataplane/configuration_test.go +++ b/internal/mode/static/state/dataplane/configuration_test.go @@ -66,6 +66,7 @@ func getExpectedConfiguration() Configuration { Logging: Logging{ ErrorLevel: defaultErrorLogLevel, }, + NginxPlus: NginxPlus{}, } } @@ -865,6 +866,11 @@ func TestBuildConfiguration(t *testing.T) { Valid: true, } + defaultConfig := Configuration{ + Logging: Logging{ErrorLevel: defaultErrorLogLevel}, + NginxPlus: NginxPlus{}, + } + tests := []struct { graph *graph.Graph msg string @@ -1551,39 +1557,17 @@ func TestBuildConfiguration(t *testing.T) { { graph: getModifiedGraph(func(g *graph.Graph) *graph.Graph { g.GatewayClass.Valid = false - g.Gateway.Listeners = append(g.Gateway.Listeners, &graph.Listener{ - Name: "listener-80-1", - Source: listener80, - Valid: true, - Routes: map[graph.RouteKey]*graph.L7Route{ - graph.CreateRouteKey(hr1): routeHR1, - }, - }) - g.Routes = map[graph.RouteKey]*graph.L7Route{ - graph.CreateRouteKey(hr1): routeHR1, - } return g }), - expConf: Configuration{Logging: Logging{ErrorLevel: defaultErrorLogLevel}}, + expConf: defaultConfig, msg: "invalid gatewayclass", }, { graph: getModifiedGraph(func(g *graph.Graph) *graph.Graph { - g.GatewayClass.Valid = false - g.Gateway.Listeners = append(g.Gateway.Listeners, &graph.Listener{ - Name: "listener-80-1", - Source: listener80, - Valid: true, - Routes: map[graph.RouteKey]*graph.L7Route{ - graph.CreateRouteKey(hr1): routeHR1, - }, - }) - g.Routes = map[graph.RouteKey]*graph.L7Route{ - graph.CreateRouteKey(hr1): routeHR1, - } + g.GatewayClass = nil return g }), - expConf: Configuration{Logging: Logging{ErrorLevel: defaultErrorLogLevel}}, + expConf: defaultConfig, msg: "missing gatewayclass", }, { @@ -1591,7 +1575,7 @@ func TestBuildConfiguration(t *testing.T) { g.Gateway = nil return g }), - expConf: Configuration{Logging: Logging{ErrorLevel: defaultErrorLogLevel}}, + expConf: defaultConfig, msg: "missing gateway", }, { @@ -2271,9 +2255,9 @@ func TestBuildConfiguration(t *testing.T) { Spec: ngfAPIv1alpha1.NginxProxySpec{ RewriteClientIP: &ngfAPIv1alpha1.RewriteClientIP{ SetIPRecursively: helpers.GetPointer(true), - TrustedAddresses: []ngfAPIv1alpha1.Address{ + TrustedAddresses: []ngfAPIv1alpha1.RewriteClientIPAddress{ { - Type: ngfAPIv1alpha1.CIDRAddressType, + Type: ngfAPIv1alpha1.RewriteClientIPCIDRAddressType, Value: "1.1.1.1/32", }, }, @@ -2366,6 +2350,40 @@ func TestBuildConfiguration(t *testing.T) { }), msg: "SnippetsFilters with main and http snippet", }, + { + graph: getModifiedGraph(func(g *graph.Graph) *graph.Graph { + g.Gateway.Source.ObjectMeta = metav1.ObjectMeta{ + Name: "gw", + Namespace: "ns", + } + g.Gateway.Listeners = append(g.Gateway.Listeners, &graph.Listener{ + Name: "listener-80-1", + Source: listener80, + Valid: true, + Routes: map[graph.RouteKey]*graph.L7Route{}, + }) + g.NginxProxy = &graph.NginxProxy{ + Valid: true, + Source: &ngfAPIv1alpha1.NginxProxy{ + Spec: ngfAPIv1alpha1.NginxProxySpec{ + NginxPlus: &ngfAPIv1alpha1.NginxPlus{ + AllowedAddresses: []ngfAPIv1alpha1.NginxPlusAllowAddress{ + {Type: ngfAPIv1alpha1.NginxPlusAllowIPAddressType, Value: "127.0.0.3"}, + {Type: ngfAPIv1alpha1.NginxPlusAllowIPAddressType, Value: "25.0.0.3"}, + }, + }, + }, + }, + } + return g + }), + expConf: getModifiedExpectedConfiguration(func(conf Configuration) Configuration { + conf.SSLServers = []VirtualServer{} + conf.SSLKeyPairs = map[SSLKeyPairID]SSLKeyPair{} + return conf + }), + msg: "NginxProxy with NginxPlus allowed addresses configured but running on nginx oss", + }, } for _, test := range tests { @@ -2378,6 +2396,7 @@ func TestBuildConfiguration(t *testing.T) { test.graph, fakeResolver, 1, + false, ) g.Expect(result.BackendGroups).To(ConsistOf(test.expConf.BackendGroups)) @@ -2391,6 +2410,126 @@ func TestBuildConfiguration(t *testing.T) { g.Expect(result.Telemetry).To(Equal(test.expConf.Telemetry)) g.Expect(result.BaseHTTPConfig).To(Equal(test.expConf.BaseHTTPConfig)) g.Expect(result.Logging).To(Equal(test.expConf.Logging)) + g.Expect(result.NginxPlus).To(Equal(test.expConf.NginxPlus)) + }) + } +} + +func TestBuildConfiguration_Plus(t *testing.T) { + t.Parallel() + fooEndpoints := []resolver.Endpoint{ + { + Address: "10.0.0.0", + Port: 8080, + }, + } + + fakeResolver := &resolverfakes.FakeServiceResolver{} + fakeResolver.ResolveReturns(fooEndpoints, nil) + + listener80 := v1.Listener{ + Name: "listener-80-1", + Hostname: nil, + Port: 80, + Protocol: v1.HTTPProtocolType, + } + + defaultPlusConfig := Configuration{ + Logging: Logging{ErrorLevel: defaultErrorLogLevel}, + NginxPlus: NginxPlus{AllowedAddresses: []string{"127.0.0.1"}}, + } + + tests := []struct { + graph *graph.Graph + msg string + expConf Configuration + }{ + { + graph: getModifiedGraph(func(g *graph.Graph) *graph.Graph { + g.Gateway.Source.ObjectMeta = metav1.ObjectMeta{ + Name: "gw", + Namespace: "ns", + } + g.Gateway.Listeners = append(g.Gateway.Listeners, &graph.Listener{ + Name: "listener-80-1", + Source: listener80, + Valid: true, + Routes: map[graph.RouteKey]*graph.L7Route{}, + }) + g.NginxProxy = &graph.NginxProxy{ + Valid: true, + Source: &ngfAPIv1alpha1.NginxProxy{ + Spec: ngfAPIv1alpha1.NginxProxySpec{ + NginxPlus: &ngfAPIv1alpha1.NginxPlus{ + AllowedAddresses: []ngfAPIv1alpha1.NginxPlusAllowAddress{ + {Type: ngfAPIv1alpha1.NginxPlusAllowIPAddressType, Value: "127.0.0.3"}, + {Type: ngfAPIv1alpha1.NginxPlusAllowIPAddressType, Value: "25.0.0.3"}, + }, + }, + }, + }, + } + return g + }), + expConf: getModifiedExpectedConfiguration(func(conf Configuration) Configuration { + conf.SSLServers = []VirtualServer{} + conf.SSLKeyPairs = map[SSLKeyPairID]SSLKeyPair{} + conf.NginxPlus = NginxPlus{AllowedAddresses: []string{"127.0.0.3", "25.0.0.3"}} + return conf + }), + msg: "NginxProxy with NginxPlus allowed addresses configured", + }, + { + graph: getModifiedGraph(func(g *graph.Graph) *graph.Graph { + g.GatewayClass.Valid = false + return g + }), + expConf: defaultPlusConfig, + msg: "invalid gatewayclass", + }, + { + graph: getModifiedGraph(func(g *graph.Graph) *graph.Graph { + g.GatewayClass = nil + return g + }), + expConf: defaultPlusConfig, + msg: "missing gatewayclass", + }, + { + graph: getModifiedGraph(func(g *graph.Graph) *graph.Graph { + g.Gateway = nil + return g + }), + expConf: defaultPlusConfig, + msg: "missing gateway", + }, + } + + for _, test := range tests { + t.Run(test.msg, func(t *testing.T) { + t.Parallel() + g := NewWithT(t) + + result := BuildConfiguration( + context.TODO(), + test.graph, + fakeResolver, + 1, + true, + ) + + g.Expect(result.BackendGroups).To(ConsistOf(test.expConf.BackendGroups)) + g.Expect(result.Upstreams).To(ConsistOf(test.expConf.Upstreams)) + g.Expect(result.HTTPServers).To(ConsistOf(test.expConf.HTTPServers)) + g.Expect(result.SSLServers).To(ConsistOf(test.expConf.SSLServers)) + g.Expect(result.TLSPassthroughServers).To(ConsistOf(test.expConf.TLSPassthroughServers)) + g.Expect(result.SSLKeyPairs).To(Equal(test.expConf.SSLKeyPairs)) + g.Expect(result.Version).To(Equal(1)) + g.Expect(result.CertBundles).To(Equal(test.expConf.CertBundles)) + g.Expect(result.Telemetry).To(Equal(test.expConf.Telemetry)) + g.Expect(result.BaseHTTPConfig).To(Equal(test.expConf.BaseHTTPConfig)) + g.Expect(result.Logging).To(Equal(test.expConf.Logging)) + g.Expect(result.NginxPlus).To(Equal(test.expConf.NginxPlus)) }) } } @@ -3928,9 +4067,9 @@ func TestBuildRewriteIPSettings(t *testing.T) { Spec: ngfAPIv1alpha1.NginxProxySpec{ RewriteClientIP: &ngfAPIv1alpha1.RewriteClientIP{ Mode: helpers.GetPointer(ngfAPIv1alpha1.RewriteClientIPModeProxyProtocol), - TrustedAddresses: []ngfAPIv1alpha1.Address{ + TrustedAddresses: []ngfAPIv1alpha1.RewriteClientIPAddress{ { - Type: ngfAPIv1alpha1.CIDRAddressType, + Type: ngfAPIv1alpha1.RewriteClientIPCIDRAddressType, Value: "10.9.9.4/32", }, }, @@ -3955,9 +4094,9 @@ func TestBuildRewriteIPSettings(t *testing.T) { Spec: ngfAPIv1alpha1.NginxProxySpec{ RewriteClientIP: &ngfAPIv1alpha1.RewriteClientIP{ Mode: helpers.GetPointer(ngfAPIv1alpha1.RewriteClientIPModeXForwardedFor), - TrustedAddresses: []ngfAPIv1alpha1.Address{ + TrustedAddresses: []ngfAPIv1alpha1.RewriteClientIPAddress{ { - Type: ngfAPIv1alpha1.CIDRAddressType, + Type: ngfAPIv1alpha1.RewriteClientIPCIDRAddressType, Value: "76.89.90.11/24", }, }, @@ -3982,21 +4121,21 @@ func TestBuildRewriteIPSettings(t *testing.T) { Spec: ngfAPIv1alpha1.NginxProxySpec{ RewriteClientIP: &ngfAPIv1alpha1.RewriteClientIP{ Mode: helpers.GetPointer(ngfAPIv1alpha1.RewriteClientIPModeXForwardedFor), - TrustedAddresses: []ngfAPIv1alpha1.Address{ + TrustedAddresses: []ngfAPIv1alpha1.RewriteClientIPAddress{ { - Type: ngfAPIv1alpha1.CIDRAddressType, + Type: ngfAPIv1alpha1.RewriteClientIPCIDRAddressType, Value: "5.5.5.5/12", }, { - Type: ngfAPIv1alpha1.CIDRAddressType, + Type: ngfAPIv1alpha1.RewriteClientIPCIDRAddressType, Value: "1.1.1.1/26", }, { - Type: ngfAPIv1alpha1.CIDRAddressType, + Type: ngfAPIv1alpha1.RewriteClientIPCIDRAddressType, Value: "2.2.2.2/32", }, { - Type: ngfAPIv1alpha1.CIDRAddressType, + Type: ngfAPIv1alpha1.RewriteClientIPCIDRAddressType, Value: "3.3.3.3/24", }, }, @@ -4383,3 +4522,96 @@ func TestBuildAuxiliarySecrets(t *testing.T) { g.Expect(buildAuxiliarySecrets(secrets)).To(Equal(expSecrets)) } + +func TestBuildNginxPlus(t *testing.T) { + defaultNginxPlus := NginxPlus{AllowedAddresses: []string{"127.0.0.1"}} + + t.Parallel() + tests := []struct { + msg string + g *graph.Graph + expNginxPlus NginxPlus + }{ + { + msg: "NginxProxy is nil", + g: &graph.Graph{}, + expNginxPlus: defaultNginxPlus, + }, + { + msg: "NginxPlus default values are used when NginxProxy doesn't specify NginxPlus settings", + g: &graph.Graph{ + NginxProxy: &graph.NginxProxy{ + Valid: true, + Source: &ngfAPIv1alpha1.NginxProxy{ + Spec: ngfAPIv1alpha1.NginxProxySpec{}, + }, + }, + }, + expNginxPlus: defaultNginxPlus, + }, + { + msg: "NginxProxy specifies one allowed address", + g: &graph.Graph{ + NginxProxy: &graph.NginxProxy{ + Valid: true, + Source: &ngfAPIv1alpha1.NginxProxy{ + Spec: ngfAPIv1alpha1.NginxProxySpec{ + NginxPlus: &ngfAPIv1alpha1.NginxPlus{ + AllowedAddresses: []ngfAPIv1alpha1.NginxPlusAllowAddress{ + {Type: ngfAPIv1alpha1.NginxPlusAllowIPAddressType, Value: "127.0.0.3"}, + }, + }, + }, + }, + }, + }, + expNginxPlus: NginxPlus{AllowedAddresses: []string{"127.0.0.3"}}, + }, + { + msg: "NginxProxy specifies multiple allowed addresses", + g: &graph.Graph{ + NginxProxy: &graph.NginxProxy{ + Valid: true, + Source: &ngfAPIv1alpha1.NginxProxy{ + Spec: ngfAPIv1alpha1.NginxProxySpec{ + NginxPlus: &ngfAPIv1alpha1.NginxPlus{ + AllowedAddresses: []ngfAPIv1alpha1.NginxPlusAllowAddress{ + {Type: ngfAPIv1alpha1.NginxPlusAllowIPAddressType, Value: "127.0.0.3"}, + {Type: ngfAPIv1alpha1.NginxPlusAllowIPAddressType, Value: "25.0.0.3"}, + }, + }, + }, + }, + }, + }, + expNginxPlus: NginxPlus{AllowedAddresses: []string{"127.0.0.3", "25.0.0.3"}}, + }, + { + msg: "NginxProxy specifies 127.0.0.1 as allowed address", + g: &graph.Graph{ + NginxProxy: &graph.NginxProxy{ + Valid: true, + Source: &ngfAPIv1alpha1.NginxProxy{ + Spec: ngfAPIv1alpha1.NginxProxySpec{ + NginxPlus: &ngfAPIv1alpha1.NginxPlus{ + AllowedAddresses: []ngfAPIv1alpha1.NginxPlusAllowAddress{ + {Type: ngfAPIv1alpha1.NginxPlusAllowIPAddressType, Value: "127.0.0.1"}, + }, + }, + }, + }, + }, + }, + expNginxPlus: defaultNginxPlus, + }, + } + + for _, tc := range tests { + t.Run(tc.msg, func(t *testing.T) { + t.Parallel() + g := NewWithT(t) + + g.Expect(buildNginxPlus(tc.g)).To(Equal(tc.expNginxPlus)) + }) + } +} diff --git a/internal/mode/static/state/dataplane/types.go b/internal/mode/static/state/dataplane/types.go index 8a46dbf126..5d2eac9551 100644 --- a/internal/mode/static/state/dataplane/types.go +++ b/internal/mode/static/state/dataplane/types.go @@ -50,6 +50,8 @@ type Configuration struct { Telemetry Telemetry // Logging defines logging related settings for NGINX. Logging Logging + // NginxPlus specifies NGINX Plus additional settings. + NginxPlus NginxPlus // BaseHTTPConfig holds the configuration options at the http context. BaseHTTPConfig BaseHTTPConfig // Version represents the version of the generated configuration. @@ -400,6 +402,12 @@ type Logging struct { ErrorLevel string } +// NginxPlus specifies NGINX Plus additional settings. +type NginxPlus struct { + // AllowedAddresses specifies IPAddresses or CIDR blocks to the allow list for accessing the NGINX Plus API. + AllowedAddresses []string +} + // DeploymentContext contains metadata about NGF and the cluster. // This is JSON marshaled into a file created by the generator, hence the json tags. type DeploymentContext struct { diff --git a/internal/mode/static/state/graph/nginxproxy.go b/internal/mode/static/state/graph/nginxproxy.go index 01e6e6de5b..831a205120 100644 --- a/internal/mode/static/state/graph/nginxproxy.go +++ b/internal/mode/static/state/graph/nginxproxy.go @@ -133,6 +133,8 @@ func validateNginxProxy( allErrs = append(allErrs, validateRewriteClientIP(npCfg)...) + allErrs = append(allErrs, validateNginxPlus(npCfg)...) + return allErrs } @@ -216,15 +218,15 @@ func validateRewriteClientIP(npCfg *ngfAPI.NginxProxy) field.ErrorList { valuePath := trustedAddressesPath.Child("value") switch addr.Type { - case ngfAPI.CIDRAddressType: + case ngfAPI.RewriteClientIPCIDRAddressType: if err := k8svalidation.IsValidCIDR(valuePath, addr.Value); err != nil { allErrs = append(allErrs, err...) } - case ngfAPI.IPAddressType: + case ngfAPI.RewriteClientIPIPAddressType: if err := k8svalidation.IsValidIP(valuePath, addr.Value); err != nil { allErrs = append(allErrs, err...) } - case ngfAPI.HostnameAddressType: + case ngfAPI.RewriteClientIPHostnameAddressType: if errs := k8svalidation.IsDNS1123Subdomain(addr.Value); len(errs) > 0 { for _, e := range errs { allErrs = append(allErrs, field.Invalid(valuePath, addr.Value, e)) @@ -236,9 +238,9 @@ func validateRewriteClientIP(npCfg *ngfAPI.NginxProxy) field.ErrorList { field.NotSupported(trustedAddressesPath.Child("type"), addr.Type, []string{ - string(ngfAPI.CIDRAddressType), - string(ngfAPI.IPAddressType), - string(ngfAPI.HostnameAddressType), + string(ngfAPI.RewriteClientIPCIDRAddressType), + string(ngfAPI.RewriteClientIPIPAddressType), + string(ngfAPI.RewriteClientIPHostnameAddressType), }, ), ) @@ -248,3 +250,43 @@ func validateRewriteClientIP(npCfg *ngfAPI.NginxProxy) field.ErrorList { return allErrs } + +func validateNginxPlus(npCfg *ngfAPI.NginxProxy) field.ErrorList { + var allErrs field.ErrorList + spec := field.NewPath("spec") + + if npCfg.Spec.NginxPlus != nil { + nginxPlus := npCfg.Spec.NginxPlus + nginxPlusPath := spec.Child("nginxPlus") + + if nginxPlus.AllowedAddresses != nil { + for _, addr := range nginxPlus.AllowedAddresses { + valuePath := nginxPlusPath.Child("value") + + switch addr.Type { + case ngfAPI.NginxPlusAllowCIDRAddressType: + if err := k8svalidation.IsValidCIDR(valuePath, addr.Value); err != nil { + allErrs = append(allErrs, err...) + } + case ngfAPI.NginxPlusAllowIPAddressType: + if err := k8svalidation.IsValidIP(valuePath, addr.Value); err != nil { + allErrs = append(allErrs, err...) + } + default: + allErrs = append( + allErrs, + field.NotSupported(nginxPlusPath.Child("type"), + addr.Type, + []string{ + string(ngfAPI.NginxPlusAllowCIDRAddressType), + string(ngfAPI.NginxPlusAllowIPAddressType), + }, + ), + ) + } + } + } + } + + return allErrs +} diff --git a/internal/mode/static/state/graph/nginxproxy_test.go b/internal/mode/static/state/graph/nginxproxy_test.go index 8c688d9812..325a996321 100644 --- a/internal/mode/static/state/graph/nginxproxy_test.go +++ b/internal/mode/static/state/graph/nginxproxy_test.go @@ -275,17 +275,17 @@ func TestValidateNginxProxy(t *testing.T) { IPFamily: helpers.GetPointer[ngfAPI.IPFamilyType](ngfAPI.Dual), RewriteClientIP: &ngfAPI.RewriteClientIP{ SetIPRecursively: helpers.GetPointer(true), - TrustedAddresses: []ngfAPI.Address{ + TrustedAddresses: []ngfAPI.RewriteClientIPAddress{ { - Type: ngfAPI.CIDRAddressType, + Type: ngfAPI.RewriteClientIPCIDRAddressType, Value: "2001:db8:a0b:12f0::1/32", }, { - Type: ngfAPI.IPAddressType, + Type: ngfAPI.RewriteClientIPIPAddressType, Value: "1.1.1.1", }, { - Type: ngfAPI.HostnameAddressType, + Type: ngfAPI.RewriteClientIPHostnameAddressType, Value: "example.com", }, }, @@ -399,25 +399,25 @@ func TestValidateRewriteClientIP(t *testing.T) { Spec: ngfAPI.NginxProxySpec{ RewriteClientIP: &ngfAPI.RewriteClientIP{ SetIPRecursively: helpers.GetPointer(true), - TrustedAddresses: []ngfAPI.Address{ + TrustedAddresses: []ngfAPI.RewriteClientIPAddress{ { - Type: ngfAPI.CIDRAddressType, + Type: ngfAPI.RewriteClientIPCIDRAddressType, Value: "2001:db8:a0b:12f0::1/32", }, { - Type: ngfAPI.CIDRAddressType, + Type: ngfAPI.RewriteClientIPCIDRAddressType, Value: "10.56.32.11/32", }, { - Type: ngfAPI.IPAddressType, + Type: ngfAPI.RewriteClientIPIPAddressType, Value: "1.1.1.1", }, { - Type: ngfAPI.IPAddressType, + Type: ngfAPI.RewriteClientIPIPAddressType, Value: "2001:db8:a0b:12f0::1", }, { - Type: ngfAPI.HostnameAddressType, + Type: ngfAPI.RewriteClientIPHostnameAddressType, Value: "example.com", }, }, @@ -434,13 +434,13 @@ func TestValidateRewriteClientIP(t *testing.T) { Spec: ngfAPI.NginxProxySpec{ RewriteClientIP: &ngfAPI.RewriteClientIP{ SetIPRecursively: helpers.GetPointer(true), - TrustedAddresses: []ngfAPI.Address{ + TrustedAddresses: []ngfAPI.RewriteClientIPAddress{ { - Type: ngfAPI.CIDRAddressType, + Type: ngfAPI.RewriteClientIPCIDRAddressType, Value: "2001:db8::/129", }, { - Type: ngfAPI.CIDRAddressType, + Type: ngfAPI.RewriteClientIPCIDRAddressType, Value: "10.0.0.1/32", }, }, @@ -459,13 +459,13 @@ func TestValidateRewriteClientIP(t *testing.T) { Spec: ngfAPI.NginxProxySpec{ RewriteClientIP: &ngfAPI.RewriteClientIP{ SetIPRecursively: helpers.GetPointer(true), - TrustedAddresses: []ngfAPI.Address{ + TrustedAddresses: []ngfAPI.RewriteClientIPAddress{ { - Type: ngfAPI.IPAddressType, + Type: ngfAPI.RewriteClientIPIPAddressType, Value: "1.2.3.4.5", }, { - Type: ngfAPI.IPAddressType, + Type: ngfAPI.RewriteClientIPIPAddressType, Value: "10.0.0.1", }, }, @@ -484,13 +484,13 @@ func TestValidateRewriteClientIP(t *testing.T) { Spec: ngfAPI.NginxProxySpec{ RewriteClientIP: &ngfAPI.RewriteClientIP{ SetIPRecursively: helpers.GetPointer(true), - TrustedAddresses: []ngfAPI.Address{ + TrustedAddresses: []ngfAPI.RewriteClientIPAddress{ { - Type: ngfAPI.HostnameAddressType, + Type: ngfAPI.RewriteClientIPHostnameAddressType, Value: "bad-host$%^", }, { - Type: ngfAPI.HostnameAddressType, + Type: ngfAPI.RewriteClientIPHostnameAddressType, Value: "example.com", }, }, @@ -524,28 +524,28 @@ func TestValidateRewriteClientIP(t *testing.T) { Spec: ngfAPI.NginxProxySpec{ RewriteClientIP: &ngfAPI.RewriteClientIP{ Mode: helpers.GetPointer(ngfAPI.RewriteClientIPModeProxyProtocol), - TrustedAddresses: []ngfAPI.Address{ - {Type: ngfAPI.CIDRAddressType, Value: "2001:db8:a0b:12f0::1/32"}, - {Type: ngfAPI.CIDRAddressType, Value: "2001:db8:a0b:12f0::1/32"}, - {Type: ngfAPI.CIDRAddressType, Value: "2001:db8:a0b:12f0::1/32"}, - {Type: ngfAPI.CIDRAddressType, Value: "2001:db8:a0b:12f0::1/32"}, - {Type: ngfAPI.CIDRAddressType, Value: "2001:db8:a0b:12f0::1/32"}, - {Type: ngfAPI.CIDRAddressType, Value: "2001:db8:a0b:12f0::1/32"}, - {Type: ngfAPI.CIDRAddressType, Value: "2001:db8:a0b:12f0::1/32"}, - {Type: ngfAPI.CIDRAddressType, Value: "2001:db8:a0b:12f0::1/32"}, - {Type: ngfAPI.CIDRAddressType, Value: "2001:db8:a0b:12f0::1/32"}, - {Type: ngfAPI.CIDRAddressType, Value: "2001:db8:a0b:12f0::1/32"}, - {Type: ngfAPI.CIDRAddressType, Value: "2001:db8:a0b:12f0::1/32"}, - {Type: ngfAPI.CIDRAddressType, Value: "2001:db8:a0b:12f0::1/32"}, - {Type: ngfAPI.CIDRAddressType, Value: "2001:db8:a0b:12f0::1/32"}, - {Type: ngfAPI.CIDRAddressType, Value: "2001:db8:a0b:12f0::1/32"}, - {Type: ngfAPI.CIDRAddressType, Value: "2001:db8:a0b:12f0::1/32"}, - {Type: ngfAPI.CIDRAddressType, Value: "2001:db8:a0b:12f0::1/32"}, - {Type: ngfAPI.CIDRAddressType, Value: "2001:db8:a0b:12f0::1/32"}, - {Type: ngfAPI.CIDRAddressType, Value: "2001:db8:a0b:12f0::1/32"}, - {Type: ngfAPI.CIDRAddressType, Value: "2001:db8:a0b:12f0::1/32"}, - {Type: ngfAPI.CIDRAddressType, Value: "2001:db8:a0b:12f0::1/32"}, - {Type: ngfAPI.CIDRAddressType, Value: "2001:db8:a0b:12f0::1/32"}, + TrustedAddresses: []ngfAPI.RewriteClientIPAddress{ + {Type: ngfAPI.RewriteClientIPCIDRAddressType, Value: "2001:db8:a0b:12f0::1/32"}, + {Type: ngfAPI.RewriteClientIPCIDRAddressType, Value: "2001:db8:a0b:12f0::1/32"}, + {Type: ngfAPI.RewriteClientIPCIDRAddressType, Value: "2001:db8:a0b:12f0::1/32"}, + {Type: ngfAPI.RewriteClientIPCIDRAddressType, Value: "2001:db8:a0b:12f0::1/32"}, + {Type: ngfAPI.RewriteClientIPCIDRAddressType, Value: "2001:db8:a0b:12f0::1/32"}, + {Type: ngfAPI.RewriteClientIPCIDRAddressType, Value: "2001:db8:a0b:12f0::1/32"}, + {Type: ngfAPI.RewriteClientIPCIDRAddressType, Value: "2001:db8:a0b:12f0::1/32"}, + {Type: ngfAPI.RewriteClientIPCIDRAddressType, Value: "2001:db8:a0b:12f0::1/32"}, + {Type: ngfAPI.RewriteClientIPCIDRAddressType, Value: "2001:db8:a0b:12f0::1/32"}, + {Type: ngfAPI.RewriteClientIPCIDRAddressType, Value: "2001:db8:a0b:12f0::1/32"}, + {Type: ngfAPI.RewriteClientIPCIDRAddressType, Value: "2001:db8:a0b:12f0::1/32"}, + {Type: ngfAPI.RewriteClientIPCIDRAddressType, Value: "2001:db8:a0b:12f0::1/32"}, + {Type: ngfAPI.RewriteClientIPCIDRAddressType, Value: "2001:db8:a0b:12f0::1/32"}, + {Type: ngfAPI.RewriteClientIPCIDRAddressType, Value: "2001:db8:a0b:12f0::1/32"}, + {Type: ngfAPI.RewriteClientIPCIDRAddressType, Value: "2001:db8:a0b:12f0::1/32"}, + {Type: ngfAPI.RewriteClientIPCIDRAddressType, Value: "2001:db8:a0b:12f0::1/32"}, + {Type: ngfAPI.RewriteClientIPCIDRAddressType, Value: "2001:db8:a0b:12f0::1/32"}, + {Type: ngfAPI.RewriteClientIPCIDRAddressType, Value: "2001:db8:a0b:12f0::1/32"}, + {Type: ngfAPI.RewriteClientIPCIDRAddressType, Value: "2001:db8:a0b:12f0::1/32"}, + {Type: ngfAPI.RewriteClientIPCIDRAddressType, Value: "2001:db8:a0b:12f0::1/32"}, + {Type: ngfAPI.RewriteClientIPCIDRAddressType, Value: "2001:db8:a0b:12f0::1/32"}, }, }, }, @@ -560,13 +560,13 @@ func TestValidateRewriteClientIP(t *testing.T) { Spec: ngfAPI.NginxProxySpec{ RewriteClientIP: &ngfAPI.RewriteClientIP{ Mode: helpers.GetPointer(ngfAPI.RewriteClientIPModeType("invalid")), - TrustedAddresses: []ngfAPI.Address{ + TrustedAddresses: []ngfAPI.RewriteClientIPAddress{ { - Type: ngfAPI.CIDRAddressType, + Type: ngfAPI.RewriteClientIPCIDRAddressType, Value: "2001:db8:a0b:12f0::1/32", }, { - Type: ngfAPI.CIDRAddressType, + Type: ngfAPI.RewriteClientIPCIDRAddressType, Value: "10.0.0.1/32", }, }, @@ -599,9 +599,9 @@ func TestValidateRewriteClientIP(t *testing.T) { Spec: ngfAPI.NginxProxySpec{ RewriteClientIP: &ngfAPI.RewriteClientIP{ SetIPRecursively: helpers.GetPointer(true), - TrustedAddresses: []ngfAPI.Address{ + TrustedAddresses: []ngfAPI.RewriteClientIPAddress{ { - Type: ngfAPI.AddressType("invalid"), + Type: ngfAPI.RewriteClientIPAddressType("invalid"), Value: "2001:db8::/129", }, }, @@ -773,3 +773,107 @@ func TestValidateLogging(t *testing.T) { }) } } + +func TestValidateNginxPlus(t *testing.T) { + t.Parallel() + + tests := []struct { + np *ngfAPI.NginxProxy + name string + errorString string + expectErrCount int + }{ + { + np: &ngfAPI.NginxProxy{ + Spec: ngfAPI.NginxProxySpec{ + NginxPlus: &ngfAPI.NginxPlus{ + AllowedAddresses: []ngfAPI.NginxPlusAllowAddress{ + {Type: ngfAPI.NginxPlusAllowIPAddressType, Value: "2001:db8:a0b:12f0::1"}, + {Type: ngfAPI.NginxPlusAllowCIDRAddressType, Value: "2001:db8:a0b:12f0::1/32"}, + {Type: ngfAPI.NginxPlusAllowIPAddressType, Value: "127.0.0.3"}, + {Type: ngfAPI.NginxPlusAllowCIDRAddressType, Value: "127.0.0.3/32"}, + }, + }, + }, + }, + name: "valid NginxPlus", + errorString: "", + expectErrCount: 0, + }, + { + np: &ngfAPI.NginxProxy{ + Spec: ngfAPI.NginxProxySpec{ + NginxPlus: &ngfAPI.NginxPlus{ + AllowedAddresses: []ngfAPI.NginxPlusAllowAddress{ + {Type: ngfAPI.NginxPlusAllowCIDRAddressType, Value: "2001:db8:a0b:12f0::1/32"}, + {Type: ngfAPI.NginxPlusAllowCIDRAddressType, Value: "127.0.0.3/37"}, + }, + }, + }, + }, + name: "invalid CIDR in AllowedAddresses", + errorString: "spec.nginxPlus.value: Invalid value: \"127.0.0.3/37\": must be a valid CIDR value, " + + "(e.g. 10.9.8.0/24 or 2001:db8::/64)", + expectErrCount: 1, + }, + { + np: &ngfAPI.NginxProxy{ + Spec: ngfAPI.NginxProxySpec{ + NginxPlus: &ngfAPI.NginxPlus{ + AllowedAddresses: []ngfAPI.NginxPlusAllowAddress{ + {Type: ngfAPI.NginxPlusAllowIPAddressType, Value: "127.0.0.3"}, + {Type: ngfAPI.NginxPlusAllowIPAddressType, Value: "127.0.0.3.5/32"}, + }, + }, + }, + }, + name: "invalid IP address in AllowedAddresses", + errorString: "spec.nginxPlus.value: Invalid value: \"127.0.0.3.5/32\": must be a valid IP address, " + + "(e.g. 10.9.8.7 or 2001:db8::ffff)", + expectErrCount: 1, + }, + { + np: &ngfAPI.NginxProxy{ + Spec: ngfAPI.NginxProxySpec{ + NginxPlus: &ngfAPI.NginxPlus{ + AllowedAddresses: []ngfAPI.NginxPlusAllowAddress{ + {Type: ngfAPI.NginxPlusAllowAddressType("Hostname"), Value: "example.com"}, + }, + }, + }, + }, + name: "hostname type in AllowedAddresses", + errorString: "spec.nginxPlus.type: Unsupported value: \"Hostname\": supported " + + "values: \"CIDR\", \"IPAddress\"", + expectErrCount: 1, + }, + { + np: &ngfAPI.NginxProxy{ + Spec: ngfAPI.NginxProxySpec{ + NginxPlus: &ngfAPI.NginxPlus{ + AllowedAddresses: []ngfAPI.NginxPlusAllowAddress{ + {Type: ngfAPI.NginxPlusAllowAddressType("invalid"), Value: "example.com"}, + }, + }, + }, + }, + name: "invalid type in AllowedAddresses", + errorString: "spec.nginxPlus.type: Unsupported value: \"invalid\": supported " + + "values: \"CIDR\", \"IPAddress\"", + expectErrCount: 1, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + t.Parallel() + g := NewWithT(t) + + allErrs := validateNginxPlus(test.np) + g.Expect(allErrs).To(HaveLen(test.expectErrCount)) + if len(allErrs) > 0 { + g.Expect(allErrs.ToAggregate().Error()).To(Equal(test.errorString)) + } + }) + } +} diff --git a/site/content/reference/api.md b/site/content/reference/api.md index 77eafa4ac6..0f829e7ac7 100644 --- a/site/content/reference/api.md +++ b/site/content/reference/api.md @@ -364,13 +364,26 @@ NginxLogging +nginxPlus
+ + +NginxPlus + + + + +(Optional) +

NginxPlus specifies NGINX Plus additional settings.

+ + + + disableHTTP2
bool -(Optional)

DisableHTTP2 defines if http2 should be disabled for all servers. Default is false, meaning http2 will be enabled for all servers.

@@ -707,78 +720,6 @@ sigs.k8s.io/gateway-api/apis/v1alpha2.PolicyStatus -

Address - -

-

-(Appears on: -RewriteClientIP) -

-

-

Address is a struct that specifies address type and value.

-

- - - - - - - - - - - - - - - - - -
FieldDescription
-type
- - -AddressType - - -
-

Type specifies the type of address.

-
-value
- -string - -
-

Value specifies the address value.

-
-

AddressType -(string alias)

-

-

-(Appears on: -Address) -

-

-

AddressType specifies the type of address.

-

- - - - - - - - - - - - - - -
ValueDescription

"CIDR"

CIDRAddressType specifies that the address is a CIDR block.

-

"Hostname"

HostnameAddressType specifies that the address is a Hostname.

-

"IPAddress"

IPAddressType specifies that the address is an IP address.

-

ClientBody

@@ -1399,6 +1340,109 @@ crit, alert, and emerg messages to be logged. NginxPlus + + +

+(Appears on: +NginxProxySpec) +

+

+

NginxPlus specifies NGINX Plus additional settings. These will only be applied if NGINX Plus is being used.

+

+ + + + + + + + + + + + + +
FieldDescription
+allowedAddresses
+ + +[]NginxPlusAllowAddress + + +
+(Optional) +

AllowedAddresses specifies IPAddresses or CIDR blocks to the allow list for accessing the NGINX Plus API.

+
+

NginxPlusAllowAddress + +

+

+(Appears on: +NginxPlus) +

+

+

NginxPlusAllowAddress specifies the address type and value for an NginxPlus allow address.

+

+ + + + + + + + + + + + + + + + + +
FieldDescription
+type
+ + +NginxPlusAllowAddressType + + +
+

Type specifies the type of address.

+
+value
+ +string + +
+

Value specifies the address value.

+
+

NginxPlusAllowAddressType +(string alias)

+

+

+(Appears on: +NginxPlusAllowAddress) +

+

+

NginxPlusAllowAddressType specifies the type of address.

+

+ + + + + + + + + + + + +
ValueDescription

"CIDR"

NginxPlusAllowCIDRAddressType specifies that the address is a CIDR block.

+

"IPAddress"

NginxPlusAllowIPAddressType specifies that the address is an IP address.

+

NginxProxySpec

@@ -1476,13 +1520,26 @@ NginxLogging +nginxPlus
+ + +NginxPlus + + + + +(Optional) +

NginxPlus specifies NGINX Plus additional settings.

+ + + + disableHTTP2
bool -(Optional)

DisableHTTP2 defines if http2 should be disabled for all servers. Default is false, meaning http2 will be enabled for all servers.

@@ -1598,8 +1655,8 @@ Sets NGINX directive real_ip_recursive: -[]Address + +[]RewriteClientIPAddress @@ -1618,6 +1675,78 @@ This field is required if mode is set.

+

RewriteClientIPAddress + +

+

+(Appears on: +RewriteClientIP) +

+

+

RewriteClientIPAddress specifies the address type and value for a RewriteClientIP address.

+

+ + + + + + + + + + + + + + + + + +
FieldDescription
+type
+ + +RewriteClientIPAddressType + + +
+

Type specifies the type of address.

+
+value
+ +string + +
+

Value specifies the address value.

+
+

RewriteClientIPAddressType +(string alias)

+

+

+(Appears on: +RewriteClientIPAddress) +

+

+

RewriteClientIPAddressType specifies the type of address.

+

+ + + + + + + + + + + + + + +
ValueDescription

"CIDR"

RewriteClientIPCIDRAddressType specifies that the address is a CIDR block.

+

"Hostname"

RewriteClientIPHostnameAddressType specifies that the address is a Hostname.

+

"IPAddress"

RewriteClientIPIPAddressType specifies that the address is an IP address.

+

RewriteClientIPModeType (string alias)