Chapter 7. Tutorial: Using AWS WAF and AWS ALBs to protect ROSA workloads AWS WAF is a web application firewall that lets you monitor the HTTP and HTTPS requests that are forwarded to your protected web application resources. You can use an AWS Application Load Balancer (ALB) to add a Web Application Firewall (WAF) to your Red Hat OpenShift Service on AWS (ROSA) workloads. Using an external solution protects ROSA resources from experiencing denial of service due to handling the WAF. Important It is recommended that you use the more flexible CloudFront method unless you absolutely must use an ALB based solution. 7.1. Prerequisites Multiple availability zone (AZ) ROSA (HCP or Classic) cluster. Note AWS ALBs require at least two public subnets across AZs, per the AWS documentation . For this reason, only multiple AZ ROSA clusters can be used with ALBs. You have access to the OpenShift CLI ( oc ). You have access to the AWS CLI ( aws ). 7.1.1. Environment setup Prepare the environment variables: USD export AWS_PAGER="" USD export CLUSTER=USD(oc get infrastructure cluster -o=jsonpath="{.status.infrastructureName}") USD export REGION=USD(oc get infrastructure cluster -o=jsonpath="{.status.platformStatus.aws.region}") USD export OIDC_ENDPOINT=USD(oc get authentication.config.openshift.io cluster -o jsonpath='{.spec.serviceAccountIssuer}' | sed 's|^https://||') USD export AWS_ACCOUNT_ID=USD(aws sts get-caller-identity --query Account --output text) USD export SCRATCH="/tmp/USD{CLUSTER}/alb-waf" USD mkdir -p USD{SCRATCH} USD echo "Cluster: USD(echo USD{CLUSTER} | sed 's/-[a-z0-9]\{5\}USD//'), Region: USD{REGION}, OIDC Endpoint: USD{OIDC_ENDPOINT}, AWS Account ID: USD{AWS_ACCOUNT_ID}" 7.1.2. AWS VPC and subnets Note This section only applies to clusters that were deployed into existing VPCs. If you did not deploy your cluster into an existing VPC, skip this section and proceed to the installation section below. Set the below variables to the proper values for your ROSA deployment: USD export VPC_ID=<vpc-id> 1 USD export PUBLIC_SUBNET_IDS=(<space-separated-list-of-ids>) 2 USD export PRIVATE_SUBNET_IDS=(<space-separated-list-of-ids>) 3 1 Replace with the VPC ID of the cluster, for example: export VPC_ID=vpc-04c429b7dbc4680ba . 2 Replace with a space-separated list of the private subnet IDs of the cluster, making sure to preserve the () . For example: export PUBLIC_SUBNET_IDS=(subnet-056fd6861ad332ba2 subnet-08ce3b4ec753fe74c subnet-071aa28228664972f) . 3 Replace with a space-separated list of the private subnet IDs of the cluster, making sure to preserve the () . For example: export PRIVATE_SUBNET_IDS=(subnet-0b933d72a8d72c36a subnet-0817eb72070f1d3c2 subnet-0806e64159b66665a) . Add a tag to your cluster's VPC with the cluster identifier: USD aws ec2 create-tags --resources USD{VPC_ID} \ --tags Key=kubernetes.io/cluster/USD{CLUSTER},Value=shared --region USD{REGION} Add a tag to your public subnets: USD aws ec2 create-tags \ --resources USD{PUBLIC_SUBNET_IDS} \ --tags Key=kubernetes.io/role/elb,Value='1' \ Key=kubernetes.io/cluster/USD{CLUSTER},Value=shared \ --region USD{REGION} Add a tag to your private subnets: USD aws ec2 create-tags \ --resources USD{PRIVATE_SUBNET_IDS} \ --tags Key=kubernetes.io/role/internal-elb,Value='1' \ Key=kubernetes.io/cluster/USD{CLUSTER},Value=shared \ --region USD{REGION} 7.2. Deploy the AWS Load Balancer Operator The AWS Load Balancer Operator is used to used to install, manage and configure an instance of aws-load-balancer-controller in a ROSA cluster. To deploy ALBs in ROSA, we need to first deploy the AWS Load Balancer Operator. Create a new project to deploy the AWS Load Balancer Operator into by running the following command: USD oc new-project aws-load-balancer-operator Create an AWS IAM policy for the AWS Load Balancer Controller if one does not already exist by running the following command: Note The policy is sourced from the upstream AWS Load Balancer Controller policy . This is required by the operator to function. USD POLICY_ARN=USD(aws iam list-policies --query \ "Policies[?PolicyName=='aws-load-balancer-operator-policy'].{ARN:Arn}" \ --output text) USD if [[ -z "USD{POLICY_ARN}" ]]; then wget -O "USD{SCRATCH}/load-balancer-operator-policy.json" \ https://raw.githubusercontent.com/kubernetes-sigs/aws-load-balancer-controller/main/docs/install/iam_policy.json POLICY_ARN=USD(aws --region "USDREGION" --query Policy.Arn \ --output text iam create-policy \ --policy-name aws-load-balancer-operator-policy \ --policy-document "file://USD{SCRATCH}/load-balancer-operator-policy.json") fi Create an AWS IAM trust policy for AWS Load Balancer Operator: USD cat <<EOF > "USD{SCRATCH}/trust-policy.json" { "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Condition": { "StringEquals" : { "USD{OIDC_ENDPOINT}:sub": ["system:serviceaccount:aws-load-balancer-operator:aws-load-balancer-operator-controller-manager", "system:serviceaccount:aws-load-balancer-operator:aws-load-balancer-controller-cluster"] } }, "Principal": { "Federated": "arn:aws:iam::USDAWS_ACCOUNT_ID:oidc-provider/USD{OIDC_ENDPOINT}" }, "Action": "sts:AssumeRoleWithWebIdentity" } ] } EOF Create an AWS IAM role for the AWS Load Balancer Operator: USD ROLE_ARN=USD(aws iam create-role --role-name "USD{CLUSTER}-alb-operator" \ --assume-role-policy-document "file://USD{SCRATCH}/trust-policy.json" \ --query Role.Arn --output text) Attach the AWS Load Balancer Operator policy to the IAM role we created previously by running the following command: USD aws iam attach-role-policy --role-name "USD{CLUSTER}-alb-operator" \ --policy-arn USD{POLICY_ARN} Create a secret for the AWS Load Balancer Operator to assume our newly created AWS IAM role: USD cat << EOF | oc apply -f - apiVersion: v1 kind: Secret metadata: name: aws-load-balancer-operator namespace: aws-load-balancer-operator stringData: credentials: | [default] role_arn = USD{ROLE_ARN} web_identity_token_file = /var/run/secrets/openshift/serviceaccount/token EOF Install the AWS Load Balancer Operator: USD cat << EOF | oc apply -f - apiVersion: operators.coreos.com/v1 kind: OperatorGroup metadata: name: aws-load-balancer-operator namespace: aws-load-balancer-operator spec: upgradeStrategy: Default --- apiVersion: operators.coreos.com/v1alpha1 kind: Subscription metadata: name: aws-load-balancer-operator namespace: aws-load-balancer-operator spec: channel: stable-v1.0 installPlanApproval: Automatic name: aws-load-balancer-operator source: redhat-operators sourceNamespace: openshift-marketplace startingCSV: aws-load-balancer-operator.v1.0.0 EOF Deploy an instance of the AWS Load Balancer Controller using the operator: Note If you get an error here wait a minute and try again, it means the Operator has not completed installing yet. USD cat << EOF | oc apply -f - apiVersion: networking.olm.openshift.io/v1 kind: AWSLoadBalancerController metadata: name: cluster spec: credentials: name: aws-load-balancer-operator enabledAddons: - AWSWAFv2 EOF Check the that the operator and controller pods are both running: USD oc -n aws-load-balancer-operator get pods You should see the following, if not wait a moment and retry: NAME READY STATUS RESTARTS AGE aws-load-balancer-controller-cluster-6ddf658785-pdp5d 1/1 Running 0 99s aws-load-balancer-operator-controller-manager-577d9ffcb9-w6zqn 2/2 Running 0 2m4s 7.3. Deploy a sample application Create a new project for our sample application: USD oc new-project hello-world Deploy a hello world application: USD oc new-app -n hello-world --image=docker.io/openshift/hello-openshift Convert the pre-created service resource to a NodePort service type: USD oc -n hello-world patch service hello-openshift -p '{"spec":{"type":"NodePort"}}' Deploy an AWS ALB using the AWS Load Balancer Operator: USD cat << EOF | oc apply -f - apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: hello-openshift-alb namespace: hello-world annotations: alb.ingress.kubernetes.io/scheme: internet-facing spec: ingressClassName: alb rules: - http: paths: - path: / pathType: Exact backend: service: name: hello-openshift port: number: 8080 EOF Curl the AWS ALB Ingress endpoint to verify the hello world application is accessible: Note AWS ALB provisioning takes a few minutes. If you receive an error that says curl: (6) Could not resolve host , please wait and try again. USD INGRESS=USD(oc -n hello-world get ingress hello-openshift-alb -o jsonpath='{.status.loadBalancer.ingress[0].hostname}') USD curl "http://USD{INGRESS}" Example output Hello OpenShift! 7.3.1. Configure the AWS WAF The AWS WAF service is a web application firewall that lets you monitor, protect, and control the HTTP and HTTPS requests that are forwarded to your protected web application resources, like ROSA. Create a AWS WAF rules file to apply to our web ACL: USD cat << EOF > USD{SCRATCH}/waf-rules.json [ { "Name": "AWS-AWSManagedRulesCommonRuleSet", "Priority": 0, "Statement": { "ManagedRuleGroupStatement": { "VendorName": "AWS", "Name": "AWSManagedRulesCommonRuleSet" } }, "OverrideAction": { "None": {} }, "VisibilityConfig": { "SampledRequestsEnabled": true, "CloudWatchMetricsEnabled": true, "MetricName": "AWS-AWSManagedRulesCommonRuleSet" } }, { "Name": "AWS-AWSManagedRulesSQLiRuleSet", "Priority": 1, "Statement": { "ManagedRuleGroupStatement": { "VendorName": "AWS", "Name": "AWSManagedRulesSQLiRuleSet" } }, "OverrideAction": { "None": {} }, "VisibilityConfig": { "SampledRequestsEnabled": true, "CloudWatchMetricsEnabled": true, "MetricName": "AWS-AWSManagedRulesSQLiRuleSet" } } ] EOF This will enable the Core (Common) and SQL AWS Managed Rule Sets. Create an AWS WAF Web ACL using the rules we specified above: USD WAF_ARN=USD(aws wafv2 create-web-acl \ --name USD{CLUSTER}-waf \ --region USD{REGION} \ --default-action Allow={} \ --scope REGIONAL \ --visibility-config SampledRequestsEnabled=true,CloudWatchMetricsEnabled=true,MetricName=USD{CLUSTER}-waf-metrics \ --rules file://USD{SCRATCH}/waf-rules.json \ --query 'Summary.ARN' \ --output text) Annotate the Ingress resource with the AWS WAF Web ACL ARN: USD oc annotate -n hello-world ingress.networking.k8s.io/hello-openshift-alb \ alb.ingress.kubernetes.io/wafv2-acl-arn=USD{WAF_ARN} Wait for 10 seconds for the rules to propagate and test that the app still works: USD curl "http://USD{INGRESS}" Example output Hello OpenShift! Test that the WAF denies a bad request: USD curl -X POST "http://USD{INGRESS}" \ -F "user='<script><alert>Hello></alert></script>'" Example output <html> <head><title>403 Forbidden</title></head> <body> <center><h1>403 Forbidden</h1></center> </body> </html Note Activation of the AWS WAF integration can sometimes take several minutes. If you do not receive a 403 Forbidden error, please wait a few seconds and try again. The expected result is a 403 Forbidden error, which means the AWS WAF is protecting your application. 7.4. Additional resources Adding Extra Security with AWS WAF, CloudFront and ROSA | Amazon Web Services on YouTube

Commands:
export AWS_PAGER="" export CLUSTER=USD(oc get infrastructure cluster -o=jsonpath="{.status.infrastructureName}") export REGION=USD(oc get infrastructure cluster -o=jsonpath="{.status.platformStatus.aws.region}") export OIDC_ENDPOINT=USD(oc get authentication.config.openshift.io cluster -o jsonpath='{.spec.serviceAccountIssuer}' | sed 's|^https://||') export AWS_ACCOUNT_ID=USD(aws sts get-caller-identity --query Account --output text) export SCRATCH="/tmp/USD{CLUSTER}/alb-waf" mkdir -p USD{SCRATCH} echo "Cluster: USD(echo USD{CLUSTER} | sed 's/-[a-z0-9]\{5\}USD//'), Region: USD{REGION}, OIDC Endpoint: USD{OIDC_ENDPOINT}, AWS Account ID: USD{AWS_ACCOUNT_ID}"
export VPC_ID=<vpc-id> 1 export PUBLIC_SUBNET_IDS=(<space-separated-list-of-ids>) 2 export PRIVATE_SUBNET_IDS=(<space-separated-list-of-ids>) 3
aws ec2 create-tags --resources USD{VPC_ID} --tags Key=kubernetes.io/cluster/USD{CLUSTER},Value=shared --region USD{REGION}
aws ec2 create-tags --resources USD{PUBLIC_SUBNET_IDS} --tags Key=kubernetes.io/role/elb,Value='1' Key=kubernetes.io/cluster/USD{CLUSTER},Value=shared --region USD{REGION}
aws ec2 create-tags --resources USD{PRIVATE_SUBNET_IDS} --tags Key=kubernetes.io/role/internal-elb,Value='1' Key=kubernetes.io/cluster/USD{CLUSTER},Value=shared --region USD{REGION}
oc new-project aws-load-balancer-operator
POLICY_ARN=USD(aws iam list-policies --query "Policies[?PolicyName=='aws-load-balancer-operator-policy'].{ARN:Arn}" --output text)
if [[ -z "USD{POLICY_ARN}" ]]; then wget -O "USD{SCRATCH}/load-balancer-operator-policy.json" https://raw.githubusercontent.com/kubernetes-sigs/aws-load-balancer-controller/main/docs/install/iam_policy.json POLICY_ARN=USD(aws --region "USDREGION" --query Policy.Arn --output text iam create-policy --policy-name aws-load-balancer-operator-policy --policy-document "file://USD{SCRATCH}/load-balancer-operator-policy.json") fi
cat <<EOF > "USD{SCRATCH}/trust-policy.json" { "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Condition": { "StringEquals" : { "USD{OIDC_ENDPOINT}:sub": ["system:serviceaccount:aws-load-balancer-operator:aws-load-balancer-operator-controller-manager", "system:serviceaccount:aws-load-balancer-operator:aws-load-balancer-controller-cluster"] } }, "Principal": { "Federated": "arn:aws:iam::USDAWS_ACCOUNT_ID:oidc-provider/USD{OIDC_ENDPOINT}" }, "Action": "sts:AssumeRoleWithWebIdentity" } ] } EOF
ROLE_ARN=USD(aws iam create-role --role-name "USD{CLUSTER}-alb-operator" --assume-role-policy-document "file://USD{SCRATCH}/trust-policy.json" --query Role.Arn --output text)
aws iam attach-role-policy --role-name "USD{CLUSTER}-alb-operator" --policy-arn USD{POLICY_ARN}
cat << EOF | oc apply -f - apiVersion: v1 kind: Secret metadata: name: aws-load-balancer-operator namespace: aws-load-balancer-operator stringData: credentials: | [default] role_arn = USD{ROLE_ARN} web_identity_token_file = /var/run/secrets/openshift/serviceaccount/token EOF
cat << EOF | oc apply -f - apiVersion: operators.coreos.com/v1 kind: OperatorGroup metadata: name: aws-load-balancer-operator namespace: aws-load-balancer-operator spec: upgradeStrategy: Default --- apiVersion: operators.coreos.com/v1alpha1 kind: Subscription metadata: name: aws-load-balancer-operator namespace: aws-load-balancer-operator spec: channel: stable-v1.0 installPlanApproval: Automatic name: aws-load-balancer-operator source: redhat-operators sourceNamespace: openshift-marketplace startingCSV: aws-load-balancer-operator.v1.0.0 EOF
cat << EOF | oc apply -f - apiVersion: networking.olm.openshift.io/v1 kind: AWSLoadBalancerController metadata: name: cluster spec: credentials: name: aws-load-balancer-operator enabledAddons: - AWSWAFv2 EOF
oc -n aws-load-balancer-operator get pods
NAME READY STATUS RESTARTS AGE aws-load-balancer-controller-cluster-6ddf658785-pdp5d 1/1 Running 0 99s aws-load-balancer-operator-controller-manager-577d9ffcb9-w6zqn 2/2 Running 0 2m4s
oc new-project hello-world
oc new-app -n hello-world --image=docker.io/openshift/hello-openshift
oc -n hello-world patch service hello-openshift -p '{"spec":{"type":"NodePort"}}'
cat << EOF | oc apply -f - apiVersion: networking.k8s.io/v1 kind: Ingress metadata: name: hello-openshift-alb namespace: hello-world annotations: alb.ingress.kubernetes.io/scheme: internet-facing spec: ingressClassName: alb rules: - http: paths: - path: / pathType: Exact backend: service: name: hello-openshift port: number: 8080 EOF
INGRESS=USD(oc -n hello-world get ingress hello-openshift-alb -o jsonpath='{.status.loadBalancer.ingress[0].hostname}') curl "http://USD{INGRESS}"
Hello OpenShift!
cat << EOF > USD{SCRATCH}/waf-rules.json [ { "Name": "AWS-AWSManagedRulesCommonRuleSet", "Priority": 0, "Statement": { "ManagedRuleGroupStatement": { "VendorName": "AWS", "Name": "AWSManagedRulesCommonRuleSet" } }, "OverrideAction": { "None": {} }, "VisibilityConfig": { "SampledRequestsEnabled": true, "CloudWatchMetricsEnabled": true, "MetricName": "AWS-AWSManagedRulesCommonRuleSet" } }, { "Name": "AWS-AWSManagedRulesSQLiRuleSet", "Priority": 1, "Statement": { "ManagedRuleGroupStatement": { "VendorName": "AWS", "Name": "AWSManagedRulesSQLiRuleSet" } }, "OverrideAction": { "None": {} }, "VisibilityConfig": { "SampledRequestsEnabled": true, "CloudWatchMetricsEnabled": true, "MetricName": "AWS-AWSManagedRulesSQLiRuleSet" } } ] EOF
WAF_ARN=USD(aws wafv2 create-web-acl --name USD{CLUSTER}-waf --region USD{REGION} --default-action Allow={} --scope REGIONAL --visibility-config SampledRequestsEnabled=true,CloudWatchMetricsEnabled=true,MetricName=USD{CLUSTER}-waf-metrics --rules file://USD{SCRATCH}/waf-rules.json --query 'Summary.ARN' --output text)
oc annotate -n hello-world ingress.networking.k8s.io/hello-openshift-alb alb.ingress.kubernetes.io/wafv2-acl-arn=USD{WAF_ARN}
curl "http://USD{INGRESS}"
Hello OpenShift!
curl -X POST "http://USD{INGRESS}" -F "user='<script><alert>Hello></alert></script>'"
<html> <head><title>403 Forbidden</title></head> <body> <center><h1>403 Forbidden</h1></center> </body> </html