Skip to content

fix(platform): harden ALB, Cognito pools, subnets and bucket TLS - #165

Open
Alexanderamiri wants to merge 2 commits into
mainfrom
fix/sec-hardening
Open

Alexanderamiri wants to merge 2 commits into
mainfrom
fix/sec-hardening

Conversation

@Alexanderamiri

Copy link
Copy Markdown
Member

Why

The security audit found five small hardening gaps in platform-owned resources. Each was checked against live state.

  • ELB.4: the platform ALB does not drop invalid header fields.
  • Cognito.6: neither Cognito pool has deletion protection. Deleting a pool would lock out every app login.
  • EC2.15: the public subnets auto-assign public IPs. Nothing relies on this:
    • the NAT has an EIP;
    • the ALB manages its own addresses;
    • ECS tasks run in private subnets with assign_public_ip = false.
  • EC2.2: the platform VPC's default security group (sg-0753bdbbc175cc80b) still has its default rules. Nothing uses it: 0 ENIs reference it.
  • S3.5: the CUR and Athena-results buckets do not enforce TLS.

What

  • ingress/main.tf: set drop_invalid_header_fields = true on the ALB.
  • identity/main.tf: set deletion_protection = "ACTIVE" on the internal and external pools. This is two one-line edits.
  • networking/main.tf:
    • set map_public_ip_on_launch = false on public_a and public_b;
    • add an aws_default_security_group with no rules. Terraform adopts the default SG and strips its default rules.
  • cost-analytics/main.tf:
    • add a DenyInsecureTransport statement to the cur_data bucket policy;
    • add a new athena_results bucket policy that contains only that deny.

How verified

  • terraform fmt -recursive -check and terraform validate pass. Pytest passes (107).
  • Live checks:
    • Both public subnets have MapPublicIpOnLaunch=True.
    • The VPC has 8 ENIs, and none uses the default SG:
      • 3 ECS tasks, with no public IP;
      • 2 ALB ENIs;
      • 1 NAT ENI, with an EIP;
      • 2 VPC endpoint ENIs.
    • athena_results has no bucket policy today.
  • The org boundary does not deny ec2:ModifySubnetAttribute, RevokeSecurityGroup*, or the Cognito/ELB/S3 updates.
  • Expected plan:
    • in-place updates: the ALB, the 2 user pools, the 2 subnets and the CUR bucket policy;
    • creates: the default-SG adoption and the Athena bucket policy;
    • no destroys.

Follow-ups

  • None. The Athena workgroup and the Data Exports service both use TLS, so the new deny changes nothing for legitimate clients.

🤖 Generated with Claude Code

drop_invalid_header_fields on the ALB, deletion protection on both Cognito
pools, no auto-assigned public IPs on public subnets, rule-less default SG
for the platform VPC, TLS-only policies on the CUR and Athena buckets.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
@Alexanderamiri
Alexanderamiri requested a review from a team as a code owner October 24, 2026 13:38
@github-actions

Copy link
Copy Markdown

Terraform Plan

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

Plan output
Acquiring state lock. This may take a few moments...
aws_ssm_parameter.atlassian_api_key: Refreshing state... [id=/javabin/platform/atlassian-api-key]
aws_ssm_parameter.atlassian_org_id: Refreshing state... [id=/javabin/platform/atlassian-org-id]

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.cost_analytics.aws_s3_bucket_policy.athena_results will be created
  + resource "aws_s3_bucket_policy" "athena_results" {
      + bucket = "javabin-athena-results-553637109631"
      + id     = (known after apply)
      + policy = jsonencode(
            {
              + Statement = [
                  + {
                      + Action    = "s3:*"
                      + Condition = {
                          + Bool = {
                              + "aws:SecureTransport" = "false"
                            }
                        }
                      + Effect    = "Deny"
                      + Principal = "*"
                      + Resource  = [
                          + "arn:aws:s3:::javabin-athena-results-553637109631",
                          + "arn:aws:s3:::javabin-athena-results-553637109631/*",
                        ]
                      + Sid       = "DenyInsecureTransport"
                    },
                ]
              + Version   = "2012-10-17"
            }
        )
    }

  # module.cost_analytics.aws_s3_bucket_policy.cur_data will be updated in-place
  ~ resource "aws_s3_bucket_policy" "cur_data" {
        id     = "javabin-cur-553637109631"
      ~ policy = jsonencode(
          ~ {
              ~ Statement = [
                    {
                        Action    = [
                            "s3:PutObject",
                            "s3:GetBucketPolicy",
                        ]
                        Condition = {
                            StringEquals = {
                                "aws:SourceAccount" = "553637109631"
                            }
                        }
                        Effect    = "Allow"
                        Principal = {
                            Service = "bcm-data-exports.amazonaws.com"
                        }
                        Resource  = [
                            "arn:aws:s3:::javabin-cur-553637109631",
                            "arn:aws:s3:::javabin-cur-553637109631/*",
                        ]
                        Sid       = "AllowDataExportsDelivery"
                    },
                  + {
                      + Action    = "s3:*"
                      + Condition = {
                          + Bool = {
                              + "aws:SecureTransport" = "false"
                            }
                        }
                      + Effect    = "Deny"
                      + Principal = "*"
                      + Resource  = [
                          + "arn:aws:s3:::javabin-cur-553637109631",
                          + "arn:aws:s3:::javabin-cur-553637109631/*",
                        ]
                      + Sid       = "DenyInsecureTransport"
                    },
                ]
                # (1 unchanged attribute hidden)
            }
        )
        # (1 unchanged attribute hidden)
    }

  # module.identity.aws_cognito_user_pool.external will be updated in-place
  ~ resource "aws_cognito_user_pool" "external" {
      ~ deletion_protection       = "INACTIVE" -> "ACTIVE"
        id                        = "eu-central-1_gdFOsE4EM"
        name                      = "javabin-external"
        tags                      = {
            "Name" = "javabin-external"
        }
        # (10 unchanged attributes hidden)

        # (7 unchanged blocks hidden)
    }

  # module.identity.aws_cognito_user_pool.internal will be updated in-place
  ~ resource "aws_cognito_user_pool" "internal" {
      ~ deletion_protection       = "INACTIVE" -> "ACTIVE"
        id                        = "eu-central-1_Icikv3dtD"
        name                      = "javabin-internal"
        tags                      = {
            "Name" = "javabin-internal"
        }
        # (11 unchanged attributes hidden)

        # (8 unchanged blocks hidden)
    }

  # module.ingress.aws_lb.main will be updated in-place
  ~ resource "aws_lb" "main" {
      ~ drop_invalid_header_fields                  = false -> true
        id                                          = "arn:aws:elasticloadbalancing:eu-central-1:553637109631:loadbalancer/app/javabin-platform-alb/bec1dd43ab8341b9"
        name                                        = "javabin-platform-alb"
        tags                                        = {
            "Name" = "javabin-platform-alb"
            "team" = "shared"
        }
        # (23 unchanged attributes hidden)

        # (4 unchanged blocks hidden)
    }

  # module.networking.aws_default_security_group.main will be created
  + resource "aws_default_security_group" "main" {
      + arn                    = (known after apply)
      + description            = (known after apply)
      + egress                 = (known after apply)
      + id                     = (known after apply)
      + ingress                = (known after apply)
      + name                   = (known after apply)
      + name_prefix            = (known after apply)
      + owner_id               = (known after apply)
      + revoke_rules_on_delete = false
      + tags                   = {
          + "Name" = "javabin-default-deny"
          + "team" = "shared"
        }
      + tags_all               = {
          + "Name"        = "javabin-default-deny"
          + "environment" = "production"
          + "managed-by"  = "terraform"
          + "repo"        = "javaBin/platform"
          + "service"     = "platform"
          + "team"        = "shared"
        }
      + vpc_id                 = "vpc-0cd3502de2527a310"
    }

  # module.networking.aws_subnet.public_a will be updated in-place
  ~ resource "aws_subnet" "public_a" {
        id                                             = "subnet-0f6bfec917146b856"
      ~ map_public_ip_on_launch                        = true -> false
        tags                                           = {
            "Name" = "javabin-public-a"
            "team" = "shared"
            "tier" = "public"
        }
        # (15 unchanged attributes hidden)
    }

  # module.networking.aws_subnet.public_b will be updated in-place
  ~ resource "aws_subnet" "public_b" {
        id                                             = "subnet-0eb818326ee94a266"
      ~ map_public_ip_on_launch                        = true -> false
        tags                                           = {
            "Name" = "javabin-public-b"
            "team" = "shared"
            "tier" = "public"
        }
        # (15 unchanged attributes hidden)
    }

Plan: 2 to add, 6 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: 🟡 MEDIUM

Plan adds 2 resources and updates 6 in place with 0 destroyed; mostly hardening (TLS-only enforcement, deletion protection, header validation, subnet IP mapping) but the public subnet change and default SG creation need confirmation before apply.

  • 🔒 [security] Two new S3 bucket policies (javabin-athena-results and javabin-cur) add a Deny for aws:SecureTransport = false on all S3 actions for all principals. This is a hardening change and does not open access. The existing AllowDataExportsDelivery statement on the CUR bucket is unchanged, so AWS Data Exports delivery should continue to work.
  • 🔒 [security] aws_default_security_group.main is being created. Terraform takes ownership of the VPC default security group, and with no ingress or egress blocks shown it should strip the default allow-all rules. The plan does not show these attributes, so verify the resulting rules are empty and that nothing in the VPC relies on the default SG.
  • 🔒 [security] Both public subnets (public_a, public_b) change map_public_ip_on_launch from true to false. This is not a security opening, but instances launched there will no longer get public IPs automatically. Confirm nothing (ECS tasks, bastions, NAT or ALB-adjacent workloads) depends on auto-assigned public IPs.
  • ✅ [routine] Cognito user pools javabin-external and javabin-internal change deletion_protection from INACTIVE to ACTIVE. This is a safety improvement and does not change authentication behaviour. Note it will block a future terraform destroy until it is turned off.
  • ✅ [routine] ALB javabin-platform-alb sets drop_invalid_header_fields to true. This is a hardening change. Clients sending malformed header names will be rejected, so it is worth confirming no legacy client depends on them. The ALB is updated in place with no replacement.
  • 💰 [cost] No new billable resources (NAT gateways, RDS, load balancers, or similar) are created. The 2 additions are an S3 bucket policy and a default security group, both free.

@github-actions

Copy link
Copy Markdown

Terraform Plan

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

Plan output
Acquiring state lock. This may take a few moments...
aws_ssm_parameter.atlassian_api_key: Refreshing state... [id=/javabin/platform/atlassian-api-key]
aws_ssm_parameter.atlassian_org_id: Refreshing state... [id=/javabin/platform/atlassian-org-id]

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.cost_analytics.aws_s3_bucket_policy.athena_results will be created
  + resource "aws_s3_bucket_policy" "athena_results" {
      + bucket = "javabin-athena-results-553637109631"
      + id     = (known after apply)
      + policy = jsonencode(
            {
              + Statement = [
                  + {
                      + Action    = "s3:*"
                      + Condition = {
                          + Bool = {
                              + "aws:SecureTransport" = "false"
                            }
                        }
                      + Effect    = "Deny"
                      + Principal = "*"
                      + Resource  = [
                          + "arn:aws:s3:::javabin-athena-results-553637109631",
                          + "arn:aws:s3:::javabin-athena-results-553637109631/*",
                        ]
                      + Sid       = "DenyInsecureTransport"
                    },
                ]
              + Version   = "2012-10-17"
            }
        )
    }

  # module.cost_analytics.aws_s3_bucket_policy.cur_data will be updated in-place
  ~ resource "aws_s3_bucket_policy" "cur_data" {
        id     = "javabin-cur-553637109631"
      ~ policy = jsonencode(
          ~ {
              ~ Statement = [
                    {
                        Action    = [
                            "s3:PutObject",
                            "s3:GetBucketPolicy",
                        ]
                        Condition = {
                            StringEquals = {
                                "aws:SourceAccount" = "553637109631"
                            }
                        }
                        Effect    = "Allow"
                        Principal = {
                            Service = "bcm-data-exports.amazonaws.com"
                        }
                        Resource  = [
                            "arn:aws:s3:::javabin-cur-553637109631",
                            "arn:aws:s3:::javabin-cur-553637109631/*",
                        ]
                        Sid       = "AllowDataExportsDelivery"
                    },
                  + {
                      + Action    = "s3:*"
                      + Condition = {
                          + Bool = {
                              + "aws:SecureTransport" = "false"
                            }
                        }
                      + Effect    = "Deny"
                      + Principal = "*"
                      + Resource  = [
                          + "arn:aws:s3:::javabin-cur-553637109631",
                          + "arn:aws:s3:::javabin-cur-553637109631/*",
                        ]
                      + Sid       = "DenyInsecureTransport"
                    },
                ]
                # (1 unchanged attribute hidden)
            }
        )
        # (1 unchanged attribute hidden)
    }

  # module.identity.aws_cognito_user_pool.external will be updated in-place
  ~ resource "aws_cognito_user_pool" "external" {
      ~ deletion_protection       = "INACTIVE" -> "ACTIVE"
        id                        = "eu-central-1_gdFOsE4EM"
        name                      = "javabin-external"
        tags                      = {
            "Name" = "javabin-external"
        }
        # (10 unchanged attributes hidden)

        # (7 unchanged blocks hidden)
    }

  # module.identity.aws_cognito_user_pool.internal will be updated in-place
  ~ resource "aws_cognito_user_pool" "internal" {
      ~ deletion_protection       = "INACTIVE" -> "ACTIVE"
        id                        = "eu-central-1_Icikv3dtD"
        name                      = "javabin-internal"
        tags                      = {
            "Name" = "javabin-internal"
        }
        # (11 unchanged attributes hidden)

        # (8 unchanged blocks hidden)
    }

  # module.ingress.aws_lb.main will be updated in-place
  ~ resource "aws_lb" "main" {
      ~ drop_invalid_header_fields                  = false -> true
        id                                          = "arn:aws:elasticloadbalancing:eu-central-1:553637109631:loadbalancer/app/javabin-platform-alb/bec1dd43ab8341b9"
        name                                        = "javabin-platform-alb"
        tags                                        = {
            "Name" = "javabin-platform-alb"
            "team" = "shared"
        }
        # (23 unchanged attributes hidden)

        # (4 unchanged blocks hidden)
    }

  # module.networking.aws_default_security_group.main will be created
  + resource "aws_default_security_group" "main" {
      + arn                    = (known after apply)
      + description            = (known after apply)
      + egress                 = (known after apply)
      + id                     = (known after apply)
      + ingress                = (known after apply)
      + name                   = (known after apply)
      + name_prefix            = (known after apply)
      + owner_id               = (known after apply)
      + revoke_rules_on_delete = false
      + tags                   = {
          + "Name" = "javabin-default-deny"
          + "team" = "shared"
        }
      + tags_all               = {
          + "Name"        = "javabin-default-deny"
          + "environment" = "production"
          + "managed-by"  = "terraform"
          + "repo"        = "javaBin/platform"
          + "service"     = "platform"
          + "team"        = "shared"
        }
      + vpc_id                 = "vpc-0cd3502de2527a310"
    }

  # module.networking.aws_subnet.public_a will be updated in-place
  ~ resource "aws_subnet" "public_a" {
        id                                             = "subnet-0f6bfec917146b856"
      ~ map_public_ip_on_launch                        = true -> false
        tags                                           = {
            "Name" = "javabin-public-a"
            "team" = "shared"
            "tier" = "public"
        }
        # (15 unchanged attributes hidden)
    }

  # module.networking.aws_subnet.public_b will be updated in-place
  ~ resource "aws_subnet" "public_b" {
        id                                             = "subnet-0eb818326ee94a266"
      ~ map_public_ip_on_launch                        = true -> false
        tags                                           = {
            "Name" = "javabin-public-b"
            "team" = "shared"
            "tier" = "public"
        }
        # (15 unchanged attributes hidden)
    }

Plan: 2 to add, 6 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: 🟡 MEDIUM

Plan adds 2 resources and updates 6 in place with 0 destroys; mostly hardening (TLS-only S3 policies, deletion protection, ALB header validation, default SG lockdown), but disabling auto-assigned public IPs on two public subnets could break workloads that depend on them.

  • ✅ [routine] Two S3 bucket policies get a Deny statement for aws:SecureTransport=false: a new policy on javabin-athena-results and an added statement on javabin-cur. The CUR bucket's existing Allow for bcm-data-exports delivery is unchanged. Deny applies to all principals, so only non-TLS requests are blocked; this is a hardening change, but confirm no tooling uses plain HTTP endpoints.
  • ✅ [routine] Cognito user pools javabin-external and javabin-internal get deletion_protection ACTIVE. This is a safety improvement that prevents accidental pool deletion; no data loss from this change.
  • ✅ [routine] ALB javabin-platform-alb sets drop_invalid_header_fields to true. Requests with malformed headers will be dropped at the load balancer. Low risk, but verify no legacy clients send non-standard headers.
  • 🔒 [security] aws_default_security_group.main is created for the VPC with tags indicating a deny-all intent. Terraform manages the default SG but with no ingress/egress blocks, it removes all default rules. Any resource currently relying on the default SG for traffic will lose connectivity. Confirm the intended rules and check which resources use the default SG.
  • 💥 [destruction] Subnets javabin-public-a and javabin-public-b switch map_public_ip_on_launch from true to false. No replacement, but new instances/ENIs launched there will no longer get public IPs automatically. Existing instances are unaffected. Confirm nothing (e.g. ECS tasks, bastions, or Lambda-less services) depends on auto-assigned public IPs. No cost impact.

@Alexanderamiri

Copy link
Copy Markdown
Member Author

Review fix: docs/platform-modules.md now notes that drop_invalid_header_fields drops header names containing underscores for every app on the shared ALB. moresleep server code reads only Authorization and If-Unmodified-Since. Before applying, ask the cupcake and jzwatch owners whether any client sends underscore header names.

Alexanderamiri added a commit that referenced this pull request Oct 24, 2026
## Why

Part of #157. The internal pool (`javabin-internal`,
`eu-central-1_Icikv3dtD`) is open to the internet today:

- `AllowAdminCreateUserOnly` is `false`, so self sign-up is on.
- app-auth-sync puts `COGNITO` on every client, so the hosted UI shows a
sign-up link.

Together, anyone can create a user in the internal pool with their own
Gmail address. An app with `auth: internal` and no `groups` accepts that
user. Nothing checks the email domain on the server side.

The `COGNITO` entry also puts a password form in front of Google. The
frosting client was hand-edited in the console to `[GoogleSAML]` to
remove the form. The 03:30 UTC reconcile undoes that edit.

## What

- **`identity/main.tf`**
- The internal pool gets `admin_create_user_config {
allow_admin_create_user_only = true }`.
  - The internal `auth-claims` function gets two new env vars:
    - `REQUIRED_EMAIL_DOMAIN = var.google_workspace_domain` (`java.no`);
    - `FEDERATED_IDENTITY_PROVIDERS`, set from the internal IdP list.
- **New variables** `internal_identity_providers` (default
`["GoogleSAML"]`) and `external_identity_providers` (default
`["COGNITO"]`).
- They are declared in the identity module, output from it, and passed
to app-auth-sync as `INTERNAL_IDENTITY_PROVIDERS` /
`EXTERNAL_IDENTITY_PROVIDERS`.
- Validation: the internal list must not include `COGNITO` and must not
be empty.
- **app-auth-sync**
  - Each client gets exactly its pool's IdP list.
- If a configured IdP doesn't exist in the pool, the request is skipped:
an `ERROR` log and `status: error` (reconcile lists it under `error`).
Nothing is created, moved or wiped, and the existing client keeps
working.
- Settings are resolved before the pool-switch cleanup, so a
misconfigured target pool can't delete the client in the old pool.
- **auth-claims**: when `REQUIRED_EMAIL_DOMAIN` is set, a token is
refused unless both of these hold:
  - the email ends with `@java.no` (case-insensitive);
- the user is federated through a listed IdP (from the `identities`
attribute, or the `<Provider>_` username), or has `email_verified=true`.

The user sees "Sign in with your @java.no account." Logs mask the
address as `e***@java.no`.
- **Review follow-ups**
- app-auth-sync rejects `aws.cognito.signin.user.admin` for `pool:
internal` (status `invalid`, client not created or changed). That scope
would let a signed-in user call `UpdateUserAttributes` and point their
email at another `@java.no` user, whose registry groups auth-claims
would then grant. No live request uses it (only cupcake, with `openid
email profile`). The external pool still allows it.
- New `aws_cloudwatch_log_metric_filter` + `aws_cloudwatch_metric_alarm`
`javabin-app-auth-sync-skipped` on `client left unchanged` in
`/aws/lambda/javabin-app-auth-sync`, posting to `javabin-alerts` (Slack
via slack-alert). The `javabin-alerts` topic policy gets an
`AllowCloudWatchAlarmsPublish` statement scoped to this account's
alarms.
- `internal_identity_providers` documents that each entry is trusted to
vouch for email, so it must release only Workspace accounts (Google OIDC
needs an Internal-consent client, or an `email_verified`/`hd` check
first).
  - auth-claims masks the email in the group-denial log too.
- **Docs**: `docs/lambda-functions.md` (both functions) and the
`docs/app-yaml-reference.md` auth section, which covers no password form
or sign-up, the `@java.no` rule, and how a denial arrives as
`error_description`.

## How verified

- Unit tests: `python3 -m unittest discover -s
terraform/lambda-src/tests` (79 OK) and `scripts/tests` (43 OK). New
tests cover:
- the fixed IdP list, and the reconcile reverting a console edit to the
list;
- env config, the missing-IdP skip on create, update, pool switch and
reconcile, and an empty list;
- for the domain rule: SAML users with `email_verified=false`, lookalike
domains, native users with and without a verified email, other IdPs, and
a pool without the rule.
- `terraform fmt -check -recursive` and `terraform validate` pass.
- Read-only checks against live state:
- **Users:** the pool has 4 users. All are `GoogleSAML_…`, `@java.no`,
`EXTERNAL_PROVIDER`, `email_verified=false`, identities provider
`GoogleSAML`. All 4 pass through the federated-IdP branch. This exact
shape is a unit test.
- **Auth requests:** there is one, `/javabin/apps/cupcake/cupcake/auth`
(frosting, public client, groups `[helter]`). After this change its
client gets `[GoogleSAML]`, the same as the console edit, so the
reconcile no longer reverts it. cupcake-backend is `client: none` and is
unaffected.
- **IdPs:** the internal pool has exactly one IdP, `GoogleSAML`, so the
configured list resolves. The external pool keeps `["COGNITO"]`, which
is what it gets today (it has no IdPs).
- **Plan expectation:**
- in-place updates: `aws_cognito_user_pool.internal`
(`admin_create_user_config` is not ForceNew),
`aws_lambda_function.auth_claims` (env) and
`aws_lambda_function.app_auth_sync` (env + code),
`aws_sns_topic_policy.alerts` (new statement);
- creates: the log metric filter and the alarm (the log group already
exists);
  - no destroys, no pool replacement.

## Rollout & user actions

- None needed after merge. The CI apply updates the pool and both
Lambdas.
- The next auth-request change, or the 03:30 UTC reconcile, sets every
internal client to `[GoogleSAML]`. To apply it right away, run `aws
lambda invoke --function-name javabin-app-auth-sync --payload
'{"action":"reconcile"}' --cli-binary-format raw-in-base64-out
/dev/stdout`.
- Existing sessions keep working. Refresh tokens pass the domain check
because all current users qualify.

## Follow-ups

- **Interaction with #158 (`feat/alb-login`).** That PR edits the same
functions in app-auth-sync (`client_settings`, `sync`, `reconcile`) and
adds `auth_claims_external` from the same handler. The external function
doesn't set `REQUIRED_EMAIL_DOMAIN`, so it skips the domain check, which
is intended.
- Rebase on top of this PR: `client_settings()` now takes `pool` first.
- `service-routing`'s hardcoded `internal_identity_provider =
"GoogleSAML"` can be dropped once clients have a single IdP. The hosted
UI already redirects.
- **Overlap with #165.** It adds `deletion_protection = "ACTIVE"` to
both pools (raised in review here too; left to #165 to avoid a
duplicate). The change is a different attribute, so expect a trivial
text merge at most.
- **Not done here:** `user_attribute_update_settings {
attributes_require_verification_before_update = ["email"] }` on the
internal pool. Blocking the user-admin scope closes the reported path;
the pool setting would also affect how SAML attribute mapping updates
email, which can't be checked without applying, so it is left for a
separate, tested change.
- **#157 next step: switching to Google OIDC.**
  - Set `internal_identity_providers = ["Google"]` once the IdP exists.
- Google users then count as federated. That is safe only with an
Internal-consent GCP client (java.no accounts only), as the design note
proposes. Otherwise, map `email_verified` and drop Google from the
trusted list.
- Bring `GoogleSAML` under Terraform with an `import` block, or remove
it after cutover.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 5.5 (1M context) <[email protected]>

This branch has not been deployed

No deployments
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