diff --git a/cloud-init-cve-2023-1786-redact-instance-data-json-main.patch b/cloud-init-cve-2023-1786-redact-instance-data-json-main.patch new file mode 100644 index 0000000..f9ffac4 --- /dev/null +++ b/cloud-init-cve-2023-1786-redact-instance-data-json-main.patch @@ -0,0 +1,175 @@ +--- cloudinit/sources/DataSourceLXD.py.orig ++++ cloudinit/sources/DataSourceLXD.py +@@ -173,6 +173,8 @@ class DataSourceLXD(sources.DataSource): + "user.meta-data", + "user.vendor-data", + "user.user-data", ++ "cloud-init.user-data", ++ "cloud-init.vendor-data", + ) + + skip_hotplug_detect = True +--- cloudinit/sources/DataSourceVultr.py.orig ++++ cloudinit/sources/DataSourceVultr.py +@@ -5,6 +5,8 @@ + # Vultr Metadata API: + # https://www.vultr.com/metadata/ + ++from typing import Tuple ++ + import cloudinit.sources.helpers.vultr as vultr + from cloudinit import log as log + from cloudinit import sources, util, version +@@ -27,6 +29,9 @@ BUILTIN_DS_CONFIG = { + class DataSourceVultr(sources.DataSource): + + dsname = "Vultr" ++ sensitive_metadata_keys: Tuple[ ++ str, ... ++ ] = sources.DataSource.sensitive_metadata_keys + ("startup-script",) + + def __init__(self, sys_cfg, distro, paths): + super(DataSourceVultr, self).__init__(sys_cfg, distro, paths) +@@ -54,13 +59,8 @@ class DataSourceVultr(sources.DataSource + self.get_datasource_data(self.metadata) + + # Dump some data so diagnosing failures is manageable +- LOG.debug("Vultr Vendor Config:") +- LOG.debug(util.json_dumps(self.metadata["vendor-data"])) + LOG.debug("SUBID: %s", self.metadata["instance-id"]) + LOG.debug("Hostname: %s", self.metadata["local-hostname"]) +- if self.userdata_raw is not None: +- LOG.debug("User-Data:") +- LOG.debug(self.userdata_raw) + + return True + +@@ -146,7 +146,4 @@ if __name__ == "__main__": + config = md["vendor-data"] + sysinfo = vultr.get_sysinfo() + +- print(util.json_dumps(sysinfo)) +- print(util.json_dumps(config)) +- + # vi: ts=4 expandtab +--- cloudinit/sources/__init__.py.orig ++++ cloudinit/sources/__init__.py +@@ -132,6 +132,12 @@ def redact_sensitive_keys(metadata, reda + + Replace any keys values listed in 'sensitive_keys' with redact_value. + """ ++ # While 'sensitive_keys' should already sanitized to only include what ++ # is in metadata, it is possible keys will overlap. For example, if ++ # "merged_cfg" and "merged_cfg/ds/userdata" both match, it's possible that ++ # "merged_cfg" will get replaced first, meaning "merged_cfg/ds/userdata" ++ # no longer represents a valid key. ++ # Thus, we still need to do membership checks in this function. + if not metadata.get("sensitive_keys", []): + return metadata + md_copy = copy.deepcopy(metadata) +@@ -139,9 +145,14 @@ def redact_sensitive_keys(metadata, reda + path_parts = key_path.split("/") + obj = md_copy + for path in path_parts: +- if isinstance(obj[path], dict) and path != path_parts[-1]: ++ if ( ++ path in obj ++ and isinstance(obj[path], dict) ++ and path != path_parts[-1] ++ ): + obj = obj[path] +- obj[path] = redact_value ++ if path in obj: ++ obj[path] = redact_value + return md_copy + + +@@ -249,6 +260,14 @@ class DataSource(CloudInitPickleMixin, m + sensitive_metadata_keys: Tuple[str, ...] = ( + "merged_cfg", + "security-credentials", ++ "userdata", ++ "user-data", ++ "user_data", ++ "vendordata", ++ "vendor-data", ++ # Provide ds/vendor_data to avoid redacting top-level ++ # "vendor_data": {enabled: True} ++ "ds/vendor_data", + ) + + # True on datasources that may not see hotplugged devices reflected +--- cloudinit/stages.py.orig ++++ cloudinit/stages.py +@@ -203,7 +203,9 @@ class Init: + util.ensure_dirs(self._initial_subdirs()) + log_file = util.get_cfg_option_str(self.cfg, "def_log_file") + if log_file: +- util.ensure_file(log_file, mode=0o640, preserve_mode=True) ++ # At this point the log file should have already been created ++ # in the setupLogging function of log.py ++ util.ensure_file(log_file, mode=0o640, preserve_mode=False) + perms = self.cfg.get("syslog_fix_perms") + if not perms: + perms = {} +--- tests/unittests/sources/test_init.py.orig ++++ tests/unittests/sources/test_init.py +@@ -464,6 +464,12 @@ class TestDataSource(CiTestCase): + ( + "merged_cfg", + "security-credentials", ++ "userdata", ++ "user-data", ++ "user_data", ++ "vendordata", ++ "vendor-data", ++ "ds/vendor_data", + ), + datasource.sensitive_metadata_keys, + ) +@@ -574,6 +580,12 @@ class TestDataSource(CiTestCase): + ( + "merged_cfg", + "security-credentials", ++ "userdata", ++ "user-data", ++ "user_data", ++ "vendordata", ++ "vendor-data", ++ "ds/vendor_data", + ), + datasource.sensitive_metadata_keys, + ) +--- tests/unittests/test_stages.py.orig ++++ tests/unittests/test_stages.py +@@ -606,19 +606,23 @@ class TestInit_InitializeFilesystem: + # Assert we create it 0o640 by default if it doesn't already exist + assert 0o640 == stat.S_IMODE(log_file.stat().mode) + +- def test_existing_file_permissions_are_not_modified(self, init, tmpdir): +- """If the log file already exists, we should not modify its permissions ++ def test_existing_file_permissions(self, init, tmpdir): ++ """Test file permissions are set as expected. ++ ++ CIS Hardening requires 640 permissions. These permissions are ++ currently hardcoded on every boot, but if there's ever a reason ++ to change this, we need to then ensure that they ++ are *not* set every boot. + + See https://bugs.launchpad.net/cloud-init/+bug/1900837. + """ +- # Use a mode that will never be made the default so this test will +- # always be valid +- mode = 0o606 + log_file = tmpdir.join("cloud-init.log") + log_file.ensure() +- log_file.chmod(mode) ++ # Use a mode that will never be made the default so this test will ++ # always be valid ++ log_file.chmod(0o606) + init._cfg = {"def_log_file": str(log_file)} + + init._initialize_filesystem() + +- assert mode == stat.S_IMODE(log_file.stat().mode) ++ assert 0o640 == stat.S_IMODE(log_file.stat().mode) diff --git a/cloud-init.changes b/cloud-init.changes index a2c93a2..0b675ed 100644 --- a/cloud-init.changes +++ b/cloud-init.changes @@ -5,6 +5,14 @@ Thu Apr 27 12:22:11 UTC 2023 - Robert Schweikert + Config module cc_refresh_rmc_and_interface is implemented such that it will only work on RH distros. Set the module availability accordingly. +------------------------------------------------------------------- +Tue Apr 11 18:48:30 UTC 2023 - Robert Schweikert + +- Sensitive data exposure (bsc#1210277, CVE-2023-1786) + + Add hidesensitivedata + + Add cloud-init-cve-2023-1786-redact-inst-data.patch + + Do not expose sensitive data gathered from the CSP + ------------------------------------------------------------------- Thu Feb 23 13:38:43 UTC 2023 - Robert Schweikert diff --git a/cloud-init.spec b/cloud-init.spec index 28ff66b..de19f56 100644 --- a/cloud-init.spec +++ b/cloud-init.spec @@ -26,6 +26,7 @@ Url: https://github.com/canonical/cloud-init Group: System/Management Source0: %{name}-%{version}.tar.gz Source1: rsyslog-cloud-init.cfg +Source2: hidesensitivedata Patch1: datasourceLocalDisk.patch # FIXME (lp#1849296) Patch2: cloud-init-break-resolv-symlink.patch @@ -37,8 +38,9 @@ Patch4: cloud-init-no-tempnet-oci.patch Patch5: cloud-init-fix-ca-test.patch # FIXME (lp#1812117) Patch6: cloud-init-write-routes.patch +Patch7: cloud-init-cve-2023-1786-redact-instance-data-json-main.patch # FIXME https://github.com/canonical/cloud-init/pull/2148 -Patch7: cloud-init-power-rhel-only.patch +Patch8: cloud-init-power-rhel-only.patch BuildRequires: fdupes BuildRequires: filesystem # pkg-config is needed to find correct systemd unit dir @@ -144,6 +146,7 @@ Documentation and examples for cloud-init tools %patch5 %patch6 %patch7 +%patch8 # patch in the full version to version.py version_pys=$(find . -name version.py -type f) @@ -187,6 +190,8 @@ mkdir -p %{buildroot}/%{_sysconfdir}/rsyslog.d mkdir -p %{buildroot}/usr/lib/udev/rules.d/ cp -a %{SOURCE1} %{buildroot}/%{_sysconfdir}/rsyslog.d/21-cloudinit.conf mv %{buildroot}/lib/udev/rules.d/66-azure-ephemeral.rules %{buildroot}/usr/lib/udev/rules.d/ +mkdir -p %{buildroot}%{_sbindir} +install -m 755 %{SOURCE2} %{buildroot}%{_sbindir} # remove debian/ubuntu specific profile.d file (bnc#779553) rm -f %{buildroot}%{_sysconfdir}/profile.d/Z99-cloud-locale-test.sh @@ -196,6 +201,9 @@ rm %{buildroot}/%{_sysconfdir}/cloud/templates/*.debian.* rm %{buildroot}/%{_sysconfdir}/cloud/templates/*.redhat.* rm %{buildroot}/%{_sysconfdir}/cloud/templates/*.ubuntu.* +%post +/usr/sbin/hidesensitivedata + # remove duplicate files %if 0%{?suse_version} %fdupes %{buildroot}%{python3_sitelib} @@ -207,6 +215,7 @@ rm %{buildroot}/%{_sysconfdir}/cloud/templates/*.ubuntu.* %{_bindir}/cloud-id %{_bindir}/cloud-init %{_bindir}/cloud-init-per +%{_sbindir}/hidesensitivedata %dir %{_sysconfdir}/cloud %dir %{_sysconfdir}/cloud/clean.d %{_sysconfdir}/cloud/clean.d/README diff --git a/hidesensitivedata b/hidesensitivedata new file mode 100644 index 0000000..be635ce --- /dev/null +++ b/hidesensitivedata @@ -0,0 +1,36 @@ +#!/usr/bin/python3 + +import json +import os +import sys + +from pathlib import Path + +from cloudinit.atomic_helper import write_json +from cloudinit.sources import ( + DataSource, + process_instance_metadata, + redact_sensitive_keys, +) + +from cloudinit.stages import Init + +init = Init() +log_file = init.cfg["def_log_file"] +if os.path.exists(log_file): + os.chmod(log_file, 0o640) + +rundir = init.paths.run_dir +instance_data_path = Path(rundir, "instance-data.json") +if not os.path.exists(str(instance_data_path)): + sys.exit(0) +instance_json = json.load(instance_data_path.open(encoding="utf-8")) + +sensitive_keys = DataSource.sensitive_metadata_keys + +processed_json = process_instance_metadata( + instance_json, sensitive_keys=sensitive_keys +) +redacted_json = redact_sensitive_keys(processed_json) + +write_json(str(instance_data_path), redacted_json)