sg-compute.sgit.ai / security

Isolation, and the one trade-off

Every boundary below has a named mechanism rather than a policy statement, and one of them has a documented cost. A platform that publishes its own trade-offs is trusted differently from one that omits a weakness its own source code comments on — and the source is public either way.

The six boundaries

BoundaryMechanism
Job ↔ job, same nodeFresh Playwright subprocess, fresh Browser, fresh BrowserContext per request, torn down in try/finally. Not a cleared context — a new process. The lifecycle →
Session ↔ sessionA dedicated OS thread per session owning its own browser process; the registry is keyed by session id. Why the thread affinity is load-bearing →
Node ↔ nodeSeparate EC2 instances, per-node security group, and a per-node API key that is never reused
Untrusted JavaScriptJS__Expression__Allowlist — deny by default, exact match. Below.
NetworkBrowser traffic routed through the mitmproxy sidecar; “isolation is Docker network”; the sidecar's :8080 is unreachable from outside the instance
CookiesSET_COOKIE applies to the per-request context and is discarded with it — “stateless — the context is fresh per request… no session persistence”

The strongest of these is the node boundary, because it is the one that does not depend on the process model being correct: two workloads on two nodes are two EC2 instances with two security groups and two keys. Everything above that line is a software boundary; the node boundary is an AWS one.

Deny by default — and the one place it is switched off

JS__Expression__Allowlist is deny by default with exact matching. An agent calling evaluate gets rejected until an operator has explicitly populated the allowlist with the expressions it may run. Nothing is permitted by omission, and there is no pattern syntax to get wrong.

There is one bypass, allow_all, and it is set in exactly one place: the screenshot surface. The stated ground is that “each call is an isolated session” — which is true, given the per-request teardown above.

It is probably the right call, and it should still be a written decision rather than a flag. It is the single place a deny-by-default control is turned off in the whole platform, and the argument for it depends entirely on the per-request isolation continuing to hold. If that ever changes — a pooled browser, a reused context, a performance optimisation — this bypass changes meaning silently. It is on the loose-ends list.

The trade-off: a per-node API key in an EC2 tag

EC2__Platform.create_node mints a per-node key, writes it to SSM — the correct store — and then also writes it to an EC2 tag:

if node_info.instance_id:                          # tag so dashboard can read key without SSM
    EC2__Launch__Helper().add_tags(region, str(node_info.instance_id),
                                   [{'Key': 'sg-compute:host-api-key', 'Value': api_key}])

And the codebase already documents the consequence, in a schema comment:

“Visible to anyone with ec2:DescribeInstances.”

The facts, stated fairly

The mitigation, named

Read the key from SSM in the dashboard and drop the tag, accepting one extra API call per node on a page that is not latency-sensitive. That is the whole fix.

Whether to take it is Q6, published unresolved: the trade is one API call against exposure to a widely-granted IAM permission. A stated decision either way is fine. Silence is not — which is why this section exists rather than not.

This is published as an architecture note, not a vulnerability disclosure, and deliberately carries no exploitation path. The behaviour is visible in public source, the code comments on it itself, and omitting it would cost more credibility than the trade-off does.

What was checked, and is clean

Stated because it was verified rather than assumed, and because a security page that only lists problems is as unbalanced as one that lists none.

CheckedResult
.env files with real valuesnone — three .env.example only; .gitignore covers the rest
AWS access keysnone — every match is the AWS public documentation example or a synthetic fixture
Private keysnone — all synthetic, or generated at test runtime
GitHub / Slack / OpenAI / Anthropic tokensnone — zero matches
Public IP addresses in codenone
CI credentialsall from ${{ secrets.* }}

What this site will not publish

The platform documented here is live infrastructure, so the editorial rule is shapes, not addresses: <stack-name>.sg-compute.<zone> is documentation; a live FQDN is a target.

An AWS account id, ten internal hostnames, five named live stack FQDNs and sixteen real EC2 instance ids with three AMI ids were stripped from the source material before any of it was published here. Two of the source documents were the redaction list itself, and are published redacted under their own rule.

enforced

The list is encoded in the release gate, so it cannot regress. admin/build/validate.js fails the build if any file in the tree contains the account id, any of the hostnames, a stack-slug FQDN, an EC2 or AMI id shape, or a vault-key-shaped string. No tag, no publish. How the tripwire works, including its one deliberate exception →