Threat model
What Dock protects, what a Dock can and cannot reach, and where the known gaps are.
This page describes what the code does today. It is not a certification and not an audit. Where a protection is planned but not shipped, the page says so. To report a vulnerability, see SECURITY.md.
Assets and actors
| Asset | Where it lives | Who should reach it |
|---|---|---|
| Dock workloads | A Firecracker microVM or a gVisor container on a host | Untrusted: the code inside a Dock is assumed hostile |
| Dock disks and Snapshots | On the host; Snapshots in the object store (MinIO or any S3-compatible bucket) | The organization that owns the Dock |
| API keys | SHA-256 hashes in Postgres; the raw value is shown once | The user who created the key |
| Dock-bound keys | Same table, tied to one Dock | The Dock they were issued to |
| Provider keys, Environment secrets | Encrypted at rest (AES-256-GCM) with SECRETS_ENCRYPTION_KEY | Organization admins, and Docks the Environment passes them into. New Environments limit secrets to admins by default. If an Environment allows members, any member can create a Dock with it, so members can read its secrets from inside that Dock |
| Connector credentials (GitHub App private key and webhook secret, OAuth secrets) | Control plane environment (.env) | The control plane only |
| Hosts and docklets | Your machines; each docklet holds a per-host credential in its state directory | The operator |
| Control plane | API, Postgres, Redis, Restate, object store | The operator |
Actors:
- Dock workload. Untrusted code, including whatever an agent installs or runs.
- Organization member, admin, owner. Authenticated users. A member works with Docks. Admins also manage members, webhooks, secret reveal and who may use an Environment's secrets. Owners also manage other owners.
- Outside caller. Anyone with network access to the public origin but no credential.
- Operator. Runs the control plane and hosts. Trusted by design: the operator can read everything on the machines they run.
What a Dock can reach
Its own disk.
microvm: the whole root filesystem is the Dock's own virtual disk and persists across stop and resume.gvisor: only/home/user/workpersists. It is a sparse ext4 image ofdiskGiB, loop-mounted on the host, so one Dock cannot write more than its size.- Both disks are sparse files capped at
diskGiBper Dock. Host disk is not reserved for them, so the sum of all Docks' sizes can exceed the host's free space. See the known gaps.
Its own CPU and memory. The docklet sets cpu.max and memory.max per microVM (cgroup v2) and --cpus and --memory per gVisor container, from the Dock's size.
The network. Docks get a private address on a host-local subnet and are NATed out through the host. The host firewall applies these rules to every Dock:
- Traffic to the Dock subnet is dropped, so one Dock cannot reach another on the same host.
- Traffic to
169.254.0.0/16(cloud metadata and link-local),10.0.0.0/8,172.16.0.0/12,192.168.0.0/16,100.64.0.0/10and127.0.0.0/8is dropped. An operator can opt specific ranges back in with--egress-allow-private(DOCKLET_EGRESS_ALLOW_PRIVATE); that opens them for every Dock on the host. - Traffic from a Dock to the host itself is dropped, except replies to host-initiated connections, ICMP echo, and the egress proxy port on the gateway address.
- IPv6 is disabled on the host side of each Dock interface (best effort).
Everything else on the public internet is reachable. An ordinary Dock has open outbound internet access; Dock does not filter ordinary Docks by destination.
Confined Docks. GitHub-triggered runs are provisioned with an allowlist.
Their host firewall drops all forwarded traffic, and the only way out is a
CONNECT proxy on the host that admits HTTPS (port 443) to allowlisted names. The
default list is GitHub, npm, Yarn, PyPI, the Go proxy and crates.io, plus the
control plane. The proxy resolves each name once and dials only a public
address, so DNS rebinding cannot reach a private target. Setting
RUN_EGRESS_ALLOW=* turns this confinement off.
Anything you pass in. Whatever an Environment passes into a Dock is readable by anyone inside that Dock. By default an Environment passes agent credentials: the organization's provider keys and, if the Dock's creator signed in with ChatGPT, a Dock-scoped token that spends that sign-in through the control plane. Linked GitHub repositories get a read-only clone token. The Dock API key is off by default. See Environments.
Hosted ports. An exposed port is reachable by anyone who has its URL until you remove it. The hostname has a random label, but it gets its own public TLS certificate, so it shows up in Certificate Transparency logs. Treat the URL as public, not as a secret.
What a Dock cannot reach
- The host. A microVM runs under the Firecracker jailer, dropped to an unprivileged user, in a chroot and cgroup. Every microVM on a host runs as the same user (
DOCKLET_UID/DOCKLET_GID, default1000); on many distributions that is the first login account, so set a dedicated user. A gVisor container runs as a non-root user with all capabilities dropped andno-new-privileges, insiderunsc. Escape is the main risk; see the isolation section. - Other Docks. Separate disks, one TAP or veth per Dock with no shared bridge, and the subnet-drop rule above. MicroVMs do not share a kernel; each gVisor Dock is its own
runscsandbox. - Control plane credentials. The GitHub App private key, webhook secret, OAuth secrets and
SECRETS_ENCRYPTION_KEYare not sent to hosts or Docks. The only connector-derived token a Dock receives is a short-lived, read-only GitHub installation token for the repositories an Environment links, minted at apply time. - The control plane database. Postgres, Redis and Restate are not published by the self-host stack, and private ranges are dropped from Docks. A Dock reaches the control plane only through its public origin.
- The fleet API. It requires a host credential, which a Dock does not hold. The host credential sits in the docklet's state directory with owner-only permissions, outside the Dock.
Control plane
Authentication. One global guard accepts two credentials:
Authorization: Bearer dock_...API keys. Only a SHA-256 hash is stored. Keys can be revoked and can expire.- A Better Auth cookie session, used only when no Bearer header is sent.
A closed account can only reopen or erase itself; an erased account is refused everywhere. There are no JWT access tokens.
Dock-bound keys. When an Environment passes Dock credentials, the control
plane issues one key for that Dock, acting as the Dock's creator. Each apply
replaces it and withholding deletes it. A dock-bound key can call only
GET /v1/docks/:id, the Dock's three port routes, and
GET /v1/docks/:id/stats, and only for its own Dock. Every other route returns
403, and the key is confined to the Dock's organization. These keys do not
appear in the user's key list.
Roles and tenant scoping. Roles are member, admin and owner. Docks,
Environments, Snapshots and webhooks belong to an organization, and the service
layer checks membership before reads and writes. A Dock in another organization
returns 404, not 403. The last owner of an organization cannot be removed or
demoted.
Webhooks. Endpoint URLs must be https. At dial time the hostname is
resolved, every returned address is checked against private, loopback,
link-local, CGNAT, multicast, reserved and IPv6-embedded-IPv4 ranges, and the
socket connects only to the vetted address. Redirects are never followed.
Deliveries are signed with HMAC-SHA256 over the timestamp and body. Endpoint
secrets are encrypted at rest.
Fleet. Hosts connect outbound only and need no inbound port. A host enrolls
with a shared token once and receives its own credential; the control plane
stores a hash, compares in constant time, and ties every fleet call to that
host. Snapshot object access is scoped to the calling host: it can read
generations of Docks placed on it or named in a command sent to it, and cannot
overwrite recorded Snapshots. FLEET_LEGACY_SHARED_TOKEN=true re-enables the
shared token for every call; leave it off.
Rate limits. Redis counters limit API key and SSH key creation, Dock exec (per Dock, per caller and concurrent commands per Dock), terminal tickets, SSH gateway authentication (per client address), ChatGPT sign-in, GitHub run triggers (per repository), the run LLM gateway, webhook deliveries (per endpoint) and the public contact form. They fail closed: if Redis is down the request is refused (503 on HTTP routes). Request body sizes are bounded. Other routes have no limit of Dock's own.
Secrets at rest. Provider keys, Environment secrets, ChatGPT sign-in tokens and webhook secrets are
sealed with AES-256-GCM under SECRETS_ENCRYPTION_KEY. Whoever holds that key
and the database holds the secrets.
Isolation by runtime
| Runtime | Boundary | Assessment |
|---|---|---|
microvm | Firecracker on Linux KVM, under the jailer | Strongest. Each Dock has its own guest kernel behind hardware virtualization. An attacker needs a hypervisor or VMM escape. |
gvisor | runsc on Linux with Docker | Strong without a hypervisor. A user-space kernel handles the Dock's system calls, so the host kernel sees far fewer of them. The attack surface is larger than a VM's: the sandbox is a complex program, and a bug in it exposes the host. |
| libkrun on Windows and macOS | Planned | Not shipped. It is not equal to Firecracker until it has been reviewed. |
Prefer microvm for hostile or multi-tenant workloads. Use gvisor when KVM is
unavailable and you accept the larger attack surface.
No third-party audit yet
Dock has not been independently audited. Do not run Docks that hold secrets for people you do not trust on hosts that hold anything else of value.
Supply chain
- Docklet release binaries are signed with cosign (keyless, from the docklet release workflow). The host installer downloads the binary,
checksums.txtand their signature bundles, and stops unless both signatures come from the docklet release workflow on a version tag and the binary matches its checksum. If cosign is missing, the installer fetches a pinned, checksum-verified build. - The control plane images (
ghcr.io/usedock/dock-api,dock-api-migrate,dock-dashboard) are built and published to GHCR for each release and pinned byDOCK_VERSION. They are not signed with cosign today. Pin the version and check the digest yourself if that matters to you.
Out of scope and known gaps
- Noisy neighbours. CPU and memory are capped per Dock. The docklet sets no disk I/O limit, process-count limit or network bandwidth limit on either runtime, so one Dock can degrade disk and network for others on the same host.
- Host disk overcommit. Each Dock's disk is capped at
diskGiB, but the files are sparse and nothing reserves host space for them. The docklet compares a new gVisor Dock's size with free disk when it is created; microVM Docks get no disk check. Docks that together grow past the host's free disk fill that filesystem, and every Dock stored on it sees write errors. Watch host disk usage. - One jailer user for all microVMs. Every Firecracker process on a host runs as the same user, so a VMM escape lands as the user that runs every other microVM on that host.
- Open outbound internet for ordinary Docks. A Dock can send anything it can read to any public host, and can attack the internet from your IP. Confinement exists only for GitHub runs.
- Secrets inside a Dock are readable by the Dock. Use scoped, revocable keys.
- Snapshots. Dock does not encrypt Snapshot data in the object store itself. Use encryption at rest on your bucket or volume. A Snapshot of a Dock that held secrets contains them on disk.
- A malicious operator or a compromised control plane. The control plane commands every host, and the operator can read everything. Dock does not defend against its own operator.
- Host compromise. A sandbox escape or a host kernel bug is outside what Dock can prevent. The docklet runs as root on the host.
- Denial of service on the public API beyond the limits above. Put a rate-limiting proxy or firewall in front of a public instance.
- Side channels between Docks on shared hardware, such as CPU cache attacks, are not mitigated.
Reporting
Email security@usedock.io. Do not open a public issue. The policy, disclosure window and scope are in SECURITY.md.