Skip to content

Commit 77a793f

Browse files
committed
Implement WAFPolicy controller
1 parent ba4e627 commit 77a793f

22 files changed

+1977
-25
lines changed

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

0 commit comments

Comments
 (0)