Skip to content

Latest commit

 

History

History
142 lines (117 loc) · 13.3 KB

File metadata and controls

142 lines (117 loc) · 13.3 KB

Linux capability API design

Status: accepted

Decision

BoxLite exposes a high-level delta policy rather than the OCI runtime's five exact capability sets. The policy is grouped with the other expert-only container settings instead of widening the top-level box API:

Surface Add Drop
Rust, Python, REST advanced.capabilities.add advanced.capabilities.drop
Node.js / TypeScript advanced.capabilities.add advanced.capabilities.drop
Go AdvancedBoxOptions.SetCapabilities({Add: ...}) AdvancedBoxOptions.SetCapabilities({Drop: ...})
C boxlite_advanced_options_set_capabilities_add boxlite_advanced_options_set_capabilities_drop
CLI repeatable --cap-add repeatable --cap-drop

Create inputs use that nested path. The CLI flags remain familiar Docker-style shorthands and populate the nested object.

The values remain strings rather than a public enum. Linux can add capabilities independently of an SDK release, and the API host may run a different kernel and OCI library from the BoxLite guest. Boundaries validate the string shape; the guest that owns the OCI runtime validates support.

Names are case-insensitive, may omit CAP_, and are deduplicated as sets. Empty lists preserve BoxLite's 14-capability baseline. Resolution follows Moby and Apple container:

  1. With add=["ALL"], start from every capability supported by the guest and remove explicitly named drops. Named drops win in this branch.
  2. Otherwise, with drop=["ALL"], keep only explicitly named additions.
  3. Otherwise, start from the baseline, apply drops, then apply additions. A named addition therefore wins a named conflict.

Adding capabilities weakens the container boundary; SYS_ADMIN and ALL are especially broad. Prefer drop=["ALL"] plus only the minimum additions a workload needs. BoxLite's VM boundary remains separate, but it is not a reason to grant unnecessary privilege inside the guest.

The resolved set populates OCI bounding, effective, and permitted sets for init and every later exec. Inheritable and ambient remain absent. They have different privilege propagation semantics and require a separate, explicit security design if BoxLite ever exposes them.

Internally, the guest resolves the two input lists once into a CapabilitySet. That facade owns parsing, default policy, ALL precedence, canonical names for libcontainer, and OCI set construction. Downstream init/exec code carries only the resolved type and cannot reinterpret the policy.

Compatibility and rollout

Every versioned boundary negotiates capability support before a policy can be silently dropped:

  • A remote SDK re-reads linux_capabilities_enabled from GET /v1/config (uncached) immediately before creating a box with a custom policy, so a server rollback cannot be masked by a stale discovery cache.
  • A BoxLite host requires the guest to report version 0.9.8 or newer from Ping before sending the nested policy. Guest rootfs images are cached per version and reused, so an older guest can outlive its release; it would decode the new field as unknown proto and drop it.

A stale server or too-old guest therefore fails closed. Boundaries that do not carry a custom policy are unaffected: ordinary create, get, and list keep working against any server version. Inspection does not report the policy — it is create-time configuration, not box state.

The cloud control plane does not carry the policy yet. boxlite serve and the reference server are the server side of the contract above. The hosted API does not advertise linux_capabilities_enabled, so a BoxLite client refuses to send a policy to it — the gate is on the client, not the server. A client that skips that negotiation and posts advanced anyway has the field dropped, because the hosted API does not reject unknown properties.

Named get_or_create on the local runtime refuses to adopt an existing box whose capability policy differs from the requested one, so reuse cannot silently widen or narrow privileges.

An export carrying a capability policy is stamped archive v4; ordinary exports stay v3. A pre-capability importer accepts only up to v3, so it refuses the archive instead of dropping the policy and starting the box with wider privileges than the archive asked for.

Once a custom-policy box exists, do not roll a server back to a build that predates these fields: such a build cannot preserve them while recreating the box. Roll forward instead.

Project research

The projects below were reviewed at their current primary-source interfaces. Defaults differ by product, but direct container APIs consistently favor an add/drop delta over exposing all five OCI sets.

Project Interface and relevant behavior
Docker CLI Repeatable string-list flags, forwarded without client-side semantic validation (opts.go:150-156, opts.go:669-695).
Moby Engine Flat CapAdd / CapDrop string arrays (hostconfig.go:418-435); 14-capability default and the precedence adopted above (defaults.go:3-20, utils.go:72-117).
Docker Compose Flat cap_add / cap_drop sequences delegated to the engine (Compose service specification).
docker-py Optional cap_add / cap_drop lists, not a closed capability enum (containers.py:264-269).
Docker.DotNet IList<string> fields mirror Moby (HostConfig.Generated.cs:80-87).
Bollard Rust Option<Vec<String>> fields mirror Moby (HostConfig.cap_add).
Podman/libpod Flat string arrays (specgen.go:394-401); its native endpoint rejects overlap, while its Docker-compatible endpoint accepts Moby's shape (capabilities.go:125-196).
podman-py list[str] under the same keyword names (containers_create.py:615-620).
nerdctl String slices and repeatable/comma-compatible flags; warns rather than freezing unknown names in the client (container_run.go:218-222, run_security_linux.go:176-235).
containerd Lower-level ordered SpecOpts; add/drop mutate bounding, effective, and permitted, with inheritable/ambient handled separately (spec_opts.go:1066-1143).
Kubernetes API Nested Capabilities { Add, Drop }, but Capability is an open string newtype rather than an enum (types.go:3040-3052).
Kubernetes CRI Repeated add/drop strings plus a separate ambient-add field; ordinary add and ambient add are intentionally distinct (api.proto:1026-1041).
CRI-O Guest/runtime-side validation, product-specific default, add/drop resolution, and deliberate clearing of ambient/inheritable sets (capabilities_linux.go:11-42, container.go:640-785).
Nomad Docker driver Flat task fields plus an operator allowlist; its default intentionally differs from Docker by dropping NET_RAW (config.go:377-392, defaults.go:14-31).
Buildah Add/drop string arrays with documented drop-wins conflicts (run.go:155-160, buildah-run.1.md:26-48).
Apple container Persisted capAdd / capDrop arrays default to empty for backward compatibility and document the same Moby precedence BoxLite adopts (ContainerConfiguration.swift:20-60, how-to.md:470-510).
LXC lxc.cap.drop and mutually exclusive lxc.cap.keep provide subtractive and replacement policies (lxc.container.conf:1811-1850).
Incus Exposes LXC capability controls through restricted raw.lxc; privileged containers receive product-specific drops (config_options.txt:2372-2378, driver_lxc.go:787-798).
systemd-nspawn Separate add, drop, and ambient settings; ambient is explicitly not implied by ordinary additions (systemd.nspawn.xml:190-240).
OCI Runtime Spec Exact bounding, effective, inheritable, permitted, and ambient arrays, with no delta/default policy (config.md:286-299).
runc Applies the five exact OCI sets and resets ambient state explicitly (capabilities.go:47-149). Its inheritable-capability exec advisory is why BoxLite does not infer inheritable/ambient from cap_add (GHSA-f3fp-gc8g-vw66).
AWS ECS Nested KernelCapabilities with add/drop arrays and Docker-derived semantics (KernelCapabilities API).
Azure Container Instances Nested fluent add / drop string lists (SecurityContextCapabilitiesDefinition).
Terraform Docker provider Declarative nested block, but still only add/drop lists (resource_docker_container.go:262-290).

Kata Containers and gVisor were also checked. Both consume OCI/containerd or Docker/Kubernetes contracts rather than defining a competing high-level capability API, which supports keeping BoxLite's public delta policy separate from its OCI realization.

Alternatives rejected

  • Public closed enum: safer autocomplete today, but prevents a newer guest from accepting a newer kernel capability until every SDK is released again.
  • Public five-set OCI object: precise but too low-level for the common container use case and easy to misuse. Ambient and inheritable deserve separate review.
  • Flat top-level add/drop fields: common in Docker-compatible engine APIs, but BoxLite's top-level options also cover application lifecycle and resource settings. Grouping the expert-only privilege policy under advanced keeps creation extensible and matches Kubernetes, ECS, ACI, and Terraform's structured security models.
  • Host semantic validation: the host and guest may carry different OCI libraries or kernels. Freezing the supported list in Rust, TypeScript, and every SDK creates version skew; only lexical validation belongs upstream.