Cloud access (OIDC)

A workflow run can prove its identity to your own cloud and receive short-lived credentials in return, with no AWS, GCP, or Azure keys stored anywhere. The same pattern GitHub Actions uses to deploy without secrets: your cloud trusts Boardwalk's OIDC issuer, and each run mints a signed id-token that says exactly which org, workflow, and run is asking.

How it works

  1. The workflow declares permissions.id_token: "write" in its manifest.
  2. The program calls runtime.idToken(audience), which mints a signed JWT (RS256, 15-minute expiry) asserting the run's identity.
  3. The program exchanges that JWT at your cloud's federation endpoint (AWS AssumeRoleWithWebIdentity, GCP workload identity, Azure federated credentials) for short-lived credentials scoped to a role you control.

The token is minted fresh on every call and is redacted from everything the model sees, like a secret. Your cloud verifies it against the issuer's public keys; Boardwalk never holds your cloud credentials, and you never store them.

Grant the permission

export const meta = {
  slug: "s3-report",
  title: "S3 Report",
  triggers: [{ kind: "cron", expr: "0 7 * * 1" }],
  permissions: { id_token: "write" },
};

const jwt = await runtime.idToken("sts.amazonaws.com");

Without the grant, runtime.idToken() fails with an error naming the missing permission. The audience is the party you intend to present the token to; for AWS it is sts.amazonaws.com.

Set up AWS

One-time, in your AWS account: create an IAM OIDC identity provider for the Boardwalk issuer, then a role whose trust policy accepts tokens from it.

aws iam create-open-id-connect-provider \
  --url https://oidc.boardwalk.sh \
  --client-id-list sts.amazonaws.com

The role's trust policy pins which runs may assume it. IAM can match on aud and sub; the sub claim is org:<org id>:workflow:<workflow id>:run:<run id>, so pin your org (or a single workflow) with a pattern. Pin the stable ids, never display names: boardwalk whoami prints your org id, and boardwalk workflows list --json prints workflow ids.

{
  "Version": "2012-10-17",
  "Statement": [{
    "Effect": "Allow",
    "Principal": { "Federated": "arn:aws:iam::<account>:oidc-provider/oidc.boardwalk.sh" },
    "Action": "sts:AssumeRoleWithWebIdentity",
    "Condition": {
      "StringEquals": { "oidc.boardwalk.sh:aud": "sts.amazonaws.com" },
      "StringLike":   { "oidc.boardwalk.sh:sub": "org:<your org id>:workflow:<workflow id>:*" }
    }
  }]
}

Grant the role only what the workflow needs; the credentials it issues carry the role's permissions and expire on their own.

Exchange the token

AssumeRoleWithWebIdentity is an unsigned call (the token is the authentication), so plain fetchworks; the AWS SDK's @aws-sdk/client-sts works the same way if you prefer it. Ordinary code is enough — a suspended run resumes with its exact program state, so the credentials you hold survive the wait (mint fresh ones if they expired while you waited):

const creds = await (async () => {
  const jwt = await runtime.idToken("sts.amazonaws.com");
  const params = new URLSearchParams({
    Action: "AssumeRoleWithWebIdentity",
    Version: "2011-06-15",
    RoleArn: "arn:aws:iam::<account>:role/<role>",
    RoleSessionName: `bw-${runtime.runId}`,
    WebIdentityToken: jwt,
  });
  const res = await fetch(`https://sts.amazonaws.com/?${params}`, {
    method: "POST",
    headers: { Accept: "application/json" },
  });
  if (!res.ok) throw new Error(`STS: ${res.status}`);
  const body = await res.json();
  return body.AssumeRoleWithWebIdentityResponse.AssumeRoleWithWebIdentityResult.Credentials;
});

The returned AccessKeyId / SecretAccessKey / SessionToken plug into any AWS SDK client. Mint a new id-token whenever you need one; a run that slept for an hour just calls runtime.idToken() again.

The claims

{
  "iss": "https://oidc.boardwalk.sh",
  "sub": "org:<org id>:workflow:<workflow id>:run:<run id>",
  "aud": "sts.amazonaws.com",
  "org_id": "...", "workflow_id": "...", "workflow_version_id": "...", "run_id": "...",
  "trigger_kind": "cron", "actor_type": "user", "runs_on": "boardwalk/linux",
  "stage": "production", "exp": 900
}

Everything a policy needs to reason about which run is asking: the org, the workflow, the exact run, how it was triggered, and who triggered it. AWS trust policies match aud and sub; clouds that support custom claim mapping (GCP, Azure) can use the rest directly. Verification keys are public at https://oidc.boardwalk.sh/.well-known/jwks.json.

When to use stored keys instead

Federation needs a one-time trust setup in the target cloud. For a service that doesn't support OIDC federation, or a quick integration where that setup isn't worth it, store a credential as a secret and read it with secrets.get(): the same redaction guarantee applies. Prefer federation for your own cloud accounts; it removes the long-lived key entirely.