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

Open
wants to merge 1 commit into
base: feat/nap-waf
Choose a base branch
from
Open
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 @@ import (
"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 @@ func createPolicyManager(
GVK: mustExtractGVK(&ngfAPIv1alpha1.UpstreamSettingsPolicy{}),
Validator: upstreamsettings.NewValidator(validator),
},
{
GVK: mustExtractGVK(&ngfAPIv1alpha1.WAFPolicy{}),
Validator: waf.NewValidator(validator),
},
}

return policies.NewManager(mustExtractGVK, cfgs...)
Expand Down Expand Up @@ -507,6 +512,12 @@ func registerControllers(
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 @@ func prepareFirstEventBatchPreparerArgs(cfg config.Config) ([]client.Object, []c
&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 @@ import (
"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 @@ const (
// 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 @@ func (g GeneratorImpl) Generate(conf dataplane.Configuration) []agent.File {
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))
}

for id, bundle := range conf.CertBundles {
files = append(files, generateCertBundle(id, bundle))
}
Expand Down Expand Up @@ -245,3 +254,19 @@ func generateCertBundle(id dataplane.CertBundleID, cert []byte) agent.File {
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,
}
}

func GenerateWAFBundleFileName(id dataplane.WAFBundleID) string {
return filepath.Join(appProtectBundleFolder, string(id)+".tgz")
}
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)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

First arg is a string literal, doesn't seem like we need to format it.

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)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ditto

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"
case ngfAPI.SecurityLogDestinationTypeSyslog:
if dest.Syslog != nil {
return fmt.Sprintf("syslog:server=%s", dest.Syslog.Server)
}
return "stderr"
default:
return "stderr"
}
}
Loading
Loading