> ## Documentation Index
> Fetch the complete documentation index at: https://docs.xorcise.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Security and isolation

> What XORCISE contains, what it does not, what leaves your machine, and how to run it safely on a shared host.

XORCISE runs deliberately vulnerable software on your machine and exposes an unauthenticated API to drive it. Both are deliberate, and together they decide where you should and should not run XORCISE.

## What you are running

A mission is intentionally broken software. Its services carry real, exploitable flaws, because an agent that cannot compromise anything cannot be measured. Pulling a mission from the library means pulling a container image built to be broken into.

XORCISE runs each lab mission inside a **fused container** — a privileged container hosting its own nested Docker daemon, which in turn runs the mission's stack. The privilege is what makes nesting possible. It also means the fused container is not a hardened boundary: treat a container escape as possible rather than impossible.

Run XORCISE on a machine you own, on a network you trust, holding data you could afford to lose. Do not run it on a production host, a build server, a jump box, or anything else shared with work you care about.

## The trust boundary

```mermaid theme={"theme":"css-variables"}
%%{init: {'theme':'base','themeVariables':{'fontFamily':'JetBrains Mono, ui-monospace, SFMono-Regular, Consolas, monospace','fontSize':'12px','primaryColor':'#232323','primaryTextColor':'#f2ead6','primaryBorderColor':'#3a3a3a','secondaryColor':'#1a1a1a','tertiaryColor':'#141414','background':'#1a1a1a','mainBkg':'#232323','nodeBorder':'#3a3a3a','lineColor':'#6e6144','textColor':'#c7bb9f','clusterBkg':'transparent','clusterBorder':'#2f2f2f','edgeLabelBackground':'#1a1a1a','actorBkg':'#232323','actorBorder':'#3a3a3a','actorTextColor':'#f2ead6','actorLineColor':'#6e6144','signalColor':'#6e6144','signalTextColor':'#c7bb9f','labelBoxBkgColor':'#232323','labelBoxBorderColor':'#3a3a3a','labelTextColor':'#f2ead6','noteBkgColor':'#141414','noteTextColor':'#c7bb9f','noteBorderColor':'#3a3a3a','sequenceNumberColor':'#0a0805'},'flowchart':{'padding':26,'nodeSpacing':40,'rankSpacing':46},'sequence':{'useMaxWidth':true}}}%%
flowchart TD
    subgraph net["Your network — not trusted, not defended"]
        lan[Anyone who can reach<br/>the REST and OTLP ports<br/>only once the bind is widened]
    end

    subgraph host["Your machine — trusted"]
        brain[XORCISE brain<br/>REST + UI and OTLP<br/>bind loopback + Docker bridge<br/>no authentication]
        local[Any local process, and any<br/>container on that bridge]
        store[(~/.xorcise<br/>runs · traces<br/>results<br/>.env mode 0600)]
        agent[Your agent harness]
    end

    subgraph run["Per-run environment — not trusted"]
        fused[Fused mission container<br/>privileged, nested Docker]
        target[Mission target<br/>deliberately vulnerable]
    end

    subgraph off["Off-machine"]
        model[Your model provider]
        cat[XORCISE catalog]
        inet[The internet]
    end

    local -->|no credential needed| brain
    lan -.->|only when XORCISE_HOST=0.0.0.0,<br/>or the bridge gateway is unresolvable| brain
    brain --> store
    brain -->|Docker| fused
    fused --> target
    agent -->|per-run tailnet, by IP| target
    brain -.->|judge and terrain transcript| model
    brain -.->|browse and pull| cat
    target -.->|NAT egress, not blocked| inet
```

Three zones matter. Your machine is inside the boundary. The per-run environment is outside it, and XORCISE keeps it away from your host. The network your machine sits on is also outside it: the default bind keeps it off the operator surface, but nothing on your own machine is kept off it, and widening the bind hands the network the same unauthenticated access every local process already has.

## What isolation covers, and what it does not

Isolation in XORCISE is two mechanisms, both real and both narrow.

**Topology.** Mission services live on ordinary bridge networks belonging to a Docker daemon nested inside the fused container. Your host has no route to those addresses. Your agent reaches them only across the run's private tailnet, and only by IP address — XORCISE runs its Headscale control plane with MagicDNS off and no global nameservers, so nothing on the tailnet resolves by name. Each run carves its own `/24` out of `10.200.0.0/16` and holds that reservation until it goes terminal.

**A fail-closed access policy.** XORCISE renders exactly one Headscale accept rule per run: that run's agent user, to that run's entry CIDRs, and nothing else. There is no default-allow rule, so Headscale's default-deny covers everything the rules do not name. Before applying any policy XORCISE refuses it outright if it contains a wildcard source or destination, more than one rule per agent user, or a destination belonging to another run. Agent A can reach mission A and cannot reach mission B.

What isolation is **not**:

* It is **not** a Docker `internal` network. The run's networks are ordinary bridge networks with ordinary NAT.
* It is **not** an egress firewall. **Traffic from the mission target out to the internet is not blocked.** A mission that phones home, or a target an agent has compromised, reaches the internet the same way any container on your machine does.
* It is **not** a defence against container escape, for the privilege reason above.

If you need the mission environment to have no internet access, cut that egress yourself at the host or network level. XORCISE does not do it for you.

## Open ports and no authentication

In the default `local` deployment topology, the REST plus web UI plane and the OTLP receiver bind **loopback** — `127.0.0.1` and the IPv6 `::1` — plus, on native Linux, the Docker bridge gateway. The bridge address is what makes a container-launched agent's `host.docker.internal` reachable; on Docker Desktop that name already resolves to the host loopback, so no extra address is needed there.

The bind widens to the IPv4 wildcard **`0.0.0.0`** in exactly two cases: you set `XORCISE_HOST=0.0.0.0` yourself, or you are on Linux and XORCISE cannot determine a bindable Docker bridge gateway at boot, where a loopback-only bind would break every run rather than lock anything down.

The `host` setting, which defaults to `127.0.0.1`, is the address XORCISE prints and health-polls. Setting it to something other than `0.0.0.0` does not narrow the bind below loopback.

Neither plane authenticates.

* The operator API — `/api/runs`, `/api/agents`, `/api/config`, `/api/missions`, `/api/catalog`, `/api/system` and `/api/fs` — accepts any caller. `/api/fs/list` lists directory names starting from your home directory.
* The per-run run-control verbs under `/api/runs/{run_id}/…` do carry a per-run bearer token, but they live in the same application on the same port. That token scopes those verbs only; it protects nothing else.
* The OTLP receiver's `/v1/traces` and `/v1/logs` take no credential at all. Runs are separated by the `xorcise.run_id` resource attribute alone.

The practical consequence: anyone who can reach the port can read your runs, results and traces, rewrite your configuration, and create runs against your Docker daemon. Under the default bind that means every process on your machine and every container on the Docker bridge — not a small set, and none of it authenticated. Widen the bind and it means the network too, so on a shared, untrusted or public network, do not leave XORCISE up.

<Warning>
  The REST API, the web UI and the OTLP receiver require no credential. By default they are reachable from your own machine and from any container on the Docker bridge; set `XORCISE_HOST=0.0.0.0` and they are reachable from the network as well. Anyone who can reach those ports can read your runs and traces, change your configuration, and start runs. Firewall the ports, or run `xorcise down` when you are not using XORCISE.
</Warning>

Both planes auto-increment across a 50-port window when their default port is busy, so firewall the ports `xorcise up` printed rather than assuming `3001` and `4318`.

## What leaves your machine

XORCISE is local by default, not local unconditionally. Three things reach the network, and the judge is the one that matters most.

| Destination                                                                                            | What XORCISE sends                                                                                                                                                       | When                                                                                       | How to prevent it                                                                                               |
| ------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------ | --------------------------------------------------------------------------------------------------------------- |
| **Your model provider** — `https://api.openai.com/v1` by default, or whatever `model_base_url` you set | The **distilled run transcript**: your agent's spans and prose, plus the mission rubric. Terrain attribution sends batches of the same evidence. Both carry your API key | Whenever the judge grades a run, and when a run goes terminal if terrain attribution is on | Configure no judge model. Set `terrain_auto_attribute` to `false` to keep attribution strictly viewer-triggered |
| **The hosted catalog** at `catalog_url`                                                                | Only the mission id you are browsing, fetching or pulling. No run data, no traces, no results, and no credential of any kind                                             | Browsing the library, pulling a mission, and the Settings status probe                     | `xorcise catalog disconnect`                                                                                    |
| **The container registry**                                                                             | A login with the short-lived scoped token the catalog mints on demand, then an image pull                                                                                | Only when you pull a library mission                                                       | Do not pull library missions                                                                                    |
| `pkgs.tailscale.com`                                                                                   | An anonymous request for the pinned Tailscale client, roughly 37 MB per architecture                                                                                     | The first tailnet join per architecture, then served from `~/.xorcise/cache/tailscale`     | Pre-seed the cache                                                                                              |

Docker also pulls the Headscale, Tailscale and mission-base images the first time it needs them. That is Docker's egress on your behalf, not XORCISE reporting anything.

Everything else stays on your machine. Raw OTLP traces and logs, runs, agents, results, grades and terrain data all live in `~/.xorcise/xorcise.db`; mission bundles you ingest yourself live in `~/.xorcise/missions`. **XORCISE has no analytics, no crash reporting, no usage beacon and no phone-home.** The trace-mirror settings (`otel_mirror_enabled`, `otel_mirror_endpoint`) are a reserved seam with no exporter behind them; enabling one fails fast at boot rather than quietly forwarding anything.

Never repeat the shorter claim that nothing leaves your machine. Once a judge model is configured, the transcript of every graded run goes to that provider.

## Where secrets live

Your model API keys live in `~/.xorcise/.env`, created at mode `0600` and re-set to `0600` on every write. That file is the only copy — nothing else on your machine holds the key.

XORCISE is careful not to hand the key back out. `xorcise config show` and `GET /api/config` return a `key_hint`, never the key. When a judge call fails, XORCISE stores the provider's error text so you can diagnose it, and redacts the configured key out of that text first, because providers routinely echo a bad key back in the error body.

Three more things are worth knowing:

* `run_control_signing_secret` ships as the literal string `dev-insecure-signing-secret`. It signs the short-lived attachment download URLs whose lifetime `attachment_ttl_seconds` controls. Override it on any host other than your own laptop.
* The private CA that signs your local Headscale certificate lives at `~/.xorcise/headscale/certs/ca.key`. Delete the whole `certs/` directory or none of it — removing the key alone leaves a CA that cannot be regenerated.
* Backups are not managed. Any copy of `.env` you make by hand holds the key in cleartext.

## Harden a shared host

Do these three things before running XORCISE anywhere other than a machine only you can reach. Substitute the ports `xorcise up` printed.

<Steps>
  <Step title="Close the ports">
    Close the ports to everything except loopback and the Docker bridge, so your agent container still reaches the brain while the rest of the network does not:

    ```bash theme={"theme":"css-variables"}
    sudo ufw allow in on lo to any port 3001 proto tcp
    sudo ufw allow in on docker0 to any port 3001 proto tcp
    sudo ufw deny 3001/tcp
    sudo ufw allow in on lo to any port 4318 proto tcp
    sudo ufw allow in on docker0 to any port 4318 proto tcp
    sudo ufw deny 4318/tcp
    ```
  </Step>

  <Step title="Replace the attachment signing secret">
    Replace the attachment signing secret by adding a line to `~/.xorcise/.env`, then restart XORCISE with `xorcise down && xorcise up`:

    ```bash theme={"theme":"css-variables"}
    XORCISE_RUN_CONTROL_SIGNING_SECRET=<a long random string>
    ```
  </Step>

  <Step title="Stop XORCISE">
    Stop XORCISE when you are not using it. This releases both ports, tears down the local control plane, and reaps any per-run mission containers left behind:

    ```bash theme={"theme":"css-variables"}
    xorcise down
    ```
  </Step>
</Steps>

## Next steps

* [Configuration keys](/reference/configuration) — every setting named on this page, with its type, default and environment variable.
