HOW TO SECURE FLY.IO - SECURITY GUIDE | VIBEEVAL
Fly.io gives you micro-VMs and a private network — but secrets in fly.toml, public service bindings, and open app surfaces still ship. Harden the deploy config, then scan the live app.
SCAN YOUR FLY.IO APP NOW
Enter your public Fly app URL — we check for exposed keys, open admin routes, and auth gaps that survive a clean fly.toml.
Fly.io Security Context
Fly.io runs your container in micro-VMs across global regions, with a private 6PN WireGuard network connecting your apps. Two distinct surfaces beyond standard container security: (1) your fly.toml is a deploy config that may include settings (or exclude them) that affect security — secrets handling, internal vs public services, region pinning; (2) the 6PN private network needs to actually be used — by default, every service is reachable on its public Anycast IP unless you explicitly bind to the private network.
Compared to pure serverless (Vercel/Netlify functions), Fly gives you a real process, optional volumes, and first-class private networking — closer to “small VPS done right.” That power means Dockerfile and network mistakes hurt more: a public Redis or Postgres on Fly is a full incident, not a theoretical concern. AI tools that emit fly launch defaults without a second pass are counting on you to notice.
AI-generated apps land on Fly more often than teams expect: a Cursor session produces a Dockerfile, a one-liner deploys it, and nobody revisits fly.toml after the first successful health check. The platform is solid. The failure modes are almost always application-layer (open admin routes, leaked keys in the bundle, missing auth) plus a small set of Fly-specific misconfigurations (secrets in [env], public Postgres proxy, root containers). This guide covers both.
What you own vs what Fly owns
| Layer | Fly owns | You own |
|---|---|---|
| TLS at the edge | Cert issuance, termination | Custom domains, cert verification |
| Network | 6PN, Anycast, WireGuard | Which services bind public vs private |
| Secrets storage | Encryption at rest for fly secrets |
Not putting secrets in git / [env] |
| Runtime isolation | Firecracker micro-VMs | App auth, authorization, input validation |
| Volumes | Encryption at rest | Snapshots, restore tests, client-side encryption |
| Logs | Short retention stream | Shipping, retention, alerting |
Treat Fly as a strong host. Put your audit effort into secrets, networking, the Dockerfile, and the running app.
Security Checklist
1. Use fly secrets, never [env] in fly.toml
fly secrets set DATABASE_URL=postgres://... stores the value encrypted at rest, only decrypted into the container’s env at boot. The [env] table in fly.toml ends up in your git repo — anything in it is in commit history forever. Never put a secret in [env].
AI-generated mistake: the model pastes a working .env into fly.toml under [env] so “deploy just works.” It works — and the Stripe live key is now in every clone of the repo.
# Correct
fly secrets set DATABASE_URL="$DATABASE_URL" STRIPE_SECRET_KEY="$STRIPE_SECRET_KEY"
fly secrets list # names only — values never printed
# Wrong — do not commit this
# [env]
# STRIPE_SECRET_KEY = "sk_live_..."
If a secret was ever in [env] or a committed file, rotate it. Bots scrape public repos continuously.
2. Use Private Networking for internal services
For backend services that don’t need a public IP: fly.toml [[services]] with no internal_port exposed publicly, or bind only on the private network. Communicate between apps in the same org via <app-name>.internal — traffic stays on the 6PN network and never touches the public internet.
# Worker / queue / internal API — no public services block
# Other apps reach it at: http://my-worker.internal:8080
# Public web app — explicit HTTP service
[http_service]
internal_port = 8080
force_https = true
auto_stop_machines = "stop"
auto_start_machines = true
min_machines_running = 1
How to verify: fly ips list -a my-worker should show no public IPv4/IPv6 for pure internal services. From another app: curl http://my-worker.internal:8080/health.
3. Enable TLS for all public services
Fly.io provisions TLS certs automatically when you fly certs add yourdomain.com. Verify with fly certs show yourdomain.com — confirm the cert is issued and the chain is complete. For Anycast addresses (<app>.fly.dev), TLS is on by default.
Always set force_https = true under [http_service] so plain HTTP never serves application traffic. For custom domains, add CAA DNS records at your registrar limiting issuance to Let’s Encrypt (or your CA of choice).
4. Configure health checks
In fly.toml:
[[services.http_checks]]
interval = "10s"
timeout = "2s"
grace_period = "5s"
method = "get"
path = "/health"
Health checks aren’t security on their own, but a service that doesn’t pass health gets cycled — limiting the lifespan of a compromised process. Add a /health endpoint that returns 200 only when the app is actually serving traffic correctly (DB ping optional; keep it cheap).
Do not put secrets or verbose diagnostics on /health — it is often polled from the public internet.
5. Configure team permissions
In Org → Members: review quarterly. Owner / Member roles. Anyone with member access can deploy, read secrets, and SSH into machines (fly ssh console). Remove ex-team members same-day; rotate secrets after departure.
For contractors: create a short-lived org membership, never share a personal API token, and revoke access when the engagement ends.
6. Encrypt persistent volumes
Fly Volumes are encrypted at rest by default. Verify with fly volumes list and check encryption status. For more sensitive data (tokens, PII dumps), layer additional client-side encryption before writing to the volume so a snapshot restore to the wrong environment is still useless without the app key.
7. Configure Postgres security on Fly Postgres
If using fly postgres create:
- Rotate the operator password after creation (
fly postgres connect/ managed password reset). - Never embed the connection string in
[env]or source — usefly secrets set. - Prefer private/flycast access:
<app>-db.flycastkeeps traffic on 6PN. - Restrict
pg_hba.conf-equivalent access so only your app apps connect. - Enable automated backups and test restore once before you need it.
# Attach Postgres as a secret reference rather than pasting the URL into git
fly postgres attach <postgres-app> -a <web-app>
AI scaffolds often print the full DATABASE_URL in deploy logs. Confirm logs are not shipping that string to a public channel.
8. Set machine resource limits
In fly.toml [[vm]]: set explicit memory and cpu per VM. A misconfigured VM with autoscaling and no limits is a DoS amplifier — an attacker triggers expensive operations until the bill becomes a problem.
[[vm]]
size = "shared-cpu-1x"
memory = "512mb"
cpus = 1
Pair this with application-level rate limits on expensive routes (LLM, image processing, export jobs).
9. Enable audit logging
fly logs --app <app>: review weekly. For longer retention and queryability: fly logs ship to your log destination (Datadog, Logtail, or S3). Track: deploys, secret changes, machine restarts, SSH sessions.
Without off-platform log retention you cannot answer “who rotated this secret last Tuesday” after an incident.
10. Configure auto-scaling carefully
In fly.toml [http_service]: set min_machines_running (cost floor) and bound max machines via machine count / regions. Don’t auto-scale unbounded — an attacker can trigger growth and you pay.
11. Configure Fly Proxy headers
Fly Proxy adds Fly-Client-IP, Fly-Region, etc. — your application should read the client IP from these (not the TCP-level peer, which is the proxy). Without this, rate limits and access logs see the proxy IP, not the actual user.
// Prefer platform-provided client IP
const ip = req.headers["fly-client-ip"] || req.headers["x-forwarded-for"]?.split(",")[0];
Never trust a client-supplied X-Forwarded-For alone when deciding admin access.
12. Configure backups for volumes
fly volumes snapshot create <volume-id> for one-shot snapshots; for production, automate with a cron job that snapshots daily and prunes old snapshots. Snapshots are encrypted at rest. Test restore once before you need it — untested backups are fiction.
13. Configure monitoring
Fly Metrics dashboard shows CPU / memory / network per machine. Set alerts (via Grafana / Prometheus integration) for: CPU spikes (could be cryptojacking after a compromise), unusual outbound traffic (data exfiltration), machine restart loops (DoS or app instability).
14. Audit your Dockerfile
AI-generated Dockerfiles often skip basic hygiene:
# Prefer non-root, pinned base, minimal surface
FROM node:22-bookworm-slim@sha256:<digest>
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev
COPY . .
USER node
EXPOSE 8080
CMD ["node", "server.js"]
Audit for: running as root (use USER nonroot / node), exposing unnecessary ports (EXPOSE only what you serve), pulling base images with :latest tag (pin digests), ADD from URLs (use COPY from local files), secrets baked via ENV or ARG that leak into image layers.
Scan images in CI (trivy image, grype) before fly deploy.
15. SSH access controls
fly ssh console requires WireGuard or a configured SSH key. Confirm: WireGuard tokens are scoped per developer, SSH keys are not shared, ssh sessions are logged via the audit trail. Disable or tightly control SSH for production apps where break-glass access is enough.
16. Run a security scan
The full VibeEval scan probes your deployed Fly app for missing auth, BOLA, and webhook trust — independent of where it’s hosted. Config hardening does not catch “the admin route has no session check.”
Common mistakes in AI-generated Fly apps
- Secrets in
fly.toml[env]because the AI mirrored a local.env. - Every service public — workers and Redis-like sidecars get Anycast IPs.
- Root containers with
:latestbase images. - Postgres public with default password still in place after
fly postgres create. - No rate limits on LLM/proxy routes that bill per request.
- Health endpoint that dumps config “for debugging.”
- Shared org membership for freelancers who keep access after the contract ends.
- Assuming
fly secretsmeans the app is secure — secrets management ≠ authorization. - One Fly org for personal experiments and company prod — blast radius and offboarding become messy.
- Leaving default fly.dev hostnames weakly protected while custom domains get the real auth work.
- Copy-pasting fly.toml from another app including leftover public services.
- No rate limits or budgets on expensive AI routes (security and finance incident).
Incident vignette (composite)
A team deployed an AI chatbot on Fly with the OpenAI key in [env]. The repo was briefly public during an open-source experiment. The key was scraped, the model bill spiked, and Fly itself was fine — config hygiene was not. Rotation, fly secrets, private repo, and rate limits closed the loop. Platform uptime never entered the story.
Multi-app architectures on Fly (6PN)
A common AI-generated “microservice” layout on Fly:
web (public) ──6PN──► api (private) ──6PN──► postgres / redis (private)
Harden it:
- Only
web(or a dedicated edge proxy) has public IPs. apilistens on internal only; no public services block.- Postgres via flycast/private; no
0.0.0.0bind for DB ports. - Shared secrets via
fly secretsper app — do not reuse one god token across every machine if you can scope. - Mutual trust on 6PN is not authentication of end users — your API still validates sessions/JWTs. 6PN only keeps traffic off the public Internet.
AI scaffolds often give every process a public HTTP service “to make health checks easy.” Prefer internal health checks or Fly’s checks against the private port without Anycast.
fly.toml security-sensitive knobs
| Knob | Safer choice | Notes |
|---|---|---|
force_https |
true |
Always for public HTTP |
[env] |
Non-secrets only | NODE_ENV, feature flags without credentials |
min_machines_running |
≥1 for stateful primary if needed | Balance cost vs cold-start auth races |
auto_stop_machines |
OK for stateless | Ensure deploys and migrations still work |
processes / multiple groups |
Split web vs worker | Workers stay private |
| Release command | Migrate with care | Migrations need secrets; logs must not print them |
Region choice affects latency and data residency — pick explicitly for regulated data rather than defaulting without thought.
WireGuard, fly ssh, and operator access
Operators use fly wireguard and fly ssh console for break-glass debugging. Treat that path as privileged:
- Personal access tokens: short-lived where possible; never commit
~/.fly/config.yml. - Remove WireGuard peers for departed contractors.
- Prefer
fly sshaudit visibility + just-in-time access over permanent open SSH culture. - Do not leave reverse shells or debug listeners bound to
0.0.0.0“for a minute.”
If a laptop with Fly tokens is stolen, rotate org tokens and review recent deploys and secret changes immediately.
AI-generated Docker anti-patterns on Fly
Beyond root and :latest:
- Multi-stage builds skipped — build tools and npm caches in production image.
- Copying
.envinto the image — secrets in layers forever. curl | bashinstallers in Dockerfile without checksums.- Healthcheck that hits external paid APIs — expensive and flaky.
- Single process supervising many roles without restart isolation.
Minimal production pattern: multi-stage build, non-root user, pinned digest, only production deps, secrets only at runtime via fly secrets.
Observability: what to alert on
| Signal | Why it matters |
|---|---|
| Sudden egress bandwidth | Exfil or crypto mining |
| CPU pegged on all machines | Mining / tight loop exploit |
| Auth failure spikes | Credential stuffing |
| Deploy frequency anomaly | Compromised CI token |
| Secret set events | Confirm authorized |
| Restart loops | Bad release or attack-induced crash |
Ship logs off Fly. Point-in-time fly logs is not an incident archive.
Cost-based DoS and AI routes
Fly makes scale easy; AI endpoints make each request expensive. Combine:
- App-level rate limits (see API abuse protection)
- Per-machine CPU/memory caps
- Bounded autoscaling
- Separate apps for “cheap web” vs “expensive worker” so abuse of one does not take down marketing pages
A missing rate limit on /api/complete is both a security and a finance incident.
How to verify a hardened Fly deploy
# 1. No secrets in git-tracked config
git grep -nE 'sk_live_|postgres://|SECRET|API_KEY' -- fly.toml Dockerfile
# 2. Public vs private networking
fly ips list -a <app>
fly services list -a <app>
# 3. Secrets present by name only
fly secrets list -a <app>
# 4. Certs valid
fly certs show yourdomain.com -a <app>
# 5. Image hygiene
# trivy image registry.fly.io/<app>:<tag>
# 6. Live app surface
# Run VibeEval / Token Leak Checker / Security Headers Checker on https://your-app.fly.dev
Manual BOLA check: create two user accounts, swap resource IDs in API calls, confirm 401/403. Fly cannot do this for you.
Smoke-test internal networking
From a one-off machine in the same org:
fly ssh console -a web
# curl -sS http://api.internal:8080/health
Confirm workers without public IPs are reachable only this way.
Related Resources
Free Self-Audit Suite
Five free scanners.
Vibe Coding Security Risk Guide
Full risk catalogue.
PostgreSQL Guide
Secure your Fly Postgres deployment.
Is Railway Safe?
Compare Fly’s isolation model to Railway’s container defaults.
Vibe Code Scanner
Live-app scan for auth, keys, and open surfaces after deploy.
Reference fly.toml for a public web + private worker
app = "acme-web"
primary_region = "iad"
[http_service]
internal_port = 8080
force_https = true
auto_stop_machines = "stop"
auto_start_machines = true
min_machines_running = 1
[[vm]]
size = "shared-cpu-1x"
memory = "512mb"
[env]
NODE_ENV = "production"
Worker app: omit public http_service or bind only on 6PN. Reach at http://acme-worker.internal:8080.
Secrets rotation runbook
fly secrets set STRIPE_SECRET_KEY=sk_live_new -a acme-web
fly deploy -a acme-web
After contractor offboarding: rotate all org tokens, WireGuard peers, and app secrets they could fly ssh to print. Assume values were readable by anyone with deploy rights.
Postgres on Fly: private-only posture
fly postgres attach acme-db -a acme-web
fly ips list -a acme-db # no surprise public IPs
Rotate operator password post-create. Never paste DATABASE_URL into Issues or chat. AI deploy logs that echo connection strings require immediate rotation.
Image supply chain
FROM node:22-bookworm-slim@sha256:REPLACE_WITH_DIGEST
WORKDIR /app
COPY package*.json ./
RUN npm ci --omit=dev
COPY . .
USER node
EXPOSE 8080
CMD ["node", "server.js"]
CI: trivy image / grype before fly deploy. Never COPY .env. Never curl | bash without checksums.
6PN trust vs end-user auth
Private networking stops internet strangers from TCP-connecting to Redis. It does not authenticate browser users. Your API still validates sessions/JWTs. Treat “internal” HTTP without auth as hostile once any public app in the org is compromised.
AI route cost controls on Fly
app.post("/api/complete", auth, userRateLimit, budgetCheck, handler);
Cap machines, bound autoscaling, split cheap web from expensive workers. A public /api/complete without limits is a finance incident.
SSH and WireGuard hygiene
- Personal access tokens not committed
- Remove peers for departed contractors same day
- Prefer break-glass over standing SSH culture on prod
Stolen laptop with Fly tokens: rotate org tokens, review recent deploys and secret set events immediately.
Verify hardened deploy
git grep -nE 'sk_live_|postgres://|SECRET|API_KEY' -- fly.toml Dockerfile || true
fly ips list -a acme-web
fly ips list -a acme-worker
fly secrets list -a acme-web
fly certs show example.com -a acme-web
Then live scan the public hostname for app-layer holes Fly cannot see.
Automate Your Security Checks
VibeEval scans applications running on Fly.io for the categories above plus the long tail — exposed keys in the bundle, open admin routes, missing ownership checks, and webhook trust. Paste your public Fly URL and get findings with fix prompts in under a minute.
SCAN THE APP, NOT JUST FLY.TOML
Config hardening is half the job. VibeEval probes the running Fly app for the AI-generated gaps fly secrets cannot fix.
14-day free trial · No credit card · Cancel anytime