Skip to content

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.ai and the API is http://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-Origin headers.

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:

  1. From CloudFront's known IP ranges (enforced via AWS-managed prefix lists in the security group)
  2. That includes a secret x-vega-origin-verify header (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 caching
  • api-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:

  1. The user enters their credentials on the dashboard login page.
  2. The frontend calls Cognito directly (SRP authentication protocol) and receives JWT tokens.
  3. For every subsequent API request, the frontend sends Authorization: Bearer <token>.
  4. The vega-api validates 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/amd64build-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)

  1. User creates a scan in the dashboard.
  2. vega-api writes a queued scan row to RDS Postgres.
  3. vega-api sends a message to the SQS queue containing the scan ID.
  4. vega-worker (which is always running) receives the SQS message.
  5. vega-worker claims the scan with a Postgres row lock (prevents double-execution).
  6. vega-worker calls AWS ECS RunTask to start a vega-core-runner container.
  7. vega-core-runner downloads the source snapshot from S3.
  8. Inside the container, vega-core (the Python scan engine) runs two phases:
  9. Planning: vega-core spawns a codex subprocess to analyze the repo structure and produce a list of components to audit.
  10. Auditing: for each component, vega-core spawns another codex subprocess to analyze that code against the threat profile and generate findings.
  11. Every codex subprocess calls the LLM via vega-llm-proxy using an OpenAI-compatible API. The runner holds only a short-lived, scan-scoped proxy token — the real provider API key lives in the proxy.
  12. vega-core emits events as it works. Findings and events are written to Postgres. Artifacts are uploaded to S3.
  13. Scan status is updated to completed.
  14. The vega-core-runner ECS task exits. You're only billed for the time it ran.
  15. 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:GetObject or s3: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 a kms_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.