Onboarding AWS
Grant Ringleader least-privilege, keyless access to run workstation EC2 instances in your AWS account, with Terraform or CloudFormation.
Let a Ringleader control plane run Workstation
EC2 instances in your own AWS account — your account, your bill, your VPC —
using only the permissions the workstation lifecycle needs, no access key, and
never account admin. You do it by trusting Ringleader’s OIDC issuer through an
IAM OIDC identity provider and an IAM role assumed with
sts:AssumeRoleWithWebIdentity.
The model
Ringleader control plane
│ signs a short-lived OIDC token:
│ iss = <issuer>/org/<your-org-id>, sub = org:<your-org-id>,
│ aud = <issuer>/org/<your-org-id>/aws
▼
IAM OIDC identity provider in YOUR account
│ admits ONLY the per-org issuer, and the trust pins BOTH aud AND sub
▼
ringleader-workstations (the IAM role you create)
│ assumed via sts:AssumeRoleWithWebIdentity; least-privilege EC2 lifecycle
▼
creates / manages / terminates workstation EC2 instances in YOUR account- Keyless. No IAM user and no access key is created. Ringleader authenticates with a short-lived, Ringleader-signed token and assumes your role. Delete the OIDC provider (or the role) and access stops.
- Pinned to your organization. The role’s trust policy admits only your organization’s subject and the per-org audience. A token minted for any other Ringleader customer carries a different subject and your account refuses it — not merely Ringleader.
- Least privilege. The role holds only the EC2 instance lifecycle, EC2
read-only
Describe*, and the read of the AWS-owned public parameters that resolve an AMI. No IAM, no S3, no account admin.
The least-privilege policy
The role’s permission policy is exactly:
ec2:Describe*(instances, images, subnets, security groups, volumes, network interfaces, tags, availability zones) — read-only, on*(EC2Describehas no resource-level scoping),ec2:RunInstances/TerminateInstances/StartInstances/StopInstances/CreateTags/DeleteTags— the instance lifecycle and tag upkeep, optionally bounded to one region with anaws:RequestedRegioncondition,ssm:GetParameters/GetParameteronarn:aws:ssm:*::parameter/aws/service/*— the AWS-owned public AMI parameters that animage.distribution/image.versionresolves to at launch.
No iam:* at all, unless you opt into per-workstation instance profiles — see
workstations that run as an identity.
Before you start
Ringleader gives you two values:
| Value | What it is | Example |
|---|---|---|
| issuer URL | The origin Ringleader signs its tokens from. No trailing slash. | https://oidc-app.ringleader.dev |
| organization id | Your organization’s id — a UUID, never its name. | 0192f5bf-af83-7178-8d0a-f1c7aea06bde |
Everything the trust needs is derived from those two:
| Derived value | Value |
|---|---|
OIDC provider URL (iss) | <issuer-url>/org/<org-id> |
Audience / OIDC client id (aud) | <issuer-url>/org/<org-id>/aws |
Subject (sub) | org:<org-id> |
You also need an AWS account to run the workstations in (your billing boundary), and rights to create an IAM OIDC provider and an IAM role in it — a one-time human step.
Apply the assets
Both paths do the same thing; use whichever your team already runs. They live in github.com/ringleader-dev/cloud-onboarding.
| Path | Creates | Use when |
|---|---|---|
| CloudFormation | the OIDC provider + federated role + optional landing-pad network, via aws cloudformation deploy | you deploy with the aws CLI or the console |
| Terraform | the same, end to end (the module derives the TLS thumbprint automatically) | you manage infra as code |
CloudFormation
git clone https://github.com/ringleader-dev/cloud-onboarding
cd cloud-onboarding/aws/cloudformation
ISSUER_URL=https://oidc-app.ringleader.dev \
ORG_UID=0192f5bf-af83-7178-8d0a-f1c7aea06bde \
REGION=us-east-1 \
CREATE_NETWORK=true \
SSH_SOURCE_CIDR=203.0.113.0/24 \
./deploy.shdeploy.sh recomputes the issuer’s TLS thumbprint, deploys the stack, and prints
the values to hand back.
Terraform
cd cloud-onboarding/aws/terraform/examples/standalone
cp terraform.tfvars.example terraform.tfvars # fill in issuer + org_uid
terraform init && terraform apply
terraform output handoffThe module is reusable — reference it as a module source from your own configuration rather than copying it, if you prefer.
The thumbprint is a formality
What the trust pins
Derived from the two values above, these are what confine the trust to your organization. The assets set them for you; this is what to check if a mint is ever refused.
| Pin | Value | Where it lives |
|---|---|---|
| Issuer | <issuer-url>/org/<org-id> | the IAM OIDC provider’s URL |
Audience (aud) | <issuer-url>/org/<org-id>/aws | the provider’s client-id list, and the role trust’s aud condition |
Subject (sub) | org:<org-id> | the role trust’s sub condition |
The role trust policy pins both aud and sub. Pinning only the issuer
would admit any token that issuer signs; pinning both confines the trust to your
organization and this cloud.
Reaching your workstations
Ringleader has no bastion and no SSH tunnel: rl shell, rl tmux,
port-forwards and VS Code Web all dial the workstation on TCP 22. Two different things
need two different kinds of connectivity, and conflating them produces a workstation that
looks perfectly healthy and that nobody can use:
| Needs | Provided by | |
|---|---|---|
Bringing the workstation up — it finishing setup and reporting Ready | egress from the instance to the Ringleader control plane | a public IP + internet gateway (the default), or a NAT gateway |
Using the workstation — rl shell, rl tmux, port-forwards, VS Code Web | inbound TCP 22, from wherever you run rl | a security-group rule — or private connectivity |
A workstation gets a public IP by default
(providerConfig.aws.assignPublicIp, default true), so the internet gateway
alone gives it egress — no NAT gateway, and no hourly NAT bill. The landing pad
(CREATE_NETWORK=true / create_network) opens inbound TCP 22 to the CIDRs you
name:
SSH_SOURCE_CIDR=203.0.113.0/24 # CloudFormationssh_source_ranges = ["203.0.113.0/24"] # TerraformFor a private workstation, set assignPublicIp: false and create the NAT gateway
(create_nat_gateway) so it still has egress, then reach the subnet over VPN,
Direct Connect, or peering. A workstation with egress but no inbound path finishes setting up,
reports Ready, and nobody can open it.
Optional: workstations that run AS an identity
By default a workstation runs with no instance profile, so nothing inside the
workstation can act as any AWS principal — the safe default. To let software on a workstation hold
its own AWS identity, an administrator names an instance profile on the
CloudIdentity or the workstation
(providerConfig.aws.iamInstanceProfile).
Attaching an instance profile needs iam:PassRole, which the onboarding role does
not grant unless you opt in (enable_workstation_identities, off by default and
scoped to one IAM path). Read its warning before enabling it, and enable it only in
an account dedicated to Ringleader workstations.
What you hand back to Ringleader
| Value | Where it lands |
|---|---|
role ARN (arn:aws:iam::<account-id>:role/ringleader-workstations) | CloudAccount spec.aws.targetRoleArn |
| region | CloudIdentity providerConfig.aws.region |
| subnet id (only if you created the landing pad) | providerConfig.aws.subnetId |
| security group id (only if you created the landing pad) | providerConfig.aws.securityGroupIds |
The role ARN becomes which cloud identity to use; the rest become the VM’s size and placement. See per-organization federation for the objects Ringleader builds from them.
AMIs are x86-64
ubuntu-24.04, debian-12, amazonlinux-2023, …) resolves
x86-64 AMIs, so pick an x86-64 instance type. The default,
providerConfig.aws.instanceType: t3.medium, is one; an m6i.* or c6i.* is fine
too. An arm64 (Graviton) instance type will not match these images.Verifying
Once an administrator has created the CloudAccount and CloudIdentity from the values you handed back, confirm the Ringleader side resolves your trust before anyone tries to boot a workstation:
rl wait cloudidentity aws -n dev --for Ready --timeout 2m
rl cloudidentity get aws -n dev -o yaml # status.valid: truestatus.valid: true means the identity is usable — the account resolves and the
cloud identity it selects can be used. false points at the role ARN or the trust configuration;
rl cloudidentity describe aws -n dev says which.
Revoking
Access stops the moment the trust is gone — there are no keys to chase:
- Delete the OIDC provider (or the role) — Ringleader can no longer assume it.
- Terraform:
terraform destroy. - CloudFormation: delete the stack.
See also
- CloudAccount: the object that records which cloud identity to use.
- CloudIdentity: the credential paths and the VM’s size and placement.
- Per-organization federation: how the trust works end to end.
- Providers: the full
providerConfig.awsfield set.