buildroot: ability to drop capabilities
Add a new member variable `caps` that if not `None` indicates the capabilities to retain, i.e. all other capabilities not specified will be dropped via `bubblewrap` (`--cap-drop`). Add corresponding tests.
This commit is contained in:
parent
1874c71920
commit
4ac62abbc3
2 changed files with 97 additions and 0 deletions
|
|
@ -16,6 +16,10 @@ import subprocess
|
|||
import tempfile
|
||||
import time
|
||||
|
||||
from typing import Optional
|
||||
|
||||
from osbuild.util import linux
|
||||
|
||||
|
||||
__all__ = [
|
||||
"BuildRoot",
|
||||
|
|
@ -79,6 +83,10 @@ class BuildRoot(contextlib.AbstractContextManager):
|
|||
The `run()` method allows running applications in this environment. Some
|
||||
state is persistent across runs, including data in `/var`. It is deleted
|
||||
only when exiting the context manager.
|
||||
|
||||
If `BuildRoot.caps` is not `None`, only the capabilities listed in this
|
||||
set will be retained (all others will be dropped), otherwise all caps
|
||||
are retained.
|
||||
"""
|
||||
|
||||
def __init__(self, root, runner, libdir, var, *, rundir="/run/osbuild"):
|
||||
|
|
@ -94,6 +102,7 @@ class BuildRoot(contextlib.AbstractContextManager):
|
|||
self.proc = None
|
||||
self.tmp = None
|
||||
self.mount_boot = True
|
||||
self.caps: Optional[set] = None
|
||||
|
||||
@staticmethod
|
||||
def _mknod(path, name, mode, major, minor):
|
||||
|
|
@ -273,6 +282,8 @@ class BuildRoot(contextlib.AbstractContextManager):
|
|||
"--unshare-net"
|
||||
]
|
||||
|
||||
cmd += self.build_capabilities_args()
|
||||
|
||||
cmd += mounts
|
||||
cmd += ["--", f"/run/osbuild/lib/runners/{self._runner}"]
|
||||
cmd += argv
|
||||
|
|
@ -321,6 +332,32 @@ class BuildRoot(contextlib.AbstractContextManager):
|
|||
|
||||
return CompletedBuild(proc, output)
|
||||
|
||||
def build_capabilities_args(self):
|
||||
"""Build the capabilities arguments for bubblewrap"""
|
||||
args = []
|
||||
|
||||
# If no capabilities are explicitly requested we retain all of them
|
||||
if self.caps is None:
|
||||
return args
|
||||
|
||||
# Under the assumption that we are running as root, the capabilities
|
||||
# for the child process (bubblewrap) are calculated as follows:
|
||||
# P'(effective) = P'(permitted)
|
||||
# P'(permitted) = P(inheritable) | P(bounding)
|
||||
# Thus bubblewrap will effectively run with all capabilities that
|
||||
# are present in the bounding set. If run as root, bubblewrap will
|
||||
# preserve all capabilities in the effective set when running the
|
||||
# container, which corresponds to our bounding set.
|
||||
# Therefore: drop all capabilities present in the bounding set minus
|
||||
# the ones explicitly requested.
|
||||
have = linux.cap_bound_set()
|
||||
drop = have - self.caps
|
||||
|
||||
for cap in sorted(drop):
|
||||
args += ["--cap-drop", cap]
|
||||
|
||||
return args
|
||||
|
||||
@classmethod
|
||||
def read_with_timeout(cls, proc, poller, start, timeout):
|
||||
fd = proc.stdout.fileno()
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import pytest
|
|||
from osbuild.buildroot import BuildRoot
|
||||
from osbuild.monitor import LogMonitor, NullMonitor
|
||||
from osbuild.pipeline import detect_host_runner
|
||||
from osbuild.util import linux
|
||||
|
||||
from ..test import TestBase
|
||||
|
||||
|
|
@ -226,3 +227,62 @@ def test_env_isolation(tempdir):
|
|||
|
||||
for k in have:
|
||||
assert k in allowed
|
||||
|
||||
|
||||
@pytest.mark.skipif(not TestBase.can_bind_mount(), reason="root only")
|
||||
def test_caps(tempdir):
|
||||
runner = detect_host_runner()
|
||||
libdir = os.path.abspath(os.curdir)
|
||||
var = pathlib.Path(tempdir, "var")
|
||||
var.mkdir()
|
||||
|
||||
ipc = pathlib.Path(tempdir, "ipc")
|
||||
ipc.mkdir()
|
||||
|
||||
monitor = NullMonitor(sys.stderr.fileno())
|
||||
with BuildRoot("/", runner, libdir, var) as root:
|
||||
|
||||
def run_and_get_caps():
|
||||
cmd = ["/bin/sh", "-c", "cat /proc/self/status > /ipc/status"]
|
||||
r = root.run(cmd, monitor, binds=[f"{ipc}:/ipc"])
|
||||
|
||||
assert r.returncode == 0
|
||||
with open(os.path.join(ipc, "status"), encoding="utf-8") as f:
|
||||
data = f.readlines()
|
||||
assert data
|
||||
|
||||
print(data)
|
||||
perm = list(filter(lambda x: x.startswith("CapEff"), data))
|
||||
|
||||
assert perm and len(perm) == 1
|
||||
perm = perm[0]
|
||||
|
||||
perm = perm[7:].strip() # strip "CapEff"
|
||||
print(perm)
|
||||
|
||||
caps = linux.cap_mask_to_set(int(perm, base=16))
|
||||
return caps
|
||||
|
||||
# check case of `BuildRoot.caps` is `None`, i.e. don't drop capabilities,
|
||||
# thus the effective capabilities should be the bounding set
|
||||
assert root.caps is None
|
||||
|
||||
bound_set = linux.cap_bound_set()
|
||||
|
||||
caps = run_and_get_caps()
|
||||
assert caps == bound_set
|
||||
|
||||
# drop everything but `CAP_SYS_ADMIN`
|
||||
assert "CAP_SYS_ADMIN" in bound_set
|
||||
|
||||
enable = set(["CAP_SYS_ADMIN"])
|
||||
disable = bound_set - enable
|
||||
|
||||
root.caps = enable
|
||||
|
||||
caps = run_and_get_caps()
|
||||
|
||||
for e in enable:
|
||||
assert e in caps
|
||||
for d in disable:
|
||||
assert d not in caps
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue