Onboarding AWS
Grant Ringleader least-privilege, keyless access to run workstation EC2 instances in your AWS account, with Terraform or CloudFormation.
Your developers’ workstations can run as EC2 instances in your own AWS account, on your bill and inside your VPC. This page sets that up.
You run one Terraform module, or one CloudFormation stack, in an account you own. It creates an IAM role for Ringleader to use, and tells AWS to let Ringleader assume it without an access key. You send a few values back to Ringleader, and from then on your developers create and delete workstations themselves. Ringleader never holds a key to your account, and never gets account admin.
What this creates in your account
Onboarding creates three things in your account, all visible in the AWS console afterwards, plus an optional fourth:
- An IAM OIDC identity provider. This is how AWS confirms that a request really comes from Ringleader and is meant for your organization, without Ringleader ever holding an access key.
- An IAM role called
ringleader-workstations. This is the role Ringleader assumes when it creates or terminates an instance. - A permission policy on that role. It lets the role create, start, stop and terminate EC2 instances and read what it needs to do that, and nothing else. The full list is below.
- Optionally, a network for the workstations: a VPC, a subnet, a NAT gateway, and
two security groups. Both paths create it unless you turn it off
(
create_network = falseorCREATE_NETWORK=false).
What Ringleader can and cannot do in your account
There is no key to leak. No IAM user and no access key is created, so there is nothing to rotate and nothing to steal. Each time Ringleader acts, it presents a short-lived signed token and AWS checks it. Delete the OIDC provider or the role and Ringleader loses access immediately.
Other Ringleader customers cannot reach your account. Every token names the organization it is for, and your role accepts only tokens that name yours. AWS turns the rest away; that decision is made in your account, not by Ringleader.
Ringleader can only manage EC2 instances. No billing, no account admin, and the only
IAM permission is the narrow iam:PassRole described under
runtime identities. The base policy is exactly:
- twelve read-only
ec2:Describeactions (instances, instance status, instance types, images, subnets, security groups, VPCs, volumes, volume modifications, network interfaces, tags and availability zones), named one by one rather than granted as a wildcard, on*because EC2’sDescribeactions support no resource-level scoping, ec2:DescribeInstanceAttribute, also on*, in a statement of its own so that it takes the same optional region bound as the actions below. It is the one read here that returns content rather than shape: asked for theuserDataattribute it hands back an instance’s whole boot payload, which is how Ringleader checks that what it wrote to a workstation carries no credential,ec2:RunInstances/TerminateInstances/StartInstances/StopInstances/ModifyInstanceAttribute/ModifyVolume/CreateTags/DeleteTags, the instance lifecycle, growing a workstation’s root volume, 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.
Three features are on by default and add to this:
runtime identities add iam:PassRole on one IAM
path, egress control adds security-group and routing
actions, and artifact storage adds S3 access to
buckets named ringleader-*. Each section says what it adds and how to turn it off.
Before you start
You will have received two values from Ringleader when your organization was set up. If you do not have them, ask us.
| Value | What it is | Example |
|---|---|---|
| issuer URL | The address Ringleader’s tokens come 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 |
You also need an AWS account to run the workstations in. We recommend an account used for nothing else. Workstations are billed to it, and everything Ringleader is allowed to do is limited to it, so a dedicated account keeps both the bill and the risk in one place. In it you need permission to create an IAM OIDC provider and an IAM role. You need it only for this setup, which you do once.
Set it up
Two paths that do the same thing, so use whichever your team already runs. Both live in github.com/ringleader-dev/cloud-onboarding, and both finish by printing the values you send back to Ringleader (see what you send back).
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 deploys the stack and prints the values to send back. SSH_SOURCE_CIDR is
where your engineers connect from, for an SSH rule of your own; see
Reaching your workstations for who can connect without it.
Terraform
cd cloud-onboarding/aws/terraform/examples/standalone
cp terraform.tfvars.example terraform.tfvars
# Edit terraform.tfvars: the two values above, your region, and
# ssh_source_ranges, the addresses your engineers connect from.
terraform init && terraform apply
terraform output handoff # the values to send backThe module is reusable. If you prefer, reference it as a module source from your own configuration rather than copying it.
If you know AWS OIDC providers: the thumbprint is handled
What your account checks
When Ringleader asks to act in your account, AWS checks three things about the token it presents. All three are built from the two values Ringleader gave you, and the module and the stack configure them for you.
| What AWS checks | Value | Where it is configured |
|---|---|---|
| Who signed the token | <issuer-url>/org/<org-id> | the IAM OIDC provider’s URL |
| Which organization it speaks for | org:<org-id> | the role trust policy’s sub condition |
| Which cloud it may be used against | <issuer-url>/org/<org-id>/aws | the provider’s client-id list, and the role trust’s aud condition |
The role checks all three, not just who signed the token. Checking only that would accept any token Ringleader signs, including one it signed for another customer.
If AWS ever turns Ringleader away, compare these three against what was created. A mismatch in one of them is the usual cause, and a trailing slash on the issuer is the usual mismatch.
Reaching your workstations
rl shell, rl tmux, port-forwards and VS Code Web all connect to the workstation over
SSH on TCP 22, straight to the machine. A workstation behind an
Edge is reached
through a port on the edge instance instead. A workstation therefore needs two connections,
and they are set up separately:
| Needs | Provided by | |
|---|---|---|
Coming up, meaning it finishes setup and reports Ready | a way out, from the instance to Ringleader | a public IP and the internet gateway (the default), or a NAT gateway |
Being used: rl shell, rl tmux, port-forwards, VS Code Web | a way in, on TCP 22, from wherever you run rl | Ringleader’s own security group, a rule of yours, or private connectivity |
The first comes with the network the module creates. For the second, Ringleader creates a
security group of its own in the workstation’s VPC and attaches it beside the groups the
workstation already has. It admits TCP 22 and 2222 from any address, unless the
CloudAccount
lists sshSourceRanges, and it has no outbound rule. EC2 combines every group on an
interface, so it opens those two ports and changes nothing else. Creating it needs the
egress control grant.
The module and the stack can also add a rule of your own on TCP 22 and 2222, for the addresses your engineers connect from:
ssh_source_ranges = ["203.0.113.0/24"] # Terraform, in terraform.tfvarsSSH_SOURCE_CIDR=203.0.113.0/24 ./deploy.sh # CloudFormationThat rule goes on the security groups every workstation is given, so it covers every instance in them, not only Ringleader’s workstations. It applies alongside Ringleader’s group. It keeps workstations reachable from those addresses when Ringleader cannot create its group: with egress control turned off, or for a workstation in a VPC the grant does not cover.
A workstation cannot fully confirm any of this for you. It sets itself up over its own
outbound connection, so it reports Ready whether or not anything can reach it. When
Ringleader could not create its group, the workstation reports the SSHAdmissionMissing
condition. Otherwise, open a shell to check.
Public IP addresses, and the NAT gateway
A workstation gets a public IP by default, and with it the internet gateway gives it
outbound access for free. The network the module creates also includes a NAT
gateway, so that a workstation without a public IP still has outbound access.
That NAT gateway bills by the hour whether or not anything uses it. If all your
workstations will have public IPs, set create_nat_gateway = false and
create_gateway_subnet = false (Terraform) or CREATE_NAT_GATEWAY=false and
CREATE_GATEWAY_SUBNET=false (CloudFormation) and you pay nothing for it.
If you would rather none of your workstations had a public IP, keep the NAT gateway and set
allowPublicAddresses: false on the CloudAccount. To set it for the workstations one
CloudIdentity builds instead, the Ringleader
object that says how workstations in your account are built, set this on it:
spec:
overrideProviderConfig:
aws:
assignPublicIp: falseUse overrideProviderConfig when developers must not be able to turn it back on, and
defaultProviderConfig when they may. You then reach the workstations over your VPN,
Direct Connect or peering.
Runtime identities (on by default)
By default a workstation has no AWS identity of its own. Nothing running on it, including an AI coding agent, can touch anything in your account. That is the safe default, and there is nothing to narrow.
If you want a workstation to be able to reach something in your account, say one S3
bucket, you give it an IAM role to run as. You create that role yourself, and whoever
administers Ringleader names it on the CloudIdentity, or a developer names it on their
workstation (providerConfig.aws.iamInstanceProfile).
For Ringleader to attach a role to an instance, its own role needs iam:PassRole. Both
paths grant that, scoped so that Ringleader can pass only roles you create under the IAM
path /ringleader-workstations/, and only to EC2. Until you create a role under that
path, the grant can pass nothing, which is why leaving it on costs you nothing. Turn it
off if you will never use it, with enable_workstation_identities = false (Terraform)
or the EnableWorkstationIdentities stack parameter (CloudFormation); a workstation
that then asks for a role fails to launch, and nothing else changes.
Egress control (on by default)
By default a workstation can connect to anything your network can reach. Ringleader can narrow that to a list you choose per workstation, so a workstation can reach GitHub and your package registry and nothing else (see restricting outbound connections), enforced by security groups Ringleader creates and keeps in step with the workstation’s configuration.
It is on by default. It also lets Ringleader write the security group that admits SSH to
your workstations, so with it turned off they are reachable only through a rule of your own.
A workstation with no egress policy still has no limit on where it connects. Turn it off
with enable_egress_control = false (Terraform) or EGRESS_CONTROL=false
(CloudFormation, which passes it as the EnableEgressControl parameter).
It adds these actions:
| Action | For |
|---|---|
ec2:CreateSecurityGroup, ec2:DeleteSecurityGroup | one group per distinct egress policy, and the group that admits SSH to your workstations |
ec2:AuthorizeSecurityGroupEgress, ec2:RevokeSecurityGroupEgress | keep a policy group’s rules in step with the workstation’s configuration, and remove the allow-all outbound rule AWS puts on a new SSH group |
ec2:AuthorizeSecurityGroupIngress, ec2:RevokeSecurityGroupIngress | the SSH rules on the SSH group, and the DNS/HTTPS proxy’s own group, which has to admit workstation traffic |
ec2:UpdateSecurityGroupRuleDescriptionsIngress | mark a rule on that group as Ringleader’s own, so it revokes only the rules it wrote. It selects a rule that already exists and replaces its description text, expressing no protocol, port or address of its own, so it cannot add, remove or widen a rule |
ec2:ModifyNetworkInterfaceAttribute | move a running workstation onto the group for its policy, and attach the SSH group. Also clear the source and destination check on the proxy’s own interface, without which AWS drops every packet it forwards |
ec2:CreateRouteTable, ec2:DeleteRouteTable, ec2:CreateRoute, ec2:ReplaceRoute, ec2:DeleteRoute, ec2:AssociateRouteTable, ec2:DisassociateRouteTable, ec2:CreateSubnet, ec2:DeleteSubnet | route traffic to the DNS/HTTPS proxy, for a policy that names hostnames. An AWS route table attaches per subnet, so routing is per subnet rather than per workstation |
ec2:DescribeSecurityGroupRules, ec2:DescribeRouteTables | read back the rules and routes Ringleader wrote, so a rule or route someone else changed is reported rather than silently overwritten |
ec2:AllocateAddress, ec2:ReleaseAddress, ec2:AssociateAddress, ec2:DisassociateAddress, ec2:DescribeAddresses | reserving a fixed public address for the proxy’s VM. Ringleader does not use them today: the proxy’s public address is not reserved, and it changes when Ringleader replaces the machine |
The bound each one takes. The security-group, route-table and subnet writes are
limited to your workstations’ VPC. The two reads and the five Elastic IP actions are
limited to your region instead, because EC2’s Describe actions take no resource-level
scoping and an Elastic IP belongs to no VPC when it is allocated. The three creates are
held to your VPC by resource name rather than by a condition, because AWS authorizes a
create against the object it makes.
Where no VPC is known the VPC bound falls away and the region bound is all that is left.
Both setup paths above set a region. If you reference the Terraform module from your own
configuration instead, set allowed_regions to bound it: that is empty by default, which
places no region condition and leaves the whole account as the boundary.
Routing by subnet asks two things of your network, and neither is a subnet per policy. One proxy serves many policies from one subnet, telling them apart by source address.
- A routed subnet may hold only workstations the proxy serves, because the proxy refuses a
source address it has no rule for. The proxy does not start routing a subnet while a
machine it does not serve is in it. A machine created there later is routed anyway, and
loses its outbound access.
create_governed_subnet(Terraform) orCREATE_GOVERNED_SUBNET(CloudFormation) reserves a subnet for this. Put the workstations that have an egress policy in the subnet it outputs,governed_subnet_id(Terraform) orGovernedSubnetId(CloudFormation). The workstations subnet the module creates is never routed, because it is for every workstation in the VPC, with a policy or without one. - The proxy is never in the subnet it routes. A route table replaces the default route for
everything in its subnet, so a proxy in the subnet it routes would send its own traffic
back into itself. Its VM goes in the subnet that
create_gateway_subnet(Terraform) orCREATE_GATEWAY_SUBNET(CloudFormation) reserves. Give that subnet’s id,gateway_subnet_id(Terraform) orGatewaySubnetId(CloudFormation), to the Edge as itssubnet.
Two security groups, and which one a workstation gets
With egress control on, the network the module creates has two security groups, and you send both ids back to Ringleader. Whoever administers Ringleader gives each workstation one of them, depending on whether it has an egress policy:
| You send back | It is for | Lets in | Lets out |
|---|---|---|---|
security_group_id / SecurityGroupId | a workstation with no egress policy | TCP 22 and 2222 from your addresses | everything |
inbound_only_security_group_id / InboundOnlySecurityGroupId | a workstation with an egress policy | the same | nothing |
There are two because of a rule of AWS: a security group can only allow, never deny, and an instance may do anything any of its groups allows. So the group Ringleader builds for a policy can only add to what the instance’s other groups permit. Next to a group that lets everything out, a policy restricts nothing; next to one that lets nothing out, the instance reaches exactly what the policy lists. A workstation without a policy still needs the first group, or it cannot finish setting up.
If a workstation refuses to start, naming a security group, it was given the first group but has an egress policy. Ringleader refuses rather than start a workstation it would have to report as restricted while it can reach the whole internet. Give it the inbound-only group instead.
Do not add an outbound rule to the inbound-only group. It has none on purpose, and
one rule of any kind there undoes every egress policy in the VPC. The CloudFormation
version carries a single placeholder rule to 127.0.0.1/32, which permits nothing;
leave it alone.
Artifact storage (on by default)
This grant lets Ringleader store files for your organization in an S3 bucket in your account, instead of in Ringleader’s own storage, so the data stays in an account you control.
It cannot widen its own access (no s3:PutBucketPolicy, no ACL writes), and it creates no
bucket at apply time. Nothing uses this grant on AWS yet.
Two ways to take it, chosen by whether you name a bucket:
- Leave
artifact_storage_bucketempty and Ringleader creates and manages its own buckets, namedringleader-*. The grant is confined to that name pattern, so it reaches no bucket you already have. - Name a bucket you created and Ringleader gets object access to that one bucket and nothing else, with no ability to create, change or delete a bucket. Its region, its lifecycle rules and its encryption key stay yours. Take this one if you have a data-residency or key-custody position to defend.
Turn it off with enable_artifact_storage = false (Terraform) or the
EnableArtifactStorage stack parameter (CloudFormation). With it off, those files stay in
Ringleader’s own storage, and nothing else changes.
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 network) | providerConfig.aws.subnetId |
| security group id (only if you created the network) | providerConfig.aws.securityGroupIds |
| inbound-only security group id (only with the network and egress control) | providerConfig.aws.securityGroupIds, for a workstation with an egress policy; see two security groups |
terraform output handoff prints all of these together, and deploy.sh prints them
when it finishes.
Send them to whoever administers Ringleader for your organization, which may be you. They go into two Ringleader objects: a CloudAccount, which records which IAM role to assume in your account, and a CloudIdentity, which says how workstations in your account are built (region, instance type, subnet, security groups). See how Ringleader signs in to your cloud for a worked example of both.
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
sent back, confirm Ringleader can use them before anyone creates a workstation.
Replace aws and dev with the CloudIdentity’s name and namespace:
rl wait cloudidentity aws -n dev --for Ready --timeout 2m
rl cloudidentity get aws -n dev -o yaml # status.valid: truestatus.valid: true means Ringleader can use the identity. false means it cannot,
and rl cloudidentity describe aws -n dev says why. The usual causes are a wrong role
ARN or a mismatch in one of the three values your account checks.
Revoking
Ringleader loses access the moment the OIDC provider or the role is deleted. There are no keys to hunt down:
- In the console: delete the OIDC provider, or the role.
- Terraform:
terraform destroy. - CloudFormation: delete the stack.
See also
- CloudAccount: the object that records which IAM role to assume in your account.
- CloudIdentity: how workstations in your account are built.
- How Ringleader signs in to your cloud: why no key changes hands, and the four steps from onboarding to a running workstation.
- Providers: the full
providerConfig.awsfield set.