Federated identity
A Fly Service often needs a resource on another cloud: an S3 bucket, a Pub/Sub topic. Its Machines can prove which App they belong to, so AWS and GCP can trust them directly. There is no access key or service account key to create, store or rotate.
You bind the resource the same way you would on a Lambda or Cloud Run. Federation is what Alchemy picks for a Fly Service.
Machines sign their own identity
Section titled “Machines sign their own identity”Every Machine serves part of the Machines API on a Unix socket,
/.fly/api, already authenticated as that Machine. A process on it can
ask for an OIDC token without holding any secret:
curl --unix-socket /.fly/api -X POST http://localhost/v1/tokens/oidc \ -d '{"aud": "sts.amazonaws.com"}'The token is signed by your organization’s issuer,
https://oidc.fly.io/<org>, and names the Machine in its subject:
{ "iss": "https://oidc.fly.io/my-org", "aud": "sts.amazonaws.com", "sub": "my-org:api-app:3d8d9e1a2b3c4d", "app_name": "api-app", "exp": 1762300800}A token lasts 10 minutes. The socket answers only root, the default user
in the images Alchemy builds. A Service whose image switches to another
user fails its first cross-cloud call with OidcTokenError.
Call AWS
Section titled “Call AWS”Bind an S3 bucket to a Service:
import * as AWS from "@alchemy.run/aws";import * as Fly from "@alchemy.run/fly";import * as Effect from "effect/Effect";import * as HttpServerResponse from "effect/http/HttpServerResponse";
export const Uploads = AWS.S3.Bucket("Uploads");
export default class Api extends Fly.Service<Api>()( "Api", { main: import.meta.url, port: 3000 }, Effect.gen(function* () { const putObject = yield* AWS.S3.PutObject(yield* Uploads); return { fetch: putObject({ Key: "hello.txt", Body: "hello" }).pipe( Effect.as(HttpServerResponse.text("stored")), Effect.orDie, ), }; }).pipe(Effect.provide(AWS.S3.PutObjectHttp)),) {}The binding registers your organization’s issuer as an IAM OIDC
provider, and creates a role that holds s3:PutObject on the bucket.
The role trusts tokens whose subject starts with the Service’s App, so
every Machine of the App can assume it and no other App can:
{ "Effect": "Allow", "Principal": { "Federated": "arn:aws:iam::123456789012:oidc-provider/oidc.fly.io/my-org" }, "Action": "sts:AssumeRoleWithWebIdentity", "Condition": { "StringEquals": { "oidc.fly.io/my-org:aud": "sts.amazonaws.com" }, "StringLike": { "oidc.fly.io/my-org:sub": "my-org:api-app:*" } }}At runtime the Machine mints a token and trades it for the role’s credentials:
One OIDC provider per organization
Section titled “One OIDC provider per organization”IAM allows one OIDC provider per issuer URL in an account. Fly’s issuer is per organization, so every stack that binds a Fly Service in the same account and organization shares one provider. The provider only says that Fly’s signatures are trusted; each role’s condition decides which App may use it.
Destroying a stack leaves the provider in place, because other stacks may still use it. Delete it yourself once no stack needs it:
aws iam delete-open-id-connect-provider \ --open-id-connect-provider-arn arn:aws:iam::123456789012:oidc-provider/oidc.fly.io/my-orgCall GCP
Section titled “Call GCP”Bind a Pub/Sub topic the same way:
import * as Fly from "@alchemy.run/fly";import * as GCP from "@alchemy.run/gcp";import * as Effect from "effect/Effect";import * as HttpServerResponse from "effect/http/HttpServerResponse";
export const Events = GCP.PubSub.Topic("Events", {});
export default class Api extends Fly.Service<Api>()( "Api", { main: import.meta.url, port: 3000 }, Effect.gen(function* () { const publish = yield* GCP.PubSub.Publish(yield* Events); return { fetch: publish({ body: { messages: [{ data: btoa("hello") }] } }).pipe( Effect.as(HttpServerResponse.text("published")), Effect.orDie, ), }; }).pipe(Effect.provide(GCP.PubSub.PublishHttp)),) {}The binding creates a service account that holds the publisher role on the topic. The stack gets a workload identity pool whose provider trusts your organization’s issuer and maps each token to its App. The service account lets that one App impersonate it:
principalSet://iam.googleapis.com/projects/123456789/locations/global/workloadIdentityPools/<pool>/attribute.fly_app/api-appAt runtime the Machine mints a token addressed to the pool’s provider, trades it at Google STS for a federated token, and trades that for the service account’s access token, which lasts an hour.
A pool isn’t tied to an issuer, so each stack has its own and destroys it with the stack.
Calls between Services
Section titled “Calls between Services”A Service calling another Fly Service needs no token either. Fly’s
proxy signs every private request with a Fly-Src header naming the
calling App, and a Service answers only the Apps that bind it. See
Connect Services.
Sprites hold a key
Section titled “Sprites hold a key”A Sprite is not a Machine and has no
/.fly/api socket, so it can’t mint tokens. A Sprite that binds an AWS
or GCP resource gets a key for an identity that may do only what that
Sprite needs, deployed as a secret. The binding code is the same.
New grants take a few minutes
Section titled “New grants take a few minutes”AWS usually accepts a new role’s trust within seconds. A GCP grant to a federated principal can take a few minutes to land, and until it does calls fail with a permission error. Retry the first call after a deploy.
Where next
Section titled “Where next”- Identity Federation — how federation works across every cloud Alchemy supports.
- AWS federated identity and GCP federated identity — the same trust from the target cloud’s side.
- Connect Services — Services that call each other over Fly’s private network.