·4 min read

Hiding an Internal API the Right Way: Why a Proxy Isn't Security

build-in-publicsecuritycloudflarezero-trustnextjsrust|
PostLinkedIn

Hiding an Internal API the Right Way: Why a Proxy Isn't Security

Putting your API behind a proxy feels secure. It isn't — at least not by itself. The URL is hidden, the browser never sees the origin, job done, right?

That instinct is the part most people get wrong. Hiding a URL is obscurity, and OWASP has been clear for years that security-through-obscurity is not a control. A proxy only becomes a real trust boundary when the origin cannot be reached any other way. Until then you've just added a hop in front of a door that's still unlocked.

I hit this designing the API layer for a real-time Solana DEX indexer — a Rust/Axum service that a Next.js frontend (running on Cloudflare Workers) consumes. The indexer is internal. It should never be hit directly from the public internet. Here's the design I landed on, and why each layer earns its place.

The three layers

1. A Cloudflare Zero Trust Tunnel. The Rust indexer runs on baremetal behind a VPN. It is exposed to the outside world only through cloudflared, which makes an outbound-only connection to Cloudflare. There are no public inbound ports on the origin. You can't port-scan your way in, because there's nothing listening at the front door. This is the layer that turns "hidden" into "unreachable." 2. A Cloudflare Access service token. The tunnel is gated by an Access policy that requires a service token. Cloudflare rejects any request without the token at the edge, before it ever touches the origin. This is what makes the proxy a boundary instead of a redirect. 3. A BFF proxy. A Next.js Route Handler exposes a same-origin path — /api/indexer — and injects the service token server-side. The browser only ever sees the same-origin path. The token never reaches the client.

That third layer is not decoration. A service token is a machine-to-machine credential, and a machine-to-machine credential must never live in the browser. The server-side proxy is the only place it can be injected. The anti-pattern here is shipping it as a NEXT_PUBLIC_ variable — at which point it's baked into the JS bundle and you've handed your edge credential to every visitor. The BFF is mandatory precisely because it's the credential boundary.

Defense in depth

The three layers are the spine. A few more things make it hold up under pressure:

  • Validate the Access JWT at the origin. Cloudflare forwards a Cf-Access-Jwt-Assertion header. The Rust service should verify it rather than blindly trusting that traffic came through the edge. Don't assume your front door is the only path forever.
  • Enforce per-user authorization server-side. The service token authenticates the proxy, not the user. User-level access control still has to happen behind it.
  • Allowlist the proxy path. A wildcard proxy is an SSRF waiting to happen — see CVE-2025-6087, an SSRF in the OpenNext Cloudflare wildcard-proxy pattern. Constrain what the proxy is allowed to forward to.
  • Rotate tokens safely. Dual-token rotation: create the new token, swap it in, verify, then delete the old one. No window where nothing works.

The takeaway

The lesson that took me longest to internalize: the proxy was never the security. The unreachability of the origin is. Once the only route in runs through an authenticated edge, the proxy stops being a thin disguise and becomes a boundary you can actually reason about. Hiding the URL is the easy part. Closing every other door is the work.

Oleksandr Yusypenko

Oleksandr Yusypenko

Senior Full-Stack + AI Engineer. Building in public — AI agents, LangGraph, production systems.