Changeset - 736e06e7ffd6
[Not reviewed]
0 10 0
Branko Majic (branko) - 3 months ago 2024-02-18 14:04:25
branko@majic.rs
MAR-194: Use IP addresses instead names for maintenance allowed connections:

- Less ambigious. Solves problems around names being resolvable to
different IPs depending on what DNS server is used.
- Parameter renamed to better represent what is specified.
- Updated requirements to allow execution of ipv4/ipv6 filters.
- Pin the rich requirement to a lower version for compatibility
reasons.
- Implement tests for IPv6 connectivity tests.
- Improve rendering of base rules (indentation).
10 files changed with 65 insertions and 45 deletions:
0 comments (0 inline, 0 general)
docs/releasenotes.rst
Show inline comments
 
@@ -34,12 +34,17 @@ Dropped support for Debian 10 (Buster).
 

	
 
    The ``pip_check_requirements_py3`` /
 
    ``pip_check_requirements_py3_in`` role parameters have been
 
    renamed to ``pip_check_requirements`` /
 
    ``pip_check_requirements_in``.
 

	
 
  * Parameter ``maintenance_allowed_hosts`` has been dropped and
 
    replaced with parameter ``maintenance_allowed_sources``. The new
 
    parameter expects a list of IPv4 and IPv6 addresses (or
 
    subnets). Resolvable names can no longer be specified.
 

	
 
* ``wsgi_website`` role
 

	
 
  * Dropped support for Python 2.7. Only Python 3 is supported now.
 

	
 
    The ``python_version`` role parameter has been dropped. The
 
    ``python_interpreter`` parameter is still available, but it
docs/rolereference.rst
Show inline comments
 
@@ -389,20 +389,21 @@ Parameters
 
  higher than ``incoming_connection_limit``), even if it would go above the
 
  specified connection limit.
 

	
 
**maintenance** (boolean, optional, ``False``)
 
  Specifies if maintenance mode should be enabled or not. In
 
  maintenance mode incoming TCP connections are allowed only from
 
  explicitly listed hosts (see ``maintenance_allowed_hosts``
 
  explicitly listed hosts (see ``maintenance_allowed_sources``
 
  parameter). All ports are covered by this rule, with sole exception
 
  being the TCP port 22 (SSH). The SSH port is never blocked via
 
  maintenance mode.
 

	
 
**maintenance_allowed_hosts** (list, optional,  ``[]``)
 
  List of hosts that should be allowed to connect to the server when
 
  in maintenance mode.
 
**maintenance_allowed_sources** (list, optional,  ``[]``)
 
  List of source addreses (IPv4 or IPv6) that should be allowed to
 
  connect to the server when in maintenance mode. Subnets can be
 
  specified as well.
 

	
 
**ntp_servers** (list, optional, ``[]``)
 
  List of NTP servers to use for synchronising the time on managed
 
  machine using NTP. If no time synchronisation should be set-up, set
 
  to empty list. Default is not to configure time synchronisation.
 

	
docs/usage.rst
Show inline comments
 
@@ -144,13 +144,13 @@ packages, and to prepare the environment a bit on the Ansible server:
 

	
 
   ::
 

	
 
     mkdir ~/mysite/
 
     mkvirtualenv -p /usr/bin/python3 -a ~/mysite/ mysite
 
     pip install -U pip setuptools
 
     pip install 'ansible~=2.9.0' dnspython
 
     pip install 'ansible~=2.9.0' dnspython netaddr
 

	
 
.. warning::
 
   The ``dnspython`` package is important since it is used internally via
 
   ``dig`` lookup plugin.
 

	
 

	
 
@@ -515,13 +515,13 @@ Each server needs to share some common configuration in order to be functioning
 
properly. This includes set-up of some shared accounts, perhaps some hardening
 
etc.
 

	
 
.. note::
 
   Should you ever need to limit what hosts can connect to a server
 
   for some kind of maintenance or upgrade purposes, the ``common``
 
   role comes with ``maintenance`` and ``maintenance_allowed_hosts``
 
   role comes with ``maintenance`` and ``maintenance_allowed_sources``
 
   parameters. See :ref:`rolereference` for more information.
 

	
 
Let's take care of this common configuration right away:
 

	
 
1. Create playbook for the communications server:
 

	
requirements.in
Show inline comments
 
ansible~=2.9.0
 
defusedxml
 
dnspython
 
gimmecert~=0.5.0
 
molecule~=2.22.0
 
netaddr
 
paramiko
 
pip
 
pip-tools
 
python-ldap
 
python-vagrant
 
# @TODO: Required for ansible-lint due to breaking changes in newer version.
 
rich<11.0.0
 
setuptools
 
sh~=1.14.0
 
sphinx~=1.7.0
 
sphinx_rtd_theme~=0.4.0
requirements.txt
Show inline comments
 
@@ -51,24 +51,27 @@ click==8.1.7
 
click-completion==0.5.2
 
    # via molecule
 
colorama==0.4.6
 
    # via
 
    #   molecule
 
    #   python-gilt
 
    #   rich
 
commonmark==0.9.1
 
    # via rich
 
cookiecutter==2.5.0
 
    # via molecule
 
cryptography==3.2.1
 
    # via
 
    #   ansible
 
    #   gimmecert
 
    #   paramiko
 
defusedxml==0.7.1
 
    # via -r requirements.in
 
distlib==0.3.8
 
    # via virtualenv
 
dnspython==2.5.0
 
dnspython==2.6.0
 
    # via -r requirements.in
 
docutils==0.20.1
 
    # via sphinx
 
exceptiongroup==1.2.0
 
    # via pytest
 
fasteners==0.19
 
@@ -93,22 +96,20 @@ jinja2==3.1.3
 
    # via
 
    #   ansible
 
    #   click-completion
 
    #   cookiecutter
 
    #   molecule
 
    #   sphinx
 
markdown-it-py==3.0.0
 
    # via rich
 
markupsafe==2.1.5
 
    # via jinja2
 
mccabe==0.7.0
 
    # via flake8
 
mdurl==0.1.2
 
    # via markdown-it-py
 
molecule==2.22
 
    # via -r requirements.in
 
netaddr==1.2.1
 
    # via -r requirements.in
 
nodeenv==1.8.0
 
    # via pre-commit
 
packaging==23.2
 
    # via
 
    #   build
 
    #   pytest
 
@@ -118,13 +119,13 @@ paramiko==2.12.0
 
    #   -r requirements.in
 
    #   molecule
 
pathspec==0.12.1
 
    # via yamllint
 
pexpect==4.9.0
 
    # via molecule
 
pip-tools==7.3.0
 
pip-tools==7.4.0
 
    # via -r requirements.in
 
platformdirs==4.2.0
 
    # via virtualenv
 
pluggy==1.4.0
 
    # via pytest
 
pre-commit==1.21.0
 
@@ -149,14 +150,16 @@ pygments==2.17.2
 
    # via
 
    #   rich
 
    #   sphinx
 
pynacl==1.5.0
 
    # via paramiko
 
pyproject-hooks==1.0.0
 
    # via build
 
pytest==8.0.0
 
    # via
 
    #   build
 
    #   pip-tools
 
pytest==8.0.1
 
    # via testinfra
 
python-dateutil==2.8.2
 
    # via
 
    #   arrow
 
    #   gimmecert
 
python-gilt==1.2.3
 
@@ -178,14 +181,15 @@ pyyaml==5.4.1
 
    #   python-gilt
 
    #   yamllint
 
requests==2.31.0
 
    # via
 
    #   cookiecutter
 
    #   sphinx
 
rich==13.7.0
 
rich==10.16.2
 
    # via
 
    #   -r requirements.in
 
    #   ansible-lint
 
    #   cookiecutter
 
ruamel-yaml==0.18.6
 
    # via ansible-lint
 
ruamel-yaml-clib==0.2.8
 
    # via ruamel-yaml
 
@@ -233,19 +237,19 @@ tomli==2.0.1
 
    #   pyproject-hooks
 
    #   pytest
 
tree-format==0.1.2
 
    # via molecule
 
types-python-dateutil==2.8.19.20240106
 
    # via arrow
 
urllib3==2.2.0
 
urllib3==2.2.1
 
    # via requests
 
virtualenv==20.25.0
 
    # via pre-commit
 
wheel==0.42.0
 
    # via pip-tools
 
yamllint==1.35.0
 
yamllint==1.35.1
 
    # via molecule
 
zipp==3.17.0
 
    # via importlib-metadata
 

	
 
# The following packages are considered to be unsafe in a requirements file:
 
pip==24.0
roles/common/defaults/main.yml
Show inline comments
 
@@ -29,13 +29,13 @@ pip_check_requirements:
 
  - tomli==2.0.1
 
  - typing-extensions==4.7.1
 
  - wheel==0.41.3
 
  - zipp==3.15.0
 
ntp_servers: []
 
maintenance: false
 
maintenance_allowed_hosts: []
 
maintenance_allowed_sources: []
 

	
 
# Internal use only.
 
prompt_colour_mapping:
 
  black: "0;30"
 
  red: "0;31"
 
  green: "0;32"
roles/common/molecule/default/group_vars/parameters-optional.yml
Show inline comments
 
@@ -48,14 +48,15 @@ prompt_id: test
 
# overriding the default configuration.
 
ntp_servers:
 
  - "0.debian.pool.ntp.org"
 
  - "1.debian.pool.ntp.org"
 
  - "2.debian.pool.ntp.org"
 
maintenance: true
 
maintenance_allowed_hosts:
 
  - client1
 
maintenance_allowed_sources:
 
  - 192.168.56.3  # client1
 
  - fd00::192:168:56:3  # client1
 
pip_check_requirements_in:
 
  - pip >= 0.3.1
 
  - pip-tools >= 0.3.2
 
  - setuptools >= 0.3.3
 
  - wheel >= 0.3.4
 

	
roles/common/molecule/default/tests/test_maintenance_from_allowed_client.py
Show inline comments
 
@@ -13,31 +13,33 @@ parameters_mandatory_hosts = testinfra.utils.ansible_runner.AnsibleRunner(
 

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

	
 

	
 
@pytest.mark.parametrize("target_host", parameters_mandatory_hosts + parameters_optional_hosts)
 
def test_ssh_connectivity(host, target_host):
 
@pytest.mark.parametrize("ip_protocol", [4, 6])
 
def test_ssh_connectivity(host, target_host, ip_protocol):
 
    """
 
    Test if SSH server is reachable.
 
    """
 

	
 
    with host.sudo():
 

	
 
        scan = host.run('nmap -p 22 -oG - %s', target_host)
 
        scan = host.run('nmap -%s -p 22 -oG - %s', str(ip_protocol), target_host)
 

	
 
        assert scan.rc == 0
 
        assert "Ports: 22/open/tcp//ssh" in scan.stdout
 

	
 

	
 
@pytest.mark.parametrize("target_host", parameters_mandatory_hosts + parameters_optional_hosts)
 
def test_http_connectivity(host, target_host):
 
@pytest.mark.parametrize("ip_protocol", [4, 6])
 
def test_http_connectivity(host, target_host, ip_protocol):
 
    """
 
    Test if HTTP server is reachable.
 
    """
 

	
 
    with host.sudo():
 

	
 
        scan = host.run('nmap -p 80 -oG - %s', target_host)
 
        scan = host.run('nmap -%s -p 80 -oG - %s', str(ip_protocol), target_host)
 

	
 
        assert scan.rc == 0
 
        assert "Ports: 80/open/tcp//http" in scan.stdout
roles/common/molecule/default/tests/test_maintenance_from_disallowed_client.py
Show inline comments
 
@@ -13,45 +13,48 @@ parameters_mandatory_hosts = testinfra.utils.ansible_runner.AnsibleRunner(
 

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

	
 

	
 
@pytest.mark.parametrize("target_host", parameters_mandatory_hosts + parameters_optional_hosts)
 
def test_ssh_connectivity(host, target_host):
 
@pytest.mark.parametrize("ip_protocol", [4, 6])
 
def test_ssh_connectivity(host, target_host, ip_protocol):
 
    """
 
    Test if SSH server is reachable.
 
    """
 

	
 
    with host.sudo():
 

	
 
        scan = host.run('nmap -p 22 -oG - %s', target_host)
 
        scan = host.run('nmap -%s -p 22 -oG - %s', str(ip_protocol), target_host)
 

	
 
        assert scan.rc == 0
 
        assert "Ports: 22/open/tcp//ssh" in scan.stdout
 

	
 

	
 
@pytest.mark.parametrize("target_host", parameters_mandatory_hosts)
 
def test_http_connectivity_allowed(host, target_host):
 
@pytest.mark.parametrize("ip_protocol", [4, 6])
 
def test_http_connectivity_allowed(host, target_host, ip_protocol):
 
    """
 
    Test if HTTP server is reachable.
 
    """
 

	
 
    with host.sudo():
 

	
 
        scan = host.run('nmap -p 80 -oG - %s', target_host)
 
        scan = host.run('nmap -%s -p 80 -oG - %s', str(ip_protocol), target_host)
 

	
 
        assert scan.rc == 0
 
        assert "Ports: 80/open/tcp//http" in scan.stdout
 

	
 

	
 
@pytest.mark.parametrize("target_host", parameters_optional_hosts)
 
def test_http_connectivity_disallowed(host, target_host):
 
@pytest.mark.parametrize("ip_protocol", [4, 6])
 
def test_http_connectivity_disallowed(host, target_host, ip_protocol):
 
    """
 
    Test if HTTP server is reachable.
 
    """
 

	
 
    with host.sudo():
 

	
 
        scan = host.run('nmap -p 80 -oG - %s', target_host)
 
        scan = host.run('nmap -%s -p 80 -oG - %s', str(ip_protocol), target_host)
 

	
 
        assert scan.rc == 0
 
        assert "Ports: 80/filtered/tcp//http" in scan.stdout
roles/common/templates/00-base.conf.j2
Show inline comments
 
#jinja2:trim_blocks:True,lstrip_blocks:True
 
# IPv4
 
domain ip {
 
    table filter {
 
        chain INPUT {
 
            policy DROP;
 
            interface lo ACCEPT;
 
@@ -12,14 +13,14 @@ domain ip {
 
            # of established and related connections.
 
            proto tcp tcp-flags (FIN SYN RST ACK) SYN jump flood;
 
            # Accept some common incoming connections.
 
            proto icmp icmp-type echo-request ACCEPT;
 
            proto tcp dport 22 ACCEPT;
 
{% if maintenance %}
 
            # Validate source IP against list of allowed hosts in maintenance mode.
 
            jump allowed_hosts;
 
            # Validate source IP against list of allowed source addresses in maintenance mode.
 
            jump allowed_sources;
 
{% endif %}
 
        }
 

	
 
        # The flood chain is used for controlling the rate of the incoming connections.
 
        chain flood {
 
            # Rate-limit the ping requests.
 
@@ -34,17 +35,18 @@ domain ip {
 
                    hashlimit-mode srcip hashlimit-name icmp RETURN;
 
                LOG;
 
                DROP;
 
            }
 
        }
 
{% if maintenance %}
 
        # Resume processing in case of allowed hosts, drop packets for
 
        # any other hosts.
 
        chain allowed_hosts {
 
            {% for host in maintenance_allowed_hosts %}
 
            saddr {{ host }} RETURN;
 
        # Resume processing for allowed source addresses, otherwise drop packets.
 
        chain allowed_sources {
 
            {% for source in maintenance_allowed_sources %}
 
                {% if source | ipv4 %}
 
            saddr {{ source }} RETURN;
 
                {% endif %}
 
            {% endfor %}
 
            DROP;
 
        }
 
{% endif %}
 
    }
 
}
 
@@ -67,14 +69,14 @@ domain ip6 {
 
            proto icmp icmp-type neighbor-solicitation ACCEPT;
 
            proto icmp icmp-type neighbor-advertisement ACCEPT;
 
            # Accept some common incoming connections.
 
            proto icmp icmp-type echo-request ACCEPT;
 
            proto tcp dport 22 ACCEPT;
 
{% if maintenance %}
 
            # Validate source IP against list of allowed hosts in maintenance mode.
 
            jump allowed_hosts;
 
            # Validate source IP against list of allowed source addresses in maintenance mode.
 
            jump allowed_sources;
 
{% endif %}
 
        }
 

	
 
        # The flood chain is used for controlling the rate of the incoming connections.
 
        chain flood {
 
            # Rate-limit the ping requests.
 
@@ -89,18 +91,17 @@ domain ip6 {
 
                    hashlimit-mode srcip hashlimit-name icmp RETURN;
 
                LOG;
 
                DROP;
 
            }
 
        }
 
{% if maintenance %}
 
        # Resume processing in case of allowed hosts, drop packets for
 
        # any other hosts.
 
        chain allowed_hosts {
 
            {% for host in maintenance_allowed_hosts %}
 
                {% if lookup('dig', host + '/AAAA') not in ['NXDOMAIN', ''] %}
 
            saddr {{ host }} RETURN;
 
        # Resume processing for allowed source addresses, otherwise drop packets.
 
        chain allowed_sources {
 
            {% for source in maintenance_allowed_sources %}
 
                {% if source | ipv6 %}
 
            saddr {{ source }} RETURN;
 
                {% endif %}
 
            {% endfor %}
 
            DROP;
 
        }
 
{% endif %}
 
    }
0 comments (0 inline, 0 general)