Least privilege in your cloud
The least-privilege access to give the identity Cua connects as, and what Cua keeps when a machine does not come online.
Details for Use your own cloud and Your cloud resources and costs.
Least-privilege access#
Cua creates no IAM users, roles or keys, so it acts as whatever identity you
connect. Connect a dedicated one with only the access below instead of an
administrator. Everything Cua creates carries the tag cua-managed=true, and
the policy lets it create only tagged resources and stop, start, terminate or
delete only tagged ones, so it cannot touch anything else in the account.
cua cloud test checks the create calls as dry runs and names this page when
AWS refuses them.
AWS#
Create a customer managed policy from this JSON (not an inline policy: it is
longer than the 2048 characters AWS allows inline for a user) and attach it
to a dedicated IAM user or role, then connect with that identity's profile
(cua cloud connect aws --profile <name>):
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "CuaIdentityAndRead",
"Effect": "Allow",
"Action": [
"sts:GetCallerIdentity",
"ec2:DescribeImages",
"ec2:DescribeInstances",
"ec2:DescribeInstanceTypeOfferings",
"ec2:DescribeSecurityGroups",
"ec2:DescribeSubnets",
"ec2:DescribeVpcs"
],
"Resource": "*"
},
{
"Sid": "CuaResolvePublicImageParameters",
"Effect": "Allow",
"Action": "ssm:GetParameters",
"Resource": "arn:aws:ssm:*::parameter/aws/service/*"
},
{
"Sid": "CuaLaunchTaggedInstancesAndVolumes",
"Effect": "Allow",
"Action": "ec2:RunInstances",
"Resource": [
"arn:aws:ec2:*:*:instance/*",
"arn:aws:ec2:*:*:volume/*"
],
"Condition": {
"StringEquals": {
"aws:RequestTag/cua-managed": "true"
}
}
},
{
"Sid": "CuaLaunchInstancesWithIMDSv2Only",
"Effect": "Deny",
"Action": "ec2:RunInstances",
"Resource": "arn:aws:ec2:*:*:instance/*",
"Condition": {
"StringNotEquals": {
"ec2:MetadataHttpTokens": "required"
}
}
},
{
"Sid": "CuaLaunchUsesExistingNetworkAndImages",
"Effect": "Allow",
"Action": "ec2:RunInstances",
"Resource": [
"arn:aws:ec2:*::image/*",
"arn:aws:ec2:*:*:subnet/*",
"arn:aws:ec2:*:*:network-interface/*",
"arn:aws:ec2:*:*:security-group/*"
]
},
{
"Sid": "CuaCreateTaggedSecurityGroups",
"Effect": "Allow",
"Action": "ec2:CreateSecurityGroup",
"Resource": "arn:aws:ec2:*:*:security-group/*",
"Condition": {
"StringEquals": {
"aws:RequestTag/cua-managed": "true"
}
}
},
{
"Sid": "CuaCreateSecurityGroupsInAVpc",
"Effect": "Allow",
"Action": "ec2:CreateSecurityGroup",
"Resource": "arn:aws:ec2:*:*:vpc/*"
},
{
"Sid": "CuaTagOnlyWhatItCreates",
"Effect": "Allow",
"Action": "ec2:CreateTags",
"Resource": [
"arn:aws:ec2:*:*:instance/*",
"arn:aws:ec2:*:*:volume/*",
"arn:aws:ec2:*:*:security-group/*"
],
"Condition": {
"StringEquals": {
"ec2:CreateAction": [
"RunInstances",
"CreateSecurityGroup"
],
"aws:RequestTag/cua-managed": "true"
}
}
},
{
"Sid": "CuaManageItsOwnInstances",
"Effect": "Allow",
"Action": [
"ec2:StartInstances",
"ec2:StopInstances",
"ec2:TerminateInstances",
"ec2:GetConsoleOutput"
],
"Resource": "arn:aws:ec2:*:*:instance/*",
"Condition": {
"StringEquals": {
"aws:ResourceTag/cua-managed": "true"
}
}
},
{
"Sid": "CuaManageItsOwnSecurityGroups",
"Effect": "Allow",
"Action": [
"ec2:AuthorizeSecurityGroupIngress",
"ec2:DeleteSecurityGroup"
],
"Resource": "arn:aws:ec2:*:*:security-group/*",
"Condition": {
"StringEquals": {
"aws:ResourceTag/cua-managed": "true"
}
}
}
]
}Why it is shaped this way:
- Reads cannot be scoped.
Describe*calls andsts:GetCallerIdentitydo not support resource-level permissions in AWS, so they allow*. They change nothing. - Creates require the tag.
ec2:RunInstancesfor instances and volumes andec2:CreateSecurityGrouprequire the request to carrycua-managed=true(aws:RequestTag).ec2:CreateTagsis allowed only as part of those two creates (ec2:CreateAction), so Cua cannot re-tag an existing resource to claim it. - Changes require the tag on the resource. Start, stop, terminate, read
the console, authorize ingress and delete a security group all check
aws:ResourceTag/cua-managed. - A launch references things it does not own. The image, subnet, network
interface and security group of a launch are separate resources in the
RunInstancescall and cannot carry the request tag, so they are their own statement. Cua only ever adds a rule to a security group it created, and the dry run incua cloud testruns before that group exists. - IMDSv2 only. A second statement denies launching an instance without
HttpTokens=required, which is how Cua always launches. - Image lookup.
ssm:GetParameterson/aws/service/*resolves the public image names (Deep Learning and others) AWS publishes. - No
iam:*. The policy cannot create or pass a role, so a Cua instance never has an instance profile.
To narrow it further, add a StringEquals condition on aws:RequestedRegion
to every statement that has a condition.
Google Cloud#
Create a custom role with these Compute Engine permissions and grant it to the account or service account you connect, on the project:
compute.instances.create
compute.instances.delete
compute.instances.get
compute.instances.list
compute.instances.start
compute.instances.stop
compute.instances.setMetadata
compute.instances.setLabels
compute.instances.getSerialPortOutput
compute.disks.create
compute.networks.create
compute.networks.get
compute.networks.delete
compute.subnetworks.use
compute.subnetworks.useExternalIpcua cloud test gcp checks all of them with testIamPermissions and lists
the ones that are missing. The same list is in gcp.rs
(PERMISSIONS), and a test keeps this page equal to it.
When a machine does not come online#
A create that fails to boot reads the machine's console (AWS
GetConsoleOutput, Google Cloud serial port 1) before it deletes the
machine, keeps the last 200 lines without anything that looks like a token,
secret or key in ~/.cua/cloud/boot-logs/<id>.log (mode 0600), and prints the
path and the last ten lines in the error. The read has 20 seconds and never
blocks the cleanup.
How Cua cleans up, disconnects and logs what it does: Cleanup, disconnect and audit in your cloud.