Skip to content

Implement WAFPolicy controller #3532

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 2 commits into
base: feat/nap-waf
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion build/Dockerfile.nginxplus
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ FROM alpine:${ALPINE_VERSION}
ARG NGINX_PLUS_VERSION=R34
# renovate: datasource=github-tags depName=nginx/agent
ARG NGINX_AGENT_VERSION=v3.0.2
ARG APP_PROTECT_VERSION=34.5.342
ARG APP_PROTECT_VERSION=34.5.442
ARG INCLUDE_NAP_WAF=false
ARG NJS_DIR
ARG NGINX_CONF_DIR
Expand Down
12 changes: 12 additions & 0 deletions internal/controller/manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@
"github.com/nginx/nginx-gateway-fabric/internal/controller/nginx/config/policies/clientsettings"
"github.com/nginx/nginx-gateway-fabric/internal/controller/nginx/config/policies/observability"
"github.com/nginx/nginx-gateway-fabric/internal/controller/nginx/config/policies/upstreamsettings"
"github.com/nginx/nginx-gateway-fabric/internal/controller/nginx/config/policies/waf"
ngxvalidation "github.com/nginx/nginx-gateway-fabric/internal/controller/nginx/config/validation"
"github.com/nginx/nginx-gateway-fabric/internal/controller/provisioner"
"github.com/nginx/nginx-gateway-fabric/internal/controller/state"
Expand Down Expand Up @@ -326,6 +327,10 @@
GVK: mustExtractGVK(&ngfAPIv1alpha1.UpstreamSettingsPolicy{}),
Validator: upstreamsettings.NewValidator(validator),
},
{
GVK: mustExtractGVK(&ngfAPIv1alpha1.WAFPolicy{}),
Validator: waf.NewValidator(validator),
},

Check warning on line 333 in internal/controller/manager.go

View check run for this annotation

Codecov / codecov/patch

internal/controller/manager.go#L330-L333

Added lines #L330 - L333 were not covered by tests
}

return policies.NewManager(mustExtractGVK, cfgs...)
Expand Down Expand Up @@ -507,6 +512,12 @@
controller.WithK8sPredicate(k8spredicate.GenerationChangedPredicate{}),
},
},
{
objectType: &ngfAPIv1alpha1.WAFPolicy{},
options: []controller.Option{
controller.WithK8sPredicate(k8spredicate.GenerationChangedPredicate{}),
},
},
}

if cfg.ExperimentalFeatures {
Expand Down Expand Up @@ -745,6 +756,7 @@
&ngfAPIv1alpha1.ClientSettingsPolicyList{},
&ngfAPIv1alpha2.ObservabilityPolicyList{},
&ngfAPIv1alpha1.UpstreamSettingsPolicyList{},
&ngfAPIv1alpha1.WAFPolicyList{},
partialObjectMetadataList,
}

Expand Down
4 changes: 4 additions & 0 deletions internal/controller/manager_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ func TestPrepareFirstEventBatchPreparerArgs(t *testing.T) {
&ngfAPIv1alpha1.ClientSettingsPolicyList{},
&ngfAPIv1alpha2.ObservabilityPolicyList{},
&ngfAPIv1alpha1.UpstreamSettingsPolicyList{},
&ngfAPIv1alpha1.WAFPolicyList{},
},
},
{
Expand Down Expand Up @@ -97,6 +98,7 @@ func TestPrepareFirstEventBatchPreparerArgs(t *testing.T) {
&ngfAPIv1alpha1.ClientSettingsPolicyList{},
&ngfAPIv1alpha2.ObservabilityPolicyList{},
&ngfAPIv1alpha1.UpstreamSettingsPolicyList{},
&ngfAPIv1alpha1.WAFPolicyList{},
},
},
{
Expand Down Expand Up @@ -124,6 +126,7 @@ func TestPrepareFirstEventBatchPreparerArgs(t *testing.T) {
&ngfAPIv1alpha2.ObservabilityPolicyList{},
&ngfAPIv1alpha1.SnippetsFilterList{},
&ngfAPIv1alpha1.UpstreamSettingsPolicyList{},
&ngfAPIv1alpha1.WAFPolicyList{},
},
},
{
Expand Down Expand Up @@ -154,6 +157,7 @@ func TestPrepareFirstEventBatchPreparerArgs(t *testing.T) {
&ngfAPIv1alpha2.ObservabilityPolicyList{},
&ngfAPIv1alpha1.SnippetsFilterList{},
&ngfAPIv1alpha1.UpstreamSettingsPolicyList{},
&ngfAPIv1alpha1.WAFPolicyList{},
},
},
}
Expand Down
25 changes: 25 additions & 0 deletions internal/controller/nginx/config/generator.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
"github.com/nginx/nginx-gateway-fabric/internal/controller/nginx/config/policies/clientsettings"
"github.com/nginx/nginx-gateway-fabric/internal/controller/nginx/config/policies/observability"
"github.com/nginx/nginx-gateway-fabric/internal/controller/nginx/config/policies/upstreamsettings"
"github.com/nginx/nginx-gateway-fabric/internal/controller/nginx/config/policies/waf"
"github.com/nginx/nginx-gateway-fabric/internal/controller/state/dataplane"
"github.com/nginx/nginx-gateway-fabric/internal/framework/file"
)
Expand Down Expand Up @@ -44,6 +45,9 @@
// includesFolder is the folder where are all include files are stored.
includesFolder = configFolder + "/includes"

// appProtectBundleFolder is the folder where the NGINX App Protect WAF bundles are stored.
appProtectBundleFolder = "/etc/app_protect/bundles"

// httpConfigFile is the path to the configuration file with HTTP configuration.
httpConfigFile = httpFolder + "/http.conf"

Expand Down Expand Up @@ -119,10 +123,15 @@
policyGenerator := policies.NewCompositeGenerator(
clientsettings.NewGenerator(),
observability.NewGenerator(conf.Telemetry),
waf.NewGenerator(),
)

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

for id, bundle := range conf.WAF.WAFBundles {
files = append(files, generateWAFBundle(id, bundle))
}

Check warning on line 133 in internal/controller/nginx/config/generator.go

View check run for this annotation

Codecov / codecov/patch

internal/controller/nginx/config/generator.go#L132-L133

Added lines #L132 - L133 were not covered by tests

for id, bundle := range conf.CertBundles {
files = append(files, generateCertBundle(id, bundle))
}
Expand Down Expand Up @@ -245,3 +254,19 @@
func generateCertBundleFileName(id dataplane.CertBundleID) string {
return filepath.Join(secretsFolder, string(id)+".crt")
}

func generateWAFBundle(id dataplane.WAFBundleID, bundle []byte) agent.File {
return agent.File{
Meta: &pb.FileMeta{
Name: GenerateWAFBundleFileName(id),
Hash: filesHelper.GenerateHash(bundle),
Permissions: file.RegularFileMode,
Size: int64(len(bundle)),
},
Contents: bundle,
}

Check warning on line 267 in internal/controller/nginx/config/generator.go

View check run for this annotation

Codecov / codecov/patch

internal/controller/nginx/config/generator.go#L258-L267

Added lines #L258 - L267 were not covered by tests
}

func GenerateWAFBundleFileName(id dataplane.WAFBundleID) string {
return filepath.Join(appProtectBundleFolder, string(id)+".tgz")

Check warning on line 271 in internal/controller/nginx/config/generator.go

View check run for this annotation

Codecov / codecov/patch

internal/controller/nginx/config/generator.go#L270-L271

Added lines #L270 - L271 were not covered by tests
}
2 changes: 2 additions & 0 deletions internal/controller/nginx/config/policies/policy.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ type Policy interface {
type GlobalSettings struct {
// TelemetryEnabled is whether telemetry is enabled in the NginxProxy resource.
TelemetryEnabled bool
// WAFEnabled is whether WAF is enabled in the NginxProxy resource.
WAFEnabled bool
}

// ValidateTargetRef validates a policy's targetRef for the proper group and kind.
Expand Down
121 changes: 121 additions & 0 deletions internal/controller/nginx/config/policies/waf/generator.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
package waf

import (
"fmt"
"text/template"

ngfAPI "github.com/nginx/nginx-gateway-fabric/apis/v1alpha1"
"github.com/nginx/nginx-gateway-fabric/internal/controller/nginx/config/http"
"github.com/nginx/nginx-gateway-fabric/internal/controller/nginx/config/policies"
"github.com/nginx/nginx-gateway-fabric/internal/framework/helpers"
)

var tmpl = template.Must(template.New("waf policy").Parse(wafTemplate))

const wafTemplate = `
{{- if .BundlePath }}
app_protect_enable on;
app_protect_policy_file "{{ .BundlePath }}";
{{- end }}
{{- if .SecurityLogs }}
app_protect_security_log_enable on;
{{- range .SecurityLogs }}
{{- if .LogProfile }}
app_protect_security_log "{{ .LogProfile }}" {{ .Destination }};
{{- else if .LogProfileBundlePath }}
app_protect_security_log "{{ .LogProfileBundlePath }}" {{ .Destination }};
{{- end }}
{{- end }}
{{- end }}
`

// Generator generates nginx configuration based on a WAF policy.
type Generator struct {
policies.UnimplementedGenerator
}

// NewGenerator returns a new instance of Generator.
func NewGenerator() *Generator {
return &Generator{}
}

// GenerateForServer generates policy configuration for the server block.
func (g Generator) GenerateForServer(pols []policies.Policy, _ http.Server) policies.GenerateResultFiles {
return generate(pols)
}

// GenerateForLocation generates policy configuration for a normal location block.
func (g Generator) GenerateForLocation(pols []policies.Policy, _ http.Location) policies.GenerateResultFiles {
return generate(pols)
}

func generate(pols []policies.Policy) policies.GenerateResultFiles {
files := make(policies.GenerateResultFiles, 0, len(pols))

for _, pol := range pols {
wp, ok := pol.(*ngfAPI.WAFPolicy)
if !ok {
continue
}

fields := map[string]any{}

if wp.Spec.PolicySource != nil && wp.Spec.PolicySource.FileLocation != "" {
fileLocation := wp.Spec.PolicySource.FileLocation
bundleName := helpers.ToSafeFileName(fileLocation)
bundlePath := fmt.Sprintf("%s/%s.tgz", "/etc/app_protect/bundles", bundleName)
fields["BundlePath"] = bundlePath
}

if len(wp.Spec.SecurityLogs) > 0 {
securityLogs := make([]map[string]string, 0, len(wp.Spec.SecurityLogs))

for _, secLog := range wp.Spec.SecurityLogs {
logEntry := map[string]string{}

if secLog.LogProfile != nil {
logEntry["LogProfile"] = string(*secLog.LogProfile)
}

if secLog.LogProfileBundle != nil && secLog.LogProfileBundle.FileLocation != "" {
bundleName := helpers.ToSafeFileName(secLog.LogProfileBundle.FileLocation)
bundlePath := fmt.Sprintf("%s/%s.tgz", "/etc/app_protect/bundles", bundleName)
logEntry["LogProfileBundlePath"] = bundlePath
}

destination := formatSecurityLogDestination(secLog.Destination)
logEntry["Destination"] = destination

securityLogs = append(securityLogs, logEntry)
}

fields["SecurityLogs"] = securityLogs
}

files = append(files, policies.File{
Name: fmt.Sprintf("WafPolicy_%s_%s.conf", wp.Namespace, wp.Name),
Content: helpers.MustExecuteTemplate(tmpl, fields),
})
}

return files
}

func formatSecurityLogDestination(dest ngfAPI.SecurityLogDestination) string {
switch dest.Type {
case ngfAPI.SecurityLogDestinationTypeStderr:
return "stderr"
case ngfAPI.SecurityLogDestinationTypeFile:
if dest.File != nil {
return dest.File.Path
}
return "stderr"

Check warning on line 112 in internal/controller/nginx/config/policies/waf/generator.go

View check run for this annotation

Codecov / codecov/patch

internal/controller/nginx/config/policies/waf/generator.go#L112

Added line #L112 was not covered by tests
case ngfAPI.SecurityLogDestinationTypeSyslog:
if dest.Syslog != nil {
return fmt.Sprintf("syslog:server=%s", dest.Syslog.Server)
}
return "stderr"
default:
return "stderr"

Check warning on line 119 in internal/controller/nginx/config/policies/waf/generator.go

View check run for this annotation

Codecov / codecov/patch

internal/controller/nginx/config/policies/waf/generator.go#L117-L119

Added lines #L117 - L119 were not covered by tests
}
}
Loading
Loading