Skip to content

Add team-scoped Terraform backend access to CI team roles#91

Merged
Alexanderamiri merged 1 commit into
mainfrom
fix/team-role-backend-access
Mar 17, 2026
Merged

Add team-scoped Terraform backend access to CI team roles#91
Alexanderamiri merged 1 commit into
mainfrom
fix/team-role-backend-access

Conversation

@Alexanderamiri
Copy link
Copy Markdown
Member

Summary

Team CI roles (javabin-ci-team-{team}) need access to the shared Terraform state bucket and lock table. Without this, terraform plan fails with AccessDeniedException on DynamoDB lock operations.

Access is fully team-scoped:

  • S3: Only apps/{team}/* prefix — teams can't read/write other teams' state
  • DynamoDB: LeadingKeys condition restricts lock operations to the team's own state paths

This maintains end-to-end ABAC isolation:

Layer Scope
S3 state apps/{team}/* only
DynamoDB locks Keys matching */apps/{team}/* only
Resource tags aws:ResourceTag/team == team
Resource names Boundary enforces {team}-* prefix

Test plan

  • Merge and apply
  • Re-run test app CI — tf-plan should acquire lock and succeed

Team roles need access to the shared state bucket and lock table.
- S3: scoped to apps/{team}/* prefix (can't touch other teams' state)
- DynamoDB: scoped via LeadingKeys to team's state paths only
@Alexanderamiri Alexanderamiri requested a review from a team as a code owner March 17, 2026 23:27
@Alexanderamiri Alexanderamiri enabled auto-merge (squash) March 17, 2026 23:28
@github-actions
Copy link
Copy Markdown

Terraform Plan

🚧 Changes detected — Plan: 0 to add, 1 to change, 0 to destroy.

Plan output
Acquiring state lock. This may take a few moments...

Terraform used the selected providers to generate the following execution
plan. Resource actions are indicated with the following symbols:
  ~ update in-place

Terraform will perform the following actions:

  # module.iam.aws_iam_role_policy.ci_team_allow["testteam"] will be updated in-place
  ~ resource "aws_iam_role_policy" "ci_team_allow" {
        id     = "javabin-ci-team-testteam:team-management"
        name   = "team-management"
      ~ policy = jsonencode(
          ~ {
              ~ Statement = [
                    {
                        Action    = "*"
                        Condition = {
                            StringEqualsIfExists = {
                                "aws:RequestTag/team"  = "testteam"
                                "aws:ResourceTag/team" = "testteam"
                            }
                        }
                        Effect    = "Allow"
                        Resource  = "*"
                        Sid       = "AllowWithTeamTagIsolation"
                    },
                  + {
                      + Action   = [
                          + "s3:GetObject",
                          + "s3:PutObject",
                          + "s3:DeleteObject",
                          + "s3:ListBucket",
                        ]
                      + Effect   = "Allow"
                      + Resource = [
                          + "arn:aws:s3:::javabin-terraform-state-553637109631",
                          + "arn:aws:s3:::javabin-terraform-state-553637109631/apps/testteam/*",
                        ]
                      + Sid      = "AllowTerraformBackend"
                    },
                  + {
                      + Action    = [
                          + "dynamodb:GetItem",
                          + "dynamodb:PutItem",
                          + "dynamodb:DeleteItem",
                        ]
                      + Condition = {
                          + "ForAllValues:StringLike" = {
                              + "dynamodb:LeadingKeys" = "javabin-terraform-state-553637109631/apps/testteam/*"
                            }
                        }
                      + Effect    = "Allow"
                      + Resource  = "arn:aws:dynamodb:eu-central-1:553637109631:table/javabin-terraform-app-locks"
                      + Sid       = "AllowTerraformLocking"
                    },
                ]
                # (1 unchanged attribute hidden)
            }
        )
        # (1 unchanged attribute hidden)
    }

Plan: 0 to add, 1 to change, 0 to destroy.

─────────────────────────────────────────────────────────────────────────────

Saved the plan to: tfplan

To perform exactly these actions, run the following command to apply:
    terraform apply "tfplan"

LLM Review

Risk: 🟢 LOW

Terraform plan adds S3 and DynamoDB permissions to the testteam CI role for Terraform state management.

  • [routine] Adding S3 backend access permissions (GetObject, PutObject, DeleteObject, ListBucket) to testteam CI role for Terraform state bucket and team-specific paths
  • [routine] Adding DynamoDB state locking permissions (GetItem, PutItem, DeleteItem) to testteam CI role with LeadingKeys condition restricting to team-specific paths
  • [routine] Single in-place policy update to existing IAM role with no resource destruction or creation
  • 🔒 [security] Permissions are properly scoped with team tag isolation and path-based restrictions, preventing cross-team access to other teams' Terraform state
  • 💰 [cost] No new billable resources being created; only policy modifications to existing infrastructure

@Alexanderamiri Alexanderamiri merged commit 9f7fc5f into main Mar 17, 2026
3 checks passed
@Alexanderamiri Alexanderamiri deleted the fix/team-role-backend-access branch March 17, 2026 23:28
Alexanderamiri added a commit that referenced this pull request May 9, 2026
## Summary
Team CI roles (`javabin-ci-team-{team}`) need access to the shared
Terraform state bucket and lock table. Without this, `terraform plan`
fails with `AccessDeniedException` on DynamoDB lock operations.

Access is fully team-scoped:
- **S3**: Only `apps/{team}/*` prefix — teams can't read/write other
teams' state
- **DynamoDB**: `LeadingKeys` condition restricts lock operations to the
team's own state paths

This maintains end-to-end ABAC isolation:
| Layer | Scope |
|-------|-------|
| S3 state | `apps/{team}/*` only |
| DynamoDB locks | Keys matching `*/apps/{team}/*` only |
| Resource tags | `aws:ResourceTag/team == team` |
| Resource names | Boundary enforces `{team}-*` prefix |

## Test plan
- [ ] Merge and apply
- [ ] Re-run test app CI — tf-plan should acquire lock and succeed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant