From 389de87aed7fa3836d4ed22efc875d8417a161a8 Mon Sep 17 00:00:00 2001 From: Matteo Sozzi Date: Wed, 8 May 2024 13:52:25 +0200 Subject: [PATCH] lxc: added option for unprivileged containers. Added extra option to enable unprivileged containers. This includes a patch to remove the hard-coded path to `lxc-user-nic` and a new security wrapper to set SUID to `lxc-user-nic`. --- nixos/modules/virtualisation/lxc.nix | 38 ++++++++ nixos/tests/all-tests.nix | 1 + nixos/tests/incus/incusd-options.nix | 8 +- nixos/tests/lxc/default.nix | 124 +++++++++++++++++++++++++++ pkgs/by-name/lx/lxc/package.nix | 24 +++++- pkgs/by-name/lx/lxc/user-nic.diff | 13 +++ 6 files changed, 205 insertions(+), 3 deletions(-) create mode 100644 nixos/tests/lxc/default.nix create mode 100644 pkgs/by-name/lx/lxc/user-nic.diff diff --git a/nixos/modules/virtualisation/lxc.nix b/nixos/modules/virtualisation/lxc.nix index 1ef322588a68..d1f4852cec64 100644 --- a/nixos/modules/virtualisation/lxc.nix +++ b/nixos/modules/virtualisation/lxc.nix @@ -23,6 +23,8 @@ in ''; }; + unprivilegedContainers = lib.mkEnableOption "support for unprivileged users to launch containers"; + systemConfig = lib.mkOption { type = lib.types.lines; @@ -53,6 +55,15 @@ in administration access in LXC. See {manpage}`lxc-usernet(5)`. ''; }; + + bridgeConfig = + lib.mkOption { + type = lib.types.lines; + default = ""; + description = '' + This is the config file for override lxc-net bridge default settings. + ''; + }; }; ###### implementation @@ -62,6 +73,8 @@ in environment.etc."lxc/lxc.conf".text = cfg.systemConfig; environment.etc."lxc/lxc-usernet".text = cfg.usernetConfig; environment.etc."lxc/default.conf".text = cfg.defaultConfig; + environment.etc."lxc/lxc-net".text = cfg.bridgeConfig; + environment.pathsToLink = [ "/share/lxc" ]; systemd.tmpfiles.rules = [ "d /var/lib/lxc/rootfs 0755 root root -" ]; security.apparmor.packages = [ cfg.package ]; @@ -73,5 +86,30 @@ in include ${cfg.package}/etc/apparmor.d/lxc-containers ''; }; + + # We don't need the `lxc-user` group, unless the unprivileged containers are enabled. + users.groups = lib.mkIf cfg.unprivilegedContainers { lxc-user = {}; }; + + # `lxc-user-nic` needs suid to attach to bridge for unpriv containers. + security.wrappers = lib.mkIf cfg.unprivilegedContainers { + lxcUserNet = { + source = "${pkgs.lxc}/libexec/lxc/lxc-user-nic"; + setuid = true; + owner = "root"; + group = "lxc-user"; + program = "lxc-user-nic"; + permissions = "u+rx,g+x,o-rx"; + }; + }; + + # Add lxc-net service if unpriv mode is enabled. + systemd.packages = lib.mkIf cfg.unprivilegedContainers [ pkgs.lxc ]; + systemd.services = lib.mkIf cfg.unprivilegedContainers { + lxc-net = { + enable = true; + wantedBy = [ "multi-user.target" ]; + path = [ pkgs.iproute2 pkgs.iptables pkgs.getent pkgs.dnsmasq ]; + }; + }; }; } diff --git a/nixos/tests/all-tests.nix b/nixos/tests/all-tests.nix index 0cd52cbec9a3..0fd7b9eb3b0c 100644 --- a/nixos/tests/all-tests.nix +++ b/nixos/tests/all-tests.nix @@ -538,6 +538,7 @@ in { loki = handleTest ./loki.nix {}; luks = handleTest ./luks.nix {}; lvm2 = handleTest ./lvm2 {}; + lxc = handleTest ./lxc {}; lxd = pkgs.recurseIntoAttrs (handleTest ./lxd { inherit handleTestOn; }); lxd-image-server = handleTest ./lxd-image-server.nix {}; #logstash = handleTest ./logstash.nix {}; diff --git a/nixos/tests/incus/incusd-options.nix b/nixos/tests/incus/incusd-options.nix index 3a355ca7c888..a223f1c8cb55 100644 --- a/nixos/tests/incus/incusd-options.nix +++ b/nixos/tests/incus/incusd-options.nix @@ -16,8 +16,12 @@ import ../make-test-python.nix ( }; }; - container-image-metadata = "${releases.incusContainerMeta.${pkgs.stdenv.hostPlatform.system}}/tarball/nixos-system-${pkgs.stdenv.hostPlatform.system}.tar.xz"; - container-image-rootfs = "${releases.incusContainerImage.${pkgs.stdenv.hostPlatform.system}}/nixos-lxc-image-${pkgs.stdenv.hostPlatform.system}.squashfs"; + container-image-metadata = "${ + releases.incusContainerMeta.${pkgs.stdenv.hostPlatform.system} + }/tarball/nixos-system-${pkgs.stdenv.hostPlatform.system}.tar.xz"; + container-image-rootfs = "${ + releases.incusContainerImage.${pkgs.stdenv.hostPlatform.system} + }/nixos-lxc-image-${pkgs.stdenv.hostPlatform.system}.squashfs"; in { name = "incusd-options"; diff --git a/nixos/tests/lxc/default.nix b/nixos/tests/lxc/default.nix new file mode 100644 index 000000000000..0f67010863ef --- /dev/null +++ b/nixos/tests/lxc/default.nix @@ -0,0 +1,124 @@ +import ../make-test-python.nix ( + { pkgs, lib, ... }: + + let + releases = import ../../release.nix { + configuration = { + # Building documentation makes the test unnecessarily take a longer time: + documentation.enable = lib.mkForce false; + }; + }; + + lxc-image-metadata = releases.lxdContainerMeta.${pkgs.stdenv.hostPlatform.system}; + lxc-image-rootfs = releases.lxdContainerImage.${pkgs.stdenv.hostPlatform.system}; + + in + { + name = "lxc-container-unprivileged"; + + meta = { + maintainers = lib.teams.lxc.members; + }; + + nodes.machine = + { lib, pkgs, ... }: + { + virtualisation = { + diskSize = 6144; + cores = 2; + memorySize = 512; + writableStore = true; + + lxc = { + enable = true; + unprivilegedContainers = true; + systemConfig = '' + lxc.lxcpath = /tmp/lxc + ''; + defaultConfig = '' + lxc.net.0.type = veth + lxc.net.0.link = lxcbr0 + lxc.net.0.flags = up + lxc.net.0.hwaddr = 00:16:3e:xx:xx:xx + lxc.idmap = u 0 100000 65536 + lxc.idmap = g 0 100000 65536 + ''; + # Permit user alice to connect to bridge + usernetConfig = '' + @lxc-user veth lxcbr0 10 + ''; + bridgeConfig = '' + LXC_IPV6_ADDR="" + LXC_IPV6_MASK="" + LXC_IPV6_NETWORK="" + LXC_IPV6_NAT="false" + ''; + }; + }; + + # Needed for lxc + environment.systemPackages = with pkgs; [ + pkgs.wget + pkgs.dnsmasq + ]; + + # Create user for test + users.users.alice = { + isNormalUser = true; + password = "test"; + description = "Lxc unprivileged user with access to lxcbr0"; + extraGroups = [ "lxc-user" ]; + subGidRanges = [ + { + startGid = 100000; + count = 65536; + } + ]; + subUidRanges = [ + { + startUid = 100000; + count = 65536; + } + ]; + }; + + users.users.bob = { + isNormalUser = true; + password = "test"; + description = "Lxc unprivileged user without access to lxcbr0"; + subGidRanges = [ + { + startGid = 100000; + count = 65536; + } + ]; + subUidRanges = [ + { + startUid = 100000; + count = 65536; + } + ]; + }; + }; + + testScript = '' + machine.wait_for_unit("lxc-net.service") + + # Copy config files for alice + machine.execute("su -- alice -c 'mkdir -p ~/.config/lxc'") + machine.execute("su -- alice -c 'cp /etc/lxc/default.conf ~/.config/lxc/'") + machine.execute("su -- alice -c 'cp /etc/lxc/lxc.conf ~/.config/lxc/'") + + machine.succeed("su -- alice -c 'lxc-create -t local -n test -- --metadata ${lxc-image-metadata}/*/*.tar.xz --fstree ${lxc-image-rootfs}/*/*.tar.xz'") + machine.succeed("su -- alice -c 'lxc-start test'") + machine.succeed("su -- alice -c 'lxc-stop test'") + + # Copy config files for bob + machine.execute("su -- bob -c 'mkdir -p ~/.config/lxc'") + machine.execute("su -- bob -c 'cp /etc/lxc/default.conf ~/.config/lxc/'") + machine.execute("su -- bob -c 'cp /etc/lxc/lxc.conf ~/.config/lxc/'") + + machine.fail("su -- bob -c 'lxc-start test'") + ''; + } +) diff --git a/pkgs/by-name/lx/lxc/package.nix b/pkgs/by-name/lx/lxc/package.nix index 7b24d70b5d0a..409e550109f2 100644 --- a/pkgs/by-name/lx/lxc/package.nix +++ b/pkgs/by-name/lx/lxc/package.nix @@ -48,16 +48,37 @@ stdenv.mkDerivation (finalAttrs: { patches = [ # fix docbook2man version detection ./docbook-hack.patch + + # Fix hardcoded path of lxc-user-nic + # This is needed to use unprivileged containers + ./user-nic.diff ]; mesonFlags = [ - "-Dinstall-init-files=false" + "-Dinstall-init-files=true" "-Dinstall-state-dirs=false" "-Dspecfile=false" "-Dtools-multicall=true" "-Dtools=false" + "-Dusernet-config-path=/etc/lxc/lxc-usernet" + "-Ddistrosysconfdir=${placeholder "out"}/etc/lxc" + "-Dsystemd-unitdir=${placeholder "out"}/lib/systemd/system" ]; + # /run/current-system/sw/share + postInstall = '' + substituteInPlace $out/etc/lxc/lxc --replace-fail "$out/etc/lxc" "/etc/lxc" + substituteInPlace $out/libexec/lxc/lxc-net --replace-fail "$out/etc/lxc" "/etc/lxc" + + substituteInPlace $out/share/lxc/templates/lxc-download --replace-fail "$out/share" "/run/current-system/sw/share" + substituteInPlace $out/share/lxc/templates/lxc-local --replace-fail "$out/share" "/run/current-system/sw/share" + substituteInPlace $out/share/lxc/templates/lxc-oci --replace-fail "$out/share" "/run/current-system/sw/share" + + substituteInPlace $out/share/lxc/config/common.conf --replace-fail "$out/share" "/run/current-system/sw/share" + substituteInPlace $out/share/lxc/config/userns.conf --replace-fail "$out/share" "/run/current-system/sw/share" + substituteInPlace $out/share/lxc/config/oci.common.conf --replace-fail "$out/share" "/run/current-system/sw/share" + ''; + enableParallelBuilding = true; doCheck = true; @@ -66,6 +87,7 @@ stdenv.mkDerivation (finalAttrs: { tests = { incus-legacy-init = nixosTests.incus.container-legacy-init; incus-systemd-init = nixosTests.incus.container-systemd-init; + lxc = nixosTests.lxc; lxd = nixosTests.lxd.container; }; diff --git a/pkgs/by-name/lx/lxc/user-nic.diff b/pkgs/by-name/lx/lxc/user-nic.diff new file mode 100644 index 000000000000..8edcb5d9b46d --- /dev/null +++ b/pkgs/by-name/lx/lxc/user-nic.diff @@ -0,0 +1,13 @@ +diff --git a/src/lxc/network.c b/src/lxc/network.c +index 0a99d32..850e975 100644 +--- a/src/lxc/network.c ++++ b/src/lxc/network.c +@@ -2940,7 +2940,7 @@ int lxc_find_gateway_addresses(struct lxc_handler *handler) + + #ifdef IN_LIBLXC + +-#define LXC_USERNIC_PATH LIBEXECDIR "/lxc/lxc-user-nic" ++#define LXC_USERNIC_PATH "/run/wrappers/bin/lxc-user-nic" + static int lxc_create_network_unpriv_exec(const char *lxcpath, + const char *lxcname, + struct lxc_netdev *netdev, pid_t pid,