From 80a203bd238cba9591e8cd0a4725284a919033c3 Mon Sep 17 00:00:00 2001 From: Sunil Mohan Adapa Date: Tue, 12 Jul 2022 13:15:56 -0700 Subject: [PATCH] config: Add option to set logging mode: none/volatile/persistent - None disables logging altogether. This is useful when we want to prevent FreedomBox from collecting IP addresses of visitors and other sensitive information. - Volatile logs are kept in RAM until the system is rebooted. Only 5% of RAM will be used at most and only 2 days worth of logs are kept. - Permanent will store logs into /var/log/journal. systemd-journald defaults will apply. 10% of disk capacity is used at most, capped at 4GiB. Also logging will stop if free space is below 15%. Maximum of 100 files are kept. No time based cleanup is done. Tests: - Set the logging mode to disabled. Observe that `journalctl -f` does not show any logs (say when performing plinth actions). - Set the logging mode to volatile. Observe that `journalctl` shows that logging is set to /run/log/journal/ and 5% of available memory is set as maximum. - Set the logging mode to persistent. Observe that `journalctl` shows that logging is set to /var/log/journal/ and 10% of disk space is set as maximum. Signed-off-by: Sunil Mohan Adapa Reviewed-by: James Valleroy --- plinth/modules/config/forms.py | 12 +++++ plinth/modules/config/privileged.py | 58 ++++++++++++++++++++++ plinth/modules/config/tests/test_config.py | 12 +++-- plinth/modules/config/views.py | 11 ++++ 4 files changed, 89 insertions(+), 4 deletions(-) create mode 100644 plinth/modules/config/privileged.py diff --git a/plinth/modules/config/forms.py b/plinth/modules/config/forms.py index 1b57e41fb..a35044823 100644 --- a/plinth/modules/config/forms.py +++ b/plinth/modules/config/forms.py @@ -99,3 +99,15 @@ class ConfigurationForm(forms.Form): help_text=gettext_lazy( 'Show apps and features that require more technical ' 'knowledge.')) + + logging_mode = forms.ChoiceField( + label=gettext_lazy('System-wide logging'), + choices=(('none', gettext_lazy('Disable logging, for privacy')), + ('volatile', + gettext_lazy('Keep some in memory until a restart, ' + 'for performance')), + ('persistent', + gettext_lazy('Write to disk, useful for debugging'))), + help_text=gettext_lazy( + 'Logs contain information about who accessed the system and debug ' + 'information from various services')) diff --git a/plinth/modules/config/privileged.py b/plinth/modules/config/privileged.py new file mode 100644 index 000000000..264f668ae --- /dev/null +++ b/plinth/modules/config/privileged.py @@ -0,0 +1,58 @@ +# SPDX-License-Identifier: AGPL-3.0-or-later +"""Configure miscellaneous system settings.""" + +import pathlib + +import augeas + +from plinth import action_utils +from plinth.actions import privileged + +JOURNALD_FILE = pathlib.Path('/etc/systemd/journald.conf.d/50-freedombox.conf') + + +def load_augeas(): + """Initialize Augeas.""" + aug = augeas.Augeas(flags=augeas.Augeas.NO_LOAD + + augeas.Augeas.NO_MODL_AUTOLOAD) + aug.transform('Puppet', str(JOURNALD_FILE)) + aug.set('/augeas/context', '/files' + str(JOURNALD_FILE)) + aug.load() + return aug + + +def get_logging_mode() -> str: + """Return the logging mode as none, volatile or persistent.""" + aug = load_augeas() + storage = aug.get('Journal/Storage') + if storage in ('volatile', 'persistent', 'none'): + return storage + + # journald's default is 'auto'. On Debian systems, 'auto' is same + # 'persistent' because /var/log/journal exists by default. + return 'persistent' + + +@privileged +def set_logging_mode(mode: str) -> None: + """Set the current logging mode.""" + if mode not in ('volatile', 'persistent', 'none'): + raise ValueError('Invalid mode') + + aug = load_augeas() + aug.set('Journal/Storage', mode) + if mode == 'volatile': + aug.set('Journal/RuntimeMaxUse', '5%') + aug.set('Journal/MaxFileSec', '6h') + aug.set('Journal/MaxRetentionSec', '2day') + else: + aug.remove('Journal/RuntimeMaxUse') + aug.remove('Journal/MaxFileSec') + aug.remove('Journal/MaxRetentionSec') + + JOURNALD_FILE.parent.mkdir(exist_ok=True) + aug.save() + + # systemd-journald is socket activated, it may not be running and it does + # not support reload. + action_utils.service_try_restart('systemd-journald') diff --git a/plinth/modules/config/tests/test_config.py b/plinth/modules/config/tests/test_config.py index 378626a4b..265512e25 100644 --- a/plinth/modules/config/tests/test_config.py +++ b/plinth/modules/config/tests/test_config.py @@ -29,14 +29,16 @@ def test_hostname_field(): for hostname in valid_hostnames: form = ConfigurationForm({ 'hostname': hostname, - 'domainname': 'example.com' + 'domainname': 'example.com', + 'logging_mode': 'volatile' }) assert form.is_valid() for hostname in invalid_hostnames: form = ConfigurationForm({ 'hostname': hostname, - 'domainname': 'example.com' + 'domainname': 'example.com', + 'logging_mode': 'volatile' }) assert not form.is_valid() @@ -57,14 +59,16 @@ def test_domainname_field(): for domainname in valid_domainnames: form = ConfigurationForm({ 'hostname': 'example', - 'domainname': domainname + 'domainname': domainname, + 'logging_mode': 'volatile' }) assert form.is_valid() for domainname in invalid_domainnames: form = ConfigurationForm({ 'hostname': 'example', - 'domainname': domainname + 'domainname': domainname, + 'logging_mode': 'volatile' }) assert not form.is_valid() diff --git a/plinth/modules/config/views.py b/plinth/modules/config/views.py index ceddffad2..1e5599022 100644 --- a/plinth/modules/config/views.py +++ b/plinth/modules/config/views.py @@ -13,6 +13,7 @@ from plinth.modules import config from plinth.signals import (domain_added, domain_removed, post_hostname_change, pre_hostname_change) +from . import privileged from .forms import ConfigurationForm LOGGER = logging.getLogger(__name__) @@ -30,6 +31,7 @@ class ConfigAppView(views.AppView): 'domainname': config.get_domainname(), 'homepage': config.get_home_page(), 'advanced_mode': config.get_advanced_mode(), + 'logging_mode': privileged.get_logging_mode(), } def form_valid(self, form): @@ -37,6 +39,8 @@ class ConfigAppView(views.AppView): old_status = form.initial new_status = form.cleaned_data + is_changed = False + if old_status['hostname'] != new_status['hostname']: try: set_hostname(new_status['hostname']) @@ -87,6 +91,13 @@ class ConfigAppView(views.AppView): messages.success(self.request, _('Hiding advanced apps and features')) + if old_status['logging_mode'] != new_status['logging_mode']: + privileged.set_logging_mode(new_status['logging_mode']) + is_changed = True + + if is_changed: + messages.success(self.request, _('Configuration updated')) + return super(views.AppView, self).form_valid(form)