lorax-composer: Add firewall support to blueprints
You can now open ports in the firewall, using port numbers or service
names:
    [customizations.firewall]
    ports = ["22:tcp", "80:tcp", "imap:tcp", "53:tcp", "53:udp"]
Or enable/disable services registered with firewalld:
     [customizations.firewall.services]
     enabled = ["ftp", "ntp", "dhcp"]
     disabled = ["telnet"]
If the template contains firewall --disabled it cannot be overridden,
under the assumption that it is required for the image to boot in the
selected environment.
(cherry picked from commit 4d35668ab5)
Related: rhbz#1718473
			
			
This commit is contained in:
		
							parent
							
								
									a9c5581aa9
								
							
						
					
					
						commit
						780f00d81a
					
				@ -281,9 +281,8 @@ By default the firewall blocks all access except for services that enable their
 | 
			
		||||
like ``sshd``. This command can be used to open other ports or services. Ports are configured using
 | 
			
		||||
the port:protocol format::
 | 
			
		||||
 | 
			
		||||
    [customizations.firewall.ports]
 | 
			
		||||
    enabled = ["80:tcp", "imap:tcp", "53:tcp", "53:udp"]
 | 
			
		||||
    disabled = ["23:tcp", "mysql:tcp"]
 | 
			
		||||
    [customizations.firewall]
 | 
			
		||||
    ports = ["22:tcp", "80:tcp", "imap:tcp", "53:tcp", "53:udp"]
 | 
			
		||||
 | 
			
		||||
Numeric ports, or their names from ``/etc/services`` can be used in the ``ports`` enabled/disabled lists.
 | 
			
		||||
 | 
			
		||||
@ -292,12 +291,15 @@ in a ``customizations.firewall.services`` section::
 | 
			
		||||
 | 
			
		||||
    [customizations.firewall.services]
 | 
			
		||||
    enabled = ["ftp", "ntp", "dhcp"]
 | 
			
		||||
    disabled = ["telnet"]
 | 
			
		||||
 | 
			
		||||
Note that these  are different from the names in ``/etc/services``, and only ``enabled`` is supported.
 | 
			
		||||
 | 
			
		||||
Both are optional, if they are not used leave them out or set them to an empty list ``[]``. If you
 | 
			
		||||
only want the default firewall setup this section can be omitted from the blueprint.
 | 
			
		||||
 | 
			
		||||
NOTE: The ``Google`` and ``OpenStack`` templates explicitly disable the firewall for their environment.
 | 
			
		||||
This cannot be overridden by the blueprint.
 | 
			
		||||
 | 
			
		||||
[customizations.services]
 | 
			
		||||
*************************
 | 
			
		||||
 | 
			
		||||
@ -287,6 +287,53 @@ def get_keyboard_layout(recipe):
 | 
			
		||||
    return recipe["customizations"]["locale"]["keyboard"]
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def firewall_cmd(line, settings):
 | 
			
		||||
    """ Update the firewall line with the new ports and services
 | 
			
		||||
 | 
			
		||||
    :param line: The firewall ... line
 | 
			
		||||
    :type line: str
 | 
			
		||||
    :param settings: A dict with the list of services and ports to enable and disable
 | 
			
		||||
    :type settings: dict
 | 
			
		||||
 | 
			
		||||
    Using pykickstart to process the line is the best way to make sure it
 | 
			
		||||
    is parsed correctly, and re-assembled for inclusion into the final kickstart
 | 
			
		||||
    """
 | 
			
		||||
    ks_version = makeVersion()
 | 
			
		||||
    ks = KickstartParser(ks_version, errorsAreFatal=False, missingIncludeIsFatal=False)
 | 
			
		||||
    ks.readKickstartFromString(line)
 | 
			
		||||
 | 
			
		||||
    # Do not override firewall --disabled
 | 
			
		||||
    if ks.handler.firewall.enabled != False and settings:
 | 
			
		||||
        ks.handler.firewall.ports = settings["ports"]
 | 
			
		||||
        ks.handler.firewall.services = settings["enabled"]
 | 
			
		||||
        ks.handler.firewall.remove_services = settings["disabled"]
 | 
			
		||||
 | 
			
		||||
    # Converting back to a string includes a comment, return just the keyboard line
 | 
			
		||||
    return str(ks.handler.firewall).splitlines()[-1]
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def get_firewall_settings(recipe):
 | 
			
		||||
    """Return the customizations.firewall settings
 | 
			
		||||
 | 
			
		||||
    :param recipe: The recipe
 | 
			
		||||
    :type recipe: Recipe object
 | 
			
		||||
    :returns: A dict of settings
 | 
			
		||||
    :rtype: dict
 | 
			
		||||
    """
 | 
			
		||||
    settings = {"ports": [], "enabled": [], "disabled": []}
 | 
			
		||||
 | 
			
		||||
    if "customizations" not in recipe or \
 | 
			
		||||
       "firewall" not in recipe["customizations"]:
 | 
			
		||||
        return settings
 | 
			
		||||
 | 
			
		||||
    settings["ports"] = recipe["customizations"]["firewall"].get("ports", [])
 | 
			
		||||
 | 
			
		||||
    if "services" in recipe["customizations"]["firewall"]:
 | 
			
		||||
        settings["enabled"] = recipe["customizations"]["firewall"]["services"].get("enabled", [])
 | 
			
		||||
        settings["disabled"] = recipe["customizations"]["firewall"]["services"].get("disabled", [])
 | 
			
		||||
    return settings
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def customize_ks_template(ks_template, recipe):
 | 
			
		||||
    """ Customize the kickstart template and return it
 | 
			
		||||
 | 
			
		||||
@ -320,6 +367,9 @@ def customize_ks_template(ks_template, recipe):
 | 
			
		||||
                "keyboard":   [keyboard_cmd,
 | 
			
		||||
                               get_keyboard_layout(recipe),
 | 
			
		||||
                               'keyboard --xlayouts us --vckeymap us', True],
 | 
			
		||||
                "firewall":   [firewall_cmd,
 | 
			
		||||
                               get_firewall_settings(recipe),
 | 
			
		||||
                               'firewall --enabled', True],
 | 
			
		||||
               }
 | 
			
		||||
    found = {}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@ -20,6 +20,7 @@ import unittest
 | 
			
		||||
from pylorax.api.compose import add_customizations, compose_types
 | 
			
		||||
from pylorax.api.compose import timezone_cmd, get_timezone_settings
 | 
			
		||||
from pylorax.api.compose import lang_cmd, get_languages, keyboard_cmd, get_keyboard_layout
 | 
			
		||||
from pylorax.api.compose import firewall_cmd, get_firewall_settings
 | 
			
		||||
from pylorax.api.compose import get_kernel_append, bootloader_append, customize_ks_template
 | 
			
		||||
from pylorax.api.recipes import recipe_from_toml
 | 
			
		||||
from pylorax.sysutils import joinpaths
 | 
			
		||||
@ -361,6 +362,65 @@ languages = ["en_CA.utf8", "en_HK.utf8"]
 | 
			
		||||
                         "de (dvorak)"),
 | 
			
		||||
                         "keyboard 'de (dvorak)'")
 | 
			
		||||
 | 
			
		||||
    def test_get_firewall_settings(self):
 | 
			
		||||
        """Test get_firewall_settings function"""
 | 
			
		||||
        blueprint_data = """name = "test-firewall"
 | 
			
		||||
description = "test recipe"
 | 
			
		||||
version = "0.0.1"
 | 
			
		||||
        """
 | 
			
		||||
        firewall_ports = """
 | 
			
		||||
[customizations.firewall]
 | 
			
		||||
ports = ["22:tcp", "80:tcp", "imap:tcp", "53:tcp", "53:udp"]
 | 
			
		||||
"""
 | 
			
		||||
        firewall_services = """
 | 
			
		||||
[customizations.firewall.services]
 | 
			
		||||
enabled = ["ftp", "ntp", "dhcp"]
 | 
			
		||||
disabled = ["telnet"]
 | 
			
		||||
"""
 | 
			
		||||
        blueprint2_data = blueprint_data + firewall_ports
 | 
			
		||||
        blueprint3_data = blueprint_data + firewall_services
 | 
			
		||||
        blueprint4_data = blueprint_data + firewall_ports + firewall_services
 | 
			
		||||
 | 
			
		||||
        recipe = recipe_from_toml(blueprint_data)
 | 
			
		||||
        self.assertEqual(get_firewall_settings(recipe), {'ports': [], 'enabled': [], 'disabled': []})
 | 
			
		||||
 | 
			
		||||
        recipe = recipe_from_toml(blueprint2_data)
 | 
			
		||||
        self.assertEqual(get_firewall_settings(recipe),
 | 
			
		||||
                {"ports": ["22:tcp", "80:tcp", "imap:tcp", "53:tcp", "53:udp"],
 | 
			
		||||
                 "enabled": [], "disabled": []})
 | 
			
		||||
 | 
			
		||||
        recipe = recipe_from_toml(blueprint3_data)
 | 
			
		||||
        self.assertEqual(get_firewall_settings(recipe),
 | 
			
		||||
                {"ports": [],
 | 
			
		||||
                 "enabled": ["ftp", "ntp", "dhcp"], "disabled": ["telnet"]})
 | 
			
		||||
 | 
			
		||||
        recipe = recipe_from_toml(blueprint4_data)
 | 
			
		||||
        self.assertEqual(get_firewall_settings(recipe),
 | 
			
		||||
                {"ports": ["22:tcp", "80:tcp", "imap:tcp", "53:tcp", "53:udp"],
 | 
			
		||||
                 "enabled": ["ftp", "ntp", "dhcp"], "disabled": ["telnet"]})
 | 
			
		||||
 | 
			
		||||
    def test_firewall_cmd(self):
 | 
			
		||||
        """Test firewall_cmd function"""
 | 
			
		||||
 | 
			
		||||
        self.assertEqual(firewall_cmd("firewall --enabled", {}), "firewall --enabled")
 | 
			
		||||
        self.assertEqual(firewall_cmd("firewall --enabled",
 | 
			
		||||
                         {"ports": ["22:tcp", "80:tcp", "imap:tcp", "53:tcp", "53:udp"],
 | 
			
		||||
                         "enabled": [], "disabled": []}),
 | 
			
		||||
                         "firewall --enabled --port=22:tcp,80:tcp,imap:tcp,53:tcp,53:udp")
 | 
			
		||||
        self.assertEqual(firewall_cmd("firewall --enabled",
 | 
			
		||||
                         {"ports": ["22:tcp", "80:tcp", "imap:tcp", "53:tcp", "53:udp"],
 | 
			
		||||
                         "enabled": ["ftp", "ntp", "dhcp"], "disabled": []}),
 | 
			
		||||
                         "firewall --enabled --port=22:tcp,80:tcp,imap:tcp,53:tcp,53:udp --service=ftp,ntp,dhcp")
 | 
			
		||||
        self.assertEqual(firewall_cmd("firewall --enabled",
 | 
			
		||||
                         {"ports": ["22:tcp", "80:tcp", "imap:tcp", "53:tcp", "53:udp"],
 | 
			
		||||
                         "enabled": ["ftp", "ntp", "dhcp"], "disabled": ["telnet"]}),
 | 
			
		||||
                         "firewall --enabled --port=22:tcp,80:tcp,imap:tcp,53:tcp,53:udp --service=ftp,ntp,dhcp --remove-service=telnet")
 | 
			
		||||
        # Make sure that --disabled overrides setting ports and services
 | 
			
		||||
        self.assertEqual(firewall_cmd("firewall --disabled",
 | 
			
		||||
                         {"ports": ["22:tcp", "80:tcp", "imap:tcp", "53:tcp", "53:udp"],
 | 
			
		||||
                         "enabled": ["ftp", "ntp", "dhcp"], "disabled": ["telnet"]}),
 | 
			
		||||
                         "firewall --disabled")
 | 
			
		||||
 | 
			
		||||
    def _checkBootloader(self, result, append_str, line_limit=0):
 | 
			
		||||
        """Find the bootloader line and make sure append_str is in it"""
 | 
			
		||||
        # Optionally check to make sure the change is at the top of the template
 | 
			
		||||
@ -426,6 +486,28 @@ languages = ["en_CA.utf8", "en_HK.utf8"]
 | 
			
		||||
            line_num += 1
 | 
			
		||||
        return False
 | 
			
		||||
 | 
			
		||||
    def _checkFirewall(self, result, settings, line_limit=0):
 | 
			
		||||
        """Find the firewall line and make sure it is as expected"""
 | 
			
		||||
        # Optionally check to make sure the change is at the top of the template
 | 
			
		||||
        line_num = 0
 | 
			
		||||
        for line in result.splitlines():
 | 
			
		||||
            if line.startswith("firewall"):
 | 
			
		||||
                # First layout is used twice, so total count should be n+1
 | 
			
		||||
                ports = all([bool(p in line) for p in settings["ports"]])
 | 
			
		||||
                enabled = all([bool(e in line) for e in settings["enabled"]])
 | 
			
		||||
                disabled = all([bool(d in line) for d in settings["disabled"]])
 | 
			
		||||
 | 
			
		||||
                if ports and enabled and disabled:
 | 
			
		||||
                    if line_limit == 0 or line_num < line_limit:
 | 
			
		||||
                        return True
 | 
			
		||||
                    else:
 | 
			
		||||
                        print("FAILED: firewall not in the first %d lines of the output" % line_limit)
 | 
			
		||||
                        return False
 | 
			
		||||
                else:
 | 
			
		||||
                    print("FAILED: %s not matching %s" % (settings, line))
 | 
			
		||||
            line_num += 1
 | 
			
		||||
        return False
 | 
			
		||||
 | 
			
		||||
    def test_template_defaults(self):
 | 
			
		||||
        """Test that customize_ks_template includes defaults correctly"""
 | 
			
		||||
        blueprint_data = """name = "test-kernel"
 | 
			
		||||
@ -478,6 +560,13 @@ ntpservers = ["0.north-america.pool.ntp.org", "1.north-america.pool.ntp.org"]
 | 
			
		||||
[customizations.locale]
 | 
			
		||||
keyboard = "de (dvorak)"
 | 
			
		||||
languages = ["en_CA.utf8", "en_HK.utf8"]
 | 
			
		||||
 | 
			
		||||
[customizations.firewall]
 | 
			
		||||
ports = ["22:tcp", "80:tcp", "imap:tcp", "53:tcp", "53:udp"]
 | 
			
		||||
 | 
			
		||||
[customizations.firewall.services]
 | 
			
		||||
enabled = ["ftp", "ntp", "dhcp"]
 | 
			
		||||
disabled = ["telnet"]
 | 
			
		||||
"""
 | 
			
		||||
        tz_dict = {"timezone": "US/Samoa", "ntpservers": ["0.north-america.pool.ntp.org", "1.north-america.pool.ntp.org"]}
 | 
			
		||||
        recipe = recipe_from_toml(blueprint_data)
 | 
			
		||||
@ -492,6 +581,10 @@ languages = ["en_CA.utf8", "en_HK.utf8"]
 | 
			
		||||
        self.assertEqual(sum([1 for l in result.splitlines() if l.startswith("lang")]), 1)
 | 
			
		||||
        self.assertTrue(self._checkKeyboard(result, "de (dvorak)", line_limit=4))
 | 
			
		||||
        self.assertEqual(sum([1 for l in result.splitlines() if l.startswith("keyboard")]), 1)
 | 
			
		||||
        self.assertTrue(self._checkFirewall(result,
 | 
			
		||||
                        {"ports": ["22:tcp", "80:tcp", "imap:tcp", "53:tcp", "53:udp"],
 | 
			
		||||
                         "enabled": ["ftp", "ntp", "dhcp"], "disabled": ["telnet"]}, line_limit=6))
 | 
			
		||||
        self.assertEqual(sum([1 for l in result.splitlines() if l.startswith("firewall")]), 1)
 | 
			
		||||
 | 
			
		||||
        # Test against a kickstart with a bootloader line
 | 
			
		||||
        result = customize_ks_template("firewall --enabled\nbootloader --location=mbr\n", recipe)
 | 
			
		||||
@ -532,6 +625,18 @@ languages = ["en_CA.utf8", "en_HK.utf8"]
 | 
			
		||||
            if sum([1 for l in result.splitlines() if l.startswith("keyboard")]) != 1:
 | 
			
		||||
                errors.append(("keyboard for compose_type %s failed: More than 1 entry" % compose_type, result))
 | 
			
		||||
 | 
			
		||||
            # google and openstack templates requires the firewall to be disabled
 | 
			
		||||
            if compose_type == "google" or compose_type == "openstack":
 | 
			
		||||
                if not self._checkFirewall(result, {'ports': [], 'enabled': [], 'disabled': []}):
 | 
			
		||||
                    errors.append(("firewall for compose_type %s failed" % compose_type, result))
 | 
			
		||||
            else:
 | 
			
		||||
                if not self._checkFirewall(result,
 | 
			
		||||
                               {"ports": ["22:tcp", "80:tcp", "imap:tcp", "53:tcp", "53:udp"],
 | 
			
		||||
                                "enabled": ["ftp", "ntp", "dhcp"], "disabled": ["telnet"]}):
 | 
			
		||||
                    errors.append(("firewall for compose_type %s failed" % compose_type, result))
 | 
			
		||||
            if sum([1 for l in result.splitlines() if l.startswith("firewall")]) != 1:
 | 
			
		||||
                errors.append(("firewall for compose_type %s failed: More than 1 entry" % compose_type, result))
 | 
			
		||||
 | 
			
		||||
        # Print the bad results
 | 
			
		||||
        for e, r in errors:
 | 
			
		||||
            print("%s:\n%s\n\n" % (e, r))
 | 
			
		||||
 | 
			
		||||
@ -494,6 +494,56 @@ languages = ["en_CA.utf8", "en_HK.utf8"]
 | 
			
		||||
        self.assertEqual(ks.handler.lang.lang, "en_CA.utf8")
 | 
			
		||||
        self.assertEqual(ks.handler.lang.addsupport, ["en_HK.utf8"])
 | 
			
		||||
 | 
			
		||||
    def test_firewall_ports(self):
 | 
			
		||||
        blueprint_data = """name = "test-firewall"
 | 
			
		||||
description = "test recipe"
 | 
			
		||||
version = "0.0.1"
 | 
			
		||||
"""
 | 
			
		||||
        blueprint2_data = blueprint_data + """
 | 
			
		||||
[customizations.firewall]
 | 
			
		||||
ports = ["22:tcp", "80:tcp", "imap:tcp", "53:tcp", "53:udp"]
 | 
			
		||||
"""
 | 
			
		||||
        ks = self._blueprint_to_ks(blueprint_data)
 | 
			
		||||
        self.assertEqual(ks.handler.firewall.ports, [])
 | 
			
		||||
        self.assertEqual(ks.handler.firewall.services, [])
 | 
			
		||||
        self.assertEqual(ks.handler.firewall.remove_services, [])
 | 
			
		||||
 | 
			
		||||
        ks = self._blueprint_to_ks(blueprint2_data)
 | 
			
		||||
        self.assertEqual(ks.handler.firewall.ports, ["22:tcp", "80:tcp", "imap:tcp", "53:tcp", "53:udp"])
 | 
			
		||||
        self.assertEqual(ks.handler.firewall.services, [])
 | 
			
		||||
        self.assertEqual(ks.handler.firewall.remove_services, [])
 | 
			
		||||
 | 
			
		||||
    def test_firewall_services(self):
 | 
			
		||||
        blueprint_data = """name = "test-firewall"
 | 
			
		||||
description = "test recipe"
 | 
			
		||||
version = "0.0.1"
 | 
			
		||||
 | 
			
		||||
[customizations.firewall.services]
 | 
			
		||||
enabled = ["ftp", "ntp", "dhcp"]
 | 
			
		||||
disabled = ["telnet"]
 | 
			
		||||
"""
 | 
			
		||||
        ks = self._blueprint_to_ks(blueprint_data)
 | 
			
		||||
        self.assertEqual(ks.handler.firewall.ports, [])
 | 
			
		||||
        self.assertEqual(ks.handler.firewall.services, ["ftp", "ntp", "dhcp"])
 | 
			
		||||
        self.assertEqual(ks.handler.firewall.remove_services, ["telnet"])
 | 
			
		||||
 | 
			
		||||
    def test_firewall(self):
 | 
			
		||||
        blueprint_data = """name = "test-firewall"
 | 
			
		||||
description = "test recipe"
 | 
			
		||||
version = "0.0.1"
 | 
			
		||||
 | 
			
		||||
[customizations.firewall]
 | 
			
		||||
ports = ["22:tcp", "80:tcp", "imap:tcp", "53:tcp", "53:udp"]
 | 
			
		||||
 | 
			
		||||
[customizations.firewall.services]
 | 
			
		||||
enabled = ["ftp", "ntp", "dhcp"]
 | 
			
		||||
disabled = ["telnet"]
 | 
			
		||||
"""
 | 
			
		||||
        ks = self._blueprint_to_ks(blueprint_data)
 | 
			
		||||
        self.assertEqual(ks.handler.firewall.ports, ["22:tcp", "80:tcp", "imap:tcp", "53:tcp", "53:udp"])
 | 
			
		||||
        self.assertEqual(ks.handler.firewall.services, ["ftp", "ntp", "dhcp"])
 | 
			
		||||
        self.assertEqual(ks.handler.firewall.remove_services, ["telnet"])
 | 
			
		||||
 | 
			
		||||
    def test_user(self):
 | 
			
		||||
        blueprint_data = """name = "test-user"
 | 
			
		||||
description = "test recipe"
 | 
			
		||||
 | 
			
		||||
		Loading…
	
		Reference in New Issue
	
	Block a user