Skip to content

Add CI broker Lambda for hard team validation, fix QA items#74

Merged
Alexanderamiri merged 1 commit into
mainfrom
fix/ci-broker-and-qa
Mar 16, 2026
Merged

Add CI broker Lambda for hard team validation, fix QA items#74
Alexanderamiri merged 1 commit into
mainfrom
fix/ci-broker-and-qa

Conversation

@Alexanderamiri
Copy link
Copy Markdown
Member

Summary

CI Broker (security hardening)

Replaces direct OIDC → team role assumption with a Lambda-brokered pattern:

Before: Workflow → OIDC → javabin-ci-team-{team} (workflow resolves team - soft)
After:  Workflow → OIDC → javabin-ci-app-broker → Lambda → validates team via GitHub API → javabin-ci-team-{team} (hard)
  • Team/deploy roles no longer have OIDC trust. Only the broker and apply-gate Lambdas can assume them.
  • javabin-ci-app-broker OIDC role can ONLY invoke the broker Lambda (zero AWS resource access).
  • Broker Lambda calls GitHub API to validate repo→team membership, then returns STS credentials.
  • Same pattern as apply-gate, but for plan and deploy phases.

QA fixes

  • FAILED risk propagation: No longer hard-fails the plan step. Propagates to apply gate where the override flow handles it (enables deploys when Bedrock is down).
  • Team in state path: apps/{team}/{repo}/terraform.tfstate prevents cross-team state collision.
  • Password-set rate limiting: Lambda reserved concurrency = 5 (prevents brute force).
  • Apply-gate: Updated to assume ci-team-* roles (was ci-app-*).

Test plan

  • terraform plan — verify broker Lambda, IAM roles, trust policies
  • Test app repo CI: broker role → invoke Lambda → receive team credentials
  • Verify repo not in a team gets clear error from broker
  • Verify Bedrock failure produces FAILED risk that can be overridden
  • Verify password-set Lambda has reserved_concurrent_executions = 5

CI Broker (security hardening):
- New ci-broker Lambda validates repo→team via GitHub API before
  issuing STS credentials. Team/deploy roles now trust ONLY the
  broker and apply-gate Lambdas — no direct OIDC assumption.
- New javabin-ci-app-broker OIDC role can ONLY invoke the broker
  Lambda (no AWS resource access)
- Workflows assume broker role → call Lambda → receive team-scoped
  credentials. Team check is now Lambda-enforced (hard), not
  workflow-enforced (soft).

QA fixes:
- FAILED risk now propagates to apply gate (enables override when
  Bedrock is down, instead of hard-failing the plan step)
- S3 state path includes team: apps/{team}/{repo}/terraform.tfstate
- Password-set Lambda limited to 5 concurrent executions (rate limit)
- Apply-gate updated to assume ci-team-* roles (was ci-app-*)
@github-actions
Copy link
Copy Markdown

Terraform Plan

🚧 Changes detected — Plan: 6 to add, 4 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:
  + create
  ~ update in-place

Terraform will perform the following actions:

  # module.iam.aws_iam_role.ci_app_broker will be created
  + resource "aws_iam_role" "ci_app_broker" {
      + arn                   = (known after apply)
      + assume_role_policy    = jsonencode(
            {
              + Statement = [
                  + {
                      + Action    = "sts:AssumeRoleWithWebIdentity"
                      + Condition = {
                          + StringEquals = {
                              + "token.actions.githubusercontent.com:aud" = "sts.amazonaws.com"
                            }
                          + StringLike   = {
                              + "token.actions.githubusercontent.com:job_workflow_ref" = [
                                  + "javaBin/platform/.github/workflows/tf-plan.yml@refs/heads/main",
                                  + "javaBin/platform/.github/workflows/docker-build.yml@refs/heads/main",
                                  + "javaBin/platform/.github/workflows/ecs-deploy.yml@refs/heads/main",
                                ]
                              + "token.actions.githubusercontent.com:sub"              = "repo:javaBin/*:*"
                            }
                        }
                      + Effect    = "Allow"
                      + Principal = {
                          + Federated = "arn:aws:iam::553637109631:oidc-provider/token.actions.githubusercontent.com"
                        }
                    },
                ]
              + Version   = "2012-10-17"
            }
        )
      + create_date           = (known after apply)
      + force_detach_policies = false
      + id                    = (known after apply)
      + managed_policy_arns   = (known after apply)
      + max_session_duration  = 3600
      + name                  = "javabin-ci-app-broker"
      + name_prefix           = (known after apply)
      + path                  = "/"
      + permissions_boundary  = "arn:aws:iam::553637109631:policy/javabin-developer-boundary"
      + tags_all              = {
          + "environment" = "production"
          + "managed-by"  = "terraform"
          + "project"     = "javabin"
          + "team"        = "javabin"
        }
      + unique_id             = (known after apply)
    }

  # module.iam.aws_iam_role.ci_deploy["platform-test-team"] will be updated in-place
  ~ resource "aws_iam_role" "ci_deploy" {
      ~ assume_role_policy    = jsonencode(
            {
              - Statement = [
                  - {
                      - Action    = "sts:AssumeRoleWithWebIdentity"
                      - Condition = {
                          - StringEquals = {
                              - "token.actions.githubusercontent.com:aud" = "sts.amazonaws.com"
                            }
                          - StringLike   = {
                              - "token.actions.githubusercontent.com:job_workflow_ref" = [
                                  - "javaBin/platform/.github/workflows/docker-build.yml@refs/heads/main",
                                  - "javaBin/platform/.github/workflows/ecs-deploy.yml@refs/heads/main",
                                ]
                              - "token.actions.githubusercontent.com:sub"              = "repo:javaBin/*:*"
                            }
                        }
                      - Effect    = "Allow"
                      - Principal = {
                          - Federated = "arn:aws:iam::553637109631:oidc-provider/token.actions.githubusercontent.com"
                        }
                    },
                ]
              - Version   = "2012-10-17"
            }
        ) -> (known after apply)
        id                    = "javabin-ci-deploy-platform-test-team"
        name                  = "javabin-ci-deploy-platform-test-team"
        tags                  = {
            "Name" = "javabin-ci-deploy-platform-test-team"
            "team" = "platform-test-team"
        }
        # (9 unchanged attributes hidden)

        # (4 unchanged blocks hidden)
    }

  # module.iam.aws_iam_role.ci_team["platform-test-team"] will be updated in-place
  ~ resource "aws_iam_role" "ci_team" {
      ~ assume_role_policy    = jsonencode(
            {
              - Statement = [
                  - {
                      - Action    = "sts:AssumeRoleWithWebIdentity"
                      - Condition = {
                          - StringEquals = {
                              - "token.actions.githubusercontent.com:aud" = "sts.amazonaws.com"
                            }
                          - StringLike   = {
                              - "token.actions.githubusercontent.com:job_workflow_ref" = [
                                  - "javaBin/platform/.github/workflows/tf-plan.yml@refs/heads/main",
                                ]
                              - "token.actions.githubusercontent.com:sub"              = "repo:javaBin/*:*"
                            }
                        }
                      - Effect    = "Allow"
                      - Principal = {
                          - Federated = "arn:aws:iam::553637109631:oidc-provider/token.actions.githubusercontent.com"
                        }
                      - Sid       = "AllowPlanViaOIDC"
                    },
                  - {
                      - Action    = "sts:AssumeRole"
                      - Effect    = "Allow"
                      - Principal = {
                          - AWS = "arn:aws:iam::553637109631:role/javabin-apply-gate"
                        }
                      - Sid       = "AllowApplyViaGateLambda"
                    },
                ]
              - Version   = "2012-10-17"
            }
        ) -> (known after apply)
        id                    = "javabin-ci-team-platform-test-team"
        name                  = "javabin-ci-team-platform-test-team"
        tags                  = {
            "Name" = "javabin-ci-team-platform-test-team"
            "team" = "platform-test-team"
        }
        # (9 unchanged attributes hidden)

        # (2 unchanged blocks hidden)
    }

  # module.iam.aws_iam_role_policy.ci_app_broker will be created
  + resource "aws_iam_role_policy" "ci_app_broker" {
      + id          = (known after apply)
      + name        = "invoke-ci-broker-only"
      + name_prefix = (known after apply)
      + policy      = (known after apply)
      + role        = (known after apply)
    }

  # module.lambdas.aws_iam_role.ci_broker will be created
  + resource "aws_iam_role" "ci_broker" {
      + arn                   = (known after apply)
      + assume_role_policy    = jsonencode(
            {
              + Statement = [
                  + {
                      + Action    = "sts:AssumeRole"
                      + Effect    = "Allow"
                      + Principal = {
                          + Service = "lambda.amazonaws.com"
                        }
                    },
                ]
              + Version   = "2012-10-17"
            }
        )
      + create_date           = (known after apply)
      + force_detach_policies = false
      + id                    = (known after apply)
      + managed_policy_arns   = (known after apply)
      + max_session_duration  = 3600
      + name                  = "javabin-ci-broker"
      + name_prefix           = (known after apply)
      + path                  = "/"
      + permissions_boundary  = "arn:aws:iam::553637109631:policy/javabin-developer-boundary"
      + tags_all              = {
          + "environment" = "production"
          + "managed-by"  = "terraform"
          + "project"     = "javabin"
          + "team"        = "javabin"
        }
      + unique_id             = (known after apply)
    }

  # module.lambdas.aws_iam_role_policy.apply_gate will be updated in-place
  ~ resource "aws_iam_role_policy" "apply_gate" {
        id     = "javabin-apply-gate:javabin-apply-gate"
        name   = "javabin-apply-gate"
      ~ policy = jsonencode(
          ~ {
              ~ Statement = [
                    # (1 unchanged element hidden)
                    {
                        Action   = [
                            "s3:GetObject",
                            "s3:PutObject",
                        ]
                        Effect   = "Allow"
                        Resource = "arn:aws:s3:::javabin-ci-plan-artifacts-553637109631/*"
                        Sid      = "PlanBucketAccess"
                    },
                  ~ {
                      ~ Resource = "arn:aws:iam::553637109631:role/javabin-ci-app-*" -> "arn:aws:iam::553637109631:role/javabin-ci-team-*"
                      ~ Sid      = "AssumeAppRoles" -> "AssumeTeamRoles"
                        # (2 unchanged attributes hidden)
                    },
                ]
                # (1 unchanged attribute hidden)
            }
        )
        # (1 unchanged attribute hidden)
    }

  # module.lambdas.aws_iam_role_policy.ci_broker will be created
  + resource "aws_iam_role_policy" "ci_broker" {
      + id          = (known after apply)
      + name        = "javabin-ci-broker"
      + name_prefix = (known after apply)
      + policy      = jsonencode(
            {
              + Statement = [
                  + {
                      + Action   = "ssm:GetParameter"
                      + Effect   = "Allow"
                      + Resource = [
                          + "arn:aws:ssm:eu-central-1:553637109631:parameter/javabin/platform/github-app-id",
                          + "arn:aws:ssm:eu-central-1:553637109631:parameter/javabin/platform/github-app-key",
                        ]
                      + Sid      = "ReadGitHubAppCredentials"
                    },
                  + {
                      + Action   = "sts:AssumeRole"
                      + Effect   = "Allow"
                      + Resource = [
                          + "arn:aws:iam::553637109631:role/javabin-ci-team-*",
                          + "arn:aws:iam::553637109631:role/javabin-ci-deploy-*",
                        ]
                      + Sid      = "AssumeTeamAndDeployRoles"
                    },
                ]
              + Version   = "2012-10-17"
            }
        )
      + role        = (known after apply)
    }

  # module.lambdas.aws_iam_role_policy_attachment.ci_broker_logs will be created
  + resource "aws_iam_role_policy_attachment" "ci_broker_logs" {
      + id         = (known after apply)
      + policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
      + role       = "javabin-ci-broker"
    }

  # module.lambdas.aws_lambda_function.ci_broker will be created
  + resource "aws_lambda_function" "ci_broker" {
      + architectures                  = (known after apply)
      + arn                            = (known after apply)
      + code_sha256                    = (known after apply)
      + filename                       = "lambdas/builds/ci_broker.zip"
      + function_name                  = "javabin-ci-broker"
      + handler                        = "handler.handler"
      + id                             = (known after apply)
      + invoke_arn                     = (known after apply)
      + last_modified                  = (known after apply)
      + memory_size                    = 128
      + package_type                   = "Zip"
      + publish                        = false
      + qualified_arn                  = (known after apply)
      + qualified_invoke_arn           = (known after apply)
      + reserved_concurrent_executions = -1
      + role                           = (known after apply)
      + runtime                        = "python3.12"
      + signing_job_arn                = (known after apply)
      + signing_profile_version_arn    = (known after apply)
      + skip_destroy                   = false
      + source_code_hash               = "qe8GUbBJMIBMAk8k1XkaphEsodx8ALgmTCycQ5a+RUI="
      + source_code_size               = (known after apply)
      + tags_all                       = {
          + "environment" = "production"
          + "managed-by"  = "terraform"
          + "project"     = "javabin"
          + "team"        = "javabin"
        }
      + timeout                        = 30
      + version                        = (known after apply)

      + environment {
          + variables = {
              + "AWS_ACCOUNT_ID"       = "553637109631"
              + "GITHUB_APP_ID_PARAM"  = "/javabin/platform/github-app-id"
              + "GITHUB_APP_KEY_PARAM" = "/javabin/platform/github-app-key"
              + "GITHUB_ORG"           = "javaBin"
              + "PROJECT"              = "javabin"
            }
        }
    }

  # module.lambdas.aws_lambda_function.password_set will be updated in-place
  ~ resource "aws_lambda_function" "password_set" {
        id                             = "javabin-password-set"
      ~ reserved_concurrent_executions = -1 -> 5
        tags                           = {}
        # (22 unchanged attributes hidden)

        # (4 unchanged blocks hidden)
    }

Plan: 6 to add, 4 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

Plan creates a new CI broker Lambda function and updates IAM roles to support GitHub Actions workflows, with routine Lambda concurrency tuning.

  • [routine] New Lambda function 'ci_broker' created with standard Python 3.12 runtime, 128MB memory, 30s timeout. Includes IAM role with permissions boundary and basic execution logs policy.
  • [routine] IAM role 'ci_app_broker' created for GitHub Actions OIDC federation with restricted assume role policy limited to specific workflows (tf-plan, docker-build, ecs-deploy) in javaBin organization.
  • [routine] Password-set Lambda concurrency limit increased from unlimited (-1) to 5 reserved concurrent executions, preventing runaway scaling.
  • [routine] Apply-gate Lambda policy updated to assume 'javabin-ci-team-' roles instead of 'javabin-ci-app-' roles, aligning with new role naming convention.
  • 🔒 [security] CI broker Lambda has permissions to read GitHub App credentials from SSM Parameter Store and assume team/deploy roles. Credentials are properly stored in SSM (not hardcoded) and access is scoped to specific parameters.

@Alexanderamiri Alexanderamiri merged commit f598638 into main Mar 16, 2026
3 checks passed
@Alexanderamiri Alexanderamiri deleted the fix/ci-broker-and-qa branch March 16, 2026 22:18
Alexanderamiri added a commit that referenced this pull request May 9, 2026
## Summary

### CI Broker (security hardening)
Replaces direct OIDC → team role assumption with a Lambda-brokered
pattern:

```
Before: Workflow → OIDC → javabin-ci-team-{team} (workflow resolves team - soft)
After:  Workflow → OIDC → javabin-ci-app-broker → Lambda → validates team via GitHub API → javabin-ci-team-{team} (hard)
```

- **Team/deploy roles no longer have OIDC trust.** Only the broker and
apply-gate Lambdas can assume them.
- **javabin-ci-app-broker** OIDC role can ONLY invoke the broker Lambda
(zero AWS resource access).
- **Broker Lambda** calls GitHub API to validate repo→team membership,
then returns STS credentials.
- Same pattern as apply-gate, but for plan and deploy phases.

### QA fixes
- **FAILED risk propagation**: No longer hard-fails the plan step.
Propagates to apply gate where the override flow handles it (enables
deploys when Bedrock is down).
- **Team in state path**: `apps/{team}/{repo}/terraform.tfstate`
prevents cross-team state collision.
- **Password-set rate limiting**: Lambda reserved concurrency = 5
(prevents brute force).
- **Apply-gate**: Updated to assume `ci-team-*` roles (was `ci-app-*`).

## Test plan
- [ ] `terraform plan` — verify broker Lambda, IAM roles, trust policies
- [ ] Test app repo CI: broker role → invoke Lambda → receive team
credentials
- [ ] Verify repo not in a team gets clear error from broker
- [ ] Verify Bedrock failure produces FAILED risk that can be overridden
- [ ] Verify password-set Lambda has reserved_concurrent_executions = 5
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