NOFire.ai
Blog/Engineering

How Brig could have interrupted the Binance attack

Binance's initial incident report names the path a malicious interview repository overwrote on an engineer's laptop. That write is a boundary a microVM sandbox can enforce before anyone knows the code is malicious. This post walks through the Brig v0.5.0 setup that contains it: a disposable guest home, no credentials, an egress allowlist, and a credential broker for the agents that do need an API.

How Brig could have interrupted the Binance attack

The most useful detail in Binance's initial incident report is a filesystem path:

/opt/homebrew/Cellar/pyenv/2.8.0/libexec/pyenv

A repository supplied as a job interview assignment ran code during setup and replaced that development tool on an engineer's work computer. Untrusted code changed a program that every later task on the laptop would trust.

That is a boundary we can enforce before we know whether the code is malicious.

Binance's initial report describes a campaign that moved from developer endpoints, through internal AI tooling and deployment credentials, into wallet-signing infrastructure. Binance puts the impact at about $6.34 billion. The investigation is ongoing.

We build Brig to put a microVM boundary around developer and agent execution. The technology underneath Brig is what runs NOFire AI's production, and Brig itself is what our engineering team uses day to day. For an untrusted assignment, I want a fresh guest home, no host project, no credentials and an explicit network policy. This post is that configuration, command by command, on Brig v0.5.0.

The attack crossed several boundaries

Binance describes these stages:

StageWhat the report says
JuneEngineer A downloaded a tampered Homebrew installation while trying to install an AI coding tool. Binance detected it, replaced the device and rotated related credentials. Information about the team had already been exposed.
SeptemberEngineer B ran the malicious interview repository. Its setup replaced a development-tool component and opened a connection to an attacker-controlled server.
20 OctoberThe attackers used the compromised computer and an internal AI tool to gain control of two other engineers' machines. They found a container-deployment token.
29 OctoberStolen credentials deployed a container and ran the attack against wallet-signing operations.

The public report says little about the internal AI-tool pivot and does not describe a prompt-injection mechanism. It does describe a path from one compromised endpoint to more machines and to production authority.

The first execution boundary matters because everything after it gets harder once the host is compromised. Installing a sandbox after the infection does not make that host trustworthy again. Recovery needs a rebuilt endpoint and revoked or rotated credentials.

Every command below was run on Brig v0.5.0 on macOS.

Start isolation before repository setup

A repository becomes executable input as soon as you install its dependencies, run its setup, load a development task or invoke a tool it supplies. The sandbox has to cover that whole process, not the part after setup.

For an interview assignment, the environment gets no production credentials and no existing host project. The repository lives in a fresh guest home. There is no reason for it to inherit the engineer's SSH agent, cloud configuration, browser state or unrelated working directories.

Brig's security model gives the guest its own kernel and limits host filesystem exposure to the guest home, the project you name and the mounts a profile declares. Keeping the host Homebrew prefix outside those mounts removes the direct write path used in the disclosed attack.

Figure 1: what an untrusted repository can reach inside the sandbox. The Homebrew prefix, the SSH agent, the keychain, cloud and Git configuration and other working directories stay on the host. The sandbox sees one host directory, a guest home Brig created for this session, and the clone is made inside it.
Figure 1: what an untrusted repository can reach inside the sandbox. The Homebrew prefix, the SSH agent, the keychain, cloud and Git configuration and other working directories stay on the host. The sandbox sees one host directory, a guest home Brig created for this session, and the clone is made inside it.

An adapted payload could still corrupt the guest's tools. To make that write reach the host it would need a separate escape or an exposed integration. The runtime and those integrations remain part of the security boundary.

Install from the release channel

On Apple Silicon with macOS 15 or newer, from a Homebrew installation you already trust:

brew tap brig-sh/brig
brew trust brig-sh/brig
brew install --cask brig
 
brig version
brig doctor

brew trust is required because the cask comes from a third-party tap. The commands follow the install page. For Linux, use the documented runtime installation, including the virtualization support it lists.

The installer is part of the trusted setup. Moving the eventual agent into a VM does not protect a host on which a malicious installer has already run. That is the June stage of Binance's timeline.

A profile with no credential bindings

Save this as review-shell.yaml and import it with brig agent import review-shell.yaml:

name: review-shell
desc: Disposable environment for reviewing an untrusted project
kind: agent
binary: bash
 
image: ghcr.io/brig-sh/claude-code-stock:root
hypervisor: hvi
network: isolated
genericBoot: true
guestHome: /root
 
mem: 4096
cpus: 2

This boots Brig's published Claude Code image and starts Bash. It inherits nothing from the built-in claude-code profile: no secrets, no env, no files, no extra host mounts. In Brig's profile format, kind: agent runs any Linux CLI named in binary. It does not need a model.

Restrict the network before the sandbox boots

A microVM can still make network requests. Brig's isolated posture gives the sandbox a network of its own, and that network reaches the internet until a policy says otherwise.

For a public Bitbucket repository with npm dependencies, the policy names two hosts. Policies live in ~/.config/brig/policies, or under $XDG_CONFIG_HOME or BRIG_POLICY_DIR when those are set (where policies live). Save the following as interview-egress.yaml there:

apiVersion: brig.sh/v1alpha1
kind: Policy
metadata:
  name: interview-egress
  description: Source host and package registry for this review
spec:
  egress:
    default: deny
    allow:
      - host: bitbucket.org
      - host: registry.npmjs.org

These two hosts fit this example. Check the project and its package sources. Another registry, a binary download or a Git dependency needs its own entry.

Use a new session name for each untrusted repository. A named session keeps its state until you remove it. Attach the policy to that session only, then read the plan before anything starts:

brig policy attach interview-egress review-shell -n interview
brig policy check review-shell -n interview
brig plan review-shell@interview --no-project

-n belongs to the policy commands and @interview names the same session on the run line (session bindings). --no-project refuses to mount a host project, including one the session ran with before. brig plan prints what the run would get and boots nothing:

SESSION      interview
PROFILE      review-shell
SANDBOX      brig-review-shell-interview
RUNTIME      hull (/opt/homebrew/bin/hull), backend hvi
ISOLATION    microVM (hull, hvi backend, rootfs overlay-block)
HOME         /Users/you/.brig/homes/brig-review-shell-interview (read-write, mounted at /root)
IMAGE        ghcr.io/brig-sh/claude-code-stock:root (pull missing)
VERIFY       warn, against brig's own trust policy
NETWORK      isolated (a network of this sandbox's own)
POLICY       interview-egress
EGRESS       default deny
             allow bitbucket.org
             allow registry.npmjs.org
CREDENTIALS  (none)
LIMITS       4096 MB, 2 vCPUs

That is the whole permission set. Boot it, confirm it, and open a shell:

brig run review-shell@interview --no-project -d
brig info review-shell@interview
brig sh review-shell@interview

From this point, clone and run the assignment inside the guest shell:

mkdir -p "$HOME/interview"
cd "$HOME/interview"
 
# Replace this with the repository you are reviewing.
git clone -- https://bitbucket.org/OWNER/REPOSITORY.git project
cd project
 
# For an npm project with a committed lockfile.
npm ci --ignore-scripts

The last flag suppresses lifecycle scripts for that install. Later builds, tests and explicit scripts still run the project's code. Keep them in the same guest. If a tool is missing, add it through an image you build or a package source you reviewed.

Brig enforces the policy outside the guest kernel, at the network gateway on macOS and in nftables on Linux. A process in the guest cannot lift the rules by changing its own firewall. A run path that cannot enforce the policy is refused rather than booted unconstrained.

Check the boundary you configured

Inside the guest, try an allowed destination, an unrelated name and a raw address:

curl -sS -m 15 -o /dev/null -w '%{http_code}\n' https://bitbucket.org/
curl -sS -m 10 -o /dev/null https://example.com/
curl -sS -m 10 -o /dev/null https://93.184.215.14/

Under this policy the first returns a status and the other two fail:

200
curl: (6) Could not resolve host: example.com
curl: (7) Failed to connect to 93.184.215.14 port 443 after 0 ms: Couldn't connect to server

A name the policy does not allow does not resolve, and an address it does not allow does not connect. Read these next to the plan. A failed request on its own can also mean a broken network or a server that is down.

Check for credential variables without printing a value:

for name in GH_TOKEN GITHUB_TOKEN AWS_ACCESS_KEY_ID AWS_SECRET_ACCESS_KEY; do
  if printenv "$name" >/dev/null; then
    printf 'Unexpected credential variable: %s\n' "$name"
  fi
done

That covers the environment. The plan and brig info cover the mounts and the files. Read both for a profile someone else wrote.

Hostname filtering has a limit. Brig's host rules decide an address, not a name. Neither enforcer reads TLS SNI or an HTTP Host header, so two names served from one address get the same verdict (limits). An allowed service can also host attacker-controlled content. A sensitive authenticated operation needs a narrower permission than reachability of a domain, which is what the broker below is for.

Remove the environment when the task is over

Exit the guest shell, then on the host:

brig stop review-shell@interview
brig rm review-shell@interview

stop keeps the session. rm removes the sandbox and deletes the guest home Brig created for it. Neither touches a project you mounted (what survives).

That is why the example uses --no-project. A writable checkout can carry malicious hooks or configuration back to the host without exploiting the hypervisor. If you export results, treat them as untrusted artifacts and review them before running anything on the host.

Keep credentials outside the workload that uses them

Some agents need a real API. Passing a production token into their environment gives every process that controls that environment the same token.

Brig's credential broker is the alternative. The guest receives a route URL and a session token. The host checks the token, checks the route's restrictions, adds the real credential and forwards the request. The credential never enters the guest.

Figure 2: one brokered API request. The agent sends a GET to the route URL with a session token. The gateway's policy is default deny, and the route reaches brig-broker on the host. The broker checks the token and the request.allow list, adds X-Api-Key from Brig's secret store and forwards the request upstream. A HEAD, or a path outside the list, is refused with a 403 before anything reaches the upstream. One audit line is written per request.
Figure 2: one brokered API request. The agent sends a GET to the route URL with a session token. The gateway's policy is default deny, and the route reaches brig-broker on the host. The broker checks the token and the request.allow list, adds X-Api-Key from Brig's secret store and forwards the request upstream. A HEAD, or a path outside the list, is refused with a 403 before anything reaches the upstream. One audit line is written per request.

An agent that needs operational inventory can receive a route limited to reads. This is a template for an API your organization owns. Replace the upstream and provision a matching credential before using it. Save inventory-read.yaml in the policy directory:

apiVersion: brig.sh/v1alpha1
kind: Policy
metadata:
  name: inventory-read
spec:
  egress:
    default: deny
  routes:
    - name: inventory
      upstream: https://inventory.internal.example.com
      credential:
        secretRef:
          name: inventory-token
      inject:
        headers:
          set:
            - name: X-Api-Key
              value: "{value}"
      guest:
        env:
          INVENTORY_URL: "{route.url}"
          INVENTORY_TOKEN: "{session.token}"
      request:
        allow:
          methods: [GET]
          paths: ["/v2/items/*", "/v2/search"]

A route needs no preset. This one names the upstream, the header the key goes in and the variables the guest gets, the same shape as the custom routes on the broker page. Request rules and headers cover what request.allow and inject accept.

Supply the token on stdin from your secret manager, then attach the route to a session of its own. This example reads from the 1Password CLI; replace the reference with your own:

op read 'op://VAULT/ITEM/FIELD' | brig secret create inventory-token
 
brig policy attach inventory-read review-shell -n inventory
brig policy check review-shell -n inventory
brig plan review-shell@inventory --no-project
brig run review-shell@inventory --no-project -d
brig sh review-shell@inventory

brig secret create reads the value from stdin and keeps it in your keyring. It is never a command-line argument. check prints one row per run path and says where the route is enforced, and the plan's BROKER row names the secret that stays on the host.

Inside that guest, the application uses the route URL and the session token:

curl -fsS -H "Authorization: Bearer $INVENTORY_TOKEN" "$INVENTORY_URL/v2/items/payments"

The broker presents the real X-Api-Key to the upstream. A different method, or a path outside the allowlist, is refused before any upstream request.

Check the method restriction with a HEAD to the route. The allowlist permits only GET, so this returns 403:

curl -sS -I -m 10 -H "Authorization: Bearer $INVENTORY_TOKEN" -o /dev/null -w '%{http_code}\n' "$INVENTORY_URL/v2/search"

A POST to the same path, or a GET to a path off the list, gets the same status and a body that names the rule:

brig broker refused this request
policy:  inventory-read
route:   inventory
rule:    spec.routes[inventory].request.allow.methods
reason:  method "POST" is not in the allow list [GET]
sandbox: brig-review-shell-inventory

A request without the session token gets a 401. A direct connection from the guest to any other host still fails, because the policy's egress default is deny. Each of these refusals is one line in the broker's audit log on the host.

The permission still exists. A compromised workload can read the allowed inventory. The broker does not establish that every permitted request matches the human's intent, and a method and path rule does not validate a request body.

For an agent that needs a model provider, Brig ships route presets, including one for Claude Code's own login. The rule is the same: choose which credentials and which operations the session receives.

Deployment and signing need their own authorization

In Binance's disclosed chain, a deployment credential became a path to a signing service. At that point, filesystem isolation is not the control. An isolated process that holds broad deployment authority can still deploy an attacker-selected workload.

I would make deployment authorization specific to the change:

  • The destination environment and workload are identified.
  • The artifact digest is approved and its provenance is checked.
  • The operation is limited to what that deployment requires.
  • Any required approval is enforced by the deployment service before execution.
  • The resulting workload identity cannot inherit broad access to signing infrastructure.

Those controls belong at the service that accepts the change. They have to hold when the caller is compromised.

Signing needs another boundary. Transaction type, destination, value and delegation authority have to be evaluated before a signature is issued. A service that protects key material but accepts unauthorized signing requests still exposes the authority those keys represent.

Binance reports that malicious EIP-7702 authorizations continued to matter after normal withdrawals were suspended. The EIP-7702 specification defines persistent delegation and an explicit mechanism to clear it. Stopping the process that requested an authorization does not revoke an authorization already on chain, obviously.

A financial incident response plan therefore has to separate three operations: stopping new requests, revoking credentials, and undoing durable external permissions.

Give the security team evidence from outside the guest

For an organization, isolation also needs an operational record. The security team should be able to connect a sandbox's activity with the developer, the endpoint, the identity and the production systems involved.

Open-source Brig already writes one audit line per brokered request on the host: the route, the method, the path, the status and the reason for a refusal. The line never holds a credential, a token, a header value or a body. That is a specific API audit trail.

Our enterprise offering around Brig adds host-side observation, centralized action records, policy administration and SIEM integration through OpenTelemetry and OCSF. Its private beta includes SIEM destinations such as Splunk, Microsoft Sentinel, CrowdStrike and Elastic. The collector observes supported network, disk and file activity from outside the guest, and records connect workloads and sessions with attempted actions, policy decisions and outcomes.

The fleet controls, the host collector and the SIEM export are in private beta. Our enterprise agent security page lists their availability and scope.

For this attack pattern, I would correlate an unexpected sandbox API request with endpoint alerts, use of the developer's deployment identity and changes to production workloads. That gives the SOC a sequence across systems to investigate.

A SIEM event is evidence after the fact. An egress rule or an authorization check has to reject a disallowed operation before it reaches the target. Host-side collection also assumes a trustworthy host. Central retention does not repair a compromised endpoint.

Put the boundary where the code starts running

Each stage in Binance's timeline meets a boundary. Brig provides the first three. The last belongs elsewhere:

StageBoundary
June, a tampered installerA verified install channel. BRIG_VERIFY=require refuses an image it cannot verify. The installer itself stays part of the trusted setup.
September, a repository's setupA microVM whose only host directory is a guest home Brig created, no credentials, and a default-deny egress policy. brig rm deletes what the repository touched.
20 October, lateral movement and a found tokenNo token in the workload. An agent that needs an API gets a route with a method and path allowlist, and the credential stays on the host.
29 October, deploy and signAuthorization at the deployment service and the signer, bound to the artifact, the environment and the transaction. Not a sandbox property.

A malicious repository should be able to ruin its disposable workspace. It should not acquire the engineer's working environment along with it.

The developer should not have to write this

The setup above comes down to a profile, a policy and a named session. A brokered route adds another policy and a secret. The profiles and policies are reusable YAML. We should review, version and distribute them as part of how we ship software.

The team that owns the risk should own this configuration: SRE or platform, security, or the team running the service being accessed. Keep it in a repository, review changes, and sign and distribute the bundle like any other release. Developers import the profile with brig agent import and receive the policy directory, including attachments.yaml, as a unit. Keep credentials separate from that bundle. Each engineer gets the same reviewed configuration without having to work out the security model from scratch.

In our enterprise offering, we make it easier to validate, sign, and distribute policies and configuration for Brig. But the FOSS version can get you going, today.

What the developer keeps is one command:

brig run review-shell@interview --no-project   # a repository you do not trust
brig run claude-code ~/code/service            # your own project, under the team's policy

The same shape covers a foreign repository, a dependency upgrade nobody has vetted yet, and an agent working on the developer's own project, which is how our team runs its day-to-day development. The plan is readable before the first boot, the sandbox is up in seconds, and nothing on the laptop is at stake.

Brig is open source under Apache 2.0. Inspect the profile, read the plan and test what the guest can reach. We wrote down what a sandbox has to guarantee in the sandboxing manifesto. For fleet policy, external observation and SIEM integration, talk to us about your deployment.

Open source

Don't trust the sandbox. Test it.

brig runs each coding agent in its own microVM, with its own Linux kernel, on your Mac or Linux machine. Apache 2.0, no account, no gated features. Every claim comes with the command that checks it.