diff --git a/plinth/modules/firewall/__init__.py b/plinth/modules/firewall/__init__.py index 9603b8f06..4a66ba641 100644 --- a/plinth/modules/firewall/__init__.py +++ b/plinth/modules/firewall/__init__.py @@ -45,7 +45,7 @@ class FirewallApp(app_module.App): app_id = 'firewall' - _version = 2 + _version = 3 can_be_disabled = False diff --git a/plinth/modules/firewall/privileged.py b/plinth/modules/firewall/privileged.py index 56592cdc1..6bf4d9ebd 100644 --- a/plinth/modules/firewall/privileged.py +++ b/plinth/modules/firewall/privileged.py @@ -62,6 +62,63 @@ def set_firewall_backend(backend): action_utils.service_restart('firewalld') +def _setup_local_service_protection(): + """Create the basic set of direct rules for protecting local services. + + Local service protection means that only administrators and Apache web + server should be able to access certain services and not other users who + have logged into the system. This is needed because some of the services + are protected with authentication and authorization provided by Apache web + server. If services are contacted directly then auth can be bypassed by all + local users. + + Firewalld does not have a mechanism to do this directly but it allows + inserting 'direct' rules into firewall. nftables is our default backend by + 'direct' rules always invoke 'ip(6)tables' commands. Luckily, ip(6)tables + are compatibility wrappers provided by nftables. Hence we must use iptables + syntax even though we deal with nftables. + + In nftables, there is no direct way to write the blocking rules. To deal + with traffic for incoming services, we have to write the rules an 'input' + chain. However, this chain does not have the information about the user who + originated this traffic. Only the 'output' chain has this information. This + may be fixed in the future. See: + https://github.com/firewalld/firewalld/issues/725 + + Our workaround for the situation is to mark the packets in the 'output' + chain and then use that wmark in the 'input'. Since we have a fixed set of + users want to allow, a single bit in the 32bit 'mark' property of the + packet is sufficient. + """ + + def _run_firewall_cmd(args): + subprocess.run(args, stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, check=True) + + def _add_rule(permanent, *rule): + try: + _run_firewall_cmd(['firewall-cmd'] + permanent + + ['--direct', '--query-passthrough'] + list(rule)) + except subprocess.CalledProcessError: + _run_firewall_cmd(['firewall-cmd'] + permanent + + ['--direct', '--add-passthrough'] + list(rule)) + + for permanent in [[], ['--permanent']]: + for ip_type in ['ipv4', 'ipv6']: + for owner_type in ['--uid-owner', '--gid-owner']: + for user_group in ['root', 'www-data']: + _add_rule(permanent, ip_type, '-A', 'OUTPUT', '-m', + 'owner', owner_type, user_group, '-j', 'MARK', + '--or-mark', '0x800000') + + for permanent in [[], ['--permanent']]: + for ip_type in ['ipv4', 'ipv6']: + _add_rule(permanent, ip_type, '-A', 'INPUT', '-m', 'conntrack', + '--ctstate', 'ESTABLISHED,RELATED', '-j', 'ACCEPT') + _add_rule(permanent, ip_type, '-A', 'INPUT', '-m', 'mark', + '--mark', '0x800000/0x800000', '-j', 'ACCEPT') + + @privileged def setup(): """Perform basic firewalld setup.""" @@ -70,3 +127,5 @@ def setup(): stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, check=True) set_firewall_backend('nftables') + + _setup_local_service_protection()