Skip to content

Commit

Permalink
NAP DOS - AllowList (#5824)
Browse files Browse the repository at this point in the history
  • Loading branch information
pasmant authored Jul 8, 2024
1 parent c5102cb commit ad88fb9
Show file tree
Hide file tree
Showing 34 changed files with 517 additions and 29 deletions.
2 changes: 1 addition & 1 deletion build/scripts/nap-dos.sh
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,5 @@

set -e

mkdir -p /root/app_protect_dos /etc/nginx/dos/policies /etc/nginx/dos/logconfs /shared/cores /var/log/adm /var/run/adm
mkdir -p /root/app_protect_dos /etc/nginx/dos/policies /etc/nginx/dos/logconfs /etc/nginx/dos/allowlist /shared/cores /var/log/adm /var/run/adm
chmod 777 /shared/cores /var/log/adm /var/run/adm /etc/app_protect_dos
10 changes: 10 additions & 0 deletions config/crd/bases/appprotectdos.f5.com_dosprotectedresources.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,16 @@ spec:
description: DosProtectedResourceSpec defines the properties and values
a DosProtectedResource can have.
properties:
allowList:
description: AllowList is a list of allowed IPs and subnet masks
items:
description: AllowListEntry represents an IP address and a subnet
mask.
properties:
ipWithMask:
type: string
type: object
type: array
apDosMonitor:
description: 'ApDosMonitor is how NGINX App Protect DoS monitors the
stress level of the protected object. The monitor requests are sent
Expand Down
10 changes: 10 additions & 0 deletions deploy/crds-nap-dos.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,16 @@ spec:
description: DosProtectedResourceSpec defines the properties and values
a DosProtectedResource can have.
properties:
allowList:
description: AllowList is a list of allowed IPs and subnet masks
items:
description: AllowListEntry represents an IP address and a subnet
mask.
properties:
ipWithMask:
type: string
type: object
type: array
apDosMonitor:
description: 'ApDosMonitor is how NGINX App Protect DoS monitors the
stress level of the protected object. The monitor requests are sent
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ spec:
|``dosSecurityLog.enable`` | Enables security log. | ``bool`` | No |
|``dosSecurityLog.apDosLogConf`` | The [App Protect DoS log conf]({{< relref "installation/integrations/app-protect-dos/configuration.md#app-protect-dos-logs" >}}) resource. Accepts an optional namespace. | ``string`` | No |
|``dosSecurityLog.dosLogDest`` | The log destination for the security log. Accepted variables are ``syslog:server=<ip-address &#124; localhost &#124; dns-name>:<port>``, ``stderr``, ``<absolute path to file>``. Default is ``"syslog:server=127.0.0.1:514"``. | ``string`` | No |
|``allowList`` | List of allowed IP addresses and subnet masks. Each entry is represented by an `IPWithMask` string. | ``[]AllowListEntry`` | No |
{{% /table %}}

### DosProtectedResource.apDosPolicy
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,8 @@ spec:
enable: true
apDosLogConf: "doslogconf"
dosLogDest: "syslog-svc.default.svc.cluster.local:514"
allowList:
- ipWithMask: "192.168.1.1/24"
- ipWithMask: "10.244.0.1/32"
- ipWithMask: "2023::4ef3/128"
- ipWithMask: "2034::2300/120"
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,8 @@ spec:
enable: true
apDosLogConf: "doslogconf"
dosLogDest: "syslog-svc.default.svc.cluster.local:514"
allowList:
- ipWithMask: "192.168.1.1/24"
- ipWithMask: "10.244.0.1/32"
- ipWithMask: "2023::4ef3/128"
- ipWithMask: "2034::2300/120"
57 changes: 57 additions & 0 deletions internal/configs/configurator.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ const (
appProtectUserSigIndex = "/etc/nginx/waf/nac-usersigs/index.conf"
appProtectDosPolicyFolder = "/etc/nginx/dos/policies/"
appProtectDosLogConfFolder = "/etc/nginx/dos/logconfs/"
appProtectDosAllowListFolder = "/etc/nginx/dos/allowlist/"
)

// DefaultServerSecretPath is the full path to the Secret with a TLS cert and a key for the default server. #nosec G101
Expand Down Expand Up @@ -1679,6 +1680,11 @@ func (cnf *Configurator) updateApResources(ingEx *IngressEx) *AppProtectResource

func (cnf *Configurator) updateDosResource(dosEx *DosEx) {
if dosEx != nil {
if dosEx.DosProtected != nil {
allowListFileName := appProtectDosAllowListFileName(dosEx.DosProtected.GetNamespace(), dosEx.DosProtected.GetName())
allowListContent := generateApDosAllowListFileContent(dosEx.DosProtected.Spec.AllowList)
cnf.nginxManager.CreateAppProtectResourceFile(allowListFileName, allowListContent)
}
if dosEx.DosPolicy != nil {
policyFileName := appProtectDosPolicyFileName(dosEx.DosPolicy.GetNamespace(), dosEx.DosPolicy.GetName())
policyContent := generateApResourceFileContent(dosEx.DosPolicy)
Expand Down Expand Up @@ -1738,6 +1744,48 @@ func generateApResourceFileContent(apResource *unstructured.Unstructured) []byte
return data
}

func generateApDosAllowListFileContent(allowList []v1beta1.AllowListEntry) []byte {
type IPAddress struct {
IPAddress string `json:"ipAddress"`
}

type IPAddressList struct {
IPAddresses []IPAddress `json:"ipAddresses"`
BlockRequests string `json:"blockRequests"`
}

type Policy struct {
IPAddressLists []IPAddressList `json:"ip-address-lists"`
}

type AllowListPolicy struct {
Policy Policy `json:"policy"`
}

ipAddresses := make([]IPAddress, len(allowList))
for i, entry := range allowList {
ipAddresses[i] = IPAddress{IPAddress: entry.IPWithMask}
}

allowListPolicy := AllowListPolicy{
Policy: Policy{
IPAddressLists: []IPAddressList{
{
IPAddresses: ipAddresses,
BlockRequests: "transparent",
},
},
},
}

data, err := json.Marshal(allowListPolicy)
if err != nil {
return nil
}

return data
}

// ResourceOperation represents a function that changes configuration in relation to an unstructured resource.
type ResourceOperation func(resource *v1beta1.DosProtectedResource, ingExes []*IngressEx, mergeableIngresses []*MergeableIngresses, vsExes []*VirtualServerEx) (Warnings, error)

Expand Down Expand Up @@ -1862,6 +1910,10 @@ func appProtectDosLogConfFileName(namespace string, name string) string {
return fmt.Sprintf("%s%s_%s.json", appProtectDosLogConfFolder, namespace, name)
}

func appProtectDosAllowListFileName(namespace string, name string) string {
return fmt.Sprintf("%s%s_%s.json", appProtectDosAllowListFolder, namespace, name)
}

// DeleteAppProtectDosPolicy updates Ingresses and VirtualServers that use AP Dos Policy after that policy is deleted
func (cnf *Configurator) DeleteAppProtectDosPolicy(resource *unstructured.Unstructured) {
cnf.nginxManager.DeleteAppProtectResourceFile(appProtectDosPolicyFileName(resource.GetNamespace(), resource.GetName()))
Expand All @@ -1872,6 +1924,11 @@ func (cnf *Configurator) DeleteAppProtectDosLogConf(resource *unstructured.Unstr
cnf.nginxManager.DeleteAppProtectResourceFile(appProtectDosLogConfFileName(resource.GetNamespace(), resource.GetName()))
}

// DeleteAppProtectDosAllowList updates Ingresses and VirtualServers that use AP Allow List Configuration after that policy is deleted
func (cnf *Configurator) DeleteAppProtectDosAllowList(obj *v1beta1.DosProtectedResource) {
cnf.nginxManager.DeleteAppProtectResourceFile(appProtectDosAllowListFileName(obj.Namespace, obj.Name))
}

// AddInternalRouteConfig adds internal route server to NGINX Configuration and reloads NGINX
func (cnf *Configurator) AddInternalRouteConfig() error {
cnf.staticCfgParams.EnableInternalRoutes = true
Expand Down
63 changes: 63 additions & 0 deletions internal/configs/configurator_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package configs

import (
"encoding/json"
"os"
"reflect"
"testing"
Expand All @@ -15,6 +16,7 @@ import (
"github.com/nginxinc/kubernetes-ingress/internal/configs/version2"
"github.com/nginxinc/kubernetes-ingress/internal/nginx"
conf_v1 "github.com/nginxinc/kubernetes-ingress/pkg/apis/configuration/v1"
"github.com/nginxinc/kubernetes-ingress/pkg/apis/dos/v1beta1"
)

func createTestStaticConfigParams() *StaticConfigParams {
Expand Down Expand Up @@ -1651,3 +1653,64 @@ var (
},
}
)

func TestGenerateApDosAllowListFileContent(t *testing.T) {
tests := []struct {
name string
allowList []v1beta1.AllowListEntry
want []byte
wantErr bool
}{
{
name: "Empty allow list",
allowList: []v1beta1.AllowListEntry{},
want: []byte(`{"policy":{"ip-address-lists":[{"ipAddresses":[],"blockRequests":"transparent"}]}}`),
wantErr: false,
},
{
name: "Single valid IPv4 entry",
allowList: []v1beta1.AllowListEntry{
{IPWithMask: "192.168.1.1/32"},
},
want: []byte(`{"policy":{"ip-address-lists":[{"ipAddresses":[{"ipAddress":"192.168.1.1/32"}],"blockRequests":"transparent"}]}}`),
wantErr: false,
},
{
name: "Single valid IPv6 entry",
allowList: []v1beta1.AllowListEntry{
{IPWithMask: "2001:0db8:85a3:0000:0000:8a2e:0370:7334/128"},
},
want: []byte(`{"policy":{"ip-address-lists":[{"ipAddresses":[{"ipAddress":"2001:0db8:85a3:0000:0000:8a2e:0370:7334/128"}],"blockRequests":"transparent"}]}}`),
wantErr: false,
},
{
name: "Multiple valid entries",
allowList: []v1beta1.AllowListEntry{
{IPWithMask: "192.168.1.1/32"},
{IPWithMask: "2001:0db8:85a3:0000:0000:8a2e:0370:7334/128"},
},
want: []byte(`{"policy":{"ip-address-lists":[{"ipAddresses":[{"ipAddress":"192.168.1.1/32"},{"ipAddress":"2001:0db8:85a3:0000:0000:8a2e:0370:7334/128"}],"blockRequests":"transparent"}]}}`),
wantErr: false,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := generateApDosAllowListFileContent(tt.allowList)
if (got == nil) != tt.wantErr {
t.Errorf("generateApDosAllowListFileContent() error = %v, wantErr %v", got == nil, tt.wantErr)
return
}
if !tt.wantErr && !reflect.DeepEqual(got, tt.want) {
var gotFormatted, wantFormatted interface{}
if err := json.Unmarshal(got, &gotFormatted); err != nil {
t.Errorf("Failed to unmarshal got: %v", err)
}
if err := json.Unmarshal(tt.want, &wantFormatted); err != nil {
t.Errorf("Failed to unmarshal want: %v", err)
}
t.Errorf("generateApDosAllowListFileContent() = \n%#v, \nwant \n%#v", gotFormatted, wantFormatted)
}
})
}
}
5 changes: 5 additions & 0 deletions internal/configs/dos.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ type appProtectDosResource struct {
AppProtectDosAccessLogDst string
AppProtectDosPolicyFile string
AppProtectDosLogConfFile string
AppProtectDosAllowListPath string
}

func getAppProtectDosResource(dosEx *DosEx) *appProtectDosResource {
Expand All @@ -26,6 +27,10 @@ func getAppProtectDosResource(dosEx *DosEx) *appProtectDosResource {
}
dosResource.AppProtectDosName = protected.Namespace + "/" + protected.Name + "/" + protected.Spec.Name

if protected.Spec.AllowList != nil {
dosResource.AppProtectDosAllowListPath = appProtectDosAllowListFileName(protected.Namespace, protected.Name)
}

if protected.Spec.ApDosMonitor != nil {
dosResource.AppProtectDosMonitorURI = protected.Spec.ApDosMonitor.URI
dosResource.AppProtectDosMonitorProtocol = protected.Spec.ApDosMonitor.Protocol
Expand Down
34 changes: 34 additions & 0 deletions internal/configs/dos_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,25 @@ func TestUpdateApDosResource(t *testing.T) {
},
},
}
appProtectDosProtectedWithAllowList := &v1beta1.DosProtectedResource{
TypeMeta: v1.TypeMeta{},
ObjectMeta: v1.ObjectMeta{
Name: "dosWithAllowList",
Namespace: "test-ns",
},
Spec: v1beta1.DosProtectedResourceSpec{
Enable: true,
Name: "dos-protected",
ApDosMonitor: &v1beta1.ApDosMonitor{
URI: "example.com",
},
DosAccessLogDest: "127.0.0.1:5561",
AllowList: []v1beta1.AllowListEntry{
{IPWithMask: "192.168.1.0/24"},
{IPWithMask: "10.0.0.0/8"},
},
},
}

tests := []struct {
dosProtectedEx *DosEx
Expand Down Expand Up @@ -127,6 +146,21 @@ func TestUpdateApDosResource(t *testing.T) {
},
msg: "app protect dos policy and log conf",
},
{
dosProtectedEx: &DosEx{
DosProtected: appProtectDosProtectedWithAllowList,
DosPolicy: appProtectDosPolicy,
},
expected: &appProtectDosResource{
AppProtectDosEnable: "on",
AppProtectDosName: "test-ns/dosWithAllowList/dos-protected",
AppProtectDosMonitorURI: "example.com",
AppProtectDosAccessLogDst: "syslog:server=127.0.0.1:5561",
AppProtectDosPolicyFile: "/etc/nginx/dos/policies/test-ns_test-name.json",
AppProtectDosAllowListPath: "/etc/nginx/dos/allowlist/test-ns_dosWithAllowList.json",
},
msg: "app protect dos with allow list",
},
}

for _, test := range tests {
Expand Down
1 change: 1 addition & 0 deletions internal/configs/ingress.go
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,7 @@ func generateNginxCfg(p NginxCfgParams) (version1.IngressNginxConfig, Warnings)
server.AppProtectDosMonitorProtocol = p.dosResource.AppProtectDosMonitorProtocol
server.AppProtectDosMonitorTimeout = p.dosResource.AppProtectDosMonitorTimeout
server.AppProtectDosName = p.dosResource.AppProtectDosName
server.AppProtectDosAllowListPath = p.dosResource.AppProtectDosAllowListPath
server.AppProtectDosAccessLogDst = p.dosResource.AppProtectDosAccessLogDst
server.AppProtectDosPolicyFile = p.dosResource.AppProtectDosPolicyFile
server.AppProtectDosLogConfFile = p.dosResource.AppProtectDosLogConfFile
Expand Down
32 changes: 18 additions & 14 deletions internal/configs/ingress_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -2398,13 +2398,14 @@ func TestGenerateNginxCfgForAppProtectDos(t *testing.T) {
isPlus := true
configParams := NewDefaultConfigParams(isPlus)
dosResource := &appProtectDosResource{
AppProtectDosEnable: "on",
AppProtectDosName: "dos.example.com",
AppProtectDosMonitorURI: "monitor-name",
AppProtectDosAccessLogDst: "access-log-dest",
AppProtectDosPolicyFile: "/etc/nginx/dos/policies/default_policy",
AppProtectDosLogEnable: true,
AppProtectDosLogConfFile: "/etc/nginx/dos/logconfs/default_logconf syslog:server=127.0.0.1:514",
AppProtectDosEnable: "on",
AppProtectDosName: "dos.example.com",
AppProtectDosMonitorURI: "monitor-name",
AppProtectDosAccessLogDst: "access-log-dest",
AppProtectDosPolicyFile: "/etc/nginx/dos/policies/default_policy",
AppProtectDosLogEnable: true,
AppProtectDosLogConfFile: "/etc/nginx/dos/logconfs/default_logconf syslog:server=127.0.0.1:514",
AppProtectDosAllowListPath: "/etc/nginx/dos/allowlist/default_dos",
}
staticCfgParams := &StaticConfigParams{
MainAppProtectDosLoadModule: true,
Expand All @@ -2414,6 +2415,7 @@ func TestGenerateNginxCfgForAppProtectDos(t *testing.T) {
expected.Servers[0].AppProtectDosEnable = "on"
expected.Servers[0].AppProtectDosPolicyFile = "/etc/nginx/dos/policies/default_policy"
expected.Servers[0].AppProtectDosLogConfFile = "/etc/nginx/dos/logconfs/default_logconf syslog:server=127.0.0.1:514"
expected.Servers[0].AppProtectDosAllowListPath = "/etc/nginx/dos/allowlist/default_dos"
expected.Servers[0].AppProtectDosLogEnable = true
expected.Servers[0].AppProtectDosName = "dos.example.com"
expected.Servers[0].AppProtectDosMonitorURI = "monitor-name"
Expand Down Expand Up @@ -2465,13 +2467,14 @@ func TestGenerateNginxCfgForMergeableIngressesForAppProtectDos(t *testing.T) {
isPlus := true
configParams := NewDefaultConfigParams(isPlus)
dosResource := &appProtectDosResource{
AppProtectDosEnable: "on",
AppProtectDosName: "dos.example.com",
AppProtectDosMonitorURI: "monitor-name",
AppProtectDosAccessLogDst: "access-log-dest",
AppProtectDosPolicyFile: "/etc/nginx/dos/policies/default_policy",
AppProtectDosLogEnable: true,
AppProtectDosLogConfFile: "/etc/nginx/dos/logconfs/default_logconf syslog:server=127.0.0.1:514",
AppProtectDosEnable: "on",
AppProtectDosName: "dos.example.com",
AppProtectDosMonitorURI: "monitor-name",
AppProtectDosAccessLogDst: "access-log-dest",
AppProtectDosPolicyFile: "/etc/nginx/dos/policies/default_policy",
AppProtectDosLogEnable: true,
AppProtectDosLogConfFile: "/etc/nginx/dos/logconfs/default_logconf syslog:server=127.0.0.1:514",
AppProtectDosAllowListPath: "/etc/nginx/dos/allowlist/default_dos",
}
staticCfgParams := &StaticConfigParams{
MainAppProtectDosLoadModule: true,
Expand All @@ -2481,6 +2484,7 @@ func TestGenerateNginxCfgForMergeableIngressesForAppProtectDos(t *testing.T) {
expected.Servers[0].AppProtectDosEnable = "on"
expected.Servers[0].AppProtectDosPolicyFile = "/etc/nginx/dos/policies/default_policy"
expected.Servers[0].AppProtectDosLogConfFile = "/etc/nginx/dos/logconfs/default_logconf syslog:server=127.0.0.1:514"
expected.Servers[0].AppProtectDosAllowListPath = "/etc/nginx/dos/allowlist/default_dos"
expected.Servers[0].AppProtectDosLogEnable = true
expected.Servers[0].AppProtectDosName = "dos.example.com"
expected.Servers[0].AppProtectDosMonitorURI = "monitor-name"
Expand Down
2 changes: 2 additions & 0 deletions internal/configs/version1/__snapshots__/template_test.snap
Original file line number Diff line number Diff line change
Expand Up @@ -558,6 +558,7 @@ server {
access_log /var/log/dos log_dos if=$loggable;
app_protect_dos_monitor uri=/path/to/monitor protocol=http1 timeout=30;
app_protect_dos_name "testdos";
app_protect_dos_access_file "/etc/nginx/dos/allowlist/default_test.example.com";
if ($scheme = http) {
Expand Down Expand Up @@ -653,6 +654,7 @@ server {
access_log /var/log/dos log_dos if=$loggable;
app_protect_dos_monitor uri=/path/to/monitor protocol=http1 timeout=30;
app_protect_dos_name "testdos";
app_protect_dos_access_file "/etc/nginx/dos/allowlist/default_test.example.com";
if ($scheme = http) {
Expand Down
1 change: 1 addition & 0 deletions internal/configs/version1/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,7 @@ type Server struct {
AppProtectDosMonitorProtocol string
AppProtectDosMonitorTimeout uint64
AppProtectDosName string
AppProtectDosAllowListPath string
AppProtectDosAccessLogDst string

SpiffeCerts bool
Expand Down
Loading

0 comments on commit ad88fb9

Please sign in to comment.