# `ExSandbox.Hardening.Linux`
[🔗](https://github.com/FoundryStack/ex_sandbox/blob/v1.2.0/lib/ex_sandbox/hardening/linux.ex#L1)

OS-level confinement for the BEAM mechanism on Linux (005 T004 – T012,
contracts/hardening.md, research R2, R3, R9).

## The security boundary is here, not in the BEAM

Nothing inside the Erlang VM can cap a process's memory or deny it a socket.
`005` settled that the boundary is the operating system, so this module
composes five kernel-enforced constructions and refuses to launch when it
cannot build all of them.

    systemd-run --scope -p MemoryMax=… -p CPUQuota=…   # FR-008
      setpriv --reuid=… --regid=… --clear-groups        # FR-007
        bwrap --ro-bind / --bind <storage>              # FR-010
          env -i <granted…> BINDIR=<…>                  # FR-004
            <erlexec> <peer args>

## Refusal, never degradation

`build_command/2` returns `{:error, :hardening_unavailable}` when any
capability is missing. It never emits a command with a layer omitted, because
that failure is invisible: the sandbox launches, the happy path is identical,
and the only difference is whether tenant code is confined.

This is why `available?/0` requires **all five** capabilities rather than
most. Cgroup caps without privilege separation still permits reading platform
files — "mostly hardened" describes a host that is not hardened.

## Probes attempt; they never infer

Every probe below performs the operation it reports on. Reading `:os.type()`
would report `true` on a Linux host with no cgroup delegation, no `setpriv`,
or an unprivileged process — which is exactly the silent-failure mode R9
exists to prevent, and exactly what an operator cannot see.

## What `verify_applied/1` can and cannot establish

It reads the limits **in force on the running process** — the cgroup's
effective values — rather than the command that requested them. R9b measured
a limiter invoked with correct arguments, present in the process tree, named
in configuration, and silently not applied. Reading back the request would
have reported success.

Inspection is still not proof that a limit *stops* a breach; establishing that
is the conformance suite's job (`012-FR-012a`). The two are complementary and
neither substitutes for the other.

# `capability_map`

```elixir
@type capability_map() :: %{
  resource_limits: boolean(),
  privilege_separation: boolean(),
  filesystem_confinement: boolean(),
  network_restriction: boolean(),
  disk_quota: boolean()
}
```

# `available?`

```elixir
@spec available?() :: boolean()
```

True only when every capability is present.

There is no partial state: see the moduledoc.

# `build_command`

```elixir
@spec build_command(ExSandbox.Sandbox.t(), [{String.t(), String.t()}]) ::
  {:ok, {String.t(), [String.t()]}}
  | {:error,
     :hardening_unavailable | :invalid_limits | {:forbidden_env, [String.t()]}}
```

Builds the command that launches a sandbox under full confinement.

`granted_env` is an **allowlist** — the sandbox receives these pairs and
nothing else. Returns `{:error, :hardening_unavailable}` when any capability
is missing, and `{:error, {:forbidden_env, keys}}` when `granted_env` carries
a platform-shaped secret.

# `capabilities`

```elixir
@spec capabilities() :: capability_map()
```

Probes what this host can actually enforce.

Probed once at gateway startup rather than per provision — the answer is a
property of the host, and probing per launch would put shell-outs on the
provisioning path for a value that does not change.

# `compose_for_inspection`
*since 005 T009* 

```elixir
@spec compose_for_inspection(ExSandbox.Sandbox.t(), [{String.t(), String.t()}]) ::
  {:ok, {String.t(), [String.t()]}} | {:error, term()}
```

The command `build_command/2` would emit, without the availability gate.

Public for one reason: the command's *shape* must be testable on hosts where
hardening is unavailable, which is every developer machine that is not Linux.
The alternative is that `env -i` -- one word, easily dropped, and invisible
when missing because the sandbox still launches -- goes unverified until CI.

Callers must use `build_command/2`. This deliberately skips the availability
check, so a caller reaching for it directly would build exactly the degraded
command R9 forbids.

# `prepare_storage`

```elixir
@spec prepare_storage(ExSandbox.Sandbox.t()) :: :ok | {:error, term()}
```

Create this sandbox's writable storage, owned by the uid it will drop to
(FR-009, FR-010).

Called before `build_command/2`'s output is spawned. `bwrap` refuses a bind
whose source does not exist -- "Can't find source path ..." -- so without this
every launch exits 1 having constructed a perfectly correct command.

Ownership matters as much as existence. Created as root and left that way, the
sandbox drops privilege and cannot write to its own storage: the launch
succeeds and every write fails, which is harder to diagnose than an outright
refusal. Mode `0o700` keeps it to the owning uid, since every sandbox on a
gateway shares this root under a different uid.

# `scope_unit_name`

```elixir
@spec scope_unit_name(ExSandbox.Sandbox.t()) :: String.t()
```

The systemd scope unit a sandbox runs in.

Public because `provision_failure_reason/1` must name the same unit to read
its `Result` after death, and a mismatch between the two would silently report
every breach as a platform fault.

# `storage_path`

```elixir
@spec storage_path(ExSandbox.Sandbox.t()) :: String.t()
```

Where a sandbox's writable storage is bound, on the host and inside the
sandbox alike — `bwrap` binds it at the same path on both sides.

Public so callers and tests name the real location rather than assuming one. A
test writing to a made-up path gets `:enoent`, which satisfies "the sandbox
could not write here" just as well as a quota would — so the assertion passes
while measuring nothing.

# `verify_applied`

```elixir
@spec verify_applied(integer()) ::
  {:ok, map()} | {:error, {:not_applied, atom()} | :not_applied | :unverifiable}
```

Reads the confinement actually in force on a running sandbox process.

`{:error, :not_applied}` means the process is running unconfined and the
caller must terminate it — a running unconfined sandbox is what Principle II
forbids. `{:error, :unverifiable}` means this host cannot answer, which is
never reported as `:ok`.

---

*Consult [api-reference.md](api-reference.md) for complete listing*
