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 = false or CREATE_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:Describe actions (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’s Describe actions 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 the userData attribute 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 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.

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.

ValueWhat it isExample
issuer URLThe address Ringleader’s tokens come from. No trailing slash.https://oidc-app.ringleader.dev
organization idYour 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.sh

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

The 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

An IAM OIDC provider requires a certificate thumbprint. Both paths fill it in from the live certificate, so you never compute it by hand.

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 checksValueWhere it is configured
Who signed the token<issuer-url>/org/<org-id>the IAM OIDC provider’s URL
Which organization it speaks fororg:<org-id>the role trust policy’s sub condition
Which cloud it may be used against<issuer-url>/org/<org-id>/awsthe 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:

NeedsProvided by
Coming up, meaning it finishes setup and reports Readya way out, from the instance to Ringleadera public IP and the internet gateway (the default), or a NAT gateway
Being used: rl shell, rl tmux, port-forwards, VS Code Weba way in, on TCP 22, from wherever you run rlRingleader’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.tfvars
SSH_SOURCE_CIDR=203.0.113.0/24 ./deploy.sh   # CloudFormation

That 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: false

Use 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:

ActionFor
ec2:CreateSecurityGroup, ec2:DeleteSecurityGroupone group per distinct egress policy, and the group that admits SSH to your workstations
ec2:AuthorizeSecurityGroupEgress, ec2:RevokeSecurityGroupEgresskeep 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:RevokeSecurityGroupIngressthe SSH rules on the SSH group, and the DNS/HTTPS proxy’s own group, which has to admit workstation traffic
ec2:UpdateSecurityGroupRuleDescriptionsIngressmark 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:ModifyNetworkInterfaceAttributemove 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:DeleteSubnetroute 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:DescribeRouteTablesread 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:DescribeAddressesreserving 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) or CREATE_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) or GovernedSubnetId (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) or CREATE_GATEWAY_SUBNET (CloudFormation) reserves. Give that subnet’s id, gateway_subnet_id (Terraform) or GatewaySubnetId (CloudFormation), to the Edge as its subnet.

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 backIt is forLets inLets out
security_group_id / SecurityGroupIda workstation with no egress policyTCP 22 and 2222 from your addresseseverything
inbound_only_security_group_id / InboundOnlySecurityGroupIda workstation with an egress policythe samenothing

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_bucket empty and Ringleader creates and manages its own buckets, named ringleader-*. 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

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

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 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: true

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