Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,9 @@ with Sandbox("devbox") as sb:
Each sandbox is a microVM under KVM, and the VMM itself is confined: no new
privileges, seccomp, a reduced capability set, its own network namespace and,
under `isolation: strict`, a UID that is not yours.
`security/escape-test.sh` tries what an agent gone wrong would. See
`box inspect <name> --security-report` shows what a sandbox can and cannot
touch, read from the configuration box applies and checked against the running
VMM. `security/escape-test.sh` tries what an agent gone wrong would. See
[SECURITY.md](SECURITY.md) for the threat model.

## Development
Expand Down
12 changes: 12 additions & 0 deletions SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,18 @@ rather than let two sandboxes share a UID. `box doctor` shows how many strict
sandboxes your range holds. A destroyed sandbox's kept `/data` is handed back
to you, so a later sandbox given the same slot does not own it.

## Checking it yourself

`box inspect <name> --security-report` (or `=json`) states what a sandbox's
VMM can and cannot touch: its host UID, capabilities, `no_new_privs`,
seccomp, namespaces and limits, the network, `/data` and every host mount,
and what box last proved about the kernel boundary. It is read back out of
the configuration box hands the backend -- podman's arguments, or the OCI
spec for krun and the Firecracker jail -- so it cannot describe a confinement
box does not apply. When the sandbox is up, the running VMM is read from
`/proc` as well, and any difference is a failure (exit status 1). The escape
suite checks the report against the VMM itself.

## What it is not

- **Not a sandbox for image builds.** `box build` runs `podman build`, so a
Expand Down
2 changes: 1 addition & 1 deletion docs/docs.html
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ <h2 id="boxfile">Boxfile</h2>
<h2 id="cli">CLI</h2>
<div class="table-wrap build"><table>
<thead><tr><th>Command</th><th>What it does</th></tr></thead>
<tbody><tr><td class="mono">box new &lt;name&gt; [--from example]</td><td>Create a sandbox, optionally from a shipped example</td></tr><tr><td class="mono">box build &lt;name&gt;</td><td>Build the image and prove the sandbox has its own kernel</td></tr><tr><td class="mono">box run &lt;name&gt; -- &lt;cmd&gt;</td><td>Run one command in a fresh VM</td></tr><tr><td class="mono">box up / down &lt;name&gt;</td><td>Keep a VM running, or stop it</td></tr><tr><td class="mono">box exec &lt;name&gt; -- &lt;cmd&gt;</td><td>Run in the running VM; state carries over</td></tr><tr><td class="mono">box shell &lt;name&gt;</td><td>Interactive session</td></tr><tr><td class="mono">box fork &lt;name&gt; &lt;fork&gt;</td><td>Branch /data</td></tr><tr><td class="mono">box diff / apply / discard &lt;fork&gt;</td><td>Review, merge or drop a fork's changes</td></tr><tr><td class="mono">box data import / export &lt;name&gt; &lt;dir&gt;</td><td>Move files into or out of /data</td></tr><tr><td class="mono">box snapshot / restore &lt;name&gt; [label]</td><td>Archive /data and roll back</td></tr><tr><td class="mono">box reset &lt;name&gt;</td><td>Empty /data</td></tr><tr><td class="mono">box serve</td><td>The local API the SDKs use</td></tr><tr><td class="mono">box doctor</td><td>Check the host setup, with a fix for each failure</td></tr><tr><td class="mono">box ls, env, logs, verify</td><td>Inspect sandboxes</td></tr><tr><td class="mono">box rename, destroy, nuke</td><td>Remove or rename</td></tr></tbody>
<tbody><tr><td class="mono">box new &lt;name&gt; [--from example]</td><td>Create a sandbox, optionally from a shipped example</td></tr><tr><td class="mono">box build &lt;name&gt;</td><td>Build the image and prove the sandbox has its own kernel</td></tr><tr><td class="mono">box run &lt;name&gt; -- &lt;cmd&gt;</td><td>Run one command in a fresh VM</td></tr><tr><td class="mono">box up / down &lt;name&gt;</td><td>Keep a VM running, or stop it</td></tr><tr><td class="mono">box exec &lt;name&gt; -- &lt;cmd&gt;</td><td>Run in the running VM; state carries over</td></tr><tr><td class="mono">box shell &lt;name&gt;</td><td>Interactive session</td></tr><tr><td class="mono">box fork &lt;name&gt; &lt;fork&gt;</td><td>Branch /data</td></tr><tr><td class="mono">box diff / apply / discard &lt;fork&gt;</td><td>Review, merge or drop a fork's changes</td></tr><tr><td class="mono">box data import / export &lt;name&gt; &lt;dir&gt;</td><td>Move files into or out of /data</td></tr><tr><td class="mono">box snapshot / restore &lt;name&gt; [label]</td><td>Archive /data and roll back</td></tr><tr><td class="mono">box reset &lt;name&gt;</td><td>Empty /data</td></tr><tr><td class="mono">box serve</td><td>The local API the SDKs use</td></tr><tr><td class="mono">box doctor</td><td>Check the host setup, with a fix for each failure</td></tr><tr><td class="mono">box inspect &lt;name&gt; [--security-report=json]</td><td>What the sandbox can and cannot touch, read from the configuration box applies and checked against the running VMM</td></tr><tr><td class="mono">box ls, env, logs, verify</td><td>Inspect sandboxes</td></tr><tr><td class="mono">box rename, destroy, nuke</td><td>Remove or rename</td></tr></tbody>
</table></div>

<h2 id="sdk">SDKs</h2>
Expand Down
149 changes: 149 additions & 0 deletions internal/cli/inspect.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
package cli

import (
"encoding/json"
"fmt"
"os"
"strings"

"github.com/spf13/cobra"

"box/internal/runtime"
)

func inspectCmd() *cobra.Command {
var format string
c := &cobra.Command{
Use: "inspect <name>", Short: "what a sandbox can and cannot touch", GroupID: groupInspect,
Long: "A security report: the VMM's identity and confinement, the network, what\n" +
"the sandbox can read and write on the host, and what box last proved.\n\n" +
"It is read back out of the configuration box hands the backend, not\n" +
"from the Boxfile, so it cannot describe a confinement box does not\n" +
"apply. When the sandbox is up, the running VMM is read too, and any\n" +
"difference is a failure: the command then exits 1.",
Args: cobra.ExactArgs(1),
ValidArgsFunction: completeName,
Example: " box inspect agent --security-report\n" +
" box inspect agent --security-report=json | jq .findings",
RunE: func(_ *cobra.Command, args []string) error {
name := args[0]
s, err := loadSpec(name)
if err != nil {
return err
}
r, err := runtime.SecurityReport(name, s)
if err != nil {
return err
}
switch format {
case "json":
enc := json.NewEncoder(os.Stdout)
enc.SetIndent("", " ")
if err := enc.Encode(r); err != nil {
return err
}
case "text", "":
printReport(r)
default:
return fmt.Errorf("--security-report is text or json, not %q", format)
}
if r.Failed() {
exitCode = 1
}
return nil
},
}
c.Flags().StringVar(&format, "security-report", "text", "report format: text or json")
c.Flags().Lookup("security-report").NoOptDefVal = "text"
return c
}

func printReport(r runtime.Report) {
fmt.Printf("%s (backend %s, isolation %s)\n", r.Sandbox, r.Backend, r.Isolation)
fmt.Printf("read from %s\n\n", r.ReadFrom)

v := r.VMM
who := fmt.Sprintf("host UID %d", v.HostUID)
switch {
case v.RunsAsYou:
who += " (you)"
case v.Slot != nil:
who += fmt.Sprintf(" (strict slot %d, owns nothing of yours or of another sandbox)", *v.Slot)
}
caps := "none"
if len(v.Capabilities) > 0 {
caps = strings.Join(v.Capabilities, " ")
}
fmt.Println("VMM")
row("runs as", who)
row("capabilities", caps)
row("no_new_privs", yesno(v.NoNewPrivileges))
row("seccomp", v.Seccomp)
row("namespaces", strings.Join(v.Namespaces, " "))
row("limits", fmt.Sprintf("%d processes, %d MiB", v.PidsLimit, v.MemoryLimitMiB))

fmt.Println("\nnetwork")
row("mode", r.Network.Mode)
row("own namespace", yesno(r.Network.OwnNamespace))
row("host loopback", map[bool]string{true: "reachable", false: "not reachable"}[r.Network.HostLoopbackReachable])
row("egress", r.Network.Egress)

fmt.Println("\nfilesystem")
row("root", map[bool]string{true: "read-only", false: "writable (reset every run)"}[r.Filesystem.RootReadOnly])
d := r.Filesystem.Data
data := d.Host + " (" + d.Kind
if d.Owner != "" {
data += ", owner " + d.Owner + ", mode " + d.Mode
}
row("/data", data+"), kept between runs")
if len(r.Filesystem.Mounts) == 0 {
row("host mounts", "none")
}
for _, m := range r.Filesystem.Mounts {
row("mount", fmt.Sprintf("%s -> %s (%s)", m.Host, m.Guest, m.Mode))
}

fmt.Println("\nhost")
row("KVM", yesno(r.Host.KVM))
row("kernel", r.Host.Kernel)
if r.Host.GuestKernel != "" {
state := "current"
if !r.Host.IsolationCurrent {
state = "stale: the runtime changed since"
}
row("isolation", fmt.Sprintf("guest kernel %s, verified %s (%s)", r.Host.GuestKernel, r.Host.IsolationVerifiedAt, state))
}

if r.Live != nil {
fmt.Println("\nrunning VMM")
l := r.Live
row("pid", fmt.Sprint(l.PID))
row("runs as", fmt.Sprintf("host UID %d", l.HostUID))
lcaps := "none"
if len(l.Capabilities) > 0 {
lcaps = strings.Join(l.Capabilities, " ")
}
row("capabilities", lcaps)
row("no_new_privs", yesno(l.NoNewPrivileges))
row("seccomp", l.Seccomp)
if l.Matches {
row("matches", "yes, everything above")
} else {
row("matches", "NO")
}
}

fmt.Println("\nfindings")
for _, f := range r.Findings {
fmt.Printf(" %-4s %s\n", strings.ToUpper(f.Level), f.Text)
}
}

func row(k, v string) { fmt.Printf(" %-14s %s\n", k, v) }

func yesno(b bool) string {
if b {
return "yes"
}
return "no"
}
2 changes: 1 addition & 1 deletion internal/cli/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ func newRoot() *cobra.Command {
resetCmd(), snapshotCmd(), restoreCmd(),
forkCmd(), diffCmd(), applyCmd(), discardCmd(),
dataCmd(),
envCmd(), logsCmd(), verifyCmd(), doctorCmd(),
envCmd(), logsCmd(), verifyCmd(), inspectCmd(), doctorCmd(),
renameCmd(), destroyCmd(), nukeCmd(),
agentCmd(), prepareCmd(), tendCmd(), overlayCmd(), krunCmd(), exportCmd(), netnsCmd(), fcCmd(), tarOutCmd(), tarInCmd(),
)
Expand Down
6 changes: 6 additions & 0 deletions internal/runtime/backend_firecracker.go
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,12 @@ func fcLaunchCmd(r fcRun) (*exec.Cmd, error) {
if err != nil {
return nil, err
}
// Firecracker's API and vsock sockets live in the VM's directory. Past
// the Unix socket limit it cannot create them and only says it never
// opened its API socket, so say what is wrong instead.
if p := filepath.Join(dir, "api.sock"); len(p) > 103 {
return nil, fmt.Errorf("the VM's socket path %s is %d bytes, longer than a Unix socket allows (103); shorten BOX_HOME", p, len(p))
}
fcBackend{}.Remove(r.VM)
if err := os.MkdirAll(dir, 0o700); err != nil {
return nil, err
Expand Down
Loading
Loading