diff --git a/deploy/eks-ci/eks-ci-role.yaml b/deploy/eks-ci/eks-ci-role.yaml new file mode 100644 index 0000000..e42b943 --- /dev/null +++ b/deploy/eks-ci/eks-ci-role.yaml @@ -0,0 +1,205 @@ +# The AWS-side identity the ksail "System Test - EKS" workflow assumes via +# GitHub Actions OIDC (step 2 of the rollout documented in kustomization.yaml; +# platform#2326 child 2). Trust is scoped to exactly one GitHub environment — +# repo:devantler-tech/ksail:environment:ci — through the account's GitHub OIDC +# provider (github-oidc-provider.yaml), so no long-lived key exists anywhere. +# maxSessionDuration is 7200s because the smoke job may run up to 120 min +# including cluster cleanup. +# +# Permissions are an INLINE policy (platform#2564: Policy/RolePolicyAttachment +# MRDs are deliberately not activated) modeled on eksctl's documented minimum +# policies, name-scoped to the workflow's st-eks-*/eksctl-st-eks-* naming +# wherever IAM supports resource-level scoping (CloudFormation stacks, IAM +# roles/instance profiles, EKS clusters). EC2/autoscaling/SSM-AMI actions are +# account-wide because eksctl drives them through CloudFormation with the +# caller's credentials and their create-time actions do not support name +# scoping; the blast radius remains bounded by the single-environment trust. +# iam:CreateRole is conditioned on the eks-ci-smoke-boundary permissions +# boundary (eks-ci-smoke-boundary.yaml) and no *PermissionsBoundary action is +# granted, so roles this principal creates (and can pass to EC2) are capped at +# the smoke-test compute plane even if an arbitrary policy is written to them. +apiVersion: iam.aws.m.upbound.io/v1beta1 +kind: Role +metadata: + name: eks-ci +spec: + forProvider: + description: >- + GitHub OIDC role for the ksail System Test - EKS smoke workflow + (repo:devantler-tech/ksail:environment:ci). Creates and deletes + throwaway st-eks-* EKS clusters via eksctl. + maxSessionDuration: 7200 + assumeRolePolicy: | + { + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Principal": { + "Federated": "arn:aws:iam::939001610192:oidc-provider/token.actions.githubusercontent.com" + }, + "Action": "sts:AssumeRoleWithWebIdentity", + "Condition": { + "StringEquals": { + "token.actions.githubusercontent.com:aud": "sts.amazonaws.com", + "token.actions.githubusercontent.com:sub": "repo:devantler-tech/ksail:environment:ci" + } + } + } + ] + } + inlinePolicy: + - name: eks-ci-smoke + policy: | + { + "Version": "2012-10-17", + "Statement": [ + { + "Sid": "CloudFormationScoped", + "Effect": "Allow", + "Action": "cloudformation:*", + "Resource": "arn:aws:cloudformation:*:939001610192:stack/eksctl-st-eks-*/*" + }, + { + "Sid": "CloudFormationRead", + "Effect": "Allow", + "Action": [ + "cloudformation:ListStacks", + "cloudformation:GetTemplateSummary" + ], + "Resource": "*" + }, + { + "Sid": "EksScoped", + "Effect": "Allow", + "Action": "eks:*", + "Resource": [ + "arn:aws:eks:*:939001610192:cluster/st-eks-*", + "arn:aws:eks:*:939001610192:nodegroup/st-eks-*/*/*", + "arn:aws:eks:*:939001610192:addon/st-eks-*/*/*", + "arn:aws:eks:*:939001610192:access-entry/st-eks-*/*/*/*/*", + "arn:aws:eks:*:939001610192:accessEntry/st-eks-*/*/*/*/*" + ] + }, + { + "Sid": "EksRead", + "Effect": "Allow", + "Action": [ + "eks:DescribeAddonVersions", + "eks:DescribeAddonConfiguration", + "eks:ListClusters" + ], + "Resource": "*" + }, + { + "Sid": "Ec2AndAutoscaling", + "Effect": "Allow", + "Action": [ + "ec2:*", + "autoscaling:*", + "elasticloadbalancing:*" + ], + "Resource": "*" + }, + { + "Sid": "SsmAmiLookup", + "Effect": "Allow", + "Action": "ssm:GetParameter", + "Resource": [ + "arn:aws:ssm:*::parameter/aws/service/eks/optimized-ami/*", + "arn:aws:ssm:*::parameter/aws/service/bottlerocket/*" + ] + }, + { + "Sid": "IamCreateRoleBounded", + "Effect": "Allow", + "Action": "iam:CreateRole", + "Resource": "arn:aws:iam::939001610192:role/eksctl-st-eks-*", + "Condition": { + "StringEquals": { + "iam:PermissionsBoundary": "arn:aws:iam::939001610192:policy/eks-ci-smoke-boundary" + } + } + }, + { + "Sid": "IamScopedRoleLifecycle", + "Effect": "Allow", + "Action": [ + "iam:DeleteRole", + "iam:GetRole", + "iam:TagRole", + "iam:UntagRole", + "iam:AttachRolePolicy", + "iam:DetachRolePolicy", + "iam:PutRolePolicy", + "iam:GetRolePolicy", + "iam:DeleteRolePolicy", + "iam:ListRolePolicies", + "iam:ListAttachedRolePolicies", + "iam:ListInstanceProfilesForRole", + "iam:PassRole" + ], + "Resource": "arn:aws:iam::939001610192:role/eksctl-st-eks-*" + }, + { + "Sid": "IamScopedInstanceProfiles", + "Effect": "Allow", + "Action": [ + "iam:CreateInstanceProfile", + "iam:DeleteInstanceProfile", + "iam:GetInstanceProfile", + "iam:TagInstanceProfile", + "iam:AddRoleToInstanceProfile", + "iam:RemoveRoleFromInstanceProfile" + ], + "Resource": "arn:aws:iam::939001610192:instance-profile/eksctl-st-eks-*" + }, + { + "Sid": "IamServiceLinkedRoles", + "Effect": "Allow", + "Action": "iam:CreateServiceLinkedRole", + "Resource": "*", + "Condition": { + "StringEquals": { + "iam:AWSServiceName": [ + "eks.amazonaws.com", + "eks-nodegroup.amazonaws.com", + "autoscaling.amazonaws.com", + "elasticloadbalancing.amazonaws.com" + ] + } + } + }, + { + "Sid": "IamOidcProviderLifecycle", + "Effect": "Allow", + "Action": [ + "iam:CreateOpenIDConnectProvider", + "iam:DeleteOpenIDConnectProvider", + "iam:TagOpenIDConnectProvider", + "iam:UntagOpenIDConnectProvider" + ], + "Resource": "arn:aws:iam::939001610192:oidc-provider/oidc.eks.*" + }, + { + "Sid": "IamReadForEksctl", + "Effect": "Allow", + "Action": [ + "iam:GetOpenIDConnectProvider", + "iam:ListPolicies", + "iam:GetPolicy", + "iam:GetPolicyVersion" + ], + "Resource": "*" + }, + { + "Sid": "StsIdentity", + "Effect": "Allow", + "Action": "sts:GetCallerIdentity", + "Resource": "*" + } + ] + } + providerConfigRef: + kind: ProviderConfig + name: default diff --git a/deploy/eks-ci/eks-ci-smoke-boundary.yaml b/deploy/eks-ci/eks-ci-smoke-boundary.yaml new file mode 100644 index 0000000..739d559 --- /dev/null +++ b/deploy/eks-ci/eks-ci-smoke-boundary.yaml @@ -0,0 +1,60 @@ +# Permissions boundary for every IAM role the eks-ci principal creates +# (eksctl-st-eks-* cluster/nodegroup/IRSA roles). eks-ci-role.yaml conditions +# iam:CreateRole on this boundary (iam:PermissionsBoundary), and the principal +# holds no iam:*PermissionsBoundary or role-update actions, so a workflow that +# assumes eks-ci cannot mint a role more privileged than this cap and pass it +# to EC2 to escape the smoke-test scope. The allowlist is the union of what +# eksctl-created cluster/node/IRSA roles exercise in the smoke path (EKS +# control plane, node compute/networking, ECR pulls, CNI, logs/metrics, SSM +# agent channels); deliberately NO iam:* or sts:AssumeRole* — a bounded role +# stays inside the compute plane even if an arbitrary inline policy is put on +# it. If a future smoke needs an action outside the cap, the failure names it +# and this file is the tuning point. +# +# Requires the policies.iam.aws.m.upbound.io MRD activated platform-side (the +# aws ManagedResourceActivationPolicy); merge that platform change first or +# this manifest sits Pending without error. +apiVersion: iam.aws.m.upbound.io/v1beta1 +kind: Policy +metadata: + name: eks-ci-smoke-boundary +spec: + forProvider: + description: >- + Permissions boundary capping IAM roles created by the eks-ci GitHub + OIDC role for throwaway st-eks-* smoke clusters. + policy: | + { + "Version": "2012-10-17", + "Statement": [ + { + "Sid": "SmokeComputePlaneCap", + "Effect": "Allow", + "Action": [ + "autoscaling:*", + "cloudwatch:*", + "ec2:*", + "ec2messages:*", + "ecr:BatchCheckLayerAvailability", + "ecr:BatchGetImage", + "ecr:GetAuthorizationToken", + "ecr:GetDownloadUrlForLayer", + "eks:*", + "eks-auth:AssumeRoleForPodIdentity", + "elasticloadbalancing:*", + "kms:CreateGrant", + "kms:DescribeKey", + "logs:*", + "ssm:GetParameter", + "ssm:GetParameters", + "ssmmessages:*", + "sts:GetCallerIdentity", + "sts:TagSession" + ], + "Resource": "*" + } + ] + } + providerConfigRef: + kind: ProviderConfig + name: default diff --git a/deploy/eks-ci/kustomization.yaml b/deploy/eks-ci/kustomization.yaml index 1f040c6..3f1597e 100644 --- a/deploy/eks-ci/kustomization.yaml +++ b/deploy/eks-ci/kustomization.yaml @@ -8,19 +8,20 @@ # Two-step rollout (the role's trust policy must name the provider's EXACT # ARN, which embeds the AWS account id — only observable after step 1 # reconciles; principal ARNs cannot be wildcarded): -# 1. (this slice) The GitHub OIDC identity provider. Reconciling it proves -# the OpenBao bootstrap credential end-to-end and surfaces the account id -# in status.atProvider.arn. -# 2. (follow-up) The `Role`: trust scoped to +# 1. (done) The GitHub OIDC identity provider. Reconciled on prod +# (Synced=True/Ready=True) after platform#2598 unblocked provider egress; +# its status.atProvider.arn is the Federated principal the role trusts. +# 2. (this slice) The `Role` (eks-ci-role.yaml): trust scoped to # repo:devantler-tech/ksail:environment:ci, maxSessionDuration 7200 (the # smoke job may run 120 min incl. cleanup), least-privilege permissions -# as an INLINE policy (per platform#2564: Policy/RolePolicyAttachment -# MRDs are deliberately NOT activated), scoped to the workflow's -# st-eks-*/eksctl-st-eks-* naming. Its ARN then goes to the ksail `ci` -# environment as AWS_OIDC_ROLE_ARN (child 3, the workflow's clean-skip -# gate). +# as an INLINE policy (platform#2564; the one Policy MR here is the +# permissions boundary capping roles eksctl creates), scoped to the workflow's +# st-eks-*/eksctl-st-eks-* naming. Once it reconciles, its ARN goes to +# the ksail `ci` environment as AWS_OIDC_ROLE_ARN (child 3, the +# workflow's clean-skip gate). apiVersion: kustomize.config.k8s.io/v1beta1 kind: Kustomization resources: - github-oidc-provider.yaml - + - eks-ci-smoke-boundary.yaml + - eks-ci-role.yaml