From 0937872119e642b3fc689fc2bf156e44dccf140d Mon Sep 17 00:00:00 2001 From: Clark Boylan Date: Mon, 15 May 2023 10:44:17 -0700 Subject: [PATCH] Use bwrap --disable-userns if possible Newer bwrap has added the ability to disable additional nested user namespace creation from with the bwrap execution context. Take advantage of this feature in Zuul if we are able to in order to fortify Zuul's security position. In particular we need two conditions to take advantage of this. 1) bwrap must be new enough to support the feature (>=0.8.0) and 2) we must be running with user namespaces enabled. We explicitly check for both conditions and add the appropriate invocation flags to bwrap when the conditions are met. Change-Id: Idf933a0847cb8570b551892186ca9c0057be127f --- Dockerfile | 5 ++ ...bwrap-disable-userns-bbb3f3a2932415c4.yaml | 8 +++ zuul/driver/bubblewrap/__init__.py | 70 ++++++++++++++++++- 3 files changed, 82 insertions(+), 1 deletion(-) create mode 100644 releasenotes/notes/bwrap-disable-userns-bbb3f3a2932415c4.yaml diff --git a/Dockerfile b/Dockerfile index df326bd8ad..588de4b8cd 100644 --- a/Dockerfile +++ b/Dockerfile @@ -63,6 +63,11 @@ ENV DEBIAN_FRONTEND=noninteractive COPY --from=builder /output/ /output RUN /output/install-from-bindep zuul_base \ +# Install newer bwrap from backports for --disable-userns support (>=0.8.0) + && apt-get update \ + && apt-get install -y bubblewrap/bullseye-backports \ + && apt-get clean \ + && rm -rf /var/lib/apt/lists/* \ && rm -rf /output \ && useradd -u 10001 -m -d /var/lib/zuul -c "Zuul Daemon" zuul \ # This enables git protocol v2 which is more efficient at negotiating diff --git a/releasenotes/notes/bwrap-disable-userns-bbb3f3a2932415c4.yaml b/releasenotes/notes/bwrap-disable-userns-bbb3f3a2932415c4.yaml new file mode 100644 index 0000000000..acf7b1f23d --- /dev/null +++ b/releasenotes/notes/bwrap-disable-userns-bbb3f3a2932415c4.yaml @@ -0,0 +1,8 @@ +--- +security: + - | + Zuul will execute bwrap with --disable-userns set if two conditions + hold. 1) The version of bwrap is 0.8.0 or newer and 2) User namespaces + are enabled in the zuul-executor runtime context. Doing so will + prevent the zuul-executor bwrap runtimes from creating additional + user namespaces which fortifies Zuul's security position. diff --git a/zuul/driver/bubblewrap/__init__.py b/zuul/driver/bubblewrap/__init__.py index 935aefcaed..4c85299a5e 100644 --- a/zuul/driver/bubblewrap/__init__.py +++ b/zuul/driver/bubblewrap/__init__.py @@ -19,9 +19,11 @@ import fcntl import grp import logging import os +import os.path import psutil import pwd import shlex +import subprocess import threading import re import struct @@ -153,9 +155,71 @@ class BubblewrapDriver(Driver, WrapperInterface): name = 'bubblewrap' release_file_re = re.compile(r'^\W+-release$') + bwrap_version_re = re.compile(r'^(\d+\.\d+\.\d+).*') def __init__(self): + self.userns_enabled = self._is_userns_enabled() + self.bwrap_version = self._parse_bwrap_version() self.bwrap_command = self._bwrap_command() + # Validate basic bwrap functionality before we attempt to run + # workloads under bwrap. + context = self.getExecutionContext() + popen = context.getPopen(work_dir='/tmp', + ssh_auth_sock='/dev/null') + p = popen(['id'], + stdout=subprocess.PIPE, stderr=subprocess.STDOUT) + p.communicate() + if p.returncode != 0: + self.log.error("Non zero return code executing: %s", + " ".join(shlex.quote(c) + for c in popen.command + ['id'])) + raise Exception('bwrap execution validation failed. You can use ' + '`zuul-bwrap /tmp id` to investigate manually.') + + def _is_userns_enabled(self): + # This is based on the bwrap checks found here: + # https://github.com/containers/bubblewrap/blob/ + # ad76c2d6ba8091a7afa95568e46af2261b362439/bubblewrap.c#L2735 + return_val = False + if os.path.exists('/proc/self/ns/user'): + # Rhel 7 specific case + if os.path.exists('/sys/module/user_namespace/parameters/enable'): + with open('/sys/module/user_namespace/parameters/enable') as f: + s = f.read() + if not s or s[0] != 'Y': + return return_val + if os.path.exists('/proc/sys/user/max_user_namespaces'): + with open('/proc/sys/user/max_user_namespaces') as f: + s = f.read() + try: + i = int(s.strip()) + if i < 1: + return return_val + except ValueError: + # If we can't determine the max namespace count but + # namespaces are generally enabled then we should + # treat them as enabled. + return_val = True + return_val = True + return return_val + + def _parse_bwrap_version(self): + p = subprocess.run(['bwrap', '--version'], text=True, + stdout=subprocess.PIPE, stderr=subprocess.STDOUT) + # We don't know for sure what version schema bwrap may end up using. + # Match what they have done historically and be a bit forgiving of + # alpha, beta, rc, etc annotations. + r = self.bwrap_version_re.match(p.stdout.split()[-1]) + if p.returncode == 0 and r: + return tuple(map(int, r.group(1).split('.'))) + else: + if p.returncode == 0: + self.log.warning('Unable to determine bwrap version, from ' + '"%s". Using 0.0.0' % p.stdout.strip()) + else: + self.log.warning('Unable to determine bwrap version, got ' + 'returncode "%s". Using 0.0.0' % p.returncode) + return (0, 0, 0) def reconfigure(self, tenant): pass @@ -164,6 +228,10 @@ class BubblewrapDriver(Driver, WrapperInterface): pass def _bwrap_command(self): + if self.bwrap_version >= (0, 8, 0) and self.userns_enabled: + userns_flags = ['--unshare-user', '--disable-userns'] + else: + userns_flags = [] bwrap_command = [ 'setpriv', '--ambient-caps', @@ -195,7 +263,7 @@ class BubblewrapDriver(Driver, WrapperInterface): '--gid', '{gid}', '--file', '{uid_fd}', '/etc/passwd', '--file', '{gid_fd}', '/etc/group', - ] + ] + userns_flags for path in ['/lib64', '/etc/nsswitch.conf',