Add Lambda VPC proxy for MBE-to-OpenEMS B2B access#75
Conversation
Provisions an isolated dev environment on AWS: VPC 10.100.0.0/16 with a public subnet, t3.large Ubuntu 22.04 instance running the full docker-compose stack via setup.sh, security group scoped to an allowed_ips variable (no 0.0.0.0/0 exposure), and an IAM role for SSM Session Manager access (no SSH key required). State is stored in the existing openems-deployment-tf-state-file S3 bucket under a separate key (iac/dev/terraform.tfstate) so it does not collide with the production ECS deployment in iac/. User-data clones the local-deployment branch and runs setup.sh --edges 2 on first boot. Bootstrap takes ~10 min on a fresh instance. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Signed-off-by: Alejandro Malbet <amalbet@gmail.com>
Signed-off-by: Aidan Barnes <66229298+aidan-barnes-axm@users.noreply.github.com>
- backend.tf: point to Aidan's state bucket (docker-openems-feature-dev-iac) and lock table (docker-openems-feature-dev-iac-state-lock) in the dev account - provider.tf: change Environment tag from "dev" to "aidev" to match the IAM policies Aidan configured for the dev account Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Signed-off-by: Alejandro Malbet <amalbet@gmail.com>
Implements ticket #73. Adds a Node.js 20 Lambda function inside the dev VPC that proxies JSON-RPC requests from Vercel (MBE) to the OpenEMS Backend on its private IP via port 8082. Auth is IAM SigV4 via Function URL. - iac/dev/lambda/index.mjs: passthrough proxy with error handling, native fetch (no npm deps), handles isBase64Encoded, returns 502 on unreachable - iac/dev/lambda.tf: all Lambda resources — archive_file, IAM role + AWSLambdaVPCAccessExecutionRole, Lambda SG (egress 8082 to EC2 SG), aws_lambda_function (128MB/30s/VPC), Function URL (AWS_IAM/BUFFERED), CloudWatch log group (14-day retention), MBE invoker IAM user + access key + user policy (lambda:InvokeFunctionUrl on this Lambda ARN only) - iac/dev/security.tf: aws_security_group_rule ingress 8082 from Lambda SG to EC2 SG (SG-to-SG reference, no CIDR) - iac/dev/outputs.tf: rename b2b_url → b2b_url_direct, add b2b_url_lambda, lambda_invoker_access_key_id, lambda_invoker_secret_key (sensitive) - iac/dev/variables.tf: add openems_b2b_creds (sensitive, no default) - iac/dev/terraform.tfvars.example: document openems_b2b_creds format Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Signed-off-by: Alejandro Malbet <amalbet@gmail.com>
- lambda.tf: use `security_groups` (list) instead of invalid `source_security_group_id` for inline egress block - .gitignore: add lambda/proxy.zip (created by archive_file at plan time) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Signed-off-by: Alejandro Malbet <amalbet@gmail.com>
S3 bucket and DynamoDB table names are now provided at init time via backend.tfvars (gitignored), not hardcoded in backend.tf. This keeps infrastructure metadata out of the public repo. Usage: terraform init -backend-config=backend.tfvars Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> Signed-off-by: Alejandro Malbet <amalbet@gmail.com>
OpenEMS runs the B2B REST (JSON-RPC) endpoint on 8075 per the `Backend2Backend.Rest` config. Port 8082 is the UI ↔ Backend WebSocket (the browser connects there from the UI at :4200). PR #75 was using 8082 everywhere for "B2B REST" — the Lambda proxy would have connected to the UI websocket instead of the REST endpoint. Verified against docker-openems local-deployment: - docker-compose.yml maps 8075:8075 with comment "B2B REST API" - openems-backend/config.d/Backend2Backend/Rest/*.config → port=8075 - openems-backend/config.d/Ui/Websocket.config → port=8082 - metering-billing-engine/src/lib/openems/__tests__/client.test.ts:33 uses http://localhost:8075 as the B2B base URL Changes: - security.tf: keep 8082 ingress (relabeled "UI Backend WebSocket"), add new 8075 ingress for B2B REST, change Lambda→EC2 SG rule to 8075 - lambda.tf: Lambda SG egress 8082 → 8075 - lambda/index.mjs: /jsonrpc endpoint 8082 → 8075 - outputs.tf: b2b_url_direct 8082 → 8075; new ui_backend_ws_url output - README.md: port table updated; 8082 now "UI ↔ Backend WebSocket", 8075 is "B2B REST" Found during 2026-04-21 end-to-end validation of the dev stack. Signed-off-by: Alejandro Malbet <amalbet@gmail.com>
The EC2 instance is provisioned with http_tokens = "required" (IMDSv2 enforced) in ec2.tf:25-28. The bootstrap log echoes used plain `curl http://169.254.169.254/...` which silently fails under IMDSv2 — the final 3 lines of the bootstrap log would print empty URLs instead of the instance's public IP. Replaces the 3 separate `curl` calls with a single token fetch, then reuses the token for the metadata query. Also corrects the B2B port in the bootstrap echo from 8082 to 8075 (per the previous commit). Impact is cosmetic only (setup.sh itself doesn't hit metadata), but the pattern is flagged in mbe-docs/docs/learnings.md as a recurring bug worth fixing at the source. Signed-off-by: Alejandro Malbet <amalbet@gmail.com>
Update (2026-04-21): validated end-to-end + 2 bug fixesValidated this PR end-to-end against the dev AWS account today:
Found 2 bugs in the process and pushed fixes to this branch — both small, labeled, and self-contained. No destructive changes:
@aidan-barnes-axm — please review when you have a chance. Separately, I've also flagged a couple of non-Terraform bugs in |
Review findings[P1] Proxy targets the UI websocket port instead of B2B REST
The Lambda forwards JSON-RPC to port [P1] Function URL invoker lacks required Lambda permission
The Vercel IAM user is granted only [P3] Bootstrap logs use IMDSv1 despite requiring IMDSv2
The EC2 resource sets |
…r condition Lambda Function URLs with AuthType=AWS_IAM require BOTH lambda:InvokeFunctionUrl AND lambda:InvokeFunction on the caller's identity policy. The previous policy granted only lambda:InvokeFunctionUrl, so mbe_invoker would have hit a 403 the first time it tried to invoke the Function URL — even with valid SigV4 signing. Not caught earlier because end-to-end testing today used my own profile (AWSLambda_FullAccess), which has both permissions. The scoped service user wouldn't. Also adds the lambda:InvokedViaFunctionUrl=true condition to ensure the user can ONLY invoke via the Function URL — not via the direct Lambda Invoke API. Tighter security posture matching the original intent of the scoped policy. Per @tushabe review on PR #75 — thanks Aaron. Signed-off-by: Alejandro Malbet <amalbet@gmail.com>
|
@tushabe — really useful review, thank you. Status on each finding: P1 #1 (port 8082 → 8075) — already addressedSame bug I caught and fixed earlier today in commit P1 #2 (
|
|
@amalbet unfortunately I don't have write access to this repo yet so i can only provide review feedback, I can't approve PRs. Over to @aidan-barnes-axm and @axmsoftware |
Summary
Adds a Lambda VPC proxy to
iac/dev/that allows MBE (on Vercel) to securely access the OpenEMS B2B REST API without exposing the backend to the public internet. This is Option 3 from the architecture discussion in PR #70.Closes #73
Changes
New files:
iac/dev/lambda/index.mjs— Node.js 20 proxy function (handles errors, base64 encoding)iac/dev/lambda.tf— Lambda function, IAM roles, security group, Function URL, CloudWatch log group, Vercel invoker IAM user + access keyiac/dev/backend.tfvars.example— template for backend config (bucket + table names provided at init time, not committed)Modified files:
iac/dev/backend.tf— switched to partial backend pattern (no hardcoded bucket/table names)iac/dev/security.tf— added EC2 SG ingress rule from Lambda SG on port 8082 (SG-to-SG)iac/dev/outputs.tf— renamedb2b_url→b2b_url_direct, addedb2b_url_lambda, IAM key outputsiac/dev/variables.tf— addedopenems_b2b_creds(sensitive)iac/dev/terraform.tfvars.example— documented new variableiac/dev/.gitignore— addedbackend.tfvars,lambda/proxy.zipiac/dev/README.md— updated init instructions for backend configPrerequisites
Before
terraform apply:backend.tfvars, gitignored)AWSLambda_FullAccessor scoped)openems_b2b_credsset interraform.tfvars(base64-encodeduser:password)Test plan
terraform init -backend-config=backend.tfvarssucceedsterraform planshows Lambda + IAM + SG resources to create (no changes to existing EC2/VPC)terraform applysucceedsawscurl: POST to the Function URL with SigV4 → valid JSON-RPC response from backendterraform destroycleans up all Lambda resources + log group🤖 Generated with Claude Code