Protecting a Public Endpoint When There's No WAF or Rate Limiting
Every port you expose is reachable from the internet, and the platform's edge does exactly two things: terminate TLS and route by hostname. There's no web application firewall, no rate limiting, no IP allowlist, and no authentication layer in front of your app. That's not unusual for a small container host, but it's worth knowing precisely, because it means every protection your service has is one you put there. The good news is that the cheap 80% — don't expose it, authenticate it, rate-limit it in-process, and give it real credentials — takes an afternoon and covers most of what actually happens to small services.
What the platform does and doesn't do
| Provided | |
|---|---|
| TLS termination with a valid certificate | Yes |
| Routing to your container by hostname | Yes |
| Rate limiting | No |
| Web application firewall | No |
| IP allowlist / geo blocking | No |
| Authentication in front of your app | No |
| Private networking between apps | No |
The last row is the one people are most often surprised by, and it's why a database you run here is reachable over a public TCP endpoint rather than an internal network. Credentials are the boundary.
Start by not exposing it
The highest-value control is the one that removes the attack surface entirely.
- Workers don't need ports. A Discord or Telegram bot, a queue consumer, a scheduled job — none of these listen for anything. If your app has no port binding, there's nothing to reach.
- Remove bindings you added for debugging. A database port you exposed once to run a migration from your laptop stays exposed until you delete it.
- Question every port. An admin interface on a second port is a second thing to defend.
This sounds obvious and it's routinely where the real exposure comes from.
Obscurity buys a little — don't spend it
Apps get a generated hostname with a hash in it, and because certificates are issued as a wildcard for the platform domain rather than per-app, your specific hostname doesn't show up in public Certificate Transparency logs the way a per-app certificate would. That's genuinely better than the usual situation.
It is still not a security control. Hostnames leak through referrer headers, shared links, browser history, screenshots, and anything your app calls out to. Treat an unguessable URL as a speed bump that keeps you out of untargeted scans — not as a reason to skip authentication.
Authenticate anything that isn't meant to be public
If an endpoint changes state, reveals data, or costs you money to serve, it needs a check that isn't "nobody knows the URL."
- A shared secret in a header is enough for a machine-to-machine endpoint. Compare it in constant time, the same as a webhook signature.
- Signed payloads where the caller supports them — GitHub, Stripe, Telegram all sign their deliveries, and verifying is strictly better than a bearer token you have to keep secret in both directions.
- Real auth for anything user-facing. Don't hand-roll sessions.
For databases, the credential is the perimeter. Long random passwords, not reused anywhere, kept in environment variables — and remove the port binding when you're not actively using it.
Rate limit in your own process
There's no rate limiting at the edge, so it goes in your app. For a single small service this is genuinely easy — a dict keyed by IP or API key holding recent timestamps is enough, and it's the same in-memory pattern that makes Redis unnecessary at this size.
Two things to get right:
- Rate limit the expensive endpoints specifically. A global limit that's generous enough for browsing is useless for the endpoint that runs a database query per call.
- Use the right key. IP for anonymous traffic, API key or user ID for authenticated. IP alone is weak behind shared networks, but it's much better than nothing.
Also set timeouts and body size limits. An endpoint that accepts an unbounded upload or waits forever on a slow client is a resource-exhaustion problem long before anyone tries a real attack — and on a small quota, exhaustion means an out-of-memory kill that looks like a mystery restart.
What you genuinely can't do here
Being straight about the ceiling, because it should shape what you host:
- No DDoS scrubbing. A determined volumetric attack isn't something an app-level defence answers.
- No WAF rules. No managed protection against injection or bot patterns — your input validation is the only layer.
- No edge IP allowlist. You can filter by IP inside your app, but the request still reaches you.
If you need those, the usual answer is to put a CDN or reverse proxy with a WAF in front — which requires pointing a custom domain at your service, and custom domains aren't available as a platform feature today. That's a real constraint, and it's the honest reason this platform suits side projects, bots, internal tools, and small APIs better than something with a compliance requirement or a target on its back.
A short checklist
- Delete port bindings you don't need. Workers need none.
- Authenticate every endpoint that isn't genuinely public.
- Verify signatures where the caller offers them, in constant time.
- Rate limit the expensive endpoints in-process.
- Set request timeouts and body size limits.
- Long random credentials on any database port, rotated when they leak.
- Watch your logs for the shape of traffic changing.
FAQ
Does the platform provide a WAF or rate limiting?
No. The edge terminates TLS and routes by hostname. Rate limiting, filtering, and authentication are your application's responsibility.
Is my app's URL secret?
Not reliably. Certificates are wildcards, so your hostname isn't published in Certificate Transparency logs — but URLs leak through referrers, links, and screenshots. Don't use it as an access control.
How do I protect a database I run here?
With credentials, since there's no private network. Use a long unique password, keep it in an environment variable, and remove the port binding when you aren't using it.
How should I rate limit without Redis?
In memory, in your process. A per-key structure of recent timestamps is enough for a single small service and avoids a network round trip.
Can I restrict access by IP?
Only inside your app — there's no edge allowlist, so the request still arrives before you reject it.
What if I need DDoS protection?
That needs a CDN or scrubbing service in front, which needs a custom domain. If that's a hard requirement, this isn't the right platform for that particular service.
Wrapping up
"No WAF" sounds alarming and mostly isn't, for the kind of thing that belongs on a small container: a bot with no open ports, an internal tool behind a token, an API serving a handful of clients. What matters is knowing the boundary is your code rather than the infrastructure, and spending an afternoon on the parts that actually get exercised — closing ports you don't need, checking credentials properly, and limiting what a single caller can cost you.