Vega on AWS
This page explains how Vega works when deployed to AWS — what each AWS service does, how the pieces talk to each other, and the exact flow of a scan from start to finish.
Architecture diagram
flowchart TD
User[Browser user]
subgraph edge["Edge"]
CF[CloudFront CDN]
S3FE[S3: frontend bucket\nstatic React files]
end
subgraph vpc["VPC — private network"]
ALB[Application Load Balancer\npublic subnet]
subgraph services["Long-running services — ECS Fargate"]
API[vega-api\nprivate subnet]
Worker[vega-worker\nprivate subnet]
Proxy[vega-llm-proxy\nprivate subnet]
end
subgraph runner["vega-core-runner — ECS RunTask, one per scan"]
VegaCore[vega-core\nPython: planning + auditing]
Codex[codex\nNode.js subprocess]
VegaCore -->|"CodexAdapter\nspawns subprocess"| Codex
end
RDS[(RDS Postgres\nprivate subnet)]
end
subgraph aws_services["AWS managed services"]
SQS[SQS scan queue]
S3SRC[S3: source bucket]
S3ART[S3: artifacts bucket]
Cognito[Cognito user pool]
SM[Secrets Manager]
CW[CloudWatch logs]
ECR[ECR image registry]
end
Provider[AI provider API]
User --> CF
CF --> S3FE
CF --> ALB
ALB --> API
API --> Cognito
API --> RDS
API --> S3SRC
API --> SQS
SQS --> Worker
Worker --> RDS
Worker -->|RunTask| VegaCore
VegaCore --> RDS
VegaCore --> S3SRC
VegaCore --> S3ART
Codex -->|"OpenAI-compatible API\nscan-scoped proxy token"| Proxy
Proxy --> Provider
API --> CW
Worker --> CW
VegaCore --> CW
Proxy --> CW
API --> SM
Worker --> SM
ECS -->|pull images| ECR
Edge and frontend
Where the frontend lives
The frontend is not a running server or a container. The React app is built into a set of static files (HTML, JavaScript, CSS, images) and uploaded to an S3 bucket. S3 is AWS's object storage — it's a durable file store, not a web server.
CloudFront is a CDN (Content Delivery Network) that sits in front of S3 and turns those static files into a real website. It handles HTTPS, routes requests to the right origin, and optionally caches responses at AWS edge locations near users worldwide.
So the deployment pattern is:
Browser → CloudFront → S3 bucket (static files)
↘ ALB → vega-api ECS container (/v1/* requests)
There is no Node.js server, no container, and no Kubernetes for the frontend. This is intentional.
Why CloudFront + S3 instead of a container or a server?
The Vega frontend is a Single-Page Application (SPA) — once the browser downloads the app bundle, all navigation happens in JavaScript. The "server" the browser really needs is the JSON API (vega-api), not a frontend server.
Given that, running a container just to serve static files would mean:
- Paying for always-on ECS Fargate compute (CPU + memory)
- Maintaining a web server configuration (nginx, etc.)
- Handling container health checks, restarts, and scaling
The S3 + CloudFront pattern avoids all of that:
| What you need | CloudFront + S3 | A container (nginx) |
|---|---|---|
| Hosting static files | ✅ native | ✅ needs nginx config |
| HTTPS with a real cert | ✅ free ACM cert | ✅ needs cert management |
| Custom domain | ✅ CloudFront alias | ✅ ALB listener rule |
| Global performance | ✅ 450+ edge locations | ✗ single region |
| Serving the API too | ✅ CloudFront routes /v1/* to ALB |
✅ reverse-proxy config |
| Cost | ~$0–5/month at low traffic | ~$15–30/month per Fargate task |
| Zero idle cost | ✅ S3 charges per request | ✗ container always running |
The API routing problem (and why CloudFront solves it)
A browser SPA needs to call the API. The naive approach — putting the API on a different domain — causes a CORS and mixed content problem:
- If the dashboard is
https://vega.nebusec.aiand the API ishttp://api.dev.vega.nebusec.ai, the browser blocks the call (HTTP from HTTPS is mixed content). - Even with HTTPS on both, the browser sends a CORS preflight, which requires the API to respond with the right
Access-Control-Allow-Originheaders.
CloudFront eliminates this entirely. Because CloudFront routes /v1/* to the API ALB from the same origin the browser is already on, the browser sees a single domain. No CORS headers needed, no mixed-content issues.
How the API origin is protected
The ALB (Application Load Balancer) in front of vega-api is not exposed to the public internet. It only accepts traffic:
- From CloudFront's known IP ranges (enforced via AWS-managed prefix lists in the security group)
- That includes a secret
x-vega-origin-verifyheader (generated by Terraform, injected by CloudFront, checked by the ALB listener rule)
If someone tries to reach the ALB directly — by finding its DNS name — the listener returns 403. The only real path to the API is through CloudFront.
Terraform module
All of this is managed by infra/terraform/modules/frontend_hosting/main.tf. It creates:
- A private S3 bucket (all public access blocked, encrypted)
- A CloudFront Origin Access Control (signed SigV4 requests from CloudFront to S3 — S3 won't serve the files to anything else)
- A CloudFront distribution with two origins:
frontend-s3— serves/*from S3 with long-lived cachingapi-alb— serves/v1/*with caching disabled, all request headers forwarded, and the origin-verify header injected
The S3 bucket policy is automatically generated by Terraform to allow only the CloudFront distribution to read objects. Nothing else has read access.
Authentication with Cognito
AWS Cognito is a managed user authentication service. Vega uses it so the team doesn't have to build user management from scratch. Here's how it works:
- The user enters their credentials on the dashboard login page.
- The frontend calls Cognito directly (SRP authentication protocol) and receives JWT tokens.
- For every subsequent API request, the frontend sends
Authorization: Bearer <token>. - The
vega-apivalidates the JWT signature using Cognito's public JWKS (JSON Web Key Set) endpoint.
Relevant code: app/auth/cognito.py, app/auth/service.py.
Terraform module: infra/terraform/modules/cognito/main.tf.
Network isolation with VPC
A VPC (Virtual Private Cloud) is a private network inside AWS. All Vega services run inside the VPC. The database, worker, runner, and LLM proxy are in private subnets — they have no direct internet access. The API's load balancer is in a public subnet so it can receive traffic from CloudFront.
Security groups act as firewalls. For example: - The database security group only accepts connections from the API and runner task security groups. - The LLM proxy security group only accepts connections from runner tasks. - The worker security group only needs outbound access to SQS, RDS, and ECS.
Relevant Terraform modules: infra/terraform/modules/network/main.tf, infra/terraform/modules/security/main.tf.
Compute with ECS Fargate
ECS (Elastic Container Service) runs Docker containers. Fargate is the serverless mode — you don't manage EC2 instances. You define how much CPU and memory a container needs, and AWS handles the rest.
Vega has three long-running ECS services (the API, worker, and LLM proxy) and two ECS task definitions used for one-off runs (the runner and maintenance):
| Type | Service/Task | How it runs |
|---|---|---|
| Long-running service | vega-api |
Always running, ECS restarts on failure |
| Long-running service | vega-worker |
Always running, polls SQS in a loop |
| Long-running service | vega-llm-proxy |
Always running, handles AI proxy requests |
| One-off task | vega-core-runner |
Launched per scan via ECS RunTask, exits when done |
| One-off task | vega-maintenance |
Launched manually for migrations and cleanup |
All task definitions are configured for the X86_64 CPU architecture (linux/amd64). Docker images must be built targeting linux/amd64 — build-images.sh enforces this by default. Building on Apple Silicon without --platform linux/amd64 produces ARM images that will fail at startup with exec format error on Fargate.
Scan flow in AWS (step by step)
- User creates a scan in the dashboard.
vega-apiwrites aqueuedscan row to RDS Postgres.vega-apisends a message to the SQS queue containing the scan ID.vega-worker(which is always running) receives the SQS message.vega-workerclaims the scan with a Postgres row lock (prevents double-execution).vega-workercalls AWS ECSRunTaskto start avega-core-runnercontainer.vega-core-runnerdownloads the source snapshot from S3.- Inside the container, vega-core (the Python scan engine) runs two phases:
- Planning: vega-core spawns a
codexsubprocess to analyze the repo structure and produce a list of components to audit. - Auditing: for each component, vega-core spawns another
codexsubprocess to analyze that code against the threat profile and generate findings. - Every
codexsubprocess calls the LLM viavega-llm-proxyusing an OpenAI-compatible API. The runner holds only a short-lived, scan-scoped proxy token — the real provider API key lives in the proxy. - vega-core emits events as it works. Findings and events are written to Postgres. Artifacts are uploaded to S3.
- Scan status is updated to
completed. - The
vega-core-runnerECS task exits. You're only billed for the time it ran. - The dashboard reads the updated scan status and findings via the API.
Inside the scan runner
The vega-core-runner container is where the actual AI-powered scan happens. It is not a simple service — it contains two distinct processes working together.
flowchart TD
subgraph container["vega-core-runner ECS container (one per scan)"]
RunScript["scripts/run-scan-runner.py\nEntry point"]
Service["ProjectService\napp/projects/service.py"]
Adapter["vega-core adapter\napp/projects/vega_core_adapter.py"]
subgraph vcore["vega-core (Python library — src/)"]
EntryPoint["entry points\nplan_repo / run_scan / verify_and_patch"]
Orchestrator["StageOrchestrator\npipeline sequencing"]
Planner["PlanStage\nidentifies components to audit"]
ThreatModel["ThreatModelStage\nbuilds threat model"]
Auditor["AuditStage\nper component, repeated"]
CodexAdapter["CodexAdapter\nsrc/agents/adapters/codex.py"]
EntryPoint --> Orchestrator
Orchestrator --> Planner
Orchestrator --> ThreatModel
Orchestrator -->|per component| Auditor
Planner --> CodexAdapter
ThreatModel --> CodexAdapter
Auditor --> CodexAdapter
end
RunScript --> Service --> Adapter --> EntryPoint
CodexAdapter -->|events & findings| Adapter
end
subgraph codex_proc["codex subprocess (Node.js, bundled in image)"]
CodexBin["codex exec --json\nreads source files\ncalls LLM API"]
end
subgraph outputs["Outputs"]
Postgres[(RDS Postgres\nevents + findings)]
S3Art[S3 artifacts bucket\nvega-core-events.jsonl\nvega-core-report.json\nvega-core-debug-bundle.zip]
end
LLMProxy["vega-llm-proxy\n(separate ECS service)"]
Provider["AI provider\nOpenAI etc."]
CodexAdapter -->|"spawn subprocess\nstdin: task prompt\nstdout: JSON events"| CodexBin
CodexBin -->|"OpenAI-compatible API\nBearer: scan-scoped token"| LLMProxy
LLMProxy --> Provider
Adapter --> Postgres
Adapter --> S3Art
What vega-core does
vega-core is a Python library (src/). It does not call the LLM directly. Instead it orchestrates the scan through a stage pipeline. Each stage submits an AgentTask to the AgentRunner, which dispatches it to CodexAdapter.
| Stage | What happens |
|---|---|
Plan (plan_repo) |
PlanStage spawns a codex exec --json subprocess with a prompt describing the repository structure. Codex reads the source tree and returns a structured plan: which components to audit and in what order. |
Threat Model (run_scan) |
ThreatModelStage spawns a Codex subprocess to analyze the plan and produce a markdown threat model document describing attack surfaces and risk areas. |
Audit (run_scan) |
For each component in the plan, AuditStage spawns a Codex subprocess with the component's source files and the threat model. Codex returns structured findings. vega-core emits these as finding_updated events. |
Verify (verify_and_patch) |
VerifyStage spawns Codex per bug to confirm whether the vulnerability is reachable and reproducible. |
Patch (verify_and_patch) |
PatchStage generates, reviews, applies, and validates a minimal diff for each verified bug. |
What Codex does
Codex (codex npm package, pre-installed in the runner image) is the AI CLI tool. It does not have any Vega-specific logic. vega-core points it at a directory, gives it a prompt, and reads JSON events from its stdout.
In the vega-core-runner ECS container there is no Docker-in-Docker. The codex binary is installed directly in the image alongside Node.js. This is different from the local development setup, where codex runs inside a sandboxed vega-codex-runner Docker container for isolation.
What vega-llm-proxy does
vega-llm-proxy is a separate, always-running ECS service. It acts as a credential firewall between the runner and the AI provider:
- The runner holds only a scan-scoped proxy token (short-lived, generated per scan by the worker).
- The proxy validates that token, enforces per-scan token/cost limits, and forwards the request to the real provider using the provider API key it holds in Secrets Manager.
- If a runner is compromised or abuses its quota, the proxy rejects further requests without exposing the real API key.
All codex calls in the runner use OPENAI_BASE_URL pointing at vega-llm-proxy, so Codex never touches the real provider endpoint directly.
VPC endpoints
What they are
By default, when a container inside the VPC needs to talk to an AWS service — for example, pulling an image from ECR or writing a log to CloudWatch — the traffic leaves the VPC, travels across the public internet, and comes back to the AWS service. This means:
- You need either a NAT Gateway (adds cost and a fixed hourly charge) or a public IP on each task.
- Traffic is exposed to the public internet even though both sides are within AWS.
A VPC endpoint is a private tunnel from your VPC directly into an AWS service, entirely within the AWS network. No NAT Gateway, no public IP, no internet exposure.
Two types
| Type | How it works | Cost |
|---|---|---|
| Gateway endpoint | Adds a route entry to private route tables. Traffic to the service goes straight through the route table entry. | Free |
| Interface endpoint | Creates a private network interface (ENI) inside your subnet. DNS resolves the service hostname to this private IP. | Fixed hourly charge per AZ + small data-processing fee |
Which endpoints Vega deploys
Both dev and prod deploy the same set, defined in private_runtime_endpoint_services in infra/terraform/envs/<env>/main.tf:
| Endpoint | Type | Why it exists |
|---|---|---|
| S3 | Gateway | The vega-core-runner runs in private subnets and reads source uploads and writes scan artifacts to S3. Gateway type — free, no ENI needed. |
| ecr.api | Interface | ECS needs this to authenticate with ECR and resolve image metadata before pulling. Required for any task that pulls from ECR in a private subnet. |
| ecr.dkr | Interface | This is the Docker registry data path — the actual image layer download. ecr.api alone is not enough; both must be present for a successful image pull. |
| logs | Interface | All ECS tasks use the awslogs log driver to ship stdout/stderr to CloudWatch. Without this endpoint, private tasks lose their logs. |
| secretsmanager | Interface | Task definitions inject DATABASE_URL and other secrets at startup from Secrets Manager. Private tasks cannot start without this endpoint. |
Why there is no KMS endpoint
You might expect a KMS endpoint given that all S3 buckets are encrypted with a customer-managed KMS key (alias/vega-<env>-s3). However, the KMS endpoint is not needed because:
- S3 SSE-KMS encryption and decryption happens server-side inside S3's infrastructure. When a task calls
s3:GetObjectors3:PutObject, S3 calls KMS internally on the task's behalf. The task itself never makes a direct KMS API call. - Secrets Manager uses the AWS-managed default key (
aws/secretsmanager) — no customer-managed KMS key is involved, and the Secrets module does not pass akms_key_id. - No application code directly calls
boto3.client("kms").
The KMS endpoint was originally included alongside the other private-runtime endpoints when the runner was first moved to private subnets (commit 44cddff, May 2026), under the reasonable but incorrect assumption that SSE-KMS requires the client to call KMS directly. It was removed after confirming S3 handles those calls server-side.
Terraform location
The interface endpoints are created in the environment main.tf files:
resource "aws_vpc_endpoint" "private_runtime" {
for_each = local.private_runtime_endpoint_services
# ...
}
The S3 gateway endpoint is created in infra/terraform/modules/network/main.tf and is always enabled (enable_s3_endpoint = true).
Logs and observability
CloudWatch is AWS's logging service. Every ECS container is configured to send its stdout/stderr to a CloudWatch log group. When debugging an AWS issue, CloudWatch logs are almost always the first place to look.
Log groups follow this naming pattern: /vega/<env>/<service-name>
Relevant Terraform: infra/terraform/modules/observability/main.tf.