Uh oh!
There was an error while loading. Please reload this page.
feat(edge): Lambda@Edge Cognito enforcement on CloudFront (split-stack, region-agnostic) - #86
Conversation
Extends the WebUI auth design with the Lambda@Edge enforcement layer: - Architecture (viewer-request Lambda@Edge, cognito-at-edge library) - Lambda@Edge constraints (us-east-1, no env vars, Node 18, 50MB limit) - Build & deploy flow (npm install, placeholder substitution, S3 upload) - Signing key handling (Secrets Manager, deterministic ARN, IAM scoped) - Handler code skeleton - Cost estimate (~$0.40-0.50/mo per deployment) - Rollback path - Deferred: logout endpoint, non-Cognito localhost access Implementation lands in follow-up commits on this branch.
Implements v2 of the WebUI auth design (see docs/design/kirocrew-webui-auth.md).
Adds a viewer-request Lambda@Edge on the KiroCrew CloudFront distribution
that validates a Cognito session cookie via cognito-at-edge. Unauthenticated
requests are redirected to the Cognito hosted UI; /auth/callback exchanges
the code for tokens and sets the session cookie.
New pack directory: packs/kirocrew/webui-auth-edge/
- package.json — cognito-at-edge dependency
- index.js — Lambda@Edge handler (fetches all config from Secrets Manager
at cold start; only SECRET_NAME is baked in at build time)
- build.sh — substitutes placeholder, runs npm install --production,
zips (~1.4 MB), emits zip path on stdout
CFN changes (deploy/cloudformation/template.yaml):
- Parameters: EdgeLambdaS3Bucket, EdgeLambdaS3Key
- Resources (all Condition: EnableWebUI):
- WebUIEdgeSigningKeySecret (Secrets Manager, GenerateSecretString)
- WebUIEdgeLambdaRole (trust: lambda + edgelambda)
- WebUIEdgeLambdaFunction (Node 18.x, us-east-1, code from S3)
- WebUIEdgeLambdaVersion (required for CloudFront association)
- KiroCrewDistribution: LambdaFunctionAssociations[viewer-request]
- WebUIUserCreationFunction: now writes BOTH admin creds and edge auth
config (poolId + clientId + cognitoDomain + signingKey) to Secrets Manager
- WebUIUserCreationRole: added Get+PutSecretValue on the edge secret
- Outputs: WebUIEdgeFunctionArn, WebUIEdgeSigningKeySecretArn
Installer changes:
- New function build_and_upload_edge_lambda() runs after prepare_repo,
before deploy_cfn_stack. Builds the zip with SECRET_NAME baked in
(deterministic: /lowkey/${ENV_NAME}/webui-edge-signing-key), creates
the CFN templates bucket if needed, uploads zip, exports
EDGE_LAMBDA_S3_BUCKET/EDGE_LAMBDA_S3_KEY for build_deploy_params.
- PARAM_CFN_NAMES/PARAM_VALUES extended (28 entries each).
Validated:
- bash -n install.sh: OK
- aws cloudformation validate-template: OK (40 params, CAPABILITY_NAMED_IAM)
- Build script tested end-to-end: produces valid zip (42 npm packages)
- Python ast.parse on Custom Resource Lambda: OKP0 fixes:
- CFN Rule WebUIEdgeRequiresUsEast1 enforces stack in us-east-1 when
EnableWebUIAuth=true (Lambda@Edge is a CloudFront-only resource that
must live in us-east-1). Also asserts EdgeLambdaS3Bucket/Key are set
so console-mode deploys can't silently miss the edge Lambda.
- Split the single WebUIEdgeSigningKeySecret into two secrets to fix
the update-time KeyError:
* WebUIEdgeSigningKeySecret — raw HMAC key, CFN GenerateSecretString
only, never rewritten. Read by Custom Resource on first Create.
* WebUIEdgeConfigSecret — merged {poolId, clientId, cognitoDomain,
signingKey} for the Lambda@Edge to consume. Written by Custom
Resource; safe to rewrite on Update.
- Custom Resource IAM policy updated: read on signing-key, write on
admin-secret + edge-config-secret.
- Custom Resource property renamed EdgeSecretArn -> EdgeConfigSecretArn
to make the intent explicit.
- Lambda@Edge index.js: placeholder renamed __SECRET_NAME__ ->
__CONFIG_SECRET_NAME__; reads from webui-edge-config secret, rejects
'pending' placeholder values loudly.
P1 fixes:
- Content-addressed zip name: SHA now hashes the actual substituted
index.js + package.json, so any code change or config change
produces a new S3 key. This forces CFN to see a diff on Code.S3Key
and publish a new AWS::Lambda::Version, propagating updates to
CloudFront edges.
- Console-mode deploy now rejects EnableWebUIAuth=true with a clear
error pointing at CLI mode (console can't build+upload the edge
zip locally).
Installer:
- build_and_upload_edge_lambda uses CONFIG_SECRET_NAME env var
(matches new build.sh contract).
- Secret name updated to /lowkey/${ENV_NAME}/webui-edge-config.
Validated:
- bash -n install.sh: OK
- aws cloudformation validate-template: OK (40 params)
- Python ast.parse on Custom Resource Lambda: OK
- build.sh test still produces valid zipP0 #1: Lambda@Edge IAM policy pointed at wrong secret - WebUIEdgeLambdaRole 'FetchSigningKey' granted GetSecretValue on WebUIEdgeSigningKeySecret, but the Lambda@Edge code reads the merged WebUIEdgeConfigSecret (the one populated by the Custom Resource). At runtime the Lambda would AccessDenied on every cold start. - Renamed policy FetchSigningKey -> FetchEdgeConfig, changed Resource to !Ref WebUIEdgeConfigSecret. P1 #1: AWS::Lambda::Version wouldn't publish new versions on updates - The content-addressed zip (round-1 fix) updates $LATEST correctly but WebUIEdgeLambdaVersion has no property that changes between deploys, so CFN never replaces it and CloudFront stays pinned to the old version. - Fix: added CFN parameter EdgeLambdaCodeSha256 (base64 SHA of the uploaded zip), Rule assertion, and CodeSha256 property on the Version resource plus the SHA in Description. When code changes, the SHA changes, CFN sees a diff on Version, publishes a new numbered version, CloudFront picks it up. - Installer: build_and_upload_edge_lambda now computes 'openssl dgst -sha256 -binary <zip> | openssl base64 -A' and exports EDGE_LAMBDA_CODE_SHA256 through PARAM_CFN_NAMES/PARAM_VALUES. Validated: - bash -n install.sh: OK - aws cloudformation validate-template: OK (41 params, was 40) - PARAM_CFN_NAMES count: 29 (was 28)
P1 #1: nonceSigningSecret was passed at wrong nesting level - cognito-at-edge silently ignored the top-level 'nonceSigningSecret' and the invalid 'cookieCompatibility: amplify' option, meaning CSRF nonce HMAC signing never actually engaged. - Fix: pass as csrfProtection: { nonceSigningSecret: cfg.signingKey }; removed cookieCompatibility. P1 #2: S3 key and CodeSha256 could drift out of sync - The old build hashed only source files (index.js + package.json), producing a stable S3 key even when node_modules changed. But CodeSha256 is computed from the zip binary. Different SHA + same S3 key => CFN sees Function unchanged, but Version.CodeSha256 no longer matches => stack update fails. - Fix: hash the zip bytes and use that SHA as the S3 key. S3 key and CodeSha256 are now derived from the same source of truth. P2 #1: openssl not in preflight - build_and_upload_edge_lambda now uses openssl (via install.sh) to compute CodeSha256. Added require_cmd checks for node/npm/zip/openssl at the start of the function so the failure mode is clear. P2 #2: sha256sum breaks on macOS - packs/.../build.sh now defines a portable sha256_hex helper that prefers openssl, falls back to sha256sum, then shasum -a 256. Validated: - bash -n install.sh: OK - bash -n build.sh: OK - node --check index.js: OK - aws cloudformation validate-template: OK (41 params) - Live build test produced valid zip: edge-lambda-38101e37911dc140.zip
Design doc: - New section 'Split-Stack Architecture (v3)' in docs/design/kirocrew-webui-auth.md - Covers: problem (region lock), solution, deployment flow, cross-region secret writes, file layout, installer 2-phase flow, uninstall, residual risks CFN (WIP - installer not yet wired): - deploy/cloudformation/edge-stack.yaml: new companion stack (us-east-1 only) containing Lambda@Edge function/version, IAM role, two secrets - deploy/cloudformation/template.yaml: Lambda@Edge resources removed; new params (EdgeLambdaVersionArn + 4 secret name/ARN params) replace old S3/SHA params; CFN Rule updated to require edge params not us-east-1 region; CloudFront uses !Ref EdgeLambdaVersionArn; Custom Resource props updated Installer 2-phase deploy and cross-region SM client refactor pending.
P0: Custom Resource cross-region SM client
- Was single boto3.client using main-stack region, which would ResourceNotFoundException
on the us-east-1 edge secrets.
- Now creates two clients: sm_local (main region) for admin secret,
sm_edge (us-east-1) for signing-key read and edge-config write.
- Reads EdgeRegion + EdgeConfigSecretName + SigningKeySecretName from
Custom Resource ResourceProperties.
P1-1: Output name-vs-ARN correctness in edge-stack.yaml
- EdgeConfigSecretName and SigningKeySecretName previously used
!Ref WebUIEdge...Secret which returns the ARN, not the name.
- Fixed to !Sub '/lowkey/${EnvironmentName}/webui-edge-config' and
'/lowkey/${EnvironmentName}/webui-edge-signing-key' respectively.
These are the deterministic names the CFN Secret Name property
produces, so the outputs and the actual Secrets Manager names align.
P1-2: Node runtime upgrade
- edge-stack.yaml: Runtime nodejs18.x -> nodejs20.x
(Node 18 EOL Sept 2025; deployments may be rejected by Lambda.)
- Also removed misleading DependsOn: WebUIEdgeConfigSecret on the
Lambda function (implicit dep via IAM role policy is sufficient).
Validated: main template + edge stack both pass validate-template.
Installer 2-phase deploy is still pending.Build and upload the Lambda@Edge artifact to a dedicated us-east-1 bucket.\nDeploy the companion edge stack first, capture its outputs, and pass the resulting version and secret parameters to the main stack.\nUpdate the main-stack output references and post-deploy display for the split-stack resources.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit:c78bd2bebe
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| return new Authenticator({ | ||
| region: REGION, | ||
| userPoolId: cfg.poolId, | ||
| userPoolAppId: cfg.clientId, |
There was a problem hiding this comment.
Use the Cognito user pool's region for authentication
When the main stack is deployed outside us-east-1, the user pool and its issuer/JWKS endpoints live in DEPLOY_REGION, but the Authenticator is always configured with us-east-1. Consequently, token validation and Cognito requests target the wrong region and the supposedly region-agnostic WebUI authentication fails. Keep the Secrets Manager client in us-east-1, but store the main-stack region in the edge config and pass that region to Authenticator.
Useful? React with 👍 / 👎.
| WebUIEdgeLambdaVersion: | ||
| Type: AWS::Lambda::Version | ||
| Properties: | ||
| FunctionName: !Ref WebUIEdgeLambdaFunction | ||
| Description: !Sub 'KiroCrew WebUI Cognito auth (${EnvironmentName}) sha256=${EdgeLambdaCodeSha256}' | ||
| CodeSha256: !Ref EdgeLambdaCodeSha256 |
There was a problem hiding this comment.
Retain associated edge versions during companion-stack updates
When EdgeLambdaCodeSha256 changes, CloudFormation replaces this version and then attempts to delete the old version while the separately deployed main stack's CloudFront distribution still references it. Lambda rejects deletion of a replicated Lambda@Edge version, so the edge-stack update can fail before the installer gets the new ARN and updates the main stack. Add an update-retention strategy for old versions or otherwise sequence association removal/update before deleting them.
Useful? React with 👍 / 👎.
| local edge_stack_name="${ENV_NAME}-edge-stack" | ||
| step "Deploy WebUI Lambda@Edge companion stack" | ||
| if ! aws cloudformation deploy \ | ||
| --template-file "${CLONE_DIR}/deploy/cloudformation/edge-stack.yaml" \ | ||
| --stack-name "$edge_stack_name" \ | ||
| --region us-east-1 \ |
There was a problem hiding this comment.
Teach the uninstaller to remove companion resources
For an authenticated KiroCrew deployment, this creates a second stack in us-east-1, but the repository-wide teardown path in uninstall.sh only discovers a stack containing the selected VPC in SCAN_REGION and only offers the regional ${wm}-cfn-templates-* bucket for cleanup. Uninstall therefore reports success while leaving the edge stack, Lambda, IAM role, signing/config secrets, and ${ENV_NAME}-edge-${ACCOUNT_ID} bucket behind; a later deployment with the same environment name can then collide with those deterministic resources.
Useful? React with 👍 / 👎.
P1 #1: Lambda@Edge was using us-east-1 for Cognito auth - The Authenticator (cognito-at-edge) needs the region where the Cognito user pool lives, NOT the region where the Lambda@Edge itself runs. Only Secrets Manager is us-east-1 (secret is co-located with the edge Lambda for latency). Token validation, JWKS fetch, and Cognito API calls need the user-pool region. - index.js: renamed REGION -> SECRETS_REGION (still us-east-1) and added cfg.cognitoRegion (from the merged edge config secret) as the Authenticator's region. Required in the config load-validation. - Custom Resource in template.yaml: write_edge_config now accepts and writes a 'cognitoRegion' field alongside poolId/clientId/domain/ signingKey. Passes 'region' (the main-stack region, which is where the pool lives) as that value. P1 #2: Old edge Lambda versions blocked stack updates - AWS::Lambda::Version replacement tried to delete the old version while CloudFront still referenced it -> Lambda rejection (replicated Lambda@Edge takes ~1hr to GC after CloudFront disassociates) -> edge-stack update rollback. - edge-stack.yaml: added DeletionPolicy: Retain + UpdateReplacePolicy: Retain on WebUIEdgeLambdaVersion. Old versions accumulate harmlessly (Lambda Versions are free). Stack updates now succeed cleanly. Deferred (P2 from same review): - uninstall.sh doesn't know about the us-east-1 companion stack. Roy said uninstall is less important for now. Validated: - bash -n install.sh: OK - node --check packs/kirocrew/webui-auth-edge/index.js: OK - aws cloudformation validate-template (main + edge): OK
royosherove
commented
Aug 16, 2026
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit:4c554eaa90
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| region: cfg.cognitoRegion, | ||
| userPoolId: cfg.poolId, | ||
| userPoolAppId: cfg.clientId, | ||
| userPoolDomain: cfg.cognitoDomain, |
There was a problem hiding this comment.
Configure the registered Cognito callback path
When WebUI authentication is enabled, cognito-at-edge defaults its authorization-code handler and generated redirect URI to /parseauth, but the Cognito client in deploy/cloudformation/template.yaml allows only /auth/callback. Since this Authenticator does not override parseAuthPath, Cognito rejects the generated redirect URI (and /auth/callback is not processed as the callback), preventing users from completing login. Set parseAuthPath: '/auth/callback' here or register the library's default path.
Useful? React with 👍 / 👎.
Codex P1 on PR #86 commit 4c554ea. cognito-at-edge defaults its authorization-code handler and generated redirect_uri to '/parseauth'. Our Cognito user pool client (in template.yaml) allows only '/auth/callback' and 'http://localhost:5476/ auth/callback' as callback URLs. Without an override, Cognito rejected the generated redirect_uri and '/auth/callback' was unused. Fix: pass parseAuthPath: '/auth/callback' to the Authenticator so the generated redirect_uri matches the registered CallbackURLs. Validated: node --check on index.js.
royosherove
commented
Aug 16, 2026
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit:43f8fbb0d9
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| LambdaFunctionAssociations: !If | ||
| - EnableWebUI | ||
| - - EventType: viewer-request | ||
| LambdaFunctionARN: !Ref EdgeLambdaVersionArn |
There was a problem hiding this comment.
Protect the public ALB path too
When EnableWebUIAuth is true, this association authenticates only traffic passing through CloudFront, but KiroCrewALBSecurityGroup still permits port 80 from 0.0.0.0/0 and KiroCrewHTTPListener forwards every request without checking x-origin-verify. Anyone can therefore use the exported KiroCrewALBDns URL to reach the dashboard directly and bypass Cognito entirely. Make the listener forward only when the secret header matches (with a rejecting default action), or otherwise restrict direct ALB access.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit:6057bff174
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| ServiceToken: !GetAtt WebUIUserCreationFunction.Arn | ||
| UserPoolId: !Ref WebUIUserPool | ||
| ClientId: !Ref WebUIUserPoolClient | ||
| CognitoDomain: !Sub '${WebUIUserPoolDomain}.auth.${AWS::Region}.amazoncognito.com' |
There was a problem hiding this comment.
Pass the Cognito domain prefix to Authenticator
When WebUI authentication is enabled, this stores the full hosted-UI hostname in the edge config, but cognito-at-edge treats userPoolDomain as the Cognito domain prefix and appends .auth.<region>.amazoncognito.com itself. Login and token-exchange URLs therefore target a duplicated, nonexistent hostname such as ...amazoncognito.com.auth.<region>.amazoncognito.com, preventing authentication. Pass the domain resource's prefix (for example, !Ref WebUIUserPoolDomain) to the edge config while retaining the full hostname only for display outputs.
Useful? React with 👍 / 👎.
Uh oh!
There was an error while loading. Please reload this page.
Ships Cognito authentication enforcement on the KiroCrew CloudFront distribution via Lambda@Edge. Uses a split-stack architecture so the main stack can deploy in any region while the Lambda@Edge lives in its required us-east-1 home.
Architecture
template.yaml) — deploys in user's chosen region. VPC, EC2, ALB, Cognito, CloudFront, admin secret, Custom Resource. References edge-stack outputs as parameters.edge-stack.yaml) — always deploys in us-east-1. Lambda@Edge function + version, IAM role, signing-key secret, edge-config secret.$DEPLOY_REGIONwith edge outputs as params.Lambda@Edge handler
cognito-at-edgelibrary, viewer-request trigger__CONFIG_SECRET_NAME__baked in at build time)Design & review history
Docs:
docs/design/kirocrew-webui-auth.md— v1 (in-installer API calls), v2 (in-CFN Lambda@Edge, us-east-1 locked), v3 (split-stack, region-agnostic).The branch went through 5 review rounds. All P0/P1 findings addressed:
csrfProtectionnesting, SHA-based S3 key, openssl preflight, portable sha256Files
deploy/cloudformation/edge-stack.yaml(new) — us-east-1 companion stackdeploy/cloudformation/template.yaml— main stack; Lambda@Edge resources removed, new params for edge outputspacks/kirocrew/webui-auth-edge/{index.js,package.json,build.sh}(new) — Lambda@Edge source + buildinstall.sh—build_and_upload_edge_lambda+ newdeploy_edge_stackfunction, wired into main() 2-phase flowdocs/design/kirocrew-webui-auth.md— v2 and v3 design sectionsValidation
bash -n install.sh✓node --check packs/kirocrew/webui-auth-edge/index.js✓aws cloudformation validate-templateon both templates ✓ (main 43 params, edge 5 params, CAPABILITY_NAMED_IAM)PARAM_CFN_NAMESandPARAM_VALUESin sync (31 entries)Deferred (not blocking)
aws cloudformation deployfor the edge stack blocks silently — should print a progress hint before the callput-bucket-versioning/put-public-access-blocknow hard-fail instead of tolerating existing state (idempotent in practice, but stricter than original)Commits
8 commits ahead of main, each with a clear message trace:
30efc6ddocs: Lambda@Edge design (v2)28a3f4einitial feature (v2)b139e7dround-1 fixes8a70dc9round-2 fixes7ecf3c6round-3 fixes72c5d99split-stack design v3 + CFN refactorcaa1269v3 design-review P0/P1 fixesc78bd2binstaller 2-phase deploy (Luna)