diff --git a/docs/releasenotes.rst b/docs/releasenotes.rst index 2a2279f6c7e346664b5b9aaca3ac5d55d7a97a92..2ce40fce8bb397fd066bd07a69b0e8352eb0095c 100644 --- a/docs/releasenotes.rst +++ b/docs/releasenotes.rst @@ -37,6 +37,11 @@ Dropped support for Debian 10 (Buster). 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. diff --git a/docs/rolereference.rst b/docs/rolereference.rst index 51089c160b5a55e28edd151b63337b4d6bf89f12..1d15010a572c6143a63d58b0b665e25826b78163 100644 --- a/docs/rolereference.rst +++ b/docs/rolereference.rst @@ -392,14 +392,15 @@ Parameters **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 diff --git a/docs/usage.rst b/docs/usage.rst index 5f007a2ea1fa1bd1c2f57af7ef38cdbd8fad3a1e..ef698d3fdbbf64cbda7d2a69aa54a319cb675220 100644 --- a/docs/usage.rst +++ b/docs/usage.rst @@ -147,7 +147,7 @@ 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 @@ -518,7 +518,7 @@ 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: diff --git a/requirements.in b/requirements.in index 0dd2bd6b5078463043d4b367aebee167b7ee6fef..b21ee10f6962924027ed7c6f0e7e09ce2137d0a1 100644 --- a/requirements.in +++ b/requirements.in @@ -3,11 +3,14 @@ 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 diff --git a/requirements.txt b/requirements.txt index b0941af05f24d5de77dd65b1e7bf2c7d5d61049d..5adcd6b10a8bcae883ab9e7691fbb4ea7e4a5082 100644 --- a/requirements.txt +++ b/requirements.txt @@ -54,6 +54,9 @@ 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 @@ -65,7 +68,7 @@ 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 @@ -96,16 +99,14 @@ jinja2==3.1.3 # 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 @@ -121,7 +122,7 @@ 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 @@ -152,8 +153,10 @@ pygments==2.17.2 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 @@ -181,8 +184,9 @@ 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 @@ -236,13 +240,13 @@ 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 diff --git a/roles/common/defaults/main.yml b/roles/common/defaults/main.yml index b8a6d9f48f4def5114344c263534f7a1ba1851bf..e2944342593c5fcd1eaf24cc69c6dbd0be8bb450 100644 --- a/roles/common/defaults/main.yml +++ b/roles/common/defaults/main.yml @@ -32,7 +32,7 @@ pip_check_requirements: - zipp==3.15.0 ntp_servers: [] maintenance: false -maintenance_allowed_hosts: [] +maintenance_allowed_sources: [] # Internal use only. prompt_colour_mapping: diff --git a/roles/common/molecule/default/group_vars/parameters-optional.yml b/roles/common/molecule/default/group_vars/parameters-optional.yml index 5501c5be31e304c2cf40558e0837ea9bc6ceaddf..67e22d6015ac7970b745fdad9249a0cff9a60f43 100644 --- a/roles/common/molecule/default/group_vars/parameters-optional.yml +++ b/roles/common/molecule/default/group_vars/parameters-optional.yml @@ -51,8 +51,9 @@ ntp_servers: - "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 diff --git a/roles/common/molecule/default/tests/test_maintenance_from_allowed_client.py b/roles/common/molecule/default/tests/test_maintenance_from_allowed_client.py index 21fcfab1017555cf95e0c80b0fee8018ee0c7bfe..5a0a2ee81e8e3f50f81947b8b52ff5287881b598 100644 --- a/roles/common/molecule/default/tests/test_maintenance_from_allowed_client.py +++ b/roles/common/molecule/default/tests/test_maintenance_from_allowed_client.py @@ -16,28 +16,30 @@ parameters_optional_hosts = testinfra.utils.ansible_runner.AnsibleRunner( @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 diff --git a/roles/common/molecule/default/tests/test_maintenance_from_disallowed_client.py b/roles/common/molecule/default/tests/test_maintenance_from_disallowed_client.py index 4b9a9877a98c942b48fe432db8e814bb2013852f..60e75b9ca9b0a1f31df84a490dadce69858841e7 100644 --- a/roles/common/molecule/default/tests/test_maintenance_from_disallowed_client.py +++ b/roles/common/molecule/default/tests/test_maintenance_from_disallowed_client.py @@ -16,42 +16,45 @@ parameters_optional_hosts = testinfra.utils.ansible_runner.AnsibleRunner( @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 diff --git a/roles/common/templates/00-base.conf.j2 b/roles/common/templates/00-base.conf.j2 index 435e61da2872948d22132b3367ea85712f0b5d6e..c51f83a0dc979c4ff3eedc6d6a46d6c64b02c5a4 100644 --- a/roles/common/templates/00-base.conf.j2 +++ b/roles/common/templates/00-base.conf.j2 @@ -1,3 +1,4 @@ +#jinja2:trim_blocks:True,lstrip_blocks:True # IPv4 domain ip { table filter { @@ -15,8 +16,8 @@ domain ip { 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 %} } @@ -37,11 +38,12 @@ domain ip { } } {% 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; } @@ -70,8 +72,8 @@ domain ip6 { 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 %} } @@ -92,12 +94,11 @@ domain ip6 { } } {% 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;