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:

  1. Rotate the operator password after creation (fly postgres connect / managed password reset).
  2. Never embed the connection string in [env] or source — use fly secrets set.
  3. Prefer private/flycast access: <app>-db.flycast keeps traffic on 6PN.
  4. Restrict pg_hba.conf-equivalent access so only your app apps connect.
  5. 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

  1. Secrets in fly.toml [env] because the AI mirrored a local .env.
  2. Every service public — workers and Redis-like sidecars get Anycast IPs.
  3. Root containers with :latest base images.
  4. Postgres public with default password still in place after fly postgres create.
  5. No rate limits on LLM/proxy routes that bill per request.
  6. Health endpoint that dumps config “for debugging.”
  7. Shared org membership for freelancers who keep access after the contract ends.
  8. Assuming fly secrets means the app is secure — secrets management ≠ authorization.
  9. One Fly org for personal experiments and company prod — blast radius and offboarding become messy.
  10. Leaving default fly.dev hostnames weakly protected while custom domains get the real auth work.
  11. Copy-pasting fly.toml from another app including leftover public services.
  12. 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.
  • api listens on internal only; no public services block.
  • Postgres via flycast/private; no 0.0.0.0 bind for DB ports.
  • Shared secrets via fly secrets per 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 ssh audit 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:

  1. Multi-stage builds skipped — build tools and npm caches in production image.
  2. Copying .env into the image — secrets in layers forever.
  3. curl | bash installers in Dockerfile without checksums.
  4. Healthcheck that hits external paid APIs — expensive and flaky.
  5. 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.

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

SCAN MY FLY APP