Public hosting

Last updated: Nov 4, 2026


Rationale

Each public HTTPS endpoint at *.fluidattacks.com is behind a chain of four hops: Cloudflare at the edge, Amazon CloudFront as the origin-side CDN, and an Amazon S3 bucket for static content or an AWS Lambda Function URL for dynamic content as the origin. Each hop applies one security property. Origin Access Control (OAC) locks the origin cryptographically. CloudFront signs each origin request with AWS Signature Version 4, and the origin rejects each request that does not have a correct CloudFront signature. The diagram below shows the request flow.

Fluid Attacks manages the infrastructure as code with reusable Terraform modules. Each service uses the same configuration shape. Documentation sites, static asset hosts, HTTP APIs, and server-side-rendered applications are all behind this chain.

Request flow

These are the primary reasons why Fluid Attacks selected it and not other alternatives:

  • It applies origin protection cryptographically at the AWS platform layer with two independent mechanisms. Viewer mTLS (Authenticated Origin Pulls) controls who can connect to the CloudFront distribution. OAC with SigV4 controls who can connect to the Lambda URL or S3 origin. Both replace the in-house IP-allowlist mechanisms that Fluid Attacks would otherwise have to operate and rotate. Unsigned origin requests get a 403 at the AWS service boundary. The distribution rejects TLS handshakes without AOP before they get to the HTTP layer.
  • AWS also goes in this direction. AWS released OAC for Lambda Function URL origins in April 2024 to make sure that "only CloudFront gets to the origin". AWS did not release the same function for API Gateway HTTP API v2. This is a clear signal about the location of modern HTTPS services.
  • Developers use one mental model. Each new public service writes one Terraform module block and selects the origin shape that applies (bucket, Lambda URL, or both). It is not necessary to select an edge pattern for each service.
  • This architecture does not add an in-house authentication surface at the origin. There are no shared secrets, no authorizer Lambdas, and no rotation procedures.
  • The edge protections (Cloudflare WAF, bot management, rate limiting) are automatically in front of CloudFront. This is because the same cdn module points the Cloudflare DNS record to the CloudFront distribution, with the proxy on.
  • For traceability, the cdn module configures the delivery of logs with CloudWatch Logs Delivery V2 to a central S3 logging bucket.
  • CloudFront automatically terminates TLS with modern ciphers (the viewer must use TLS 1.3). ACM issues the certificates automatically, and ACM validates them with DNS records in Cloudflare.
  • It supports all the verbs for services that use Lambda (POST, PUT, PATCH, DELETE). For distributions that are only static, it accepts only read-only verbs. Requests with a body operate correctly for all clients. The cdn module deploys a Lambda@Edge function on each distribution that uses Lambda. This function calculates the hash of the request body and sets x-amz-content-sha256 before CloudFront signs the origin request with OAC SigV4. Thus, it closes the one gap that OAC for Lambda Function URLs does not close without help.
  • It supports streaming responses. The streaming = true mode of the cdn module stops CloudFront compression. It also configures the distribution to send text/event-stream bodies without a buffer. Thus, the fixes service can stream long AI responses from Bedrock, through Lambda (RESPONSE_STREAM invoke mode with the AWS Lambda Web Adapter), to the viewer with Server-Sent Events. Without this mode, fixes would have to be a WebSocket-on-API-Gateway exception. With it, fixes uses the same cdn + Lambda URL + OAC chain as all the other services.

Alternatives

These are the alternatives that Fluid Attacks examined before it selected this architecture. The list starts with the alternative that is most interesting for the requirements of Fluid Attacks. Each alternative replaces the legacy pattern (Cloudflare without a proxy + API Gateway with no CloudFront in front).

API Gateway-based alternatives (retired)

Before Fluid Attacks selected this architecture, it examined three variants that keep API Gateway and add a layer on top:

  • Shared-secret authorizer: A CloudFront custom header adds a shared secret to each origin request. An API Gateway Lambda authorizer validates it. Fluid Attacks must manage the lifecycle of the secret on three AWS surfaces, and each request costs one more Lambda invocation.
  • Origin mTLS: Cloudflare gives a client certificate through Authenticated Origin Pulls. The custom domain of API Gateway validates it against a truststore in S3. Its architecture is symmetric with OAC SigV4, but only HTTP API v2 and REST API v1 support it (not WebSocket v2). Also, Fluid Attacks must coordinate the rotation of the trust store continuously.
  • REST API v1 with resource policies: Each consumer changes from HTTP API v2 to REST API v1, which supports resource policies and WAF attachment. Then, aws:SourceIp restricts the origin access to the CloudFront IP ranges. The cost of each request is approximately 3.5 times the cost of HTTP API v2, and AWS does not actively modernize this product family.

The three alternatives give up the origin guarantee that the AWS platform applies, which OAC gives without cost. In its place, Fluid Attacks would have to operate the origin control or continuously rotate items on AWS surfaces. None of them supported the WebSocket condition (fixes). REST v1 supports only HTTP, mTLS is not available on WebSocket v2, and a $connect authorizer is a gate that a Lambda function makes (not a platform gate). After Fluid Attacks moved fixes to Server-Sent Events on a Lambda Function URL, it retired all the API Gateway tier. The full historical evaluation is in the Amazon CloudFront ADR's "Amazon API Gateway" alternative.

Application Load Balancer with containerized compute

With this alternative, each consumer is behind an Application Load Balancer (ALB). The security group of the ALB lets in only the traffic from a customer-managed prefix list of Cloudflare IP ranges. The compute runs on containers, not on Lambda.

  • It applies origin protection at the network layer with security group rules. This protection is equivalent to cryptographic protection, because the ENI drops the packets from IPs that are not in the list.
  • It supports WebSocket to EC2 or ECS/Fargate targets by default, and this includes the fixes condition.
  • ALB to a Lambda target does not support WebSocket. Thus, to use ALB for WebSocket, you must use containers, not serverless.
  • It adds the operational surface of a container platform (cluster management, task definitions, service discovery, container images, auto-scaling, blue-green deployments) to each consumer that moves.
  • It changes the compute billing model from a cost for each invocation (Lambda) to a service that always runs (a minimum of one task for each service, approximately $20–40 each month for each task on Fargate).
  • Each consumer must move fully from serverless to containers. It is a change of the platform, not a change of pattern.
  • It cannot use the current Lambda module ecosystem of Fluid Attacks again. Fluid Attacks would have to build a new containerized deployment workflow for each service.

Usage

At this time, this architecture serves docs.fluidattacks.com, design.fluidattacks.com, public.fluidattacks.com, db.fluidattacks.com, infers.fluidattacks.com, tracks.fluidattacks.com, views.fluidattacks.com, gitlab-bot.fluidattacks.com, reviews.fluidattacks.com, watches.fluidattacks.com, and the fixes SSE endpoints (fixes-code.fluidattacks.com, fixes-sca-npm.fluidattacks.com, fixes-sca-pypi.fluidattacks.com, fixes-sca-maven.fluidattacks.com, fixes-sca-packagist.fluidattacks.com).

Fluid Attacks uses the cdn Terraform module as the only entry point to expose a service at *.fluidattacks.com. Each consumer writes one module "X_cdn" block and configures the origins that apply (bucket, lambda, or both). It also declares the path-based routing and the edge Cache-Control of each route in a routes list. These are the usual consumer shapes:

  • bucket alone with a catch-all * route: a site that is only static (for example, design.fluidattacks.com and public.fluidattacks.com)
  • lambda alone with a catch-all * route: a Lambdalith (for example, infers.fluidattacks.com and tracks.fluidattacks.com)
  • bucket and lambda together, with explicit static-prefix routes for content-hashed assets (cache = "immutable") and a catch-all * route to the Lambda: a server-side-rendered site with static assets (for example, docs.fluidattacks.com and db.fluidattacks.com)
  • lambda with streaming = true: a Lambda Function URL in RESPONSE_STREAM mode for Server-Sent Events responses (for example, fixes-code.fluidattacks.com and its SCA siblings)

CloudFront uses the first route that matches, from top to bottom. The catch-all * route must be the last route. A precondition requires cache = "none" on Lambda routes. If a Lambda route declares a different value, the plan fails. This is because the default cache key of Cloudflare does not include the auth headers or the request bodies. If Cloudflare kept a Lambda response in its cache, it could serve this response to a different requester, and the authorization of each request would not operate. Bucket routes use the cache tier that agrees with the bytes ("immutable" for content-hashed assets, "short" for stable URLs with bytes that can change).

Caller identity for IAM-gated routes

CloudFront OAC signs each origin request again with its own SigV4 identity. Thus, the Authorization header of a viewer does not go past the edge. Some services must know who calls them. For example, tracks lets only some internal roles use its audit-write endpoints. These services use a pre-signed STS GetCallerIdentity URL in an X-Fluid-Identity header. The caller signs the URL with its own AWS credentials. The back end sends a request to this URL. STS examines the signature and returns the ARN of the caller. The back end compares the ARN in the STS response with a configured allowlist. STS validates the identity, and AWS IAM is the trust anchor. This method does not add shared secrets. Vault's AWS auth method and the Kubernetes aws-iam-authenticator use the same pattern to send AWS-IAM identity across an opaque transport.

The cdn module owns these resources:

  • The CloudFront distribution
  • The two Origin Access Controls
  • The Lambda Function URL (with AUTH_TYPE=AWS_IAM and a scoped CloudFront invoke permission)
  • The viewer mTLS trust store (Authenticated Origin Pulls). It rejects each handshake with a client certificate that the private CA of Fluid Attacks did not sign. Thus, it rejects all sources other than the Cloudflare zone of Fluid Attacks.
  • The origin-request Lambda@Edge function that adds the body hash to requests with a body
  • The ACM certificate and its issuance, which ACM validates with DNS records in Cloudflare
  • The Cloudflare DNS record that points to the distribution
  • The CloudWatch Logs Delivery V2 configuration 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, and a TLS-deny baseline policy) and the merged bucket policy (the OAC allow from cdn and the other statements that the consumer gives). The lambda module owns the function. Only cdn is responsible for the public exposure. The lambda module does not make a Function URL.

Other dependencies

On this page