diff --git a/API.md b/API.md index 86689a9b4..9a5a38962 100644 --- a/API.md +++ b/API.md @@ -427,6 +427,8 @@ Get all available addons. "host_ipc": "bool", "host_dbus": "bool", "privileged": ["NET_ADMIN", "SYS_ADMIN"], + "seccomp": "disable|default|custom", + "apparmor": "disable|default|custom", "devices": ["/dev/xy"], "auto_uart": "bool", "icon": "bool", diff --git a/hassio/addons/addon.py b/hassio/addons/addon.py index 83cd86d84..f8283f3af 100644 --- a/hassio/addons/addon.py +++ b/hassio/addons/addon.py @@ -23,7 +23,9 @@ from ..const import ( ATTR_STATE, ATTR_TIMEOUT, ATTR_AUTO_UPDATE, ATTR_NETWORK, ATTR_WEBUI, ATTR_HASSIO_API, ATTR_AUDIO, ATTR_AUDIO_OUTPUT, ATTR_AUDIO_INPUT, ATTR_GPIO, ATTR_HOMEASSISTANT_API, ATTR_STDIN, ATTR_LEGACY, ATTR_HOST_IPC, - ATTR_HOST_DBUS, ATTR_AUTO_UART, ATTR_DISCOVERY, ATTR_SERVICES) + ATTR_HOST_DBUS, ATTR_AUTO_UART, ATTR_DISCOVERY, ATTR_SERVICES, + ATTR_SECCOMP, ATTR_APPARMOR, SECURITY_CUSTOM, SECURITY_DISABLE, + SECURITY_DEFAULT) from ..coresys import CoreSysAttributes from ..docker.addon import DockerAddon from ..utils.json import write_json_file, read_json_file @@ -316,6 +318,29 @@ class Addon(CoreSysAttributes): """Return list of privilege.""" return self._mesh.get(ATTR_PRIVILEGED) + @property + def seccomp(self): + """Return True if seccomp is enabled.""" + if not self._mesh.get(ATTR_SECCOMP): + return SECURITY_DISABLE + elif self.path_seccomp.exists(): + return SECURITY_CUSTOM + return SECURITY_DEFAULT + + @property + def apparmor(self): + """Return True if seccomp is enabled.""" + if not self._mesh.get(ATTR_SECCOMP): + return SECURITY_DISABLE + elif self.path_apparmor.exists(): + return SECURITY_CUSTOM + return SECURITY_DEFAULT + + @property + def seccomp_profile(self): + """Return True if it not use the default profile.""" + return Path(self.path_location, f"{ATTR_SECCOMP}.json").exists() + @property def legacy(self): """Return if the add-on don't support hass labels.""" @@ -474,6 +499,16 @@ class Addon(CoreSysAttributes): """Return path to addon changelog.""" return Path(self.path_location, 'CHANGELOG.md') + @property + def path_seccomp(self): + """Return path to custom seccomp profile.""" + return Path(self.path_location, 'seccomp.json') + + @property + def path_apparmor(self): + """Return path to custom AppArmor profile.""" + return Path(self.path_location, 'apparmor') + def save_data(self): """Save data of addon.""" self._addons.data.save_data() diff --git a/hassio/addons/validate.py b/hassio/addons/validate.py index 0094b826f..c5ed81d28 100644 --- a/hassio/addons/validate.py +++ b/hassio/addons/validate.py @@ -17,7 +17,8 @@ from ..const import ( ATTR_AUTO_UPDATE, ATTR_WEBUI, ATTR_AUDIO, ATTR_AUDIO_INPUT, ATTR_HOST_IPC, ATTR_AUDIO_OUTPUT, ATTR_HASSIO_API, ATTR_BUILD_FROM, ATTR_SQUASH, ATTR_ARGS, ATTR_GPIO, ATTR_HOMEASSISTANT_API, ATTR_STDIN, ATTR_LEGACY, - ATTR_HOST_DBUS, ATTR_AUTO_UART, ATTR_SERVICES, ATTR_DISCOVERY) + ATTR_HOST_DBUS, ATTR_AUTO_UART, ATTR_SERVICES, ATTR_DISCOVERY, + ATTR_SECCOMP, ATTR_APPARMOR) from ..validate import NETWORK_PORT, DOCKER_PORTS, ALSA_CHANNEL _LOGGER = logging.getLogger(__name__) @@ -107,6 +108,8 @@ SCHEMA_ADDON_CONFIG = vol.Schema({ vol.Optional(ATTR_MAP, default=list): [vol.Match(RE_VOLUME)], vol.Optional(ATTR_ENVIRONMENT): {vol.Match(r"\w*"): vol.Coerce(str)}, vol.Optional(ATTR_PRIVILEGED): [vol.In(PRIVILEGED_ALL)], + vol.Optional(ATTR_SECCOMP, default=True): vol.Boolean(), + vol.Optional(ATTR_APPARMOR, default=True): vol.Boolean(), vol.Optional(ATTR_AUDIO, default=False): vol.Boolean(), vol.Optional(ATTR_GPIO, default=False): vol.Boolean(), vol.Optional(ATTR_HASSIO_API, default=False): vol.Boolean(), diff --git a/hassio/api/addons.py b/hassio/api/addons.py index b1909cbd3..bb00750e2 100644 --- a/hassio/api/addons.py +++ b/hassio/api/addons.py @@ -17,7 +17,7 @@ from ..const import ( ATTR_CHANGELOG, ATTR_HOST_IPC, ATTR_HOST_DBUS, ATTR_LONG_DESCRIPTION, ATTR_CPU_PERCENT, ATTR_MEMORY_LIMIT, ATTR_MEMORY_USAGE, ATTR_NETWORK_TX, ATTR_NETWORK_RX, ATTR_BLK_READ, ATTR_BLK_WRITE, ATTR_ICON, ATTR_SERVICES, - ATTR_DISCOVERY, + ATTR_DISCOVERY, ATTR_SECCOMP, ATTR_APPARMOR, CONTENT_TYPE_PNG, CONTENT_TYPE_BINARY, CONTENT_TYPE_TEXT) from ..coresys import CoreSysAttributes from ..validate import DOCKER_PORTS @@ -123,6 +123,8 @@ class APIAddons(CoreSysAttributes): ATTR_HOST_IPC: addon.host_ipc, ATTR_HOST_DBUS: addon.host_dbus, ATTR_PRIVILEGED: addon.privileged, + ATTR_SECCOMP: addon.seccomp, + ATTR_APPARMOR: addon.apparmor, ATTR_DEVICES: self._pretty_devices(addon), ATTR_ICON: addon.with_icon, ATTR_LOGO: addon.with_logo, diff --git a/hassio/const.py b/hassio/const.py index b0935e4cf..c7f2b8d5a 100644 --- a/hassio/const.py +++ b/hassio/const.py @@ -159,6 +159,8 @@ ATTR_DISCOVERY = 'discovery' ATTR_PROTECTED = 'protected' ATTR_CRYPTO = 'crypto' ATTR_BRANCH = 'branch' +ATTR_SECCOMP = 'seccomp' +ATTR_APPARMOR = 'apparmor' SERVICE_MQTT = 'mqtt' @@ -202,3 +204,7 @@ SNAPSHOT_FULL = 'full' SNAPSHOT_PARTIAL = 'partial' CRYPTO_AES128 = 'aes128' + +SECURITY_CUSTOM = 'custom' +SECURITY_DEFAULT = 'default' +SECURITY_DISABLE = 'disable' diff --git a/hassio/docker/addon.py b/hassio/docker/addon.py index e60a99202..76a6586de 100644 --- a/hassio/docker/addon.py +++ b/hassio/docker/addon.py @@ -9,7 +9,7 @@ from .interface import DockerInterface from ..addons.build import AddonBuild from ..const import ( MAP_CONFIG, MAP_SSL, MAP_ADDONS, MAP_BACKUP, MAP_SHARE, ENV_TOKEN, - ENV_TIME) + ENV_TIME, SECURITY_CUSTOM, SECURITY_DISABLE) from ..utils import process_lock _LOGGER = logging.getLogger(__name__) @@ -121,14 +121,21 @@ class DockerAddon(DockerInterface): @property def security_opt(self): """Controlling security opt.""" - privileged = self.addon.privileged or [] + security = [] - # Disable AppArmor sinse it make troubles wit SYS_ADMIN - if 'SYS_ADMIN' in privileged: - return [ - "apparmor:unconfined", - ] - return None + # AppArmor + if self.addon.apparmor == SECURITY_DISABLE: + security.append("apparmor:unconfined") + elif self.addon.apparmor == SECURITY_DEFAULT: + security.append(f"apparmor={self.addon.slug}") + + # Seccomp + if self.addon.seccomp == SECURITY_DISABLE: + security.append("seccomp=unconfined") + elif self.addon.seccomp == SECURITY_CUSTOM: + security.append(f"seccomp={self.addon.path_seccomp}") + + return security or None @property def tmpfs(self):