Spent a bit of time digging into this
- Org tokens - Same 3P caveat, 10.5min expiry ✗
- Deploy tokens - Same 3P caveat, 10.5min expiry ✗
- Readonly tokens - Only read access, can’t start/stop machines ✗
- Machine-exec tokens - For executing commands IN machines, not API control ✗
Macaroon authentication system Fly.io uses as i understand it:
Example simple caveat:
“This token can only be used for organization ID xxx App YYY”
“This token expires on 2027-01-20”
“This token can only read/control (rC), not create/delete”
[
{
"type": "Organization",
"body": {
"id": xxxx,
"mask": "rC"
}
},
{
"type": "Apps",
"body": {
"apps": {
"yyyy": "rC"
}
}
}
]
These are first-party caveats - restrictions checked by Fly.io themselves.
Fly.io Adds 3rd party caveats to this when you attenuate them.
Third-Party (3P) Caveats
3P caveats are restrictions that require another service to verify.
The flow:
-
You present your token to Fly.io
Token says: “I’m valid, BUT you must also prove user auth with api.fly.io/aaa”
-
Fly.io sees the 3P caveat and says: “Okay, prove you’re authenticated”
-
You must get a “discharge macaroon” from https://api.fly.io/aaa/v1
- This discharge proves you’re an authenticated user
- The discharge has a ValidityWindow (10.5 minutes)
-
You send BOTH tokens together:
Authorization: Bearer fm2_,fm2_
-
Fly.io checks:
✓ Original token is valid
✓ Discharge token is valid from the 3P service
✓ ValidityWindow hasn’t expired
→ Request allowed
How this process works.
When you run:
fly tokens create deploy -a appname -x 8760h
Fly.io creates a token that says:
- “Valid for 1 year”
- “BUT also requires 3P authentication from api.fly.io/aaa”
The 3P service (api.fly.io/aaa) is Fly.io’s authentication/authorization service. It issues discharge tokens that:
- Prove you’re an authenticated user (not stolen/leaked token)
- Expire quickly (10.5 minutes) for security
- Are meant to be refreshed by human users logging in
What’s in the Tokens
When i worked to debug the tokens, we get
Token Part 1 (from api.fly.io/v1):
{
"type": "Organization",
"body": {"id": x, "mask": "rC"} // First-party: org scoped
},
{
"type": "3P", // Third-party caveat
"body": {
"Location": "https://api.fly.io/aaa/v1", // "Check with this service"
"Ticket": "OOC5H3gf..." // Cryptographic ticket to exchange
}
}
Token Part 2 (discharge from api.fly.io/aaa/v1):
{
"type": "ValidityWindow", // THIS IS THE PROBLEM
"body": {
"not_before": 1768805891,
"not_after": 1768806521 // Expires in 10.5 minutes
}
}