imdmp

Your API Keys Shouldn't Exist

Why the best API key is the one you never needed in the first place — and how Tailscale makes your backend invisible to the public internet.

Network mesh diagram

Sat May 23 - Written by: Danny Pagta

Last week, someone posted on Twitter that their AWS bill hit $14,000 overnight. A bot found their exposed API key in a public repo and spun up crypto miners. Their project was a weekend side project. The key was committed by accident.

This happens every day. GitHub now scans for exposed secrets and revokes them automatically, which is how common it has become. We’ve normalized a brittle setup: every backend gets a key, and every leaked key can turn into a bill.

There’s a simpler option. Don’t put the backend on the public internet.

The Problem Isn’t Your Key Management

The standard architecture for indie developers looks like this: your frontend talks to your backend over the public internet. Your backend needs to know who’s calling it, so you add API keys. Now you have to store them, validate them on every request, rotate them, and avoid leaking them.

That’s a lot of infrastructure and cognitive overhead dedicated to answering one question: “Is this request coming from something I trust?”

It’s also fragile. API keys are shared secrets, and once leaked, anyone can use them. Rate limiting helps, but a leaked key with a high rate limit is still a leaked key. If your backend runs on usage-based infrastructure (most serverless platforms, any AI API), a leaked key isn’t only a security incident, it’s a financial one.

What If Your Backend Didn’t Exist on the Internet?

The alternative is simple: run the backend without a public listener and only expose it over a private network. From the public internet, there’s nothing to scan or hit.

The way you reach the backend is by being an authenticated member of an encrypted mesh network that the backend is also connected to. This is what Tailscale does.

Tailscale creates a WireGuard-encrypted mesh network (a “tailnet”) between your devices. Every device gets a stable private IP and a DNS name. Your laptop, your server in Singapore, your Raspberry Pi at home, all of them can talk to each other as if they’re on the same local network, no matter where they physically are.

If your backend is only listening on its Tailscale IP, the public internet can’t reach it. And if you only need it to talk to your frontend or your own machines, you stop needing service-to-service API keys in the first place.

”But My Users Need to Reach the API”

They do, but through your frontend, not by talking to the backend directly.

Your users interact with your frontend. Your frontend is public, because that’s how the web works. But your frontend doesn’t have to expose your backend to the world. If your frontend is server-side rendered (SSR), the server makes API calls on behalf of the user. That server is a process you control, and that process can be a node on your Tailscale network.

The architecture becomes:

  1. User’s browser hits your public frontend (served through Cloudflare, Vercel, wherever)
  2. Your frontend server receives the request
  3. The frontend server calls your backend API over the Tailscale mesh
  4. The backend processes it and returns the result
  5. The frontend server sends the response to the user’s browser

The user never talks to your backend, doesn’t know your backend exists, and has no way to attack it.

This is a normal architecture pattern: public edge, private backend. Big companies do it with VPCs, private subnets, and a lot more networking overhead (Google’s BeyondCorp, AWS VPC with private subnets). Tailscale is a lightweight way to get most of that separation as a solo developer.

What You Actually Eliminate

When your backend is Tailscale-only, you can usually remove or simplify a few things:

API key management for service-to-service calls. Your frontend server is on the tailnet. Your backend is on the tailnet. Access moves down a layer: Tailscale node identity and ACLs decide which machines can talk to which services.

DDoS protection for your API. You can’t DDoS something that doesn’t have a public endpoint. The only public surface is your frontend, which sits behind Cloudflare (free tier) and gets their DDoS protection automatically.

Denial-of-wallet attacks. This matters if you’re on usage-based infrastructure. If your API endpoint is public and calls an AI service (OpenAI, Gemini, Anthropic), someone who finds that endpoint can rack up your bill by spamming requests. With a Tailscale-only backend, there’s nothing to spam. I run an OCR endpoint backed by a paid AI API. Public, it would be a denial-of-wallet target. Behind Tailscale, my monthly bill is bounded by my own traffic.

Firewall rules and security groups. No public ports means no firewall rules to configure. The default state is “nothing is accessible,” and you selectively allow access through Tailscale’s ACL policy.

TLS certificate management between services. WireGuard handles encryption. You don’t need Let’s Encrypt or your own certificates for internal service-to-service communication.

The 10-Minute Setup

At a high level, the setup looks like this:

  1. Install Tailscale on your server: curl -fsSL https://tailscale.com/install.sh | sh && tailscale up
  2. Install Tailscale on your dev machine (download from tailscale.com)
  3. Both devices appear in your tailnet. They can now reach each other by name.
  4. Configure your backend to listen on 0.0.0.0:8080 (or just localhost:8080 with tailscale serve)
  5. Remove the public IP from your server, or close all inbound ports in your firewall
  6. From your dev machine: curl http://your-server:8080/health

Your backend is now invisible to the internet and reachable only by your authorized devices.

Access Control in One File

Tailscale ACLs let you define exactly who can reach what. It’s a JSON file that governs your entire network policy:

{
  "acls": [
    {"action": "accept", "src": ["tag:frontend"], "dst": ["tag:backend:8080"]},
    {"action": "accept", "src": ["tag:backend"], "dst": ["tag:database:5432"]},
    {"action": "accept", "src": ["tag:danny"], "dst": ["*:*"]}
  ]
}

That’s it. The frontend can reach the backend on port 8080. The backend can reach the database on port 5432. My dev machine can reach everything. Everything else is denied by default.

Compare this to managing API keys, OAuth scopes, JWT validation middleware, rate limiting configurations, IP allowlists, and firewall rules, all of which you’d need to approximate the same level of access control on a public API.

”This Sounds Like a VPN. VPNs Are Annoying.”

Tailscale is technically a VPN, but operationally it feels lighter than the old corporate-VPN model. You install it, sign in, and your machines can usually reach each other without babysitting a tunnel. It stays connected in the background and works across NAT, firewalls, cellular, and wifi.

The mesh architecture means devices connect to each other directly when they can, with relay fallback when they can’t. Your laptop in a coffee shop can connect straight to your server in Singapore. Latency is close to a normal internet connection, with no extra hop through a VPN concentrator.

Where This Doesn’t Apply

Not everything benefits from this model:

Static content sites. If your site is HTML/CSS/JS served from a CDN with no backend, there’s nothing to protect. Cloudflare Pages and similar platforms handle this perfectly.

Client-side API calls. If your frontend makes API calls from the user’s browser (fetch from JavaScript), those requests originate from the user’s machine, which isn’t on your tailnet. You’d need a public endpoint for this. The SSR proxy pattern avoids the problem entirely.

Serverless edge functions. Cloudflare Workers and similar platforms can’t run Tailscale, because there’s no persistent process to install it on. If your entire architecture is serverless, Tailscale doesn’t fit into the compute layer (though it still works for databases and other stateful services).

High-traffic public APIs. If you’re building an API that third parties consume (a Stripe-like developer API), it needs to be public by definition. Tailscale is for internal infrastructure, not public-facing APIs.

The Bigger Picture

Most indie tutorials start from the same assumption: your API is public, so now you need auth, rate limits, and damage control.

For a lot of solo projects, that’s backwards. If the backend only needs to talk to your frontend or your own machines, don’t publish it by default. Put it on a private network first.

That won’t fit every architecture, but when it fits, it removes a surprising amount of security and billing risk.

The best API key is the one you never needed.