From 97418c03dc244dc8134d9bebf627af94594f63e2 Mon Sep 17 00:00:00 2001 From: Julian Sommer <115516918+juliansommer@users.noreply.github.com> Date: Fri, 3 Jul 2026 13:51:38 +1000 Subject: [PATCH] feat: iam roles doc --- .../iam-roles-and-instance-profiles/README.md | 406 ++++++++++++++++++ docs/identity/imds/README.md | 2 +- 2 files changed, 407 insertions(+), 1 deletion(-) create mode 100644 docs/identity/iam-roles-and-instance-profiles/README.md diff --git a/docs/identity/iam-roles-and-instance-profiles/README.md b/docs/identity/iam-roles-and-instance-profiles/README.md new file mode 100644 index 000000000..49275d9ef --- /dev/null +++ b/docs/identity/iam-roles-and-instance-profiles/README.md @@ -0,0 +1,406 @@ +--- +title: "IAM Roles and Instance Profiles" +description: "Create IAM roles with trust policies, wrap them in instance profiles, and grant EC2 instances credentials without static keys." +category: "Identity" +tags: + - iam + - roles + - instance profiles + - trust policies + - credentials + - security +resources: + - title: "Spinifex Repository" + url: "https://github.com/mulgadc/spinifex" + - title: "AWS IAM Roles Documentation" + url: "https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles.html" + - title: "AWS Instance Profiles Documentation" + url: "https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_use_switch-role-ec2_instance-profiles.html" +--- + +# IAM Roles and Instance Profiles + +> Create IAM roles with trust policies, wrap them in instance profiles, and grant EC2 instances credentials without static keys. + +## Table of Contents + +- [Overview](#overview) +- [Prerequisites](#prerequisites) +- [Roles](#roles) +- [Granting Permissions to a Role](#granting-permissions-to-a-role) +- [Instance Profiles](#instance-profiles) +- [Launching an Instance with a Role](#launching-an-instance-with-a-role) +- [Managing Profile Associations at Runtime](#managing-profile-associations-at-runtime) +- [Assuming a Role](#assuming-a-role) +- [Cleaning Up](#cleaning-up) +- [Command Reference](#command-reference) +- [Troubleshooting](#troubleshooting) + +--- + +## Overview + +A role is an IAM identity with permissions but no long-lived credentials. Instead of an access key pair, a role has a **trust policy** declaring who may assume it; whoever assumes the role receives short-lived, auto-rotating STS credentials. + +An **instance profile** is the container that binds a role to an EC2 instance. An instance launched with a profile gets the role's credentials delivered through [IMDS](/docs/imds) — no static keys baked into the image, no `~/.aws/credentials` inside the guest. + +The full arc is: create a role → attach permissions → wrap it in an instance profile → launch an instance with the profile → the guest picks up credentials automatically. Each instance profile holds exactly **one role** (matching AWS). + +Like all IAM resources, roles and instance profiles are scoped to the account that creates them. + +## Prerequisites + +- A running Spinifex cluster with the AWS gateway started +- An account created via `spx admin account create` (see [Spinifex Admin CLI](/docs/spinifex-admin-cli)) +- AWS CLI configured with the account's profile: + +```bash +export AWS_PROFILE=spinifex-myteam +``` + +## Instructions + +## Roles + +### Create a Role + +Every role needs a trust policy. For a role that EC2 instances will use, trust the `ec2.amazonaws.com` service principal: + +```bash +cat > /tmp/ec2-trust.json << 'EOF' +{ + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Principal": { "Service": "ec2.amazonaws.com" }, + "Action": "sts:AssumeRole" + } + ] +} +EOF + +aws iam create-role --role-name app-server \ + --assume-role-policy-document file:///tmp/ec2-trust.json \ + --description "Role for app servers" +``` + +```json +{ + "Role": { + "Path": "/", + "RoleName": "app-server", + "RoleId": "AROA1A2B3C4D5E6F7890", + "Arn": "arn:aws:iam::000000000001:role/app-server", + "CreateDate": "2026-07-03T10:00:00Z", + "AssumeRolePolicyDocument": { + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Principal": { "Service": "ec2.amazonaws.com" }, + "Action": "sts:AssumeRole" + } + ] + }, + "Description": "Role for app servers", + "MaxSessionDuration": 3600 + } +} +``` + +To let a user (rather than EC2) assume the role, trust an `AWS` principal instead: + +```json +{ + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Principal": { "AWS": "arn:aws:iam::000000000001:user/admin" }, + "Action": "sts:AssumeRole" + } + ] +} +``` + +**Trust policy validation:** Spinifex rejects `NotPrincipal`, `NotAction`, empty `Principal` blocks, and empty-string `Action` elements at write time with `MalformedPolicyDocument`. `Condition` blocks are rejected except the `StringEquals` form used for web identity federation. + +### Get, List, and Update Roles + +```bash +aws iam get-role --role-name app-server +aws iam list-roles +aws iam list-roles --path-prefix /services/ +``` + +Update the description or the maximum STS session duration (3600 seconds by default): + +```bash +aws iam update-role --role-name app-server \ + --description "Updated" --max-session-duration 7200 +``` + +Replace the trust policy on an existing role: + +```bash +aws iam update-assume-role-policy --role-name app-server \ + --policy-document file:///tmp/ec2-trust.json +``` + +## Granting Permissions to a Role + +A freshly created role can do nothing. Grant permissions the same two ways as users: attach managed policies, or embed inline policies. + +### Attach a Managed Policy + +```bash +aws iam attach-role-policy --role-name app-server \ + --policy-arn arn:aws:iam::000000000001:policy/S3ReadOnly + +aws iam list-attached-role-policies --role-name app-server +aws iam detach-role-policy --role-name app-server \ + --policy-arn arn:aws:iam::000000000001:policy/S3ReadOnly +``` + +See [IAM Users and Policies](/docs/iam-users-and-policies) for creating managed policies and the policy document format. + +### Inline Role Policies + +Inline policies live inside the role and are deleted with it: + +```bash +aws iam put-role-policy --role-name app-server \ + --policy-name ec2-describe \ + --policy-document '{"Version":"2012-10-17","Statement":[{"Effect":"Allow","Action":["ec2:DescribeInstances"],"Resource":"*"}]}' + +aws iam list-role-policies --role-name app-server +aws iam get-role-policy --role-name app-server --policy-name ec2-describe +aws iam delete-role-policy --role-name app-server --policy-name ec2-describe +``` + +## Instance Profiles + +Create a profile and add the role to it: + +```bash +aws iam create-instance-profile --instance-profile-name app-server-profile +aws iam add-role-to-instance-profile \ + --instance-profile-name app-server-profile --role-name app-server +``` + +Inspect it — the `Roles` array shows the bound role: + +```bash +aws iam get-instance-profile --instance-profile-name app-server-profile +aws iam list-instance-profiles +aws iam list-instance-profiles-for-role --role-name app-server +``` + +A profile holds at most one role; adding a second returns `LimitExceeded`. To swap roles, remove the current one first: + +```bash +aws iam remove-role-from-instance-profile \ + --instance-profile-name app-server-profile --role-name app-server +``` + +## Launching an Instance with a Role + +Pass the profile by name (or ARN) at launch: + +```bash +aws ec2 run-instances \ + --image-id ami-0dd52c90440ff4150 \ + --instance-type t3.micro \ + --subnet-id subnet-a0e5fc381376d82a1 \ + --iam-instance-profile Name=app-server-profile +``` + +The instance description shows the association: + +```bash +aws ec2 describe-instances --instance-ids i-d2e09ff7de71b6341 \ + --query 'Reservations[0].Instances[0].IamInstanceProfile' +``` + +```json +{ + "Arn": "arn:aws:iam::000000000001:instance-profile/app-server-profile", + "Id": "AIPA1A2B3C4D5E6F7890" +} +``` + +Inside the guest, the AWS CLI and SDKs pick up the role's credentials from IMDS with no configuration — see [IMDS](/docs/imds) for fetching them manually, rotation timing, and limits. + +## Managing Profile Associations at Runtime + +Profiles can be attached to, removed from, or swapped on a **running** instance — no relaunch needed. + +List associations: + +```bash +aws ec2 describe-iam-instance-profile-associations \ + --filters Name=instance-id,Values=i-d2e09ff7de71b6341 +``` + +```json +{ + "IamInstanceProfileAssociations": [ + { + "AssociationId": "iip-assoc-9d7a617c605e337cd", + "InstanceId": "i-d2e09ff7de71b6341", + "IamInstanceProfile": { + "Arn": "arn:aws:iam::000000000001:instance-profile/app-server-profile" + }, + "State": "associated" + } + ] +} +``` + +Attach a profile to an instance launched without one: + +```bash +aws ec2 associate-iam-instance-profile \ + --instance-id i-d2e09ff7de71b6341 \ + --iam-instance-profile Name=app-server-profile +``` + +Swap to a different profile (returns a new association ID): + +```bash +aws ec2 replace-iam-instance-profile-association \ + --association-id iip-assoc-9d7a617c605e337cd \ + --iam-instance-profile Name=other-profile +``` + +Remove the profile — the guest's `iam/` metadata subtree starts returning 404: + +```bash +aws ec2 disassociate-iam-instance-profile \ + --association-id iip-assoc-9d7a617c605e337cd +``` + +## Assuming a Role + +Users and services can assume a role directly with STS, provided the trust policy allows their principal: + +```bash +aws sts assume-role \ + --role-arn arn:aws:iam::000000000001:role/deploy \ + --role-session-name release-42 +``` + +```json +{ + "Credentials": { + "AccessKeyId": "ASIA1A2B3C4D5E6F7890", + "SecretAccessKey": "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY", + "SessionToken": "IQoJb3JpZ2luX2VjE...", + "Expiration": "2026-07-03T11:00:00Z" + }, + "AssumedRoleUser": { + "AssumedRoleId": "AROA1A2B3C4D5E6F7890:release-42", + "Arn": "arn:aws:sts::000000000001:assumed-role/deploy/release-42" + } +} +``` + +The returned `ASIA`-prefixed credentials are temporary, valid up to the role's `MaxSessionDuration`. + +## Cleaning Up + +Deletion order matters — a role must be empty and unreferenced before it can be deleted: + +```bash +# 1. Remove the role from any instance profiles +aws iam remove-role-from-instance-profile \ + --instance-profile-name app-server-profile --role-name app-server +aws iam delete-instance-profile --instance-profile-name app-server-profile + +# 2. Delete inline policies and detach managed policies +aws iam delete-role-policy --role-name app-server --policy-name ec2-describe +aws iam detach-role-policy --role-name app-server \ + --policy-arn arn:aws:iam::000000000001:policy/S3ReadOnly + +# 3. Delete the role +aws iam delete-role --role-name app-server +``` + +## Command Reference + +| Command | Key Parameters | Notes | +| ------------------------------------------- | ----------------------------------------------------------------------------- | ------------------------------------ | +| `create-role` | `--role-name`, `--assume-role-policy-document`, `--path`, `--description`, `--max-session-duration`, `--tags` | Trust policy required | +| `get-role` | `--role-name` | | +| `list-roles` | `--path-prefix` | | +| `update-role` | `--role-name`, `--description`, `--max-session-duration` | | +| `update-assume-role-policy` | `--role-name`, `--policy-document` | Replaces the trust policy | +| `delete-role` | `--role-name` | Must have no policies or profiles | +| `attach-role-policy` / `detach-role-policy` | `--role-name`, `--policy-arn` | | +| `list-attached-role-policies` | `--role-name` | | +| `put-role-policy` | `--role-name`, `--policy-name`, `--policy-document` | Inline policy | +| `get-role-policy` / `delete-role-policy` | `--role-name`, `--policy-name` | | +| `list-role-policies` | `--role-name` | Inline policy names | +| `tag-role` / `untag-role` / `list-role-tags` | `--role-name`, `--tags`/`--tag-keys` | | +| `create-instance-profile` | `--instance-profile-name`, `--path`, `--tags` | | +| `get-instance-profile` | `--instance-profile-name` | Includes bound role | +| `list-instance-profiles` | `--path-prefix` | | +| `list-instance-profiles-for-role` | `--role-name` | | +| `add-role-to-instance-profile` | `--instance-profile-name`, `--role-name` | Max 1 role per profile | +| `remove-role-from-instance-profile` | `--instance-profile-name`, `--role-name` | | +| `delete-instance-profile` | `--instance-profile-name` | Must contain no role | +| `associate-iam-instance-profile` | `--instance-id`, `--iam-instance-profile` | Works on running instances | +| `disassociate-iam-instance-profile` | `--association-id` | | +| `replace-iam-instance-profile-association` | `--association-id`, `--iam-instance-profile` | Returns a new association ID | +| `describe-iam-instance-profile-associations`| `--association-ids`, `--filters` (instance-id, state) | | + +## Troubleshooting + +### MalformedPolicyDocument When Creating a Role + +The trust policy is invalid. In addition to the JSON checks that apply to all policies, trust policies must not use `NotPrincipal`, `NotAction`, an empty `Principal` block, or an empty-string `Action`. `Condition` blocks are rejected except `StringEquals` conditions used with web identity federation. + +### DeleteConflict When Deleting a Role + +The role still has attached managed policies, inline policies, or is bound to an instance profile. Remove all three first: + +```bash +aws iam list-attached-role-policies --role-name app-server +aws iam list-role-policies --role-name app-server +aws iam list-instance-profiles-for-role --role-name app-server +``` + +### DeleteConflict When Deleting an Instance Profile + +The profile still contains a role: + +```bash +aws iam remove-role-from-instance-profile \ + --instance-profile-name app-server-profile --role-name app-server +aws iam delete-instance-profile --instance-profile-name app-server-profile +``` + +### LimitExceeded When Adding a Role to a Profile + +The profile already holds a role — the limit is one per profile. Remove the existing role first, or create a second profile. + +### InvalidIamInstanceProfile.NotFound at Launch + +The profile name or ARN passed to `run-instances` does not exist in your account. Check the spelling: + +```bash +aws iam list-instance-profiles +``` + +### Instance Has No Credentials in IMDS + +If the `iam/` metadata subtree returns 404, the instance has no associated profile. Attach one without relaunching: + +```bash +aws ec2 associate-iam-instance-profile \ + --instance-id i-d2e09ff7de71b6341 \ + --iam-instance-profile Name=app-server-profile +``` + +If the profile is associated but contains no role, the `security-credentials/` listing is empty; add the role and credentials appear on the next request. diff --git a/docs/identity/imds/README.md b/docs/identity/imds/README.md index e9a9c0697..3e89ad652 100644 --- a/docs/identity/imds/README.md +++ b/docs/identity/imds/README.md @@ -153,7 +153,7 @@ Returns the decoded user data, or 404 if the instance was launched without any. ## IAM Role Credentials -Instances launched with an IAM instance profile get short-lived, auto-rotating credentials through IMDS — no static keys baked into the image. Creating roles and instance profiles is covered in the IAM Roles and Instance Profiles guide. +Instances launched with an IAM instance profile get short-lived, auto-rotating credentials through IMDS — no static keys baked into the image. Creating roles and instance profiles is covered in [IAM Roles and Instance Profiles](/docs/iam-roles-and-instance-profiles). ```bash # Discover the role name