---
title: "Hardening"
description: "Dial the controls to match your threat level"
icon: "user-shield"
---

The defaults are tuned for the common case: running untrusted code that needs the internet. When your threat model is sharper, such as multi-tenant, credential-handling, or fully offline, tighten the knobs below. Each one is independent, so apply the ones that fit.

## Turn the network off

If a workload doesn't need the network, remove it. Nothing to filter is the strongest filter.

<CodeGroup>
```typescript TypeScript
await using sb = await Sandbox.builder("isolated")
    .image("python")
    .disableNetwork()
    .create();
```

```rust Rust
let sb = Sandbox::builder("isolated")
    .image("python")
    .network(|n| n.enabled(false))
    .create()
    .await?;
```

```python Python
from microsandbox import Network, Sandbox

sb = await Sandbox.create("isolated", image="python", network=Network.none())
```

```go Go
sb, err := m.CreateSandbox(ctx, "isolated",
    m.WithImage("python"),
    m.WithNetwork(m.NetworkPolicy.None()),
)
if err != nil {
    return err
}
```

```ruby Ruby
sb = Microsandbox::Sandbox.create("isolated", image: "python", network: :none)
```

```bash CLI
msb create python --name isolated --no-net
```
</CodeGroup>

## Deny egress by default

When a workload needs only a handful of destinations, switch from allow-public to deny-by-default and allow exactly what's required. In any allow-by-default policy, remember to `deny@meta` so the cloud metadata service stays blocked.

<CodeGroup>
```typescript TypeScript
import { NetworkPolicy, Sandbox } from "microsandbox";

await using sb = await Sandbox.builder("restricted-worker")
    .image("alpine")
    .network((n) => n.policy(
        NetworkPolicy.builder()
            .defaultDeny()
            .egress((e) => e.tcp().port(443).allowPublic())
            .egress((e) => e.udp().tcp().port(53).allowHost())
            .build(),
    ))
    .create();
```

```rust Rust
use microsandbox::{NetworkPolicy, Sandbox};

let policy = NetworkPolicy::builder()
    .default_deny()
    .egress(|e| e.tcp().port(443).allow_public())
    .egress(|e| e.udp().tcp().port(53).allow_host())
    .build()?;

let sb = Sandbox::builder("restricted-worker")
    .image("alpine")
    .network(|n| n.policy(policy))
    .create()
    .await?;
```

```python Python
from microsandbox import Action, DestGroup, Destination, Network, NetworkPolicy, Protocol, Rule, Sandbox

sb = await Sandbox.create(
    "restricted-worker",
    image="alpine",
    network=Network(policy=NetworkPolicy(
        default_egress=Action.DENY,
        rules=(
            Rule.allow(destination=Destination.group(DestGroup.PUBLIC), protocol=Protocol.TCP, port=443),
            *Rule.allow_dns(),
        ),
    )),
)
```

```go Go
sb, err := m.CreateSandbox(ctx, "restricted-worker",
    m.WithImage("alpine"),
    m.WithNetwork(&m.NetworkConfig{
        DefaultEgress: m.PolicyActionDeny,
        Rules: []m.PolicyRule{
            {Action: m.PolicyActionAllow, Direction: m.PolicyDirectionEgress, Destination: "public", Protocol: m.PolicyProtocolTCP, Port: "443"},
            m.Rule.AllowDNS(),
        },
    }),
)
```

```bash CLI
msb create alpine --name restricted-worker \
  --net-default-egress deny \
  --net-rule "allow@public:tcp:443,allow@dns"
```
</CodeGroup>

See [Networking overview](/networking/overview#low-level-custom-policies) for the full rule syntax and SDK equivalents.

## Drop in-guest privileges

Two independent steps, both worth taking for untrusted code:

- Run the workload as a **non-root user**.
- Apply the **restricted security profile**, which sets `no_new_privs`, drops the mount-admin capability, and forces `nosuid,nodev` on user mounts.

<CodeGroup>
```typescript TypeScript
await using sb = await Sandbox.builder("worker")
    .image("python")
    .user("1000:1000")
    .security("restricted")
    .create();
```

```rust Rust
use microsandbox::{Sandbox, SecurityProfile};

let sb = Sandbox::builder("worker")
    .image("python")
    .user("1000:1000")
    .security(SecurityProfile::Restricted)
    .create()
    .await?;
```

```python Python
from microsandbox import Sandbox, SecurityProfile

sb = await Sandbox.create(
    "worker",
    image="python",
    user="1000:1000",
    security=SecurityProfile.RESTRICTED,
)
```

```go Go
sb, err := m.CreateSandbox(ctx, "worker",
    m.WithImage("python"),
    m.WithUser("1000:1000"),
    m.WithSecurityProfile(m.SecurityProfileRestricted),
)
```

```ruby Ruby
sb = Microsandbox::Sandbox.create(
  "worker", image: "python", user: "1000:1000"
)
# Ruby supports the non-root user option, but does not expose
# the restricted security-profile option shown in the other tabs.
```

```bash CLI
msb create python --name worker --user 1000:1000 --security restricted
```
</CodeGroup>

The restricted profile is incompatible with workloads that need those capabilities, such as `sudo` or Docker-in-Docker. See [Isolation boundary](/security/isolation#in-guest-privilege) for the SDK forms.

## Mount least privilege

- Mount only the directories a workload actually needs.
- Use **read-only** mounts wherever write isn't required. microsandbox enforces read-only host-side, so the guest can't remount around it.
- Avoid sharing a writable volume across sandboxes, since a shared volume is a deliberate [isolation hole](/security/filesystem#sharing-storage-between-sandboxes).
- Prefer a recent Linux kernel for writable bind mounts, where `openat2` containment is strongest.

## Pin and trust your images

- Pin images **by digest**, not a moving tag, for reproducibility.
- Pull from registries you control or trust, because content is digest-verified but [not signature-verified](/security/filesystem#image-supply-chain).

## Handle credentials with secrets

- Inject credentials with [secrets](/security/secrets) and narrow allow lists, so a value only materializes at the destinations that need it.
- Keep [`trust_host_cas`](/security/network#trusting-host-cas) off unless you're behind a corporate MITM proxy.
- For credentialed egress you want to inspect, enable [TLS inspection](/networking/tls) and pin [DNS](/networking/dns).

## Bound resources and lifetime

- Set vCPU and memory caps appropriate to the workload.
- Set an idle timeout or a maximum duration so abandoned sandboxes get reclaimed.

## Choosing a posture

| Use case | Network | In-guest privilege | Storage |
|---|---|---|---|
| Trusted internal tool | Default public egress | Root is fine | Mounts as needed |
| Untrusted code / AI agent with internet | Default, or deny-by-default allowlist | Non-root + restricted | Read-only mounts |
| Multi-tenant / per-user | Deny-by-default allowlist | Non-root + restricted | No shared writable volumes, one sandbox per tenant |
| Credential-handling egress | Deny-by-default + TLS inspection | Non-root + restricted | Minimal and read-only, with narrow secret allow lists |
| Fully offline / compute-only | `--no-net` | Non-root + restricted | Read-only or none |

## Where hardening stops

These knobs shrink the blast radius and tighten what a workload can reach. The boundary they sit on top of is still the hypervisor. See the [security model overview](/security/overview) for what that boundary does and doesn't cover.
