summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorZuul <zuul@review.opendev.org>2023-05-17 18:42:53 +0000
committerGerrit Code Review <review@openstack.org>2023-05-17 18:42:53 +0000
commitdeab00c8bd37b2d8e8c223cb10e7585e54282250 (patch)
tree9ac0a387037bfa2d8a06d7b8bb6ecd0a13269597
parent38423ed88373b909b864fc0bdf5d7268137b242e (diff)
parent0937872119e642b3fc689fc2bf156e44dccf140d (diff)
downloadzuul-deab00c8bd37b2d8e8c223cb10e7585e54282250.tar.gz
Merge "Use bwrap --disable-userns if possible"
-rw-r--r--Dockerfile5
-rw-r--r--releasenotes/notes/bwrap-disable-userns-bbb3f3a2932415c4.yaml8
-rw-r--r--zuul/driver/bubblewrap/__init__.py70
3 files changed, 82 insertions, 1 deletions
diff --git a/Dockerfile b/Dockerfile
index df326bd8a..588de4b8c 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 000000000..acf7b1f23
--- /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 935aefcae..4c85299a5 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',