diff --git a/src/core/devices/nm-device-ethernet.c b/src/core/devices/nm-device-ethernet.c
index 97cf84a1b6..98e1194a54 100644
--- a/src/core/devices/nm-device-ethernet.c
+++ b/src/core/devices/nm-device-ethernet.c
@@ -627,10 +627,17 @@ build_supplicant_config(NMDeviceEthernet *self, GError **error)
     mtu      = nm_platform_link_get_mtu(nm_device_get_platform(NM_DEVICE(self)),
                                    nm_device_get_ifindex(NM_DEVICE(self)));
 
-    config = nm_supplicant_config_new(NM_SUPPL_CAP_MASK_NONE);
+    config = nm_supplicant_config_new(NM_SUPPL_CAP_MASK_NONE,
+                                      nm_utils_get_connection_first_permissions_user(connection));
 
     security = nm_connection_get_setting_802_1x(connection);
-    if (!nm_supplicant_config_add_setting_8021x(config, security, con_uuid, mtu, TRUE, error)) {
+    if (!nm_supplicant_config_add_setting_8021x(config,
+                                                security,
+                                                con_uuid,
+                                                mtu,
+                                                TRUE,
+                                                nm_device_get_private_files(NM_DEVICE(self)),
+                                                error)) {
         g_prefix_error(error, "802-1x-setting: ");
         g_clear_object(&config);
     }
diff --git a/src/core/devices/nm-device-macsec.c b/src/core/devices/nm-device-macsec.c
index 130708bb23..ae124475b5 100644
--- a/src/core/devices/nm-device-macsec.c
+++ b/src/core/devices/nm-device-macsec.c
@@ -199,7 +199,8 @@ build_supplicant_config(NMDeviceMacsec *self, GError **error)
     mtu      = nm_platform_link_get_mtu(nm_device_get_platform(NM_DEVICE(self)),
                                    nm_device_get_ifindex(NM_DEVICE(self)));
 
-    config = nm_supplicant_config_new(NM_SUPPL_CAP_MASK_NONE);
+    config = nm_supplicant_config_new(NM_SUPPL_CAP_MASK_NONE,
+                                      nm_utils_get_connection_first_permissions_user(connection));
 
     s_macsec = nm_device_get_applied_setting(NM_DEVICE(self), NM_TYPE_SETTING_MACSEC);
 
@@ -212,7 +213,13 @@ build_supplicant_config(NMDeviceMacsec *self, GError **error)
 
     if (nm_setting_macsec_get_mode(s_macsec) == NM_SETTING_MACSEC_MODE_EAP) {
         s_8021x = nm_connection_get_setting_802_1x(connection);
-        if (!nm_supplicant_config_add_setting_8021x(config, s_8021x, con_uuid, mtu, TRUE, error)) {
+        if (!nm_supplicant_config_add_setting_8021x(config,
+                                                    s_8021x,
+                                                    con_uuid,
+                                                    mtu,
+                                                    TRUE,
+                                                    nm_device_get_private_files(NM_DEVICE(self)),
+                                                    error)) {
             g_prefix_error(error, "802-1x-setting: ");
             return NULL;
         }
diff --git a/src/core/devices/nm-device-private.h b/src/core/devices/nm-device-private.h
index c597e05260..18daaed0bb 100644
--- a/src/core/devices/nm-device-private.h
+++ b/src/core/devices/nm-device-private.h
@@ -180,4 +180,8 @@ void nm_device_auth_request(NMDevice                      *self,
                             NMManagerDeviceAuthRequestFunc callback,
                             gpointer                       user_data);
 
+void nm_device_link_properties_set(NMDevice *self, gboolean reapply);
+
+GHashTable *nm_device_get_private_files(NMDevice *self);
+
 #endif /* NM_DEVICE_PRIVATE_H */
diff --git a/src/core/devices/nm-device-utils.c b/src/core/devices/nm-device-utils.c
index 2bf24ae6da..6b627ee0f4 100644
--- a/src/core/devices/nm-device-utils.c
+++ b/src/core/devices/nm-device-utils.c
@@ -222,7 +222,7 @@ resolve_addr_helper_cb(GObject *source, GAsyncResult *result, gpointer user_data
     gs_free_error GError *error  = NULL;
     gs_free char         *output = NULL;
 
-    output = nm_utils_spawn_helper_finish(result, &error);
+    output = nm_utils_spawn_helper_finish_string(result, &error);
     if (nm_utils_error_is_cancelled(error))
         return;
 
@@ -237,8 +237,9 @@ resolve_addr_spawn_helper(ResolveAddrInfo *info)
     char addr_str[NM_INET_ADDRSTRLEN];
 
     nm_inet_ntop(info->addr_family, &info->address, addr_str);
-    _LOG2D(info, "start lookup via nm-daemon-helper");
-    nm_utils_spawn_helper(NM_MAKE_STRV("resolve-address", addr_str),
+    _LOG2D(info, "start lookup via nm-daemon-helper");
+    nm_utils_spawn_helper(NM_MAKE_STRV("resolve-address", addr_str),
+                          FALSE,
                           g_task_get_cancellable(info->task),
                           resolve_addr_helper_cb,
                           info);
diff --git a/src/core/devices/nm-device.c b/src/core/devices/nm-device.c
index d080b540a5..e37d35945a 100644
--- a/src/core/devices/nm-device.c
+++ b/src/core/devices/nm-device.c
@@ -326,6 +326,12 @@ typedef struct {
     int           addr_family;
 } HostnameResolver;
 
+typedef enum {
+    PRIVATE_FILES_STATE_UNKNOWN = 0,
+    PRIVATE_FILES_STATE_READING,
+    PRIVATE_FILES_STATE_DONE,
+} PrivateFilesState;
+
 /*****************************************************************************/
 
 enum {
@@ -756,6 +762,13 @@ typedef struct _NMDevicePrivate {
         guint64  rx_bytes;
     } stats;
 
+    struct {
+        GHashTable       *table;
+        GCancellable     *cancellable;
+        char             *user;
+        PrivateFilesState state;
+    } private_files;
+
     bool mtu_force_set_done : 1;
 
     bool needs_ip6_subnet : 1;
@@ -9737,6 +9750,49 @@ tc_commit(NMDevice *self)
     return TRUE;
 }
 
+static void
+read_private_files_cb(GObject *source_object, GAsyncResult *result, gpointer data)
+{
+    gs_unref_hashtable GHashTable *table = NULL;
+    gs_free_error GError          *error = NULL;
+    NMDevice                      *self;
+    NMDevicePrivate               *priv;
+
+    table = nm_utils_read_private_files_finish(result, &error);
+    if (nm_utils_error_is_cancelled(error))
+        return;
+
+    self = NM_DEVICE(data);
+    priv = NM_DEVICE_GET_PRIVATE(self);
+
+    if (error) {
+        NMConnection *connection = nm_device_get_applied_connection(self);
+
+        _LOGW(LOGD_DEVICE,
+              "could not read files for private connection %s owned by user '%s': %s",
+              connection ? nm_connection_get_uuid(connection) : NULL,
+              priv->private_files.user,
+              error->message);
+        nm_device_state_changed(self, NM_DEVICE_STATE_FAILED, NM_DEVICE_STATE_REASON_CONFIG_FAILED);
+        return;
+    }
+
+    _LOGD(LOGD_DEVICE, "private files successfully read");
+
+    priv->private_files.state = PRIVATE_FILES_STATE_DONE;
+    priv->private_files.table = g_steal_pointer(&table);
+    g_clear_pointer(&priv->private_files.user, g_free);
+    g_clear_object(&priv->private_files.cancellable);
+
+    nm_device_activate_schedule_stage2_device_config(self, FALSE);
+}
+
+GHashTable *
+nm_device_get_private_files(NMDevice *self)
+{
+    return NM_DEVICE_GET_PRIVATE(self)->private_files.table;
+}
+
 /*
  * activate_stage2_device_config
  *
@@ -9749,6 +9805,7 @@ activate_stage2_device_config(NMDevice *self)
 {
     NMDevicePrivate *priv = NM_DEVICE_GET_PRIVATE(self);
     NMDeviceClass   *klass;
+    NMConnection    *applied;
     NMActStageReturn ret;
     NMSettingWired  *s_wired;
     gboolean         no_firmware = FALSE;
@@ -9757,6 +9814,68 @@ activate_stage2_device_config(NMDevice *self)
 
     nm_device_state_changed(self, NM_DEVICE_STATE_CONFIG, NM_DEVICE_STATE_REASON_NONE);
 
+    applied = nm_device_get_applied_connection(self);
+
+    /* If the connection is private (owned by a specific user), we need to
+     * verify that the user has permission to access any files specified in
+     * the connection, such as certificates and keys. We do that by calling
+     * nm_utils_read_private_files() and saving the file contents in a hash
+     * table that can be accessed later during the activation. It is important
+     * to never access the files again to avoid TOCTOU bugs.
+     */
+    switch (priv->private_files.state) {
+    case PRIVATE_FILES_STATE_UNKNOWN:
+    {
+        gs_free const char **paths = NULL;
+        NMSettingConnection *s_con;
+        const char          *user;
+
+        s_con = nm_connection_get_setting_connection(applied);
+        nm_assert(s_con);
+        user = _nm_setting_connection_get_first_permissions_user(s_con);
+
+        priv->private_files.user = g_strdup(user);
+        if (!priv->private_files.user) {
+            priv->private_files.state = PRIVATE_FILES_STATE_DONE;
+            break;
+        }
+
+        paths = nm_utils_get_connection_private_files_paths(applied);
+        if (!paths) {
+            priv->private_files.state = PRIVATE_FILES_STATE_DONE;
+            break;
+        }
+
+        if (_nm_setting_connection_get_num_permissions_users(s_con) > 1) {
+            _LOGW(LOGD_DEVICE,
+                  "private connections with multiple users are not allowed to reference "
+                  "certificates and keys on the filesystem. Specify only one user in the "
+                  "connection.permissions property.");
+            nm_device_state_changed(self,
+                                    NM_DEVICE_STATE_FAILED,
+                                    NM_DEVICE_STATE_REASON_CONFIG_FAILED);
+            return;
+        }
+
+        priv->private_files.state       = PRIVATE_FILES_STATE_READING;
+        priv->private_files.cancellable = g_cancellable_new();
+
+        _LOGD(LOGD_DEVICE, "reading private files");
+        nm_utils_read_private_files(paths,
+                                    priv->private_files.user,
+                                    priv->private_files.cancellable,
+                                    read_private_files_cb,
+                                    self);
+        return;
+    }
+    case PRIVATE_FILES_STATE_READING:
+        /* wait */
+        return;
+    case PRIVATE_FILES_STATE_DONE:
+        /* proceed */
+        break;
+    }
+
     if (!nm_device_sys_iface_state_is_external_or_assume(self))
         _ethtool_state_set(self);
 
@@ -15537,6 +15656,12 @@ nm_device_cleanup(NMDevice *self, NMDeviceStateReason reason, CleanupType cleanu
     if (NM_DEVICE_GET_CLASS(self)->deactivate)
         NM_DEVICE_GET_CLASS(self)->deactivate(self);
 
+    /* Clean up private files */
+    nm_clear_g_cancellable(&priv->private_files.cancellable);
+    g_clear_pointer(&priv->private_files.table, g_hash_table_unref);
+    g_clear_pointer(&priv->private_files.user, g_free);
+    priv->private_files.state = PRIVATE_FILES_STATE_UNKNOWN;
+
     ifindex = nm_device_get_ip_ifindex(self);
 
     if (cleanup_type == CLEANUP_TYPE_DECONFIGURE) {
diff --git a/src/core/devices/wifi/nm-device-wifi.c b/src/core/devices/wifi/nm-device-wifi.c
index 03625f8d6d..e0803f56a7 100644
--- a/src/core/devices/wifi/nm-device-wifi.c
+++ b/src/core/devices/wifi/nm-device-wifi.c
@@ -2931,7 +2931,8 @@ build_supplicant_config(NMDeviceWifi         *self,
     s_wireless = nm_connection_get_setting_wireless(connection);
     g_return_val_if_fail(s_wireless != NULL, NULL);
 
-    config = nm_supplicant_config_new(nm_supplicant_interface_get_capabilities(priv->sup_iface));
+    config = nm_supplicant_config_new(nm_supplicant_interface_get_capabilities(priv->sup_iface),
+                                      nm_utils_get_connection_first_permissions_user(connection));
 
     /* Warn if AP mode may not be supported */
     if (nm_streq0(nm_setting_wireless_get_mode(s_wireless), NM_SETTING_WIRELESS_MODE_AP)
@@ -3007,6 +3008,7 @@ build_supplicant_config(NMDeviceWifi         *self,
                 mtu,
                 pmf,
                 fils,
+                nm_device_get_private_files(NM_DEVICE(self)),
                 error)) {
             g_prefix_error(error, "802-11-wireless-security: ");
             goto error;
diff --git a/src/core/nm-core-utils.c b/src/core/nm-core-utils.c
index 28d9a788d6..a086f8a5ab 100644
--- a/src/core/nm-core-utils.c
+++ b/src/core/nm-core-utils.c
@@ -4867,6 +4867,7 @@ typedef struct {
     int      child_stdin;
     int      child_stdout;
     int      child_stderr;
+    gboolean binary_output;
     GSource *input_source;
     GSource *output_source;
     GSource *error_source;
@@ -4950,7 +4951,17 @@ helper_complete(HelperInfo *info, GError *error)
     }
 
     nm_clear_g_cancellable_disconnect(g_task_get_cancellable(info->task), &info->cancellable_id);
-    g_task_return_pointer(info->task, nm_str_buf_finalize(&info->in_buffer, NULL), g_free);
+
+    if (info->binary_output) {
+        g_task_return_pointer(
+            info->task,
+            g_bytes_new(nm_str_buf_get_str_unsafe(&info->in_buffer), info->in_buffer.len),
+            (GDestroyNotify) (g_bytes_unref));
+    } else {
+        g_task_return_pointer(info->task,
+                              nm_str_buf_finalize(&info->in_buffer, NULL) ?: g_new0(char, 1),
+                              g_free);
+    }
     helper_info_free(info);
 }
 
@@ -5096,6 +5107,7 @@ helper_cancelled(GObject *object, gpointer user_data)
 
 void
 nm_utils_spawn_helper(const char *const  *args,
+                      gboolean            binary_output,
                       GCancellable       *cancellable,
                       GAsyncReadyCallback callback,
                       gpointer            cb_data)
@@ -5114,8 +5126,13 @@ nm_utils_spawn_helper(const char *const  *args,
         .child_stdin  = -1,
         .child_stdout = -1,
         .pid          = -1,
+        .binary_output = binary_output,
     };
 
+    /* Store if the caller requested binary output so that we can check later
+     * that the right result function is called. */
+    g_task_set_task_data(info->task, GINT_TO_POINTER(binary_output), NULL);
+
     if (!g_spawn_async_with_pipes("/",
                                   (char **) NM_MAKE_STRV(LIBEXECDIR "/nm-daemon-helper"),
                                   (char **) NM_MAKE_STRV(),
@@ -5208,11 +5225,25 @@ nm_utils_spawn_helper(const char *const  *args,
 }
 
 char *
-nm_utils_spawn_helper_finish(GAsyncResult *result, GError **error)
+nm_utils_spawn_helper_finish_string(GAsyncResult *result, GError **error)
+{
+    GTask *task = G_TASK(result);
+
+    nm_assert(nm_g_task_is_valid(result, NULL, nm_utils_spawn_helper));
+    /* Check binary_output */
+    nm_assert(GPOINTER_TO_INT(g_task_get_task_data(task)) == FALSE);
+
+    return g_task_propagate_pointer(task, error);
+}
+
+GBytes *
+nm_utils_spawn_helper_finish_binary(GAsyncResult *result, GError **error)
 {
     GTask *task = G_TASK(result);
 
     nm_assert(nm_g_task_is_valid(result, NULL, nm_utils_spawn_helper));
+    /* Check binary_output */
+    nm_assert(GPOINTER_TO_INT(g_task_get_task_data(task)) == TRUE);
 
     return g_task_propagate_pointer(task, error);
 }
@@ -5319,3 +5350,185 @@ nm_utils_shorten_hostname(const char *hostname, char **shortened)
     *shortened = g_steal_pointer(&s);
     return TRUE;
 }
+
+const char *
+nm_utils_get_connection_first_permissions_user(NMConnection *connection)
+{
+    NMSettingConnection *s_con;
+
+    s_con = nm_connection_get_setting_connection(connection);
+    nm_assert(s_con);
+
+    return _nm_setting_connection_get_first_permissions_user(s_con);
+}
+
+/*****************************************************************************/
+
+const char **
+nm_utils_get_connection_private_files_paths(NMConnection *connection)
+{
+    GPtrArray          *files;
+    gs_free NMSetting **settings = NULL;
+    guint               num_settings;
+    guint               i;
+
+    files    = g_ptr_array_new();
+    settings = nm_connection_get_settings(connection, &num_settings);
+    for (i = 0; i < num_settings; i++) {
+        _nm_setting_get_private_files(settings[i], files);
+    }
+    g_ptr_array_add(files, NULL);
+
+    return (const char **) g_ptr_array_free(files, files->len == 1);
+}
+
+typedef struct _ReadInfo ReadInfo;
+
+typedef struct {
+    char     *path;
+    ReadInfo *read_info;
+} FileInfo;
+
+struct _ReadInfo {
+    GTask      *task;
+    GHashTable *table;
+    GPtrArray  *file_infos; /* of FileInfo */
+    GError     *first_error;
+    guint       num_pending;
+};
+
+static void
+read_file_helper_cb(GObject *source, GAsyncResult *result, gpointer user_data)
+{
+    FileInfo              *file_info = user_data;
+    ReadInfo              *read_info = file_info->read_info;
+    gs_unref_bytes GBytes *output    = NULL;
+    gs_free_error GError  *error     = NULL;
+
+    output = nm_utils_spawn_helper_finish_binary(result, &error);
+
+    nm_assert(read_info->num_pending > 0);
+    read_info->num_pending--;
+
+    if (nm_utils_error_is_cancelled(error)) {
+        /* nop */
+    } else if (error) {
+        nm_log_dbg(LOGD_CORE,
+                   "read-private-files: failed to read file '%s': %s",
+                   file_info->path,
+                   error->message);
+        if (!read_info->first_error) {
+            /* @error just says "helper process exited with status X".
+             * Return a more human-friendly one. */
+            read_info->first_error = g_error_new(NM_UTILS_ERROR,
+                                                 NM_UTILS_ERROR_UNKNOWN,
+                                                 "error reading file '%s'",
+                                                 file_info->path);
+        }
+    } else {
+        nm_log_dbg(LOGD_SUPPLICANT,
+                   "read-private-files: successfully read file '%s'",
+                   file_info->path);
+
+        /* Store the file contents in the hash table */
+        if (!read_info->table) {
+            read_info->table = g_hash_table_new_full(nm_str_hash,
+                                                     g_str_equal,
+                                                     g_free,
+                                                     (GDestroyNotify) g_bytes_unref);
+        }
+        g_hash_table_insert(read_info->table,
+                            g_steal_pointer(&file_info->path),
+                            g_steal_pointer(&output));
+    }
+
+    g_clear_pointer(&file_info->path, g_free);
+
+    /* If all operations are completed, return  */
+    if (read_info->num_pending == 0) {
+        if (read_info->first_error) {
+            g_task_return_error(read_info->task, g_steal_pointer(&read_info->first_error));
+        } else {
+            g_task_return_pointer(read_info->task,
+                                  g_steal_pointer(&read_info->table),
+                                  (GDestroyNotify) g_hash_table_unref);
+        }
+
+        if (read_info->table)
+            g_hash_table_unref(read_info->table);
+        if (read_info->file_infos)
+            g_ptr_array_unref(read_info->file_infos);
+
+        g_object_unref(read_info->task);
+        g_free(read_info);
+    }
+}
+
+/**
+ * nm_utils_read_private_files:
+ * @paths: array of file paths to be read
+ * @user: name of the user to impersonate when reading the files
+ * @cancellable: cancellable to cancel the operation
+ * @callback: callback to invoke on completion
+ * @cb_data: data for @callback
+ *
+ * Reads the given list of files @paths on behalf of user @user. Invokes
+ * @callback asynchronously on completion. The callback must use
+ * nm_utils_read_private_files_finish() to obtain the result.
+ */
+void
+nm_utils_read_private_files(const char *const  *paths,
+                            const char         *user,
+                            GCancellable       *cancellable,
+                            GAsyncReadyCallback callback,
+                            gpointer            cb_data)
+{
+    ReadInfo *read_info;
+    FileInfo *file_info;
+    guint     i;
+
+    g_return_if_fail(paths && paths[0]);
+    g_return_if_fail(cancellable);
+    g_return_if_fail(callback);
+    g_return_if_fail(cb_data);
+
+    read_info  = g_new(ReadInfo, 1);
+    *read_info = (ReadInfo) {
+        .task = nm_g_task_new(NULL, cancellable, nm_utils_read_private_files, callback, cb_data),
+        .file_infos = g_ptr_array_new_with_free_func(g_free),
+    };
+
+    for (i = 0; paths[i]; i++) {
+        file_info  = g_new(FileInfo, 1);
+        *file_info = (FileInfo) {
+            .path      = g_strdup(paths[i]),
+            .read_info = read_info,
+        };
+        g_ptr_array_add(read_info->file_infos, file_info);
+        read_info->num_pending++;
+
+        nm_utils_spawn_helper(NM_MAKE_STRV("read-file-as-user", user, paths[i]),
+                              TRUE,
+                              cancellable,
+                              read_file_helper_cb,
+                              file_info);
+    }
+}
+
+/**
+ * nm_utils_read_private_files_finish:
+ * @result: the GAsyncResult
+ * @error: on return, the error
+ *
+ * Returns the files read by nm_utils_read_private_files(). The return value
+ * is a hash table {char * -> GBytes *}. Free it with g_hash_table_unref().
+ */
+GHashTable *
+nm_utils_read_private_files_finish(GAsyncResult *result, GError **error)
+{
+    GTask *task = G_TASK(result);
+
+    nm_assert(nm_g_task_is_valid(result, NULL, nm_utils_read_private_files));
+
+    return g_task_propagate_pointer(task, error);
+}
diff --git a/src/core/nm-core-utils.h b/src/core/nm-core-utils.h
index bc93649630..e9b31dc7b3 100644
--- a/src/core/nm-core-utils.h
+++ b/src/core/nm-core-utils.h
@@ -466,11 +466,13 @@ guint8 nm_wifi_utils_level_to_quality(int val);
 /*****************************************************************************/
 
 void nm_utils_spawn_helper(const char *const  *args,
+                           gboolean            binary_output,
                            GCancellable       *cancellable,
                            GAsyncReadyCallback callback,
                            gpointer            cb_data);
 
-char *nm_utils_spawn_helper_finish(GAsyncResult *result, GError **error);
+char   *nm_utils_spawn_helper_finish_string(GAsyncResult *result, GError **error);
+GBytes *nm_utils_spawn_helper_finish_binary(GAsyncResult *result, GError **error);
 
 /*****************************************************************************/
 
@@ -478,4 +480,19 @@ uid_t nm_utils_get_nm_uid(void);
 
 gid_t nm_utils_get_nm_gid(void);
 
+/*****************************************************************************/
+
+const char *nm_utils_get_connection_first_permissions_user(NMConnection *connection);
+
+/*****************************************************************************/
+
+const char **nm_utils_get_connection_private_files_paths(NMConnection *connection);
+
+void        nm_utils_read_private_files(const char *const  *paths,
+                                        const char         *user,
+                                        GCancellable       *cancellable,
+                                        GAsyncReadyCallback callback,
+                                        gpointer            cb_data);
+GHashTable *nm_utils_read_private_files_finish(GAsyncResult *result, GError **error);
+
 #endif /* __NM_CORE_UTILS_H__ */
diff --git a/src/core/supplicant/nm-supplicant-config.c b/src/core/supplicant/nm-supplicant-config.c
index 1d9372e09f..bdde994eeb 100644
--- a/src/core/supplicant/nm-supplicant-config.c
+++ b/src/core/supplicant/nm-supplicant-config.c
@@ -30,6 +30,7 @@ typedef struct {
 typedef struct {
     GHashTable    *config;
     GHashTable    *blobs;
+    char          *private_user;
     NMSupplCapMask capabilities;
     guint32        ap_scan;
     bool           fast_required : 1;
@@ -60,7 +61,7 @@ _get_capability(NMSupplicantConfigPrivate *priv, NMSupplCapType type)
 }
 
 NMSupplicantConfig *
-nm_supplicant_config_new(NMSupplCapMask capabilities)
+nm_supplicant_config_new(NMSupplCapMask capabilities, const char *private_user)
 {
     NMSupplicantConfigPrivate *priv;
     NMSupplicantConfig        *self;
@@ -69,6 +70,7 @@ nm_supplicant_config_new(NMSupplCapMask capabilities)
     priv = NM_SUPPLICANT_CONFIG_GET_PRIVATE(self);
 
     priv->capabilities = capabilities;
+    priv->private_user = g_strdup(private_user);
 
     return self;
 }
@@ -258,19 +260,19 @@ static gboolean
 nm_supplicant_config_add_blob_for_connection(NMSupplicantConfig *self,
                                              GBytes             *field,
                                              const char         *name,
-                                             const char         *con_uid,
+                                             const char         *con_uuid,
                                              GError            **error)
 {
     if (field && g_bytes_get_size(field)) {
-        gs_free char *uid = NULL;
+        gs_free char *blob_id = NULL;
         char         *p;
 
-        uid = g_strdup_printf("%s-%s", con_uid, name);
-        for (p = uid; *p; p++) {
+        blob_id = g_strdup_printf("%s-%s", con_uuid, name);
+        for (p = blob_id; *p; p++) {
             if (*p == '/')
                 *p = '-';
         }
-        if (!nm_supplicant_config_add_blob(self, name, field, uid, error))
+        if (!nm_supplicant_config_add_blob(self, name, field, blob_id, error))
             return FALSE;
     }
     return TRUE;
@@ -283,6 +285,7 @@ nm_supplicant_config_finalize(GObject *object)
 
     g_hash_table_destroy(priv->config);
     nm_clear_pointer(&priv->blobs, g_hash_table_destroy);
+    nm_clear_pointer(&priv->private_user, g_free);
 
     G_OBJECT_CLASS(nm_supplicant_config_parent_class)->finalize(object);
 }
@@ -822,6 +825,7 @@ nm_supplicant_config_add_setting_wireless_security(NMSupplicantConfig
                                                    guint32                       mtu,
                                                    NMSettingWirelessSecurityPmf  pmf,
                                                    NMSettingWirelessSecurityFils fils,
+                                                   GHashTable                   *files,
                                                    GError                      **error)
 {
     NMSupplicantConfigPrivate    *priv          = NM_SUPPLICANT_CONFIG_GET_PRIVATE(self);
@@ -1176,6 +1180,7 @@ nm_supplicant_config_add_setting_wireless_security(NMSupplicantConfig
                                                         con_uuid,
                                                         mtu,
                                                         FALSE,
+                                                        files,
                                                         error))
                 return FALSE;
         }
@@ -1257,6 +1262,7 @@ nm_supplicant_config_add_setting_8021x(NMSupplicantConfig *self,
                                        const char         *con_uuid,
                                        guint32             mtu,
                                        gboolean            wired,
+                                       GHashTable         *files,
                                        GError            **error)
 {
     NMSupplicantConfigPrivate    *priv;
@@ -1486,24 +1492,21 @@ nm_supplicant_config_add_setting_8021x(NMSupplicantConfig *self,
     }
 
     /* CA certificate */
+    path  = NULL;
+    bytes = NULL;
     if (ca_cert_override) {
-        if (!add_string_val(self, ca_cert_override, "ca_cert", FALSE, NULL, error))
-            return FALSE;
+        /* This is a build-time-configured system-wide file path, no need to pass
+         * it as a blob */
+        path = ca_cert_override;
     } else {
         switch (nm_setting_802_1x_get_ca_cert_scheme(setting)) {
         case NM_SETTING_802_1X_CK_SCHEME_BLOB:
             bytes = nm_setting_802_1x_get_ca_cert_blob(setting);
-            if (!nm_supplicant_config_add_blob_for_connection(self,
-                                                              bytes,
-                                                              "ca_cert",
-                                                              con_uuid,
-                                                              error))
-                return FALSE;
             break;
         case NM_SETTING_802_1X_CK_SCHEME_PATH:
             path = nm_setting_802_1x_get_ca_cert_path(setting);
-            if (!add_string_val(self, path, "ca_cert", FALSE, NULL, error))
-                return FALSE;
+            if (priv->private_user)
+                bytes = nm_g_hash_table_lookup(files, path);
             break;
         case NM_SETTING_802_1X_CK_SCHEME_PKCS11:
             if (!add_pkcs11_uri_with_pin(self,
@@ -1519,26 +1522,32 @@ nm_supplicant_config_add_setting_8021x(NMSupplicantConfig *self,
             break;
         }
     }
+    if (bytes) {
+        if (!nm_supplicant_config_add_blob_for_connection(self, bytes, "ca_cert", con_uuid, error))
+            return FALSE;
+    } else if (path) {
+        /* Private connections cannot use paths other than the system CA store */
+        g_return_val_if_fail(ca_cert_override || !priv->private_user, FALSE);
+        if (!add_string_val(self, path, "ca_cert", FALSE, NULL, error))
+            return FALSE;
+    }
 
     /* Phase 2 CA certificate */
+    path  = NULL;
+    bytes = NULL;
     if (ca_cert_override) {
-        if (!add_string_val(self, ca_cert_override, "ca_cert2", FALSE, NULL, error))
-            return FALSE;
+        /* This is a build-time-configured system-wide file path, no need to pass
+         * it as a blob */
+        path = ca_cert_override;
     } else {
         switch (nm_setting_802_1x_get_phase2_ca_cert_scheme(setting)) {
         case NM_SETTING_802_1X_CK_SCHEME_BLOB:
             bytes = nm_setting_802_1x_get_phase2_ca_cert_blob(setting);
-            if (!nm_supplicant_config_add_blob_for_connection(self,
-                                                              bytes,
-                                                              "ca_cert2",
-                                                              con_uuid,
-                                                              error))
-                return FALSE;
             break;
         case NM_SETTING_802_1X_CK_SCHEME_PATH:
             path = nm_setting_802_1x_get_phase2_ca_cert_path(setting);
-            if (!add_string_val(self, path, "ca_cert2", FALSE, NULL, error))
-                return FALSE;
+            if (priv->private_user)
+                bytes = nm_g_hash_table_lookup(files, path);
             break;
         case NM_SETTING_802_1X_CK_SCHEME_PKCS11:
             if (!add_pkcs11_uri_with_pin(
@@ -1555,6 +1564,15 @@ nm_supplicant_config_add_setting_8021x(NMSupplicantConfig *self,
             break;
         }
     }
+    if (bytes) {
+        if (!nm_supplicant_config_add_blob_for_connection(self, bytes, "ca_cert2", con_uuid, error))
+            return FALSE;
+    } else if (path) {
+        /* Private connections cannot use paths other than the system CA store */
+        g_return_val_if_fail(ca_cert_override || !priv->private_user, FALSE);
+        if (!add_string_val(self, path, "ca_cert2", FALSE, NULL, error))
+            return FALSE;
+    }
 
     /* Subject match */
     value = nm_setting_802_1x_get_subject_match(setting);
@@ -1606,21 +1624,17 @@ nm_supplicant_config_add_setting_8021x(NMSupplicantConfig *self,
 
     /* Private key */
     added = FALSE;
+    path  = NULL;
+    bytes = NULL;
     switch (nm_setting_802_1x_get_private_key_scheme(setting)) {
     case NM_SETTING_802_1X_CK_SCHEME_BLOB:
         bytes = nm_setting_802_1x_get_private_key_blob(setting);
-        if (!nm_supplicant_config_add_blob_for_connection(self,
-                                                          bytes,
-                                                          "private_key",
-                                                          con_uuid,
-                                                          error))
-            return FALSE;
         added = TRUE;
         break;
     case NM_SETTING_802_1X_CK_SCHEME_PATH:
         path = nm_setting_802_1x_get_private_key_path(setting);
-        if (!add_string_val(self, path, "private_key", FALSE, NULL, error))
-            return FALSE;
+        if (priv->private_user)
+            bytes = nm_g_hash_table_lookup(files, path);
         added = TRUE;
         break;
     case NM_SETTING_802_1X_CK_SCHEME_PKCS11:
@@ -1637,6 +1651,19 @@ nm_supplicant_config_add_setting_8021x(NMSupplicantConfig *self,
     default:
         break;
     }
+    if (bytes) {
+        if (!nm_supplicant_config_add_blob_for_connection(self,
+                                                          bytes,
+                                                          "private_key",
+                                                          con_uuid,
+                                                          error))
+            return FALSE;
+    } else if (path) {
+        /* Private connections cannot use paths */
+        g_return_val_if_fail(!priv->private_user, FALSE);
+        if (!add_string_val(self, path, "private_key", FALSE, NULL, error))
+            return FALSE;
+    }
 
     if (added) {
         NMSetting8021xCKFormat format;
@@ -1660,20 +1687,16 @@ nm_supplicant_config_add_setting_8021x(NMSupplicantConfig *self,
             /* Only add the client cert if the private key is not PKCS#12, as
              * wpa_supplicant configuration directs us to do.
              */
+            path  = NULL;
+            bytes = NULL;
             switch (nm_setting_802_1x_get_client_cert_scheme(setting)) {
             case NM_SETTING_802_1X_CK_SCHEME_BLOB:
                 bytes = nm_setting_802_1x_get_client_cert_blob(setting);
-                if (!nm_supplicant_config_add_blob_for_connection(self,
-                                                                  bytes,
-                                                                  "client_cert",
-                                                                  con_uuid,
-                                                                  error))
-                    return FALSE;
                 break;
             case NM_SETTING_802_1X_CK_SCHEME_PATH:
                 path = nm_setting_802_1x_get_client_cert_path(setting);
-                if (!add_string_val(self, path, "client_cert", FALSE, NULL, error))
-                    return FALSE;
+                if (priv->private_user)
+                    bytes = nm_g_hash_table_lookup(files, path);
                 break;
             case NM_SETTING_802_1X_CK_SCHEME_PKCS11:
                 if (!add_pkcs11_uri_with_pin(
@@ -1689,26 +1712,35 @@ nm_supplicant_config_add_setting_8021x(NMSupplicantConfig *self,
             default:
                 break;
             }
+            if (bytes) {
+                if (!nm_supplicant_config_add_blob_for_connection(self,
+                                                                  bytes,
+                                                                  "client_cert",
+                                                                  con_uuid,
+                                                                  error))
+                    return FALSE;
+            } else if (path) {
+                /* Private connections cannot use paths */
+                g_return_val_if_fail(!priv->private_user, FALSE);
+                if (!add_string_val(self, path, "client_cert", FALSE, NULL, error))
+                    return FALSE;
+            }
         }
     }
 
     /* Phase 2 private key */
     added = FALSE;
+    path  = NULL;
+    bytes = NULL;
     switch (nm_setting_802_1x_get_phase2_private_key_scheme(setting)) {
     case NM_SETTING_802_1X_CK_SCHEME_BLOB:
         bytes = nm_setting_802_1x_get_phase2_private_key_blob(setting);
-        if (!nm_supplicant_config_add_blob_for_connection(self,
-                                                          bytes,
-                                                          "private_key2",
-                                                          con_uuid,
-                                                          error))
-            return FALSE;
         added = TRUE;
         break;
     case NM_SETTING_802_1X_CK_SCHEME_PATH:
         path = nm_setting_802_1x_get_phase2_private_key_path(setting);
-        if (!add_string_val(self, path, "private_key2", FALSE, NULL, error))
-            return FALSE;
+        if (priv->private_user)
+            bytes = nm_g_hash_table_lookup(files, path);
         added = TRUE;
         break;
     case NM_SETTING_802_1X_CK_SCHEME_PKCS11:
@@ -1726,6 +1758,19 @@ nm_supplicant_config_add_setting_8021x(NMSupplicantConfig *self,
     default:
         break;
     }
+    if (bytes) {
+        if (!nm_supplicant_config_add_blob_for_connection(self,
+                                                          bytes,
+                                                          "private_key2",
+                                                          con_uuid,
+                                                          error))
+            return FALSE;
+    } else if (path) {
+        /* Private connections cannot use paths */
+        g_return_val_if_fail(!priv->private_user, FALSE);
+        if (!add_string_val(self, path, "private_key2", FALSE, NULL, error))
+            return FALSE;
+    }
 
     if (added) {
         NMSetting8021xCKFormat format;
@@ -1749,20 +1794,16 @@ nm_supplicant_config_add_setting_8021x(NMSupplicantConfig *self,
             /* Only add the client cert if the private key is not PKCS#12, as
              * wpa_supplicant configuration directs us to do.
              */
+            path  = NULL;
+            bytes = NULL;
             switch (nm_setting_802_1x_get_phase2_client_cert_scheme(setting)) {
             case NM_SETTING_802_1X_CK_SCHEME_BLOB:
                 bytes = nm_setting_802_1x_get_phase2_client_cert_blob(setting);
-                if (!nm_supplicant_config_add_blob_for_connection(self,
-                                                                  bytes,
-                                                                  "client_cert2",
-                                                                  con_uuid,
-                                                                  error))
-                    return FALSE;
                 break;
             case NM_SETTING_802_1X_CK_SCHEME_PATH:
                 path = nm_setting_802_1x_get_phase2_client_cert_path(setting);
-                if (!add_string_val(self, path, "client_cert2", FALSE, NULL, error))
-                    return FALSE;
+                if (priv->private_user)
+                    bytes = nm_g_hash_table_lookup(files, path);
                 break;
             case NM_SETTING_802_1X_CK_SCHEME_PKCS11:
                 if (!add_pkcs11_uri_with_pin(
@@ -1778,6 +1819,19 @@ nm_supplicant_config_add_setting_8021x(NMSupplicantConfig *self,
             default:
                 break;
             }
+            if (bytes) {
+                if (!nm_supplicant_config_add_blob_for_connection(self,
+                                                                  bytes,
+                                                                  "client_cert2",
+                                                                  con_uuid,
+                                                                  error))
+                    return FALSE;
+            } else if (path) {
+                /* Private connections cannot use paths */
+                g_return_val_if_fail(!priv->private_user, FALSE);
+                if (!add_string_val(self, path, "client_cert2", FALSE, NULL, error))
+                    return FALSE;
+            }
         }
     }
 
diff --git a/src/core/supplicant/nm-supplicant-config.h b/src/core/supplicant/nm-supplicant-config.h
index 585cf9588d..7033fe7af4 100644
--- a/src/core/supplicant/nm-supplicant-config.h
+++ b/src/core/supplicant/nm-supplicant-config.h
@@ -29,7 +29,7 @@ typedef struct _NMSupplicantConfigClass NMSupplicantConfigClass;
 
 GType nm_supplicant_config_get_type(void);
 
-NMSupplicantConfig *nm_supplicant_config_new(NMSupplCapMask capabilities);
+NMSupplicantConfig *nm_supplicant_config_new(NMSupplCapMask capabilities, const char *private_user);
 
 guint32 nm_supplicant_config_get_ap_scan(NMSupplicantConfig *self);
 
@@ -57,6 +57,7 @@ gboolean nm_supplicant_config_add_setting_wireless_security(NMSupplicantConfig
                                                             guint32         mtu,
                                                             NMSettingWirelessSecurityPmf  pmf,
                                                             NMSettingWirelessSecurityFils fils,
+                                                            GHashTable                   *files,
                                                             GError                      **error);
 
 gboolean nm_supplicant_config_add_no_security(NMSupplicantConfig *self, GError **error);
@@ -66,6 +67,7 @@ gboolean nm_supplicant_config_add_setting_8021x(NMSupplicantConfig *self,
                                                 const char         *con_uuid,
                                                 guint32             mtu,
                                                 gboolean            wired,
+                                                GHashTable         *files,
                                                 GError            **error);
 
 gboolean nm_supplicant_config_add_setting_macsec(NMSupplicantConfig *self,
diff --git a/src/core/supplicant/nm-supplicant-interface.c b/src/core/supplicant/nm-supplicant-interface.c
index 18525724f4..247cc81fe8 100644
--- a/src/core/supplicant/nm-supplicant-interface.c
+++ b/src/core/supplicant/nm-supplicant-interface.c
@@ -46,6 +46,7 @@ typedef struct {
     gpointer                     user_data;
     guint                        fail_on_idle_id;
     guint                        blobs_left;
+    guint                        remove_blobs_left;
     guint                        calls_left;
     struct _AddNetworkData      *add_network_data;
 } AssocData;
@@ -2257,6 +2258,7 @@ assoc_add_blob_cb(GObject *source, GAsyncResult *result, gpointer user_data)
         return;
     }
 
+    nm_assert(priv->assoc_data->blobs_left > 0);
     priv->assoc_data->blobs_left--;
     _LOGT("assoc[" NM_HASH_OBFUSCATE_PTR_FMT "]: blob added (%u left)",
           NM_HASH_OBFUSCATE_PTR(priv->assoc_data),
@@ -2265,6 +2267,148 @@ assoc_add_blob_cb(GObject *source, GAsyncResult *result, gpointer user_data)
         assoc_call_select_network(self);
 }
 
+static void
+assoc_add_blobs(NMSupplicantInterface *self)
+{
+    NMSupplicantInterfacePrivate *priv = NM_SUPPLICANT_INTERFACE_GET_PRIVATE(self);
+    GHashTable                   *blobs;
+    GHashTableIter                iter;
+    const char                   *blob_name;
+    GBytes                       *blob_data;
+
+    blobs                        = nm_supplicant_config_get_blobs(priv->assoc_data->cfg);
+    priv->assoc_data->blobs_left = nm_g_hash_table_size(blobs);
+
+    _LOGT("assoc[" NM_HASH_OBFUSCATE_PTR_FMT "]: need to add %u blobs",
+          NM_HASH_OBFUSCATE_PTR(priv->assoc_data),
+          priv->assoc_data->blobs_left);
+
+    if (priv->assoc_data->blobs_left == 0) {
+        assoc_call_select_network(self);
+        return;
+    }
+
+    g_hash_table_iter_init(&iter, blobs);
+    while (g_hash_table_iter_next(&iter, (gpointer) &blob_name, (gpointer) &blob_data)) {
+        _LOGT("assoc[" NM_HASH_OBFUSCATE_PTR_FMT "]: adding blob '%s'",
+              NM_HASH_OBFUSCATE_PTR(priv->assoc_data),
+              blob_name);
+        _dbus_connection_call(
+            self,
+            NM_WPAS_DBUS_IFACE_INTERFACE,
+            "AddBlob",
+            g_variant_new("(s@ay)", blob_name, nm_g_bytes_to_variant_ay(blob_data)),
+            G_VARIANT_TYPE("()"),
+            G_DBUS_CALL_FLAGS_NONE,
+            DBUS_TIMEOUT_MSEC,
+            priv->assoc_data->cancellable,
+            assoc_add_blob_cb,
+            self);
+    }
+}
+
+static void
+assoc_remove_blob_cb(GObject *source, GAsyncResult *result, gpointer user_data)
+{
+    NMSupplicantInterface        *self;
+    NMSupplicantInterfacePrivate *priv;
+    gs_free_error GError         *error = NULL;
+    gs_unref_variant GVariant    *res   = NULL;
+
+    res = g_dbus_connection_call_finish(G_DBUS_CONNECTION(source), result, &error);
+    if (nm_utils_error_is_cancelled(error))
+        return;
+
+    self = NM_SUPPLICANT_INTERFACE(user_data);
+    priv = NM_SUPPLICANT_INTERFACE_GET_PRIVATE(self);
+
+    /* We don't consider a failure fatal. The new association might be able
+     * to proceed even with the existing blobs, if they don't conflict with new
+     * ones. */
+
+    nm_assert(priv->assoc_data->remove_blobs_left > 0);
+    priv->assoc_data->remove_blobs_left--;
+
+    if (error) {
+        g_dbus_error_strip_remote_error(error);
+        _LOGD("assoc[" NM_HASH_OBFUSCATE_PTR_FMT "]: failed to delete blob: %s",
+              NM_HASH_OBFUSCATE_PTR(priv->assoc_data),
+              error->message);
+    } else {
+        _LOGT("assoc[" NM_HASH_OBFUSCATE_PTR_FMT "]: blob removed (%u left)",
+              NM_HASH_OBFUSCATE_PTR(priv->assoc_data),
+              priv->assoc_data->remove_blobs_left);
+    }
+
+    if (priv->assoc_data->remove_blobs_left == 0)
+        assoc_add_blobs(self);
+}
+
+static void
+assoc_get_blobs_cb(GObject *source, GAsyncResult *result, gpointer user_data)
+{
+    NMSupplicantInterface        *self;
+    NMSupplicantInterfacePrivate *priv;
+    gs_free_error GError         *error = NULL;
+    gs_unref_variant GVariant    *res   = NULL;
+    gs_unref_variant GVariant    *value = NULL;
+    GVariantIter                  iter;
+    const char                   *blob_name;
+    GVariant                     *blob_data;
+
+    res = g_dbus_connection_call_finish(G_DBUS_CONNECTION(source), result, &error);
+    if (nm_utils_error_is_cancelled(error))
+        return;
+
+    self = NM_SUPPLICANT_INTERFACE(user_data);
+    priv = NM_SUPPLICANT_INTERFACE_GET_PRIVATE(self);
+
+    if (error) {
+        _LOGD("assoc[" NM_HASH_OBFUSCATE_PTR_FMT "]: failed to get blob list: %s",
+              NM_HASH_OBFUSCATE_PTR(priv->assoc_data),
+              error->message);
+        assoc_add_blobs(self);
+        return;
+    }
+
+    g_variant_get(res, "(v)", &value);
+
+    /* While the "Blobs" property is documented as type "as", it is actually "a{say}" */
+    if (!value || !g_variant_is_of_type(value, G_VARIANT_TYPE("a{say}"))) {
+        _LOGD("assoc[" NM_HASH_OBFUSCATE_PTR_FMT "]: failed to get blob list: wrong return type %s",
+              NM_HASH_OBFUSCATE_PTR(priv->assoc_data),
+              value ? g_variant_get_type_string(value) : "NULL");
+        assoc_add_blobs(self);
+        return;
+    }
+
+    g_variant_iter_init(&iter, value);
+    priv->assoc_data->remove_blobs_left = g_variant_iter_n_children(&iter);
+    _LOGT("assoc[" NM_HASH_OBFUSCATE_PTR_FMT "]: need to delete %u blobs",
+          NM_HASH_OBFUSCATE_PTR(priv->assoc_data),
+          priv->assoc_data->remove_blobs_left);
+
+    if (priv->assoc_data->remove_blobs_left == 0) {
+        assoc_add_blobs(self);
+    } else {
+        while (g_variant_iter_loop(&iter, "{&s@ay}", &blob_name, &blob_data)) {
+            _LOGT("assoc[" NM_HASH_OBFUSCATE_PTR_FMT "]: removing blob '%s'",
+                  NM_HASH_OBFUSCATE_PTR(priv->assoc_data),
+                  blob_name);
+            _dbus_connection_call(self,
+                                  NM_WPAS_DBUS_IFACE_INTERFACE,
+                                  "RemoveBlob",
+                                  g_variant_new("(s)", blob_name),
+                                  G_VARIANT_TYPE("()"),
+                                  G_DBUS_CALL_FLAGS_NONE,
+                                  DBUS_TIMEOUT_MSEC,
+                                  priv->assoc_data->cancellable,
+                                  assoc_remove_blob_cb,
+                                  self);
+        }
+    }
+}
+
 static void
 assoc_add_network_cb(GObject *source, GAsyncResult *result, gpointer user_data)
 {
@@ -2272,12 +2416,8 @@ assoc_add_network_cb(GObject *source, GAsyncResult *result, gpointer user_data)
     AssocData                      *assoc_data;
     NMSupplicantInterface          *self;
     NMSupplicantInterfacePrivate   *priv;
-    gs_unref_variant GVariant      *res   = NULL;
-    gs_free_error GError           *error = NULL;
-    GHashTable                     *blobs;
-    GHashTableIter                  iter;
-    const char                     *blob_name;
-    GBytes                         *blob_data;
+    gs_unref_variant GVariant      *res         = NULL;
+    gs_free_error GError           *error       = NULL;
     nm_auto_ref_string NMRefString *name_owner  = NULL;
     nm_auto_ref_string NMRefString *object_path = NULL;
 
@@ -2329,34 +2469,21 @@ assoc_add_network_cb(GObject *source, GAsyncResult *result, gpointer user_data)
     nm_assert(!priv->net_path);
     g_variant_get(res, "(o)", &priv->net_path);
 
-    /* Send blobs first; otherwise jump to selecting the network */
-    blobs                        = nm_supplicant_config_get_blobs(priv->assoc_data->cfg);
-    priv->assoc_data->blobs_left = blobs ? g_hash_table_size(blobs) : 0u;
-
-    _LOGT("assoc[" NM_HASH_OBFUSCATE_PTR_FMT "]: network added (%s) (%u blobs left)",
+    _LOGT("assoc[" NM_HASH_OBFUSCATE_PTR_FMT "]: network added (%s)",
           NM_HASH_OBFUSCATE_PTR(priv->assoc_data),
-          priv->net_path,
-          priv->assoc_data->blobs_left);
-
-    if (priv->assoc_data->blobs_left == 0) {
-        assoc_call_select_network(self);
-        return;
-    }
+          priv->net_path);
 
-    g_hash_table_iter_init(&iter, blobs);
-    while (g_hash_table_iter_next(&iter, (gpointer) &blob_name, (gpointer) &blob_data)) {
-        _dbus_connection_call(
-            self,
-            NM_WPAS_DBUS_IFACE_INTERFACE,
-            "AddBlob",
-            g_variant_new("(s@ay)", blob_name, nm_g_bytes_to_variant_ay(blob_data)),
-            G_VARIANT_TYPE("()"),
-            G_DBUS_CALL_FLAGS_NONE,
-            DBUS_TIMEOUT_MSEC,
-            priv->assoc_data->cancellable,
-            assoc_add_blob_cb,
-            self);
-    }
+    /* Delete any existing blobs before adding new ones */
+    _dbus_connection_call(self,
+                          DBUS_INTERFACE_PROPERTIES,
+                          "Get",
+                          g_variant_new("(ss)", NM_WPAS_DBUS_IFACE_INTERFACE, "Blobs"),
+                          G_VARIANT_TYPE("(v)"),
+                          G_DBUS_CALL_FLAGS_NONE,
+                          DBUS_TIMEOUT_MSEC,
+                          priv->assoc_data->cancellable,
+                          assoc_get_blobs_cb,
+                          self);
 }
 
 static void
diff --git a/src/core/supplicant/tests/test-supplicant-config.c b/src/core/supplicant/tests/test-supplicant-config.c
index 1ca5b26e56..416fe0054f 100644
--- a/src/core/supplicant/tests/test-supplicant-config.c
+++ b/src/core/supplicant/tests/test-supplicant-config.c
@@ -98,7 +98,8 @@ build_supplicant_config(NMConnection  *connection,
     NMSetting8021x                     *s_8021x;
     gboolean                            success;
 
-    config = nm_supplicant_config_new(capabilities);
+    config = nm_supplicant_config_new(capabilities,
+                                      nm_utils_get_connection_first_permissions_user(connection));
 
     s_wifi = nm_connection_get_setting_wireless(connection);
     g_assert(s_wifi);
@@ -120,6 +121,7 @@ build_supplicant_config(NMConnection  *connection,
                                                                mtu,
                                                                pmf,
                                                                fils,
+                                                               NULL,
                                                                &error);
     } else {
         success = nm_supplicant_config_add_no_security(config, &error);
diff --git a/src/libnm-core-impl/nm-setting-8021x.c b/src/libnm-core-impl/nm-setting-8021x.c
index 1bc4de9d88..20dca9f3f7 100644
--- a/src/libnm-core-impl/nm-setting-8021x.c
+++ b/src/libnm-core-impl/nm-setting-8021x.c
@@ -3115,6 +3115,86 @@ need_secrets(NMSetting *setting, gboolean check_rerequest)
 
 /*****************************************************************************/
 
+static void
+get_private_files(NMSetting *setting, GPtrArray *files)
+{
+    const struct {
+        const char *property;
+        NMSetting8021xCKScheme (*get_scheme_func)(NMSetting8021x *);
+        const char *(*get_path_func)(NMSetting8021x *);
+    } cert_props[] = {
+        {NM_SETTING_802_1X_CA_CERT,
+         nm_setting_802_1x_get_ca_cert_scheme,
+         nm_setting_802_1x_get_ca_cert_path},
+        {NM_SETTING_802_1X_CLIENT_CERT,
+         nm_setting_802_1x_get_client_cert_scheme,
+         nm_setting_802_1x_get_client_cert_path},
+        {NM_SETTING_802_1X_PRIVATE_KEY,
+         nm_setting_802_1x_get_private_key_scheme,
+         nm_setting_802_1x_get_private_key_path},
+        {NM_SETTING_802_1X_PHASE2_CA_CERT,
+         nm_setting_802_1x_get_phase2_ca_cert_scheme,
+         nm_setting_802_1x_get_phase2_ca_cert_path},
+        {NM_SETTING_802_1X_PHASE2_CLIENT_CERT,
+         nm_setting_802_1x_get_phase2_client_cert_scheme,
+         nm_setting_802_1x_get_phase2_client_cert_path},
+        {NM_SETTING_802_1X_PHASE2_PRIVATE_KEY,
+         nm_setting_802_1x_get_phase2_private_key_scheme,
+         nm_setting_802_1x_get_phase2_private_key_path},
+    };
+    NMSetting8021x *s_8021x = NM_SETTING_802_1X(setting);
+    const char     *path;
+    guint           i;
+
+    if (NM_MORE_ASSERT_ONCE(5)) {
+        GObjectClass        *klass;
+        gs_free GParamSpec **properties = NULL;
+        guint                n_properties;
+        gboolean             found;
+        guint                j;
+
+        /* Check that all the properties in the setting with flag CERT_KEY_FILE
+         * are listed in the table, and vice versa. */
+
+        klass = G_OBJECT_GET_CLASS(setting);
+
+        properties = g_object_class_list_properties(klass, &n_properties);
+        for (i = 0; i < n_properties; i++) {
+            if (!(properties[i]->flags & NM_SETTING_PARAM_CERT_KEY_FILE))
+                continue;
+
+            found = FALSE;
+            for (j = 0; j < G_N_ELEMENTS(cert_props); j++) {
+                if (nm_streq0(properties[i]->name, cert_props[j].property)) {
+                    found = TRUE;
+                    break;
+                }
+            }
+
+            nm_assert(found);
+        }
+
+        for (i = 0; i < G_N_ELEMENTS(cert_props); i++) {
+            GParamSpec *prop;
+
+            prop = g_object_class_find_property(klass, cert_props[i].property);
+            nm_assert(prop);
+            nm_assert(prop->flags & NM_SETTING_PARAM_CERT_KEY_FILE);
+        }
+    }
+
+    for (i = 0; i < G_N_ELEMENTS(cert_props); i++) {
+        if (cert_props[i].get_scheme_func(s_8021x) == NM_SETTING_802_1X_CK_SCHEME_PATH) {
+            path = cert_props[i].get_path_func(s_8021x);
+            if (path) {
+                g_ptr_array_add(files, (gpointer) path);
+            }
+        }
+    }
+}
+
+/*****************************************************************************/
+
 static void
 get_property(GObject *object, guint prop_id, GValue *value, GParamSpec *pspec)
 {
@@ -3207,8 +3287,9 @@ nm_setting_802_1x_class_init(NMSetting8021xClass *klass)
     object_class->set_property = set_property;
     object_class->finalize     = finalize;
 
-    setting_class->verify       = verify;
-    setting_class->need_secrets = need_secrets;
+    setting_class->verify            = verify;
+    setting_class->need_secrets      = need_secrets;
+    setting_class->get_private_files = get_private_files;
 
     /**
      * NMSetting8021x:eap:
@@ -3327,7 +3408,7 @@ nm_setting_802_1x_class_init(NMSetting8021xClass *klass)
                                              obj_properties,
                                              NM_SETTING_802_1X_CA_CERT,
                                              PROP_CA_CERT,
-                                             NM_SETTING_PARAM_NONE,
+                                             NM_SETTING_PARAM_CERT_KEY_FILE,
                                              NMSetting8021xPrivate,
                                              ca_cert);
 
@@ -3513,7 +3594,7 @@ nm_setting_802_1x_class_init(NMSetting8021xClass *klass)
                                              obj_properties,
                                              NM_SETTING_802_1X_CLIENT_CERT,
                                              PROP_CLIENT_CERT,
-                                             NM_SETTING_PARAM_NONE,
+                                             NM_SETTING_PARAM_CERT_KEY_FILE,
                                              NMSetting8021xPrivate,
                                              client_cert);
 
@@ -3740,7 +3821,7 @@ nm_setting_802_1x_class_init(NMSetting8021xClass *klass)
                                              obj_properties,
                                              NM_SETTING_802_1X_PHASE2_CA_CERT,
                                              PROP_PHASE2_CA_CERT,
-                                             NM_SETTING_PARAM_NONE,
+                                             NM_SETTING_PARAM_CERT_KEY_FILE,
                                              NMSetting8021xPrivate,
                                              phase2_ca_cert);
 
@@ -3931,7 +4012,7 @@ nm_setting_802_1x_class_init(NMSetting8021xClass *klass)
                                              obj_properties,
                                              NM_SETTING_802_1X_PHASE2_CLIENT_CERT,
                                              PROP_PHASE2_CLIENT_CERT,
-                                             NM_SETTING_PARAM_NONE,
+                                             NM_SETTING_PARAM_CERT_KEY_FILE,
                                              NMSetting8021xPrivate,
                                              phase2_client_cert);
 
@@ -4092,7 +4173,7 @@ nm_setting_802_1x_class_init(NMSetting8021xClass *klass)
                                              obj_properties,
                                              NM_SETTING_802_1X_PRIVATE_KEY,
                                              PROP_PRIVATE_KEY,
-                                             NM_SETTING_PARAM_NONE,
+                                             NM_SETTING_PARAM_CERT_KEY_FILE,
                                              NMSetting8021xPrivate,
                                              private_key);
 
@@ -4177,7 +4258,7 @@ nm_setting_802_1x_class_init(NMSetting8021xClass *klass)
                                              obj_properties,
                                              NM_SETTING_802_1X_PHASE2_PRIVATE_KEY,
                                              PROP_PHASE2_PRIVATE_KEY,
-                                             NM_SETTING_PARAM_NONE,
+                                             NM_SETTING_PARAM_CERT_KEY_FILE,
                                              NMSetting8021xPrivate,
                                              phase2_private_key);
 
diff --git a/src/libnm-core-impl/nm-setting-connection.c b/src/libnm-core-impl/nm-setting-connection.c
index 0ec36a8930..750c547a24 100644
--- a/src/libnm-core-impl/nm-setting-connection.c
+++ b/src/libnm-core-impl/nm-setting-connection.c
@@ -393,6 +393,47 @@ nm_setting_connection_permissions_user_allowed(NMSettingConnection *setting, con
     return FALSE;
 }
 
+guint
+_nm_setting_connection_get_num_permissions_users(NMSettingConnection *setting)
+{
+    NMSettingConnectionPrivate *priv;
+    guint                       i;
+    guint                       count = 0;
+
+    nm_assert(NM_IS_SETTING_CONNECTION(setting));
+    priv = NM_SETTING_CONNECTION_GET_PRIVATE(setting);
+
+    for (i = 0; priv->permissions && i < priv->permissions->len; i++) {
+        const Permission *permission = &nm_g_array_index(priv->permissions, Permission, i);
+
+        if (permission->ptype == PERM_TYPE_USER) {
+            count++;
+        }
+    }
+
+    return count;
+}
+
+const char *
+_nm_setting_connection_get_first_permissions_user(NMSettingConnection *setting)
+{
+    NMSettingConnectionPrivate *priv;
+    guint                       i;
+
+    nm_assert(NM_IS_SETTING_CONNECTION(setting));
+    priv = NM_SETTING_CONNECTION_GET_PRIVATE(setting);
+
+    for (i = 0; priv->permissions && i < priv->permissions->len; i++) {
+        const Permission *permission = &nm_g_array_index(priv->permissions, Permission, i);
+
+        if (permission->ptype == PERM_TYPE_USER) {
+            return permission->item;
+        }
+    }
+
+    return NULL;
+}
+
 /**
  * nm_setting_connection_add_permission:
  * @setting: the #NMSettingConnection
diff --git a/src/libnm-core-impl/nm-setting-private.h b/src/libnm-core-impl/nm-setting-private.h
index fff6c1ccc5..857f9668cd 100644
--- a/src/libnm-core-impl/nm-setting-private.h
+++ b/src/libnm-core-impl/nm-setting-private.h
@@ -148,6 +148,11 @@ struct _NMSettingClass {
                                guint /* NMSettingParseFlags */ parse_flags,
                                GError                        **error);
 
+    /* returns a list of certificate/key files referenced in the connection.
+     * When the connection is private, we need to verify that the owner of
+     * the connection has access to them. */
+    void (*get_private_files)(NMSetting *setting, GPtrArray *files);
+
     const struct _NMMetaSettingInfo *setting_info;
 };
 
@@ -346,6 +351,11 @@ struct _NMRange {
  */
 #define NM_SETTING_PARAM_TO_DBUS_IGNORE_FLAGS (1 << (7 + G_PARAM_USER_SHIFT))
 
+/* The property can refer to a certificate or key stored on disk. As such,
+ * special care is needed when accessing the file for private connections.
+ */
+#define NM_SETTING_PARAM_CERT_KEY_FILE (1 << (8 + G_PARAM_USER_SHIFT))
+
 extern const NMSettInfoPropertType nm_sett_info_propert_type_setting_name;
 extern const NMSettInfoPropertType nm_sett_info_propert_type_deprecated_interface_name;
 extern const NMSettInfoPropertType nm_sett_info_propert_type_deprecated_ignore_i;
@@ -835,9 +845,10 @@ _nm_properties_override(GArray *properties_override, const NMSettInfoProperty *p
     {                                                                                              \
         GParamSpec *_param_spec;                                                                   \
                                                                                                    \
-        G_STATIC_ASSERT(!NM_FLAGS_ANY((param_flags),                                               \
-                                      ~(NM_SETTING_PARAM_SECRET | NM_SETTING_PARAM_INFERRABLE      \
-                                        | NM_SETTING_PARAM_FUZZY_IGNORE)));                        \
+        G_STATIC_ASSERT(                                                                           \
+            !NM_FLAGS_ANY((param_flags),                                                           \
+                          ~(NM_SETTING_PARAM_SECRET | NM_SETTING_PARAM_INFERRABLE                  \
+                            | NM_SETTING_PARAM_FUZZY_IGNORE | NM_SETTING_PARAM_CERT_KEY_FILE)));   \
                                                                                                    \
         _param_spec =                                                                              \
             g_param_spec_boxed("" prop_name "",                                                    \
diff --git a/src/libnm-core-impl/nm-setting.c b/src/libnm-core-impl/nm-setting.c
index 5a46a1b439..f4ee84d7cf 100644
--- a/src/libnm-core-impl/nm-setting.c
+++ b/src/libnm-core-impl/nm-setting.c
@@ -2197,6 +2197,34 @@ init_from_dbus(NMSetting                      *setting,
     return TRUE;
 }
 
+static void
+get_private_files(NMSetting *setting, GPtrArray *files)
+{
+    if (NM_MORE_ASSERTS) {
+        GParamSpec **properties;
+        guint        n_properties;
+        int          i;
+
+        properties = g_object_class_list_properties(G_OBJECT_GET_CLASS(setting), &n_properties);
+        for (i = 0; i < n_properties; i++) {
+            if (properties[i]->flags & NM_SETTING_PARAM_CERT_KEY_FILE) {
+                /* Certificates and keys needs special handling, see setting 802.1X */
+                nm_assert_not_reached();
+            }
+        }
+        g_free(properties);
+    }
+}
+
+void
+_nm_setting_get_private_files(NMSetting *setting, GPtrArray *files)
+{
+    g_return_if_fail(NM_IS_SETTING(setting));
+    g_return_if_fail(files);
+
+    NM_SETTING_GET_CLASS(setting)->get_private_files(setting, files);
+}
+
 /**
  * nm_setting_get_dbus_property_type:
  * @setting: an #NMSetting
@@ -4436,6 +4464,7 @@ nm_setting_class_init(NMSettingClass *setting_class)
     setting_class->enumerate_values          = enumerate_values;
     setting_class->aggregate                 = aggregate;
     setting_class->init_from_dbus            = init_from_dbus;
+    setting_class->get_private_files         = get_private_files;
 
     /**
      * NMSetting:name:
diff --git a/src/libnm-core-intern/nm-core-internal.h b/src/libnm-core-intern/nm-core-internal.h
index 9e9d55aa09..8185d19b23 100644
--- a/src/libnm-core-intern/nm-core-internal.h
+++ b/src/libnm-core-intern/nm-core-internal.h
@@ -1071,4 +1071,11 @@ gboolean nm_connection_need_secrets_for_rerequest(NMConnection *connection);
 
 const GPtrArray *_nm_setting_ovs_port_get_trunks_arr(NMSettingOvsPort *self);
 
+/*****************************************************************************/
+
+guint       _nm_setting_connection_get_num_permissions_users(NMSettingConnection *setting);
+const char *_nm_setting_connection_get_first_permissions_user(NMSettingConnection *setting);
+
+void _nm_setting_get_private_files(NMSetting *setting, GPtrArray *files);
+
 #endif
diff --git a/src/libnm-std-aux/nm-std-utils.c b/src/libnm-std-aux/nm-std-utils.c
index 18692b198c..9c7228ae46 100644
--- a/src/libnm-std-aux/nm-std-utils.c
+++ b/src/libnm-std-aux/nm-std-utils.c
@@ -4,9 +4,14 @@
 
 #include "nm-std-utils.h"
 
-#include <stdint.h>
 #include <assert.h>
+#include <fcntl.h>
+#include <grp.h>
 #include <limits.h>
+#include <net/if.h>
+#include <pwd.h>
+#include <stdint.h>
+#include <sys/types.h>
 
 /*****************************************************************************/
 
@@ -88,3 +93,111 @@ out_huge:
     }
     return SIZE_MAX;
 }
+
+/*****************************************************************************/
+
+bool
+nm_utils_set_effective_user(const char *user, char *errbuf, size_t errbuf_len)
+{
+    struct passwd *pwentry;
+    int            errsv;
+    char           error[1024];
+
+    errno   = 0;
+    pwentry = getpwnam(user);
+    if (!pwentry) {
+        errsv = errno;
+        if (errsv == 0) {
+            snprintf(errbuf, errbuf_len, "user not found");
+        } else {
+            snprintf(errbuf,
+                     errbuf_len,
+                     "error getting user entry: %d (%s)\n",
+                     errsv,
+                     strerror_r(errsv, error, sizeof(error)));
+        }
+        return false;
+    }
+
+    if (setgid(pwentry->pw_gid) != 0) {
+        errsv = errno;
+        snprintf(errbuf,
+                 errbuf_len,
+                 "failed to change group to %u: %d (%s)\n",
+                 pwentry->pw_gid,
+                 errsv,
+                 strerror_r(errsv, error, sizeof(error)));
+        return false;
+    }
+
+    if (initgroups(user, pwentry->pw_gid) != 0) {
+        errsv = errno;
+        snprintf(errbuf,
+                 errbuf_len,
+                 "failed to reset supplementary group list to %u: %d (%s)\n",
+                 pwentry->pw_gid,
+                 errsv,
+                 strerror_r(errsv, error, sizeof(error)));
+        return false;
+    }
+
+    if (setuid(pwentry->pw_uid) != 0) {
+        errsv = errno;
+        snprintf(errbuf,
+                 errbuf_len,
+                 "failed to change user to %u: %d (%s)\n",
+                 pwentry->pw_uid,
+                 errsv,
+                 strerror_r(errsv, error, sizeof(error)));
+        return false;
+    }
+
+    return true;
+}
+
+/*****************************************************************************/
+
+bool
+nm_utils_read_file_to_stdout(const char *filename, char *errbuf, size_t errbuf_len)
+{
+    nm_auto_close int fd = -1;
+    char              buffer[4096];
+    char              error[1024];
+    ssize_t           bytes_read;
+    int               errsv;
+
+    fd = open(filename, O_RDONLY);
+    if (fd == -1) {
+        errsv = errno;
+        snprintf(errbuf,
+                 errbuf_len,
+                 "error opening the file: %d (%s)",
+                 errsv,
+                 strerror_r(errsv, error, sizeof(error)));
+        return false;
+    }
+
+    while ((bytes_read = read(fd, buffer, sizeof(buffer))) > 0) {
+        if (fwrite(buffer, 1, bytes_read, stdout) != (size_t) bytes_read) {
+            errsv = errno;
+            snprintf(errbuf,
+                     errbuf_len,
+                     "error writing to stdout: %d (%s)",
+                     errsv,
+                     strerror_r(errsv, error, sizeof(error)));
+            return false;
+        }
+    }
+
+    if (bytes_read < 0) {
+        errsv = errno;
+        snprintf(errbuf,
+                 errbuf_len,
+                 "error reading the file: %d (%s)",
+                 errsv,
+                 strerror_r(errsv, error, sizeof(error)));
+        return false;
+    }
+
+    return true;
+}
diff --git a/src/libnm-std-aux/nm-std-utils.h b/src/libnm-std-aux/nm-std-utils.h
index 6aa787eb6a..9d3c5dfaa1 100644
--- a/src/libnm-std-aux/nm-std-utils.h
+++ b/src/libnm-std-aux/nm-std-utils.h
@@ -35,4 +35,8 @@
 
 size_t nm_utils_get_next_realloc_size(bool true_realloc, size_t requested);
 
+bool nm_utils_set_effective_user(const char *user, char *errbuf, size_t errbuf_size);
+
+bool nm_utils_read_file_to_stdout(const char *filename, char *errbuf, size_t errbuf_len);
+
 #endif /* __NM_STD_UTILS_H__ */
diff --git a/src/meson.build b/src/meson.build
index 92e95e68ef..5593d955d9 100644
--- a/src/meson.build
+++ b/src/meson.build
@@ -95,8 +95,7 @@ if enable_nmtui
 endif
 subdir('nmcli')
 subdir('nm-dispatcher')
-subdir('nm-priv-helper')
-subdir('nm-daemon-helper')
+subdir('nm-helpers')
 subdir('nm-online')
 if enable_nmtui
   subdir('nmtui')
diff --git a/src/nm-daemon-helper/meson.build b/src/nm-daemon-helper/meson.build
deleted file mode 100644
index da0d6571e1..0000000000
--- a/src/nm-daemon-helper/meson.build
+++ /dev/null
@@ -1,15 +0,0 @@
-executable(
-  'nm-daemon-helper',
-  'nm-daemon-helper.c',
-  include_directories : [
-    src_inc,
-    top_inc,
-  ],
-  link_with: [
-    libnm_std_aux,
-  ],
-  link_args: ldflags_linker_script_binary,
-  link_depends: linker_script_binary,
-  install: true,
-  install_dir: nm_libexecdir,
-)
diff --git a/src/nm-daemon-helper/nm-daemon-helper.c b/src/nm-daemon-helper/nm-daemon-helper.c
deleted file mode 100644
index a447d63cfe..0000000000
--- a/src/nm-daemon-helper/nm-daemon-helper.c
+++ /dev/null
@@ -1,129 +0,0 @@
-/* SPDX-License-Identifier: LGPL-2.1-or-later */
-
-/* Copyright (C) 2021 Red Hat, Inc. */
-
-#include "libnm-std-aux/nm-default-std.h"
-
-#include <stdio.h>
-#include <stdlib.h>
-#include <arpa/inet.h>
-#include <netdb.h>
-#if defined(__GLIBC__)
-#include <nss.h>
-#endif
-#include <stdarg.h>
-
-enum {
-    RETURN_SUCCESS      = 0,
-    RETURN_INVALID_CMD  = 1,
-    RETURN_INVALID_ARGS = 2,
-    RETURN_ERROR        = 3,
-};
-
-static char *
-read_arg(void)
-{
-    nm_auto_free char *arg = NULL;
-    size_t             len = 0;
-
-    if (getdelim(&arg, &len, '\0', stdin) < 0)
-        return NULL;
-
-    return nm_steal_pointer(&arg);
-}
-
-static int
-more_args(void)
-{
-    nm_auto_free char *arg = NULL;
-
-    arg = read_arg();
-
-    return !!arg;
-}
-
-static int
-cmd_version(void)
-{
-    if (more_args())
-        return RETURN_INVALID_ARGS;
-
-    printf("1");
-    return RETURN_SUCCESS;
-}
-
-static int
-cmd_resolve_address(void)
-{
-    nm_auto_free char *address = NULL;
-    union {
-        struct sockaddr_in  in;
-        struct sockaddr_in6 in6;
-    } sockaddr;
-    socklen_t sockaddr_size;
-    char      name[NI_MAXHOST];
-    int       ret;
-
-    address = read_arg();
-    if (!address)
-        return RETURN_INVALID_ARGS;
-
-    if (more_args())
-        return RETURN_INVALID_ARGS;
-
-    memset(&sockaddr, 0, sizeof(sockaddr));
-#if defined(__GLIBC__)
-    __nss_configure_lookup("hosts", "dns");
-#endif
-
-    if (inet_pton(AF_INET, address, &sockaddr.in.sin_addr) == 1) {
-        sockaddr.in.sin_family = AF_INET;
-        sockaddr_size          = sizeof(struct sockaddr_in);
-    } else if (inet_pton(AF_INET6, address, &sockaddr.in6.sin6_addr) == 1) {
-        sockaddr.in6.sin6_family = AF_INET6;
-        sockaddr_size            = sizeof(struct sockaddr_in6);
-    } else
-        return RETURN_INVALID_ARGS;
-
-    ret = getnameinfo((struct sockaddr *) &sockaddr,
-                      sockaddr_size,
-                      name,
-                      sizeof(name),
-                      NULL,
-                      0,
-                      NI_NAMEREQD);
-    if (ret != 0) {
-        if (ret == EAI_SYSTEM) {
-            fprintf(stderr,
-                    "getnameinfo() failed: %d (%s), system error: %d (%s)\n",
-                    ret,
-                    gai_strerror(ret),
-                    errno,
-                    strerror(errno));
-        } else {
-            fprintf(stderr, "getnameinfo() failed: %d (%s)\n", ret, gai_strerror(ret));
-        }
-        return RETURN_ERROR;
-    }
-
-    printf("%s", name);
-
-    return RETURN_SUCCESS;
-}
-
-int
-main(int argc, char **argv)
-{
-    nm_auto_free char *cmd = NULL;
-
-    cmd = read_arg();
-    if (!cmd)
-        return RETURN_INVALID_CMD;
-
-    if (nm_streq(cmd, "version"))
-        return cmd_version();
-    if (nm_streq(cmd, "resolve-address"))
-        return cmd_resolve_address();
-
-    return RETURN_INVALID_CMD;
-}
diff --git a/src/nm-helpers/README.md b/src/nm-helpers/README.md
new file mode 100644
index 0000000000..ab0ea02444
--- /dev/null
+++ b/src/nm-helpers/README.md
@@ -0,0 +1,43 @@
+nm-helpers
+==========
+
+This directory contains stand-alone helper programs used by various
+components.
+
+nm-daemon-helper
+----------------
+
+A internal helper application that is spawned by NetworkManager to
+perform certain actions which can't be done in the daemon. 
+
+Currently it's used to do a reverse DNS lookup after reconfiguring the
+libc resolver (which is a process-wide operation), and to read files
+on behalf of unprivileged users (which requires a seteuid that affects
+all the threads of the process).
+
+This is not directly useful to the user.
+
+nm-priv-helper
+--------------
+
+This is a D-Bus activatable, exit-on-idle service, which
+provides an internal API to NetworkManager daemon.
+
+This has no purpose for the user, it is an implementation detail
+of the daemon.
+
+The purpose is that `nm-priv-helper` can execute certain
+privileged operations which NetworkManager process is not
+allowed to. We want to sandbox NetworkManager as much as
+possible, and nm-priv-helper provides a controlled way to
+perform some very specific operations.
+
+As such, nm-priv-helper should still be sandboxed too to only
+being able to execute the operations that are necessary for
+NetworkManager.
+
+nm-priv-helper will reject all D-Bus requests that are not
+originating from the current name owner of
+"org.freedesktop.NetworkManager".  That is, it is supposed to
+only reply to NetworkManager daemon and as such is not useful to
+the user directly.
diff --git a/src/nm-helpers/meson.build b/src/nm-helpers/meson.build
new file mode 100644
index 0000000000..5f330cbc94
--- /dev/null
+++ b/src/nm-helpers/meson.build
@@ -0,0 +1,56 @@
+# SPDX-License-Identifier: LGPL-2.1-or-later
+
+# nm-daemon-helper
+
+executable(
+  'nm-daemon-helper',
+  'nm-daemon-helper.c',
+  include_directories : [
+    src_inc,
+    top_inc,
+  ],
+  link_with: [
+    libnm_std_aux,
+  ],
+  link_args: ldflags_linker_script_binary,
+  link_depends: linker_script_binary,
+  install: true,
+  install_dir: nm_libexecdir,
+)
+
+# nm-priv-helper
+
+configure_file(
+  input: 'org.freedesktop.nm_priv_helper.service.in',
+  output: '@BASENAME@',
+  install_dir: dbus_system_bus_services_dir,
+  configuration: data_conf,
+)
+
+install_data(
+  'nm-priv-helper.conf',
+  install_dir: dbus_conf_dir,
+)
+
+executable(
+  'nm-priv-helper',
+  'nm-priv-helper.c',
+  include_directories : [
+    src_inc,
+    top_inc,
+  ],
+  dependencies: [
+    glib_dep,
+  ],
+  link_with: [
+    libnm_base,
+    libnm_log_null,
+    libnm_glib_aux,
+    libnm_std_aux,
+    libc_siphash,
+  ],
+  link_args: ldflags_linker_script_binary,
+  link_depends: linker_script_binary,
+  install: true,
+  install_dir: nm_libexecdir,
+)
diff --git a/src/nm-helpers/nm-daemon-helper.c b/src/nm-helpers/nm-daemon-helper.c
new file mode 100644
index 0000000000..340a6332b6
--- /dev/null
+++ b/src/nm-helpers/nm-daemon-helper.c
@@ -0,0 +1,162 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+/* Copyright (C) 2021 Red Hat, Inc. */
+
+#include "libnm-std-aux/nm-default-std.h"
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <arpa/inet.h>
+#include <netdb.h>
+#if defined(__GLIBC__)
+#include <nss.h>
+#endif
+#include <stdarg.h>
+
+enum {
+    RETURN_SUCCESS      = 0,
+    RETURN_INVALID_CMD  = 1,
+    RETURN_INVALID_ARGS = 2,
+    RETURN_ERROR        = 3,
+};
+
+static char *
+read_arg(void)
+{
+    nm_auto_free char *arg = NULL;
+    size_t             len = 0;
+
+    if (getdelim(&arg, &len, '\0', stdin) < 0)
+        return NULL;
+
+    return nm_steal_pointer(&arg);
+}
+
+static int
+more_args(void)
+{
+    nm_auto_free char *arg = NULL;
+
+    arg = read_arg();
+
+    return !!arg;
+}
+
+static int
+cmd_version(void)
+{
+    if (more_args())
+        return RETURN_INVALID_ARGS;
+
+    printf("1");
+    return RETURN_SUCCESS;
+}
+
+static int
+cmd_resolve_address(void)
+{
+    nm_auto_free char *address = NULL;
+    union {
+        struct sockaddr_in  in;
+        struct sockaddr_in6 in6;
+    } sockaddr;
+    socklen_t sockaddr_size;
+    char      name[NI_MAXHOST];
+    int       ret;
+
+    address = read_arg();
+    if (!address)
+        return RETURN_INVALID_ARGS;
+
+    if (more_args())
+        return RETURN_INVALID_ARGS;
+
+    memset(&sockaddr, 0, sizeof(sockaddr));
+#if defined(__GLIBC__)
+    __nss_configure_lookup("hosts", "dns");
+#endif
+
+    if (inet_pton(AF_INET, address, &sockaddr.in.sin_addr) == 1) {
+        sockaddr.in.sin_family = AF_INET;
+        sockaddr_size          = sizeof(struct sockaddr_in);
+    } else if (inet_pton(AF_INET6, address, &sockaddr.in6.sin6_addr) == 1) {
+        sockaddr.in6.sin6_family = AF_INET6;
+        sockaddr_size            = sizeof(struct sockaddr_in6);
+    } else
+        return RETURN_INVALID_ARGS;
+
+    ret = getnameinfo((struct sockaddr *) &sockaddr,
+                      sockaddr_size,
+                      name,
+                      sizeof(name),
+                      NULL,
+                      0,
+                      NI_NAMEREQD);
+    if (ret != 0) {
+        if (ret == EAI_SYSTEM) {
+            fprintf(stderr,
+                    "getnameinfo() failed: %d (%s), system error: %d (%s)\n",
+                    ret,
+                    gai_strerror(ret),
+                    errno,
+                    strerror(errno));
+        } else {
+            fprintf(stderr, "getnameinfo() failed: %d (%s)\n", ret, gai_strerror(ret));
+        }
+        return RETURN_ERROR;
+    }
+
+    printf("%s", name);
+
+    return RETURN_SUCCESS;
+}
+
+static int
+cmd_read_file_as_user(void)
+{
+    nm_auto_free char *user     = NULL;
+    nm_auto_free char *filename = NULL;
+    char               error[1024];
+
+    user = read_arg();
+    if (!user)
+        return RETURN_INVALID_ARGS;
+
+    filename = read_arg();
+    if (!filename)
+        return RETURN_INVALID_ARGS;
+
+    if (more_args())
+        return RETURN_INVALID_ARGS;
+
+    if (!nm_utils_set_effective_user(user, error, sizeof(error))) {
+        fprintf(stderr, "Failed to set effective user '%s': %s", user, error);
+        return RETURN_ERROR;
+    }
+
+    if (!nm_utils_read_file_to_stdout(filename, error, sizeof(error))) {
+        fprintf(stderr, "Failed to read file '%s' as user '%s': %s", filename, user, error);
+        return RETURN_ERROR;
+    }
+
+    return RETURN_SUCCESS;
+}
+
+int
+main(int argc, char **argv)
+{
+    nm_auto_free char *cmd = NULL;
+
+    cmd = read_arg();
+    if (!cmd)
+        return RETURN_INVALID_CMD;
+
+    if (nm_streq(cmd, "version"))
+        return cmd_version();
+    if (nm_streq(cmd, "resolve-address"))
+        return cmd_resolve_address();
+    if (nm_streq(cmd, "read-file-as-user"))
+        return cmd_read_file_as_user();
+
+    return RETURN_INVALID_CMD;
+}
diff --git a/src/nm-helpers/nm-priv-helper.c b/src/nm-helpers/nm-priv-helper.c
new file mode 100644
index 0000000000..e29113b112
--- /dev/null
+++ b/src/nm-helpers/nm-priv-helper.c
@@ -0,0 +1,695 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include "libnm-glib-aux/nm-default-glib-i18n-prog.h"
+
+#include <gio/gunixfdlist.h>
+
+#include "c-list/src/c-list.h"
+#include "libnm-base/nm-priv-helper-utils.h"
+#include "libnm-glib-aux/nm-dbus-aux.h"
+#include "libnm-glib-aux/nm-io-utils.h"
+#include "libnm-glib-aux/nm-logging-base.h"
+#include "libnm-glib-aux/nm-time-utils.h"
+
+/* nm-priv-helper doesn't link with libnm-core nor libnm-base, but these
+ * headers can be used independently. */
+#include "libnm-core-public/nm-dbus-interface.h"
+
+/*****************************************************************************/
+
+#define IDLE_TIMEOUT_MSEC     2000
+#define IDLE_TIMEOUT_INFINITY G_MAXINT32
+
+/*****************************************************************************/
+
+/* Serves only the purpose to mark environment variables that are honored by
+ * the application. You can search for this macro, and find what options are supported. */
+#define _ENV(var) ("" var "")
+
+/*****************************************************************************/
+
+typedef struct _GlobalData GlobalData;
+
+typedef struct {
+    CList       pending_jobs_lst;
+    GlobalData *gl;
+} PendingJobData;
+
+struct _GlobalData {
+    GDBusConnection *dbus_connection;
+    GCancellable    *quit_cancellable;
+
+    GSource *source_sigterm;
+
+    CList pending_jobs_lst_head;
+
+    GSource *source_idle_timeout;
+
+    char *name_owner;
+
+    gint64 start_timestamp_msec;
+
+    guint name_owner_changed_id;
+    guint service_regist_id;
+
+    guint32 timeout_msec;
+
+    bool name_owner_initialized;
+
+    /* This is controlled by $NM_PRIV_HELPER_NO_AUTH_FOR_TESTING. It disables authentication
+     * of the request, so it is ONLY for testing. */
+    bool no_auth_for_testing;
+
+    bool name_requested;
+    bool reject_new_requests;
+
+    bool shutdown_quitting;
+    bool shutdown_timeout;
+};
+
+/*****************************************************************************/
+
+static void _pending_job_register_object(GlobalData *gl, GObject *obj);
+
+/*****************************************************************************/
+
+#define _nm_log(level, ...) _nm_log_simple_printf((level), __VA_ARGS__)
+
+#define _NMLOG(level, ...)                 \
+    G_STMT_START                           \
+    {                                      \
+        const NMLogLevel _level = (level); \
+                                           \
+        if (_nm_logging_enabled(_level)) { \
+            _nm_log(_level, __VA_ARGS__);  \
+        }                                  \
+    }                                      \
+    G_STMT_END
+
+/*****************************************************************************/
+
+static void
+_handle_ping(GlobalData *gl, GDBusMethodInvocation *invocation, const char *arg)
+{
+    gs_free char *msg = NULL;
+    gint64        running_msec;
+
+    running_msec = nm_utils_clock_gettime_msec(CLOCK_BOOTTIME) - gl->start_timestamp_msec;
+
+    msg = g_strdup_printf("pid=%lu, unique-name=%s, nm-name-owner=%s, since=%" G_GINT64_FORMAT
+                          ".%03d%s, pong=%s",
+                          (unsigned long) getpid(),
+                          g_dbus_connection_get_unique_name(gl->dbus_connection),
+                          gl->name_owner ?: "(none)",
+                          (gint64) (running_msec / 1000),
+                          (int) (running_msec % 1000),
+                          gl->no_auth_for_testing ? ", no-auth-for-testing" : "",
+                          arg);
+    g_dbus_method_invocation_return_value(invocation, g_variant_new("(s)", msg));
+}
+
+static void
+_handle_get_fd(GlobalData *gl, GDBusMethodInvocation *invocation, guint32 fd_type)
+{
+    nm_auto_close int            fd      = -1;
+    gs_unref_object GUnixFDList *fd_list = NULL;
+    gs_free_error GError        *error   = NULL;
+
+    if (fd_type != (NMPrivHelperGetFDType) fd_type)
+        fd_type = NM_PRIV_HELPER_GET_FD_TYPE_NONE;
+
+    fd = nm_priv_helper_utils_open_fd(fd_type, &error);
+    if (fd < 0) {
+        g_dbus_method_invocation_take_error(invocation, g_steal_pointer(&error));
+        return;
+    }
+
+    fd_list = g_unix_fd_list_new_from_array(&fd, 1);
+    nm_steal_fd(&fd);
+
+    g_dbus_method_invocation_return_value_with_unix_fd_list(invocation, NULL, fd_list);
+}
+
+/*****************************************************************************/
+
+static gboolean
+_signal_callback_term(gpointer user_data)
+{
+    GlobalData *gl = user_data;
+
+    _LOGD("sigterm received (%s)",
+          c_list_is_empty(&gl->pending_jobs_lst_head) ? "quit mainloop" : "cancel operations");
+
+    gl->shutdown_quitting = TRUE;
+    g_cancellable_cancel(gl->quit_cancellable);
+    return G_SOURCE_CONTINUE;
+}
+
+/*****************************************************************************/
+
+static GDBusConnection *
+_bus_get(GCancellable *cancellable, int *out_exit_code)
+{
+    gs_free_error GError            *error           = NULL;
+    gs_unref_object GDBusConnection *dbus_connection = NULL;
+
+    dbus_connection = nm_g_bus_get_blocking(cancellable, &error);
+
+    if (!dbus_connection) {
+        gboolean was_cancelled = nm_utils_error_is_cancelled(error);
+
+        NM_SET_OUT(out_exit_code, was_cancelled ? EXIT_SUCCESS : EXIT_FAILURE);
+        if (!was_cancelled)
+            _LOGE("dbus: failure to get D-Bus connection: %s", error->message);
+        return NULL;
+    }
+
+    /* On bus-disconnect, GDBus will raise(SIGTERM), which we handle like a
+     * regular request to quit. */
+    g_dbus_connection_set_exit_on_close(dbus_connection, TRUE);
+
+    _LOGD("dbus: unique name: %s", g_dbus_connection_get_unique_name(dbus_connection));
+
+    return g_steal_pointer(&dbus_connection);
+}
+
+/*****************************************************************************/
+
+static void
+_name_owner_changed_cb(GDBusConnection *connection,
+                       const char      *sender_name,
+                       const char      *object_path,
+                       const char      *interface_name,
+                       const char      *signal_name,
+                       GVariant        *parameters,
+                       gpointer         user_data)
+{
+    GlobalData *gl = user_data;
+    const char *new_owner;
+
+    if (!gl->name_owner_initialized)
+        return;
+
+    if (!g_variant_is_of_type(parameters, G_VARIANT_TYPE("(sss)")))
+        return;
+
+    g_variant_get(parameters, "(&s&s&s)", NULL, NULL, &new_owner);
+    new_owner = nm_str_not_empty(new_owner);
+
+    _LOGD("%s name-owner changed: %s -> %s",
+          NM_DBUS_SERVICE,
+          gl->name_owner ?: "(null)",
+          new_owner ?: "(null)");
+
+    nm_strdup_reset(&gl->name_owner, new_owner);
+}
+
+typedef struct {
+    GlobalData *gl;
+    char      **p_name_owner;
+    gboolean    is_cancelled;
+} BusFindNMNameOwnerData;
+
+static void
+_bus_find_nm_nameowner_cb(const char *name_owner, GError *error, gpointer user_data)
+{
+    BusFindNMNameOwnerData *data = user_data;
+
+    *data->p_name_owner              = nm_strdup_not_empty(name_owner);
+    data->is_cancelled               = nm_utils_error_is_cancelled(error);
+    data->gl->name_owner_initialized = TRUE;
+}
+
+static gboolean
+_bus_find_nm_nameowner(GlobalData *gl)
+{
+    BusFindNMNameOwnerData data;
+    guint                  name_owner_changed_id;
+    gs_free char          *name_owner = NULL;
+
+    name_owner_changed_id =
+        nm_dbus_connection_signal_subscribe_name_owner_changed(gl->dbus_connection,
+                                                               NM_DBUS_SERVICE,
+                                                               _name_owner_changed_cb,
+                                                               gl,
+                                                               NULL);
+
+    data = (BusFindNMNameOwnerData){
+        .gl           = gl,
+        .is_cancelled = FALSE,
+        .p_name_owner = &name_owner,
+    };
+    nm_dbus_connection_call_get_name_owner(gl->dbus_connection,
+                                           NM_DBUS_SERVICE,
+                                           10000,
+                                           gl->quit_cancellable,
+                                           _bus_find_nm_nameowner_cb,
+                                           &data);
+    while (!gl->name_owner_initialized)
+        g_main_context_iteration(NULL, TRUE);
+
+    if (data.is_cancelled) {
+        g_dbus_connection_signal_unsubscribe(gl->dbus_connection, name_owner_changed_id);
+        return FALSE;
+    }
+
+    gl->name_owner_changed_id = name_owner_changed_id;
+    gl->name_owner            = g_steal_pointer(&name_owner);
+    return TRUE;
+}
+
+/*****************************************************************************/
+
+static void
+_bus_method_call(GDBusConnection       *connection,
+                 const char            *sender,
+                 const char            *object_path,
+                 const char            *interface_name,
+                 const char            *method_name,
+                 GVariant              *parameters,
+                 GDBusMethodInvocation *invocation,
+                 gpointer               user_data)
+{
+    GlobalData *gl = user_data;
+    const char *arg_s;
+    guint32     arg_u;
+
+    nm_assert(nm_streq(object_path, NM_PRIV_HELPER_DBUS_OBJECT_PATH));
+    nm_assert(nm_streq(interface_name, NM_PRIV_HELPER_DBUS_IFACE_NAME));
+
+    if (!gl->no_auth_for_testing && !nm_streq0(sender, gl->name_owner)) {
+        _LOGT("dbus: request sender=%s, %s%s, ACCESS DENIED",
+              sender,
+              method_name,
+              g_variant_get_type_string(parameters));
+        g_dbus_method_invocation_return_error(invocation,
+                                              G_DBUS_ERROR,
+                                              G_DBUS_ERROR_ACCESS_DENIED,
+                                              "Access denied");
+        return;
+    }
+
+    if (gl->reject_new_requests) {
+        /* after the name was released, we must not accept new requests. This new
+         * request was probably targeted against the unique-name. But we already
+         * gave up the well-known name. If we'd accept new request now, they would
+         * keep the service running indefinitely (and thus preventing the service
+         * to restart and serve the well-known name. */
+        _LOGT("dbus: request sender=%s, %s%s, SERVER SHUTTING DOWN",
+              sender,
+              method_name,
+              g_variant_get_type_string(parameters));
+        g_dbus_method_invocation_return_error(invocation,
+                                              G_DBUS_ERROR,
+                                              G_DBUS_ERROR_NO_SERVER,
+                                              "Server is exiting");
+        return;
+    }
+
+    _LOGT("dbus: request sender=%s, %s%s",
+          sender,
+          method_name,
+          g_variant_get_type_string(parameters));
+
+    if (!nm_streq(interface_name, NM_PRIV_HELPER_DBUS_IFACE_NAME))
+        goto out_unknown_method;
+
+    if (nm_streq(method_name, "GetFD")) {
+        g_variant_get(parameters, "(u)", &arg_u);
+        _handle_get_fd(gl, invocation, arg_u);
+        return;
+    }
+    if (nm_streq(method_name, "Ping")) {
+        g_variant_get(parameters, "(&s)", &arg_s);
+        _handle_ping(gl, invocation, arg_s);
+        return;
+    }
+
+out_unknown_method:
+    g_dbus_method_invocation_return_error(invocation,
+                                          G_DBUS_ERROR,
+                                          G_DBUS_ERROR_UNKNOWN_METHOD,
+                                          "Unknown method %s",
+                                          method_name);
+}
+
+static GDBusInterfaceInfo *const interface_info = NM_DEFINE_GDBUS_INTERFACE_INFO(
+    NM_PRIV_HELPER_DBUS_IFACE_NAME,
+    .methods = NM_DEFINE_GDBUS_METHOD_INFOS(
+        NM_DEFINE_GDBUS_METHOD_INFO(
+            "Ping",
+            .in_args  = NM_DEFINE_GDBUS_ARG_INFOS(NM_DEFINE_GDBUS_ARG_INFO("arg", "s"), ),
+            .out_args = NM_DEFINE_GDBUS_ARG_INFOS(NM_DEFINE_GDBUS_ARG_INFO("arg", "s"), ), ),
+        NM_DEFINE_GDBUS_METHOD_INFO("GetFD",
+                                    .in_args = NM_DEFINE_GDBUS_ARG_INFOS(
+                                        NM_DEFINE_GDBUS_ARG_INFO("fd_type", "u"), ), ), ), );
+
+static gboolean
+_bus_register_service(GlobalData *gl)
+{
+    static const GDBusInterfaceVTable interface_vtable = {
+        .method_call = _bus_method_call,
+    };
+    gs_free_error GError            *error = NULL;
+    NMDBusConnectionCallBlockingData data  = {
+         .result = NULL,
+    };
+    gs_unref_variant GVariant *ret = NULL;
+    guint32                    ret_val;
+
+    gl->service_regist_id =
+        g_dbus_connection_register_object(gl->dbus_connection,
+                                          NM_PRIV_HELPER_DBUS_OBJECT_PATH,
+                                          interface_info,
+                                          NM_UNCONST_PTR(GDBusInterfaceVTable, &interface_vtable),
+                                          gl,
+                                          NULL,
+                                          &error);
+    if (gl->service_regist_id == 0) {
+        _LOGE("dbus: error registering object %s: %s",
+              NM_PRIV_HELPER_DBUS_OBJECT_PATH,
+              error->message);
+        return FALSE;
+    }
+
+    _LOGD("dbus: object %s registered", NM_PRIV_HELPER_DBUS_OBJECT_PATH);
+
+    /* regardless whether the request is successful, after we start calling
+     * RequestName, we remember that we need to ReleaseName it. */
+    gl->name_requested = TRUE;
+
+    nm_dbus_connection_call_request_name(gl->dbus_connection,
+                                         NM_PRIV_HELPER_DBUS_BUS_NAME,
+                                         DBUS_NAME_FLAG_ALLOW_REPLACEMENT
+                                             | DBUS_NAME_FLAG_REPLACE_EXISTING,
+                                         10000,
+                                         gl->quit_cancellable,
+                                         nm_dbus_connection_call_blocking_callback,
+                                         &data);
+
+    /* Note that with D-Bus activation, the first request will already hit us before RequestName
+     * completes. So when we start iterating the main context, the first request may already come
+     * in. */
+
+    ret = nm_dbus_connection_call_blocking(&data, &error);
+
+    if (nm_utils_error_is_cancelled(error))
+        return FALSE;
+
+    if (error) {
+        _LOGE("d-bus: failed to request name %s: %s", NM_PRIV_HELPER_DBUS_BUS_NAME, error->message);
+        return FALSE;
+    }
+
+    g_variant_get(ret, "(u)", &ret_val);
+
+    if (ret_val != DBUS_REQUEST_NAME_REPLY_PRIMARY_OWNER) {
+        _LOGW("dbus: request name for %s failed to take name (response %u)",
+              NM_PRIV_HELPER_DBUS_BUS_NAME,
+              ret_val);
+        return FALSE;
+    }
+
+    _LOGD("dbus: request name for %s succeeded", NM_PRIV_HELPER_DBUS_BUS_NAME);
+    return TRUE;
+}
+
+/*****************************************************************************/
+
+static gboolean
+_idle_timeout_cb(gpointer user_data)
+{
+    GlobalData *gl = user_data;
+
+    _LOGT("idle-timeout: expired");
+    nm_clear_g_source_inst(&gl->source_idle_timeout);
+    gl->shutdown_timeout = TRUE;
+    return G_SOURCE_CONTINUE;
+}
+
+static void
+_idle_timeout_restart(GlobalData *gl)
+{
+    nm_clear_g_source_inst(&gl->source_idle_timeout);
+
+    if (gl->shutdown_quitting)
+        return;
+
+    if (!c_list_is_empty(&gl->pending_jobs_lst_head))
+        return;
+
+    if (gl->timeout_msec == IDLE_TIMEOUT_INFINITY)
+        return;
+
+    nm_assert(gl->timeout_msec < G_MAXINT32);
+    G_STATIC_ASSERT_EXPR(G_MAXINT32 < G_MAXUINT);
+
+    _LOGT("idle-timeout: start (%u msec)", gl->timeout_msec);
+    gl->source_idle_timeout = nm_g_timeout_add_source(gl->timeout_msec, _idle_timeout_cb, gl);
+}
+
+/*****************************************************************************/
+
+static gboolean
+_pending_job_register_object_release_on_idle_cb(gpointer data)
+{
+    PendingJobData *idle_data = data;
+    GlobalData     *gl        = idle_data->gl;
+
+    c_list_unlink_stale(&idle_data->pending_jobs_lst);
+    nm_g_slice_free(idle_data);
+
+    _idle_timeout_restart(gl);
+    return G_SOURCE_REMOVE;
+}
+
+static void
+_pending_job_register_object_weak_cb(gpointer data, GObject *where_the_object_was)
+{
+    /* The object might be destroyed on another thread. We need
+     * to sync with the main GMainContext by scheduling an idle action
+     * there. */
+    nm_g_idle_add(_pending_job_register_object_release_on_idle_cb, data);
+}
+
+static void
+_pending_job_register_object(GlobalData *gl, GObject *obj)
+{
+    PendingJobData *idle_data;
+
+    /* if we just hit the timeout, we can ignore it. */
+    gl->shutdown_timeout = FALSE;
+
+    if (nm_clear_g_source_inst(&gl->source_idle_timeout))
+        _LOGT("idle-timeout: suspend timeout for pending request");
+
+    idle_data = g_slice_new(PendingJobData);
+
+    idle_data->gl = gl;
+    c_list_link_tail(&gl->pending_jobs_lst_head, &idle_data->pending_jobs_lst);
+
+    g_object_weak_ref(obj, _pending_job_register_object_weak_cb, idle_data);
+}
+
+/*****************************************************************************/
+
+static void
+_bus_release_name_cb(GObject *source, GAsyncResult *result, gpointer user_data)
+{
+    _nm_unused gs_unref_object GObject *keep_alive_object = NULL;
+    GlobalData                         *gl;
+
+    nm_utils_user_data_unpack(user_data, &gl, &keep_alive_object);
+
+    gl->reject_new_requests = TRUE;
+    g_main_context_wakeup(NULL);
+}
+
+static gboolean
+_bus_release_name(GlobalData *gl)
+{
+    gs_unref_object GObject *keep_alive_object = NULL;
+    int                      r;
+
+    /* We already requested a name. To exit-on-idle without race, we need to dance.
+     * See https://lists.freedesktop.org/archives/dbus/2015-May/016671.html . */
+
+    if (!gl->name_requested)
+        return FALSE;
+
+    gl->name_requested    = FALSE;
+    gl->shutdown_quitting = TRUE;
+
+    _LOGT("shutdown: release-name");
+
+    keep_alive_object = g_object_new(G_TYPE_OBJECT, NULL);
+
+    /* we use the _pending_job_register_object() mechanism to make the loop busy during
+     * shutdown. */
+    _pending_job_register_object(gl, keep_alive_object);
+
+    r = nm_sd_notify("STOPPING=1");
+    if (r < 0)
+        _LOGW("shutdown: sd_notifiy(STOPPING=1) failed: %s", nm_strerror_native(-r));
+    else
+        _LOGT("shutdown: sd_notifiy(STOPPING=1) succeeded");
+
+    g_dbus_connection_call(gl->dbus_connection,
+                           DBUS_SERVICE_DBUS,
+                           DBUS_PATH_DBUS,
+                           DBUS_INTERFACE_DBUS,
+                           "ReleaseName",
+                           g_variant_new("(s)", NM_PRIV_HELPER_DBUS_BUS_NAME),
+                           G_VARIANT_TYPE("(u)"),
+                           G_DBUS_CALL_FLAGS_NONE,
+                           10000,
+                           NULL,
+                           _bus_release_name_cb,
+                           nm_utils_user_data_pack(gl, g_steal_pointer(&keep_alive_object)));
+    return TRUE;
+}
+
+/*****************************************************************************/
+
+static void
+_initial_setup(GlobalData *gl)
+{
+    gl->no_auth_for_testing =
+        _nm_utils_ascii_str_to_int64(g_getenv(_ENV("NM_PRIV_HELPER_NO_AUTH_FOR_TESTING")),
+                                     0,
+                                     0,
+                                     1,
+                                     0);
+    gl->timeout_msec =
+        _nm_utils_ascii_str_to_int64(g_getenv(_ENV("NM_PRIV_HELPER_IDLE_TIMEOUT_MSEC")),
+                                     0,
+                                     0,
+                                     G_MAXINT32,
+                                     IDLE_TIMEOUT_MSEC);
+
+    gl->quit_cancellable = g_cancellable_new();
+
+    signal(SIGPIPE, SIG_IGN);
+    gl->source_sigterm = nm_g_unix_signal_add_source(SIGTERM, _signal_callback_term, gl);
+}
+
+/*****************************************************************************/
+
+int
+main(int argc, char **argv)
+{
+    GlobalData _gl = {
+        .quit_cancellable      = NULL,
+        .pending_jobs_lst_head = C_LIST_INIT(_gl.pending_jobs_lst_head),
+    };
+    GlobalData *const gl = &_gl;
+    int               exit_code;
+    int               r = 0;
+
+    _nm_logging_enabled_init(g_getenv(_ENV("NM_PRIV_HELPER_LOG")));
+
+    gl->start_timestamp_msec = nm_utils_clock_gettime_msec(CLOCK_BOOTTIME);
+
+    _LOGD("starting nm-priv-helper (%s)", NM_DIST_VERSION);
+
+    _initial_setup(gl);
+
+    if (gl->no_auth_for_testing) {
+        _LOGW("WARNING: running in debug mode without authentication "
+              "(NM_PRIV_HELPER_NO_AUTH_FOR_TESTING). ");
+    }
+
+    if (gl->timeout_msec != IDLE_TIMEOUT_INFINITY)
+        _LOGT("idle-timeout: %u msec", gl->timeout_msec);
+    else
+        _LOGT("idle-timeout: disabled");
+
+    gl->dbus_connection = _bus_get(gl->quit_cancellable, &r);
+    if (!gl->dbus_connection) {
+        exit_code = r;
+        goto done;
+    }
+
+    if (!_bus_find_nm_nameowner(gl)) {
+        /* abort due to cancellation. That is success. */
+        exit_code = EXIT_SUCCESS;
+        goto done;
+    }
+    _LOGD("%s name-owner: %s", NM_DBUS_SERVICE, gl->name_owner ?: "(null)");
+
+    _idle_timeout_restart(gl);
+
+    exit_code = EXIT_SUCCESS;
+
+    if (!_bus_register_service(gl)) {
+        /* We failed to RequestName, but due to D-Bus activation we
+         * might have a pending request still (on the unique name).
+         * Process it below.
+         *
+         * Let's fake a shutdown signal, and still process the request below. */
+        if (!g_cancellable_is_cancelled(gl->quit_cancellable))
+            exit_code = EXIT_FAILURE;
+        gl->shutdown_quitting = TRUE;
+
+        if (gl->name_requested) {
+            /* We requested a name, but something went wrong. Below we will release
+             * the name right away. */
+        } else {
+            /* In case we didn't even went as far to request the name. New requests
+             * can only come via the unique name, and as we are shutting down, they
+             * are rejected. */
+            gl->reject_new_requests = TRUE;
+        }
+    }
+
+    while (TRUE) {
+        if (gl->shutdown_quitting)
+            _bus_release_name(gl);
+
+        if (!c_list_is_empty(&gl->pending_jobs_lst_head)) {
+            /* we must first reply to all requests. No matter what. */
+        } else if (gl->shutdown_quitting || gl->shutdown_timeout) {
+            /* we either hit the idle timeout or received SIGTERM. Note that
+             * if we received an idle-timeout and the very moment afterwards
+             * a new request, then _bus_method_call() will clear gl->shutdown_timeout
+             * (via _pending_job_register_object()). */
+            if (!_bus_release_name(gl))
+                break;
+        }
+
+        g_main_context_iteration(NULL, TRUE);
+    }
+
+done:
+    _LOGD("shutdown: cleanup");
+
+    gl->shutdown_quitting = TRUE;
+    g_cancellable_cancel(gl->quit_cancellable);
+
+    nm_assert(c_list_is_empty(&gl->pending_jobs_lst_head));
+
+    if (gl->service_regist_id != 0) {
+        g_dbus_connection_unregister_object(gl->dbus_connection,
+                                            nm_steal_int(&gl->service_regist_id));
+    }
+    if (gl->name_owner_changed_id != 0) {
+        g_dbus_connection_signal_unsubscribe(gl->dbus_connection,
+                                             nm_steal_int(&gl->name_owner_changed_id));
+    }
+
+    if (gl->dbus_connection) {
+        g_dbus_connection_flush_sync(gl->dbus_connection, NULL, NULL);
+        g_clear_object(&gl->dbus_connection);
+    }
+
+    nm_g_main_context_iterate_ready(NULL);
+
+    nm_clear_g_free(&gl->name_owner);
+
+    nm_clear_g_source_inst(&gl->source_sigterm);
+    nm_clear_g_source_inst(&gl->source_idle_timeout);
+    g_clear_object(&gl->quit_cancellable);
+
+    _LOGD("exit (%d)", exit_code);
+    return exit_code;
+}
diff --git a/src/nm-helpers/nm-priv-helper.conf b/src/nm-helpers/nm-priv-helper.conf
new file mode 100644
index 0000000000..c0d046178b
--- /dev/null
+++ b/src/nm-helpers/nm-priv-helper.conf
@@ -0,0 +1,13 @@
+<!DOCTYPE busconfig PUBLIC
+ "-//freedesktop//DTD D-BUS Bus Configuration 1.0//EN"
+ "http://www.freedesktop.org/standards/dbus/1.0/busconfig.dtd">
+<busconfig>
+    <policy user="root">
+        <allow own="org.freedesktop.nm_priv_helper"/>
+        <allow send_destination="org.freedesktop.nm_priv_helper"/>
+    </policy>
+    <policy context="default">
+        <deny own="org.freedesktop.nm_priv_helper"/>
+        <deny send_destination="org.freedesktop.nm_priv_helper"/>
+    </policy>
+</busconfig>
diff --git a/src/nm-helpers/org.freedesktop.nm_priv_helper.service.in b/src/nm-helpers/org.freedesktop.nm_priv_helper.service.in
new file mode 100644
index 0000000000..9e86a635b2
--- /dev/null
+++ b/src/nm-helpers/org.freedesktop.nm_priv_helper.service.in
@@ -0,0 +1,5 @@
+[D-BUS Service]
+Name=org.freedesktop.nm_priv_helper
+Exec=@libexecdir@/nm-priv-helper
+User=root
+SystemdService=nm-priv-helper.service
diff --git a/src/nm-priv-helper/meson.build b/src/nm-priv-helper/meson.build
deleted file mode 100644
index 6141e0e207..0000000000
--- a/src/nm-priv-helper/meson.build
+++ /dev/null
@@ -1,36 +0,0 @@
-# SPDX-License-Identifier: LGPL-2.1-or-later
-
-configure_file(
-  input: 'org.freedesktop.nm_priv_helper.service.in',
-  output: '@BASENAME@',
-  install_dir: dbus_system_bus_services_dir,
-  configuration: data_conf,
-)
-
-install_data(
-  'nm-priv-helper.conf',
-  install_dir: dbus_conf_dir,
-)
-
-executable(
-  'nm-priv-helper',
-  'nm-priv-helper.c',
-  include_directories : [
-    src_inc,
-    top_inc,
-  ],
-  dependencies: [
-    glib_dep,
-  ],
-  link_with: [
-    libnm_base,
-    libnm_log_null,
-    libnm_glib_aux,
-    libnm_std_aux,
-    libc_siphash,
-  ],
-  link_args: ldflags_linker_script_binary,
-  link_depends: linker_script_binary,
-  install: true,
-  install_dir: nm_libexecdir,
-)
diff --git a/src/nm-priv-helper/nm-priv-helper.c b/src/nm-priv-helper/nm-priv-helper.c
deleted file mode 100644
index e29113b112..0000000000
--- a/src/nm-priv-helper/nm-priv-helper.c
+++ /dev/null
@@ -1,695 +0,0 @@
-/* SPDX-License-Identifier: LGPL-2.1-or-later */
-
-#include "libnm-glib-aux/nm-default-glib-i18n-prog.h"
-
-#include <gio/gunixfdlist.h>
-
-#include "c-list/src/c-list.h"
-#include "libnm-base/nm-priv-helper-utils.h"
-#include "libnm-glib-aux/nm-dbus-aux.h"
-#include "libnm-glib-aux/nm-io-utils.h"
-#include "libnm-glib-aux/nm-logging-base.h"
-#include "libnm-glib-aux/nm-time-utils.h"
-
-/* nm-priv-helper doesn't link with libnm-core nor libnm-base, but these
- * headers can be used independently. */
-#include "libnm-core-public/nm-dbus-interface.h"
-
-/*****************************************************************************/
-
-#define IDLE_TIMEOUT_MSEC     2000
-#define IDLE_TIMEOUT_INFINITY G_MAXINT32
-
-/*****************************************************************************/
-
-/* Serves only the purpose to mark environment variables that are honored by
- * the application. You can search for this macro, and find what options are supported. */
-#define _ENV(var) ("" var "")
-
-/*****************************************************************************/
-
-typedef struct _GlobalData GlobalData;
-
-typedef struct {
-    CList       pending_jobs_lst;
-    GlobalData *gl;
-} PendingJobData;
-
-struct _GlobalData {
-    GDBusConnection *dbus_connection;
-    GCancellable    *quit_cancellable;
-
-    GSource *source_sigterm;
-
-    CList pending_jobs_lst_head;
-
-    GSource *source_idle_timeout;
-
-    char *name_owner;
-
-    gint64 start_timestamp_msec;
-
-    guint name_owner_changed_id;
-    guint service_regist_id;
-
-    guint32 timeout_msec;
-
-    bool name_owner_initialized;
-
-    /* This is controlled by $NM_PRIV_HELPER_NO_AUTH_FOR_TESTING. It disables authentication
-     * of the request, so it is ONLY for testing. */
-    bool no_auth_for_testing;
-
-    bool name_requested;
-    bool reject_new_requests;
-
-    bool shutdown_quitting;
-    bool shutdown_timeout;
-};
-
-/*****************************************************************************/
-
-static void _pending_job_register_object(GlobalData *gl, GObject *obj);
-
-/*****************************************************************************/
-
-#define _nm_log(level, ...) _nm_log_simple_printf((level), __VA_ARGS__)
-
-#define _NMLOG(level, ...)                 \
-    G_STMT_START                           \
-    {                                      \
-        const NMLogLevel _level = (level); \
-                                           \
-        if (_nm_logging_enabled(_level)) { \
-            _nm_log(_level, __VA_ARGS__);  \
-        }                                  \
-    }                                      \
-    G_STMT_END
-
-/*****************************************************************************/
-
-static void
-_handle_ping(GlobalData *gl, GDBusMethodInvocation *invocation, const char *arg)
-{
-    gs_free char *msg = NULL;
-    gint64        running_msec;
-
-    running_msec = nm_utils_clock_gettime_msec(CLOCK_BOOTTIME) - gl->start_timestamp_msec;
-
-    msg = g_strdup_printf("pid=%lu, unique-name=%s, nm-name-owner=%s, since=%" G_GINT64_FORMAT
-                          ".%03d%s, pong=%s",
-                          (unsigned long) getpid(),
-                          g_dbus_connection_get_unique_name(gl->dbus_connection),
-                          gl->name_owner ?: "(none)",
-                          (gint64) (running_msec / 1000),
-                          (int) (running_msec % 1000),
-                          gl->no_auth_for_testing ? ", no-auth-for-testing" : "",
-                          arg);
-    g_dbus_method_invocation_return_value(invocation, g_variant_new("(s)", msg));
-}
-
-static void
-_handle_get_fd(GlobalData *gl, GDBusMethodInvocation *invocation, guint32 fd_type)
-{
-    nm_auto_close int            fd      = -1;
-    gs_unref_object GUnixFDList *fd_list = NULL;
-    gs_free_error GError        *error   = NULL;
-
-    if (fd_type != (NMPrivHelperGetFDType) fd_type)
-        fd_type = NM_PRIV_HELPER_GET_FD_TYPE_NONE;
-
-    fd = nm_priv_helper_utils_open_fd(fd_type, &error);
-    if (fd < 0) {
-        g_dbus_method_invocation_take_error(invocation, g_steal_pointer(&error));
-        return;
-    }
-
-    fd_list = g_unix_fd_list_new_from_array(&fd, 1);
-    nm_steal_fd(&fd);
-
-    g_dbus_method_invocation_return_value_with_unix_fd_list(invocation, NULL, fd_list);
-}
-
-/*****************************************************************************/
-
-static gboolean
-_signal_callback_term(gpointer user_data)
-{
-    GlobalData *gl = user_data;
-
-    _LOGD("sigterm received (%s)",
-          c_list_is_empty(&gl->pending_jobs_lst_head) ? "quit mainloop" : "cancel operations");
-
-    gl->shutdown_quitting = TRUE;
-    g_cancellable_cancel(gl->quit_cancellable);
-    return G_SOURCE_CONTINUE;
-}
-
-/*****************************************************************************/
-
-static GDBusConnection *
-_bus_get(GCancellable *cancellable, int *out_exit_code)
-{
-    gs_free_error GError            *error           = NULL;
-    gs_unref_object GDBusConnection *dbus_connection = NULL;
-
-    dbus_connection = nm_g_bus_get_blocking(cancellable, &error);
-
-    if (!dbus_connection) {
-        gboolean was_cancelled = nm_utils_error_is_cancelled(error);
-
-        NM_SET_OUT(out_exit_code, was_cancelled ? EXIT_SUCCESS : EXIT_FAILURE);
-        if (!was_cancelled)
-            _LOGE("dbus: failure to get D-Bus connection: %s", error->message);
-        return NULL;
-    }
-
-    /* On bus-disconnect, GDBus will raise(SIGTERM), which we handle like a
-     * regular request to quit. */
-    g_dbus_connection_set_exit_on_close(dbus_connection, TRUE);
-
-    _LOGD("dbus: unique name: %s", g_dbus_connection_get_unique_name(dbus_connection));
-
-    return g_steal_pointer(&dbus_connection);
-}
-
-/*****************************************************************************/
-
-static void
-_name_owner_changed_cb(GDBusConnection *connection,
-                       const char      *sender_name,
-                       const char      *object_path,
-                       const char      *interface_name,
-                       const char      *signal_name,
-                       GVariant        *parameters,
-                       gpointer         user_data)
-{
-    GlobalData *gl = user_data;
-    const char *new_owner;
-
-    if (!gl->name_owner_initialized)
-        return;
-
-    if (!g_variant_is_of_type(parameters, G_VARIANT_TYPE("(sss)")))
-        return;
-
-    g_variant_get(parameters, "(&s&s&s)", NULL, NULL, &new_owner);
-    new_owner = nm_str_not_empty(new_owner);
-
-    _LOGD("%s name-owner changed: %s -> %s",
-          NM_DBUS_SERVICE,
-          gl->name_owner ?: "(null)",
-          new_owner ?: "(null)");
-
-    nm_strdup_reset(&gl->name_owner, new_owner);
-}
-
-typedef struct {
-    GlobalData *gl;
-    char      **p_name_owner;
-    gboolean    is_cancelled;
-} BusFindNMNameOwnerData;
-
-static void
-_bus_find_nm_nameowner_cb(const char *name_owner, GError *error, gpointer user_data)
-{
-    BusFindNMNameOwnerData *data = user_data;
-
-    *data->p_name_owner              = nm_strdup_not_empty(name_owner);
-    data->is_cancelled               = nm_utils_error_is_cancelled(error);
-    data->gl->name_owner_initialized = TRUE;
-}
-
-static gboolean
-_bus_find_nm_nameowner(GlobalData *gl)
-{
-    BusFindNMNameOwnerData data;
-    guint                  name_owner_changed_id;
-    gs_free char          *name_owner = NULL;
-
-    name_owner_changed_id =
-        nm_dbus_connection_signal_subscribe_name_owner_changed(gl->dbus_connection,
-                                                               NM_DBUS_SERVICE,
-                                                               _name_owner_changed_cb,
-                                                               gl,
-                                                               NULL);
-
-    data = (BusFindNMNameOwnerData){
-        .gl           = gl,
-        .is_cancelled = FALSE,
-        .p_name_owner = &name_owner,
-    };
-    nm_dbus_connection_call_get_name_owner(gl->dbus_connection,
-                                           NM_DBUS_SERVICE,
-                                           10000,
-                                           gl->quit_cancellable,
-                                           _bus_find_nm_nameowner_cb,
-                                           &data);
-    while (!gl->name_owner_initialized)
-        g_main_context_iteration(NULL, TRUE);
-
-    if (data.is_cancelled) {
-        g_dbus_connection_signal_unsubscribe(gl->dbus_connection, name_owner_changed_id);
-        return FALSE;
-    }
-
-    gl->name_owner_changed_id = name_owner_changed_id;
-    gl->name_owner            = g_steal_pointer(&name_owner);
-    return TRUE;
-}
-
-/*****************************************************************************/
-
-static void
-_bus_method_call(GDBusConnection       *connection,
-                 const char            *sender,
-                 const char            *object_path,
-                 const char            *interface_name,
-                 const char            *method_name,
-                 GVariant              *parameters,
-                 GDBusMethodInvocation *invocation,
-                 gpointer               user_data)
-{
-    GlobalData *gl = user_data;
-    const char *arg_s;
-    guint32     arg_u;
-
-    nm_assert(nm_streq(object_path, NM_PRIV_HELPER_DBUS_OBJECT_PATH));
-    nm_assert(nm_streq(interface_name, NM_PRIV_HELPER_DBUS_IFACE_NAME));
-
-    if (!gl->no_auth_for_testing && !nm_streq0(sender, gl->name_owner)) {
-        _LOGT("dbus: request sender=%s, %s%s, ACCESS DENIED",
-              sender,
-              method_name,
-              g_variant_get_type_string(parameters));
-        g_dbus_method_invocation_return_error(invocation,
-                                              G_DBUS_ERROR,
-                                              G_DBUS_ERROR_ACCESS_DENIED,
-                                              "Access denied");
-        return;
-    }
-
-    if (gl->reject_new_requests) {
-        /* after the name was released, we must not accept new requests. This new
-         * request was probably targeted against the unique-name. But we already
-         * gave up the well-known name. If we'd accept new request now, they would
-         * keep the service running indefinitely (and thus preventing the service
-         * to restart and serve the well-known name. */
-        _LOGT("dbus: request sender=%s, %s%s, SERVER SHUTTING DOWN",
-              sender,
-              method_name,
-              g_variant_get_type_string(parameters));
-        g_dbus_method_invocation_return_error(invocation,
-                                              G_DBUS_ERROR,
-                                              G_DBUS_ERROR_NO_SERVER,
-                                              "Server is exiting");
-        return;
-    }
-
-    _LOGT("dbus: request sender=%s, %s%s",
-          sender,
-          method_name,
-          g_variant_get_type_string(parameters));
-
-    if (!nm_streq(interface_name, NM_PRIV_HELPER_DBUS_IFACE_NAME))
-        goto out_unknown_method;
-
-    if (nm_streq(method_name, "GetFD")) {
-        g_variant_get(parameters, "(u)", &arg_u);
-        _handle_get_fd(gl, invocation, arg_u);
-        return;
-    }
-    if (nm_streq(method_name, "Ping")) {
-        g_variant_get(parameters, "(&s)", &arg_s);
-        _handle_ping(gl, invocation, arg_s);
-        return;
-    }
-
-out_unknown_method:
-    g_dbus_method_invocation_return_error(invocation,
-                                          G_DBUS_ERROR,
-                                          G_DBUS_ERROR_UNKNOWN_METHOD,
-                                          "Unknown method %s",
-                                          method_name);
-}
-
-static GDBusInterfaceInfo *const interface_info = NM_DEFINE_GDBUS_INTERFACE_INFO(
-    NM_PRIV_HELPER_DBUS_IFACE_NAME,
-    .methods = NM_DEFINE_GDBUS_METHOD_INFOS(
-        NM_DEFINE_GDBUS_METHOD_INFO(
-            "Ping",
-            .in_args  = NM_DEFINE_GDBUS_ARG_INFOS(NM_DEFINE_GDBUS_ARG_INFO("arg", "s"), ),
-            .out_args = NM_DEFINE_GDBUS_ARG_INFOS(NM_DEFINE_GDBUS_ARG_INFO("arg", "s"), ), ),
-        NM_DEFINE_GDBUS_METHOD_INFO("GetFD",
-                                    .in_args = NM_DEFINE_GDBUS_ARG_INFOS(
-                                        NM_DEFINE_GDBUS_ARG_INFO("fd_type", "u"), ), ), ), );
-
-static gboolean
-_bus_register_service(GlobalData *gl)
-{
-    static const GDBusInterfaceVTable interface_vtable = {
-        .method_call = _bus_method_call,
-    };
-    gs_free_error GError            *error = NULL;
-    NMDBusConnectionCallBlockingData data  = {
-         .result = NULL,
-    };
-    gs_unref_variant GVariant *ret = NULL;
-    guint32                    ret_val;
-
-    gl->service_regist_id =
-        g_dbus_connection_register_object(gl->dbus_connection,
-                                          NM_PRIV_HELPER_DBUS_OBJECT_PATH,
-                                          interface_info,
-                                          NM_UNCONST_PTR(GDBusInterfaceVTable, &interface_vtable),
-                                          gl,
-                                          NULL,
-                                          &error);
-    if (gl->service_regist_id == 0) {
-        _LOGE("dbus: error registering object %s: %s",
-              NM_PRIV_HELPER_DBUS_OBJECT_PATH,
-              error->message);
-        return FALSE;
-    }
-
-    _LOGD("dbus: object %s registered", NM_PRIV_HELPER_DBUS_OBJECT_PATH);
-
-    /* regardless whether the request is successful, after we start calling
-     * RequestName, we remember that we need to ReleaseName it. */
-    gl->name_requested = TRUE;
-
-    nm_dbus_connection_call_request_name(gl->dbus_connection,
-                                         NM_PRIV_HELPER_DBUS_BUS_NAME,
-                                         DBUS_NAME_FLAG_ALLOW_REPLACEMENT
-                                             | DBUS_NAME_FLAG_REPLACE_EXISTING,
-                                         10000,
-                                         gl->quit_cancellable,
-                                         nm_dbus_connection_call_blocking_callback,
-                                         &data);
-
-    /* Note that with D-Bus activation, the first request will already hit us before RequestName
-     * completes. So when we start iterating the main context, the first request may already come
-     * in. */
-
-    ret = nm_dbus_connection_call_blocking(&data, &error);
-
-    if (nm_utils_error_is_cancelled(error))
-        return FALSE;
-
-    if (error) {
-        _LOGE("d-bus: failed to request name %s: %s", NM_PRIV_HELPER_DBUS_BUS_NAME, error->message);
-        return FALSE;
-    }
-
-    g_variant_get(ret, "(u)", &ret_val);
-
-    if (ret_val != DBUS_REQUEST_NAME_REPLY_PRIMARY_OWNER) {
-        _LOGW("dbus: request name for %s failed to take name (response %u)",
-              NM_PRIV_HELPER_DBUS_BUS_NAME,
-              ret_val);
-        return FALSE;
-    }
-
-    _LOGD("dbus: request name for %s succeeded", NM_PRIV_HELPER_DBUS_BUS_NAME);
-    return TRUE;
-}
-
-/*****************************************************************************/
-
-static gboolean
-_idle_timeout_cb(gpointer user_data)
-{
-    GlobalData *gl = user_data;
-
-    _LOGT("idle-timeout: expired");
-    nm_clear_g_source_inst(&gl->source_idle_timeout);
-    gl->shutdown_timeout = TRUE;
-    return G_SOURCE_CONTINUE;
-}
-
-static void
-_idle_timeout_restart(GlobalData *gl)
-{
-    nm_clear_g_source_inst(&gl->source_idle_timeout);
-
-    if (gl->shutdown_quitting)
-        return;
-
-    if (!c_list_is_empty(&gl->pending_jobs_lst_head))
-        return;
-
-    if (gl->timeout_msec == IDLE_TIMEOUT_INFINITY)
-        return;
-
-    nm_assert(gl->timeout_msec < G_MAXINT32);
-    G_STATIC_ASSERT_EXPR(G_MAXINT32 < G_MAXUINT);
-
-    _LOGT("idle-timeout: start (%u msec)", gl->timeout_msec);
-    gl->source_idle_timeout = nm_g_timeout_add_source(gl->timeout_msec, _idle_timeout_cb, gl);
-}
-
-/*****************************************************************************/
-
-static gboolean
-_pending_job_register_object_release_on_idle_cb(gpointer data)
-{
-    PendingJobData *idle_data = data;
-    GlobalData     *gl        = idle_data->gl;
-
-    c_list_unlink_stale(&idle_data->pending_jobs_lst);
-    nm_g_slice_free(idle_data);
-
-    _idle_timeout_restart(gl);
-    return G_SOURCE_REMOVE;
-}
-
-static void
-_pending_job_register_object_weak_cb(gpointer data, GObject *where_the_object_was)
-{
-    /* The object might be destroyed on another thread. We need
-     * to sync with the main GMainContext by scheduling an idle action
-     * there. */
-    nm_g_idle_add(_pending_job_register_object_release_on_idle_cb, data);
-}
-
-static void
-_pending_job_register_object(GlobalData *gl, GObject *obj)
-{
-    PendingJobData *idle_data;
-
-    /* if we just hit the timeout, we can ignore it. */
-    gl->shutdown_timeout = FALSE;
-
-    if (nm_clear_g_source_inst(&gl->source_idle_timeout))
-        _LOGT("idle-timeout: suspend timeout for pending request");
-
-    idle_data = g_slice_new(PendingJobData);
-
-    idle_data->gl = gl;
-    c_list_link_tail(&gl->pending_jobs_lst_head, &idle_data->pending_jobs_lst);
-
-    g_object_weak_ref(obj, _pending_job_register_object_weak_cb, idle_data);
-}
-
-/*****************************************************************************/
-
-static void
-_bus_release_name_cb(GObject *source, GAsyncResult *result, gpointer user_data)
-{
-    _nm_unused gs_unref_object GObject *keep_alive_object = NULL;
-    GlobalData                         *gl;
-
-    nm_utils_user_data_unpack(user_data, &gl, &keep_alive_object);
-
-    gl->reject_new_requests = TRUE;
-    g_main_context_wakeup(NULL);
-}
-
-static gboolean
-_bus_release_name(GlobalData *gl)
-{
-    gs_unref_object GObject *keep_alive_object = NULL;
-    int                      r;
-
-    /* We already requested a name. To exit-on-idle without race, we need to dance.
-     * See https://lists.freedesktop.org/archives/dbus/2015-May/016671.html . */
-
-    if (!gl->name_requested)
-        return FALSE;
-
-    gl->name_requested    = FALSE;
-    gl->shutdown_quitting = TRUE;
-
-    _LOGT("shutdown: release-name");
-
-    keep_alive_object = g_object_new(G_TYPE_OBJECT, NULL);
-
-    /* we use the _pending_job_register_object() mechanism to make the loop busy during
-     * shutdown. */
-    _pending_job_register_object(gl, keep_alive_object);
-
-    r = nm_sd_notify("STOPPING=1");
-    if (r < 0)
-        _LOGW("shutdown: sd_notifiy(STOPPING=1) failed: %s", nm_strerror_native(-r));
-    else
-        _LOGT("shutdown: sd_notifiy(STOPPING=1) succeeded");
-
-    g_dbus_connection_call(gl->dbus_connection,
-                           DBUS_SERVICE_DBUS,
-                           DBUS_PATH_DBUS,
-                           DBUS_INTERFACE_DBUS,
-                           "ReleaseName",
-                           g_variant_new("(s)", NM_PRIV_HELPER_DBUS_BUS_NAME),
-                           G_VARIANT_TYPE("(u)"),
-                           G_DBUS_CALL_FLAGS_NONE,
-                           10000,
-                           NULL,
-                           _bus_release_name_cb,
-                           nm_utils_user_data_pack(gl, g_steal_pointer(&keep_alive_object)));
-    return TRUE;
-}
-
-/*****************************************************************************/
-
-static void
-_initial_setup(GlobalData *gl)
-{
-    gl->no_auth_for_testing =
-        _nm_utils_ascii_str_to_int64(g_getenv(_ENV("NM_PRIV_HELPER_NO_AUTH_FOR_TESTING")),
-                                     0,
-                                     0,
-                                     1,
-                                     0);
-    gl->timeout_msec =
-        _nm_utils_ascii_str_to_int64(g_getenv(_ENV("NM_PRIV_HELPER_IDLE_TIMEOUT_MSEC")),
-                                     0,
-                                     0,
-                                     G_MAXINT32,
-                                     IDLE_TIMEOUT_MSEC);
-
-    gl->quit_cancellable = g_cancellable_new();
-
-    signal(SIGPIPE, SIG_IGN);
-    gl->source_sigterm = nm_g_unix_signal_add_source(SIGTERM, _signal_callback_term, gl);
-}
-
-/*****************************************************************************/
-
-int
-main(int argc, char **argv)
-{
-    GlobalData _gl = {
-        .quit_cancellable      = NULL,
-        .pending_jobs_lst_head = C_LIST_INIT(_gl.pending_jobs_lst_head),
-    };
-    GlobalData *const gl = &_gl;
-    int               exit_code;
-    int               r = 0;
-
-    _nm_logging_enabled_init(g_getenv(_ENV("NM_PRIV_HELPER_LOG")));
-
-    gl->start_timestamp_msec = nm_utils_clock_gettime_msec(CLOCK_BOOTTIME);
-
-    _LOGD("starting nm-priv-helper (%s)", NM_DIST_VERSION);
-
-    _initial_setup(gl);
-
-    if (gl->no_auth_for_testing) {
-        _LOGW("WARNING: running in debug mode without authentication "
-              "(NM_PRIV_HELPER_NO_AUTH_FOR_TESTING). ");
-    }
-
-    if (gl->timeout_msec != IDLE_TIMEOUT_INFINITY)
-        _LOGT("idle-timeout: %u msec", gl->timeout_msec);
-    else
-        _LOGT("idle-timeout: disabled");
-
-    gl->dbus_connection = _bus_get(gl->quit_cancellable, &r);
-    if (!gl->dbus_connection) {
-        exit_code = r;
-        goto done;
-    }
-
-    if (!_bus_find_nm_nameowner(gl)) {
-        /* abort due to cancellation. That is success. */
-        exit_code = EXIT_SUCCESS;
-        goto done;
-    }
-    _LOGD("%s name-owner: %s", NM_DBUS_SERVICE, gl->name_owner ?: "(null)");
-
-    _idle_timeout_restart(gl);
-
-    exit_code = EXIT_SUCCESS;
-
-    if (!_bus_register_service(gl)) {
-        /* We failed to RequestName, but due to D-Bus activation we
-         * might have a pending request still (on the unique name).
-         * Process it below.
-         *
-         * Let's fake a shutdown signal, and still process the request below. */
-        if (!g_cancellable_is_cancelled(gl->quit_cancellable))
-            exit_code = EXIT_FAILURE;
-        gl->shutdown_quitting = TRUE;
-
-        if (gl->name_requested) {
-            /* We requested a name, but something went wrong. Below we will release
-             * the name right away. */
-        } else {
-            /* In case we didn't even went as far to request the name. New requests
-             * can only come via the unique name, and as we are shutting down, they
-             * are rejected. */
-            gl->reject_new_requests = TRUE;
-        }
-    }
-
-    while (TRUE) {
-        if (gl->shutdown_quitting)
-            _bus_release_name(gl);
-
-        if (!c_list_is_empty(&gl->pending_jobs_lst_head)) {
-            /* we must first reply to all requests. No matter what. */
-        } else if (gl->shutdown_quitting || gl->shutdown_timeout) {
-            /* we either hit the idle timeout or received SIGTERM. Note that
-             * if we received an idle-timeout and the very moment afterwards
-             * a new request, then _bus_method_call() will clear gl->shutdown_timeout
-             * (via _pending_job_register_object()). */
-            if (!_bus_release_name(gl))
-                break;
-        }
-
-        g_main_context_iteration(NULL, TRUE);
-    }
-
-done:
-    _LOGD("shutdown: cleanup");
-
-    gl->shutdown_quitting = TRUE;
-    g_cancellable_cancel(gl->quit_cancellable);
-
-    nm_assert(c_list_is_empty(&gl->pending_jobs_lst_head));
-
-    if (gl->service_regist_id != 0) {
-        g_dbus_connection_unregister_object(gl->dbus_connection,
-                                            nm_steal_int(&gl->service_regist_id));
-    }
-    if (gl->name_owner_changed_id != 0) {
-        g_dbus_connection_signal_unsubscribe(gl->dbus_connection,
-                                             nm_steal_int(&gl->name_owner_changed_id));
-    }
-
-    if (gl->dbus_connection) {
-        g_dbus_connection_flush_sync(gl->dbus_connection, NULL, NULL);
-        g_clear_object(&gl->dbus_connection);
-    }
-
-    nm_g_main_context_iterate_ready(NULL);
-
-    nm_clear_g_free(&gl->name_owner);
-
-    nm_clear_g_source_inst(&gl->source_sigterm);
-    nm_clear_g_source_inst(&gl->source_idle_timeout);
-    g_clear_object(&gl->quit_cancellable);
-
-    _LOGD("exit (%d)", exit_code);
-    return exit_code;
-}
diff --git a/src/nm-priv-helper/nm-priv-helper.conf b/src/nm-priv-helper/nm-priv-helper.conf
deleted file mode 100644
index c0d046178b..0000000000
--- a/src/nm-priv-helper/nm-priv-helper.conf
+++ /dev/null
@@ -1,13 +0,0 @@
-<!DOCTYPE busconfig PUBLIC
- "-//freedesktop//DTD D-BUS Bus Configuration 1.0//EN"
- "http://www.freedesktop.org/standards/dbus/1.0/busconfig.dtd">
-<busconfig>
-    <policy user="root">
-        <allow own="org.freedesktop.nm_priv_helper"/>
-        <allow send_destination="org.freedesktop.nm_priv_helper"/>
-    </policy>
-    <policy context="default">
-        <deny own="org.freedesktop.nm_priv_helper"/>
-        <deny send_destination="org.freedesktop.nm_priv_helper"/>
-    </policy>
-</busconfig>
diff --git a/src/nm-priv-helper/org.freedesktop.nm_priv_helper.service.in b/src/nm-priv-helper/org.freedesktop.nm_priv_helper.service.in
deleted file mode 100644
index 9e86a635b2..0000000000
--- a/src/nm-priv-helper/org.freedesktop.nm_priv_helper.service.in
+++ /dev/null
@@ -1,5 +0,0 @@
-[D-BUS Service]
-Name=org.freedesktop.nm_priv_helper
-Exec=@libexecdir@/nm-priv-helper
-User=root
-SystemdService=nm-priv-helper.service
