Skip to content

Do not run privileged LXC containers. - #1688

Open
Darren Hoehna (dhoehna) wants to merge 15 commits into
mainfrom
user/dahoehna/lxc-unprivileged-check
Open

Darren Hoehna (dhoehna) wants to merge 15 commits into
mainfrom
user/dahoehna/lxc-unprivileged-check

Conversation

@dhoehna

@dhoehna Darren Hoehna (dhoehna) commented Nov 5, 2026 •

Copy link
Copy Markdown
Contributor

📖 Description

MXC will not run privileged LXC containers. After startup and before applying network rules, MXC reads the container's live UID mappings from the host. MXC stops the container if it cannot verify that container root maps to a nonroot host user. lxc-ls can incorrectly report an unprivileged container when lxc.namespace.keep = user retains the host user namespace.

build_lxc_capability_helper.rs builds a static helper embedded in MXC for network-filtered workloads. Entering the container can restore capabilities dropped on the host. The helper removes CAP_NET_ADMIN and CAP_NET_RAW after entry and before the workload starts. It restricts the requested command without changing the container's configuration or depending on container-provided libraries.

One consequence of this change is the minimum Linux version went up to 3.12. Version 3.12 is the minimum supported version to unprivileged containers.

Gudge (@MGudgin) for FYI on the minimum Linux kernel version change.

🔗 References

N/A

🔍 Validation

Ran the full LXC E2E suite

✅ Checklist

  • Signed the Contributor License Agreement
  • Linked to an issue
  • For features or changes requiring a spec: linked the issue with maintainer-applied feature-accepted
  • For implementation: linked the required approved and merged design/API spec, or explained why no spec is required
  • Feature issue remains open unless this PR completes all agreed implementation, validation, and documentation
  • Updated documentation (if applicable)
  • Updated Copilot instructions (if build, architecture, or conventions changed)
  • If this PR changes Cargo.lock, the dependency-feed-check check passes (see pull request builds)

📋 Issue Type

  • Bug fix
  • Feature
  • Task
Microsoft Reviewers: Open in CodeFlow

Darren Hoehna (dhoehna) and others added 11 commits November 4, 2026 13:22
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: ee956a7c-701c-4e30-ba23-88ed75d298fc
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: ee956a7c-701c-4e30-ba23-88ed75d298fc
LXC's reported classification replaces MXC-specific ID-map requirements.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: ee956a7c-701c-4e30-ba23-88ed75d298fc
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: ee956a7c-701c-4e30-ba23-88ed75d298fc
Remove the helper-specific tests instead of keeping a helper solely for testing.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: ee956a7c-701c-4e30-ba23-88ed75d298fc
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: ee956a7c-701c-4e30-ba23-88ed75d298fc
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: ee956a7c-701c-4e30-ba23-88ed75d298fc
Configured status can report an unprivileged container even when it retains the host user namespace.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: ee956a7c-701c-4e30-ba23-88ed75d298fc
Use complete command output instead of the bounded helper; inspection now waits for lxc-attach to finish.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: ee956a7c-701c-4e30-ba23-88ed75d298fc
A readable shifted-root range is sufficient; unreadable or nonmatching rows do not veto admission.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: ee956a7c-701c-4e30-ba23-88ed75d298fc
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: ee956a7c-701c-4e30-ba23-88ed75d298fc
@dhoehna
Darren Hoehna (dhoehna) requested a review from a team as a code owner November 5, 2026 22:20
Copilot AI balanced review requested due to automatic review settings November 5, 2026 22:20
@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
There may be pipelines that require an authorized user to comment /azp run to run.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Changes recommended

The LXC admission check trusts forgeable guest output, exposes host environment variables, and breaks existing SDK and CI flows without the required unprivileged provisioning and migration support.

5 open issues
What changed in this PR

This PR adds an unprivileged-container admission check to MXC’s LXC backend, intended to reject privileged containers before executing the requested workload.

Changes:

  • Inspect the live UID mapping after startup and stop containers that fail validation.
  • Add a regression scenario for misleading LXC privilege metadata and include it in the LXC suite.
  • Document unprivileged-container requirements and raise the minimum Linux kernel version to 3.12.
File Description
tests/​scripts/​run_lxc_unprivileged_test.sh Tests rejection, workload prevention, and cleanup for an identity-mapped container.
tests/​scripts/​run_lxc_all_tests.sh Adds the admission regression to the LXC suite.
src/​mxc-sdk/​src/​backends/​lxc/​common/​lxc_bindings.rs Adds UID-map validation and routes command execution through validated startup.
docs/​backends/​lxc/​lxc-backend.md Updates prerequisites and describes the admission check.

Review effort: Balanced

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: ee956a7c-701c-4e30-ba23-88ed75d298fc
Copilot AI balanced review requested due to automatic review settings November 6, 2026 19:09

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🔵 Needs a closer look

The admission check trusts guest-controlled output and can expose host credentials, while the V1 compatibility break lacks the required security-servicing documentation and migration guidance.

5 open issues
1 previously missed issue

In code that hasn't changed since last review

Medium severity Configure shifted UID/GID mappings in all CI hosts

.github/​workflows/​lxc-e2e.yml:51

[P1] Scheduled LXC validation (scripts/ci/prepare-linux-host.sh) and Azure SDK integration (.azure-pipelines/templates/SDK.Integration.Test.Job.yml) still install LXC without configuring shifted mappings or root subordinate UID/GID ranges. On clean hosts with default LXC configuration, the new admission check rejects their normal success-path scenarios. Provision the mappings and required mapping tools in those entry points too, preferably through shared host setup with appropriate cleanup.

Review effort: Balanced

The streaming and PTY cases need no host files; sharing /tmp can prevent mapped-container startup on hosts with nested mounts.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: ee956a7c-701c-4e30-ba23-88ed75d298fc
Namespace entry restored capabilities dropped on the host, allowing raw-socket traffic through the ICMP allowance.  Reading UID mappings on the host avoids trusting guest code during admission.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: ee956a7c-701c-4e30-ba23-88ed75d298fc
self.name
)
})?;
let root_is_shifted = mapping.lines().any(|line| {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This only requires one uid_map line that shifts root, so it admits g 0 0 65536 (host GID 0) and u 1 0 1 (container UID 1 = host root); on LXC 5.0.3 both let the workload read host root-only files.
Do we need to check gid_map, and refuse any range in either map that covers host ID 0 or any line that doesn't parse?

cmd.args(network.to_start_args(self.configured_interface_count()));
Self::run_tool(cmd)
Self::run_tool(cmd)?;
self.validate_unprivileged()

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The check runs after lxc-start, so a privileged container's init has already run as host root when it's refused; also, on a nested host, a container sharing MXC's user namespace passes because its map reads like a shifted one.

Shall we refuse a config without lxc.idmap or with lxc.namespace.keep = user before starting and refuse when /proc/<pid>/ns/user equals /proc/self/ns/user?

out
}

fn validate_unprivileged(&self) -> Result<(), String> {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Only the identity-map e2e case is tested now: no init PID, an unreadable or malformed map, and a failed lxc-stop?

Please move the map parsing into a pure function with table-driven unit tests, and bring back the fake-tool tests for those failure paths.

- Linux kernel >= 3.12.
- LXC >= 5.0 installed (`liblxc-dev` for building, `lxc-utils` for runtime).
- A GCC-compatible C11 compiler and target Linux userspace API headers for source builds.
- Root access to run MXC and to configure LXC.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This contradicts "Running without root" (line 57), and this PR also dropped its chmod o+x ~ step, which is still needed on 0750 Ubuntu homes (run_lxc_rootless_no_network_test.sh:31-32 still does it).
Please say root is needed to configure LXC and filter network traffic and restore the chmod o+x ~ step?

});
if !root_is_shifted {
return Err(format!(
"Container '{}' has no unprivileged root UID mapping. MXC will not run the requested command.",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Every root-run host with a stock /etc/lxc/default.conf hits this after upgrading, and nothing tells them to add root ranges to /etc/subuid//etc/subgid, add lxc.idmap, and recreate existing containers.
Please name those steps in the error or link the doc section that lists them?

++length;
}
linux_syscall(__NR_write, 2, (long)message, length, 0, 0);
linux_syscall(__NR_exit, 126, 0, 0, 0, 0);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Every helper failure exits 126, and MXC returns that as the workload's normal exit code with no error, so "MXC couldn't confine the workload" looks the same as a script that exited 126; before this commit a failed drop was an MXC error.

}

__attribute__((noreturn, used))
void drop_network_capabilities_and_exec(unsigned long* initial_stack)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Nothing runs this helper directly: its failure exits are untested, and the aarch64 syscall path never executes in CI either?
Please add a Linux test that runs the embedded helper in a user namespace (caps dropped on success, exit 126 on bad args or a failed exec); it would also run natively on the arm64 build job.

@microsoft-github-policy-service microsoft-github-policy-service Bot added the Needs-Author-Feedback Waiting for additional information or action from the issue or pull-request author. label Nov 9, 2026
Retain guest networking capabilities and enforce policy outside the container.  Cross-container UDP reply theft remains unresolved.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 8d35d189-89d3-400f-aaef-a33c8ce5860c
Copilot AI balanced review requested due to automatic review settings November 10, 2026 17:59

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Comment on lines +213 to +215
other => return Err(format!(
"LXC host filtering requires bridge-attached veth interfaces; lxc.net.{index}.type is {other:?}"
)),
chain ingress {{
ether type arp accept
ether type != {{ ip, ip6 }} drop
ct direction reply ct state established,related accept
let result = if active.destroy_on_exit {
LxcContainer::new(&name, None).destroy_owned()
} else {
Ok(())
@@ -36,6 +36,20 @@ jobs:
sudo DEBIAN_FRONTEND=noninteractive apt-get install -y \
lxc lxc-templates lxc-utils iptables debootstrap uidmap bridge-utils
Comment on lines +484 to +487
chain egress {{
ether type arp accept
ether type != {{ ip, ip6 }} drop
{egress} }}

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Needs-Author-Feedback Waiting for additional information or action from the issue or pull-request author.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants