Fly.io — Kurt Got Got¶
Summary¶
Fly.io security postmortem (2025-10-08) disclosing that Kurt
Mackey, the CEO, was phished and the company's
@flydotio Twitter/X account was
taken over for ~15 hours. The attacker sent a plausible-looking
"Twitter alert" email, Kurt pulled credentials from
systems/1password|1Password and logged into a lookalike
domain members-x.com (a malicious proxy for x.com), and the
attacker immediately rotated the email-of-record to
achilles19969@gmail.com, revoked tokens, and set up fresh 2FA
— locking Fly.io out until X.com's recovery process completed
("15 hours for a 2FA reset isn't outside industry norms"). The
attacker posted a low-effort crypto-airdrop scam and eventually
deleted the account's history. Nothing in Fly.io's
infrastructure was at risk because everything internal — logs,
Stripe billing, infra metrics — is gated behind Google SSO with
phishing-resistant MFA; Twitter was excluded from that regime
because it had been treated as a deprioritised legacy shared
account after the 2023-2024 "might not be a long-term thing for
us at all" window during which Fly.io decamped to Mastodon and
Bluesky. The post is more a policy narrative than a technical
deep-dive, but it canonicalises three reusable architectural
claims: (1) phishing-training doesn't work at scale — "under
continued pressure, everybody clicks" ([people.cs.uchicago.edu
phishing-training paper]);
(2) the fix is phishing-resistant authentication via
FIDO2/WebAuthn/Passkeys whose "origin- and channel-binding"
breaks the malicious-proxy flow that phishing relies on; (3) the
operational pattern is "everything behind an IdP that requires
phishing-proof MFA" (Fly.io uses Google as IdP), which reduces
the authentication-attack surface to "whatever you forgot to put
behind the IdP" — which in Fly.io's case was a legacy social
account. A secondary hygiene datum: 1Password's browser plugin
would have refused to autofill on members-x.com because the
URL didn't match the saved x.com entry — manual credential
copy-paste defeated that guardrail. Incident-response timing is
documented: Fly.io knew within 45 seconds (because the ATO
simultaneously emailed all account-email-recipients a change-of-
email notice), immediately audited 1Password access logs, and
revoked access for anyone who had recently pulled the credentials
on the worst-case assumption that an endpoint had been owned. The
post concludes that blast-radius was limited enough
("not-very-plausible crypto scam that presumably generated $0")
that Fly.io ran a watchful-waiting response rather than
escalating — canonical instance of
patterns/incident-response-calibrated-to-blast-radius.
Twitter is now moved behind Passkeys. Tier-3 source; inclusion is
marginal but the article explicitly articulates the origin-
binding mechanism of FIDO2 and the "everything-behind-an-IdP"
pattern, and serves as a real production anecdote for the
phishing-resistant-MFA concept.
Key takeaways¶
-
Twitter ATO via phishing; Fly.io infra not at risk. "Our Twitter got owned. We knew within moments of it happening. We know exactly how it happened. Nothing was at risk other than our Twitter account (and one Fly.io employee's self-esteem)." The attack is scoped to the one account that sat outside Fly.io's IdP-gated regime. Canonical instance of account takeover as a phishing outcome (Source: sources/2025-10-08-flyio-kurt-got-got).
-
Phishing-training alone doesn't work — the fix is phishing-resistant authentication. "Contrary to one popular opinion, you don't defeat phishing by training people not to click on things. I mean, tell them not to, sure! But eventually, under continued pressure, everybody clicks. There's science on this. [linked to Ho et al. OAKLAND 2025 phishing training paper] The cool kids haven't done phishing simulation training in years. What you're supposed to do instead is use phishing-resistant authentication." Canonical statement of the training-is-not-sufficient claim on the wiki (Source: sources/2025-10-08-flyio-kurt-got-got; concepts/phishing-resistant-authentication).
-
Phishing-resistant auth = origin + channel binding. "Phishing-resistant authentication works by mutual authentication (or, if you're a stickler, by origin- and channel-binding). Phishes are malicious proxies for credentials. Modern MFA schemes like FIDO2 break that proxy flow; your browser won't send real credentials to the fake site." The mechanism claim on the wiki: origin binding is the load-bearing property that defeats reverse-proxy phishing kits (Source: sources/2025-10-08-flyio-kurt-got-got; concepts/phishing-resistant-authentication).
-
"Everything behind an IdP with phishing-proof MFA" is the operational pattern. "This is, in fact, how all of our infrastructure is secured at Fly.io; specifically, we get everything behind an IdP (in our case: Google's) and have it require phishing-proof MFA. You're unlikely to phish your way to viewing logs here, or to refunding a customer bill at Stripe, or to viewing infra metrics, because all these things require an SSO login through Google." Canonical instance of patterns/phishing-resistant-mfa-behind-idp: the discipline is not "every app implements FIDO2" but "every app is downstream of a single IdP that implements FIDO2" — which both reduces the integration surface (N×1 instead of N×M) and gives you a single chokepoint for off-boarding, auditing, and policy (Source: sources/2025-10-08-flyio-kurt-got-got; concepts/sso-authentication).
-
Twitter fell outside the IdP regime because it was a legacy social account, not infra. "Twitter had been a sort of legacy shared account for us, with credentials managed in 1Password and shared with our zoomer contractor†." The account's deprioritisation was deliberate — in 2023-2024 Fly.io "decamped for Mastodon" and "later to Bluesky" and "there was a window of time… where it looked as if Twitter might not be a long term thing for us at all." Canonical instance of concepts/legacy-shared-account on the wiki: accounts that get demoted below the IdP threshold because the platform is deprioritised become the attacker's entry point when the platform unexpectedly regains relevance (Source: sources/2025-10-08-flyio-kurt-got-got).
-
Password manager as a phishing guardrail — defeated by manual copy-paste. "The 1Password browser plugin would have noticed that 'members-x.com' wasn't an 'x.com' host." Kurt pulled the credential manually rather than autofilling, which bypassed the domain-matching check. Canonical instance of the patterns/password-manager-as-phishing-guardrail pattern (and its failure mode: manual copy-paste) on the wiki. The domain-matching behaviour of the password-manager browser plugin is a poor-man's origin check — weaker than FIDO2's cryptographic origin binding, but free if the credential is only ever autofilled (Source: sources/2025-10-08-flyio-kurt-got-got).
-
Incident detection in ~45 seconds via a side-channel email notification. "We knew our X.com account had suffered an ATO because a bunch of us simultaneously got another email saying that the @flydotio account's email address now pointed to achilles19969@gmail.com. Our immediate response was to audit all accesses to the login information in 1Password, to cut all access for anybody who'd recently pulled it; your worst-case assumption in a situation like this is that someone's endpoint has been owned up." The email-of-record- change notification that the platform sent to every account- email was the detection signal, not the ATO itself. Response playbook: audit the credential store access log + revoke recent pullers on worst-case-endpoint-compromise assumption. Canonical first-party incident-detection-and-triage datum for ATO on the wiki (Source: sources/2025-10-08-flyio-kurt-got-got).
-
Recovery time was platform-gated (~15 hours), not company- gated. "The attacker immediately revoked all tokens and set up new 2FA, so while we were quickly able to reset our password, we couldn't lock them out of our account without an intervention from X.com, which took something like 15 hours to set up. (That's not a knock on X.com; 15 hours for a 2FA reset isn't outside industry norms.)" Operational datum: attacker-set 2FA + attacker-set email → victim cannot self-recover, the recovery path routes through the platform's human-reviewed support channel, and industry-normal latency is ~O(10+ hours) for that path (Source: sources/2025-10-08-flyio-kurt-got-got).
-
Watchful-waiting response because blast-radius was bounded. "In the grand scheme of things, the attack was pretty chill: a not-very-plausible crypto scam that presumably generated $0 for the attackers, 15+ hours of brand damage, and extra security engineering cycles burnt on watchful waiting. Our users weren't under attack, and the account wasn't being used to further intercept customer accounts." Canonical instance of patterns/incident-response-calibrated-to-blast-radius: the response intensity is scaled to the scope of what's actually at risk, not to the conspicuousness of the attack (Source: sources/2025-10-08-flyio-kurt-got-got).
-
"x.com" is a structurally phishable domain. "Kurt complains that 'x.com' is an extremely phishable domain." Short, generic, one-character-different-from-lots-of-things, not discriminable-by-eye at a glance — the domain itself makes origin-discrimination harder for humans (which is why machine-enforced origin binding matters more when human origin-discrimination is degraded) (Source: sources/2025-10-08-flyio-kurt-got-got).
Systems extracted¶
| System | Role in this post |
|---|---|
| systems/1password | Fly.io's shared-credential store; the attacker's victim (but only because the credential was pulled manually and copied into a lookalike domain — autofill would have blocked) |
| systems/twitter-x | The compromised platform; 2FA-reset latency ~15 hours |
| systems/google-workspace-sso | Fly.io's IdP for all internal infra; requires phishing-proof MFA |
| systems/okta | Named as the exemplar category of enterprise IdP (Fly.io uses Google specifically; Okta is adjacent) |
| systems/stripe | Named as the kind of downstream app behind Google SSO — "to refund a customer bill at Stripe… requires an SSO login through Google" |
Concepts extracted¶
| Concept | Claim canonicalised |
|---|---|
| concepts/phishing-resistant-authentication | Defined as mutual-auth / origin-and-channel binding; contrasted with password+OTP which is phishable via reverse-proxy kits; training is not a substitute |
| concepts/account-takeover-ato | Defined: attacker gains control of an account, typically by combining phishing + immediate 2FA/email rotation to lock out the legitimate owner; recovery is platform-gated |
| concepts/origin-bound-credential | Mechanism: credential is cryptographically bound to the origin it was registered against; browser refuses to sign challenges for the wrong origin; breaks the malicious-proxy pattern |
| concepts/legacy-shared-account | Pattern: accounts on platforms that were deprioritised (or for which the company considered leaving) become deprioritised in security posture and are the entry point when the platform unexpectedly regains relevance |
| concepts/fido2-webauthn | Named as the canonical phishing-resistant-MFA primitive — stub if not present |
| concepts/passkey-authentication | The Fly.io fix at the end: "our Twitter access is Passkeys now" — second canonical wiki instance |
| concepts/sso-authentication | Fly.io's Google SSO is the chokepoint; canonical instance of "everything behind an IdP that requires phishing-proof MFA" |
Patterns extracted¶
| Pattern | What it names |
|---|---|
| patterns/phishing-resistant-mfa-behind-idp | Everything internal is downstream of one IdP that enforces FIDO2/WebAuthn MFA; reduces integration from N×M (apps×authenticators) to N×1; single chokepoint for off-boarding and audit |
| patterns/password-manager-as-phishing-guardrail | The browser-plugin's domain-matching check is a free origin-check — defeats autofill phishing. Failure mode: manual copy-paste bypasses it |
| patterns/incident-response-calibrated-to-blast-radius | Scale response to what's actually at risk, not to the conspicuousness of the attack. Fly.io ran watchful-waiting because customer accounts weren't targeted and the misuse was low-leverage |
Operational numbers & timeline¶
| Metric | Value | Note |
|---|---|---|
| Detection latency | ~45 seconds | via email-of-record-change side channel |
| Password reset | seconds | self-serve, but moot because attacker rotated 2FA |
| Account recovery | ~15 hours | platform-gated (X.com support) |
| Attacker revenue (reported) | ~$0 | low-plausibility crypto airdrop scam |
| Brand-damage window | 15+ hours | |
| 1Password-sharing scope | employees + 1 contractor | shared legacy-social credential store |
| Fly.io Twitter status afterward | Passkeys enforced |
Caveats¶
- Self-reported incident narrative. No external attestation.
- Short post, light on technical detail. The architectural claims are policy-level ("everything behind an IdP", "use FIDO2"), not implementation-level. No code, no diagrams, no specific IdP configuration shown.
- Tier-3 source + first-party postmortem — inclusion is justified by the clean articulation of the origin-binding mechanism and the explicit naming of the "everything-behind-an-IdP" pattern, both of which are reusable across any org's security posture. Borderline case; notes flagged.
- Phishing-training-doesn't-work is a strong-form claim cited to a single paper (Ho et al., OAKLAND 2025). Related literature is more nuanced — training measurably reduces click-through rates in the short term, but the effect decays, and the absolute rate is never zero. Fly.io's framing is directionally right but worth pairing with the phishing-training literature if citing.
- "Everything behind an IdP" is a governance claim, not an implementation. The attack surface becomes "whatever you forgot to put behind the IdP", which is exactly what bit Fly.io. The pattern's weak point is coverage enforcement — the legacy-shared-account failure mode is the generalisable lesson.
- Twitter/X.com's 15-hour recovery latency is characterised as "not outside industry norms" by Fly.io; no independent benchmark is given. Treat as anecdotal.
- 1Password's browser-plugin-as-phishing-guardrail is a secondary control, not primary. It relies on the user autofilling rather than copy-pasting. FIDO2 on the authenticator side is the primary control.
Source¶
- Original: https://fly.io/blog/kurt-got-got/
- Raw markdown:
raw/flyio/2025-10-08-kurt-got-got-f7e0184d.md
Related¶
- companies/flyio — Fly.io company page.
- concepts/phishing-resistant-authentication — the mechanism claim canonicalised.
- concepts/account-takeover-ato — the incident shape.
- concepts/origin-bound-credential — the underlying cryptographic property.
- concepts/legacy-shared-account — the coverage-gap failure mode.
- concepts/passkey-authentication — the Fly.io fix.
- concepts/sso-authentication — the IdP chokepoint.
- patterns/phishing-resistant-mfa-behind-idp
- patterns/password-manager-as-phishing-guardrail
- patterns/incident-response-calibrated-to-blast-radius
- systems/1password
- systems/twitter-x
- systems/google-workspace-sso
- systems/okta