How to Secure Fly.io - Security Guide
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 surfaces matter beyond standard container security:
fly.tomlis a deploy config that sets secrets handling, internal vs public services, and region pinning.- The 6PN private network only helps if you use it. By default, every service is reachable on its public Anycast IP unless you bind to the private network.
Unlike serverless functions (Vercel/Netlify), Fly gives you a real process, optional volumes, and private networking. Dockerfile and network mistakes therefore cost more: a public Redis or Postgres on Fly is a full incident.
A typical AI-assisted deploy: a Cursor session produces a Dockerfile, one command deploys it, and nobody revisits fly.toml after the first passing health check. Failures are mostly application-layer (open admin routes, leaked keys in the bundle, missing auth) plus a few 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 |
Audit 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.
2. Use Private Networking for internal services
For backend services that don’t need a public IP, omit the public [[services]] block or bind only on the private network. Apps in the same org reach each other via <app-name>.internal, and that traffic stays on 6PN.
# 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 fails health gets cycled. Add a /health endpoint that returns 200 only when the app is serving correctly (DB ping optional; keep it cheap).
Do not put secrets or verbose diagnostics on /health. It is often reachable from the public internet.
5. Configure team permissions
In Org → Members, review roles (Owner / Member) quarterly. Any member can deploy, read secrets, and SSH into machines (fly ssh console). Remove ex-team members the same day and rotate secrets after they leave.
Give contractors 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. For sensitive data (tokens, PII dumps), add client-side encryption before writing to the volume, so a snapshot restored to the wrong environment is unreadable 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 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 VM with autoscaling and no limits lets an attacker trigger expensive operations and run up your bill.
[[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
Review fly logs --app <app> weekly. For longer retention and queries, use fly logs ship to Datadog, Logtail, or S3. Track deploys, secret changes, machine restarts, and SSH sessions.
Without off-platform logs 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 the maximum machine count and regions. Unbounded autoscaling lets an attacker trigger growth you pay for.
11. Configure Fly Proxy headers
Fly Proxy adds Fly-Client-IP, Fly-Region, and similar headers. Read the client IP from them, not from the TCP peer, which is the proxy. Otherwise rate limits and access logs see the proxy IP instead of the 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 a restore once before you need it.
13. Configure monitoring
Fly Metrics dashboard shows CPU / memory / network per machine. Set alerts (via Grafana / Prometheus integration) for CPU spikes (possible cryptojacking), unusual outbound traffic (possible exfiltration), and machine restart loops (DoS or app instability).
14. Audit your Dockerfile
Check AI-generated Dockerfiles for 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, and SSH sessions appear in the audit trail. For production apps, limit SSH to break-glass access.
16. Run a security scan
The VibeEval scan probes your deployed Fly app for missing auth, BOLA, and webhook trust, wherever it’s hosted. Config hardening won’t catch an admin route with 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 does not provide authorization. - One Fly org for personal experiments and company prod, which complicates offboarding.
- 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 budgets on expensive AI routes.
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. - 6PN does not authenticate end users. Your API still validates sessions/JWTs, and unauthenticated “internal” HTTP is exposed if any public app in the org is compromised.
AI scaffolds often give every process a public HTTP service “to make health checks easy.” Use Fly’s checks against the private port instead.
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 it explicitly for regulated data.
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 sshwith audit visibility and just-in-time access over standing SSH access. - Do not leave debug listeners bound to
0.0.0.0.
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, production dependencies only, secrets 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, since fly logs is not an incident archive.
Cost-based DoS and AI routes
AI endpoints make each request expensive, and Fly makes scaling easy. Combine:
- App-level rate limits (see API abuse protection)
- Per-machine CPU/memory caps
- Bounded autoscaling
- Separate apps for cheap web and expensive workers, so abuse of one does not take down marketing pages
A route like this needs auth, a per-user rate limit, and a budget check:
app.post("/api/complete", auth, userRateLimit, budgetCheck, handler);
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 print via fly ssh. Assume anyone with deploy rights could read the values.
Automate Your Security Checks
VibeEval scans applications running on Fly.io for exposed keys in the bundle, open admin routes, missing ownership checks, and webhook trust. Paste your public Fly URL to get findings with fix prompts.
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