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 * (EC2 Describe has no resource-level scoping),
  • ec2:RunInstances / TerminateInstances / StartInstances / StopInstances / CreateTags / DeleteTags — the instance lifecycle and tag upkeep, optionally bounded to one region with an aws:RequestedRegion condition,
  • ssm:GetParameters / GetParameter on arn:aws:ssm:*::parameter/aws/service/* — the AWS-owned public AMI parameters that an image.distribution / image.version resolves 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:

ValueWhat it isExample
issuer URLThe origin Ringleader signs its tokens from. No trailing slash.https://oidc-app.ringleader.dev
organization idYour organization’s id — a UUID, never its name.0192f5bf-af83-7178-8d0a-f1c7aea06bde

Everything the trust needs is derived from those two:

Derived valueValue
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.

PathCreatesUse when
CloudFormationthe OIDC provider + federated role + optional landing-pad network, via aws cloudformation deployyou deploy with the aws CLI or the console
Terraformthe 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.sh

deploy.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 handoff

The 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

An IAM OIDC provider requires a certificate thumbprint, but AWS validates a public-CA issuer against its own trust store and ignores the thumbprint. The field is still required by the API, so both paths populate it from the live TLS chain automatically — you never compute it by hand.

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.

PinValueWhere it lives
Issuer<issuer-url>/org/<org-id>the IAM OIDC provider’s URL
Audience (aud)<issuer-url>/org/<org-id>/awsthe 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:

NeedsProvided by
Bringing the workstation up — it finishing setup and reporting Readyegress from the instance to the Ringleader control planea public IP + internet gateway (the default), or a NAT gateway
Using the workstationrl shell, rl tmux, port-forwards, VS Code Webinbound TCP 22, from wherever you run rla 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    # CloudFormation
ssh_source_ranges = ["203.0.113.0/24"]   # Terraform

For 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

ValueWhere it lands
role ARN (arn:aws:iam::<account-id>:role/ringleader-workstations)CloudAccount spec.aws.targetRoleArn
regionCloudIdentity 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

The image alias table (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: true

status.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