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)
(cherry picked from commit 9f1756cc27)
This commit is contained in:
parent
725a0437c7
commit
f39e965fb2
@ -292,9 +292,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.
|
||||
|
||||
@ -303,12 +302,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]
|
||||
*************************
|
||||
|
||||
@ -281,6 +281,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
|
||||
|
||||
@ -314,6 +361,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 = {}
|
||||
|
||||
|
||||
@ -24,6 +24,7 @@ from pylorax import get_buildarch
|
||||
from pylorax.api.compose import add_customizations, get_extra_pkgs, 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.config import configure, make_dnf_dirs
|
||||
from pylorax.api.dnfbase import get_base_object
|
||||
@ -389,6 +390,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
|
||||
@ -454,6 +514,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"
|
||||
@ -506,6 +588,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)
|
||||
@ -520,6 +609,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)
|
||||
@ -560,6 +653,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))
|
||||
|
||||
@ -523,6 +523,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