Changeset - 736e06e7ffd6
[Not reviewed]
0 10 0
Branko Majic (branko) - 2 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
 
@@ -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.
docs/rolereference.rst
Show inline comments
 
@@ -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
docs/usage.rst
Show inline comments
 
@@ -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:
requirements.in
Show inline comments
 
@@ -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
requirements.txt
Show inline comments
 
@@ -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
roles/common/defaults/main.yml
Show inline comments
 
@@ -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:
roles/common/molecule/default/group_vars/parameters-optional.yml
Show inline comments
 
@@ -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
roles/common/molecule/default/tests/test_maintenance_from_allowed_client.py
Show inline comments
 
@@ -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
roles/common/molecule/default/tests/test_maintenance_from_disallowed_client.py
Show inline comments
 
@@ -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
roles/common/templates/00-base.conf.j2
Show inline comments
 
#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;
0 comments (0 inline, 0 general)