Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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)
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"
case ngfAPI.SecurityLogDestinationTypeSyslog:
if dest.Syslog != nil {
return fmt.Sprintf("syslog:server=%s", dest.Syslog.Server)
}
return "stderr"
default:
return "stderr"
}
}
Loading
Loading