luizmachado.dev

PT EN

Session 027 — ALB: native OIDC, mTLS and WAF integration

Estimated duration: 60 minutes
Prerequisites: session-026-alb-listener-rules-weighted


Objective

By the end, you will be able to configure native OIDC authentication on the ALB (delegating auth to Cognito or any OIDC IdP without code in the backend), enable mTLS with a CA trust store, and associate a WAF Web ACL to an ALB to filter requests before they reach the application.


Context

[FACT] The ALB supports three security mechanisms at the load balancer level — each operates at a different layer of the threat model and complements the others. Native OIDC delegates to the ALB the function of authenticating human users via the OAuth2/OIDC flow, eliminating the need to implement this flow in each microservice. mTLS goes beyond conventional TLS and requires the client to present an X.509 certificate signed by a trusted CA — useful for machine-to-machine authentication at service boundaries. The WAF (Web Application Firewall) acts even before the request reaches the listener, filtering malicious traffic based on L7 rules (SQL injection, XSS, rate limiting, geo-blocking).

[CONSENSUS] The combination of all three mechanisms follows the defense-in-depth principle: the WAF blocks known threats at the perimeter, mTLS ensures that only authorized clients establish a TLS connection, and OIDC ensures that only authenticated users reach the backend. Each layer can fail or be bypassed in different ways — having them together significantly reduces the attack surface.


Key concepts

1. Native OIDC on ALB — how the authentication flow works

[FACT] The ALB supports two types of authentication action in HTTPS listener rules: authenticate-oidc (for any IdP that implements OIDC) and authenticate-cognito (shortcut for integration with Cognito User Pools). Both follow the Authorization Code Grant flow of OAuth 2.0. Only HTTPS listeners support these actions — on HTTP listeners the action is rejected.

The authentication flow has 11 steps documented by AWS:

Cliente                      ALB                         IdP (OIDC)          Backend
  |                           |                              |                   |
  |--- HTTPS request -------->|                              |                   |
  |                           |-- checa cookie AWSELB ------>|                   |
  |                           |   (não existe na 1ª vez)    |                   |
  |<-- 302 redirect ----------|                              |                   |
  |   (authorization endpoint)|                              |                   |
  |--- GET /authorize ---------------------------------------->|                |
  |                           |                              |-- user login UI  |
  |<-- auth code via redirect ----------------------------------------           |
  |--- POST /oauth2/idpresponse (auth code) -->|             |                   |
  |                           |-- POST /token ----------------->|               |
  |                           |<-- id_token + access_token -----|               |
  |                           |-- GET /userinfo (access_token) ->|              |
  |                           |<-- user claims (JSON) ----------|               |
  |<-- 302 redirect ----------|                              |                   |
  |   Set-Cookie: AWSELB=...  |                              |                   |
  |--- request original ------>|                             |                   |
  |                           |-- forward + X-AMZN-OIDC-* headers ------------->|
  |<-- response -------------------------------------------------- response -----|

[FACT] The session cookie AWSELB is sent to the client after successful authentication. Subsequent requests present this cookie and the ALB validates it locally (without a roundtrip to the IdP) — jumping directly to step 9. The cookie always carries the Secure attribute (requires HTTPS). For CORS requests, it also includes SameSite=None.

[FACT] If the total of user claims + access token exceeds 11 KB, the ALB returns HTTP 500 and increments the ELBAuthUserClaimsSizeExceeded metric. Cookies larger than 4 KB are fragmented into multiple cookies (the ALB supports up to 4 shards, totaling up to 16 KB).

Headers sent to the backend:

Header Content
x-amzn-oidc-accesstoken Access token in plain text
x-amzn-oidc-identity sub claim from the user info endpoint (plain text)
x-amzn-oidc-data User claims in JWT format (ES256, base64url encoded)

[FACT] The sub field is the canonical way to identify a user — sub is stable and unique per IdP, while fields like email can change. The JWT in x-amzn-oidc-data is signed by the ALB with ES256 (ECDSA P-256 + SHA256). The public key for verification is available at: https://public-keys.auth.elb.<region>.amazonaws.com/<key-id> (the kid is in the JWT header).

OnUnauthenticatedRequest behavior:

authenticate  → redireciona para IdP (padrão; bom para SPAs e apps web tradicionais)
allow         → passa o request sem claims (bom para apps com conteúdo público + privado)
deny          → retorna HTTP 401 (bom para APIs que fazem fetch em background — evita redirect loop)

Session timeout: default 7 days, configurable from 1 second to 7 days. Independent of the cookie TTL (always 7 days in the Max-Age attribute) — the real timeout is encrypted inside the cookie value.

Client login timeout: fixed at 15 minutes. The user must complete the login at the IdP within this window; otherwise, they receive HTTP 401 and need to reload the page.

2. OIDC configuration — required and optional parameters

[FACT] For authenticate-oidc, the following fields are required in the action:

Issuer              — URL do issuer do IdP (ex: https://accounts.google.com)
AuthorizationEndpoint — URL de autorização (ex: https://accounts.google.com/o/oauth2/v2/auth)
TokenEndpoint       — URL de obtenção de tokens (ex: https://oauth2.googleapis.com/token)
UserInfoEndpoint    — URL de user info (ex: https://openidconnect.googleapis.com/v1/userinfo)
ClientId            — OAuth 2.0 client ID (registrado no IdP)
ClientSecret        — OAuth 2.0 client secret

[FACT] The DNS of the IdP endpoints must be publicly resolvable (even if it resolves to private IPs). The ALB communicates with TokenEndpoint and UserInfoEndpoint — if the ALB is internal or uses dualstack-without-public-ipv4, a NAT Gateway is required for the ALB to reach these endpoints. The ALB only supports IPv4 in this communication.

[FACT] The callback URL that must be allowed in the IdP follows the pattern: https://<ALB-DNS>/oauth2/idpresponse or https://<CNAME>/oauth2/idpresponse.

Relevant optional parameters:

SessionCookieName         — nome do cookie (padrão: AWSELBAuthSessionCookie)
                            use nomes distintos para múltiplas regras com auth no mesmo listener
SessionTimeout            — tempo de sessão em segundos (padrão: 604800 = 7 dias)
Scope                     — scopes OAuth solicitados (padrão: "openid"; adicionar "email" para claims de email)
AuthenticationRequestExtraParams — parâmetros extras para o IdP (ex: {"prompt": "login"})

For Cognito (authenticate-cognito), the required fields are:

UserPoolArn        — ARN do User Pool
UserPoolClientId   — ID do app client (deve ter client secret + code grant habilitados)
UserPoolDomain     — domínio do User Pool (ex: minha-app.auth.us-east-1.amazoncognito.com)

3. mTLS on ALB — two modes and trust store

[FACT] Mutual TLS (mTLS) inverts conventional TLS authentication: in addition to the server presenting its certificate to the client, the client must present an X.509v3 certificate signed by a CA that the ALB recognizes as trusted. This is especially relevant in B2B scenarios, machine-to-machine, and zero-trust between services.

The ALB offers two mTLS modes:

┌─────────────────────────────────────────────────────────────────────┐
│                     mTLS PASSTHROUGH                                 │
│                                                                      │
│  Cliente → [cert chain] → ALB → [X-Amzn-Mtls-Clientcert header] → Backend │
│                                                                      │
│  O ALB NÃO verifica o certificado do cliente.                        │
│  Encaminha a cadeia completa (URL-encoded PEM) no header.            │
│  O backend é responsável por validar e autorizar.                    │
└─────────────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────────────┐
│                      mTLS VERIFY                                     │
│                                                                      │
│  Cliente → [cert] → ALB → valida contra trust store → forward       │
│                           (rejeita se cert inválido/não confiável)   │
│                                                                      │
│  O ALB verifica a cadeia X.509 do cliente contra a trust store.      │
│  Envia metadados do cert ao backend via headers X-Amzn-Mtls-*.      │
└─────────────────────────────────────────────────────────────────────┘

[FACT] For Verify mode, it is necessary to create a trust store resource — a collection of root and intermediate certificates from trusted CAs. The CA bundle is stored in S3 in PEM format and associated with the trust store. To replace the bundle, use the ModifyTrustStore API — it is not possible to add individual certificates, only replace the entire bundle.

PEM bundle file requirements:
- PEM format (Privacy Enhanced Mail)
- Each certificate delimited by -----BEGIN CERTIFICATE----- and -----END CERTIFICATE-----
- Comments preceded by # (no dashes - in comments)
- No blank lines between certificates
- Supported certificates: X.509v3 with RSA 2K–8K keys or ECDSA (secp256r1/384r1/521r1)

Headers sent to the backend in Verify mode:

X-Amzn-Mtls-Clientcert-Serial-Number  → número serial hexadecimal do leaf cert
X-Amzn-Mtls-Clientcert-Issuer         → DN do issuer (RFC 2253)
X-Amzn-Mtls-Clientcert-Subject        → DN do subject (RFC 2253)
X-Amzn-Mtls-Clientcert-Validity       → NotBefore=...;NotAfter=... (ISO 8601)
X-Amzn-Mtls-Clientcert-Leaf           → leaf cert em PEM URL-encoded

Header in Passthrough mode:

X-Amzn-Mtls-Clientcert  → cadeia completa em PEM URL-encoded (leaf → root)

[FACT] Session resumption (TLS session tickets) is not supported in mTLS passthrough or verify modes. Each new TLS connection performs the full handshake.

Advertise CA Subject Names: when enabled, the ALB includes the list of CA DNs from the trust store in the CertificateRequest message of the TLS handshake. The client can use this list to select the correct certificate — reducing connection errors when the client has multiple certificates and doesn't know which one to use.

4. AWS WAF — architecture and integration with ALB

[FACT] AWS WAF v2 operates as an L7 "inspector" proxy that evaluates the HTTP/HTTPS request before it reaches the ALB target. The configuration unit is the Web ACL (Access Control List), which contains rules ordered by priority. The WAF evaluates rules in ascending priority order and applies the action of the first rule that matches.

Internet
    │
    ▼
┌─────────────┐
│  AWS WAF    │  ← Web ACL (REGIONAL scope)
│  Web ACL    │     Regra 1 (prio 0): AWSManagedRulesCommonRuleSet
│             │     Regra 2 (prio 1): AWSManagedRulesSQLiRuleSet
│             │     Regra 3 (prio 2): custom rate-limit rule
│             │     Default action: Allow
└──────┬──────┘
       │  (só passa o que não foi bloqueado)
       ▼
┌─────────────┐
│     ALB     │  ← listener rules, OIDC, mTLS
└──────┬──────┘
       │
       ▼
   Targets (ECS, EC2, Lambda...)

[FACT] To use WAF with ALB, the Web ACL must be created with REGIONAL scope (not CLOUDFRONT). Each AWS resource can only be associated with one Web ACL at a time. The WAF and the ALB must be in the same region.

Possible actions in each WAF rule:

Allow   → passa o request; termina avaliação (nenhuma outra regra é executada)
Block   → retorna HTTP 403 (padrão) ou resposta customizada; termina avaliação
Count   → incrementa contador; NÃO termina avaliação (continua para próximas regras)
CAPTCHA → apresenta desafio CAPTCHA ao usuário antes de prosseguir
Challenge → verifica se é browser real (JavaScript challenge silencioso)

[FACT] Count is especially useful for testing new rules in production without blocking traffic — you monitor the CountedRequests metrics in CloudWatch before switching to Block.

AWS Managed Rule Groups relevant for ALB:

Group What it covers
AWSManagedRulesCommonRuleSet OWASP Top 10 baseline (XSS, LFI, RFI, SSRF)
AWSManagedRulesSQLiRuleSet SQL injection in query strings, body, headers
AWSManagedRulesKnownBadInputsRuleSet Malformed inputs, Log4JRCE, Spring4Shell
AWSManagedRulesAmazonIpReputationList IPs with bad reputation (bots, scrapers, TOR)
AWSManagedRulesAnonymousIpList Anonymous proxies, VPNs, TOR exit nodes
AWSManagedRulesBotControlRuleSet Bot detection (pay-per-use; charges extra WCU)

[FACT] Each Web ACL has a WCU (WAF Capacity Units) quota. The default limit is 1,500 WCU per Web ACL. AWSManagedRulesCommonRuleSet consumes 700 WCU, AWSManagedRulesSQLiRuleSet consumes 200 WCU. When combining multiple groups, it is necessary to plan the WCU budget.

[FACT] The WAF → ALB association uses the ALB ARN. Once associated, the WAF inspects all requests that arrive at the ALB — it is not possible to select specific listeners to inspect.


Practical example

Scenario: E-commerce API with three security requirements:
1. Web users authenticate via Cognito (OIDC) before accessing /dashboard/*
2. B2B integration (partners) authenticates via X.509 certificate (mTLS verify) on /api/b2b/*
3. WAF protects the entire ALB against OWASP Top 10 and SQL injection

CDK Python — complete stack

from aws_cdk import (
    Stack, Duration,
    aws_elasticloadbalancingv2 as elbv2,
    aws_elasticloadbalancingv2_actions as elbv2_actions,
    aws_cognito as cognito,
    aws_wafv2 as wafv2,
    aws_s3 as s3,
)
from constructs import Construct

class SecureAlbStack(Stack):
    def __init__(self, scope: Construct, id: str, **kwargs):
        super().__init__(scope, id, **kwargs)

        # ── 1. ALB (assumindo que já existe VPC e targets) ───────────────────
        alb = elbv2.ApplicationLoadBalancer(
            self, "ALB",
            vpc=vpc,
            internet_facing=True,
        )

        # ── 2. Trust store para mTLS (bundle de CAs em S3) ──────────────────
        ca_bucket = s3.Bucket.from_bucket_name(self, "CaBucket", "my-ca-bundles")
        trust_store = elbv2.TrustStore(
            self, "B2BTrustStore",
            bucket=ca_bucket,
            key="ca-bundle.pem",
        )

        # ── 3. Listener HTTPS com mTLS verify ────────────────────────────────
        listener = alb.add_listener(
            "HttpsListener",
            port=443,
            certificates=[certificate],
            mutual_authentication=elbv2.MutualAuthentication(
                mode=elbv2.MutualAuthenticationMode.VERIFY,
                trust_store=trust_store,
                ignore_client_certificate_expiry=False,
            ),
            default_action=elbv2.ListenerAction.fixed_response(
                status_code=403,
                content_type=elbv2.ContentType.TEXT_PLAIN,
                message_body="Forbidden",
            ),
        )

        # ── 4. Cognito User Pool ──────────────────────────────────────────────
        user_pool = cognito.UserPool(self, "UserPool",
            self_sign_up_enabled=True,
        )
        user_pool_client = user_pool.add_client("AlbClient",
            generate_secret=True,
            o_auth=cognito.OAuthSettings(
                flows=cognito.OAuthFlows(authorization_code_grant=True),
                scopes=[cognito.OAuthScope.OPENID, cognito.OAuthScope.EMAIL],
                callback_urls=[f"https://{alb.load_balancer_dns_name}/oauth2/idpresponse"],
            ),
        )
        user_pool_domain = user_pool.add_domain("Domain",
            cognito_domain=cognito.CognitoDomainOptions(domain_prefix="minha-app"),
        )

        # ── 5. Regra OIDC (prioridade 10): /dashboard/* → autentica com Cognito
        listener.add_action(
            "DashboardAuth",
            priority=10,
            conditions=[elbv2.ListenerCondition.path_patterns(["/dashboard/*"])],
            action=elbv2_actions.AuthenticateCognitoAction(
                user_pool=user_pool,
                user_pool_client=user_pool_client,
                user_pool_domain=user_pool_domain,
                session_timeout=Duration.hours(8),
                on_unauthenticated_request=elbv2.UnauthenticatedAction.AUTHENTICATE,
                next=elbv2.ListenerAction.forward([tg_web]),
            ),
        )

        # ── 6. Regra B2B (prioridade 20): /api/b2b/* → valida header mTLS e faz forward
        # O mTLS verify já acontece no handshake TLS — aqui apenas roteamos o tráfego
        # A regra pode ainda verificar o subject DN via http-header condition
        listener.add_action(
            "B2BRoute",
            priority=20,
            conditions=[elbv2.ListenerCondition.path_patterns(["/api/b2b/*"])],
            action=elbv2.ListenerAction.forward([tg_b2b]),
        )

        # ── 7. Default: tráfego restante → TG público (sem auth)
        # (já definido no default_action do listener; poderíamos trocar pelo tg_public)

        # ── 8. WAF Web ACL (REGIONAL) ─────────────────────────────────────────
        web_acl = wafv2.CfnWebACL(
            self, "WebACL",
            scope="REGIONAL",
            default_action=wafv2.CfnWebACL.DefaultActionProperty(allow={}),
            visibility_config=wafv2.CfnWebACL.VisibilityConfigProperty(
                cloud_watch_metrics_enabled=True,
                metric_name="WebACL",
                sampled_requests_enabled=True,
            ),
            rules=[
                # Regra 1: OWASP baseline (700 WCU)
                wafv2.CfnWebACL.RuleProperty(
                    name="AWSManagedCommon",
                    priority=0,
                    override_action=wafv2.CfnWebACL.OverrideActionProperty(none={}),
                    statement=wafv2.CfnWebACL.StatementProperty(
                        managed_rule_group_statement=wafv2.CfnWebACL.ManagedRuleGroupStatementProperty(
                            vendor_name="AWS",
                            name="AWSManagedRulesCommonRuleSet",
                        ),
                    ),
                    visibility_config=wafv2.CfnWebACL.VisibilityConfigProperty(
                        cloud_watch_metrics_enabled=True,
                        metric_name="AWSManagedCommon",
                        sampled_requests_enabled=True,
                    ),
                ),
                # Regra 2: SQL injection (200 WCU)
                wafv2.CfnWebACL.RuleProperty(
                    name="AWSManagedSQLi",
                    priority=1,
                    override_action=wafv2.CfnWebACL.OverrideActionProperty(none={}),
                    statement=wafv2.CfnWebACL.StatementProperty(
                        managed_rule_group_statement=wafv2.CfnWebACL.ManagedRuleGroupStatementProperty(
                            vendor_name="AWS",
                            name="AWSManagedRulesSQLiRuleSet",
                        ),
                    ),
                    visibility_config=wafv2.CfnWebACL.VisibilityConfigProperty(
                        cloud_watch_metrics_enabled=True,
                        metric_name="AWSManagedSQLi",
                        sampled_requests_enabled=True,
                    ),
                ),
                # Regra 3: Rate limiting custom (10 WCU)
                wafv2.CfnWebACL.RuleProperty(
                    name="RateLimit",
                    priority=2,
                    action=wafv2.CfnWebACL.RuleActionProperty(
                        block=wafv2.CfnWebACL.BlockActionProperty(
                            custom_response=wafv2.CfnWebACL.CustomResponseProperty(
                                response_code=429,
                            ),
                        ),
                    ),
                    statement=wafv2.CfnWebACL.StatementProperty(
                        rate_based_statement=wafv2.CfnWebACL.RateBasedStatementProperty(
                            limit=2000,           # requests per 5-minute window
                            aggregate_key_type="IP",
                        ),
                    ),
                    visibility_config=wafv2.CfnWebACL.VisibilityConfigProperty(
                        cloud_watch_metrics_enabled=True,
                        metric_name="RateLimit",
                        sampled_requests_enabled=True,
                    ),
                ),
            ],
        )

        # ── 9. Associar Web ACL ao ALB ─────────────────────────────────────────
        wafv2.CfnWebACLAssociation(
            self, "WebACLAssociation",
            resource_arn=alb.load_balancer_arn,
            web_acl_arn=web_acl.attr_arn,
        )

CLI — essential commands

Create trust store for mTLS:

# Fazer upload do bundle de CAs para S3
aws s3 cp ca-bundle.pem s3://my-ca-bundles/ca-bundle.pem

# Criar trust store
aws elbv2 create-trust-store \
  --name b2b-trust-store \
  --ca-certificates-bundle-s3-bucket my-ca-bundles \
  --ca-certificates-bundle-s3-key ca-bundle.pem

# Saída: retorna TrustStoreArn

Modify existing listener to add mTLS verify:

aws elbv2 modify-listener \
  --listener-arn arn:aws:elasticloadbalancing:... \
  --mutual-authentication \
    Mode=verify,TrustStoreArn=arn:aws:elasticloadbalancing:...:truststore/b2b-trust-store/...,\
    IgnoreClientCertificateExpiry=false,AdvertiseTrustStoreCaNames=enabled

Create OIDC rule via CLI:

# Arquivo actions.json
cat > actions.json << 'EOF'
[
  {
    "Type": "authenticate-cognito",
    "AuthenticateCognitoConfig": {
      "UserPoolArn": "arn:aws:cognito-idp:us-east-1:123456789:userpool/us-east-1_ABC",
      "UserPoolClientId": "abcdefg123456",
      "UserPoolDomain": "minha-app",
      "SessionCookieName": "AWSELBAuthSessionCookie-dashboard",
      "SessionTimeout": 28800,
      "Scope": "openid email",
      "OnUnauthenticatedRequest": "authenticate"
    },
    "Order": 1
  },
  {
    "Type": "forward",
    "TargetGroupArn": "arn:aws:elasticloadbalancing:...:targetgroup/web-tg/...",
    "Order": 2
  }
]
EOF

aws elbv2 create-rule \
  --listener-arn arn:aws:elasticloadbalancing:... \
  --priority 10 \
  --conditions '[{"Field":"path-pattern","PathPatternConfig":{"Values":["/dashboard/*"]}}]' \
  --actions file://actions.json

Create WAF Web ACL and associate with ALB:

# Criar Web ACL (REGIONAL)
aws wafv2 create-web-acl \
  --name "ecommerce-waf" \
  --scope REGIONAL \
  --region us-east-1 \
  --default-action Allow={} \
  --rules file://waf-rules.json \
  --visibility-config \
    SampledRequestsEnabled=true,CloudWatchMetricsEnabled=true,MetricName=ecommerceWAF

# Associar ao ALB
aws wafv2 associate-web-acl \
  --web-acl-arn arn:aws:wafv2:us-east-1:123456789:regional/webacl/ecommerce-waf/... \
  --resource-arn arn:aws:elasticloadbalancing:us-east-1:123456789:loadbalancer/app/my-alb/...

# Verificar associação
aws wafv2 get-web-acl-for-resource \
  --resource-arn arn:aws:elasticloadbalancing:us-east-1:123456789:loadbalancer/app/my-alb/...

Test if a managed rule group is blocking legitimate traffic (temporary Count mode):

# Colocar regra em Count mode para testes
aws wafv2 update-web-acl \
  --name "ecommerce-waf" \
  --scope REGIONAL \
  --region us-east-1 \
  --id <web-acl-id> \
  --lock-token <lock-token> \
  --default-action Allow={} \
  --rules '[{
    "Name": "AWSManagedCommon",
    "Priority": 0,
    "OverrideAction": {"Count": {}},   ← Count ao invés de None
    "Statement": {"ManagedRuleGroupStatement": {"VendorName": "AWS", "Name": "AWSManagedRulesCommonRuleSet"}},
    "VisibilityConfig": {"SampledRequestsEnabled": true, "CloudWatchMetricsEnabled": true, "MetricName": "AWSManagedCommon"}
  }]' \
  --visibility-config SampledRequestsEnabled=true,CloudWatchMetricsEnabled=true,MetricName=ecommerceWAF

Common pitfalls

Pitfall 1: OIDC only works on HTTPS listeners — and the ALB needs outbound access to the IdP

[FACT] The authenticate-oidc and authenticate-cognito actions are rejected if configured on an HTTP listener (port 80). This is a frequent configuration error in development environments where the ALB does not have a TLS certificate. Additionally, to complete the OIDC flow, the ALB itself needs to make outbound calls to TokenEndpoint and UserInfoEndpoint. If the ALB is internal (scheme internal) or is in a private subnet without a NAT Gateway, these calls will fail silently and the user will receive HTTP 500. The diagnosis is to check the ALB access logs — authenticate-oidc actions with result AuthFailure indicate connectivity problems from the ALB to the IdP.

Pitfall 2: mTLS verify rejects the TLS connection — the backend never receives the request

In mTLS Verify mode, the rejection of an invalid client certificate happens at the TLS handshake — before any HTTP header is sent. This means the backend does not see these requests, and there is no way to distinguish via listener rules between "client without certificate" and "client with invalid certificate". The ALB simply closes the TLS connection with a handshake_failure. The ALB connection logs (not the access logs) record these failures — enable connection-log-enabled on the ALB to diagnose them. In Passthrough mode, by contrast, the TLS connection is always established and the backend receives the headers with the certificate chain to validate on its own.

Pitfall 3: WAF WCU budget and override action vs rule action

There is a subtle distinction between OverrideAction and Action in WAF rules that causes confusion. Rules that reference managed rule groups or external rule groups use OverrideAction (with values None = use the action defined inside the group, or Count = override all group actions to Count). Custom rules (rate-based, byte-match, geo-match) use Action (with values Allow, Block, Count, CAPTCHA, Challenge). Swapping the two causes API validation errors. Additionally, when exceeding the 1,500 WCU budget per Web ACL, the deploy fails with WAFSubscriptionNotFoundException or a quota error — always calculate the total WCUs of the groups before deploying.


Reflection exercise

You are building a multi-tenant SaaS platform. The ALB already has OIDC with Cognito configured to authenticate users. An enterprise partner requests that your application support machine-to-machine authentication for batch integrations — that is, an automated system from the partner needs to call your API /api/partner/* without human intervention (no browser, no redirect to login).

The problem: OIDC Authorization Code Grant requires human interaction with the browser (redirects). mTLS Verify could solve the B2B scenario — but the partner questions whether it is possible to mix OIDC and mTLS on the same ALB, and how to ensure that mTLS clients do not also need to go through the OIDC flow.

Describe the architecture you would design: how to configure the listener so that (a) requests to /dashboard/* go through OIDC Cognito, (b) requests to /api/partner/* require mTLS certificate without OIDC, and (c) the WAF protects both routes. Consider the priority order of the rules, the interaction between mTLS (which happens at the TLS handshake, before listener rules) and OIDC (which is a listener action), and how you would validate in the backend that the mTLS certificate belongs to the correct partner using the X-Amzn-Mtls-* headers.


Resources for further study

1. Authenticate users using an Application Load Balancer
URL: https://docs.aws.amazon.com/elasticloadbalancing/latest/application/listener-authenticate-users.html
What you will find: step-by-step OIDC authentication flow (with diagram), complete parameters for AuthenticateOidcActionConfig and AuthenticateCognitoActionConfig, session cookie behavior, logout, and JWT signature verification of x-amzn-oidc-data headers.
Why it is the right source: official AWS primary documentation with CLI and JSON examples.

2. Mutual authentication with TLS in Application Load Balancer
URL: https://docs.aws.amazon.com/elasticloadbalancing/latest/application/mutual-authentication.html
What you will find: comparison between passthrough and verify modes, PEM bundle requirements, complete table of X-Amzn-Mtls-* headers, Advertise CA Subject Names, and reference for connection logs.
Why it is the right source: primary documentation with real header examples and exact specifications of accepted formats.

3. Configuring mutual TLS on an Application Load Balancer
URL: https://docs.aws.amazon.com/elasticloadbalancing/latest/application/configuring-mtls-with-elb.html
What you will find: step-by-step trust store configuration, bundle upload to S3, association to listener via console and CLI.
Why it is the right source: procedural guide complementary to the conceptual one in the previous link.

4. Associating or disassociating a web ACL with an AWS resource
URL: https://docs.aws.amazon.com/waf/latest/developerguide/web-acl-associating-aws-resource.html
What you will find: procedure for associating a Web ACL to an ALB, restriction of one ACL per resource, REGIONAL scope requirement, and behavior when the ACL is disassociated.
Why it is the right source: primary documentation of the WAF + ALB association functionality.

5. AWS Managed Rules for AWS WAF
URL: https://docs.aws.amazon.com/waf/latest/developerguide/aws-managed-rule-groups.html
What you will find: complete catalog of managed rule groups with WCU for each one, description of covered threats, and changelog of updates (important for understanding when new rules may cause false positives in production).
Why it is the right source: essential reference for planning the WCU budget and choosing the right groups for your application's threat profile.