Skip to content

Commit 499cfed

Browse files
committed
Implement WAFPolicy controller
1 parent ba4e627 commit 499cfed

25 files changed

+1980
-27
lines changed

build/Dockerfile.nginxplus

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ FROM alpine:${ALPINE_VERSION}
1313
ARG NGINX_PLUS_VERSION=R34
1414
# renovate: datasource=github-tags depName=nginx/agent
1515
ARG NGINX_AGENT_VERSION=v3.0.2
16-
ARG APP_PROTECT_VERSION=34.5.342
16+
ARG APP_PROTECT_VERSION=34.5.442
1717
ARG INCLUDE_NAP_WAF=false
1818
ARG NJS_DIR
1919
ARG NGINX_CONF_DIR

internal/controller/manager.go

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ import (
4848
"github.com/nginx/nginx-gateway-fabric/internal/controller/nginx/config/policies/clientsettings"
4949
"github.com/nginx/nginx-gateway-fabric/internal/controller/nginx/config/policies/observability"
5050
"github.com/nginx/nginx-gateway-fabric/internal/controller/nginx/config/policies/upstreamsettings"
51+
"github.com/nginx/nginx-gateway-fabric/internal/controller/nginx/config/policies/waf"
5152
ngxvalidation "github.com/nginx/nginx-gateway-fabric/internal/controller/nginx/config/validation"
5253
"github.com/nginx/nginx-gateway-fabric/internal/controller/provisioner"
5354
"github.com/nginx/nginx-gateway-fabric/internal/controller/state"
@@ -326,6 +327,10 @@ func createPolicyManager(
326327
GVK: mustExtractGVK(&ngfAPIv1alpha1.UpstreamSettingsPolicy{}),
327328
Validator: upstreamsettings.NewValidator(validator),
328329
},
330+
{
331+
GVK: mustExtractGVK(&ngfAPIv1alpha1.WAFPolicy{}),
332+
Validator: waf.NewValidator(validator),
333+
},
329334
}
330335

331336
return policies.NewManager(mustExtractGVK, cfgs...)
@@ -507,6 +512,12 @@ func registerControllers(
507512
controller.WithK8sPredicate(k8spredicate.GenerationChangedPredicate{}),
508513
},
509514
},
515+
{
516+
objectType: &ngfAPIv1alpha1.WAFPolicy{},
517+
options: []controller.Option{
518+
controller.WithK8sPredicate(k8spredicate.GenerationChangedPredicate{}),
519+
},
520+
},
510521
}
511522

512523
if cfg.ExperimentalFeatures {
@@ -745,6 +756,7 @@ func prepareFirstEventBatchPreparerArgs(cfg config.Config) ([]client.Object, []c
745756
&ngfAPIv1alpha1.ClientSettingsPolicyList{},
746757
&ngfAPIv1alpha2.ObservabilityPolicyList{},
747758
&ngfAPIv1alpha1.UpstreamSettingsPolicyList{},
759+
&ngfAPIv1alpha1.WAFPolicyList{},
748760
partialObjectMetadataList,
749761
}
750762

internal/controller/manager_test.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ func TestPrepareFirstEventBatchPreparerArgs(t *testing.T) {
6868
&ngfAPIv1alpha1.ClientSettingsPolicyList{},
6969
&ngfAPIv1alpha2.ObservabilityPolicyList{},
7070
&ngfAPIv1alpha1.UpstreamSettingsPolicyList{},
71+
&ngfAPIv1alpha1.WAFPolicyList{},
7172
},
7273
},
7374
{
@@ -97,6 +98,7 @@ func TestPrepareFirstEventBatchPreparerArgs(t *testing.T) {
9798
&ngfAPIv1alpha1.ClientSettingsPolicyList{},
9899
&ngfAPIv1alpha2.ObservabilityPolicyList{},
99100
&ngfAPIv1alpha1.UpstreamSettingsPolicyList{},
101+
&ngfAPIv1alpha1.WAFPolicyList{},
100102
},
101103
},
102104
{
@@ -124,6 +126,7 @@ func TestPrepareFirstEventBatchPreparerArgs(t *testing.T) {
124126
&ngfAPIv1alpha2.ObservabilityPolicyList{},
125127
&ngfAPIv1alpha1.SnippetsFilterList{},
126128
&ngfAPIv1alpha1.UpstreamSettingsPolicyList{},
129+
&ngfAPIv1alpha1.WAFPolicyList{},
127130
},
128131
},
129132
{
@@ -154,6 +157,7 @@ func TestPrepareFirstEventBatchPreparerArgs(t *testing.T) {
154157
&ngfAPIv1alpha2.ObservabilityPolicyList{},
155158
&ngfAPIv1alpha1.SnippetsFilterList{},
156159
&ngfAPIv1alpha1.UpstreamSettingsPolicyList{},
160+
&ngfAPIv1alpha1.WAFPolicyList{},
157161
},
158162
},
159163
}

internal/controller/nginx/config/generator.go

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import (
1616
"github.com/nginx/nginx-gateway-fabric/internal/controller/nginx/config/policies/clientsettings"
1717
"github.com/nginx/nginx-gateway-fabric/internal/controller/nginx/config/policies/observability"
1818
"github.com/nginx/nginx-gateway-fabric/internal/controller/nginx/config/policies/upstreamsettings"
19+
"github.com/nginx/nginx-gateway-fabric/internal/controller/nginx/config/policies/waf"
1920
"github.com/nginx/nginx-gateway-fabric/internal/controller/state/dataplane"
2021
"github.com/nginx/nginx-gateway-fabric/internal/framework/file"
2122
)
@@ -44,6 +45,9 @@ const (
4445
// includesFolder is the folder where are all include files are stored.
4546
includesFolder = configFolder + "/includes"
4647

48+
// appProtectBundleFolder is the folder where the NGINX App Protect WAF bundles are stored.
49+
appProtectBundleFolder = "/etc/app_protect/bundles"
50+
4751
// httpConfigFile is the path to the configuration file with HTTP configuration.
4852
httpConfigFile = httpFolder + "/http.conf"
4953

@@ -119,10 +123,15 @@ func (g GeneratorImpl) Generate(conf dataplane.Configuration) []agent.File {
119123
policyGenerator := policies.NewCompositeGenerator(
120124
clientsettings.NewGenerator(),
121125
observability.NewGenerator(conf.Telemetry),
126+
waf.NewGenerator(),
122127
)
123128

124129
files = append(files, g.executeConfigTemplates(conf, policyGenerator)...)
125130

131+
for id, bundle := range conf.WAF.WAFBundles {
132+
files = append(files, generateWAFBundle(id, bundle))
133+
}
134+
126135
for id, bundle := range conf.CertBundles {
127136
files = append(files, generateCertBundle(id, bundle))
128137
}
@@ -245,3 +254,19 @@ func generateCertBundle(id dataplane.CertBundleID, cert []byte) agent.File {
245254
func generateCertBundleFileName(id dataplane.CertBundleID) string {
246255
return filepath.Join(secretsFolder, string(id)+".crt")
247256
}
257+
258+
func generateWAFBundle(id dataplane.WAFBundleID, bundle []byte) agent.File {
259+
return agent.File{
260+
Meta: &pb.FileMeta{
261+
Name: GenerateWAFBundleFileName(id),
262+
Hash: filesHelper.GenerateHash(bundle),
263+
Permissions: file.RegularFileMode,
264+
Size: int64(len(bundle)),
265+
},
266+
Contents: bundle,
267+
}
268+
}
269+
270+
func GenerateWAFBundleFileName(id dataplane.WAFBundleID) string {
271+
return filepath.Join(appProtectBundleFolder, string(id)+".tgz")
272+
}

internal/controller/nginx/config/policies/policy.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ type Policy interface {
2626
type GlobalSettings struct {
2727
// TelemetryEnabled is whether telemetry is enabled in the NginxProxy resource.
2828
TelemetryEnabled bool
29+
// WAFEnabled is whether WAF is enabled in the NginxProxy resource.
30+
WAFEnabled bool
2931
}
3032

3133
// ValidateTargetRef validates a policy's targetRef for the proper group and kind.
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
package waf
2+
3+
import (
4+
"fmt"
5+
"text/template"
6+
7+
ngfAPI "github.com/nginx/nginx-gateway-fabric/apis/v1alpha1"
8+
"github.com/nginx/nginx-gateway-fabric/internal/controller/nginx/config/http"
9+
"github.com/nginx/nginx-gateway-fabric/internal/controller/nginx/config/policies"
10+
"github.com/nginx/nginx-gateway-fabric/internal/framework/helpers"
11+
)
12+
13+
var tmpl = template.Must(template.New("waf policy").Parse(wafTemplate))
14+
15+
const wafTemplate = `
16+
{{- if .BundlePath }}
17+
app_protect_enable on;
18+
app_protect_policy_file "{{ .BundlePath }}";
19+
{{- end }}
20+
{{- if .SecurityLogs }}
21+
app_protect_security_log_enable on;
22+
{{- range .SecurityLogs }}
23+
{{- if .LogProfile }}
24+
app_protect_security_log "{{ .LogProfile }}" {{ .Destination }};
25+
{{- else if .LogProfileBundlePath }}
26+
app_protect_security_log "{{ .LogProfileBundlePath }}" {{ .Destination }};
27+
{{- end }}
28+
{{- end }}
29+
{{- end }}
30+
`
31+
32+
// Generator generates nginx configuration based on a WAF policy.
33+
type Generator struct {
34+
policies.UnimplementedGenerator
35+
}
36+
37+
// NewGenerator returns a new instance of Generator.
38+
func NewGenerator() *Generator {
39+
return &Generator{}
40+
}
41+
42+
// GenerateForServer generates policy configuration for the server block.
43+
func (g Generator) GenerateForServer(pols []policies.Policy, _ http.Server) policies.GenerateResultFiles {
44+
return generate(pols)
45+
}
46+
47+
// GenerateForLocation generates policy configuration for a normal location block.
48+
func (g Generator) GenerateForLocation(pols []policies.Policy, _ http.Location) policies.GenerateResultFiles {
49+
return generate(pols)
50+
}
51+
52+
func generate(pols []policies.Policy) policies.GenerateResultFiles {
53+
files := make(policies.GenerateResultFiles, 0, len(pols))
54+
55+
for _, pol := range pols {
56+
wp, ok := pol.(*ngfAPI.WAFPolicy)
57+
if !ok {
58+
continue
59+
}
60+
61+
fields := map[string]any{}
62+
63+
if wp.Spec.PolicySource != nil && wp.Spec.PolicySource.FileLocation != "" {
64+
fileLocation := wp.Spec.PolicySource.FileLocation
65+
bundleName := helpers.ToSafeFileName(fileLocation)
66+
bundlePath := fmt.Sprintf("%s/%s.tgz", "/etc/app_protect/bundles", bundleName)
67+
fields["BundlePath"] = bundlePath
68+
}
69+
70+
if len(wp.Spec.SecurityLogs) > 0 {
71+
securityLogs := make([]map[string]string, 0, len(wp.Spec.SecurityLogs))
72+
73+
for _, secLog := range wp.Spec.SecurityLogs {
74+
logEntry := map[string]string{}
75+
76+
if secLog.LogProfile != nil {
77+
logEntry["LogProfile"] = string(*secLog.LogProfile)
78+
}
79+
80+
if secLog.LogProfileBundle != nil && secLog.LogProfileBundle.FileLocation != "" {
81+
bundleName := helpers.ToSafeFileName(secLog.LogProfileBundle.FileLocation)
82+
bundlePath := fmt.Sprintf("%s/%s.tgz", "/etc/app_protect/bundles", bundleName)
83+
logEntry["LogProfileBundlePath"] = bundlePath
84+
}
85+
86+
destination := formatSecurityLogDestination(secLog.Destination)
87+
logEntry["Destination"] = destination
88+
89+
securityLogs = append(securityLogs, logEntry)
90+
}
91+
92+
fields["SecurityLogs"] = securityLogs
93+
}
94+
95+
files = append(files, policies.File{
96+
Name: fmt.Sprintf("WafPolicy_%s_%s.conf", wp.Namespace, wp.Name),
97+
Content: helpers.MustExecuteTemplate(tmpl, fields),
98+
})
99+
}
100+
101+
return files
102+
}
103+
104+
func formatSecurityLogDestination(dest ngfAPI.SecurityLogDestination) string {
105+
switch dest.Type {
106+
case ngfAPI.SecurityLogDestinationTypeStderr:
107+
return "stderr"
108+
case ngfAPI.SecurityLogDestinationTypeFile:
109+
if dest.File != nil {
110+
return dest.File.Path
111+
}
112+
return "stderr"
113+
case ngfAPI.SecurityLogDestinationTypeSyslog:
114+
if dest.Syslog != nil {
115+
return fmt.Sprintf("syslog:server=%s", dest.Syslog.Server)
116+
}
117+
return "stderr"
118+
default:
119+
return "stderr"
120+
}
121+
}

0 commit comments

Comments
 (0)