Example finding How it works Coverage PENTEST METHODOLOGY DOCS PRICING FAQ MCP CONTACT LOG IN SIGN UP →

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:

  1. fly.toml is a deploy config that sets secrets handling, internal vs public services, and region pinning.
  2. 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:

  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 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 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

  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 does not provide authorization.
  9. One Fly org for personal experiments and company prod, which complicates offboarding.
  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 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.
  • 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.
  • 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 ssh with 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:

  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, 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.

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

SCAN MY FLY APP →