Skip to content

feat(cli,rest): expose privileged mode via --privileged and self-hosted REST - #1205

Open
G4614 wants to merge 38 commits into
boxlite-ai:mainfrom
G4614:feat/dind-cli-rest-privileged
Open

feat(cli,rest): expose privileged mode via --privileged and self-hosted REST#1205
G4614 wants to merge 38 commits into
boxlite-ai:mainfrom
G4614:feat/dind-cli-rest-privileged

Conversation

@G4614

@G4614 G4614 commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

Adds the CLI --privileged flag and self-hosted REST/OpenAPI exposure for the AdvancedBoxOptions.privileged mechanism #646 adds — split out of #646 the same way #1156 split out the control-plane/runner/SDK surfaces, so #646 stays to the guest/core mechanism itself.

Depends on #646: needs AdvancedBoxOptions.privileged/set_privileged to exist, so the diff below overlaps with #646's own content until #646 merges — this branch is built on top of feat/dind-privileged-plumbing, not a clean fork of main. Merge #646 first; this PR's diff will then automatically shrink to just this PR's own 13 files.

For the isolated diff right now (13 files, +239/-247): G4614/boxlite@feat/dind-privileged-plumbing...feat/dind-cli-rest-privileged

Test plan:

Summary by CodeRabbit

  • New Features

    • Added privileged container support through the CLI, API, SDK, and server configuration.
    • Added a --privileged option for creating and running containers.
    • Privileged containers receive all Linux capabilities with adjusted security restrictions.
    • Server capability information now reports privileged-mode availability.
    • Security settings, including read-only paths and system mounts, are applied consistently.
  • Bug Fixes

    • Added validation for incompatible privileged and capability options.
    • Unsupported privileged requests and untrusted privileged imports are clearly rejected.
    • Capability and security settings are consistently applied during container startup.
    • Capability validation documentation now clarifies syntax and kernel-support checks.

@G4614
G4614 requested a review from a team as a code owner August 12, 2026 05:48
@boxlite-agent

boxlite-agent Bot commented Aug 12, 2026

Copy link
Copy Markdown

📦 BoxLite review — couldn't complete

claude exited 1

stdout:
{"is_error":true,"duration_api_ms":0,"num_turns":1,"stop_reason":"stop_sequence","session_id":"38d3cf10-5ce8-4112-b7e4-0731ca26e0ce","total_cost_usd":0,"usage":{"output_tokens_details":{"thinking_tokens":0},"input_tokens":0,"cache_creation_input_tokens":0,"cache_read_input_tokens":0,"output_tokens":0,"server_tool_use":{"web_search_requests":0,"web_fetch_requests":0},"service_tier":"standard","cache_creation":{"ephemeral_1h_input_tokens":0,"ephemeral_5m_input_tokens":0},"inference_geo":"","iterations":[],"speed":"standard"},"modelUsage":{},"permission_denials":[],"terminal_reason":"api_error","fast_mode_state":"off","fast_mode_disabled_reason":"sdk_opt_in_required","subtype":"success","api_error_status":403,"result":"Your organization has disabled Claude subscription access for Claude Code · Use an Anthropic API key instead, or ask your admin to enable access","type":"result","duration_ms":311,"uuid":"31b1f722-f97a-4e66-b9cb-3e120c16606f"}

stderr:
<empty>

powered by BoxLite

@coderabbitai

coderabbitai Bot commented Aug 12, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: c57f28d3-d5b8-4e60-ba22-ea48dc405cff

📥 Commits

Reviewing files that changed from the base of the PR and between c1bb563 and 85ec943.

📒 Files selected for processing (6)
  • src/guest/src/container/capabilities.rs
  • src/guest/src/container/lifecycle.rs
  • src/guest/src/container/mod.rs
  • src/guest/src/container/security_policy.rs
  • src/guest/src/container/spec.rs
  • src/guest/src/container/start.rs
🚧 Files skipped from review as they are similar to previous changes (3)
  • src/guest/src/container/start.rs
  • src/guest/src/container/lifecycle.rs
  • src/guest/src/container/spec.rs

📝 Walkthrough

Walkthrough

Privileged container support spans API schemas, REST and CLI entrypoints, runtime validation, server capability checks, host-resolved security settings, protobuf transport, guest version validation, and OCI specification generation.

Changes

Privileged mode

Layer / File(s) Summary
API and client privileged options
openapi/..., src/boxlite/src/rest/..., src/cli/src/..., docs/reference/README.md
Privileged mode and server support metadata are exposed through OpenAPI, REST, CLI, and reference documentation. Requests normalize to cap_add=["ALL"] with no dropped capabilities.
Runtime policy normalization and boundaries
src/boxlite/src/runtime/..., src/boxlite/src/experimental.rs
Runtime validation rejects conflicting capability overrides, resolves security settings, enforces import and existing-box compatibility rules, and allows privileged mode without an experimental token.
Resolved security transport
src/shared/proto/boxlite/v1/service.proto, src/boxlite/src/portal/interfaces/container.rs, src/boxlite/src/litebox/init/tasks/guest_init.rs
Resolved capabilities, readonly paths, and /sys mount options are serialized and validated before guest container setup.
Guest lifecycle and OCI specification
src/guest/src/container/..., src/guest/src/service/container.rs
Guest lifecycle code stores the resolved security policy and applies its capabilities, path restrictions, and mount options to OCI configuration and command creation.

Estimated code review effort: 4 (Complex) | ~60 minutes

Mergeability Score: ⚪ Minimal · up to 85ec9

The change is merge-ready after normal checks and review; no actionable merge-blocking risk remains.

Sequence Diagram(s)

sequenceDiagram
  participant CLI
  participant Runtime
  participant RESTServer
  participant Portal
  participant Guest
  CLI->>Runtime: request privileged box
  Runtime->>Runtime: validate and normalize capabilities
  Runtime->>RESTServer: verify privileged_enabled
  Runtime->>Portal: send resolved security configuration
  Portal->>Guest: initialize container
  Guest->>Guest: build OCI specification
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Description check ⚠️ Warning The description explains the change and verification steps, but it omits the required call graph and explicit changes and risks sections. Add the required Before and After call graph, list notable changes under Changes, and include or delete the Risks / rollout section.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly identifies the main changes: the CLI --privileged flag and self-hosted REST exposure.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (4)
src/cli/src/commands/serve/mod.rs (1)

757-786: 🎯 Functional Correctness | 🔴 Critical | ⚡ Quick win

Normalize privileged options before validation.

BoxOptions::sanitize() only validates the value. It does not call normalize_privileged(). Therefore, a request with {"advanced":{"privileged":true}} leaves capabilities empty, and the added test fails at Lines 1360-1361.

Make options mutable. If privileged mode is enabled, call options.advanced.set_privileged(true) before options.sanitize()?. This expands an empty or canonical policy without hiding conflicting overrides.

Proposed fix
-    let options = BoxOptions {
+    let mut options = BoxOptions {
         // ...
     };
+    if options.advanced.privileged {
+        options.advanced.set_privileged(true);
+    }
     options.sanitize()?;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/cli/src/commands/serve/mod.rs` around lines 757 - 786, Make the
BoxOptions value mutable, and before calling sanitize(), check whether
options.advanced.privileged is enabled and invoke
options.advanced.set_privileged(true). Keep explicit capability overrides intact
while ensuring privileged requests with empty or canonical capabilities are
normalized before validation.
src/boxlite/src/rest/runtime.rs (1)

150-161: 🎯 Functional Correctness | 🟠 Major | 🏗️ Heavy lift

Reject incompatible existing boxes before reuse.

RestRuntime::get_or_create can return a non-privileged box for a privileged request because BoxResponse does not expose the existing policy. Expose an immutable policy indicator and reject incompatible reuse. Add a regression test for this case.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/boxlite/src/rest/runtime.rs` around lines 150 - 161, Update
RestRuntime::get_or_create to compare the requested privilege policy with the
existing box before returning it, rejecting incompatible reuse and creating a
new box instead or propagating the established validation error. Expose an
immutable policy indicator on BoxResponse for this comparison, and add a
regression test covering a privileged request encountering an existing
non-privileged box.
src/boxlite/src/runtime/rt_impl.rs (2)

547-577: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Reuse compatibility check does not fully guard against granting unconfined mounts beyond what was requested.

check_options_compatibility only rejects reuse when the request needs privileged and the existing box lacks it. It does not reject the reverse case where the request sets cap_add=["ALL"] with privileged=false and the existing box is privileged. In that case, capabilities.check_compatibility sees an exact match (both canonicalize to add=["ALL"], drop=[]), the privileged check passes because requested.advanced.privileged is false, and the caller silently receives a box with cleared masked_paths/readonly_paths and a writable /sys mount that it never asked for.

Compare requested.advanced.privileged against actual_advanced.privileged for exact equality instead of the current one-directional check, so a non-privileged request can never adopt a privileged box regardless of how its capability policy happens to canonicalize.

🛡️ Proposed fix for exact privileged-flag comparison
-        if requested.advanced.privileged && !actual_advanced.privileged {
+        if requested.advanced.privileged != actual_advanced.privileged {
             return Err(BoxliteError::Unsupported(format!(
                 "box '{box_name}' was created without privileged support and cannot satisfy a required privileged request; use a different name or recreate the box"
             )));
         }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/boxlite/src/runtime/rt_impl.rs` around lines 547 - 577, Update
check_options_compatibility to require exact equality between
requested.advanced.privileged and actual_advanced.privileged, replacing the
current one-directional privileged-only rejection. Preserve the existing nested
virtualization and capability compatibility checks, and return the same
unsupported error behavior for either privileged-flag mismatch.

1220-1230: 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Validate clone options before provision_box.

src/boxlite/src/litebox/clone_export.rs:128 passes self.config.options.clone() without validation. provision_box then leaves a conflicting capability policy unchanged and persists the invalid BoxConfig. Validate the copied options before normalization, or at the provision_box boundary.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/boxlite/src/runtime/rt_impl.rs` around lines 1220 - 1230, Validate the
copied BoxOptions at the provision_box boundary before calling
options.advanced.normalize_privileged(), ensuring conflicting capability
policies are rejected or corrected before the resulting BoxConfig is persisted.
This must also cover callers such as the clone flow that pass
self.config.options.clone().
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@openapi/reference-server/server.py`:
- Around line 166-170: Update the privileged capability validation around
self.capabilities.add to canonicalize its single capability entry before
comparing the canonical shape, accepting case-insensitive “all” and the
“CAP_ALL” form. Preserve the existing rejection for non-canonical combinations,
and add reference-server tests covering both all and CAP_ALL inputs.

In `@src/boxlite/src/runtime/advanced_options.rs`:
- Around line 753-768: Update set_privileged so capability clearing occurs only
when transitioning from privileged to non-privileged: capture the previous
self.privileged state before assigning enabled, then require that prior state
and is_privileged_capability_shape() before resetting capabilities. Preserve
normalization when enabling and leave independently configured capabilities
unchanged when already non-privileged.

In `@src/guest/src/container/spec.rs`:
- Line 175: Update build_standard_mounts and the privileged mount-policy logic
to keep the guest cgroup2 hierarchy read-only: add a cgroup namespace or
equivalent isolated hierarchy before permitting writes, or exclude
/sys/fs/cgroup from the recursive /sys bind and preserve recursive read-only
behavior. Revise the related tests around cgroup namespace setup and the policy
assertions so they reject the unsafe no-rro configuration.

---

Outside diff comments:
In `@src/boxlite/src/rest/runtime.rs`:
- Around line 150-161: Update RestRuntime::get_or_create to compare the
requested privilege policy with the existing box before returning it, rejecting
incompatible reuse and creating a new box instead or propagating the established
validation error. Expose an immutable policy indicator on BoxResponse for this
comparison, and add a regression test covering a privileged request encountering
an existing non-privileged box.

In `@src/boxlite/src/runtime/rt_impl.rs`:
- Around line 547-577: Update check_options_compatibility to require exact
equality between requested.advanced.privileged and actual_advanced.privileged,
replacing the current one-directional privileged-only rejection. Preserve the
existing nested virtualization and capability compatibility checks, and return
the same unsupported error behavior for either privileged-flag mismatch.
- Around line 1220-1230: Validate the copied BoxOptions at the provision_box
boundary before calling options.advanced.normalize_privileged(), ensuring
conflicting capability policies are rejected or corrected before the resulting
BoxConfig is persisted. This must also cover callers such as the clone flow that
pass self.config.options.clone().

In `@src/cli/src/commands/serve/mod.rs`:
- Around line 757-786: Make the BoxOptions value mutable, and before calling
sanitize(), check whether options.advanced.privileged is enabled and invoke
options.advanced.set_privileged(true). Keep explicit capability overrides intact
while ensuring privileged requests with empty or canonical capabilities are
normalized before validation.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: e51ba822-e4c5-4331-b434-14d7f1a2a4bd

📥 Commits

Reviewing files that changed from the base of the PR and between 038938c and 1bb7d2d.

📒 Files selected for processing (27)
  • docs/reference/README.md
  • openapi/box.openapi.yaml
  • openapi/reference-server/server.py
  • openapi/reference-server/tests/test_handle_cache.py
  • src/boxlite/src/experimental.rs
  • src/boxlite/src/litebox/init/tasks/guest_init.rs
  • src/boxlite/src/portal/interfaces/container.rs
  • src/boxlite/src/rest/client.rs
  • src/boxlite/src/rest/runtime.rs
  • src/boxlite/src/rest/types.rs
  • src/boxlite/src/runtime/advanced_options.rs
  • src/boxlite/src/runtime/import.rs
  • src/boxlite/src/runtime/options.rs
  • src/boxlite/src/runtime/rt_impl.rs
  • src/cli/src/cli.rs
  • src/cli/src/commands/create.rs
  • src/cli/src/commands/run.rs
  • src/cli/src/commands/serve/handlers/config.rs
  • src/cli/src/commands/serve/mod.rs
  • src/cli/src/commands/serve/types.rs
  • src/guest/src/container/capabilities.rs
  • src/guest/src/container/lifecycle.rs
  • src/guest/src/container/mod.rs
  • src/guest/src/container/spec.rs
  • src/guest/src/container/start.rs
  • src/guest/src/service/container.rs
  • src/shared/proto/boxlite/v1/service.proto

Comment on lines +166 to +170
canonical = self.capabilities.drop == [] and self.capabilities.add == ["ALL"]
if (self.capabilities.add or self.capabilities.drop) and not canonical:
raise ValueError("privileged mode cannot be combined with cap_add or cap_drop")

self.capabilities.add = ["ALL"]

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Canonicalize the privileged capability shape.

Line 166 requires the exact spelling "ALL". A request with add=["all"] is valid under the documented case-insensitive capability rules, but this validator rejects it when privileged=true. The Rust runtime canonicalizes capability names before it checks this shape.

Canonicalize the single entry before comparison. Cover all and CAP_ALL in this reference-server test suite.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@openapi/reference-server/server.py` around lines 166 - 170, Update the
privileged capability validation around self.capabilities.add to canonicalize
its single capability entry before comparing the canonical shape, accepting
case-insensitive “all” and the “CAP_ALL” form. Preserve the existing rejection
for non-canonical combinations, and add reference-server tests covering both all
and CAP_ALL inputs.

Comment on lines +753 to +768
/// Toggle privileged mode, keeping the capability policy consistent in
/// both directions.
///
/// Enabling expands to the canonical shape; disabling withdraws it again,
/// so a handle that is toggled off does not leave a non-privileged box
/// holding `ALL`. An explicit policy the caller set themselves is left
/// alone — only the shape this method produced is taken back.
pub fn set_privileged(&mut self, enabled: bool) {
self.privileged = enabled;

if enabled {
self.normalize_privileged();
} else if self.capabilities.is_privileged_capability_shape() {
self.capabilities = ContainerCapabilities::default();
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Fix set_privileged(false) to only clear capabilities when transitioning away from privileged mode.

set_privileged(false) clears capabilities whenever self.capabilities.is_privileged_capability_shape() is true, regardless of whether self.privileged was already false. If a caller calls set_privileged(false) while privileged is already false and capabilities were independently set to add=["ALL"], this silently discards that explicit request. Track whether privileged mode was actually enabled before clearing.

🐛 Proposed fix to guard the clear on an actual privileged→non-privileged transition
 pub fn set_privileged(&mut self, enabled: bool) {
+    let was_privileged = self.privileged;
     self.privileged = enabled;

     if enabled {
         self.normalize_privileged();
-    } else if self.capabilities.is_privileged_capability_shape() {
+    } else if was_privileged && self.capabilities.is_privileged_capability_shape() {
         self.capabilities = ContainerCapabilities::default();
     }
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
/// Toggle privileged mode, keeping the capability policy consistent in
/// both directions.
///
/// Enabling expands to the canonical shape; disabling withdraws it again,
/// so a handle that is toggled off does not leave a non-privileged box
/// holding `ALL`. An explicit policy the caller set themselves is left
/// alone — only the shape this method produced is taken back.
pub fn set_privileged(&mut self, enabled: bool) {
self.privileged = enabled;
if enabled {
self.normalize_privileged();
} else if self.capabilities.is_privileged_capability_shape() {
self.capabilities = ContainerCapabilities::default();
}
}
/// Toggle privileged mode, keeping the capability policy consistent in
/// both directions.
///
/// Enabling expands to the canonical shape; disabling withdraws it again,
/// so a handle that is toggled off does not leave a non-privileged box
/// holding `ALL`. An explicit policy the caller set themselves is left
/// alone — only the shape this method produced is taken back.
pub fn set_privileged(&mut self, enabled: bool) {
let was_privileged = self.privileged;
self.privileged = enabled;
if enabled {
self.normalize_privileged();
} else if was_privileged && self.capabilities.is_privileged_capability_shape() {
self.capabilities = ContainerCapabilities::default();
}
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/boxlite/src/runtime/advanced_options.rs` around lines 753 - 768, Update
set_privileged so capability clearing occurs only when transitioning from
privileged to non-privileged: capture the previous self.privileged state before
assigning enabled, then require that prior state and
is_privileged_capability_shape() before resetting capabilities. Preserve
normalization when enabling and leave independently configured capabilities
unchanged when already non-privileged.

Comment thread src/guest/src/container/spec.rs Outdated
);
let namespaces = build_default_namespaces()?;
let mut mounts = build_standard_mounts(bundle_path)?;
let mut mounts = build_standard_mounts(bundle_path, security_policy.sys_mount_options.clone())?;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🔴 Critical | 🏗️ Heavy lift

Keep the guest cgroup2 hierarchy read-only.

Line 390 omits a cgroup namespace. Lines 616-628 allow privileged mode to remove rro from the recursive /sys bind. This exposes the guest-wide /sys/fs/cgroup hierarchy as writable to the container. The container can then modify cgroups used by sibling containers and the guest agent.

Keep the cgroup2 submount recursively read-only, exclude it from the /sys bind, or create an equivalent isolated hierarchy before allowing write access. Update the test because it currently asserts the unsafe no-rro policy.

Based on learnings: “do not add a writable /sys/fs/cgroup cgroup2 mount unless a cgroup namespace or equivalent isolated hierarchy is created first.”

Also applies to: 390-392, 616-628, 1329-1355

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/guest/src/container/spec.rs` at line 175, Update build_standard_mounts
and the privileged mount-policy logic to keep the guest cgroup2 hierarchy
read-only: add a cgroup namespace or equivalent isolated hierarchy before
permitting writes, or exclude /sys/fs/cgroup from the recursive /sys bind and
preserve recursive read-only behavior. Revise the related tests around cgroup
namespace setup and the policy assertions so they reject the unsafe no-rro
configuration.

Source: Learnings

@G4614
G4614 force-pushed the feat/dind-cli-rest-privileged branch 8 times, most recently from bfc8774 to 6351463 Compare August 13, 2026 07:42
G4614 and others added 18 commits August 13, 2026 15:59
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
apps/api, apps/runner, apps/libs (generated clients), and sdks/** now
ship in boxlite-ai#1156 instead. This PR is left with the
guest + core runtime + CLI + REST contract — the actual privileged/
DinD feature — so review stays scoped to the security-relevant parts
of the change instead of being split across 84 files of mixed risk.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
G4614 and others added 20 commits August 13, 2026 15:59
ResolvedSecurityPolicy::from_resolved no longer validates a canonical
privileged shape, so a test asserting that mismatched atomic options
and capabilities still resolve is testing a trivial pass-through, not
guest behavior. resolved_policy_consumes_atomic_security_options and
all_capabilities_without_privileged_keep_proc_sys_readonly already
cover the same constructor with both all-true and all-false inputs.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
cgroup_namespace and allow_all_devices are unnecessary for DinD: the
guest never enforced a restrictive device-cgroup default, and dockerd
tolerates running without a private cgroup namespace view. Only
unconfined_paths (masked/readonly path relief, for network sysctls)
and writable_sysfs (/sys stays writable, for dockerd's own cgroup
writes) are load-bearing.

ResolvedContainerSecurityConfig / ContainerAdvancedConfig /
ResolvedSecurityPolicy drop from 4 fields to 2. service.proto
renumbers ContainerAdvancedOptions to match (internal wire message,
no external consumer pinned to the old field numbers).

Before
  resolve_container_security     (Core · advanced_options.rs:789)
    -> ResolvedContainerSecurityConfig{cgroup_namespace, writable_sysfs, allow_all_devices, unconfined_paths}
         -> create_oci_spec       (Guest · spec.rs:153)
              |- build_default_namespaces(cgroup_namespace)   — adds a cgroup namespace
              |- build_linux_spec(..., allow_all_devices, ...) — builds an allow-all device-cgroup rule
              `- build_standard_mounts(..., writable_sysfs)

After
  resolve_container_security     (Core · advanced_options.rs:789)
    -> ResolvedContainerSecurityConfig{unconfined_paths, writable_sysfs}
         -> create_oci_spec       (Guest · spec.rs:153)
              |- build_default_namespaces()                    — fixed list, no cgroup namespace
              |- build_linux_spec(..., unconfined_paths)        — no device-cgroup rule
              `- build_standard_mounts(..., writable_sysfs)

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
architecture/README.md, container-capabilities.md, cli/README.md, and
rust/README.md described privileged mode with a dead link and with the
cgroup namespace / allow-all device rule this PR's own last commit
removed. Reverting to main here rather than patching them in place:
the accurate design record already lives outside the repo, and this
PR's diff should stay code, not docs that immediately went stale.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
create_inner's own normalize_privileged() call always ran on data
sanitize_local_options had already normalized moments earlier in the
same call: RuntimeImpl is never re-exported outside the crate, and
core.rs's only construction site wraps it in LocalRuntime immediately,
so every reachable caller goes through sanitize_local_options first.
Replaced the redundant call with a debug_assert documenting that
invariant.

get_or_create_rejects_privileged_upgrade called RuntimeImpl directly
with a hand-built AdvancedBoxOptions{privileged: true, ..} literal,
bypassing that pipeline the way no real caller does; updated it to use
set_privileged, which is what actually produces the normalized shape.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…ies verbatim

Host resolve_container_security() now sends literal masked_paths,
readonly_paths, and /sys mount options instead of two booleans; the guest
assigns them as-is with no reinterpretation. Matches how Docker, Podman, and
Kata Containers hand the enforcing side a finished OCI shape rather than a
flag to re-derive. capabilities stays as add/drop deltas — the guest is the
only side that knows its own kernel's capability ceiling.

Before: host sends {capabilities, unconfined_paths: bool, writable_sysfs: bool}
  → guest capabilities.rs::from_resolved(bool, bool)
  → spec.rs::build_linux_spec(.., unconfined_paths: bool) branches on it
  → spec.rs::build_standard_mounts(.., writable_sysfs: bool) branches on it

After: host resolve_container_security() resolves literal values
  → ContainerAdvancedOptions{masked_paths, readonly_paths, sys_mount_options}
  → guest capabilities.rs::from_resolved(Vec<String>, Vec<String>, Vec<String>)
  → spec.rs::build_linux_spec(.., masked_paths, readonly_paths) assigns verbatim
  → spec.rs::build_standard_mounts(.., sys_mount_options) assigns verbatim

Moved the privileged-vs-hardened test coverage to where the decision is now
made (advanced_options.rs); guest-side tests now assert pass-through fidelity.
…ollow-up PR

Keeps boxlite-ai#646 to the mechanism itself — proto, host resolve, guest apply — same
split rationale boxlite-ai#1156 already used to separate control-plane/runner/SDK
consumers from boxlite-ai#646. The Rust API (AdvancedBoxOptions.privileged,
set_privileged) stays; only the CLI --privileged flag and the self-hosted
REST DTO/OpenAPI exposure move out, since neither adds mechanism, just a
caller.

Before: CLI/REST call AdvancedBoxOptions.privileged directly, bundled with
  the guest/core mechanism in one PR.
After: CLI/REST removed here, follow-up PR re-adds them on top of this
  mechanism-only branch (rust API entry point unchanged, so the follow-up is
  pure plumbing with nothing left to test beyond wiring).
advanced_options.rs hand-copied oci-spec's default masked/readonly path
lists rather than calling the crate's own public functions for them, even
though the host crate already depends on oci-spec (runtime feature, on by
default). Two independent copies of the same 10+5-entry list, one of which
could silently drift on an oci-spec bump.

Before: default_masked_paths() -> literal 10-entry Vec<String>
  default_readonly_paths() -> literal 5-entry Vec<String>
After: default_masked_paths() -> oci_spec::runtime::get_default_maskedpaths()
  default_readonly_paths() -> oci_spec::runtime::get_default_readonly_paths()
Nothing in the DinD workflow reads a masked path: an isolated test (guest
holding every capability, masked paths left untouched) showed runc, an
overlayfs mount, a bridge+veth pair, iptables -t nat, and a network
namespace all work, and the OCI masked-path list only ever protects
host-wide /proc and /sys interfaces dockerd never touches (/proc/kcore,
/proc/keys, /sys/firmware, ...). Clearing it rode in as a side effect of
matching `docker run --privileged` wholesale, alongside the cgroup
namespace and allow-all device rule this PR already dropped for the same
"tested and found unnecessary" reason — masked_paths just never got the
same follow-up scrutiny.

Before: host resolves masked_paths (empty when privileged, oci-spec's
  default otherwise) and sends it over the wire; guest assigns it verbatim.
After: masked_paths drops out of ContainerAdvancedOptions entirely; the
  guest never calls .masked_paths(..) and keeps oci-spec's own default,
  unconditionally, exactly as it did before `privileged` existed.
An old host that predates readonly_paths/sys_mount_options doesn't know to
send them, so they arrive as empty — indistinguishable on the wire from a
new host's deliberate privileged request. sys_mount_options has no
legitimate empty value (every real host resolves at least 4 base flags,
privileged or not), so an empty list can only mean the sender doesn't know
about these fields at all. Silently proceeding doesn't even degrade
gracefully: a real boot test confirmed youki requires bind/rbind in a
mount's options to treat it as a bind mount, so an empty sys_mount_options
makes the /sys bind fail with an unrelated "failed to prepare rootfs"
error — this makes the real cause diagnosable instead.

Before: ResolvedSecurityPolicy::from_resolved(caps, [], []) ->
  build_standard_mounts(.., []) -> youki: not a bind mount, tries fstype
  "none" -> "failed to prepare rootfs" (opaque)
After: ResolvedSecurityPolicy::from_resolved rejects sys_mount_options.is_empty()
  up front -> "advanced.sys_mount_options is empty; the host predates
  resolved security fields ... recreate this box with a matching boxlite
  version" (actionable)
capabilities.rs's own module doc scoped it to "Linux capabilities for
container processes", but ResolvedSecurityPolicy bundles three fields —
capabilities is only one of them, readonly_paths/sys_mount_options are
mount paths and options, unrelated to capability resolution. Two of its
three consumers (start.rs, lifecycle.rs) only ever wanted the bundle, not
CapabilitySet itself, and vice versa for command.rs/zygote.rs — the file
was doing double duty for two different call patterns.

Before: capabilities.rs { CAPABILITIES_BY_NUMBER, CapabilitySet,
  ResolvedSecurityPolicy (capabilities + readonly_paths + sys_mount_options) }
After: capabilities.rs { CAPABILITIES_BY_NUMBER, CapabilitySet } — back to
  matching its own doc comment
  security_policy.rs (new) { ResolvedSecurityPolicy, using
  capabilities::CapabilitySet as one of its three fields }

Pure move: no logic changes. CapabilitySet's test-only len()/contains()
went from private to pub(crate) so security_policy.rs's tests (moved
along with the struct) can still reach them from a sibling module.
Keeps the word this module exists to signal: readonly_paths/sys_mount_options
here are already-resolved literal values, not rules waiting to be applied —
dropping "resolved" left "security_policy" reading like the latter, and
close enough to the host's unrelated SecurityOptions (shim sandboxing) to
invite confusion across the two crates. Pure rename: git mv + 4 import sites,
no logic changes.
Container held capabilities/readonly_paths/sys_mount_options behind one
bundled ResolvedSecurityPolicy struct. Unbundled: Container now stores
only capabilities: CapabilitySet (the one field cmd() re-reads for every
exec); readonly_paths/sys_mount_options flow as plain fn params from the
RPC boundary through create_oci_bundle/create_oci_spec and are never
stored on Container — clippy's dead_code check confirmed they were
write-only there once unbundled, which the old struct's own field
accesses elsewhere had been masking.

resolved_security.rs keeps only the version-compat guard
(validate_sys_mount_options); capability resolution moves to the call
site via the newly re-exported CapabilitySet.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The file now contains exactly one thing: a version-compat guard on the
sys_mount_options field. Every sibling module in this dir (capabilities.rs,
spec.rs, kill.rs, ...) is named after its subject, not a verb or a
cross-cutting concern -- resolved_security borrowed a 'security' framing
left over from when this file also bundled capabilities and readonly_paths,
which is gone now. Pure rename: git mv + mod.rs's two references + a test
name in spec.rs that repeated the old module name, no logic changes.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
…ed REST

Adds the CLI --privileged flag and the self-hosted REST/OpenAPI surface for
the AdvancedBoxOptions.privileged mechanism feat/dind-privileged-plumbing
(boxlite-ai#646) adds. Pure consumer: no new mechanism, just wiring a caller onto the
Rust API's set_privileged/AdvancedBoxOptions.privileged that already exists
on this branch.

Before: --privileged / the REST body field don't exist; only the Rust API
  (AdvancedBoxOptions.privileged, set_privileged) can request privileged mode.
After: cli.rs/commands::create::run parse --privileged and call
  set_privileged(true) -> AdvancedBoxOptions.privileged
  rest/types.rs deserializes the same field from the REST request body
  openapi/box.openapi.yaml documents both the request field and the
  capabilities.privileged_enabled response flag
@G4614
G4614 force-pushed the feat/dind-cli-rest-privileged branch from 6351463 to 3532c88 Compare August 13, 2026 08:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant