GCP: enabling Cloud Armor on GCP HTTPS LB for Anthos Service Mesh

If you are using Anthos Service Mesh to deliver your public applications from a GFE HTTPS LB, I would strongly suggest enabling Cloud Armor which is a WAF (web application firewall) that can mitigate and defend against a variety of attacks such as cross-site scripting and denial of service.

As a summary overview, the first step is to create a Cloud Armor security policy, then create a BackendConfig that is referenced from an annotation on the service you want to protect.

Here is how you would create a simple policy that senses cross-site scripting and denial of service.

gcloud compute security-policies create my-security-policy --description "my XSS and DDOS policy"

gcloud compute security-policies update my-security-policy --log-level=VERBOSE

gcloud compute security-policies rules create 1000 --security-policy my-security-policy --expression "evaluatePreconfiguredExpr('xss-stable')" --action deny-403 --description "XSS attack filtering"

gcloud beta compute security-policies update my-security-policy --enable-layer7-ddos-defense

With this security policy created, the next step is to add an annotation to your service that references a BackendConfig.  In turn, the BackendConfig  provides a health check and security policy reference.

Enabling security policy on ASM IngressGateway service

If you are following the pattern from Google’s documentation on “Exposing service mesh applications through GKE Ingress“, then your GCP HTTPS LB is pointing at the ASM service entrypoint.  All services you project unto the VirtualService/Gateway have an IngressGateway selected.  Therefore you can set the security policy on the IngressGateway, and all services projected unto it will benefit.

In this scenario, your ingressgateway service should have an “cloud.google.com/backend-config” annotation pointing to the BackendConfig like below.

kind: Service
metadata:
  name: istio-ingressgateway
  labels:
    app: istio-ingressgateway
    istio: ingressgateway
  annotations:
    cloud.google.com/backend-config: '{"default": "ingress-backendconfig"}'
    ...

spec:
  ports:
  - name: status-port
    port: 15021
    protocol: TCP
    targetPort: 15021
...

The BackendConfig is then defined as below, using port 15021 as the health check port, and referring to “my-security-policy” as created by gcloud earlier.

apiVersion: cloud.google.com/v1
kind: BackendConfig
metadata:
  name: ingress-backendconfig
  #namespace: default
spec:
  healthCheck:
    requestPath: /healthz/ready
    port: 15021
    type: HTTP
  securityPolicy:
    name: my-security-policy

Enabling security policy directly on individual service

If instead of pointing at the ASM entry point you are using something like Container native load-balancing where the GCP HTTPS LB uses NEG (Network Endpoint Groups) to point directly at pods, then you need to set the BackendConfig explicitly for that individual service.

Below is a simple hello world service, with the added “cloud.google.com/backend-config” annotation that points to a BackendConfig named “golang-hello-world-web-service-backendconfig”.

apiVersion: v1
kind: Service
metadata:
  name: golang-hello-world-web-service
  #namespace: default
  labels:
    app: golang-hello-world-web
  annotations:
    cloud.google.com/backend-config: '{"default": "golang-hello-world-web-service-backendconfig"}'
spec:
  ports:
  - port: 8080
    name: http
    targetPort: 8080
    protocol: TCP
  selector:
    app: golang-hello-world-web

The matching BackendConfig defines the health check at port 8080, as well as applying the “my-security-policy” created by gcloud earlier.

apiVersion: cloud.google.com/v1
kind: BackendConfig
metadata:
  name: golang-hello-world-web-service-backendconfig
spec:
  healthCheck:
    checkIntervalSec: 15
    port: 8080
    type: HTTP
    requestPath: /healthz
  securityPolicy:
    name: my-security-policy

 

REFERENCES

google, configure security policies, examples

google, WAF rules language reference

google, load balancer types

google, load balancer logging and monitoring

google, exposing service mesh apps through gke ingress

alwaysupon.com, GKE, ASM, and Cloud Armor

tudip.com, GCP HTTP LB with Cloud Armor

google, Cloud Armor

itnext.io, GCP Ingress for Anthos multi-cluster ingress and LB and WAF

NOTES

Add rule to block certain regions

gcloud compute security-policies list

my_security_policy=my-policy
rule_num=1000
gcloud compute security-policies rules create $rule_num \
--security-policy $my_security_policy \
--expression "origin.region_code == 'RU' || origin.region_code == 'IR'" \
--action "deny-403" \
--description "Block embargo countries"

Add list of preconfigured WAF rules

rules="xss-stable cve-canary sqli-v33-stable lfi-v33-stable rfi-v33-stable rce-v33-stable methodenforcement-v33-stable scannerdetection-v33-stable protocolattack-v33-stable sessionfixation-v33-stable java-v33-stable nodejs-v33-stable"
COUNTER=1000
for rule in $rules; do
  gcloud compute security-policies rules create $COUNTER --security-policy $my_security_policy --expression "evaluatePreconfiguredExpr('$rule')" --action deny-403 --description "$rule"
  ((COUNTER+=2))
done

block a single external IP address

theIP=a.b.c.d
my_security_policy=my-policy
rule_num=1000

gcloud compute security-policies describe $my_security_policy

# takes ~60 seconds to take affect
gcloud compute security-policies rules create $rule_num \
--security-policy $my_security_policy \
--description "block traffic from $theIP/32" \
--src-ip-ranges "$theIP/32" \
--action "deny-403"

# to delete rule and re-allow IP address
gcloud compute security-policies rules delete $rule_num \
--security-policy $my_security_policy --quiet

throttles traffic by domain, path, and method

ACTION=create
SECURITY_POLICY=my-security-policy
PRIORITY=100
RATE_LIMIT_THRESHOLD_COUNT=3
RATE_LIMIT_THRESHOLD_INTERVAL_SEC=30
THE_HOST=my.domain.com
THE_PATH=/test
THE_METHOD=GET
gcloud compute security-policies rules $ACTION $PRIORITY \
   --security-policy=$SECURITY_POLICY \
   --expression="request.method=='$THE_METHOD' && request.path.startsWith('$THE_PATH') && request.headers['host'].lower().contains('$THE_HOST')" \
   --action "throttle" \
   --rate-limit-threshold-count=$RATE_LIMIT_THRESHOLD_COUNT \
   --rate-limit-threshold-interval-sec=$RATE_LIMIT_THRESHOLD_INTERVAL_SEC \
   --conform-action=allow \
   --exceed-action=deny-502 \
   --enforce-on-key=sni