Public hosting
Last updated: Apr 28, 2026
Rationale
Every public HTTPS endpoint at *.fluidattacks.com
is served behind a four-hop chain:
Cloudflare at the edge,
Amazon CloudFront
as the origin-side CDN,
and either an Amazon S3 bucket
for static content
or an AWS Lambda Function URL
for dynamic content as the origin.
Every hop enforces a specific security property,
and the origin is locked down cryptographically
via Origin Access Control (OAC)
— CloudFront signs each origin request with
AWS Signature Version 4,
and the origin rejects anything that isn't a valid CloudFront-signed request.
The request flow is shown below.
The infrastructure is managed as code via reusable Terraform modules, consumed as a single configuration shape per service — documentation sites, static asset hosts, HTTP APIs, and server-side-rendered applications all sit behind this chain.
Request flow
The main reasons why we chose it over other alternatives are:
- It enforces origin protection cryptographically at the AWS platform layer via OAC and SigV4, instead of via home-grown mechanisms we would have to operate and rotate. Unsigned requests to the Lambda URL or S3 bucket return 403 at the AWS service boundary.
- It is the direction AWS itself is betting. AWS shipped OAC for Lambda Function URL origins in April 2024 specifically to solve "only CloudFront reaches the origin", and did not ship the equivalent for API Gateway HTTP API v2 — a clear signal of where modern HTTPS services should live.
- It collapses to one mental model for developers. Every new public service writes a single Terraform module block and picks whichever origin shape applies (bucket, Lambda URL, or both). No per-service decision about which edge pattern to use.
- It leaves no home-grown authentication surface to maintain. No shared secrets, no authorizer Lambdas, no rotation ceremonies.
- Edge protections (Cloudflare WAF, bot management, rate limiting)
are layered in front of CloudFront automatically,
because the same
cdnmodule points the Cloudflare DNS record at the CloudFront distribution with proxying enabled. - Traceability is handled via
CloudWatch Logs Delivery V2
to a central S3 logging bucket,
configured by the
cdnmodule itself. - CloudFront automatically terminates TLS with modern ciphers (TLS 1.3 enforced at the viewer), and ACM certificates are auto-issued and DNS-validated via Cloudflare.
- It supports the full verb set for Lambda-backed services
(POST, PUT, PATCH, DELETE)
and narrows to read-only verbs
for pure-static distributions.
Body-bearing requests work transparently for any client:
the
cdnmodule deploys a Lambda@Edge function on every Lambda-backed distribution that hashes the request body and setsx-amz-content-sha256before CloudFront's OAC SigV4 to the origin, closing the one piece OAC for Lambda Function URLs does not handle on its own.
The architecture has one documented exception.
The fixes
service exposes a WebSocket API.
Lambda URLs are HTTP-only,
and API Gateway WebSocket API v2 supports neither OAC nor mutual TLS,
so it cannot join the chain as-is.
Fixes stays on its current posture
(proxied Cloudflare + API Gateway with $connect
AWS IAM authorization as the cryptographic gate)
as a documented legacy path
until its WebSocket usage is either rearchitected to HTTP streaming
or AWS adds the missing origin-restriction primitive.
Alternatives
Below are the alternatives we evaluated before choosing this architecture, ordered from most to least interesting based on our specific needs. Each is a replacement for the legacy pattern (Cloudflare non-proxied + direct-to-AWS API Gateway) rather than a different product category.
Shared-secret authorizer on API Gateway
A CloudFront custom header injects a shared secret on every origin request; an API Gateway Lambda authorizer validates it on every incoming request.
- It covers every consumer uniformly,
including the WebSocket case (fixes)
via a
$connectauthorizer. - It operates entirely at the application layer, which means we would own the secret lifecycle across three AWS surfaces (Terraform state, CloudFront distribution config, Lambda env vars).
- It requires a per-request authorizer Lambda invocation in the hot path, with caching TTL configured at the authorizer level.
- It would force us to design a rotation procedure across CloudFront propagation and Lambda updates, with a dual-accept window to avoid downtime during secret rotation.
- It trades AWS-platform-enforced origin control for self-operated origin control — a step backwards on the "bet with the platform" axis.
- It has no documented AWS SLA on the shared-secret mechanism; reliability is inherited from the services (CloudFront, Lambda).
- It costs one extra Lambda invocation per request at the authorizer layer, amortizable via caching but non-zero.
Shared-secret authorizer on API Gateway was last reviewed on Apr 22, 2026.
Origin mutual TLS (mTLS) on API Gateway
Cloudflare presents a client certificate on every origin connection (via Authenticated Origin Pulls); API Gateway's custom domain validates the cert against a truststore in S3.
- It enforces cryptographic origin authentication at the TLS handshake, which is architecturally symmetric with OAC's SigV4 approach.
- It is only supported for API Gateway HTTP API v2 and REST API v1. API Gateway WebSocket API v2 does not support mTLS, so fixes would remain uncovered.
- It requires ongoing truststore management:
PEM upload, Cloudflare-side cert rotation coordination,
truststore_versionbumps on every rotation. - It introduces a new operational dependency on Cloudflare Authenticated Origin Pulls being correctly configured per-zone or per-hostname.
- It adds no additional monthly AWS cost, but Cloudflare plan-level requirements apply for zone-level origin pulls.
- It is compatible with CloudFront via origin mTLS (GA January 2026), but that still leaves the WebSocket gap unresolved.
Origin mTLS on API Gateway was last reviewed on Apr 22, 2026.
API Gateway REST API v1 with resource policies
Downgrade each API consumer from the newer HTTP API v2
to REST API v1,
which supports resource policies and WAF attachment,
and restrict origin access to CloudFront IP ranges via aws:SourceIp.
- It has the origin-restriction features that HTTP API v2 lacks (resource policies, WAF, per-stage configuration).
- Its request pricing is roughly 3.5× higher than HTTP API v2 ($3.50 vs $1.00 per million requests).
- It has higher cold-start latency than HTTP API v2 and is generally considered a more-featured-but-older service that AWS is not actively modernizing.
- It does not cover fixes, because REST API is HTTP-only — WebSocket APIs are a separate v2 service which itself lacks resource policies and WAF.
- It keeps our compute model on Lambda and requires no product-level changes.
- It would require a migration from v2 to v1 resource types in Terraform (different resource schemas, different integration payload formats).
API Gateway REST API v1 with resource policies was last reviewed on Apr 22, 2026.
Application Load Balancer with containerized compute
Place each consumer behind an Application Load Balancer (ALB) whose security group restricts ingress to a customer-managed prefix list of Cloudflare IP ranges, with compute running on containers instead of Lambda.
- It enforces origin protection at the network layer via security group rules, which is cryptographic-equivalent (packets from disallowed IPs are dropped at the ENI).
- It natively supports WebSocket to EC2 or ECS/Fargate targets, including the fixes case.
- ALB to Lambda target does not support WebSocket; using ALB for the WebSocket case requires containers, not serverless.
- It introduces container-platform operational surface (cluster management, task definitions, service discovery, container images, auto-scaling, blue-green deploys) across every migrated consumer.
- It changes the compute billing model from per-invocation (Lambda) to always-on (minimum of one task per service, ~$20–40 per month per task on Fargate).
- It requires a full serverless-to-containers migration per consumer — a product-level change, not a pattern swap.
- It cannot reuse our existing Lambda module ecosystem; a new containerized deployment workflow would have to be built out per service.
ALB with containerized compute was last reviewed on Apr 22, 2026.
Usage
This architecture serves
docs.fluidattacks.com,
design.fluidattacks.com,
public.fluidattacks.com,
db.fluidattacks.com,
infers.fluidattacks.com,
tracks.fluidattacks.com,
and views.fluidattacks.com today.
We use the cdn Terraform module
as the single entry point for exposing any service at *.fluidattacks.com.
Every consumer writes one module "X_cdn" block
and passes whichever origin shape applies:
bucketalone — a pure-static site (e.g.,design.fluidattacks.com,public.fluidattacks.com)lambdaalone — a Lambdalith (e.g.,infers.fluidattacks.com,tracks.fluidattacks.com)bucketandlambdatogether withlambda.static_paths— a server-side-rendered site with static assets (e.g.,docs.fluidattacks.com,db.fluidattacks.com)
Caller identity for IAM-gated routes
CloudFront OAC re-signs every origin request with its own SigV4 identity,
which means a viewer's Authorization header does not survive the edge.
Services that need to know who is calling them
(e.g., tracks
gating audit-write endpoints to specific internal roles)
use a pre-signed STS GetCallerIdentity
URL in an X-Fluid-Identity header.
The caller signs the URL with their own AWS credentials;
the back end fetches it,
parses the returned ARN,
and checks it against a configured allowlist.
STS is the validator,
AWS IAM is the trust anchor,
no shared secrets are introduced.
This is the same pattern
Vault's AWS auth method
and Kubernetes'
aws-iam-authenticator
use to translate AWS-IAM identity across an opaque transport.
The cdn module owns
the CloudFront distribution,
both Origin Access Controls,
the Lambda Function URL
(with AUTH_TYPE=AWS_IAM and a scoped CloudFront invoke permission),
the viewer-request CloudFront Function
that rejects non-Cloudflare source IPs,
the origin-request Lambda@Edge function
that injects the body hash on body-bearing requests,
the ACM certificate
and its Cloudflare-DNS-validated issuance,
the Cloudflare DNS record pointing at the distribution,
and the CloudWatch Logs Delivery V2
wiring that sends access logs
to the central common.logging S3 bucket.
The s3 module owns the private bucket
(BucketOwnerEnforced, all public-access blocks on,
AES256 at rest, TLS-deny baseline policy)
and the merged bucket policy
(OAC allow from cdn + any extra statements the consumer passes).
The lambda module owns the function itself;
public exposure is exclusively cdn's responsibility
— the module does not create a Function URL on its own.
Fixes (WebSocket) remains on
API Gateway
WebSocket API with $connect IAM authorization,
proxied through Cloudflare.
This is a documented legacy path
tracked for rearchitecture
when the product team revisits the WebSocket usage pattern.