Aller au contenu

AWS Lambda@Edge

Ce contenu n’est pas encore disponible dans votre langue.

The Prosopo Protect Lambda@Edge bundle runs on Amazon CloudFront’s viewer-request trigger. It executes Protect’s decision on every request and either forwards to your origin (allow), blocks it with a branded interstitial (block), or serves a captcha challenge (challenge).

Two-part install: read this first

Section titled Two-part install: read this first

A production Prosopo Protect integration is two independent pieces working together. Deploying only one leaves your site unprotected:

  • Part 1: the Lambda@Edge bundle (server-side). Runs on every CloudFront viewer request, executes the verdict, and blocks bad traffic before it reaches your origin. This is lambda-edge.js, deployed as a Lambda function and attached to a CloudFront behaviour.
  • Part 2: the Protect JS bundle (client-side). Loaded on every HTML page users visit. Creates the session cookie, collects the bot-signal telemetry the risk engine scores, and shows the captcha overlay when the edge returns a challenge. This is a single <script> tag in your HTML.

Without the Lambda, no request is scored. Without the JS bundle, no user ever gets a session cookie, so every /api/* and /forms/* request returns 401 no-session forever. The verify commands under Part 1 test the Lambda path only, so a green Lambda with no JS bundle deployed looks like it’s working while every real user still fails on their first XHR.

Deploy the two parts in either order, but ship both before rollout. The sections below are organised under the same two headings.

Part 1 (server-side) is delivered as a file:

  • lambda-edge.js: the Lambda code, delivered per site and per environment. Self-contained, ~100 KB. Same file for every customer; per-site config is baked in at build time.

Part 2 (client-side) is hosted by us. You add one <script> tag pointing at https://js.protect.prosopo.io/protect.bundle.js. Nothing to download or bundle yourself. We publish new versions to that URL; your pages pick them up on their next reload without a redeploy from you.

  • An AWS account with permission to create Lambda functions in us-east-1 (Lambda@Edge functions must live there), publish versions of that function, and attach viewer-request triggers to your CloudFront distributions.
  • A CloudFront distribution serving your origin.
  • The AWS CLI or the AWS Console.
  • Your Prosopo account manager has:
    • Registered your site key on the Protect dashboard with your intended defaultPathType, endpoint rules, IP-category rules, and interstitial branding. See Portal settings checklist, which applies here too.
    • Issued you a bundle scoped to your site.

Part 1: the Lambda@Edge bundle (server-side)

Section titled Part 1: the Lambda@Edge bundle (server-side)

1a. DNS: protect.<your-domain> as DNS-only

Section titled 1a. DNS: protect.&lt;your-domain&gt; as DNS-only

The bundle talks to Protect at a hostname you own, typically protect.<your-domain>. Create a CNAME record on your DNS pointing that hostname at Prosopo:

protect.<your-domain> CNAME protect.prosopo.io

Do not put this record behind a proxy layer (CloudFront, Cloudflare, or similar). Prosopo’s risk engine derives bot-signal value from the raw TLS handshake. If a proxy terminates the browser’s TLS at its edge and re-opens a fresh connection to Prosopo, the JA4 signals collapse to a single value per proxy stack and risk scoring degrades to noise. This subdomain must be DNS-only.

1b. Coordinate cert issuance with your account manager

Section titled 1b. Coordinate cert issuance with your account manager

Prosopo issues the TLS certificate for your protect.<your-domain> subdomain via Let’s Encrypt on our infrastructure. This is a two-step handoff:

  1. You: create the CNAME record. Verify it’s live:

    Terminal window
    dig protect.<your-domain>
    # ANSWER SECTION should show a CNAME to protect.prosopo.io
    # and an A record resolving directly to our edge.
  2. Us: email your account manager once DNS is propagated. We issue the certificate on our end (~30 seconds), then confirm back. Attempting to deploy the Lambda before we’ve issued the cert results in TLS handshake failures on every access-check call, and the Lambda fails open. No request is actually scored.

Once the cert is in place it auto-renews on our end every 60 days.

The bundle you receive from Prosopo is built against your CNAMEd hostname, so you don’t need to configure the URL yourself. If you rotate the CNAMEd hostname, request a fresh bundle.

Upload the bundle to Lambda, publish a version, and attach the versioned ARN to the CloudFront behaviour you want to protect.

Terminal window
zip lambda-edge.zip lambda-edge.js
# First deploy: create the function
aws lambda create-function \
--region us-east-1 \
--function-name prosopo-protect-viewer-request \
--runtime nodejs22.x \
--handler lambda-edge.handler \
--memory-size 256 \
--timeout 5 \
--role arn:aws:iam::<account-id>:role/<lambda-edge-execution-role> \
--zip-file fileb://lambda-edge.zip
# Every deploy after that: update the code
aws lambda update-function-code \
--region us-east-1 \
--function-name prosopo-protect-viewer-request \
--zip-file fileb://lambda-edge.zip
# Publish a version. Lambda@Edge only accepts versioned ARNs.
aws lambda publish-version \
--region us-east-1 \
--function-name prosopo-protect-viewer-request

The publish-version response includes a FunctionArn ending in :<version-number>. In your CloudFront distribution’s Behaviors tab, edit the behaviour you want to protect and add a Function association:

  • Event type: Viewer request
  • Function type: Lambda@Edge
  • Function ARN: the versioned ARN from publish-version.

CloudFront propagates to every edge location in 2 to 5 minutes. Repeat this attach step for each behaviour that should be protected.

Your account manager will let you know when a new bundle is available. Run update-function-code plus publish-version and point the behaviour at the new versioned ARN. CloudFront won’t pick up $LATEST.

The function’s role needs both lambda.amazonaws.com and edgelambda.amazonaws.com as trusted principals, plus the AWSLambdaBasicExecutionRole managed policy for CloudWatch Logs:

{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Service": ["lambda.amazonaws.com", "edgelambda.amazonaws.com"]
},
"Action": "sts:AssumeRole"
}
]
}

Once the trigger is attached and CloudFront has propagated, test through the distribution’s domain.

Terminal window
DIST=https://your-distribution.cloudfront.net
# 1. HTML: Protect should return "allow" (bootstrap-passthrough) and
# CloudFront forwards to your origin. Expected: 200, with
# x-prosopo-request-id in the response headers.
curl -sD - -H "Accept: text/html" -H "User-Agent: Mozilla/5.0" "$DIST/" | head
# 2. JSON, no cookie: Protect returns 401. Expected: 401,
# x-prosopo-status: no-session, x-prosopo-request-id.
curl -sD - -H "Accept: application/json" "$DIST/api/anything" | head
# 3. POST /forms/* with Accept: text/html and no cookie: same 401.
# Closes the "spoof Accept: text/html on a POST" bot pattern.
curl -sD - -H "Accept: text/html" -X POST "$DIST/forms/anything" | head

If command 1 returns anything other than 200, you may be hitting your own origin’s bot protection, not Prosopo’s. AWS WAF, CloudFront’s own managed rules, and similar tools reliably block curl regardless of what headers you set. Distinguish:

  • If the response has x-prosopo-* headers, the 4xx is from Prosopo. Check x-prosopo-status and x-prosopo-decision. See Troubleshooting.
  • If the response has server: awselb and no x-prosopo-* headers, the 4xx is from your origin’s own bot protection. Verify with a real browser instead.

X-Prosopo-Request-Id is echoed on every response Prosopo returns and written to Protect’s verdict audit log. CloudFront’s own x-amz-cf-id is the default request ID, so the same value shows up in CloudFront access logs and Protect’s audit log.

Lambda verified? Move to Part 2.

Part 2: the Protect JS bundle (client-side)

Section titled Part 2: the Protect JS bundle (client-side)

This part is mandatory. Without the bundle no session cookie is ever set, and the Lambda returns 401 no-session on every /api/* or /forms/* call in perpetuity. The Lambda verify commands under Part 1 still pass without the bundle; real user traffic doesn’t.

2a. Add the script tag to every HTML page

Section titled 2a. Add the script tag to every HTML page

Add this snippet to every HTML page your site serves. A shared header or footer template is the usual spot:

<script>
window.prosopo_config = {
site_key: "<your-site-key>",
protect_url: "https://protect.<your-domain>"
};
</script>
<script src="https://js.protect.prosopo.io/protect.bundle.js" async></script>

Both tags are required. The first sets window.prosopo_config before the second loads; the bundle reads that config to know which Protect API to talk to. Skipping the first tag causes the bundle to fall back to the CDN’s own origin (js.protect.prosopo.io) as its API URL, which returns 405 on the CORS preflight for /api/protect/init and hangs the bundle’s patched fetch.

On first page load the bundle:

  1. Calls POST https://protect.<your-domain>/api/protect/init.
  2. Receives a prosopo_session cookie scoped to .<your-domain>.
  3. Loads a per-session telemetry runtime script that collects the bot-signal data used for risk scoring.

Every subsequent request from that browser carries the cookie, and the Lambda looks up a real verdict on /api/* or /forms/* calls.

Section titled 2b. Gate fast form submissions and link clicks (optional)

Users can click Submit before the bundle finishes creating the session. The browser fires a full-page POST with no cookie and gets a 401. The security is correct but the UX is bad.

Opt any form or link into automatic held-until-session-ready behaviour by adding the data-prosopo-gate attribute:

<form action="/forms/search.php" method="post" data-prosopo-gate>
<!-- ... -->
</form>
<nav data-prosopo-gate>
<a href="/section-a/">Section A</a>
<a href="/section-b/">Section B</a>
</nav>

The bundle intercepts marked forms and same-origin links in the capture phase, holds up to 8 seconds waiting for the session, then releases with the cookie in place. The submit button flips disabled and aria-busy while held so the user gets visual feedback. Ctrl/Cmd-click, middle-click, target="_blank", download attributes, hash-only links, and cross-origin links pass through untouched.

Attributes on an ancestor cover every descendant form or link, so <nav data-prosopo-gate> gates every link inside.

Open your site in a real browser with DevTools on the Network tab. On first page load you should see:

  • A POST to https://protect.<your-domain>/api/protect/init returning 200 and a Set-Cookie: prosopo_session=… header.
  • A subsequent GET https://protect.<your-domain>/api/protect/t/<jti> fetching the per-session telemetry runtime.
  • The prosopo_session cookie set with Domain=.<your-domain> for later requests.

If the init call goes to https://js.protect.prosopo.io/api/protect/init and returns 405, the window.prosopo_config block is missing from the snippet. See Troubleshooting.

Configuring proxy, VPN and datacenter blocking

Section titled Configuring proxy, VPN and datacenter blocking

Blocking policy lives on your Prosopo dashboard, not in the bundle. Two independent rule sources feed the no-cookie edge path:

  • Site settings: recognised IP categories are tor, datacenter, proxy, vpn, abuser, mobile, and crawler. Configure per-category verdicts (allow, challenge, or block).
  • Tiered access rules: support the full rule set of IP CIDR, ASN, IP category, country, User-Agent substring, and JA4 TLS fingerprint. Rules can be scoped to your site or applied globally by Prosopo.

Either source firing blocks the request at the edge. When both match, the more restrictive verdict wins (Block > Challenge > Allow). Policy changes take effect on the next request, no redeploy needed.

  • Region: functions must be created in us-east-1.
  • Memory: 256 MB recommended. Higher tiers cost more but give proportionally more CPU.
  • Time: the viewer-request handler is capped at 5 seconds. Protect’s API calls typically return in under 40 ms (under 2 ms for cached lookups) and are budgeted to a 500 ms hard timeout, with fail-open on transient errors.
  • CloudWatch logs: written to the region closest to the viewer that served the request, not us-east-1. Check the matching region when debugging.

Each bundle is scoped to a specific site’s authentication token. Rotating the token means requesting a fresh bundle from Prosopo and redeploying it via the Deploy the Lambda flow. The old bundle keeps working until you swap the ARN on your CloudFront behaviour.

Common failure modes with their causes are on the dedicated Troubleshooting page.