Changeset - 41e53eb08518
[Not reviewed]
0 4 0
Branko Majic (branko) - 10 months ago 2025-02-05 03:22:21
branko@majic.rs
MAR-240: Tag dropped packet messages in system log (due to rate-limiting):

- Improves visibility of such messages, making it easier to
distinguish them from other firewall-related messages.
- Update documentation to mention that the messages are logged.
4 files changed with 40 insertions and 3 deletions:
0 comments (0 inline, 0 general)
docs/releasenotes.rst
Show inline comments
 
@@ -35,24 +35,29 @@ Upgraded to Ansible 10.4.x. Dropped support for Debian 11
 
* ``xmpp_server`` role
 

	
 
  * The role no longer officially supports older versions of TLS
 
    (TLSv1.1 and lower).
 

	
 
**New features/improvements**
 

	
 
* ``backup_client`` role
 

	
 
  * Switched to using Paramiko + SFTP backend (instead of pexpect +
 
    SFTP), which should improve the backup performance.
 

	
 
* ``common`` role
 

	
 
  * TCP packets dropped due to incoming connection rate-limiting are
 
    now logged with the ``RATELIMIT`` prefix in the system logs.
 

	
 
* ``ldap_server`` role
 

	
 
  * TLSv1.3 is now enabled by default (with RFC-defined mandatory
 
    ciphers), in addition to TLSv1.2.
 
  * Additional schema is enabled that introduces auxilliary object
 
    class ``optionalCountry`` with optional ``countryName / c`` and
 
    ``friendlyCountryName / co`` attributes. Useful for address books.
 

	
 
* ``mail_server`` role
 

	
 
  * TLSv1.3 is now enabled by default (with RFC-defined mandatory
 
    ciphers), in addition to TLSv1.2, for IMAP and SMTP submission.
docs/rolereference.rst
Show inline comments
 
@@ -252,25 +252,28 @@ The role implements the following:
 
* Hardens the SSH server by disabling remote ``root`` logins and password-based
 
  authentication.
 
* Allows traversing of directory ``/etc/ssl/private/`` to everyone. This lets
 
  you put TLS private keys in central location where any operating system user
 
  can reach them provided they have appropriate read/write rights on the file
 
  itself, and provided they know the exact path of the file.
 
* Deploys CA certificate files, normally used for truststore purposes, to
 
  ``/usr/local/share/ca-certificates/``.
 
* Installs ``ferm`` (for iptables management), configuring a basic firewall
 
  which allows ICMP echo requests (PING), incoming connection on TCP port 22
 
  (SSH), and also introduces rate-limitting for incoming ICMP echo request
 
  pacakges and (new) TCP connections. The rate-limitting is based on the source
 
  IP address, using the ``iptables hashlimit`` module.
 
  IP address, using the ``iptables hashlimit`` module. Incoming TCP packets
 
  that have been dropped due to rate-limiting are logged in the system log with
 
  message prefix ``RATELIMIT``. No logging is done for the dropped ICMP
 
  packets.
 
* Sets-up system for performing checks on certificates (currently only if they
 
  expire within less than 30 days). Roles that want their certificates checked
 
  should deploy a ``.conf`` to directory ``/etc/check_certificate/`` with paths
 
  to certificate files, one per line. Certificates are checked on
 
  daily basis, using crontab (resulting in failures being sent out to
 
  the ``root`` user).
 
* Deploys ``apticron`` package that performs checks for available package
 
  upgrades on daily basis. Mails are delivered to local ``root`` account, and
 
  can be redirected elsewhere via aliases. If using ``mail_forwarder`` or
 
  ``mail_server`` roles on the same server, aliases can be set-up through them.
 
* Sets-up system for performing checks on pip requirements
 
  files. Roles that want their requirements files checked should
roles/common/molecule/default/tests/test_default.py
Show inline comments
 
import ipaddress
 
import os
 
import re
 

	
 
import testinfra.utils.ansible_runner
 

	
 
import pytest
 

	
 

	
 
testinfra_hosts = testinfra.utils.ansible_runner.AnsibleRunner(
 
    os.environ['MOLECULE_INVENTORY_FILE']).get_hosts('parameters-*')
 

	
 

	
 
@@ -483,12 +484,40 @@ def test_legacy_iptables_not_present(host, iptables_family):
 
def test_legacy_iptables_removal_script(host):
 
    """
 
    Tests if the script for dropping legacy iptables rules has been
 
    deployed correctly.
 
    """
 

	
 
    script = host.file("/usr/local/sbin/drop_legacy_iptables_rules.sh")
 

	
 
    assert script.is_file
 
    assert script.user == "root"
 
    assert script.group == "root"
 
    assert script.mode == 0o755
 

	
 

	
 
@pytest.mark.parametrize('ip_protocol', [4, 6])
 
def test_tcp_rate_limit_dropped_packet_message_tagging(host, ip_protocol):
 
    """
 
    Tests log message tagging for dropped incoming TCP packets
 
    (due to rate-limiting).
 
    """
 

	
 
    ansible_runner = testinfra.utils.ansible_runner.AnsibleRunner(os.environ['MOLECULE_INVENTORY_FILE'])
 
    client = ansible_runner.get_host(ansible_runner.get_hosts('client-allowed')[0])
 

	
 
    hostname = host.run("hostname").stdout.strip()
 
    timestamp = host.run("date '+%b %d %H:%M:%S'").stdout.strip()
 
    ip_address = client.run("getent ahostsv%s %s", str(ip_protocol), hostname).stdout.strip().split("\n")[-1].split()[0]
 
    ip_address_exploded = ipaddress.ip_address(ip_address).exploded
 

	
 
    expected_message = re.compile(r"%s kernel: RATELIMIT .* DST=%s .* PROTO=TCP .* DPT=22" % (re.escape(hostname), re.escape(ip_address_exploded)))
 

	
 
    with host.sudo():
 

	
 
        # This should trigger the firewall rules and produce log entries.
 
        client.run("for i in $(seq 12); do nc.openbsd -%s -w 1 -z %s 22; done", str(ip_protocol), hostname)
 

	
 
        log = host.run("journalctl --dmesg --since %s", timestamp)
 

	
 
        assert log.rc == 0
 
        assert expected_message.search(log.stdout) is not None
roles/common/templates/00-base.conf.j2
Show inline comments
 
@@ -24,25 +24,25 @@ domain ip {
 
        # The flood chain is used for controlling the rate of the incoming connections.
 
        chain flood {
 
            # Rate-limit the ping requests.
 
            proto icmp icmp-type echo-request {
 
                mod hashlimit hashlimit {{ incoming_connection_limit }} hashlimit-burst {{ incoming_connection_limit_burst }}
 
                    hashlimit-mode srcip hashlimit-name icmp RETURN;
 
                DROP;
 
            }
 
            # Rate-limit the TCP connections.
 
            proto tcp tcp-flags (FIN SYN RST ACK) SYN {
 
                mod hashlimit hashlimit {{ incoming_connection_limit }} hashlimit-burst {{ incoming_connection_limit_burst }}
 
                    hashlimit-mode srcip hashlimit-name icmp RETURN;
 
                LOG;
 
                LOG log-prefix 'RATELIMIT ';
 
                DROP;
 
            }
 
        }
 
{% if maintenance %}
 
        # Resume processing for allowed source addresses, otherwise drop packets.
 
        chain allowed_sources {
 
            {% for source in maintenance_allowed_sources %}
 
                {% if source is ansible.utils.ipv4_address %}
 
            saddr {{ source }} RETURN;
 
                {% endif %}
 
            {% endfor %}
 
            DROP;
 
@@ -80,25 +80,25 @@ domain ip6 {
 
        # The flood chain is used for controlling the rate of the incoming connections.
 
        chain flood {
 
            # Rate-limit the ping requests.
 
            proto icmp icmp-type echo-request {
 
                mod hashlimit hashlimit {{ incoming_connection_limit }} hashlimit-burst {{ incoming_connection_limit_burst }}
 
                    hashlimit-mode srcip hashlimit-name icmp RETURN;
 
                DROP;
 
            }
 
            # Rate-limit the TCP connections.
 
            proto tcp tcp-flags (FIN SYN RST ACK) SYN {
 
                mod hashlimit hashlimit {{ incoming_connection_limit }} hashlimit-burst {{ incoming_connection_limit_burst }}
 
                    hashlimit-mode srcip hashlimit-name icmp RETURN;
 
                LOG;
 
                LOG log-prefix 'RATELIMIT ';
 
                DROP;
 
            }
 
        }
 
{% if maintenance %}
 
        # Resume processing for allowed source addresses, otherwise drop packets.
 
        chain allowed_sources {
 
            {% for source in maintenance_allowed_sources %}
 
                {% if source is ansible.utils.ipv6_address %}
 
            saddr {{ source }} RETURN;
 
                {% endif %}
 
            {% endfor %}
 
            DROP;
0 comments (0 inline, 0 general)