# AWS Lambda

**A Lambda function has no fixed address — it cold-starts on whatever IP AWS hands it, so anything downstream that wants to trust "this call came from my agent" is stuck trusting a bearer token instead.**

Any of the thousand execution environments behind your function's ARN can leak or replay that token just as well as the real one. Whisper gives the function a durable identity anyway: a public Lambda layer, one `--layers` flag, no vendoring, no Docker rebuild — and the function's *traffic*, not just its logs, leaves from a routable `/128` a stranger can verify.

## The problem with serverless identity

Every long-lived compute target — a VM, a container, a bare-metal box — earns its identity by *being somewhere*: it holds an IP, and that IP shows up in reverse DNS, in TLS certificates, in access logs a security team can correlate. Lambda breaks that assumption on purpose. The function has no home; AWS schedules it onto whatever execution environment is free, behind a NAT gateway or the Lambda service's own shared egress ranges, shared with every other customer's functions in that account and region. If you need the *receiving* system — a partner API, an internal service, a zero-trust gateway — to know which specific agent or workload made a call, you're left bolting on a bearer credential (a JWT, an API key) and hoping it never leaks, because the network layer itself gives you nothing to check it against.

Whisper's answer is the same one it gives every other compute shape: mint the function a real agent identity — forward DNS (AAAA), reverse DNS (PTR), a DANE `TLSA` key pin, an RDAP registration — anchored in `2a04:2a01::/32` (`AS219419`), and then make the function's *outbound* traffic actually leave from that `/128`. The receiving side no longer has to trust a header; it can run `dig -x` on the source address and get an independently verifiable answer.

## The public layer — zero bundling

Lambda layers exist precisely so you don't have to `pip install` a dependency into every deployment zip. Whisper ships one, public, MIT-licensed:

```
arn:aws:lambda:eu-north-1:205639151085:layer:whisper-id-egress:1
```

It bundles [`whisper-id`](https://pypi.org/project/whisper-id/) (the Python SDK — see [Python SDK](/docs/sdk-python)) plus `requests` and `PySocks`, all as **pure-Python wheels** (`--implementation py --abi none --platform any`), which is the whole trick: one zip serves **python3.12 and python3.13, x86_64 and arm64**, because there's no compiled extension to diverge per platform. Attach it and `import whisper_id` just works — no bundling, no rebuild when AWS ships a new runtime patch.

```bash
aws lambda update-function-configuration --function-name my-fn \
  --layers arn:aws:lambda:eu-north-1:205639151085:layer:whisper-id-egress:1
```

Layer ARNs are region-scoped; from outside `eu-north-1`, copy it once with `aws lambda get-layer-version-by-arn` + `publish-layer-version` into your own region, or just vendor the three pip packages yourself — they're small and dependency-light.

> Layer versions are immutable in Lambda — every rebuild publishes a new version number and needs its own `add-layer-version-permission --principal '*'` grant. If you pin `:1` in a script, check for a newer version occasionally; the ARN's *name* stays `whisper-id-egress` forever, only the trailing version increments.

Building it yourself is one shell script and one `pip install`, no different from building any other pure-Python layer:

```sh
pip install --target build/python \
  --implementation py --abi none --platform any --only-binary=:all: \
  "whisper-id[socks]" requests
(cd build && zip -qr ../layer.zip python)
aws lambda publish-layer-version --layer-name whisper-id-egress \
  --zip-file fileb://layer.zip --compatible-runtimes python3.12 python3.13 \
  --compatible-architectures x86_64 arm64 --license-info MIT
```

## Two tiers, per Postel

Every Whisper integration follows the same rule: the *keyless* half works for anyone, no account, no credential, so a stranger reading your function's response can get real value; the *keyed* half unlocks the actual control plane and egress once you supply an API key. AWS Lambda is no exception — nothing here is a demo of a capability you have to pay to unlock; both tiers are the real thing.

### Tier 1 — keyless verify (no credentials at all)

`whisper_id.verify()` re-runs the full server-side trust chain — DANE + DNSSEC + reverse-DNS + the identity JWS — over one HTTPS GET. No CLI, no API key, works from inside any Lambda regardless of network config.

**Without the SDK** — in Python it's one `requests` call to the keyless endpoint; `whisper_id.verify()` is the same call, typed and exception-safe:

```python
import requests
addr = "2a04:2a01:eb5a:ca74:cef2:2a:323d:40d4"
r = requests.get(f"https://rdap.whisper.online/verify-identity/{addr}")
print(r.json())   # {"is_whisper_agent": True, "dane_ok": True, "jws_ok": True, "evidence": {...}}
```

The full chain behind that verdict, and how to re-derive it from a terminal, is in [Verify an agent](/docs/verify). **With `whisper-id`**, inside `handler.py` (attach the layer, no other setup):

```python
import whisper_id

def handler(event, context=None):
    addr = event.get("queryStringParameters", {}).get("addr")
    is_agent = whisper_id.verify(addr)
    return {
        "statusCode": 200,
        "body": {
            "address": addr,
            "is_whisper_agent": is_agent,
            "rdap": whisper_id.rdap(addr) if is_agent else None,
        },
    }
```

```sh
aws lambda invoke --function-name whisper-egress \
  --cli-binary-format raw-in-base64-out \
  --payload '{"queryStringParameters":{"addr":"2a04:2a01:eb5a:ca74:cef2:2a:323d:40d4"}}' \
  out.json && cat out.json
```

A target that isn't a Whisper agent returns a clean `{"is_whisper_agent": false}` — never a 500 or an exception, per the [Robustness Principle](/docs/identity).

### Tier 2 — keyed egress (your traffic, your `/128`)

This is the part a bearer token can't give you: the function's *outbound HTTP request itself* leaves AWS sourced from the agent's routable `/128`, via the Tier-1.5 SOCKS5 / HTTP-CONNECT egress proxy described in [Connect & egress](/docs/connect). Because you reach that proxy over an `https://` scheme (your TLS to the proxy, wrapping the TLS to the origin), the egress token never appears outside an encrypted channel; and if a freshly minted token's source-binding hasn't propagated across the egress fleet yet, the call is transparently relayed through the HTTPS forward gateway rather than failing.

**With stock tools** — once you have a connection string and an egress token, this is standard `requests`-over-proxy, nothing Whisper-specific:

```python
import requests
proxies = {"https": "https://w:<egress-token>@connect.whisper.online:443"}
r = requests.get("https://v6.ident.me", proxies=proxies, timeout=20)
print(r.text)   # -> 2a04:2a01:eb5a:ca74:cef2:2a:323d:40d4  (the agent's own address)
```

**With Whisper**, the full `handler.py` two-tier recipe (from [`whisper-examples/lambda`](https://github.com/whisper-sec/whisper-examples/tree/main/lambda)):

```python
import os, json, ipaddress, urllib.request, whisper_id, requests

def source_ip():
    """Tier-1.5 egress proxy first; if the token's binding is still propagating
    (or the runtime denies a raw socket), fall back one hop to the HTTPS forward
    gateway. Either way the request leaves from the agent's /128."""
    proxy = os.environ.get("WHISPER_EGRESS")
    if proxy:
        try:
            return requests.get("https://v6.ident.me",
                                proxies={"https": proxy}, timeout=20).text.strip()
        except requests.exceptions.RequestException:
            pass
    gw = os.environ.get("WHISPER_FORWARD_URL", "https://forward.whisper.online")
    body = json.dumps({"agent": os.environ["WHISPER_AGENT"],
                       "url": "https://v6.ident.me"}).encode()
    req = urllib.request.Request(gw + "/forward", data=body, headers={
        "content-type": "application/json", "X-API-Key": os.environ["WHISPER_API_KEY"]})
    return json.loads(urllib.request.urlopen(req, timeout=25).read())["body"].strip()

def handler(event, context=None):
    qs = (event or {}).get("queryStringParameters") or event or {}
    if qs.get("egress") is not None:                      # Tier 2 — keyed egress
        seen = source_ip()
        want = os.environ.get("WHISPER_AGENT")
        return {"tier": "egress", "seen_ip": seen,
                "is_whisper_agent": whisper_id.verify(seen),
                "matches_agent_address": bool(want) and
                    ipaddress.ip_address(seen) == ipaddress.ip_address(want)}
    addr = qs.get("addr")                                  # Tier 1 — keyless verify
    return {"address": addr, "is_whisper_agent": whisper_id.verify(addr)}
```

Deploy it with the layer attached and the egress token in the function's environment (or AWS Secrets Manager — never in code):

```bash
zip function.zip handler.py
aws lambda create-function --function-name whisper-egress \
  --runtime python3.12 --architectures arm64 \
  --role <your-lambda-exec-role-arn> --handler handler.handler --timeout 120 \
  --layers arn:aws:lambda:eu-north-1:205639151085:layer:whisper-id-egress:1 \
  --zip-file fileb://function.zip \
  --environment 'Variables={WHISPER_EGRESS=https://w:<egress-token>@connect.whisper.online:443,WHISPER_FORWARD_URL=https://forward.whisper.online,WHISPER_AGENT=<your-/128>,WHISPER_API_KEY=<your-api-key>}'

aws lambda invoke --function-name whisper-egress \
  --cli-binary-format raw-in-base64-out --payload '{"egress":"1"}' out.json && cat out.json
# -> {"tier":"egress","seen_ip":"2a04:2a01:...","is_whisper_agent":true,"matches_agent_address":true}
```

`source_ip()` is the whole two-tier fallback in miniature: it tries the Tier-1.5 proxy first and, if that token's source-binding is still propagating across nodes (or Lambda denies the raw socket), drops to the HTTPS `forward.whisper.online/forward` gateway — keyed with the agent's API key — the same relay path documented in [Connect & egress](/docs/connect) and [the forward gateway](/docs/forward-gateway). Either way, the traffic leaves from the agent's `/128`.

Mint the agent first, from anywhere — this doesn't have to happen inside Lambda:

```sh
pip install whisper-id
whisper create --label lambda-worker      # -> address (a routable /128), fqdn, api_key
```

Then pull the egress connection string for `WHISPER_EGRESS`. The CLI's `whisper connect` deliberately brings up a *local*, bearer-free proxy (`socks5h://127.0.0.1:<port>`) — which a Lambda can't host — so take the remote `connection_string` straight from the control plane instead (see [Control plane](/docs/control-plane)):

```
CALL whisper.agents({op:'register', args:{label:'lambda-worker'}})
  -> { agent, address, fqdn, ptr, api_key }
CALL whisper.agents({op:'connect', args:{agent:'lambda-worker', tier:'socks5'}})
  -> { connection_string, http_proxy, ... }   # connection_string -> WHISPER_EGRESS
```

## Verifying it actually worked

Don't take the function's word for it — the point of address-as-identity is that anyone downstream can check independently, from a terminal, without trusting the Whisper API at all:

```sh
whisper verify --trustless 2a04:2a01:eb5a:ca74:cef2:2a:323d:40d4
```

The seven stock-tool proofs behind that verdict (`dig`, `openssl`, `whois`) are in [Verify an agent](/docs/verify).

## Node instead of Python

The same two-tier recipe exists for the Node 18+ runtime using the dependency-free [`npm whisper-edge`](https://www.npmjs.com/package/whisper-edge) SDK (`npm i whisper-edge`, zip with `node_modules`, set `WHISPER_API_KEY` for the egress tier) — see [`whisper-examples/lambda/handler.mjs`](https://github.com/whisper-sec/whisper-examples/tree/main/lambda) and the broader serverless/edge write-up at [Integrations](/docs/integrations#serverless).

---

**Next:** [Python SDK](/docs/sdk-python) for the full `whisper_id` API surface, or [Connect & egress](/docs/connect) for how the SOCKS5 / HTTP-CONNECT proxy sources traffic from the agent's `/128` on any platform, not just Lambda.
