From ee21ba2463669a5926e3ef149955af8fce7d3d8d Mon Sep 17 00:00:00 2001 From: Chad Smith Date: Mon, 9 Jan 2023 21:16:18 +0000 Subject: [PATCH] subiquity.models.network: use cloud-init networking on root read-only When cloudinit.features.NETPLAN_CONFIG_ROOT_READ_ONLY is True, cloud-init will write /etc/netplan/50-cloud-init.yaml as read-only root. This added security allows for subiquity to use cloud-init's network renderer directly allowing both datasource and network configuration passed in one place. Any netplan wifis configuration can be specified in a single network config file /etc/cloud/cloud.cfg.d/90-installer-network.cfg instead of having a separate config file for wifi, which could contain credentials. This simplifies golden image creation from images installed using subiquity because image builders will not need to track down and purge separate /etc/netplan/00-installer-config.yaml and /etc/netplan/subiquity-disable-cloudinit-networking.cfg when preparing a golden image. Eventually, netplan config validation and cloudinit will both support separation of sensitive configuration without needing to pre-categorize. This will allow cloud-init to grow to ability to write separate world-readable configuration from config which is security sensitive with no change needed in subiquity. --- subiquity/models/network.py | 58 ++++++++++++++++-------- subiquity/models/tests/test_subiquity.py | 29 +++++++++--- 2 files changed, 61 insertions(+), 26 deletions(-) diff --git a/subiquity/models/network.py b/subiquity/models/network.py index 032d553eb5..84b6962b92 100644 --- a/subiquity/models/network.py +++ b/subiquity/models/network.py @@ -15,6 +15,7 @@ import logging +from cloudinit import features from subiquitycore.models.network import NetworkModel as CoreNetworkModel log = logging.getLogger('subiquity.models.network') @@ -39,32 +40,49 @@ def render(self): # perfect solution because in principle there could be wired 802.1x # stuff that has secrets too but the subiquity UI does not support any # of that yet so this will do for now. - wifis = netplan['network'].pop('wifis', None) - r = { - 'write_files': { - 'etc_netplan_installer': { - 'path': 'etc/netplan/00-installer-config.yaml', - 'content': self.stringify_config(netplan), + use_cloudinit_net = getattr( + features, "NETPLAN_CONFIG_ROOT_READ_ONLY", False + ) + if use_cloudinit_net: + r = { + 'write_files': { + 'etc_netplan_installer': { + 'path': ('etc/cloud/cloud.cfg.d' + '/90-installer-network.cfg'), + 'content': self.stringify_config(netplan), + }, + } + } + else: + # Separate sensitive wifi config from world-readable config + wifis = netplan['network'].pop('wifis', None) + r = { + 'write_files': { + # Disable cloud-init networking + 'no_cloudinit_net': { + 'path': ('etc/cloud/cloud.cfg.d/' + 'subiquity-disable-cloudinit-networking.cfg'), + 'content': 'network: {config: disabled}\n', }, - 'nonet': { - 'path': ('etc/cloud/cloud.cfg.d/' - 'subiquity-disable-cloudinit-networking.cfg'), - 'content': 'network: {config: disabled}\n', + # World-readable netplan without sensitive wifi config + 'etc_netplan_installer': { + 'path': 'etc/netplan/00-installer-config.yaml', + 'content': self.stringify_config(netplan), }, }, } - if wifis is not None: - netplan_wifi = { - 'network': { - 'version': 2, - 'wifis': wifis, + if wifis is not None: + netplan_wifi = { + 'network': { + 'version': 2, + 'wifis': wifis, }, } - r['write_files']['etc_netplan_installer_wifi'] = { - 'path': 'etc/netplan/00-installer-config-wifi.yaml', - 'content': self.stringify_config(netplan_wifi), - 'permissions': '0600', - } + r['write_files']['etc_netplan_installer_wifi'] = { + 'path': 'etc/netplan/00-installer-config-wifi.yaml', + 'content': self.stringify_config(netplan_wifi), + 'permissions': '0600', + } return r async def target_packages(self): diff --git a/subiquity/models/tests/test_subiquity.py b/subiquity/models/tests/test_subiquity.py index f45beb6970..9daf1cc7a0 100644 --- a/subiquity/models/tests/test_subiquity.py +++ b/subiquity/models/tests/test_subiquity.py @@ -21,6 +21,8 @@ import yaml from cloudinit.config.schema import SchemaValidationError +from cloudinit import features + try: from cloudinit.config.schema import SchemaProblem except ImportError: @@ -192,7 +194,11 @@ def test_write_netplan(self): for fspec in config['write_files'].values(): if fspec['path'].startswith('etc/netplan'): if netplan_content is not None: - self.fail("writing two files to netplan?") + self.fail("writing two files for network config?") + netplan_content = fspec['content'] + if fspec['path'].endswith('cloud.cfg.d/90-installer-network.cfg'): + if netplan_content is not None: + self.fail("writing two files for network config?") netplan_content = fspec['content'] self.assertIsNot(netplan_content, None) netplan = yaml.safe_load(netplan_content) @@ -267,8 +273,6 @@ def test_cloud_init_files_emits_datasource_config_and_clean_script( model.identity.add_user(main_user) model.userdata = {} expected_files = { - 'etc/cloud/cloud.cfg.d/subiquity-disable-cloudinit-networking.cfg': - 'network: {config: disabled}\n', 'etc/cloud/cloud.cfg.d/99-installer.cfg': re.compile('datasource:\n None:\n metadata:\n instance-id: .*\n userdata_raw: "#cloud-config\\\\ngrowpart:\\\\n mode: \\\'off\\\'\\\\npreserve_hostname: true\\\\n\\\\\n'), # noqa 'etc/hostname': 'somehost\n', 'etc/cloud/ds-identify.cfg': 'policy: enabled\n', @@ -279,10 +283,23 @@ def test_cloud_init_files_emits_datasource_config_and_clean_script( cfg_files = [ "/" + key for key in expected_files.keys() if "host" not in key ] - cfg_files.append( - # Obtained from NetworkModel.render - "/etc/netplan/00-installer-config.yaml", + use_cloudinit_networking = getattr( + features, "NETPLAN_CONFIG_ROOT_READ_ONLY", False ) + if use_cloudinit_networking: + cfg_files.append( + # Obtained from NetworkModel.render + "/etc/cloud/cloud.cfg.d/90-installer-network.cfg" + ) + else: + expected_files[ + 'etc/cloud/cloud.cfg.d/' + 'subiquity-disable-cloudinit-networking.cfg' + ] = 'network: {config: disabled}\n' + cfg_files.append( + # Obtained from NetworkModel.render + "/etc/netplan/00-installer-config.yaml", + ) header = "# Autogenerated by Subiquity: 2004-03-05 ... UTC\n" with self.subTest('Stable releases Jammy do not disable cloud-init'): lsb_release.return_value = {"release": "22.04"}