Skip to content

feat(deploy): one-command Fly.io launch + fix null client IPs on Fly - #58

Merged
AdamXweb merged 1 commit into
mainfrom
feat/fly-one-command-deploy
Aug 3, 2026
Merged

feat(deploy): one-command Fly.io launch + fix null client IPs on Fly#58
AdamXweb merged 1 commit into
mainfrom
feat/fly-one-command-deploy

Conversation

@adamXbot

@adamXbot adamXbot commented Aug 2, 2026

Copy link
Copy Markdown
Collaborator

Summary

bash deploy/fly-launch.sh --app my-mantis --region iad now provisions the Fly app, a Managed Postgres cluster, the secrets and the first admin key, then deploys — in one command.

Fly has no embeddable "Deploy to Fly.io" button (unlike Heroku/Render); their one-click is the dashboard Launch UI, which still needs the app and database to exist. So the closest real equivalent is one idempotent script plus an opt-in Actions workflow for zero-click redeploys.

🐛 The existing Fly example loses every client IP

docker/Dockerfile sets NODE_ENV=production, and in production mantis distrusts forwarding headers unless TRUST_PROXY_HEADERS=1. The current fly.toml.example never sets it. Verified against the actual production image:

before:  ua=IPTest/no-trust  ->  ip=None        # X-Forwarded-For ignored entirely

The container even warns about it on boot. For a tripwire service this silently discards the single field that matters, and the per-IP login limiter fails open because it has no IP to bucket on.

[env] now sets TRUST_PROXY_HEADERS=1 and pins TRUSTED_IP_HEADER=x-forwarded-for — Fly does not strip an inbound CF-Connecting-IP, and mantis tries that header first, so without the pin a client can forge its own recorded IP with one request header. All three cases verified against the image:

Scenario Recorded IP
Plain X-Forwarded-For (Fly's normal case) 203.0.113.77 real IP, was None
Attacker forges CF-Connecting-IP: 6.6.6.6 203.0.113.88 forgery ignored
Attacker prepends XFF: 1.2.3.4, <real> 203.0.113.99 real rightmost hop wins

The Cloudflare-fronted variant is documented inline: front it with a Cloudflare-proxied domain and you must switch to cf-connecting-ip, or you record Cloudflare's edge IP instead of your visitor's.

Script safety properties

  • Write-once pepper — refuses to overwrite MANTIS_API_KEY_PEPPER; rotating it invalidates every API key ever minted
  • Idempotent — re-runs skip the existing app/cluster/secrets and just redeploy
  • Secrets via stdinfly secrets import, never argv or shell history
  • Prints the admin key directly — generates BOOTSTRAP_API_KEY itself instead of scraping first-boot logs
  • --dry-run — prints every command without creating anything
  • bash 3.2 compatible — works with macOS stock bash

Postgres

Docs move to fly mpg (Managed Postgres), Fly's supported product. The older fly postgres builds an unmanaged cluster Fly support explicitly does not cover, so it is now opt-in via --db unmanaged with a warning. --db external (Neon/Supabase/your own) and --db none are also supported.

Continuous deploy

.github/workflows/fly-deploy.yml is inert until you set FLY_DEPLOY_ENABLED=true, so forks and unconfigured clones never attempt or fail a deploy. It refuses to deploy a fly.toml missing TRUST_PROXY_HEADERS, and polls /api/health for db:ok after deploying.

Verification

flyctl isn't installed on this machine, so no live Fly deploy was performed. Everything else was exercised for real:

  • Script logic against a stubbed flyctl: dry-run, real run, re-run idempotency (pepper not rotated, cluster/app skipped), and every error path
  • Generated fly.toml parses as TOML with the correct app / region / base URL and both proxy vars
  • A script-format BOOTSTRAP_API_KEY seeds and authenticates against a real mantis container on a fresh database — seeded bootstrap admin key from BOOTSTRAP_API_KEY, HTTP 200 with the key, 401 without
  • Both workflow action SHA pins verified to exist via the GitHub API (my first recalled SHA for setup-flyctl did not exist — replaced with the verified 1.6 commit)
  • Workflow YAML parses

🤖 Generated with Claude Code

Adds deploy/fly-launch.sh — provisions the app, a Managed Postgres
cluster, secrets and the first admin key, then deploys, in one command:

    bash deploy/fly-launch.sh --app my-mantis --region iad

Fly has no embeddable "Deploy to Fly.io" button (their one-click is the
dashboard Launch UI), so a single idempotent script plus an opt-in
GitHub Actions workflow is the closest real equivalent.

BUG FIX — the existing fly.toml.example loses every client IP.
docker/Dockerfile sets NODE_ENV=production, and in production mantis
distrusts forwarding headers unless TRUST_PROXY_HEADERS=1. Verified
against the production image: every hit recorded ip = null, and the
supplied X-Forwarded-For was ignored entirely. For a tripwire service
that silently discards the one field that matters, and the per-IP login
limiter fails open with nothing to bucket on. [env] now sets it.

Also pins TRUSTED_IP_HEADER=x-forwarded-for: Fly does not strip an
inbound CF-Connecting-IP and mantis tries that header first, so without
the pin a client can forge its own recorded IP with one header. Verified
all three cases against the image — normal XFF records the real IP, a
forged CF-Connecting-IP is ignored, and a prepended XFF entry loses to
the real rightmost hop. Documented the Cloudflare-fronted variant, which
needs cf-connecting-ip instead.

Script safety properties:
- write-once pepper: refuses to overwrite MANTIS_API_KEY_PEPPER, since
  rotating it invalidates every API key ever minted
- idempotent: re-runs skip existing app/cluster/secrets and just redeploy
- secrets go through `fly secrets import` (stdin), never argv or history
- generates BOOTSTRAP_API_KEY itself so the admin key is printed directly
  rather than scraped from first-boot logs
- --dry-run prints every command without touching anything
- bash 3.2 compatible (macOS stock bash)

Docs updated to `fly mpg` (Managed Postgres); the older `fly postgres`
path is unmanaged and not covered by Fly support, so it is now opt-in
via --db unmanaged with a warning.

.github/workflows/fly-deploy.yml is inert until FLY_DEPLOY_ENABLED=true,
refuses to deploy a fly.toml missing TRUST_PROXY_HEADERS, and polls
/api/health for db:ok after deploying.

Verified: script logic exercised end-to-end against a stubbed flyctl
(dry-run, real run, re-run idempotency, error paths); generated fly.toml
parses as TOML with the right app/region/base-URL; a script-format
BOOTSTRAP_API_KEY seeds and authenticates against a real mantis instance
(200 with the key, 401 without); both workflow action SHAs verified to
exist against the GitHub API.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@AdamXweb
AdamXweb merged commit d0de478 into main Aug 3, 2026
8 checks passed
@AdamXweb
AdamXweb deleted the feat/fly-one-command-deploy branch August 3, 2026 02:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants