mirror of
https://github.com/home-assistant/supervisor.git
synced 2025-07-16 13:46:31 +00:00
Cleanup and fixup Apparmor implementation (#509)
* Cleanup and fixup Apparmor implementation * Update addon.py * Update validate.py * Create apparmor.py * Update exceptions.py * Update apparmor.py * Create apparmor.py * Update const.py * Update bootstrap.py * Update const.py * Update config.py * Update addons.py * Update apparmor.py * Add support for host AppArmor * Update apparmor.py * Update apparmor.py * Update apparmor.py * Update apparmor.py * Update apparmor.py * Update addon.py * Update apparmor.py * Update addon.py * Update addon.py * Update addon.py * Update addon.py * Update const.py * Update supervisor.py * Update supervisor.py * Update supervisor.py * Add snapshot support * some cleanup * Cleanup v2 * Update aiohttp * fix lint * fix bugs * Add info logs
This commit is contained in:
parent
561e80c2be
commit
7769d6fff1
1
API.md
1
API.md
@ -451,7 +451,6 @@ Get all available addons.
|
|||||||
"host_ipc": "bool",
|
"host_ipc": "bool",
|
||||||
"host_dbus": "bool",
|
"host_dbus": "bool",
|
||||||
"privileged": ["NET_ADMIN", "SYS_ADMIN"],
|
"privileged": ["NET_ADMIN", "SYS_ADMIN"],
|
||||||
"seccomp": "disable|default|profile",
|
|
||||||
"apparmor": "disable|default|profile",
|
"apparmor": "disable|default|profile",
|
||||||
"devices": ["/dev/xy"],
|
"devices": ["/dev/xy"],
|
||||||
"auto_uart": "bool",
|
"auto_uart": "bool",
|
||||||
|
@ -17,7 +17,7 @@ RUN apk add --no-cache \
|
|||||||
python3-dev \
|
python3-dev \
|
||||||
g++ \
|
g++ \
|
||||||
&& pip3 install --no-cache-dir \
|
&& pip3 install --no-cache-dir \
|
||||||
uvloop==0.9.1 \
|
uvloop==0.10.1 \
|
||||||
cchardet==2.1.1 \
|
cchardet==2.1.1 \
|
||||||
pycryptodome==3.4.11 \
|
pycryptodome==3.4.11 \
|
||||||
&& apk del .build-dependencies
|
&& apk del .build-dependencies
|
||||||
|
@ -25,11 +25,12 @@ from ..const import (
|
|||||||
ATTR_HASSIO_API, ATTR_AUDIO, ATTR_AUDIO_OUTPUT, ATTR_AUDIO_INPUT,
|
ATTR_HASSIO_API, ATTR_AUDIO, ATTR_AUDIO_OUTPUT, ATTR_AUDIO_INPUT,
|
||||||
ATTR_GPIO, ATTR_HOMEASSISTANT_API, ATTR_STDIN, ATTR_LEGACY, ATTR_HOST_IPC,
|
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_PROFILE, SECURITY_DISABLE,
|
ATTR_APPARMOR, SECURITY_PROFILE, SECURITY_DISABLE, SECURITY_DEFAULT)
|
||||||
SECURITY_DEFAULT)
|
|
||||||
from ..coresys import CoreSysAttributes
|
from ..coresys import CoreSysAttributes
|
||||||
from ..docker.addon import DockerAddon
|
from ..docker.addon import DockerAddon
|
||||||
from ..utils.json import write_json_file, read_json_file
|
from ..utils.json import write_json_file, read_json_file
|
||||||
|
from ..utils.apparmor import adjust_profile
|
||||||
|
from ..exceptions import HostAppArmorError
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
@ -319,21 +320,12 @@ class Addon(CoreSysAttributes):
|
|||||||
"""Return list of privilege."""
|
"""Return list of privilege."""
|
||||||
return self._mesh.get(ATTR_PRIVILEGED)
|
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_PROFILE
|
|
||||||
return SECURITY_DEFAULT
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def apparmor(self):
|
def apparmor(self):
|
||||||
"""Return True if seccomp is enabled."""
|
"""Return True if apparmor is enabled."""
|
||||||
if not self._mesh.get(ATTR_APPARMOR):
|
if not self._mesh.get(ATTR_APPARMOR):
|
||||||
return SECURITY_DISABLE
|
return SECURITY_DISABLE
|
||||||
elif self.path_apparmor.exists():
|
elif self.sys_host.apparmor.exists(self.slug):
|
||||||
return SECURITY_PROFILE
|
return SECURITY_PROFILE
|
||||||
return SECURITY_DEFAULT
|
return SECURITY_DEFAULT
|
||||||
|
|
||||||
@ -493,15 +485,10 @@ class Addon(CoreSysAttributes):
|
|||||||
"""Return path to addon changelog."""
|
"""Return path to addon changelog."""
|
||||||
return Path(self.path_location, 'CHANGELOG.md')
|
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
|
@property
|
||||||
def path_apparmor(self):
|
def path_apparmor(self):
|
||||||
"""Return path to custom AppArmor profile."""
|
"""Return path to custom AppArmor profile."""
|
||||||
return Path(self.path_location, 'apparmor')
|
return Path(self.path_location, 'apparmor.txt')
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def path_asound(self):
|
def path_asound(self):
|
||||||
@ -549,6 +536,27 @@ class Addon(CoreSysAttributes):
|
|||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
async def _install_apparmor(self):
|
||||||
|
"""Install or Update AppArmor profile for Add-on."""
|
||||||
|
exists_local = self.sys_host.apparmor.exists(self.slug)
|
||||||
|
exists_addon = self.path_apparmor.exists()
|
||||||
|
|
||||||
|
# Nothing to do
|
||||||
|
if not exists_local and not exists_addon:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Need removed
|
||||||
|
if exists_local and not exists_addon:
|
||||||
|
await self.sys_host.apparmor.remove_profile(self.slug)
|
||||||
|
return
|
||||||
|
|
||||||
|
# Need install/update
|
||||||
|
with TemporaryDirectory(dir=self.sys_config.path_tmp) as tmp_folder:
|
||||||
|
profile_file = Path(tmp_folder, 'apparmor.txt')
|
||||||
|
|
||||||
|
adjust_profile(self.slug, self.path_apparmor, self.profile_file)
|
||||||
|
await self.sys_host.apparmor.load_profile(self.slug, profile_file)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def schema(self):
|
def schema(self):
|
||||||
"""Create a schema for addon options."""
|
"""Create a schema for addon options."""
|
||||||
@ -604,6 +612,9 @@ class Addon(CoreSysAttributes):
|
|||||||
"Create Home-Assistant addon data folder %s", self.path_data)
|
"Create Home-Assistant addon data folder %s", self.path_data)
|
||||||
self.path_data.mkdir()
|
self.path_data.mkdir()
|
||||||
|
|
||||||
|
# Setup/Fix AppArmor profile
|
||||||
|
await self._install_apparmor()
|
||||||
|
|
||||||
if not await self.instance.install(self.last_version):
|
if not await self.instance.install(self.last_version):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@ -626,6 +637,11 @@ class Addon(CoreSysAttributes):
|
|||||||
with suppress(OSError):
|
with suppress(OSError):
|
||||||
self.path_asound.unlink()
|
self.path_asound.unlink()
|
||||||
|
|
||||||
|
# Cleanup apparmor profile
|
||||||
|
if self.sys_host.apparmor.exists(self.slug):
|
||||||
|
with suppress(HostAppArmorError):
|
||||||
|
await self.sys_host.apparmor.remove_profile(self.slug)
|
||||||
|
|
||||||
self._set_uninstall()
|
self._set_uninstall()
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@ -672,6 +688,9 @@ class Addon(CoreSysAttributes):
|
|||||||
return False
|
return False
|
||||||
self._set_update(self.last_version)
|
self._set_update(self.last_version)
|
||||||
|
|
||||||
|
# Setup/Fix AppArmor profile
|
||||||
|
await self._install_apparmor()
|
||||||
|
|
||||||
# restore state
|
# restore state
|
||||||
if last_state == STATE_STARTED:
|
if last_state == STATE_STARTED:
|
||||||
await self.start()
|
await self.start()
|
||||||
@ -738,7 +757,7 @@ class Addon(CoreSysAttributes):
|
|||||||
with TemporaryDirectory(dir=str(self.sys_config.path_tmp)) as temp:
|
with TemporaryDirectory(dir=str(self.sys_config.path_tmp)) as temp:
|
||||||
# store local image
|
# store local image
|
||||||
if self.need_build and not await \
|
if self.need_build and not await \
|
||||||
self.instance.export_image(Path(temp, "image.tar")):
|
self.instance.export_image(Path(temp, 'image.tar')):
|
||||||
return False
|
return False
|
||||||
|
|
||||||
data = {
|
data = {
|
||||||
@ -750,11 +769,20 @@ class Addon(CoreSysAttributes):
|
|||||||
|
|
||||||
# store local configs/state
|
# store local configs/state
|
||||||
try:
|
try:
|
||||||
write_json_file(Path(temp, "addon.json"), data)
|
write_json_file(Path(temp, 'addon.json'), data)
|
||||||
except (OSError, json.JSONDecodeError) as err:
|
except (OSError, json.JSONDecodeError) as err:
|
||||||
_LOGGER.error("Can't save meta for %s: %s", self._id, err)
|
_LOGGER.error("Can't save meta for %s: %s", self._id, err)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
# Store AppArmor Profile
|
||||||
|
if self.sys_host.apparmor.exists(self.slug):
|
||||||
|
profile = Path(temp, 'apparmor.txt')
|
||||||
|
try:
|
||||||
|
self.sys_host.apparmor.backup_profile(self.slug, profile)
|
||||||
|
except HostAppArmorError:
|
||||||
|
_LOGGER.error("Can't backup AppArmor profile")
|
||||||
|
return False
|
||||||
|
|
||||||
# write into tarfile
|
# write into tarfile
|
||||||
def _write_tarfile():
|
def _write_tarfile():
|
||||||
"""Write tar inside loop."""
|
"""Write tar inside loop."""
|
||||||
@ -789,7 +817,7 @@ class Addon(CoreSysAttributes):
|
|||||||
|
|
||||||
# read snapshot data
|
# read snapshot data
|
||||||
try:
|
try:
|
||||||
data = read_json_file(Path(temp, "addon.json"))
|
data = read_json_file(Path(temp, 'addon.json'))
|
||||||
except (OSError, json.JSONDecodeError) as err:
|
except (OSError, json.JSONDecodeError) as err:
|
||||||
_LOGGER.error("Can't read addon.json: %s", err)
|
_LOGGER.error("Can't read addon.json: %s", err)
|
||||||
|
|
||||||
@ -810,7 +838,7 @@ class Addon(CoreSysAttributes):
|
|||||||
if not await self.instance.exists():
|
if not await self.instance.exists():
|
||||||
_LOGGER.info("Restore image for addon %s", self._id)
|
_LOGGER.info("Restore image for addon %s", self._id)
|
||||||
|
|
||||||
image_file = Path(temp, "image.tar")
|
image_file = Path(temp, 'image.tar')
|
||||||
if image_file.is_file():
|
if image_file.is_file():
|
||||||
await self.instance.import_image(image_file, version)
|
await self.instance.import_image(image_file, version)
|
||||||
else:
|
else:
|
||||||
@ -833,6 +861,16 @@ class Addon(CoreSysAttributes):
|
|||||||
_LOGGER.error("Can't restore origin data: %s", err)
|
_LOGGER.error("Can't restore origin data: %s", err)
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
# Restore AppArmor
|
||||||
|
profile_file = Path(temp, 'apparmor.txt')
|
||||||
|
if profile_file.exists():
|
||||||
|
try:
|
||||||
|
await self.sys_host.apparmor.load_profile(
|
||||||
|
self.slug, profile_file)
|
||||||
|
except HostAppArmorError:
|
||||||
|
_LOGGER.error("Can't restore AppArmor profile")
|
||||||
|
return False
|
||||||
|
|
||||||
# run addon
|
# run addon
|
||||||
if data[ATTR_STATE] == STATE_STARTED:
|
if data[ATTR_STATE] == STATE_STARTED:
|
||||||
return await self.start()
|
return await self.start()
|
||||||
|
@ -18,7 +18,7 @@ from ..const import (
|
|||||||
ATTR_AUDIO_OUTPUT, ATTR_HASSIO_API, ATTR_BUILD_FROM, ATTR_SQUASH,
|
ATTR_AUDIO_OUTPUT, ATTR_HASSIO_API, ATTR_BUILD_FROM, ATTR_SQUASH,
|
||||||
ATTR_ARGS, ATTR_GPIO, ATTR_HOMEASSISTANT_API, ATTR_STDIN, ATTR_LEGACY,
|
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)
|
ATTR_APPARMOR)
|
||||||
from ..validate import NETWORK_PORT, DOCKER_PORTS, ALSA_DEVICE
|
from ..validate import NETWORK_PORT, DOCKER_PORTS, ALSA_DEVICE
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
@ -108,7 +108,6 @@ SCHEMA_ADDON_CONFIG = vol.Schema({
|
|||||||
vol.Optional(ATTR_MAP, default=list): [vol.Match(RE_VOLUME)],
|
vol.Optional(ATTR_MAP, default=list): [vol.Match(RE_VOLUME)],
|
||||||
vol.Optional(ATTR_ENVIRONMENT): {vol.Match(r"\w*"): vol.Coerce(str)},
|
vol.Optional(ATTR_ENVIRONMENT): {vol.Match(r"\w*"): vol.Coerce(str)},
|
||||||
vol.Optional(ATTR_PRIVILEGED): [vol.In(PRIVILEGED_ALL)],
|
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_APPARMOR, default=True): vol.Boolean(),
|
||||||
vol.Optional(ATTR_AUDIO, default=False): vol.Boolean(),
|
vol.Optional(ATTR_AUDIO, default=False): vol.Boolean(),
|
||||||
vol.Optional(ATTR_GPIO, default=False): vol.Boolean(),
|
vol.Optional(ATTR_GPIO, default=False): vol.Boolean(),
|
||||||
|
@ -17,7 +17,7 @@ from ..const import (
|
|||||||
ATTR_CHANGELOG, ATTR_HOST_IPC, ATTR_HOST_DBUS, ATTR_LONG_DESCRIPTION,
|
ATTR_CHANGELOG, ATTR_HOST_IPC, ATTR_HOST_DBUS, ATTR_LONG_DESCRIPTION,
|
||||||
ATTR_CPU_PERCENT, ATTR_MEMORY_LIMIT, ATTR_MEMORY_USAGE, ATTR_NETWORK_TX,
|
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_NETWORK_RX, ATTR_BLK_READ, ATTR_BLK_WRITE, ATTR_ICON, ATTR_SERVICES,
|
||||||
ATTR_DISCOVERY, ATTR_SECCOMP, ATTR_APPARMOR,
|
ATTR_DISCOVERY, ATTR_APPARMOR,
|
||||||
CONTENT_TYPE_PNG, CONTENT_TYPE_BINARY, CONTENT_TYPE_TEXT)
|
CONTENT_TYPE_PNG, CONTENT_TYPE_BINARY, CONTENT_TYPE_TEXT)
|
||||||
from ..coresys import CoreSysAttributes
|
from ..coresys import CoreSysAttributes
|
||||||
from ..validate import DOCKER_PORTS, ALSA_DEVICE
|
from ..validate import DOCKER_PORTS, ALSA_DEVICE
|
||||||
@ -126,7 +126,6 @@ class APIAddons(CoreSysAttributes):
|
|||||||
ATTR_HOST_IPC: addon.host_ipc,
|
ATTR_HOST_IPC: addon.host_ipc,
|
||||||
ATTR_HOST_DBUS: addon.host_dbus,
|
ATTR_HOST_DBUS: addon.host_dbus,
|
||||||
ATTR_PRIVILEGED: addon.privileged,
|
ATTR_PRIVILEGED: addon.privileged,
|
||||||
ATTR_SECCOMP: addon.seccomp,
|
|
||||||
ATTR_APPARMOR: addon.apparmor,
|
ATTR_APPARMOR: addon.apparmor,
|
||||||
ATTR_DEVICES: self._pretty_devices(addon),
|
ATTR_DEVICES: self._pretty_devices(addon),
|
||||||
ATTR_ICON: addon.with_icon,
|
ATTR_ICON: addon.with_icon,
|
||||||
|
@ -103,6 +103,11 @@ def initialize_system_data(coresys):
|
|||||||
_LOGGER.info("Create hassio share folder %s", config.path_share)
|
_LOGGER.info("Create hassio share folder %s", config.path_share)
|
||||||
config.path_share.mkdir()
|
config.path_share.mkdir()
|
||||||
|
|
||||||
|
# apparmor folder
|
||||||
|
if not config.path_apparmor.is_dir():
|
||||||
|
_LOGGER.info("Create hassio apparmor folder %s", config.path_apparmor)
|
||||||
|
config.path_apparmor.mkdir()
|
||||||
|
|
||||||
return config
|
return config
|
||||||
|
|
||||||
|
|
||||||
|
@ -25,6 +25,7 @@ ADDONS_DATA = PurePath("addons/data")
|
|||||||
BACKUP_DATA = PurePath("backup")
|
BACKUP_DATA = PurePath("backup")
|
||||||
SHARE_DATA = PurePath("share")
|
SHARE_DATA = PurePath("share")
|
||||||
TMP_DATA = PurePath("tmp")
|
TMP_DATA = PurePath("tmp")
|
||||||
|
APPARMOR_DATA = PurePath("apparmor")
|
||||||
|
|
||||||
DEFAULT_BOOT_TIME = datetime.utcfromtimestamp(0).isoformat()
|
DEFAULT_BOOT_TIME = datetime.utcfromtimestamp(0).isoformat()
|
||||||
|
|
||||||
@ -156,6 +157,11 @@ class CoreConfig(JsonConfig):
|
|||||||
"""Return root share data folder."""
|
"""Return root share data folder."""
|
||||||
return Path(HASSIO_DATA, SHARE_DATA)
|
return Path(HASSIO_DATA, SHARE_DATA)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def path_apparmor(self):
|
||||||
|
"""Return root apparmor profile folder."""
|
||||||
|
return Path(HASSIO_DATA, APPARMOR_DATA)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def path_extern_share(self):
|
def path_extern_share(self):
|
||||||
"""Return root share data folder extern for docker."""
|
"""Return root share data folder extern for docker."""
|
||||||
|
@ -7,6 +7,8 @@ HASSIO_VERSION = '108'
|
|||||||
URL_HASSIO_ADDONS = "https://github.com/home-assistant/hassio-addons"
|
URL_HASSIO_ADDONS = "https://github.com/home-assistant/hassio-addons"
|
||||||
URL_HASSIO_VERSION = \
|
URL_HASSIO_VERSION = \
|
||||||
"https://s3.amazonaws.com/hassio-version/{channel}.json"
|
"https://s3.amazonaws.com/hassio-version/{channel}.json"
|
||||||
|
URL_HASSIO_APPARMOR = \
|
||||||
|
"https://s3.amazonaws.com/hassio-version/apparmor.txt"
|
||||||
|
|
||||||
HASSIO_DATA = Path("/data")
|
HASSIO_DATA = Path("/data")
|
||||||
|
|
||||||
@ -162,7 +164,6 @@ ATTR_PROTECTED = 'protected'
|
|||||||
ATTR_CRYPTO = 'crypto'
|
ATTR_CRYPTO = 'crypto'
|
||||||
ATTR_BRANCH = 'branch'
|
ATTR_BRANCH = 'branch'
|
||||||
ATTR_KERNEL = 'kernel'
|
ATTR_KERNEL = 'kernel'
|
||||||
ATTR_SECCOMP = 'seccomp'
|
|
||||||
ATTR_APPARMOR = 'apparmor'
|
ATTR_APPARMOR = 'apparmor'
|
||||||
|
|
||||||
SERVICE_MQTT = 'mqtt'
|
SERVICE_MQTT = 'mqtt'
|
||||||
|
@ -267,4 +267,4 @@ class CoreSysAttributes:
|
|||||||
"""Mapping to coresys."""
|
"""Mapping to coresys."""
|
||||||
if name.startswith("sys_") and hasattr(self.coresys, name[4:]):
|
if name.startswith("sys_") and hasattr(self.coresys, name[4:]):
|
||||||
return getattr(self.coresys, name[4:])
|
return getattr(self.coresys, name[4:])
|
||||||
raise AttributeError()
|
raise AttributeError(f"Can't resolve {name} on {self}")
|
||||||
|
@ -124,18 +124,17 @@ class DockerAddon(DockerInterface):
|
|||||||
security = []
|
security = []
|
||||||
|
|
||||||
# AppArmor
|
# AppArmor
|
||||||
if self.addon.apparmor == SECURITY_DISABLE:
|
apparmor = self.sys_host.apparmor.available
|
||||||
|
if not apparmor or self.addon.apparmor == SECURITY_DISABLE:
|
||||||
security.append("apparmor:unconfined")
|
security.append("apparmor:unconfined")
|
||||||
elif self.addon.apparmor == SECURITY_PROFILE:
|
elif self.addon.apparmor == SECURITY_PROFILE:
|
||||||
security.append(f"apparmor={self.addon.slug}")
|
security.append(f"apparmor={self.addon.slug}")
|
||||||
|
|
||||||
# Seccomp
|
# Disable Seccomp / We don't support it official and it
|
||||||
if self.addon.seccomp == SECURITY_DISABLE:
|
# make troubles on some kind of host systems.
|
||||||
security.append("seccomp=unconfined")
|
security.append("seccomp=unconfined")
|
||||||
elif self.addon.seccomp == SECURITY_PROFILE:
|
|
||||||
security.append(f"seccomp={self.addon.path_seccomp}")
|
|
||||||
|
|
||||||
return security or None
|
return security
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def tmpfs(self):
|
def tmpfs(self):
|
||||||
|
@ -6,11 +6,6 @@ class HassioError(Exception):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
class HassioInternalError(HassioError):
|
|
||||||
"""Internal Hass.io error they can't handle."""
|
|
||||||
pass
|
|
||||||
|
|
||||||
|
|
||||||
class HassioNotSupportedError(HassioError):
|
class HassioNotSupportedError(HassioError):
|
||||||
"""Function is not supported."""
|
"""Function is not supported."""
|
||||||
pass
|
pass
|
||||||
@ -33,6 +28,10 @@ class HostServiceError(HostError):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class HostAppArmorError(HostError):
|
||||||
|
"""Host apparmor functions fails."""
|
||||||
|
|
||||||
|
|
||||||
# utils/gdbus
|
# utils/gdbus
|
||||||
|
|
||||||
class DBusError(HassioError):
|
class DBusError(HassioError):
|
||||||
@ -52,3 +51,20 @@ class DBusFatalError(DBusError):
|
|||||||
class DBusParseError(DBusError):
|
class DBusParseError(DBusError):
|
||||||
"""DBus parse error."""
|
"""DBus parse error."""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
# util/apparmor
|
||||||
|
|
||||||
|
class AppArmorError(HostAppArmorError):
|
||||||
|
"""General AppArmor error."""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class AppArmorFileError(AppArmorError):
|
||||||
|
"""AppArmor profile file error."""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class AppArmorInvalidError(AppArmorError):
|
||||||
|
"""AppArmor profile validate error."""
|
||||||
|
pass
|
||||||
|
@ -1,12 +1,18 @@
|
|||||||
"""Host function like audio/dbus/systemd."""
|
"""Host function like audio/dbus/systemd."""
|
||||||
|
from contextlib import suppress
|
||||||
|
import logging
|
||||||
|
|
||||||
from .alsa import AlsaAudio
|
from .alsa import AlsaAudio
|
||||||
|
from .apparmor import AppArmorControl
|
||||||
from .control import SystemControl
|
from .control import SystemControl
|
||||||
from .info import InfoCenter
|
from .info import InfoCenter
|
||||||
from .service import ServiceManager
|
from .services import ServiceManager
|
||||||
from ..const import (
|
from ..const import (
|
||||||
FEATURES_REBOOT, FEATURES_SHUTDOWN, FEATURES_HOSTNAME, FEATURES_SERVICES)
|
FEATURES_REBOOT, FEATURES_SHUTDOWN, FEATURES_HOSTNAME, FEATURES_SERVICES)
|
||||||
from ..coresys import CoreSysAttributes
|
from ..coresys import CoreSysAttributes
|
||||||
|
from ..exceptions import HassioError
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class HostManager(CoreSysAttributes):
|
class HostManager(CoreSysAttributes):
|
||||||
@ -16,6 +22,7 @@ class HostManager(CoreSysAttributes):
|
|||||||
"""Initialize Host manager."""
|
"""Initialize Host manager."""
|
||||||
self.coresys = coresys
|
self.coresys = coresys
|
||||||
self._alsa = AlsaAudio(coresys)
|
self._alsa = AlsaAudio(coresys)
|
||||||
|
self._apparmor = AppArmorControl(coresys)
|
||||||
self._control = SystemControl(coresys)
|
self._control = SystemControl(coresys)
|
||||||
self._info = InfoCenter(coresys)
|
self._info = InfoCenter(coresys)
|
||||||
self._services = ServiceManager(coresys)
|
self._services = ServiceManager(coresys)
|
||||||
@ -25,6 +32,11 @@ class HostManager(CoreSysAttributes):
|
|||||||
"""Return host ALSA handler."""
|
"""Return host ALSA handler."""
|
||||||
return self._alsa
|
return self._alsa
|
||||||
|
|
||||||
|
@property
|
||||||
|
def apparmor(self):
|
||||||
|
"""Return host apparmor handler."""
|
||||||
|
return self._apparmor
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def control(self):
|
def control(self):
|
||||||
"""Return host control handler."""
|
"""Return host control handler."""
|
||||||
@ -57,14 +69,21 @@ class HostManager(CoreSysAttributes):
|
|||||||
|
|
||||||
return features
|
return features
|
||||||
|
|
||||||
async def load(self):
|
async def reload(self):
|
||||||
"""Load host functions."""
|
"""Reload host functions."""
|
||||||
if self.sys_dbus.hostname.is_connected:
|
if self.sys_dbus.hostname.is_connected:
|
||||||
await self.info.update()
|
await self.info.update()
|
||||||
|
|
||||||
if self.sys_dbus.systemd.is_connected:
|
if self.sys_dbus.systemd.is_connected:
|
||||||
await self.services.update()
|
await self.services.update()
|
||||||
|
|
||||||
def reload(self):
|
async def load(self):
|
||||||
"""Reload host information."""
|
"""Load host information."""
|
||||||
return self.load()
|
with suppress(HassioError):
|
||||||
|
await self.reload()
|
||||||
|
|
||||||
|
# Load profile data
|
||||||
|
try:
|
||||||
|
await self.apparmor.load()
|
||||||
|
except HassioError as err:
|
||||||
|
_LOGGER.waring("Load host AppArmor on start fails: %s", err)
|
||||||
|
121
hassio/host/apparmor.py
Normal file
121
hassio/host/apparmor.py
Normal file
@ -0,0 +1,121 @@
|
|||||||
|
"""AppArmor control for host."""
|
||||||
|
import logging
|
||||||
|
import shutil
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from ..coresys import CoreSysAttributes
|
||||||
|
from ..exceptions import DBusError, HostAppArmorError
|
||||||
|
from ..utils.apparmor import validate_profile
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
SYSTEMD_SERVICES = {'hassos-apparmor.service', 'hassio-apparmor.service'}
|
||||||
|
|
||||||
|
|
||||||
|
class AppArmorControl(CoreSysAttributes):
|
||||||
|
"""Handle host apparmor controls."""
|
||||||
|
|
||||||
|
def __init__(self, coresys):
|
||||||
|
"""Initialize host power handling."""
|
||||||
|
self.coresys = coresys
|
||||||
|
self._profiles = set()
|
||||||
|
self._service = None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def available(self):
|
||||||
|
"""Return True if AppArmor is availabe on host."""
|
||||||
|
return self._service is not None
|
||||||
|
|
||||||
|
def exists(self, profile):
|
||||||
|
"""Return True if a profile exists."""
|
||||||
|
return profile in self._profiles
|
||||||
|
|
||||||
|
async def _reload_service(self):
|
||||||
|
"""Reload internal service."""
|
||||||
|
try:
|
||||||
|
await self.sys_host.services.reload(self._service)
|
||||||
|
except DBusError as err:
|
||||||
|
_LOGGER.error("Can't reload %s: %s", self._service, err)
|
||||||
|
|
||||||
|
def _get_profile(self, profile_name):
|
||||||
|
"""Get a profile from AppArmor store."""
|
||||||
|
if profile_name not in self._profiles:
|
||||||
|
_LOGGER.error("Can't find %s for removing", profile_name)
|
||||||
|
raise HostAppArmorError()
|
||||||
|
return Path(self.sys_config.path_apparmor, profile_name)
|
||||||
|
|
||||||
|
async def load(self):
|
||||||
|
"""Load available profiles."""
|
||||||
|
for content in self.sys_config.path_apparmor.iterdir():
|
||||||
|
if not content.is_file():
|
||||||
|
continue
|
||||||
|
self._profiles.add(content.name)
|
||||||
|
|
||||||
|
# Is connected with systemd?
|
||||||
|
_LOGGER.info("Load AppArmor Profiles: %s", self._profiles)
|
||||||
|
for service in SYSTEMD_SERVICES:
|
||||||
|
if not self.sys_host.services.exists(service):
|
||||||
|
continue
|
||||||
|
self._service = service
|
||||||
|
|
||||||
|
# Load profiles
|
||||||
|
if self.available:
|
||||||
|
await self._reload_service()
|
||||||
|
else:
|
||||||
|
_LOGGER.info("AppArmor is not enabled on Host")
|
||||||
|
|
||||||
|
async def load_profile(self, profile_name, profile_file):
|
||||||
|
"""Load/Update a new/exists profile into AppArmor."""
|
||||||
|
if not validate_profile(profile_file, profile_name):
|
||||||
|
_LOGGER.error("profile is not valid with name %s", profile_name)
|
||||||
|
raise HostAppArmorError()
|
||||||
|
|
||||||
|
# Copy to AppArmor folder
|
||||||
|
dest_profile = Path(self.sys_config.path_apparmor, profile_name)
|
||||||
|
try:
|
||||||
|
shutil.copy(profile_file, dest_profile)
|
||||||
|
except OSError as err:
|
||||||
|
_LOGGER.error("Can't copy %s: %s", profile_file, err)
|
||||||
|
raise HostAppArmorError() from None
|
||||||
|
|
||||||
|
# Load profiles
|
||||||
|
_LOGGER.info("Add or Update AppArmor profile: %s", profile_name)
|
||||||
|
self._profiles.add(profile_name)
|
||||||
|
if self.available:
|
||||||
|
await self._reload_service()
|
||||||
|
|
||||||
|
async def remove_profile(self, profile_name):
|
||||||
|
"""Remove a AppArmor profile."""
|
||||||
|
profile_file = self._get_profile(profile_name)
|
||||||
|
|
||||||
|
# Only remove file
|
||||||
|
if not self.available:
|
||||||
|
try:
|
||||||
|
profile_file.unlink()
|
||||||
|
except OSError as err:
|
||||||
|
_LOGGER.error("Can't remove profile: %s", err)
|
||||||
|
raise HostAppArmorError()
|
||||||
|
return
|
||||||
|
|
||||||
|
# Marks als remove and start host process
|
||||||
|
remove_profile = Path(
|
||||||
|
self.sys_config.path_apparmor, 'remove', profile_name)
|
||||||
|
try:
|
||||||
|
profile_file.rename(remove_profile)
|
||||||
|
except OSError as err:
|
||||||
|
_LOGGER.error("Can't mark profile as remove: %s", err)
|
||||||
|
raise HostAppArmorError()
|
||||||
|
|
||||||
|
_LOGGER.info("Remove AppArmor profile: %s", profile_name)
|
||||||
|
self._profiles.remove(profile_name)
|
||||||
|
await self._reload_service()
|
||||||
|
|
||||||
|
def backup_profile(self, profile_name, backup_file):
|
||||||
|
"""Backup A profile into a new file."""
|
||||||
|
profile_file = self._get_profile(profile_name)
|
||||||
|
|
||||||
|
try:
|
||||||
|
shutil.copy(profile_file, backup_file)
|
||||||
|
except OSError as err:
|
||||||
|
_LOGGER.error("Can't backup profile %s: %s", profile_name, err)
|
||||||
|
raise HostAppArmorError()
|
@ -39,7 +39,7 @@ SCHEMA_SNAPSHOT = vol.Schema({
|
|||||||
vol.Optional(ATTR_BOOT, default=True): vol.Boolean(),
|
vol.Optional(ATTR_BOOT, default=True): vol.Boolean(),
|
||||||
vol.Optional(ATTR_SSL, default=False): vol.Boolean(),
|
vol.Optional(ATTR_SSL, default=False): vol.Boolean(),
|
||||||
vol.Optional(ATTR_PORT, default=8123): NETWORK_PORT,
|
vol.Optional(ATTR_PORT, default=8123): NETWORK_PORT,
|
||||||
vol.Optional(ATTR_PASSWORD): vol.Any(None, vol.Coerce(str)),
|
vol.Optional(ATTR_PASSWORD): vol.Maybe(vol.Coerce(str)),
|
||||||
vol.Optional(ATTR_WATCHDOG, default=True): vol.Boolean(),
|
vol.Optional(ATTR_WATCHDOG, default=True): vol.Boolean(),
|
||||||
vol.Optional(ATTR_WAIT_BOOT, default=600):
|
vol.Optional(ATTR_WAIT_BOOT, default=600):
|
||||||
vol.All(vol.Coerce(int), vol.Range(min=60)),
|
vol.All(vol.Coerce(int), vol.Range(min=60)),
|
||||||
|
@ -1,8 +1,14 @@
|
|||||||
"""HomeAssistant control object."""
|
"""HomeAssistant control object."""
|
||||||
import logging
|
import logging
|
||||||
|
from pathlib import Path
|
||||||
|
from tempfile import TemporaryDirectory
|
||||||
|
|
||||||
|
import aiohttp
|
||||||
|
|
||||||
from .coresys import CoreSysAttributes
|
from .coresys import CoreSysAttributes
|
||||||
from .docker.supervisor import DockerSupervisor
|
from .docker.supervisor import DockerSupervisor
|
||||||
|
from .const import URL_HASSIO_APPARMOR
|
||||||
|
from .exceptions import HostAppArmorError
|
||||||
|
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
@ -46,6 +52,31 @@ class Supervisor(CoreSysAttributes):
|
|||||||
"""Return arch of hass.io containter."""
|
"""Return arch of hass.io containter."""
|
||||||
return self.instance.arch
|
return self.instance.arch
|
||||||
|
|
||||||
|
async def update_apparmor(self):
|
||||||
|
"""Fetch last version and update profile."""
|
||||||
|
url = URL_HASSIO_APPARMOR
|
||||||
|
try:
|
||||||
|
_LOGGER.info("Fetch AppArmor profile %s", url)
|
||||||
|
async with self.sys_websession.get(url, timeout=10) as request:
|
||||||
|
data = await request.text()
|
||||||
|
|
||||||
|
except aiohttp.ClientError as err:
|
||||||
|
_LOGGER.warning("Can't fetch AppArmor profile: %s", err)
|
||||||
|
return
|
||||||
|
|
||||||
|
with TemporaryDirectory(dir=self.sys_config.path_tmp) as tmp_dir:
|
||||||
|
profile_file = Path(tmp_dir, 'apparmor.txt')
|
||||||
|
try:
|
||||||
|
profile_file.write_text(data)
|
||||||
|
except OSError as err:
|
||||||
|
_LOGGER.error("Can't write temporary profile: %s", err)
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
await self.sys_host.apparmor.load_profile(
|
||||||
|
"hassio-supervisor", profile_file)
|
||||||
|
except HostAppArmorError:
|
||||||
|
_LOGGER.error("Can't update AppArmor profile!")
|
||||||
|
|
||||||
async def update(self, version=None):
|
async def update(self, version=None):
|
||||||
"""Update HomeAssistant version."""
|
"""Update HomeAssistant version."""
|
||||||
version = version or self.last_version
|
version = version or self.last_version
|
||||||
@ -56,6 +87,7 @@ class Supervisor(CoreSysAttributes):
|
|||||||
|
|
||||||
_LOGGER.info("Update supervisor to version %s", version)
|
_LOGGER.info("Update supervisor to version %s", version)
|
||||||
if await self.instance.install(version):
|
if await self.instance.install(version):
|
||||||
|
await self.update_apparmor()
|
||||||
self.sys_loop.call_later(1, self.sys_loop.stop)
|
self.sys_loop.call_later(1, self.sys_loop.stop)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
@ -41,7 +41,7 @@ class Tasks(CoreSysAttributes):
|
|||||||
self.jobs.add(self.sys_scheduler.register_task(
|
self.jobs.add(self.sys_scheduler.register_task(
|
||||||
self.sys_snapshots.reload, self.RUN_RELOAD_SNAPSHOTS))
|
self.sys_snapshots.reload, self.RUN_RELOAD_SNAPSHOTS))
|
||||||
self.jobs.add(self.sys_scheduler.register_task(
|
self.jobs.add(self.sys_scheduler.register_task(
|
||||||
self.sys_host.load, self.RUN_RELOAD_HOST))
|
self.sys_host.reload, self.RUN_RELOAD_HOST))
|
||||||
|
|
||||||
self.jobs.add(self.sys_scheduler.register_task(
|
self.jobs.add(self.sys_scheduler.register_task(
|
||||||
self._watchdog_homeassistant_docker,
|
self._watchdog_homeassistant_docker,
|
||||||
|
66
hassio/utils/apparmor.py
Normal file
66
hassio/utils/apparmor.py
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
"""Some functions around apparmor profiles."""
|
||||||
|
import logging
|
||||||
|
import re
|
||||||
|
|
||||||
|
from ..exceptions import AppArmorFileError, AppArmorInvalidError
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
RE_PROFILE = re.compile(r"^profile ([^ ]+).*$")
|
||||||
|
|
||||||
|
|
||||||
|
def get_profile_name(profile_file):
|
||||||
|
"""Read the profile name from file."""
|
||||||
|
profiles = set()
|
||||||
|
|
||||||
|
try:
|
||||||
|
with profile_file.open('r') as profile:
|
||||||
|
for line in profile:
|
||||||
|
match = RE_PROFILE.match(line)
|
||||||
|
if not match:
|
||||||
|
continue
|
||||||
|
profiles.add(match.group(1))
|
||||||
|
except OSError as err:
|
||||||
|
_LOGGER.error("Can't read apparmor profile: %s", err)
|
||||||
|
raise AppArmorFileError()
|
||||||
|
|
||||||
|
if len(profiles) != 1:
|
||||||
|
_LOGGER.error("To many profiles inside file: %s", profiles)
|
||||||
|
raise AppArmorInvalidError()
|
||||||
|
|
||||||
|
return profiles.pop()
|
||||||
|
|
||||||
|
|
||||||
|
def validate_profile(profile_file, profile_name):
|
||||||
|
"""Check if profile from file is valid with profile name."""
|
||||||
|
if profile_name == get_profile_name(profile_file):
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def adjust_profile(profile_file, profile_name, profile_new):
|
||||||
|
"""Fix the profile name."""
|
||||||
|
org_profile = get_profile_name(profile_file)
|
||||||
|
profile_data = []
|
||||||
|
|
||||||
|
# Process old data
|
||||||
|
try:
|
||||||
|
with profile_file.open('r') as profile:
|
||||||
|
for line in profile:
|
||||||
|
match = RE_PROFILE.match(line)
|
||||||
|
if not match:
|
||||||
|
profile_data.append(line)
|
||||||
|
else:
|
||||||
|
profile_data.append(
|
||||||
|
line.replace(org_profile, profile_name))
|
||||||
|
except OSError as err:
|
||||||
|
_LOGGER.error("Can't adjust origin profile: %s", err)
|
||||||
|
raise AppArmorFileError()
|
||||||
|
|
||||||
|
# Write into new file
|
||||||
|
try:
|
||||||
|
with profile_new.open('w') as profile:
|
||||||
|
profile.writelines(profile_data)
|
||||||
|
except OSError as err:
|
||||||
|
_LOGGER.error("Can't write new profile: %s", err)
|
||||||
|
raise AppArmorFileError()
|
@ -17,7 +17,7 @@ RE_REPOSITORY = re.compile(r"^(?P<url>[^#]+)(?:#(?P<branch>[\w\-]+))?$")
|
|||||||
NETWORK_PORT = vol.All(vol.Coerce(int), vol.Range(min=1, max=65535))
|
NETWORK_PORT = vol.All(vol.Coerce(int), vol.Range(min=1, max=65535))
|
||||||
WAIT_BOOT = vol.All(vol.Coerce(int), vol.Range(min=1, max=60))
|
WAIT_BOOT = vol.All(vol.Coerce(int), vol.Range(min=1, max=60))
|
||||||
DOCKER_IMAGE = vol.Match(r"^[\w{}]+/[\-\w{}]+$")
|
DOCKER_IMAGE = vol.Match(r"^[\w{}]+/[\-\w{}]+$")
|
||||||
ALSA_DEVICE = vol.Any(None, vol.Match(r"\d+,\d+"))
|
ALSA_DEVICE = vol.Maybe(vol.Match(r"\d+,\d+"))
|
||||||
CHANNELS = vol.In([CHANNEL_STABLE, CHANNEL_BETA, CHANNEL_DEV])
|
CHANNELS = vol.In([CHANNEL_STABLE, CHANNEL_BETA, CHANNEL_DEV])
|
||||||
|
|
||||||
|
|
||||||
@ -87,7 +87,7 @@ SCHEMA_HASS_CONFIG = vol.Schema({
|
|||||||
vol.Inclusive(ATTR_IMAGE, 'custom_hass'): DOCKER_IMAGE,
|
vol.Inclusive(ATTR_IMAGE, 'custom_hass'): DOCKER_IMAGE,
|
||||||
vol.Inclusive(ATTR_LAST_VERSION, 'custom_hass'): vol.Coerce(str),
|
vol.Inclusive(ATTR_LAST_VERSION, 'custom_hass'): vol.Coerce(str),
|
||||||
vol.Optional(ATTR_PORT, default=8123): NETWORK_PORT,
|
vol.Optional(ATTR_PORT, default=8123): NETWORK_PORT,
|
||||||
vol.Optional(ATTR_PASSWORD): vol.Any(None, vol.Coerce(str)),
|
vol.Optional(ATTR_PASSWORD): vol.Maybe(vol.Coerce(str)),
|
||||||
vol.Optional(ATTR_SSL, default=False): vol.Boolean(),
|
vol.Optional(ATTR_SSL, default=False): vol.Boolean(),
|
||||||
vol.Optional(ATTR_WATCHDOG, default=True): vol.Boolean(),
|
vol.Optional(ATTR_WATCHDOG, default=True): vol.Boolean(),
|
||||||
vol.Optional(ATTR_WAIT_BOOT, default=600):
|
vol.Optional(ATTR_WAIT_BOOT, default=600):
|
||||||
|
2
setup.py
2
setup.py
@ -42,7 +42,7 @@ setup(
|
|||||||
install_requires=[
|
install_requires=[
|
||||||
'attr==0.3.1',
|
'attr==0.3.1',
|
||||||
'async_timeout==3.0.0',
|
'async_timeout==3.0.0',
|
||||||
'aiohttp==3.2.1',
|
'aiohttp==3.3.2',
|
||||||
'docker==3.3.0',
|
'docker==3.3.0',
|
||||||
'colorlog==3.1.2',
|
'colorlog==3.1.2',
|
||||||
'voluptuous==0.11.1',
|
'voluptuous==0.11.1',
|
||||||
|
Loading…
x
Reference in New Issue
Block a user