From e465697b828d6cd9d4805d0cf55b163f8bad9f6f Mon Sep 17 00:00:00 2001 From: Jason Aricheta Date: Mon, 6 Jul 2026 10:59:29 +1200 Subject: [PATCH] fix: restrict quickstart IAM user --- README.md | 21 ++--- docs/installation.md | 12 +-- docs/operations.md | 4 + docs/security.md | 12 ++- scripts/configure-quickstart-credentials.sh | 87 +-------------------- test/scripts/quickstart-credentials.test.sh | 23 +++--- 6 files changed, 44 insertions(+), 115 deletions(-) diff --git a/README.md b/README.md index 7cb83f9..db8df4a 100644 --- a/README.md +++ b/README.md @@ -32,15 +32,18 @@ export GITHUB_REPOSITORY=OWNER/PRIVATE_REPOSITORY scripts/setup-quickstart.sh ``` -The script creates the AWS resources and runner image, configures the -repository, creates a dedicated IAM user, rotates its static access key directly -into GitHub Actions secrets, and prompts for the classic PAT. It does not use an -infrastructure framework or write the AWS secret access key to disk. - -The Quickstart IAM user deliberately includes bootstrap, image build, and runner -lifecycle permissions. Use it only with private repositories and trusted -workflow changes. See [advanced credentials](docs/advanced-credentials.md) to -replace both long-lived credentials with GitHub OIDC and a GitHub App. +The script uses your existing local AWS credentials to create the AWS resources, +runner image, roles, and a dedicated IAM user. It rotates that user's static +access key directly into GitHub Actions secrets and prompts for the classic PAT. +It does not use an infrastructure framework or write the AWS secret access key +to disk. + +The stored IAM user is restricted to image building and runner lifecycle +operations. It cannot create or modify IAM identities, roles, policies, OIDC +providers, buckets, or log groups. Use it only with private repositories and +trusted workflow changes. See +[advanced credentials](docs/advanced-credentials.md) to replace both long-lived +credentials with GitHub OIDC and a GitHub App. ## Usage diff --git a/docs/installation.md b/docs/installation.md index 9d1759f..9e3ff2e 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -32,12 +32,12 @@ GH_PERSONAL_ACCESS_TOKEN=TOKEN scripts/setup-quickstart.sh The script: -1. creates or reconciles the S3 bucket, CloudWatch log groups, and image build - and runtime IAM roles; +1. uses the active local AWS credentials to create or reconcile the S3 bucket, + CloudWatch log groups, and image build and runtime IAM roles; 2. packages, uploads, validates, and activates the runner image; 3. configures the repository variables; 4. creates a dedicated `lambda-microvm-github-runner-quickstart` IAM user; -5. grants that user bootstrap, image build, and runner lifecycle permissions; +5. grants that user only image build and runner lifecycle permissions; 6. rotates its access key directly into the `AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY` GitHub Actions secrets; 7. sets the PAT as `GH_PERSONAL_ACCESS_TOKEN`. @@ -46,8 +46,10 @@ The secret access key is never written to the setup output or printed. Re-running the script reconciles resources, builds a new image version, and rotates the dedicated access key. -> **Quickstart security boundary:** These are long-lived credentials with broad -> product permissions. Use them only in private repositories where workflow +> **Quickstart security boundary:** The local credentials perform privileged +> setup. The stored long-lived credentials cannot mutate IAM resources and are +> limited to the configured image artifacts, exact build/runtime roles, and +> Lambda MicroVM lifecycle. Use them only in private repositories where workflow > changes are trusted. Never expose them to untrusted `pull_request_target` > workflows. diff --git a/docs/operations.md b/docs/operations.md index 31e0c86..d10f60c 100644 --- a/docs/operations.md +++ b/docs/operations.md @@ -10,6 +10,10 @@ export GITHUB_REPOSITORY=OWNER/REPOSITORY scripts/configure-quickstart-credentials.sh ``` +Run it with local AWS credentials allowed to manage that IAM user, its inline +policy, and its access keys. The stored Quickstart credentials intentionally +cannot rotate themselves or change IAM policy. + The helper installs the new secret pair before deleting the previous key. Rotate the classic PAT separately with: diff --git a/docs/security.md b/docs/security.md index b7755e0..9f214e5 100644 --- a/docs/security.md +++ b/docs/security.md @@ -8,9 +8,13 @@ and trusted workflow changes only. Public fork pull requests are unsupported. ## Credentials -- Quickstart stores a classic PAT with `repo` scope and a dedicated IAM user's - access key as GitHub Actions secrets. The IAM user can reconcile this - product's bootstrap resources, build images, and manage runner MicroVMs. +- Quickstart uses the operator's active local AWS credentials to create or + reconcile IAM roles, the IAM user, S3, log groups, and other bootstrap + resources. +- Quickstart stores a classic PAT with `repo` scope and the dedicated IAM user's + access key as GitHub Actions secrets. That IAM user can use the configured + image bucket, pass only the exact build/runtime roles, build images, and + manage runner MicroVMs. It cannot create or modify IAM resources. - Quickstart is limited to private repositories with trusted workflow changes. Rotate or delete both credentials when they are no longer needed. - Advanced setup uses a short-lived GitHub App installation token and obtains @@ -24,7 +28,7 @@ and trusted workflow changes only. Public fork pull requests are unsupported. `lambda:TerminateMicrovm`, because that API does not expose a per-instance IAM resource ARN. No other Lambda or application action is granted by it. - Deployment jobs should use a separate identity and must not inherit the - Quickstart IAM user's bootstrap permissions. + Quickstart IAM user's image-build or runner-lifecycle permissions. The classic PAT, AWS secret access key, and GitHub App private key never enter the MicroVM. diff --git a/scripts/configure-quickstart-credentials.sh b/scripts/configure-quickstart-credentials.sh index dbde4ef..26525a5 100755 --- a/scripts/configure-quickstart-credentials.sh +++ b/scripts/configure-quickstart-credentials.sh @@ -46,12 +46,6 @@ BUILD_ROLE_ARN="$(jq -er '.buildRoleArn' "${SETUP_FILE}")" readonly BUILD_ROLE_ARN EXECUTION_ROLE_ARN="$(jq -er '.executionRoleArn' "${SETUP_FILE}")" readonly EXECUTION_ROLE_ARN -GITHUB_ROLE_ARN="$(jq -er '.githubLaunchRoleArn' "${SETUP_FILE}")" -readonly GITHUB_ROLE_ARN -BUILD_LOG_GROUP="$(jq -er '.buildLogGroup' "${SETUP_FILE}")" -readonly BUILD_LOG_GROUP -RUNTIME_LOG_GROUP="$(jq -er '.runtimeLogGroup' "${SETUP_FILE}")" -readonly RUNTIME_LOG_GROUP export AWS_MAX_ATTEMPTS=6 export AWS_RETRY_MODE=standard @@ -68,12 +62,7 @@ partition="${partition%%:*}" readonly partition readonly USER_ARN="arn:${partition}:iam::${account_id}:user/${USER_NAME}" -readonly OIDC_PROVIDER_ARN="arn:${partition}:iam::${account_id}:oidc-provider/token.actions.githubusercontent.com" readonly BUCKET_ARN="arn:${partition}:s3:::${ARTIFACT_BUCKET}" -readonly BUILD_LOG_ARN="arn:${partition}:logs:${REGION}:${account_id}:log-group:${BUILD_LOG_GROUP}:*" -readonly RUNTIME_LOG_ARN="arn:${partition}:logs:${REGION}:${account_id}:log-group:${RUNTIME_LOG_GROUP}:*" -readonly BUILD_LOG_GROUP_ARN="arn:${partition}:logs:${REGION}:${account_id}:log-group:${BUILD_LOG_GROUP}" -readonly RUNTIME_LOG_GROUP_ARN="arn:${partition}:logs:${REGION}:${account_id}:log-group:${RUNTIME_LOG_GROUP}" readonly INTERNET_EGRESS_ARN="arn:${partition}:lambda:${REGION}:aws:network-connector:aws-network-connector:INTERNET_EGRESS" readonly NO_INGRESS_ARN="arn:${partition}:lambda:${REGION}:aws:network-connector:aws-network-connector:NO_INGRESS" @@ -84,16 +73,9 @@ cleanup() { trap cleanup EXIT jq -n \ - --arg userArn "${USER_ARN}" \ --arg bucketArn "${BUCKET_ARN}" \ --arg buildRoleArn "${BUILD_ROLE_ARN}" \ --arg executionRoleArn "${EXECUTION_ROLE_ARN}" \ - --arg githubRoleArn "${GITHUB_ROLE_ARN}" \ - --arg oidcProviderArn "${OIDC_PROVIDER_ARN}" \ - --arg buildLogArn "${BUILD_LOG_ARN}" \ - --arg runtimeLogArn "${RUNTIME_LOG_ARN}" \ - --arg buildLogGroupArn "${BUILD_LOG_GROUP_ARN}" \ - --arg runtimeLogGroupArn "${RUNTIME_LOG_GROUP_ARN}" \ --arg internetEgressArn "${INTERNET_EGRESS_ARN}" \ --arg noIngressArn "${NO_INGRESS_ARN}" \ '{ @@ -106,16 +88,11 @@ jq -n \ Resource: "*" }, { - Sid: "ManageArtifactBucket", + Sid: "UseArtifactBucket", Effect: "Allow", Action: [ - "s3:CreateBucket", "s3:GetBucketLocation", - "s3:ListBucket", - "s3:PutBucketEncryption", - "s3:PutBucketPublicAccessBlock", - "s3:PutBucketTagging", - "s3:PutBucketVersioning" + "s3:ListBucket" ], Resource: $bucketArn }, @@ -130,72 +107,12 @@ jq -n \ ], Resource: ($bucketArn + "/*") }, - { - Sid: "DiscoverLogGroups", - Effect: "Allow", - Action: "logs:DescribeLogGroups", - Resource: "*" - }, - { - Sid: "ManageProjectLogs", - Effect: "Allow", - Action: [ - "logs:CreateLogGroup", - "logs:CreateLogStream", - "logs:PutLogEvents", - "logs:PutRetentionPolicy", - "logs:TagResource" - ], - Resource: [ - $buildLogGroupArn, - $runtimeLogGroupArn, - $buildLogArn, - $runtimeLogArn - ] - }, - { - Sid: "ManageProjectRoles", - Effect: "Allow", - Action: [ - "iam:CreateRole", - "iam:GetRole", - "iam:PutRolePolicy", - "iam:TagRole", - "iam:UpdateAssumeRolePolicy" - ], - Resource: [$buildRoleArn, $executionRoleArn, $githubRoleArn] - }, { Sid: "PassProjectRoles", Effect: "Allow", Action: "iam:PassRole", Resource: [$buildRoleArn, $executionRoleArn] }, - { - Sid: "ManageGitHubOidcProvider", - Effect: "Allow", - Action: [ - "iam:AddClientIDToOpenIDConnectProvider", - "iam:CreateOpenIDConnectProvider", - "iam:GetOpenIDConnectProvider", - "iam:TagOpenIDConnectProvider" - ], - Resource: $oidcProviderArn - }, - { - Sid: "ManageOwnQuickstartCredentials", - Effect: "Allow", - Action: [ - "iam:CreateAccessKey", - "iam:CreateUser", - "iam:DeleteAccessKey", - "iam:GetUser", - "iam:ListAccessKeys", - "iam:PutUserPolicy", - "iam:TagUser" - ], - Resource: $userArn - }, { Sid: "ManageLambdaMicrovms", Effect: "Allow", diff --git a/test/scripts/quickstart-credentials.test.sh b/test/scripts/quickstart-credentials.test.sh index 3b0a32c..a55959a 100755 --- a/test/scripts/quickstart-credentials.test.sh +++ b/test/scripts/quickstart-credentials.test.sh @@ -118,18 +118,17 @@ run_case() { "arn:aws:iam::123456789012:user/runner-quickstart" ' "${case_directory}/setup.json" >/dev/null jq -e ' - any(.Statement[]; - (.Action | type) == "array" and - (.Action | index("lambda:*Microvm*")) != null - ) and - any(.Statement[]; - (.Action | type) == "array" and - (.Action | index("iam:CreateRole")) != null - ) and - any(.Statement[]; - (.Action | type) == "array" and - (.Action | index("s3:PutObject")) != null - ) + [.Statement[].Action] | flatten | . as $actions | + ($actions | index("lambda:*Microvm*")) != null and + ($actions | index("iam:PassRole")) != null and + ($actions | index("s3:PutObject")) != null and + ($actions | index("iam:CreateRole")) == null and + ($actions | index("iam:PutRolePolicy")) == null and + ($actions | index("iam:CreateUser")) == null and + ($actions | index("iam:CreateAccessKey")) == null and + ($actions | index("iam:CreateOpenIDConnectProvider")) == null and + ($actions | index("s3:CreateBucket")) == null and + ([$actions[] | select(startswith("logs:"))] | length) == 0 ' "${case_directory}/policy.json" >/dev/null grep -q "secret set.*--env-file -" \ "${case_directory}/gh.log"