Public-data platform

Azure AKS ingestion pods → Postgres → published analytics. Read-only view.

Architecture

Platform architecture Visitors resolve maxmccann.us through Cloudflare. The write-up page is proxied by Cloudflare to GitHub Pages. The retool.maxmccann.us subdomain is delegated by Cloudflare to Azure DNS, so platform traffic goes directly to an Azure Application Gateway, which routes to Retool prod, Retool nonprod and a read-only dashboard on AKS. Ingest CronJobs load public sources into Postgres; secrets flow from Key Vault through Infisical. Visitors public web Cloudflare maxmccann.us zone Proxy + TLS maxmccann.us to GitHub Pages NS delegation retool.maxmccann.us to Azure DNS (DNS only, not proxied) Records in Terraform (dns repo) Azure: VNet with private subnets, NAT egress, AKS API allowlisted App Gateway Let's Encrypt TLS (DNS-01, Azure DNS) AKS Retool prod Retool nonprod (upgrade lane) Read-only dashboard demo.retool.maxmccann.us Ingest CronJobs one pod per public source, honest User-Agent Infisical (private) per-namespace identities Postgres (Retool) prod, nonprod, Infisical databases Postgres (public data) jobs_ingest: raw, loaders only jobs_analytics: published, read-only role for apps Key Vault root of trust Apps read only jobs_analytics as a read-only role: no CONNECT on raw, writes refused. Infisical and the admin planes have no public route. All of it is Terraform: Retool's official Azure modules plus this platform's own, with three upstream bugs patched and documented.

How it works

Vendor findings

Reviewing every plan before applying it caught three defects in Retool's own Terraform modules, each patched in a vendored copy with the proof written down:

  1. Key Vault access silently removed on the second apply: inline access policies would have stripped Retool's access to its encryption key and database password.
  2. The nonprod lane was never routed: the ingress controller was hard-wired to one namespace, which assumes one App Gateway per deployment.
  3. Every request returned 502 with nothing in the logs: the ingress class and the controller name disagreed, and the controller overwrites one with the other at startup, so it claimed no ingress. Traced in the controller's source, proved live, then patched.