potiuk commented on code in PR #66:
URL: https://github.com/apache/airflow-ci-infra/pull/66#discussion_r4360741555


##########
terraform/runs-on-access/main.tf:
##########
@@ -0,0 +1,217 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+locals {
+  account_id = data.aws_caller_identity.current.account_id
+  stack_arn  = 
"arn:aws:cloudformation:us-east-2:${local.account_id}:stack/runs-on/*"
+  state      = "arn:aws:s3:::airflow-ci-tfstate/tf-state/runs-on.tfstate"
+
+  # Immutable OIDC subject (owner and repository IDs next to their names), so 
a renamed or
+  # re-created repository cannot match. Fork pull requests get no OIDC token 
at all, and the
+  # runs-on-prod environment deploys only from main after a required reviewer 
approves (.asf.yaml).
+  github_subject = "repo:apache@47359/airflow-ci-infra@321691041"
+  github_subjects = {
+    plan  = ["${local.github_subject}:pull_request", 
"${local.github_subject}:ref:refs/heads/main"]
+    apply = ["${local.github_subject}:environment:runs-on-prod"]
+  }
+
+  # IAM names the RunsOn template generates for the runs-on stack: 
<stack>-<logical id>-<suffix>.
+  # Matching the logical-ID prefixes rather than "runs-on-*" keeps every other 
role in the account,
+  # including the ones defined here, out of reach of the CloudFormation 
service role.
+  stack_role_prefixes = [
+    "RunsOn", "EC2Instance", "ManagedLaunchTemplate", "SchedulerInvoke", 
"StackConfigMaterializer",
+    "NetworkFirewallEndpointResolver", "AlertTopicSlackWebhook",
+  ]
+  stack_iam_resources = concat(
+    [for prefix in local.stack_role_prefixes : 
"arn:aws:iam::${local.account_id}:role/runs-on-${prefix}*"],
+    [
+      
"arn:aws:iam::${local.account_id}:instance-profile/runs-on-EC2InstanceProfile*",
+      "arn:aws:iam::${local.account_id}:role/aws-service-role/*",
+    ],
+  )
+}
+
+data "aws_caller_identity" "current" {}
+
+# Shared with apache/airflow's GithubActionApacheAirflow role, so it is read 
here, never managed.
+data "aws_iam_openid_connect_provider" "github" {
+  url = "https://token.actions.githubusercontent.com";
+}
+
+data "aws_iam_policy_document" "github_trust" {
+  for_each = local.github_subjects
+
+  statement {
+    actions = ["sts:AssumeRoleWithWebIdentity"]
+    principals {
+      type        = "Federated"
+      identifiers = [data.aws_iam_openid_connect_provider.github.arn]
+    }
+    condition {
+      test     = "StringEquals"
+      variable = "token.actions.githubusercontent.com:aud"
+      values   = ["sts.amazonaws.com"]
+    }
+    condition {
+      test     = "StringEquals"
+      variable = "token.actions.githubusercontent.com:sub"
+      values   = each.value
+    }
+  }
+}
+
+resource "aws_iam_role" "terraform" {
+  for_each = local.github_subjects
+
+  name               = "runs-on-terraform-${each.key}"
+  description        = "terraform ${each.key} of terraform/runs-on from 
apache/airflow-ci-infra GitHub Actions"
+  assume_role_policy = data.aws_iam_policy_document.github_trust[each.key].json
+}
+
+# What terraform plan needs: refresh the stack, prove the license-key 
parameter exists and read
+# the state. Plans run with -lock=false, so this policy grants no writes.
+data "aws_iam_policy_document" "read" {
+  statement {
+    actions   = ["cloudformation:DescribeStacks", 
"cloudformation:DescribeStackEvents", "cloudformation:GetTemplate"]
+    resources = [local.stack_arn]
+  }
+  statement {
+    actions   = ["ssm:GetParameter"]
+    resources = 
["arn:aws:ssm:us-east-2:${local.account_id}:parameter/runs-on/license-key"]
+  }
+  # The aws/ssm key policy lets any principal in the account decrypt through 
SSM, so
+  # GetParameter alone would also return the plain license key. Terraform 
never asks for it.
+  statement {
+    effect    = "Deny"
+    actions   = ["kms:Decrypt"]
+    resources = ["*"]
+  }
+  statement {
+    actions   = ["s3:ListBucket"]
+    resources = ["arn:aws:s3:::airflow-ci-tfstate"]
+    condition {
+      test     = "StringEquals"
+      variable = "s3:prefix"
+      values   = ["tf-state/runs-on.tfstate"]
+    }
+  }
+  statement {
+    actions   = ["s3:GetObject"]
+    resources = [local.state]
+  }
+}
+
+resource "aws_iam_role_policy" "read" {
+  for_each = aws_iam_role.terraform
+
+  name   = "terraform-read"
+  role   = each.value.name
+  policy = data.aws_iam_policy_document.read.json
+}
+
+# The apply role can only resubmit the stack from RunsOn's published 
templates. CloudFormation
+# then acts as the stack's service role below, so this role holds no 
infrastructure permissions.
+data "aws_iam_policy_document" "apply" {
+  statement {
+    actions   = ["cloudformation:UpdateStack"]
+    resources = [local.stack_arn]
+    condition {
+      test     = "StringLike"
+      variable = "cloudformation:TemplateUrl"
+      values   = 
["https://runs-on.s3.eu-west-1.amazonaws.com/cloudformation/template-v*.yaml";]
+    }
+  }
+  statement {
+    actions   = ["iam:PassRole"]
+    resources = [aws_iam_role.cloudformation.arn]
+    condition {
+      test     = "StringEquals"
+      variable = "iam:PassedToService"
+      values   = ["cloudformation.amazonaws.com"]
+    }
+  }
+  statement {
+    actions   = ["s3:PutObject"]
+    resources = [local.state]
+  }
+  statement {
+    actions   = ["s3:GetObject", "s3:PutObject", "s3:DeleteObject"]
+    resources = ["${local.state}.tflock"]
+  }
+}
+
+resource "aws_iam_role_policy" "apply" {
+  name   = "terraform-apply"
+  role   = aws_iam_role.terraform["apply"].name
+  policy = data.aws_iam_policy_document.apply.json
+}
+
+data "aws_iam_policy_document" "cloudformation_trust" {
+  statement {
+    actions = ["sts:AssumeRole"]
+    principals {
+      type        = "Service"
+      identifiers = ["cloudformation.amazonaws.com"]
+    }
+  }
+}
+
+resource "aws_iam_role" "cloudformation" {
+  name               = "runs-on-cloudformation"
+  description        = "Service role CloudFormation uses to update the runs-on 
stack"
+  assume_role_policy = data.aws_iam_policy_document.cloudformation_trust.json
+}
+
+# RunsOn documents only "admin rights" for deploying its stack: the template 
creates IAM, VPC,
+# ECS, Lambda, API Gateway, SQS, DynamoDB, S3, SNS and Budgets resources, and 
the set changes
+# between releases. AdministratorAccess follows that; the denies below keep 
the role away from
+# everything in the account that is not the stack's own.
+resource "aws_iam_role_policy_attachment" "cloudformation" {
+  role       = aws_iam_role.cloudformation.name
+  policy_arn = "arn:aws:iam::aws:policy/AdministratorAccess"
+}
+
+data "aws_iam_policy_document" "cloudformation_guardrails" {
+  statement {
+    sid    = "OnlyTheStackIam"
+    effect = "Deny"
+    actions = [
+      "iam:Add*", "iam:Attach*", "iam:Change*", "iam:Create*", 
"iam:Deactivate*", "iam:Delete*",
+      "iam:Detach*", "iam:Enable*", "iam:PassRole", "iam:Put*", "iam:Remove*", 
"iam:Reset*",
+      "iam:Set*", "iam:Tag*", "iam:Untag*", "iam:Update*", "iam:Upload*",
+    ]
+    not_resources = local.stack_iam_resources

Review Comment:
   The apply role can resubmit the allowed template with arbitrary parameters, 
and the v3.3.2 template attaches `AppCustomPolicy` to the ECS task role and 
pulls the control-plane image from `AppRegistry` ([template 
L7062](https://github.com/runs-on/runs-on/blob/v3.3.2/cloudformation/template.yaml#L7062)).
 Since this deny exempts `runs-on-RunsOn*` roles, 
`AppCustomPolicy=arn:aws:iam::aws:policy/AdministratorAccess` plus an 
attacker-controlled image yields a container with admin credentials — and none 
of these guardrails apply to it, so it could rewrite the plan/apply roles and 
the state. Suggest denying `iam:AttachRolePolicy` / `iam:PutRolePolicy` unless 
`iam:PolicyARN` is in an approved list (covering `AppCustomPolicy` and 
`RunnerCustomPolicy`), or requiring a permissions boundary on stack-created 
roles. Relatedly, `template-v*.yaml` accepts any published version, including 
older ones — worth pinning tighter if possible.



##########
terraform/runs-on-access/main.tf:
##########
@@ -0,0 +1,217 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+locals {
+  account_id = data.aws_caller_identity.current.account_id
+  stack_arn  = 
"arn:aws:cloudformation:us-east-2:${local.account_id}:stack/runs-on/*"
+  state      = "arn:aws:s3:::airflow-ci-tfstate/tf-state/runs-on.tfstate"
+
+  # Immutable OIDC subject (owner and repository IDs next to their names), so 
a renamed or
+  # re-created repository cannot match. Fork pull requests get no OIDC token 
at all, and the
+  # runs-on-prod environment deploys only from main after a required reviewer 
approves (.asf.yaml).
+  github_subject = "repo:apache@47359/airflow-ci-infra@321691041"
+  github_subjects = {
+    plan  = ["${local.github_subject}:pull_request", 
"${local.github_subject}:ref:refs/heads/main"]
+    apply = ["${local.github_subject}:environment:runs-on-prod"]
+  }
+
+  # IAM names the RunsOn template generates for the runs-on stack: 
<stack>-<logical id>-<suffix>.
+  # Matching the logical-ID prefixes rather than "runs-on-*" keeps every other 
role in the account,
+  # including the ones defined here, out of reach of the CloudFormation 
service role.
+  stack_role_prefixes = [
+    "RunsOn", "EC2Instance", "ManagedLaunchTemplate", "SchedulerInvoke", 
"StackConfigMaterializer",
+    "NetworkFirewallEndpointResolver", "AlertTopicSlackWebhook",
+  ]
+  stack_iam_resources = concat(
+    [for prefix in local.stack_role_prefixes : 
"arn:aws:iam::${local.account_id}:role/runs-on-${prefix}*"],
+    [
+      
"arn:aws:iam::${local.account_id}:instance-profile/runs-on-EC2InstanceProfile*",
+      "arn:aws:iam::${local.account_id}:role/aws-service-role/*",
+    ],
+  )
+}
+
+data "aws_caller_identity" "current" {}
+
+# Shared with apache/airflow's GithubActionApacheAirflow role, so it is read 
here, never managed.
+data "aws_iam_openid_connect_provider" "github" {
+  url = "https://token.actions.githubusercontent.com";
+}
+
+data "aws_iam_policy_document" "github_trust" {
+  for_each = local.github_subjects
+
+  statement {
+    actions = ["sts:AssumeRoleWithWebIdentity"]
+    principals {
+      type        = "Federated"
+      identifiers = [data.aws_iam_openid_connect_provider.github.arn]
+    }
+    condition {
+      test     = "StringEquals"
+      variable = "token.actions.githubusercontent.com:aud"
+      values   = ["sts.amazonaws.com"]
+    }
+    condition {
+      test     = "StringEquals"
+      variable = "token.actions.githubusercontent.com:sub"
+      values   = each.value
+    }
+  }
+}
+
+resource "aws_iam_role" "terraform" {
+  for_each = local.github_subjects
+
+  name               = "runs-on-terraform-${each.key}"
+  description        = "terraform ${each.key} of terraform/runs-on from 
apache/airflow-ci-infra GitHub Actions"
+  assume_role_policy = data.aws_iam_policy_document.github_trust[each.key].json
+}
+
+# What terraform plan needs: refresh the stack, prove the license-key 
parameter exists and read
+# the state. Plans run with -lock=false, so this policy grants no writes.
+data "aws_iam_policy_document" "read" {
+  statement {
+    actions   = ["cloudformation:DescribeStacks", 
"cloudformation:DescribeStackEvents", "cloudformation:GetTemplate"]
+    resources = [local.stack_arn]
+  }
+  statement {
+    actions   = ["ssm:GetParameter"]
+    resources = 
["arn:aws:ssm:us-east-2:${local.account_id}:parameter/runs-on/license-key"]
+  }
+  # The aws/ssm key policy lets any principal in the account decrypt through 
SSM, so
+  # GetParameter alone would also return the plain license key. Terraform 
never asks for it.
+  statement {
+    effect    = "Deny"
+    actions   = ["kms:Decrypt"]

Review Comment:
   The deny on `*` also covers S3, so if the state bucket uses (or later 
switches to) SSE-KMS, both roles lose read access to the state. Scoping it with 
`kms:ViaService = ssm.us-east-2.amazonaws.com` would keep the license-key 
protection without that side effect.



##########
.github/workflows/terraform-runs-on.yml:
##########
@@ -0,0 +1,109 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+---
+# Plans terraform/runs-on on pull requests and on main; applies the plan made 
on main once a
+# required reviewer approves the runs-on-prod environment. The AWS roles live 
in
+# terraform/runs-on-access and are applied by hand.
+name: Terraform (RunsOn)
+
+on:  # yamllint disable-line rule:truthy
+  pull_request:
+    paths:
+      - "terraform/runs-on/**"
+      - ".github/workflows/terraform-runs-on.yml"
+  push:
+    branches: [main]
+    paths:
+      - "terraform/runs-on/**"
+      - ".github/workflows/terraform-runs-on.yml"
+
+permissions: {}
+
+concurrency:
+  group: terraform-runs-on-${{ github.ref }}
+  cancel-in-progress: ${{ github.event_name == 'pull_request' }}
+
+env:
+  AWS_REGION: us-east-2
+  TF_IN_AUTOMATION: "true"
+  TF_INPUT: "false"
+
+jobs:
+  plan:
+    # Fork pull requests get no OIDC token, so they cannot plan.
+    if: >-
+      github.event_name == 'push' ||
+      github.event.pull_request.head.repo.full_name == github.repository
+    runs-on: ubuntu-24.04
+    permissions:
+      contents: read
+      id-token: write
+    defaults:
+      run:
+        working-directory: terraform/runs-on
+    steps:
+      - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1  # 
v7.0.1
+        with:
+          persist-credentials: false
+      - uses: 
aws-actions/configure-aws-credentials@e6de054238d6b7531b4efff3b6587d9aade6a06c  
# v6.2.3
+        with:
+          role-to-assume: arn:aws:iam::827901512104:role/runs-on-terraform-plan
+          aws-region: ${{ env.AWS_REGION }}
+      - uses: 
hashicorp/setup-terraform@dfe3c3f87815947d99a8997f908cb6525fc44e9e  # v4.0.1
+        with:
+          terraform_version: 1.16.2
+          terraform_wrapper: false
+      - run: terraform init -lockfile=readonly
+      # The plan role cannot write the lock file; apply takes the lock and 
refuses a stale plan.
+      - run: terraform plan -lock=false -out=runs-on.tfplan
+      - if: github.event_name == 'push'
+        uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a 
 # v7.0.1
+        with:
+          name: runs-on-tfplan
+          path: terraform/runs-on/runs-on.tfplan
+          retention-days: 1

Review Comment:
   The apply job waits for a reviewer, so a merge on Friday approved on Monday 
finds the artifact expired and the apply fails — with no `workflow_dispatch` to 
re-trigger. Maybe a longer retention plus a `workflow_dispatch` trigger?



##########
terraform/runs-on-access/main.tf:
##########
@@ -0,0 +1,217 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+locals {
+  account_id = data.aws_caller_identity.current.account_id
+  stack_arn  = 
"arn:aws:cloudformation:us-east-2:${local.account_id}:stack/runs-on/*"
+  state      = "arn:aws:s3:::airflow-ci-tfstate/tf-state/runs-on.tfstate"
+
+  # Immutable OIDC subject (owner and repository IDs next to their names), so 
a renamed or
+  # re-created repository cannot match. Fork pull requests get no OIDC token 
at all, and the
+  # runs-on-prod environment deploys only from main after a required reviewer 
approves (.asf.yaml).
+  github_subject = "repo:apache@47359/airflow-ci-infra@321691041"
+  github_subjects = {
+    plan  = ["${local.github_subject}:pull_request", 
"${local.github_subject}:ref:refs/heads/main"]
+    apply = ["${local.github_subject}:environment:runs-on-prod"]
+  }
+
+  # IAM names the RunsOn template generates for the runs-on stack: 
<stack>-<logical id>-<suffix>.
+  # Matching the logical-ID prefixes rather than "runs-on-*" keeps every other 
role in the account,
+  # including the ones defined here, out of reach of the CloudFormation 
service role.
+  stack_role_prefixes = [
+    "RunsOn", "EC2Instance", "ManagedLaunchTemplate", "SchedulerInvoke", 
"StackConfigMaterializer",
+    "NetworkFirewallEndpointResolver", "AlertTopicSlackWebhook",
+  ]
+  stack_iam_resources = concat(
+    [for prefix in local.stack_role_prefixes : 
"arn:aws:iam::${local.account_id}:role/runs-on-${prefix}*"],
+    [
+      
"arn:aws:iam::${local.account_id}:instance-profile/runs-on-EC2InstanceProfile*",
+      "arn:aws:iam::${local.account_id}:role/aws-service-role/*",
+    ],
+  )
+}
+
+data "aws_caller_identity" "current" {}
+
+# Shared with apache/airflow's GithubActionApacheAirflow role, so it is read 
here, never managed.
+data "aws_iam_openid_connect_provider" "github" {
+  url = "https://token.actions.githubusercontent.com";
+}
+
+data "aws_iam_policy_document" "github_trust" {
+  for_each = local.github_subjects
+
+  statement {
+    actions = ["sts:AssumeRoleWithWebIdentity"]
+    principals {
+      type        = "Federated"
+      identifiers = [data.aws_iam_openid_connect_provider.github.arn]
+    }
+    condition {
+      test     = "StringEquals"
+      variable = "token.actions.githubusercontent.com:aud"
+      values   = ["sts.amazonaws.com"]
+    }
+    condition {
+      test     = "StringEquals"
+      variable = "token.actions.githubusercontent.com:sub"
+      values   = each.value
+    }
+  }
+}
+
+resource "aws_iam_role" "terraform" {
+  for_each = local.github_subjects
+
+  name               = "runs-on-terraform-${each.key}"
+  description        = "terraform ${each.key} of terraform/runs-on from 
apache/airflow-ci-infra GitHub Actions"
+  assume_role_policy = data.aws_iam_policy_document.github_trust[each.key].json
+}
+
+# What terraform plan needs: refresh the stack, prove the license-key 
parameter exists and read
+# the state. Plans run with -lock=false, so this policy grants no writes.
+data "aws_iam_policy_document" "read" {
+  statement {
+    actions   = ["cloudformation:DescribeStacks", 
"cloudformation:DescribeStackEvents", "cloudformation:GetTemplate"]
+    resources = [local.stack_arn]
+  }
+  statement {
+    actions   = ["ssm:GetParameter"]
+    resources = 
["arn:aws:ssm:us-east-2:${local.account_id}:parameter/runs-on/license-key"]
+  }
+  # The aws/ssm key policy lets any principal in the account decrypt through 
SSM, so
+  # GetParameter alone would also return the plain license key. Terraform 
never asks for it.
+  statement {
+    effect    = "Deny"
+    actions   = ["kms:Decrypt"]
+    resources = ["*"]
+  }
+  statement {
+    actions   = ["s3:ListBucket"]
+    resources = ["arn:aws:s3:::airflow-ci-tfstate"]
+    condition {
+      test     = "StringEquals"
+      variable = "s3:prefix"
+      values   = ["tf-state/runs-on.tfstate"]
+    }
+  }
+  statement {
+    actions   = ["s3:GetObject"]
+    resources = [local.state]
+  }
+}
+
+resource "aws_iam_role_policy" "read" {
+  for_each = aws_iam_role.terraform
+
+  name   = "terraform-read"
+  role   = each.value.name
+  policy = data.aws_iam_policy_document.read.json
+}
+
+# The apply role can only resubmit the stack from RunsOn's published 
templates. CloudFormation
+# then acts as the stack's service role below, so this role holds no 
infrastructure permissions.
+data "aws_iam_policy_document" "apply" {
+  statement {
+    actions   = ["cloudformation:UpdateStack"]
+    resources = [local.stack_arn]
+    condition {
+      test     = "StringLike"
+      variable = "cloudformation:TemplateUrl"
+      values   = 
["https://runs-on.s3.eu-west-1.amazonaws.com/cloudformation/template-v*.yaml";]
+    }
+  }
+  statement {
+    actions   = ["iam:PassRole"]
+    resources = [aws_iam_role.cloudformation.arn]
+    condition {
+      test     = "StringEquals"
+      variable = "iam:PassedToService"
+      values   = ["cloudformation.amazonaws.com"]
+    }
+  }
+  statement {
+    actions   = ["s3:PutObject"]
+    resources = [local.state]
+  }
+  statement {
+    actions   = ["s3:GetObject", "s3:PutObject", "s3:DeleteObject"]
+    resources = ["${local.state}.tflock"]
+  }
+}
+
+resource "aws_iam_role_policy" "apply" {
+  name   = "terraform-apply"
+  role   = aws_iam_role.terraform["apply"].name
+  policy = data.aws_iam_policy_document.apply.json
+}
+
+data "aws_iam_policy_document" "cloudformation_trust" {
+  statement {
+    actions = ["sts:AssumeRole"]
+    principals {
+      type        = "Service"
+      identifiers = ["cloudformation.amazonaws.com"]

Review Comment:
   This role carries `AdministratorAccess` (minus guardrails) but any principal 
with a broad `iam:PassRole` can hand it to CloudFormation for an unrelated 
stack. Consider adding `aws:SourceAccount` and `aws:SourceArn` 
(`arn:aws:cloudformation:us-east-2:<acct>:stack/runs-on/*`) conditions to the 
trust policy.



##########
.github/workflows/terraform-runs-on.yml:
##########
@@ -0,0 +1,109 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+---
+# Plans terraform/runs-on on pull requests and on main; applies the plan made 
on main once a
+# required reviewer approves the runs-on-prod environment. The AWS roles live 
in
+# terraform/runs-on-access and are applied by hand.
+name: Terraform (RunsOn)
+
+on:  # yamllint disable-line rule:truthy
+  pull_request:
+    paths:
+      - "terraform/runs-on/**"
+      - ".github/workflows/terraform-runs-on.yml"
+  push:
+    branches: [main]
+    paths:
+      - "terraform/runs-on/**"
+      - ".github/workflows/terraform-runs-on.yml"
+
+permissions: {}
+
+concurrency:
+  group: terraform-runs-on-${{ github.ref }}
+  cancel-in-progress: ${{ github.event_name == 'pull_request' }}
+
+env:
+  AWS_REGION: us-east-2
+  TF_IN_AUTOMATION: "true"
+  TF_INPUT: "false"
+
+jobs:
+  plan:
+    # Fork pull requests get no OIDC token, so they cannot plan.
+    if: >-
+      github.event_name == 'push' ||
+      github.event.pull_request.head.repo.full_name == github.repository
+    runs-on: ubuntu-24.04
+    permissions:
+      contents: read
+      id-token: write
+    defaults:
+      run:
+        working-directory: terraform/runs-on
+    steps:
+      - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1  # 
v7.0.1
+        with:
+          persist-credentials: false
+      - uses: 
aws-actions/configure-aws-credentials@e6de054238d6b7531b4efff3b6587d9aade6a06c  
# v6.2.3
+        with:
+          role-to-assume: arn:aws:iam::827901512104:role/runs-on-terraform-plan
+          aws-region: ${{ env.AWS_REGION }}
+      - uses: 
hashicorp/setup-terraform@dfe3c3f87815947d99a8997f908cb6525fc44e9e  # v4.0.1
+        with:
+          terraform_version: 1.16.2
+          terraform_wrapper: false
+      - run: terraform init -lockfile=readonly
+      # The plan role cannot write the lock file; apply takes the lock and 
refuses a stale plan.
+      - run: terraform plan -lock=false -out=runs-on.tfplan
+      - if: github.event_name == 'push'
+        uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a 
 # v7.0.1
+        with:
+          name: runs-on-tfplan
+          path: terraform/runs-on/runs-on.tfplan
+          retention-days: 1
+
+  apply:

Review Comment:
   Every matching push to main creates a pending `runs-on-prod` deployment, 
even when the plan is empty (e.g. README-only edits). `terraform plan 
-detailed-exitcode` exposed as a `has_changes` job output, and `if: 
needs.plan.outputs.has_changes == 'true'` here, would avoid approving no-op 
applies.



##########
terraform/runs-on-access/main.tf:
##########
@@ -0,0 +1,217 @@
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you under the Apache License, Version 2.0 (the
+# "License"); you may not use this file except in compliance
+# with the License.  You may obtain a copy of the License at
+#
+#   http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing,
+# software distributed under the License is distributed on an
+# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+# KIND, either express or implied.  See the License for the
+# specific language governing permissions and limitations
+# under the License.
+
+locals {
+  account_id = data.aws_caller_identity.current.account_id
+  stack_arn  = 
"arn:aws:cloudformation:us-east-2:${local.account_id}:stack/runs-on/*"
+  state      = "arn:aws:s3:::airflow-ci-tfstate/tf-state/runs-on.tfstate"
+
+  # Immutable OIDC subject (owner and repository IDs next to their names), so 
a renamed or
+  # re-created repository cannot match. Fork pull requests get no OIDC token 
at all, and the
+  # runs-on-prod environment deploys only from main after a required reviewer 
approves (.asf.yaml).
+  github_subject = "repo:apache@47359/airflow-ci-infra@321691041"
+  github_subjects = {
+    plan  = ["${local.github_subject}:pull_request", 
"${local.github_subject}:ref:refs/heads/main"]
+    apply = ["${local.github_subject}:environment:runs-on-prod"]
+  }
+
+  # IAM names the RunsOn template generates for the runs-on stack: 
<stack>-<logical id>-<suffix>.
+  # Matching the logical-ID prefixes rather than "runs-on-*" keeps every other 
role in the account,
+  # including the ones defined here, out of reach of the CloudFormation 
service role.
+  stack_role_prefixes = [
+    "RunsOn", "EC2Instance", "ManagedLaunchTemplate", "SchedulerInvoke", 
"StackConfigMaterializer",
+    "NetworkFirewallEndpointResolver", "AlertTopicSlackWebhook",
+  ]
+  stack_iam_resources = concat(
+    [for prefix in local.stack_role_prefixes : 
"arn:aws:iam::${local.account_id}:role/runs-on-${prefix}*"],
+    [
+      
"arn:aws:iam::${local.account_id}:instance-profile/runs-on-EC2InstanceProfile*",
+      "arn:aws:iam::${local.account_id}:role/aws-service-role/*",
+    ],
+  )
+}
+
+data "aws_caller_identity" "current" {}
+
+# Shared with apache/airflow's GithubActionApacheAirflow role, so it is read 
here, never managed.
+data "aws_iam_openid_connect_provider" "github" {
+  url = "https://token.actions.githubusercontent.com";
+}
+
+data "aws_iam_policy_document" "github_trust" {
+  for_each = local.github_subjects
+
+  statement {
+    actions = ["sts:AssumeRoleWithWebIdentity"]
+    principals {
+      type        = "Federated"
+      identifiers = [data.aws_iam_openid_connect_provider.github.arn]
+    }
+    condition {
+      test     = "StringEquals"
+      variable = "token.actions.githubusercontent.com:aud"
+      values   = ["sts.amazonaws.com"]
+    }
+    condition {
+      test     = "StringEquals"
+      variable = "token.actions.githubusercontent.com:sub"
+      values   = each.value
+    }
+  }
+}
+
+resource "aws_iam_role" "terraform" {
+  for_each = local.github_subjects
+
+  name               = "runs-on-terraform-${each.key}"
+  description        = "terraform ${each.key} of terraform/runs-on from 
apache/airflow-ci-infra GitHub Actions"
+  assume_role_policy = data.aws_iam_policy_document.github_trust[each.key].json
+}
+
+# What terraform plan needs: refresh the stack, prove the license-key 
parameter exists and read
+# the state. Plans run with -lock=false, so this policy grants no writes.
+data "aws_iam_policy_document" "read" {
+  statement {
+    actions   = ["cloudformation:DescribeStacks", 
"cloudformation:DescribeStackEvents", "cloudformation:GetTemplate"]
+    resources = [local.stack_arn]
+  }
+  statement {
+    actions   = ["ssm:GetParameter"]
+    resources = 
["arn:aws:ssm:us-east-2:${local.account_id}:parameter/runs-on/license-key"]
+  }
+  # The aws/ssm key policy lets any principal in the account decrypt through 
SSM, so
+  # GetParameter alone would also return the plain license key. Terraform 
never asks for it.
+  statement {
+    effect    = "Deny"
+    actions   = ["kms:Decrypt"]
+    resources = ["*"]
+  }
+  statement {
+    actions   = ["s3:ListBucket"]
+    resources = ["arn:aws:s3:::airflow-ci-tfstate"]
+    condition {
+      test     = "StringEquals"
+      variable = "s3:prefix"

Review Comment:
   The S3 backend also lists the bucket with the workspace prefix (`env:/`) 
during `terraform init`, so with only `tf-state/runs-on.tfstate` allowed, init 
is likely to fail with "Failed to get existing workspaces" for both roles. The 
PR's plan run used admin credentials, so this path hasn't been exercised. 
Adding `env:/*` (or `StringLike` on both) should fix it.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to