Skip to content

Commit

Permalink
Configure per location web application firewall
Browse files Browse the repository at this point in the history
  • Loading branch information
jcmoraisjr committed Jul 26, 2018
1 parent 54dbbad commit 0587083
Show file tree
Hide file tree
Showing 9 changed files with 141 additions and 15 deletions.
20 changes: 19 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ The following annotations are supported:
||[`ingress.kubernetes.io/rewrite-target`](#rewrite-target)|path string|-|
||[`ingress.kubernetes.io/server-alias`](#server-alias)|domain name or regex|-|
|`[1]`|[`ingress.kubernetes.io/use-resolver`](#dns-service-discovery)|resolver name]|[doc](/examples/dns-service-discovery)|
|`[1]`|[`ingress.kubernetes.io/waf`](#waf)|"modsecurity"|[doc](/examples/modsecurity)|

### Affinity

Expand Down Expand Up @@ -229,6 +230,15 @@ The following table shows some examples:
|/abc/|/abc/|/|/|
|/abc/|/abc/x|/|/x|

### WAF

Defines which web application firewall (WAF) implementation should be used
to validate requests. Currently the only supported value is `modsecurity`.
See also [modsecurity-endpoints](#modsecurity-endpoints) configmap option.

This annotation has no effect if the target web application firewall isn't
configured.

## ConfigMap

If using ConfigMap to configure HAProxy Ingress, use
Expand Down Expand Up @@ -500,7 +510,15 @@ http://cbonte.github.io/haproxy-dconv/1.8/configuration.html#3.2-maxconn
Configure a comma-separated list of `IP:port` of HAProxy agents (SPOA) for ModSecurity.
The default configuration expects the `contrib/modsecurity` implementation from HAProxy source code.

See also the [example](/examples/modsecurity) page.
Currently all http requests will be parsed by the ModSecurity agent, even if the ingress resource
wasn't configured to deny requests based on ModSecurity response.

See also:

* [`ingress.kubernetes.io/waf`](#waf) annotation
* [example](/examples/modsecurity) page

Reference:

* http://cbonte.github.io/haproxy-dconv/1.8/configuration.html#9.3
* https://www.haproxy.org/download/1.8/doc/SPOE.txt
Expand Down
16 changes: 9 additions & 7 deletions examples/modsecurity/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,8 @@ $ kubectl run echo \
--expose
```

... and create its ingress resource. No need to use a valid domain, `echo.domain` below is fine:
... and create its ingress resource. Remember to annotate waf as `modsecurity`.
No need to use a valid domain, `echo.domain` below is fine:

```console
$ kubectl create -f - <<EOF
Expand All @@ -84,6 +85,7 @@ kind: Ingress
metadata:
annotations:
ingress.kubernetes.io/ssl-redirect: "false"
ingress.kubernetes.io/waf: "modsecurity"
name: echo
spec:
rules:
Expand All @@ -110,7 +112,7 @@ Content-Type: text/plain
Test now with a malicious request:

```
curl -i '192.168.100.99?p=../../etc/passwd' -H 'Host: echo.domain'
curl -i '192.168.100.99?p=/etc/passwd' -H 'Host: echo.domain'
HTTP/1.0 403 Forbidden
Cache-Control: no-cache
Connection: close
Expand All @@ -134,10 +136,10 @@ $ kubectl -n ingress-controller logs --tail=10 modsecurity-spoa-5g5h2
1527464273.942819 [00] [client 127.0.0.1] ModSecurity: Access denied with code 403 (phase 2). Matche
d phrase "etc/passwd" at ARGS:p. [file "/etc/modsecurity/owasp-modsecurity-crs/rules/REQUEST-930-APP
LICATION-ATTACK-LFI.conf"] [line "108"] [id "930120"] [rev "4"] [msg "OS File Access Attempt"] [data
"Matched Data: etc/passwd found within ARGS:p: ../../etc/passwd"] [severity "CRITICAL"] [ver "OWASP
_CRS/3.0.0"] [maturity "9"] [accuracy "9"] [tag "application-multi"] [tag "language-multi"] [tag "pl
atform-multi"] [tag "attack-lfi"] [tag "OWASP_CRS/WEB_ATTACK/FILE_INJECTION"] [tag "WASCTC/WASC-33"]
[tag "OWASP_TOP_10/A4"] [tag "PCI/6.5.4"] [hostname "ingress.localdomain"] [uri "http://echo.domain
/"] [unique_id ""]
"Matched Data: etc/passwd found within ARGS:p: /etc/passwd"] [severity "CRITICAL"] [ver "OWASP_CRS/
3.0.0"] [maturity "9"] [accuracy "9"] [tag "application-multi"] [tag "language-multi"] [tag "platfor
m-multi"] [tag "attack-lfi"] [tag "OWASP_CRS/WEB_ATTACK/FILE_INJECTION"] [tag "WASCTC/WASC-33"] [tag
"OWASP_TOP_10/A4"] [tag "PCI/6.5.4"] [hostname "ingress.localdomain"] [uri "http://echo.domain/"] [
unique_id ""]
...
```
67 changes: 67 additions & 0 deletions pkg/common/ingress/annotations/waf/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
/*
Copyright 2018 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/

package waf

import (
"github.com/golang/glog"
"github.com/jcmoraisjr/haproxy-ingress/pkg/common/ingress/annotations/parser"
extensions "k8s.io/api/extensions/v1beta1"
"regexp"
)

const (
wafAnn = "ingress.kubernetes.io/waf"
)

var (
wafAnnRegex = regexp.MustCompile(`^(modsecurity)$`)
)

type waf struct{}

// Config is the web application firewall configuration
type Config struct {
Mode string
}

// NewParser creates a new waf annotation parser
func NewParser() parser.IngressAnnotation {
return waf{}
}

// Parse parses waf annotation
func (w waf) Parse(ing *extensions.Ingress) (interface{}, error) {
s, err := parser.GetStringAnnotation(wafAnn, ing)
if err != nil {
return Config{}, nil
}
if !wafAnnRegex.MatchString(s) {
glog.Warningf("ignoring invalid WAF option: %v", s)
return Config{}, nil
}
return Config{
Mode: s,
}, nil
}

// Equal tests for equality between two waf Config types
func (c1 *Config) Equal(c2 *Config) bool {
if c1.Mode != c2.Mode {
return false
}
return true
}
4 changes: 3 additions & 1 deletion pkg/common/ingress/controller/annotations.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,8 @@ import (
"github.com/jcmoraisjr/haproxy-ingress/pkg/common/ingress/annotations/clientbodybuffersize"
"github.com/jcmoraisjr/haproxy-ingress/pkg/common/ingress/annotations/connection"
"github.com/jcmoraisjr/haproxy-ingress/pkg/common/ingress/annotations/cors"
"github.com/jcmoraisjr/haproxy-ingress/pkg/common/ingress/annotations/dnsresolvers"
"github.com/jcmoraisjr/haproxy-ingress/pkg/common/ingress/annotations/defaultbackend"
"github.com/jcmoraisjr/haproxy-ingress/pkg/common/ingress/annotations/dnsresolvers"
"github.com/jcmoraisjr/haproxy-ingress/pkg/common/ingress/annotations/healthcheck"
"github.com/jcmoraisjr/haproxy-ingress/pkg/common/ingress/annotations/hsts"
"github.com/jcmoraisjr/haproxy-ingress/pkg/common/ingress/annotations/ipwhitelist"
Expand All @@ -49,6 +49,7 @@ import (
"github.com/jcmoraisjr/haproxy-ingress/pkg/common/ingress/annotations/upstreamhashby"
"github.com/jcmoraisjr/haproxy-ingress/pkg/common/ingress/annotations/upstreamvhost"
"github.com/jcmoraisjr/haproxy-ingress/pkg/common/ingress/annotations/vtsfilterkey"
"github.com/jcmoraisjr/haproxy-ingress/pkg/common/ingress/annotations/waf"
"github.com/jcmoraisjr/haproxy-ingress/pkg/common/ingress/errors"
"github.com/jcmoraisjr/haproxy-ingress/pkg/common/ingress/resolver"
extensions "k8s.io/api/extensions/v1beta1"
Expand Down Expand Up @@ -90,6 +91,7 @@ func newAnnotationExtractor(cfg extractorConfig) annotationExtractor {
"ServiceUpstream": serviceupstream.NewParser(),
"SessionAffinity": sessionaffinity.NewParser(),
"SlotsIncrement": slotsincrement.NewParser(cfg),
"WAF": waf.NewParser(),
"BlueGreen": bluegreen.NewParser(),
"SSLPassthrough": sslpassthrough.NewParser(),
"ConfigurationSnippet": snippet.NewParser(),
Expand Down
3 changes: 3 additions & 0 deletions pkg/common/ingress/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ import (
"github.com/jcmoraisjr/haproxy-ingress/pkg/common/ingress/annotations/ratelimit"
"github.com/jcmoraisjr/haproxy-ingress/pkg/common/ingress/annotations/redirect"
"github.com/jcmoraisjr/haproxy-ingress/pkg/common/ingress/annotations/rewrite"
"github.com/jcmoraisjr/haproxy-ingress/pkg/common/ingress/annotations/waf"
"github.com/jcmoraisjr/haproxy-ingress/pkg/common/ingress/defaults"
"github.com/jcmoraisjr/haproxy-ingress/pkg/common/ingress/resolver"
"github.com/jcmoraisjr/haproxy-ingress/pkg/common/ingress/store"
Expand Down Expand Up @@ -369,6 +370,8 @@ type Location struct {
// https://github.com/vozlt/nginx-module-vts#vhost_traffic_status_filter_by_set_key
// +optional
VtsFilterKey string `json:"vtsFilterKey,omitempty"`
// WAF has per location web application firewall configs
WAF waf.Config `json:"waf"`
// ConfigurationSnippet contains additional configuration for the backend
// to be considered in the configuration of the location
ConfigurationSnippet snippet.Config `json:"configurationSnippet"`
Expand Down
3 changes: 3 additions & 0 deletions pkg/common/ingress/types_equals.go
Original file line number Diff line number Diff line change
Expand Up @@ -393,6 +393,9 @@ func (l1 *Location) Equal(l2 *Location) bool {
if l1.UsePortInRedirects != l2.UsePortInRedirects {
return false
}
if !l1.WAF.Equal(&l2.WAF) {
return false
}
if !l1.ConfigurationSnippet.Equal(&l2.ConfigurationSnippet) {
return false
}
Expand Down
16 changes: 16 additions & 0 deletions pkg/controller/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import (
"github.com/jcmoraisjr/haproxy-ingress/pkg/common/ingress/annotations/cors"
"github.com/jcmoraisjr/haproxy-ingress/pkg/common/ingress/annotations/dnsresolvers"
"github.com/jcmoraisjr/haproxy-ingress/pkg/common/ingress/annotations/hsts"
"github.com/jcmoraisjr/haproxy-ingress/pkg/common/ingress/annotations/waf"
"github.com/jcmoraisjr/haproxy-ingress/pkg/common/ingress/defaults"
"github.com/jcmoraisjr/haproxy-ingress/pkg/common/net/ssl"
"github.com/jcmoraisjr/haproxy-ingress/pkg/types"
Expand Down Expand Up @@ -289,6 +290,7 @@ func (cfg *haConfig) createHAProxyServers() {
SSLRedirect: sslRedirect,
HSTS: serverHSTS(server),
CORS: serverCORS(server),
WAF: serverWAF(server),
HasRateLimit: serverHasRateLimit(server),
CertificateAuth: server.CertificateAuth,
Alias: server.Alias,
Expand Down Expand Up @@ -347,6 +349,7 @@ func (cfg *haConfig) newHAProxyLocations(server *ingress.Server) ([]*types.HAPro
Backend: location.Backend,
CORS: location.CorsConfig,
HSTS: location.HSTS,
WAF: location.WAF,
Rewrite: location.Rewrite,
Redirect: location.Redirect,
SSLRedirect: location.Rewrite.SSLRedirect && cfg.allowRedirect(location.Path),
Expand Down Expand Up @@ -540,6 +543,19 @@ func serverCORS(server *ingress.Server) *cors.CorsConfig {
return cors
}

func serverWAF(server *ingress.Server) *waf.Config {
var waf *waf.Config
waf = nil
for _, location := range server.Locations {
if waf == nil {
waf = &location.WAF
} else if !location.WAF.Equal(waf) {
return nil
}
}
return waf
}

func serverHasRateLimit(server *ingress.Server) bool {
for _, location := range server.Locations {
if location.RateLimit.Connections.Limit > 0 || location.RateLimit.RPS.Limit > 0 {
Expand Down
3 changes: 3 additions & 0 deletions pkg/types/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import (
"github.com/jcmoraisjr/haproxy-ingress/pkg/common/ingress/annotations/ratelimit"
"github.com/jcmoraisjr/haproxy-ingress/pkg/common/ingress/annotations/redirect"
"github.com/jcmoraisjr/haproxy-ingress/pkg/common/ingress/annotations/rewrite"
"github.com/jcmoraisjr/haproxy-ingress/pkg/common/ingress/annotations/waf"
"github.com/jcmoraisjr/haproxy-ingress/pkg/common/ingress/defaults"
)

Expand Down Expand Up @@ -139,6 +140,7 @@ type (
SSLRedirect bool `json:"sslRedirect"`
HSTS *hsts.Config `json:"hsts"`
CORS *cors.CorsConfig `json:"cors"`
WAF *waf.Config `json:"waf"`
HasRateLimit bool `json:"hasRateLimit"`
CertificateAuth authtls.AuthSSLConfig `json:"certificateAuth,omitempty"`
Alias string `json:"alias,omitempty"`
Expand All @@ -151,6 +153,7 @@ type (
Backend string `json:"backend"`
CORS cors.CorsConfig `json:"cors"`
HSTS hsts.Config `json:"hsts"`
WAF waf.Config `json:"waf"`
Rewrite rewrite.Redirect `json:"rewrite,omitempty"`
Redirect redirect.Redirect `json:"redirect,omitempty"`
Userlist Userlist `json:"userlist,omitempty"`
Expand Down
24 changes: 18 additions & 6 deletions rootfs/etc/haproxy/template/haproxy.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -289,12 +289,6 @@ frontend httpfront-{{ if $isShared }}shared-frontend{{ else if $isDefault }}defa
{{ $snippet }}
{{- end }}

{{- /*------------------------------------*/}}
{{- if ne $cfg.ModSecurity "" }}
filter spoe engine modsecurity config /etc/haproxy/spoe-modsecurity.conf
http-request deny if { var(txn.modsec.code) -m int gt 0 }
{{- end }}

{{- /*------------------------------------*/}}
http-request set-var(txn.hdr_host) req.hdr(host)
{{- if $hasHTTPStoHTTP }}
Expand All @@ -304,6 +298,24 @@ frontend httpfront-{{ if $isShared }}shared-frontend{{ else if $isDefault }}defa
{{- end }}
{{- end }}

{{- /*------------------------------------*/}}
{{- if ne $cfg.ModSecurity "" }}
filter spoe engine modsecurity config /etc/haproxy/spoe-modsecurity.conf
{{- range $server := $servers }}
{{- if $server.WAF }}
{{- if eq $server.WAF.Mode "modsecurity" }}
http-request deny if {{ $server.ACLLabel }} { var(txn.modsec.code) -m int gt 0 }
{{- end }}
{{- else }}{{/* this range only makes sense if $server.WAF is nil */}}
{{- range $location := $server.Locations }}
{{- if eq $location.WAF.Mode "modsecurity" }}
http-request deny if {{ $server.ACLLabel }}{{ $location.HAMatchPath }} { var(txn.modsec.code) -m int gt 0 }
{{- end }}
{{- end }}
{{- end }}
{{- end }}
{{- end }}

{{- /*------------------------------------*/}}
{{- range $server := $servers }}
{{- if and $server.IsCACert (not $isCACert) }}
Expand Down

0 comments on commit 0587083

Please sign in to comment.