mirror of
https://github.com/home-assistant/supervisor.git
synced 2025-07-19 15:16:33 +00:00
Using AppArmor from OS-Agent (#3254)
* Using AppArmor from OS-Agent * cleanup
This commit is contained in:
parent
8653f7a0e1
commit
d80d76a24d
@ -736,7 +736,7 @@ class Addon(AddonModel):
|
|||||||
if self.sys_host.apparmor.exists(self.slug):
|
if self.sys_host.apparmor.exists(self.slug):
|
||||||
profile = temp_path.joinpath("apparmor.txt")
|
profile = temp_path.joinpath("apparmor.txt")
|
||||||
try:
|
try:
|
||||||
self.sys_host.apparmor.backup_profile(self.slug, profile)
|
await self.sys_host.apparmor.backup_profile(self.slug, profile)
|
||||||
except HostAppArmorError as err:
|
except HostAppArmorError as err:
|
||||||
raise AddonsError(
|
raise AddonsError(
|
||||||
"Can't backup AppArmor profile", _LOGGER.error
|
"Can't backup AppArmor profile", _LOGGER.error
|
||||||
|
@ -174,13 +174,20 @@ def initialize_system(coresys: CoreSys) -> None:
|
|||||||
_LOGGER.debug("Creating Supervisor share folder at '%s'", config.path_share)
|
_LOGGER.debug("Creating Supervisor share folder at '%s'", config.path_share)
|
||||||
config.path_share.mkdir()
|
config.path_share.mkdir()
|
||||||
|
|
||||||
# Apparmor folder
|
# Apparmor folders
|
||||||
if not config.path_apparmor.is_dir():
|
if not config.path_apparmor.is_dir():
|
||||||
_LOGGER.debug(
|
_LOGGER.debug(
|
||||||
"Creating Supervisor Apparmor folder at '%s'", config.path_apparmor
|
"Creating Supervisor Apparmor Profile folder at '%s'", config.path_apparmor
|
||||||
)
|
)
|
||||||
config.path_apparmor.mkdir()
|
config.path_apparmor.mkdir()
|
||||||
|
|
||||||
|
if not config.path_apparmor_cache.is_dir():
|
||||||
|
_LOGGER.debug(
|
||||||
|
"Creating Supervisor Apparmor Cache folder at '%s'",
|
||||||
|
config.path_apparmor_cache,
|
||||||
|
)
|
||||||
|
config.path_apparmor_cache.mkdir()
|
||||||
|
|
||||||
# DNS folder
|
# DNS folder
|
||||||
if not config.path_dns.is_dir():
|
if not config.path_dns.is_dir():
|
||||||
_LOGGER.debug("Creating Supervisor DNS folder at '%s'", config.path_dns)
|
_LOGGER.debug("Creating Supervisor DNS folder at '%s'", config.path_dns)
|
||||||
|
@ -42,6 +42,7 @@ BACKUP_DATA = PurePath("backup")
|
|||||||
SHARE_DATA = PurePath("share")
|
SHARE_DATA = PurePath("share")
|
||||||
TMP_DATA = PurePath("tmp")
|
TMP_DATA = PurePath("tmp")
|
||||||
APPARMOR_DATA = PurePath("apparmor")
|
APPARMOR_DATA = PurePath("apparmor")
|
||||||
|
APPARMOR_CACHE = PurePath("apparmor/cache")
|
||||||
DNS_DATA = PurePath("dns")
|
DNS_DATA = PurePath("dns")
|
||||||
AUDIO_DATA = PurePath("audio")
|
AUDIO_DATA = PurePath("audio")
|
||||||
MEDIA_DATA = PurePath("media")
|
MEDIA_DATA = PurePath("media")
|
||||||
@ -268,6 +269,21 @@ class CoreConfig(FileConfiguration):
|
|||||||
"""Return root Apparmor profile folder."""
|
"""Return root Apparmor profile folder."""
|
||||||
return Path(SUPERVISOR_DATA, APPARMOR_DATA)
|
return Path(SUPERVISOR_DATA, APPARMOR_DATA)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def path_apparmor_cache(self) -> Path:
|
||||||
|
"""Return root Apparmor cache folder."""
|
||||||
|
return Path(SUPERVISOR_DATA, APPARMOR_CACHE)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def path_extern_apparmor(self) -> Path:
|
||||||
|
"""Return root Apparmor profile folder external."""
|
||||||
|
return Path(self.path_extern_supervisor, APPARMOR_DATA)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def path_extern_apparmor_cache(self) -> Path:
|
||||||
|
"""Return root Apparmor cache folder external."""
|
||||||
|
return Path(self.path_extern_supervisor, APPARMOR_CACHE)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def path_extern_share(self) -> PurePath:
|
def path_extern_share(self) -> PurePath:
|
||||||
"""Return root share data folder external for Docker."""
|
"""Return root share data folder external for Docker."""
|
||||||
|
@ -194,8 +194,10 @@ class DockerAddon(DockerInterface):
|
|||||||
security = super().security_opt
|
security = super().security_opt
|
||||||
|
|
||||||
# AppArmor
|
# AppArmor
|
||||||
apparmor = self.sys_host.apparmor.available
|
if (
|
||||||
if not apparmor or self.addon.apparmor == SECURITY_DISABLE:
|
not self.sys_host.apparmor.available
|
||||||
|
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}")
|
||||||
|
@ -3,41 +3,37 @@ import logging
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
import shutil
|
import shutil
|
||||||
|
|
||||||
from ..coresys import CoreSysAttributes
|
from supervisor.resolution.const import UnsupportedReason
|
||||||
|
|
||||||
|
from ..coresys import CoreSys, CoreSysAttributes
|
||||||
from ..exceptions import DBusError, HostAppArmorError
|
from ..exceptions import DBusError, HostAppArmorError
|
||||||
from ..utils.apparmor import validate_profile
|
from ..utils.apparmor import validate_profile
|
||||||
|
from .const import HostFeature
|
||||||
|
|
||||||
_LOGGER: logging.Logger = logging.getLogger(__name__)
|
_LOGGER: logging.Logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
SYSTEMD_SERVICES = {"hassos-apparmor.service", "hassio-apparmor.service"}
|
|
||||||
|
|
||||||
|
|
||||||
class AppArmorControl(CoreSysAttributes):
|
class AppArmorControl(CoreSysAttributes):
|
||||||
"""Handle host AppArmor controls."""
|
"""Handle host AppArmor controls."""
|
||||||
|
|
||||||
def __init__(self, coresys):
|
def __init__(self, coresys: CoreSys):
|
||||||
"""Initialize host power handling."""
|
"""Initialize host power handling."""
|
||||||
self.coresys = coresys
|
self.coresys: CoreSys = coresys
|
||||||
self._profiles = set()
|
self._profiles: set[str] = set()
|
||||||
self._service = None
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def available(self):
|
def available(self) -> bool:
|
||||||
"""Return True if AppArmor is available on host."""
|
"""Return True if AppArmor is available on host."""
|
||||||
return self._service is not None
|
return (
|
||||||
|
HostFeature.OS_AGENT in self.sys_host.features
|
||||||
|
and UnsupportedReason.APPARMOR not in self.sys_resolution.unsupported
|
||||||
|
)
|
||||||
|
|
||||||
def exists(self, profile):
|
def exists(self, profile_name: str) -> bool:
|
||||||
"""Return True if a profile exists."""
|
"""Return True if a profile exists."""
|
||||||
return profile in self._profiles
|
return profile_name in self._profiles
|
||||||
|
|
||||||
async def _reload_service(self):
|
def _get_profile(self, profile_name: str) -> Path:
|
||||||
"""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."""
|
"""Get a profile from AppArmor store."""
|
||||||
if profile_name not in self._profiles:
|
if profile_name not in self._profiles:
|
||||||
raise HostAppArmorError(
|
raise HostAppArmorError(
|
||||||
@ -45,27 +41,26 @@ class AppArmorControl(CoreSysAttributes):
|
|||||||
)
|
)
|
||||||
return Path(self.sys_config.path_apparmor, profile_name)
|
return Path(self.sys_config.path_apparmor, profile_name)
|
||||||
|
|
||||||
async def load(self):
|
async def load(self) -> None:
|
||||||
"""Load available profiles."""
|
"""Load available profiles."""
|
||||||
for content in self.sys_config.path_apparmor.iterdir():
|
for content in self.sys_config.path_apparmor.iterdir():
|
||||||
if not content.is_file():
|
if not content.is_file():
|
||||||
continue
|
continue
|
||||||
self._profiles.add(content.name)
|
self._profiles.add(content.name)
|
||||||
|
|
||||||
# Is connected with systemd?
|
|
||||||
_LOGGER.info("Loading AppArmor Profiles: %s", self._profiles)
|
_LOGGER.info("Loading AppArmor Profiles: %s", self._profiles)
|
||||||
for service in SYSTEMD_SERVICES:
|
|
||||||
if not self.sys_host.services.exists(service):
|
|
||||||
continue
|
|
||||||
self._service = service
|
|
||||||
|
|
||||||
# Load profiles
|
# Load profiles
|
||||||
if self.available:
|
if self.available:
|
||||||
await self._reload_service()
|
for profile_name in self._profiles:
|
||||||
|
try:
|
||||||
|
await self._load_profile(profile_name)
|
||||||
|
except HostAppArmorError:
|
||||||
|
pass
|
||||||
else:
|
else:
|
||||||
_LOGGER.info("AppArmor is not enabled on host")
|
_LOGGER.warning("AppArmor is not enabled on host")
|
||||||
|
|
||||||
async def load_profile(self, profile_name, profile_file):
|
async def load_profile(self, profile_name: str, profile_file: Path) -> None:
|
||||||
"""Load/Update a new/exists profile into AppArmor."""
|
"""Load/Update a new/exists profile into AppArmor."""
|
||||||
if not validate_profile(profile_name, profile_file):
|
if not validate_profile(profile_name, profile_file):
|
||||||
raise HostAppArmorError(
|
raise HostAppArmorError(
|
||||||
@ -73,9 +68,9 @@ class AppArmorControl(CoreSysAttributes):
|
|||||||
)
|
)
|
||||||
|
|
||||||
# Copy to AppArmor folder
|
# Copy to AppArmor folder
|
||||||
dest_profile = Path(self.sys_config.path_apparmor, profile_name)
|
dest_profile: Path = Path(self.sys_config.path_apparmor, profile_name)
|
||||||
try:
|
try:
|
||||||
shutil.copyfile(profile_file, dest_profile)
|
await self.sys_run_in_executor(shutil.copyfile, profile_file, dest_profile)
|
||||||
except OSError as err:
|
except OSError as err:
|
||||||
raise HostAppArmorError(
|
raise HostAppArmorError(
|
||||||
f"Can't copy {profile_file}: {err}", _LOGGER.error
|
f"Can't copy {profile_file}: {err}", _LOGGER.error
|
||||||
@ -84,43 +79,60 @@ class AppArmorControl(CoreSysAttributes):
|
|||||||
# Load profiles
|
# Load profiles
|
||||||
_LOGGER.info("Adding/updating AppArmor profile: %s", profile_name)
|
_LOGGER.info("Adding/updating AppArmor profile: %s", profile_name)
|
||||||
self._profiles.add(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:
|
if not self.available:
|
||||||
try:
|
|
||||||
profile_file.unlink()
|
|
||||||
except OSError as err:
|
|
||||||
raise HostAppArmorError(
|
|
||||||
f"Can't remove profile: {err}", _LOGGER.error
|
|
||||||
) from err
|
|
||||||
return
|
return
|
||||||
|
|
||||||
# Marks als remove and start host process
|
await self._load_profile(profile_name)
|
||||||
remove_profile = Path(self.sys_config.path_apparmor, "remove", profile_name)
|
|
||||||
|
async def remove_profile(self, profile_name: str) -> None:
|
||||||
|
"""Remove a AppArmor profile."""
|
||||||
|
profile_file: Path = self._get_profile(profile_name)
|
||||||
|
|
||||||
|
# Unload if apparmor is enabled
|
||||||
|
if self.available:
|
||||||
|
await self._unload_profile(profile_name)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
profile_file.rename(remove_profile)
|
await self.sys_run_in_executor(profile_file.unlink)
|
||||||
except OSError as err:
|
except OSError as err:
|
||||||
raise HostAppArmorError(
|
raise HostAppArmorError(
|
||||||
f"Can't mark profile as remove: {err}", _LOGGER.error
|
f"Can't remove profile: {err}", _LOGGER.error
|
||||||
) from err
|
) from err
|
||||||
|
|
||||||
_LOGGER.info("Removing AppArmor profile: %s", profile_name)
|
_LOGGER.info("Removing AppArmor profile: %s", profile_name)
|
||||||
self._profiles.remove(profile_name)
|
self._profiles.remove(profile_name)
|
||||||
await self._reload_service()
|
|
||||||
|
|
||||||
def backup_profile(self, profile_name, backup_file):
|
async def backup_profile(self, profile_name: str, backup_file: Path) -> None:
|
||||||
"""Backup A profile into a new file."""
|
"""Backup A profile into a new file."""
|
||||||
profile_file = self._get_profile(profile_name)
|
profile_file: Path = self._get_profile(profile_name)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
shutil.copy(profile_file, backup_file)
|
await self.sys_run_in_executor(shutil.copy, profile_file, backup_file)
|
||||||
except OSError as err:
|
except OSError as err:
|
||||||
raise HostAppArmorError(
|
raise HostAppArmorError(
|
||||||
f"Can't backup profile {profile_name}: {err}", _LOGGER.error
|
f"Can't backup profile {profile_name}: {err}", _LOGGER.error
|
||||||
) from err
|
) from err
|
||||||
|
|
||||||
|
async def _load_profile(self, profile_name: str) -> None:
|
||||||
|
"""Load a profile on the host."""
|
||||||
|
try:
|
||||||
|
await self.sys_dbus.agent.apparmor.load_profile(
|
||||||
|
self.sys_config.path_extern_apparmor.joinpath(profile_name),
|
||||||
|
self.sys_config.path_apparmor_cache,
|
||||||
|
)
|
||||||
|
except DBusError as err:
|
||||||
|
raise HostAppArmorError(
|
||||||
|
f"Can't load profile {profile_name}: {err!s}", _LOGGER.error
|
||||||
|
) from err
|
||||||
|
|
||||||
|
async def _unload_profile(self, profile_name: str) -> None:
|
||||||
|
"""Unload a profile on the host."""
|
||||||
|
try:
|
||||||
|
await self.sys_dbus.agent.apparmor.unload_profile(
|
||||||
|
self.sys_config.path_extern_apparmor.joinpath(profile_name),
|
||||||
|
self.sys_config.path_apparmor_cache,
|
||||||
|
)
|
||||||
|
except DBusError as err:
|
||||||
|
raise HostAppArmorError(
|
||||||
|
f"Can't unload profile {profile_name}: {err!s}", _LOGGER.error
|
||||||
|
) from err
|
||||||
|
Loading…
x
Reference in New Issue
Block a user