What this is
A voting system built as twelve microservices across four cloud providers — AWS, Cloudflare, Google Cloud and Alibaba Cloud — with exactly three services per provider: one PaaS, one FaaS, one IaaS. It runs entirely on free tiers.
The design goal is that no single operator can link a voter to their ballot. Identity, encrypted ballots and the decryption key are held by different services, and the key is split so that no one party can reconstruct it alone.
How a ballot is protected
- Encrypted in the browser. Each ballot is sealed with a fresh AES-GCM-256 key; that key is wrapped with the election's RSA-OAEP-256 public key. The portal and the gateway only ever see ciphertext.
- No identity on the vote. The gateway rebuilds
POST /api/votefrom three fields —election_id,ballot_enc,vote_token— and forwards noAuthorization,Cookie,User-Agent,Refereror client-IP header. - One vote per token. A spent token is refused with
409 token_used; a closed election refuses everything with403 election_closed. - Split key custody. The counting key is shared 3-of-4 by Shamir's Secret Sharing. Shares are typed at counting time and the rebuilt key lives in memory only.
- Verifiable, not readable. A voter gets a receipt identifier that proves their ballot is in the urn without revealing its content.
Service layout
- Cloudflare
- voter portal (Pages) · API gateway (Workers) · network & security (DNS, WAF, Tunnel, Access)
- AWS
- admin console (Amplify) · identity service · urne / ballot box
- Google Cloud
- election management · results · receipts
- Alibaba Cloud
- notifications · counting · publication
The gateway validates RS256 JWTs against a published JWKS, enforces per-class rate limits, and routes to the other clouds. It holds no ballot key.
Live components
The Cloudflare slice is deployed. The voter portal encrypts in-browser; the gateway is reachable and reports its own health and upstream configuration.
Backend services from the other three clouds are still being connected, so end-to-end voting is not yet available.
Edge hardening
- Content-Security-Policy with
default-src 'none'and an explicitconnect-srcallowlist — no third-party script or fetch target. - JWTs kept in memory only: no
localStorage,sessionStorage, cookie or IndexedDB anywhere in the portal. - Request bodies capped at 64 KiB; oversized payloads refused with
413 payload_too_large. nosniff,no-referrer,X-Frame-Options: DENYandno-storeon every response.- CORS by exact origin plus a scoped hostname suffix — never a wildcard.