Skip to content

Update lambda stack#3

Open
gerhc wants to merge 2 commits intomasterfrom
update-lambda-stack
Open

Update lambda stack#3
gerhc wants to merge 2 commits intomasterfrom
update-lambda-stack

Conversation

@gerhc
Copy link
Copy Markdown
Member

@gerhc gerhc commented Apr 14, 2026

Lambda Mailer — Modernization + Spam Fix

Context

The lambda-mailer repo is ~10 years old and was running on unmaintained tooling
(Zappa, Flask 1, Python 3.8). Bots were also bypassing the frontend entirely and
POSTing spam directly to the Lambda endpoint, since no server-side authentication
existed.

This PR modernizes the stack and adds a shared secret to block direct bot traffic.


What changed

Stack modernization

Before After
Python 3.8 (EOL) 3.12
Framework Flask 1 FastAPI + Mangum
Deployment Zappa (unmaintained) AWS SAM
Package manager Poetry uv
Linting Black Ruff
Tests None pytest + moto (14 tests)
  • api.py — rewritten with FastAPI, Pydantic request validation, CORS middleware,
    type hints, f-strings. Same behavior and honeypot logic as before.
  • handler.py — new Lambda entry point using Mangum (ASGI → Lambda adapter).
  • template.yaml — SAM template replacing zappa_settings.json. Defines Lambda,
    API Gateway, and a scoped IAM role (SES send only, no wildcards).
  • samconfig.toml — deploy config for dev and prod environments.
  • Makefile — shortcuts for deploy-dev, deploy-prod, test, lint, logs-dev,
    logs-prod. Reads secrets from .env automatically.
  • requirements.txt — added for SAM's Python builder to bundle dependencies.
  • .env.example — documents required secrets (never committed).

Spam fix — shared secret header

Bots were hitting the Lambda URL directly, bypassing reCAPTCHA and all frontend
validation. The fix is a shared secret (FORM_SHARED_SECRET) that the Next.js
server sends in every legitimate request via X-Form-Secret. Requests without
the correct header are rejected with 403.

Bot → POST Lambda directly → 403 (no secret) Browser → Next.js /api/contact → POST Lambda (with secret) → 200

The secret is never exposed to the browser.


Deployment

Two separate stacks are created — existing Zappa infrastructure is untouched:

Environment Stack
Dev lambda-mailer-v2-dev
Prod lambda-mailer-v2-prod
# One-time: generate secret and add to .env
openssl rand -hex 32  # → paste into .env as FORM_SHARED_SECRET

make deploy-dev
make deploy-prod

----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

Testing

make test   # 14 unit tests (validation, honeypot, CORS, secret auth)

Manual smoke test against the deployed endpoint:

# Should return {"status":"ok"}
curl -X POST <ApiUrl> \
  -H "Content-Type: application/json" \
  -H "X-Form-Secret: $(grep FORM_SHARED_SECRET .env | cut -d= -f2)" \
  -d @message.json

# Should return 403
curl -X POST <ApiUrl> -H "Content-Type: application/json" -d @message.json

----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

Required env vars (Lambda)

┌──────────────────────┬──────────────────────────────────────────────┐
│ Variable             │ Description                                  │
├──────────────────────┼──────────────────────────────────────────────┤
│ FROM_EMAIL           │ Verified SES sender address                  │
├──────────────────────┼──────────────────────────────────────────────┤
│ DESTINATION_EMAIL    │ Address to receive submissions               │
├──────────────────────┼──────────────────────────────────────────────┤
│ FORM_SHARED_SECRET   │ Shared secret (from .env, never committed)   │
├──────────────────────┼──────────────────────────────────────────────┤
│ ```                  │                                              │
└──────────────────────┴──────────────────────────────────────────────┘

gerhc added 2 commits April 10, 2026 16:14
┌───────────────────────────────┬────────────────────────────────────────────────────────────────────────────────────────┐
│ File                          │ Action                                                                                 │
├───────────────────────────────┼────────────────────────────────────────────────────────────────────────────────────────┤
│ pyproject.toml                │ Rewritten — Poetry→uv, Python 3.12, FastAPI+Mangum+boto3, ruff/mypy/pytest dev deps    │
├───────────────────────────────┼────────────────────────────────────────────────────────────────────────────────────────┤
│ api.py                        │ Rewritten — Flask→FastAPI, Pydantic validation, CORS middleware, type hints, f-strings │
├───────────────────────────────┼────────────────────────────────────────────────────────────────────────────────────────┤
│ handler.py                    │ New — Mangum Lambda entry point                                                        │
├───────────────────────────────┼────────────────────────────────────────────────────────────────────────────────────────┤
│ template.yaml                 │ New — AWS SAM template (Lambda + API Gateway + scoped IAM)                             │
├───────────────────────────────┼────────────────────────────────────────────────────────────────────────────────────────┤
│ samconfig.toml                │ New — SAM deploy config for dev/prod                                                   │
├───────────────────────────────┼────────────────────────────────────────────────────────────────────────────────────────┤
│ tests/                        │ New — 14 pytest tests (validation, honeypot, CORS, errors)                             │
├───────────────────────────────┼────────────────────────────────────────────────────────────────────────────────────────┤
│ README.md                     │ Updated — uv/SAM setup, deploy, test instructions                                      │
├───────────────────────────────┼────────────────────────────────────────────────────────────────────────────────────────┤
│ .gitignore                    │ Modernized — added .aws-sam/, .ruff_cache/, .mypy_cache/                               │
├───────────────────────────────┼────────────────────────────────────────────────────────────────────────────────────────┤
│ zappa_settings.json           │ Removed                                                                                │
├───────────────────────────────┼────────────────────────────────────────────────────────────────────────────────────────┤
│ lambda-mailer-policy-*.json   │ Removed (IAM now inline in SAM template, scoped to SES identity)                       │
├───────────────────────────────┼────────────────────────────────────────────────────────────────────────────────────────┤
│ poetry.lock                   │ Removed (replaced by uv.lock)                                                          │
└───────────────────────────────┴────────────────────────────────────────────────────────────────────────────────────────┘
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant