CVE-2022-4968 (published 2024-06-07T01:15Z)

netplan leaks the private key of wireguard to local users. Versions after 1.0 are not affected.


Weakness information:
cwe_id: NVD-CWE-noinfo
cwe_name: Insufficient Information
description: There is insufficient information about the issue to classify it; details are unkown or unspecified.
extended_description: Insufficient Information

Fix information:
repo_url: https://github.com/canonical/netplan
commit_details: repo_url: https://github.com/canonical/netplan
author: Danilo Egea Gondolfo
author_date: 2024-05-22 15:44:16+01:00
msg: libnetplan: use more restrictive file permissions

A new util.c:_netplan_g_string_free_to_file_with_permissions() was added
and accepts the owner, group and file mode as arguments. When these
properties can't be set, when the generator is called by a non-root user
for example, it will not hard-fail. This function is called by unit
tests where we can't set the owner to a privileged account for example.

When generating backend files, use more restrictive permissions:

networkd related files will be owned by root:systemd-network and have
mode 0640.

service unit files will be owned by root:root and have mode 0640.
udevd files will be owned by root:root with mode 0640.

wpa_supplicant and Network Manager files will continue with the existing
permissions.

Autopkgtests will check if the permissions are set as expected when
calling the generator.

This fix addresses CVE-2022-4968
repository: repo_url: https://github.com/canonical/netplan
repo_name: canonical/netplan
description: Declarative network configuration for various backends
repo_language: Python
owner: canonical
file_changes: filename: networkd.c
change_type: ModificationType.MODIFY
diff: @@ -302,7 +302,6 @@ STATIC void
 write_link_file(const NetplanNetDefinition* def, const char* rootdir, const char* path)
 {
     GString* s = NULL;
-    mode_t orig_umask;
 
     /* Don't write .link files for virtual devices; they use .netdev instead.
      * Don't write .link files for MODEM devices, as they aren't supported by networkd.
@@ -374,9 +373,7 @@ write_link_file(const NetplanNetDefinition* def, const char* rootdir, const char
         g_string_append_printf(s, "LargeReceiveOffload=%s\n",
         (def->large_receive_offload ? "true" : "false"));
 
-    orig_umask = umask(022);
-    _netplan_g_string_free_to_file(s, rootdir, path, ".link");
-    umask(orig_umask);
+    _netplan_g_string_free_to_file_with_permissions(s, rootdir, path, ".link", "root", "root", 0640);
 }
 
 STATIC gboolean
@@ -394,7 +391,7 @@ write_regdom(const NetplanNetDefinition* def, const char* rootdir, GError** erro
     g_string_append(s, "\n[Service]\nType=oneshot\n");
     g_string_append_printf(s, "ExecStart="SBINDIR"/iw reg set %s\n", def->regulatory_domain);
 
-    _netplan_g_string_free_to_file(s, rootdir, path, NULL);
+    _netplan_g_string_free_to_file_with_permissions(s, rootdir, path, NULL, "root", "root", 0640);
     _netplan_safe_mkdir_p_dir(link);
     if (symlink(path, link) < 0 && errno != EEXIST) {
         // LCOV_EXCL_START
@@ -586,7 +583,6 @@ STATIC void
 write_netdev_file(const NetplanNetDefinition* def, const char* rootdir, const char* path)
 {
     GString* s = NULL;
-    mode_t orig_umask;
 
     g_assert(def->type >= NETPLAN_DEF_TYPE_VIRTUAL);
 
@@ -685,11 +681,7 @@ write_netdev_file(const NetplanNetDefinition* def, const char* rootdir, const ch
         default: g_assert_not_reached(); // LCOV_EXCL_LINE
     }
 
-    /* these do not contain secrets and need to be readable by
-     * systemd-networkd - LP: #1736965 */
-    orig_umask = umask(022);
-    _netplan_g_string_free_to_file(s, rootdir, path, ".netdev");
-    umask(orig_umask);
+    _netplan_g_string_free_to_file_with_permissions(s, rootdir, path, ".netdev", "root", NETWORKD_GROUP, 0640);
 }
 
 STATIC void
@@ -833,7 +825,6 @@ _netplan_netdef_write_network_file(
     g_autoptr(GString) network = NULL;
     g_autoptr(GString) link = NULL;
     GString* s = NULL;
-    mode_t orig_umask;
 
     SET_OPT_OUT_PTR(has_been_written, FALSE);
 
@@ -1099,11 +1090,7 @@ _netplan_netdef_write_network_file(
         if (network->len > 0)
             g_string_append_printf(s, "\n[Network]\n%s", network->str);
 
-        /* these do not contain secrets and need to be readable by
-         * systemd-networkd - LP: #1736965 */
-        orig_umask = umask(022);
-        _netplan_g_string_free_to_file(s, rootdir, path, ".network");
-        umask(orig_umask);
+        _netplan_g_string_free_to_file_with_permissions(s, rootdir, path, ".network", "root", NETWORKD_GROUP, 0640);
     }
 
     SET_OPT_OUT_PTR(has_been_written, TRUE);
@@ -1115,7 +1102,6 @@ write_rules_file(const NetplanNetDefinition* def, const char* rootdir)
 {
     GString* s = NULL;
     g_autofree char* path = g_strjoin(NULL, "run/udev/rules.d/99-netplan-", def->id, ".rules", NULL);
-    mode_t orig_umask;
 
     /* do we need to write a .rules file?
      * It's only required for reliably setting the name of a physical device
@@ -1149,9 +1135,7 @@ write_rules_file(const NetplanNetDefinition* def, const char* rootdir)
 
     g_string_append_printf(s, "NAME=\"%s\"\n", def->set_name);
 
-    orig_umask = umask(022);
-    _netplan_g_string_free_to_file(s, rootdir, path, NULL);
-    umask(orig_umask);
+    _netplan_g_string_free_to_file_with_permissions(s, rootdir, path, NULL, "root", "root", 0640);
 }
 
 STATIC gboolean
@@ -1300,7 +1284,6 @@ STATIC void
 write_wpa_unit(const NetplanNetDefinition* def, const char* rootdir)
 {
     g_autofree gchar *stdouth = NULL;
-    mode_t orig_umask;
 
     stdouth = systemd_escape(def->id);
 
@@ -1319,9 +1302,7 @@ write_wpa_unit(const NetplanNetDefinition* def, const char* rootdir)
     } else {
         g_string_append(s, " -Dnl80211,wext\n");
     }
-    orig_umask = umask(022);
-    _netplan_g_string_free_to_file(s, rootdir, path, NULL);
-    umask(orig_umask);
+    _netplan_g_string_free_to_file_with_permissions(s, rootdir, path, NULL, "root", "root", 0640);
 }
 
 STATIC gboolean
@@ -1330,7 +1311,6 @@ write_wpa_conf(const NetplanNetDefinition* def, const char* rootdir, GError** er
     GHashTableIter iter;
     GString* s = g_string_new("ctrl_interface=/run/wpa_supplicant\n\n");
     g_autofree char* path = g_strjoin(NULL, "run/netplan/wpa-", def->id, ".conf", NULL);
-    mode_t orig_umask;
 
     g_debug("%s: Creating wpa_supplicant configuration file %s", def->id, path);
     if (def->type == NETPLAN_DEF_TYPE_WIFI) {
@@ -1423,9 +1403,7 @@ write_wpa_conf(const NetplanNetDefinition* def, const char* rootdir, GError** er
     }
 
     /* use tight permissions as this contains secrets */
-    orig_umask = umask(077);
-    _netplan_g_string_free_to_file(s, rootdir, path, NULL);
-    umask(orig_umask);
+    _netplan_g_string_free_to_file_with_permissions(s, rootdir, path, NULL, "root", "root", 0600);
     return TRUE;
 }
 
@@ -1569,7 +1547,7 @@ _netplan_networkd_write_wait_online(const NetplanState* np_state, const char* ro
     GString* content = g_string_new("[Unit]\n"
         "ConditionPathIsSymbolicLink=/run/systemd/generator/network-online.target.wants/systemd-networkd-wait-online.service\n");
     if (g_hash_table_size(non_optional_interfaces) == 0) {
-        _netplan_g_string_free_to_file(content, rootdir, override, NULL);
+        _netplan_g_string_free_to_file_with_permissions(content, rootdir, override, NULL, "root", "root", 0640);
         g_hash_table_destroy(non_optional_interfaces);
         return FALSE;
     }
@@ -1593,7 +1571,7 @@ _netplan_networkd_write_wait_online(const NetplanState* np_state, const char* ro
     }
     g_string_append(content, "\n");
 
-    _netplan_g_string_free_to_file(content, rootdir, override, NULL);
+    _netplan_g_string_free_to_file_with_permissions(content, rootdir, override, NULL, "root", "root", 0640);
     g_hash_table_destroy(non_optional_interfaces);
     return TRUE;
 }

programming_language: C
filename: networkd.h
change_type: ModificationType.MODIFY
diff: @@ -20,6 +20,8 @@
 #include "netplan.h"
 #include <glib.h>
 
+#define NETWORKD_GROUP "systemd-network"
+
 NETPLAN_INTERNAL gboolean
 _netplan_netdef_write_networkd(
         const NetplanState* np_state,

programming_language: C
filename: nm.c
change_type: ModificationType.MODIFY
diff: @@ -1152,13 +1152,13 @@ netplan_state_finish_nm_write(
 
     /* write generated NetworkManager drop-in config */
     if (nm_conf->len > 0)
-        _netplan_g_string_free_to_file(nm_conf, rootdir, "run/NetworkManager/conf.d/netplan.conf", NULL);
+        _netplan_g_string_free_to_file_with_permissions(nm_conf, rootdir, "run/NetworkManager/conf.d/netplan.conf", NULL, "root", "root", 0640);
     else
         g_string_free(nm_conf, TRUE);
 
     /* write generated udev rules */
     if (udev_rules->len > 0)
-        _netplan_g_string_free_to_file(udev_rules, rootdir, "run/udev/rules.d/90-netplan.rules", NULL);
+        _netplan_g_string_free_to_file_with_permissions(udev_rules, rootdir, "run/udev/rules.d/90-netplan.rules", NULL, "root", "root", 0640);
     else
         g_string_free(udev_rules, TRUE);
 

programming_language: C
filename: openvswitch.c
change_type: ModificationType.MODIFY
diff: @@ -66,7 +66,7 @@ write_ovs_systemd_unit(const char* id, const GString* cmds, const char* rootdir,
         g_string_append(s, "StartLimitBurst=0\n");
     g_string_append(s, cmds->str);
 
-    _netplan_g_string_free_to_file(s, rootdir, path, NULL);
+    _netplan_g_string_free_to_file_with_permissions(s, rootdir, path, NULL, "root", "root", 0640);
 
     _netplan_safe_mkdir_p_dir(link);
     if (symlink(path, link) < 0 && errno != EEXIST) {

programming_language: C
filename: sriov.c
change_type: ModificationType.MODIFY
diff: @@ -54,7 +54,7 @@ write_sriov_rebind_systemd_unit(GHashTable* pfs, const char* rootdir, GError** e
     g_string_truncate(interfaces, interfaces->len-1); /* cut trailing whitespace */
     g_string_append_printf(s, "ExecStart=" SBINDIR "/netplan rebind --debug %s\n", interfaces->str);
 
-    _netplan_g_string_free_to_file(s, rootdir, path, NULL);
+    _netplan_g_string_free_to_file_with_permissions(s, rootdir, path, NULL, "root", "root", 0640);
     g_string_free(interfaces, TRUE);
 
     _netplan_safe_mkdir_p_dir(link);
@@ -90,7 +90,7 @@ write_sriov_apply_systemd_unit(GHashTable* pfs, const char* rootdir, GError** er
     g_string_append(s, "\n[Service]\nType=oneshot\n");
     g_string_append_printf(s, "ExecStart=" SBINDIR "/netplan apply --sriov-only\n");
 
-    _netplan_g_string_free_to_file(s, rootdir, path, NULL);
+    _netplan_g_string_free_to_file_with_permissions(s, rootdir, path, NULL, "root", "root", 0640);
 
     _netplan_safe_mkdir_p_dir(link);
     if (symlink(path, link) < 0 && errno != EEXIST) {

programming_language: C
filename: util-internal.h
change_type: ModificationType.MODIFY
diff: @@ -40,6 +40,9 @@ _netplan_safe_mkdir_p_dir(const char* file_path);
 NETPLAN_INTERNAL void
 _netplan_g_string_free_to_file(GString* s, const char* rootdir, const char* path, const char* suffix);
 
+void
+_netplan_g_string_free_to_file_with_permissions(GString* s, const char* rootdir, const char* path, const char* suffix, const char* owner, const char* group, mode_t mode);
+
 NETPLAN_INTERNAL void
 _netplan_unlink_glob(const char* rootdir, const char* _glob);
 

programming_language: C
filename: util.c
change_type: ModificationType.MODIFY
diff: @@ -23,6 +23,9 @@
 #include <regex.h>
 #include <string.h>
 #include <sys/mman.h>
+#include <sys/types.h>
+#include <pwd.h>
+#include <grp.h>
 
 #include <glib.h>
 #include <glib/gprintf.h>
@@ -87,6 +90,49 @@ void _netplan_g_string_free_to_file(GString* s, const char* rootdir, const char*
     }
 }
 
+void _netplan_g_string_free_to_file_with_permissions(GString* s, const char* rootdir, const char* path, const char* suffix, const char* owner, const char* group, mode_t mode)
+{
+    g_autofree char* full_path = NULL;
+    g_autofree char* path_suffix = NULL;
+    g_autofree char* contents = g_string_free(s, FALSE);
+    GError* error = NULL;
+    struct passwd* pw = NULL;
+    struct group* gr = NULL;
+    int ret = 0;
+
+    path_suffix = g_strjoin(NULL, path, suffix, NULL);
+    full_path = g_build_path(G_DIR_SEPARATOR_S, rootdir ?: G_DIR_SEPARATOR_S, path_suffix, NULL);
+    _netplan_safe_mkdir_p_dir(full_path);
+    if (!g_file_set_contents_full(full_path, contents, -1, G_FILE_SET_CONTENTS_CONSISTENT | G_FILE_SET_CONTENTS_ONLY_EXISTING, mode, &error)) {
+        /* the mkdir() just succeeded, there is no sensible
+         * method to test this without root privileges, bind mounts, and
+         * simulating ENOSPC */
+        // LCOV_EXCL_START
+        g_fprintf(stderr, "ERROR: cannot create file %s: %s\n", path, error->message);
+        exit(1);
+        // LCOV_EXCL_STOP
+    }
+
+    /* Here we take the owner and group names and look up for their IDs in the passwd and group files.
+     * It's OK to fail to set the owners and mode as this code will be called from unit tests.
+     * The autopkgtests will check if the owner/group and mode are correctly set.
+     */
+    pw = getpwnam(owner);
+    if (!pw) {
+        g_debug("Failed to determine the UID of user %s: %s", owner, strerror(errno)); // LCOV_EXCL_LINE
+    }
+    gr = getgrnam(group);
+    if (!gr) {
+        g_debug("Failed to determine the GID of group %s: %s", group, strerror(errno)); // LCOV_EXCL_LINE
+    }
+    if (pw && gr) {
+        ret = chown(full_path, pw->pw_uid, gr->gr_gid);
+        if (ret != 0) {
+            g_debug("Failed to set owner and group for file %s: %s", full_path, strerror(errno));
+        }
+    }
+}
+
 /**
  * Remove all files matching given glob.
  */

programming_language: C
filename: test_auth.py
change_type: ModificationType.MODIFY
diff: @@ -226,7 +226,7 @@ network={
 
         with open(os.path.join(self.workdir.name, 'run/systemd/system/netplan-wpa-eth0.service')) as f:
             self.assertEqual(f.read(), SD_WPA % {'iface': 'eth0', 'drivers': 'wired'})
-            self.assertEqual(stat.S_IMODE(os.fstat(f.fileno()).st_mode), 0o644)
+            self.assertEqual(stat.S_IMODE(os.fstat(f.fileno()).st_mode), 0o640)
         self.assertTrue(os.path.islink(os.path.join(
             self.workdir.name, 'run/systemd/system/systemd-networkd.service.wants/netplan-wpa-eth0.service')))
 

programming_language: Python
filename: test_wifis.py
change_type: ModificationType.MODIFY
diff: @@ -140,7 +140,7 @@ network={
             self.workdir.name, 'run/systemd/system/netplan-wpa-wl0.service')))
         with open(os.path.join(self.workdir.name, 'run/systemd/system/netplan-wpa-wl0.service')) as f:
             self.assertEqual(f.read(), SD_WPA % {'iface': 'wl0', 'drivers': 'nl80211,wext'})
-            self.assertEqual(stat.S_IMODE(os.fstat(f.fileno()).st_mode), 0o644)
+            self.assertEqual(stat.S_IMODE(os.fstat(f.fileno()).st_mode), 0o640)
         self.assertTrue(os.path.islink(os.path.join(
             self.workdir.name, 'run/systemd/system/systemd-networkd.service.wants/netplan-wpa-wl0.service')))
 

programming_language: Python
filename: base.py
change_type: ModificationType.MODIFY
diff: @@ -32,6 +32,8 @@ import shutil
 import gi
 import glob
 import json
+import pwd
+import grp
 
 # make sure we point to libnetplan properly.
 os.environ.update({'LD_LIBRARY_PATH': '.:{}'.format(os.environ.get('LD_LIBRARY_PATH'))})
@@ -375,6 +377,89 @@ class IntegrationTestsBase(unittest.TestCase):
             if state:
                 self.wait_output(['ip', 'addr', 'show', iface], state, 30)
 
+        # Assert file permissions
+        self.assert_file_permissions()
+
+    def assert_file_permissions(self):
+        """ Check if the generated files have the expected permissions """
+
+        nd_expected_mode = 0o100640
+        nd_expected_owner = 'root'
+        nd_expected_group = 'systemd-network'
+
+        sd_expected_mode = 0o100640
+        sd_expected_owner = 'root'
+        sd_expected_group = 'root'
+
+        udev_expected_mode = 0o100640
+        udev_expected_owner = 'root'
+        udev_expected_group = 'root'
+
+        nm_expected_mode = 0o100600
+        nm_expected_owner = 'root'
+        nm_expected_group = 'root'
+
+        wpa_expected_mode = 0o100600
+        wpa_expected_owner = 'root'
+        wpa_expected_group = 'root'
+
+        # Check systemd-networkd files
+        base_path = '/run/systemd/network'
+        files = glob.glob(f'{base_path}/*.network') + glob.glob(f'{base_path}/*.netdev')
+        for file in files:
+            res = os.stat(file)
+            user = pwd.getpwuid(res.st_uid)
+            group = grp.getgrgid(res.st_gid)
+            self.assertEqual(res.st_mode, nd_expected_mode, f'file {file}')
+            self.assertEqual(user.pw_name, nd_expected_owner, f'file {file}')
+            self.assertEqual(group.gr_name, nd_expected_group, f'file {file}')
+
+        # Check Network Manager files
+        base_path = '/run/NetworkManager/system-connections'
+        files = glob.glob(f'{base_path}/*.nmconnection')
+        for file in files:
+            res = os.stat(file)
+            user = pwd.getpwuid(res.st_uid)
+            group = grp.getgrgid(res.st_gid)
+            self.assertEqual(res.st_mode, nm_expected_mode, f'file {file}')
+            self.assertEqual(user.pw_name, nm_expected_owner, f'file {file}')
+            self.assertEqual(group.gr_name, nm_expected_group, f'file {file}')
+
+        # Check wpa_supplicant configuration files
+        base_path = '/run/netplan'
+        files = glob.glob(f'{base_path}/wpa-*.conf')
+        for file in files:
+            res = os.stat(file)
+            user = pwd.getpwuid(res.st_uid)
+            group = grp.getgrgid(res.st_gid)
+            self.assertEqual(res.st_mode, wpa_expected_mode, f'file {file}')
+            self.assertEqual(user.pw_name, wpa_expected_owner, f'file {file}')
+            self.assertEqual(group.gr_name, wpa_expected_group, f'file {file}')
+
+        # Check systemd service unit files
+        base_path = '/run/systemd/system/'
+        files = glob.glob(f'{base_path}/netplan-*.service')
+        files += glob.glob(f'{base_path}/systemd-networkd-wait-online.service.d/*.conf')
+        for file in files:
+            res = os.stat(file)
+            user = pwd.getpwuid(res.st_uid)
+            group = grp.getgrgid(res.st_gid)
+            self.assertEqual(res.st_mode, sd_expected_mode, f'file {file}')
+            self.assertEqual(user.pw_name, sd_expected_owner, f'file {file}')
+            self.assertEqual(group.gr_name, sd_expected_group, f'file {file}')
+
+        # Check systemd-udevd files
+        udev_path = '/run/udev/rules.d'
+        link_path = '/run/systemd/network'
+        files = glob.glob(f'{udev_path}/*-netplan*.rules') + glob.glob(f'{link_path}/*.link')
+        for file in files:
+            res = os.stat(file)
+            user = pwd.getpwuid(res.st_uid)
+            group = grp.getgrgid(res.st_gid)
+            self.assertEqual(res.st_mode, udev_expected_mode, f'file {file}')
+            self.assertEqual(user.pw_name, udev_expected_owner, f'file {file}')
+            self.assertEqual(group.gr_name, udev_expected_group, f'file {file}')
+
     def state(self, iface, state):
         '''Tell generate_and_settle() to wait for a specific state'''
         return iface + '/' + state

programming_language: Python