library: change sec-opt func and adapt apps (#1569)

* change sec-opt func and adapt apps

* add test
This commit is contained in:
Stavros Kois
2025-02-10 13:38:26 +02:00
committed by GitHub
parent 1afd1dc813
commit 95821801f4
263 changed files with 662 additions and 166 deletions

View File

@@ -27,8 +27,8 @@ icon: https://media.sys.truenas.net/apps/glances/icons/icon.png
keywords:
- metric
- monitoring
lib_version: 2.1.14
lib_version_hash: 982057eeec3024ccecbeaa70e9ee59d948523a3b29d9fca6b39f127a42caa1cc
lib_version: 2.1.15
lib_version_hash: e26cffb91766782c787867b379519f7407b1fed8450dce463cefa56340a41d84
maintainers:
- email: dev@ixsystems.com
name: truenas
@@ -46,4 +46,4 @@ sources:
- https://hub.docker.com/r/nicolargo/glances
title: Glances
train: community
version: 1.0.3
version: 1.0.4

View File

@@ -6,7 +6,7 @@
{% do glances_container.add_caps(["FOWNER", "DAC_OVERRIDE", "SETGID", "SETUID", "SYS_PTRACE"]) %}
{% do glances_container.remove_security_opt("no-new-privileges") %}
{% do glances_container.add_security_opt("apparmor=unconfined") %}
{% do glances_container.add_security_opt("apparmor", "unconfined") %}
{% do glances_container.healthcheck.set_test("curl", {"port": values.network.web_port.port_number, "path": "/api/4/status"}) %}

View File

@@ -27,6 +27,7 @@ try:
valid_port_bind_mode_or_raise,
valid_pull_policy_or_raise,
)
from .security_opts import SecurityOpts
from .storage import Storage
from .sysctls import Sysctls
except ImportError:
@@ -52,6 +53,7 @@ except ImportError:
valid_port_bind_mode_or_raise,
valid_pull_policy_or_raise,
)
from security_opts import SecurityOpts
from storage import Storage
from sysctls import Sysctls
@@ -73,7 +75,7 @@ class Container:
self._hostname: str = ""
self._cap_drop: set[str] = set(["ALL"]) # Drop all capabilities by default and add caps granularly
self._cap_add: set[str] = set()
self._security_opt: set[str] = set(["no-new-privileges"])
self._security_opt: SecurityOpts = SecurityOpts(self._render_instance)
self._privileged: bool = False
self._group_add: set[int | str] = set()
self._network_mode: str = ""
@@ -227,13 +229,11 @@ class Container:
raise RenderError(f"Capability [{c}] already added")
self._cap_add.add(valid_cap_or_raise(c))
def add_security_opt(self, opt: str):
if opt in self._security_opt:
raise RenderError(f"Security Option [{opt}] already added")
self._security_opt.add(opt)
def add_security_opt(self, key: str, value: str | bool | None = None, arg: str | None = None):
self._security_opt.add_opt(key, value, arg)
def remove_security_opt(self, opt: str):
self._security_opt.remove(opt)
def remove_security_opt(self, key: str):
self._security_opt.remove_opt(key)
def set_network_mode(self, mode: str):
self._network_mode = valid_network_mode_or_raise(mode, self._render_instance.container_names())
@@ -366,8 +366,8 @@ class Container:
if self._cap_add:
result["cap_add"] = sorted(self._cap_add)
if self._security_opt:
result["security_opt"] = sorted(self._security_opt)
if self._security_opt.has_opts():
result["security_opt"] = self._security_opt.render()
if self._network_mode:
result["network_mode"] = self._network_mode

View File

@@ -0,0 +1,52 @@
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from render import Render
try:
from .error import RenderError
from .validations import valid_security_opt_or_raise
except ImportError:
from error import RenderError
from validations import valid_security_opt_or_raise
class SecurityOpt:
def __init__(self, opt: str, value: str | bool | None = None, arg: str | None = None):
self._opt: str = valid_security_opt_or_raise(opt)
self._value = str(value).lower() if isinstance(value, bool) else value
self._arg: str | None = arg
def render(self):
result = self._opt
if self._value is not None:
result = f"{result}={self._value}"
if self._arg is not None:
result = f"{result}:{self._arg}"
return result
class SecurityOpts:
def __init__(self, render_instance: "Render"):
self._render_instance = render_instance
self._opts: dict[str, SecurityOpt] = dict()
self.add_opt("no-new-privileges", True)
def add_opt(self, key: str, value: str | bool | None, arg: str | None = None):
if key in self._opts:
raise RenderError(f"Security Option [{key}] already added")
self._opts[key] = SecurityOpt(key, value, arg)
def remove_opt(self, key: str):
if key not in self._opts:
raise RenderError(f"Security Option [{key}] not found")
del self._opts[key]
def has_opts(self):
return len(self._opts) > 0
def render(self):
result = []
for opt in sorted(self._opts.values(), key=lambda o: o._opt):
result.append(opt.render())
return result

View File

@@ -250,35 +250,6 @@ def test_invalid_caps(mock_values):
c1.add_caps(["invalid_cap"])
def test_remove_security_opt(mock_values):
render = Render(mock_values)
c1 = render.add_container("test_container", "test_image")
c1.healthcheck.disable()
c1.remove_security_opt("no-new-privileges")
output = render.render()
assert "security_opt" not in output["services"]["test_container"]
def test_add_security_opt(mock_values):
render = Render(mock_values)
c1 = render.add_container("test_container", "test_image")
c1.healthcheck.disable()
c1.add_security_opt("seccomp=unconfined")
output = render.render()
assert output["services"]["test_container"]["security_opt"] == [
"no-new-privileges",
"seccomp=unconfined",
]
def test_add_duplicate_security_opt(mock_values):
render = Render(mock_values)
c1 = render.add_container("test_container", "test_image")
c1.healthcheck.disable()
with pytest.raises(Exception):
c1.add_security_opt("no-new-privileges")
def test_network_mode(mock_values):
render = Render(mock_values)
c1 = render.add_container("test_container", "test_image")

View File

@@ -0,0 +1,91 @@
import pytest
from render import Render
@pytest.fixture
def mock_values():
return {
"images": {
"test_image": {
"repository": "nginx",
"tag": "latest",
}
},
}
def test_add_security_opt(mock_values):
render = Render(mock_values)
c1 = render.add_container("test_container", "test_image")
c1.healthcheck.disable()
c1.add_security_opt("apparmor", "unconfined")
output = render.render()
assert output["services"]["test_container"]["security_opt"] == ["apparmor=unconfined", "no-new-privileges=true"]
def test_add_duplicate_security_opt(mock_values):
render = Render(mock_values)
c1 = render.add_container("test_container", "test_image")
c1.healthcheck.disable()
with pytest.raises(Exception):
c1.add_security_opt("no-new-privileges", True)
def test_add_empty_security_opt(mock_values):
render = Render(mock_values)
c1 = render.add_container("test_container", "test_image")
c1.healthcheck.disable()
with pytest.raises(Exception):
c1.add_security_opt("", True)
def test_remove_security_opt(mock_values):
render = Render(mock_values)
c1 = render.add_container("test_container", "test_image")
c1.healthcheck.disable()
c1.remove_security_opt("no-new-privileges")
output = render.render()
assert "security_opt" not in output["services"]["test_container"]
def test_add_security_opt_boolean(mock_values):
render = Render(mock_values)
c1 = render.add_container("test_container", "test_image")
c1.healthcheck.disable()
c1.remove_security_opt("no-new-privileges")
c1.add_security_opt("no-new-privileges", False)
output = render.render()
assert output["services"]["test_container"]["security_opt"] == ["no-new-privileges=false"]
def test_add_security_opt_arg(mock_values):
render = Render(mock_values)
c1 = render.add_container("test_container", "test_image")
c1.healthcheck.disable()
c1.add_security_opt("label", "type", "svirt_apache_t")
output = render.render()
assert output["services"]["test_container"]["security_opt"] == [
"label=type:svirt_apache_t",
"no-new-privileges=true",
]
def test_add_security_opt_with_invalid_opt(mock_values):
render = Render(mock_values)
c1 = render.add_container("test_container", "test_image")
c1.healthcheck.disable()
with pytest.raises(Exception):
c1.add_security_opt("invalid")
def test_add_security_opt_with_opt_containing_value(mock_values):
render = Render(mock_values)
c1 = render.add_container("test_container", "test_image")
c1.healthcheck.disable()
c1.remove_security_opt("no-new-privileges")
with pytest.raises(Exception):
c1.add_security_opt("no-new-privileges=true")
with pytest.raises(Exception):
c1.add_security_opt("apparmor:unconfined")

View File

@@ -24,6 +24,16 @@ RESTRICTED: tuple[Path, ...] = (
)
def valid_security_opt_or_raise(opt: str):
if ":" in opt or "=" in opt:
raise RenderError(f"Security Option [{opt}] cannot contain [:] or [=]. Pass value as an argument")
valid_opts = ["apparmor", "no-new-privileges", "seccomp", "systempaths", "label"]
if opt not in valid_opts:
raise RenderError(f"Security Option [{opt}] is not valid. Valid options are: [{', '.join(valid_opts)}]")
return opt
def valid_port_bind_mode_or_raise(status: str):
valid_statuses = ("published", "exposed", "")
if status not in valid_statuses:

View File

@@ -42,8 +42,8 @@ icon: https://media.sys.truenas.net/apps/steam-headless/icons/icon.png
keywords:
- games
- steam
lib_version: 2.1.14
lib_version_hash: 982057eeec3024ccecbeaa70e9ee59d948523a3b29d9fca6b39f127a42caa1cc
lib_version: 2.1.15
lib_version_hash: e26cffb91766782c787867b379519f7407b1fed8450dce463cefa56340a41d84
maintainers:
- email: dev@ixsystems.com
name: truenas
@@ -60,4 +60,4 @@ sources:
- https://github.com/Steam-Headless/docker-steam-headless
title: Steam Headless
train: community
version: 1.0.6
version: 1.0.7

View File

@@ -5,8 +5,8 @@
{% do c1.set_ipc_mode("host") %}
{% do c1.add_device_cgroup_rule("c 13:* rwm") %}
{% do c1.remove_security_opt("no-new-privileges") %}
{% do c1.add_security_opt("seccomp:unconfined") %}
{% do c1.add_security_opt("apparmor:unconfined") %}
{% do c1.add_security_opt("seccomp", "unconfined") %}
{% do c1.add_security_opt("apparmor", "unconfined") %}
{% do c1.add_caps([
"AUDIT_WRITE",
"CHOWN",

View File

@@ -27,6 +27,7 @@ try:
valid_port_bind_mode_or_raise,
valid_pull_policy_or_raise,
)
from .security_opts import SecurityOpts
from .storage import Storage
from .sysctls import Sysctls
except ImportError:
@@ -52,6 +53,7 @@ except ImportError:
valid_port_bind_mode_or_raise,
valid_pull_policy_or_raise,
)
from security_opts import SecurityOpts
from storage import Storage
from sysctls import Sysctls
@@ -73,7 +75,7 @@ class Container:
self._hostname: str = ""
self._cap_drop: set[str] = set(["ALL"]) # Drop all capabilities by default and add caps granularly
self._cap_add: set[str] = set()
self._security_opt: set[str] = set(["no-new-privileges"])
self._security_opt: SecurityOpts = SecurityOpts(self._render_instance)
self._privileged: bool = False
self._group_add: set[int | str] = set()
self._network_mode: str = ""
@@ -227,13 +229,11 @@ class Container:
raise RenderError(f"Capability [{c}] already added")
self._cap_add.add(valid_cap_or_raise(c))
def add_security_opt(self, opt: str):
if opt in self._security_opt:
raise RenderError(f"Security Option [{opt}] already added")
self._security_opt.add(opt)
def add_security_opt(self, key: str, value: str | bool | None = None, arg: str | None = None):
self._security_opt.add_opt(key, value, arg)
def remove_security_opt(self, opt: str):
self._security_opt.remove(opt)
def remove_security_opt(self, key: str):
self._security_opt.remove_opt(key)
def set_network_mode(self, mode: str):
self._network_mode = valid_network_mode_or_raise(mode, self._render_instance.container_names())
@@ -366,8 +366,8 @@ class Container:
if self._cap_add:
result["cap_add"] = sorted(self._cap_add)
if self._security_opt:
result["security_opt"] = sorted(self._security_opt)
if self._security_opt.has_opts():
result["security_opt"] = self._security_opt.render()
if self._network_mode:
result["network_mode"] = self._network_mode

View File

@@ -0,0 +1,52 @@
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from render import Render
try:
from .error import RenderError
from .validations import valid_security_opt_or_raise
except ImportError:
from error import RenderError
from validations import valid_security_opt_or_raise
class SecurityOpt:
def __init__(self, opt: str, value: str | bool | None = None, arg: str | None = None):
self._opt: str = valid_security_opt_or_raise(opt)
self._value = str(value).lower() if isinstance(value, bool) else value
self._arg: str | None = arg
def render(self):
result = self._opt
if self._value is not None:
result = f"{result}={self._value}"
if self._arg is not None:
result = f"{result}:{self._arg}"
return result
class SecurityOpts:
def __init__(self, render_instance: "Render"):
self._render_instance = render_instance
self._opts: dict[str, SecurityOpt] = dict()
self.add_opt("no-new-privileges", True)
def add_opt(self, key: str, value: str | bool | None, arg: str | None = None):
if key in self._opts:
raise RenderError(f"Security Option [{key}] already added")
self._opts[key] = SecurityOpt(key, value, arg)
def remove_opt(self, key: str):
if key not in self._opts:
raise RenderError(f"Security Option [{key}] not found")
del self._opts[key]
def has_opts(self):
return len(self._opts) > 0
def render(self):
result = []
for opt in sorted(self._opts.values(), key=lambda o: o._opt):
result.append(opt.render())
return result

Some files were not shown because too many files have changed in this diff Show More