# AWS appendix — ALB + ACM (docs only, never a shipped artifact)

Scope: operators **already on EC2**. There, an ALB is trust-neutral (the engine is on AWS
infrastructure regardless) and ACM renewal is genuinely zero-touch. This page is an appendix,
not a recommendation to move to AWS — the shipped edges are Caddy
([topologies.md](topologies.md)) and BYO proxies ([reverse-proxies.md](reverse-proxies.md)).
All ALB/NLB facts below are grounded in the [TLS/edge decision brief §3(c)](../superpowers/specs/2026-07-05-tls-edge-decision-brief.md).

## Never NLB-TLS

Do not put the engine behind an NLB TLS listener. Two disqualifiers, ratified in
[ADR-0011](../adr/0011-deployment-topology.md):

1. **Fixed 350-second idle timeout** that silently drops flows — any quiet WebSocket (hold,
   parked call, unidirectional stream) dies mid-call with no close frame.
2. **No HTTP context**: an NLB adds no `X-Forwarded-Proto/Host`, so the engine cannot
   reconstruct the public URL that `X-Twilio-Signature` is HMAC'd over
   ([Twilio's SSL-termination guidance](https://www.twilio.com/en-us/blog/developers/tutorials/building-blocks/handle-ssl-termination-twilio-node-js-helper-library))
   — genuine traffic fails signature reconstruction (enforcement itself is **planned,
   NOT yet enforced** — the webhook surface is currently unauthenticated, but the
   reconstruction helper must still receive honest headers so validation lands
   correctly when enforcement ships).

## The three mandatory ALB attribute overrides

The defaults kill telephony. All three overrides are **mandatory**, not tuning:

| Attribute | Value | Why |
|---|---|---|
| `idle_timeout.timeout_seconds` | `4000` | The default 60 s kills any quiet WS. 4000 is the ALB maximum — still finite, so the engine's app-level WS pings (`RUTSTER_WS_PING_SECS`, default 20) remain load-bearing here. |
| `client_keep_alive.seconds` | `604800` | The 3600 s default can terminate the client connection under a **>1-hour call**. Set to the 7-day maximum. |
| `routing.http.preserve_host_header.enabled` | `true` | Signature validation reconstructs the URL *as Twilio saw it* — the engine must receive the honest public `Host`, and its `RUTSTER_TRUSTED_PROXIES` must include the ALB's subnet CIDRs. |

## Copy-paste path (CLI)

Assumes: engine node(s) running per [quickstart-docker.md](quickstart-docker.md) with the
`caddy` service disabled (plaintext `:8080`), a VPC, two subnets, and a security group
allowing `:443` from anywhere and `:8080` from the ALB.

```bash
VPC=vpc-0123456789abcdef0
SUBNETS="subnet-0aaa subnet-0bbb"
SG=sg-0ccc

ALB_ARN=$(aws elbv2 create-load-balancer --name rutster-edge --type application \
  --subnets $SUBNETS --security-groups $SG \
  --query 'LoadBalancers[0].LoadBalancerArn' --output text)

# THE THREE MANDATORY OVERRIDES — do not skip:
aws elbv2 modify-load-balancer-attributes --load-balancer-arn "$ALB_ARN" --attributes \
  Key=idle_timeout.timeout_seconds,Value=4000 \
  Key=client_keep_alive.seconds,Value=604800 \
  Key=routing.http.preserve_host_header.enabled,Value=true

TG_ARN=$(aws elbv2 create-target-group --name rutster-node-1 \
  --protocol HTTP --port 8080 --vpc-id "$VPC" \
  --health-check-path /readyz \
  --query 'TargetGroups[0].TargetGroupArn' --output text)
aws elbv2 register-targets --target-group-arn "$TG_ARN" --targets Id=i-0node1

CERT_ARN=$(aws acm request-certificate --domain-name '*.pbx.example.com' \
  --validation-method DNS --query CertificateArn --output text)
# Create the DNS validation CNAME ACM reports, wait for ISSUED, then:

LISTENER_ARN=$(aws elbv2 create-listener --load-balancer-arn "$ALB_ARN" \
  --protocol HTTPS --port 443 \
  --ssl-policy ELBSecurityPolicy-TLS13-1-2-2021-06 \
  --certificates CertificateArn="$CERT_ARN" \
  --default-actions Type=forward,TargetGroupArn="$TG_ARN" \
  --query 'Listeners[0].ListenerArn' --output text)

# Fleet growth: one target group + one host-header rule per node
# (node-addressed placement — ADR-0011):
aws elbv2 create-rule --listener-arn "$LISTENER_ARN" --priority 10 \
  --conditions Field=host-header,Values=node-1.pbx.example.com \
  --actions Type=forward,TargetGroupArn="$TG_ARN"
```

Point `*.pbx.example.com` (alias record) at the ALB. First call: identical to
[quickstart-docker.md](quickstart-docker.md) — browser at your domain, Twilio webhook at
`https://pbx.example.com/v1/trunk/webhook`.

The `--ssl-policy` above serves TLS 1.2+1.3 — never pick a 1.3-only policy (Twilio's TLS 1.3
client support is undocumented; see the invariants in
[certificates.md](certificates.md)).

## WebRTC media does not ride the ALB

The ALB carries HTTPS/WSS only. WebRTC media UDP goes **direct to each node**: give every
engine node an Elastic IP, open the media UDP range in the node's security group, and set
`RUTSTER_MEDIA_ADVERTISED_IP` to that EIP.

## Caps and traps

- **Hard cap: 100 target groups per ALB.** With one TG per node (host-header routing), that
  is your fleet ceiling per ALB.
- **AWS fleet rotation can still cut a multi-hour call** — ALB infrastructure is replaced
  under maintenance without regard for your WS lifetimes. Accepted residual risk; there is no
  attribute for it.
- Health checks target `/readyz` (503 while draining or at the admission cap) — scale-in must
  be drain-then-terminate: deregister the target, wait out
  `RUTSTER_DRAIN_DEADLINE_SECS`, then terminate the instance.
