Status: accepted
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:
- With
add=["ALL"], start from every capability supported by the guest and remove explicitly named drops. Named drops win in this branch. - Otherwise, with
drop=["ALL"], keep only explicitly named additions. - 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.
Every versioned boundary negotiates capability support before a policy can be silently dropped:
- A remote SDK re-reads
linux_capabilities_enabledfromGET /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.
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.
- 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
advancedkeeps 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.