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