Everything here runs in one site, in Los Angeles. For an HTTP service with readers elsewhere, a CDN is the cheapest way to cut that distance: it answers from a location near the reader, and only requests it cannot answer travel to Los Angeles. The steps below use Cloudflare.
Fixes:
Does not fix:
1. Move your DNS to the CDN, and point the public name at the VM. An A
record for your public hostname holding the VM's IPv4 (GET /v1/vms/{id}
carries it), proxied — the orange cloud icon in Cloudflare. Only a proxied
record sends traffic through the CDN; a DNS-only record does not.
2. Keep a way in that is not proxied. SSH does not go through the proxy, so decide now how you will reach port 22 — a second hostname left unproxied, or the raw address recorded somewhere. Step 4 locks you out if you have no other way in, and getting back in then takes rescue.
An unproxied record makes the VM's address public again. If that matters, keep SSH on the raw address and restrict port 22 to where you connect from.
3. Encrypt the connection from the CDN to your VM. Put a certificate on the VM — the CDN's own origin certificate is free and long-lived — and set the SSL/TLS mode to Full (strict). Flexible mode sends traffic between the CDN and your VM unencrypted across the public Internet.
4. Accept web traffic only from the CDN. We filter nothing in front of your VM, so until you do this the VM still answers anyone who finds its address. Allow inbound 80 and 443 from the CDN's published ranges only, keep 22 reachable from where you connect, default-deny the rest.
Apply it with a scheduled undo, and fetch the ranges when you apply it rather than pasting a copy that goes out of date: Harden a VM.
5. Decide what is cacheable. Static assets cache by default; HTML and API
responses do not until you say so. Set cache rules for paths that can be
cached, send Cache-Control from your own application for the rest, and check
the cf-cache-status response header — only HIT means the CDN served the
response from its cache.
6. Fix your logs. Every request now arrives from a CDN address, so
in-guest logs, rate limits and anything that blocks by IP see the proxy
instead of the reader. The real client address comes in a request
header (CF-Connecting-IP); configure your web server's real-IP module to
trust it, from the CDN ranges only.
cf-cache-status: DYNAMIC on everything. No cache rules are matching,
so every request still reaches the VM, with one extra hop on the way.