Repository navigation
fix(platform): harden ALB, Cognito pools, subnets and bucket TLS - #165
Alexanderamiri wants to merge 2 commits into
Conversation
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]>
Terraform Plan🚧 Changes detected — Plan: 2 to add, 6 to change, 0 to destroy. Plan outputLLM ReviewRisk: 🟡 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.
|
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Terraform Plan🚧 Changes detected — Plan: 2 to add, 6 to change, 0 to destroy. Plan outputLLM ReviewRisk: 🟡 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.
|
|
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. |
## 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]>
Why
The security audit found five small hardening gaps in platform-owned resources. Each was checked against live state.
assign_public_ip = false.sg-0753bdbbc175cc80b) still has its default rules. Nothing uses it: 0 ENIs reference it.What
ingress/main.tf: setdrop_invalid_header_fields = trueon the ALB.identity/main.tf: setdeletion_protection = "ACTIVE"on theinternalandexternalpools. This is two one-line edits.networking/main.tf:map_public_ip_on_launch = falseonpublic_aandpublic_b;aws_default_security_groupwith no rules. Terraform adopts the default SG and strips its default rules.cost-analytics/main.tf:DenyInsecureTransportstatement to thecur_databucket policy;athena_resultsbucket policy that contains only that deny.How verified
terraform fmt -recursive -checkandterraform validatepass. Pytest passes (107).MapPublicIpOnLaunch=True.athena_resultshas no bucket policy today.ec2:ModifySubnetAttribute,RevokeSecurityGroup*, or the Cognito/ELB/S3 updates.Follow-ups
🤖 Generated with Claude Code