From fe0342edf4693ac14c8cb9a977afa09e4acd4daf Mon Sep 17 00:00:00 2001 From: Daan De Meyer Date: Thu, 13 Mar 2025 15:22:34 +0100 Subject: [PATCH] userdb: Add userdb.user.* and userdb.group.* credentials Let's allow providing extra userdb users and groups via credentials. Similarly to systemd-udev-load-credentials.service, we ship systemd-userdb-load-credentials.service which transform the JSON user/group records provided via the corresponding credentials to static userdb dropins in /etc/userdb. Replaces #33811 --- man/systemd.system-credentials.xml | 34 ++ man/userdbctl.xml | 30 ++ src/nspawn/nspawn-bind-user.c | 12 +- src/shared/group-record.c | 12 + src/shared/group-record.h | 3 + src/shared/user-record.c | 4 +- src/shared/user-record.h | 16 +- src/userdb/userdbctl.c | 311 +++++++++++++++++- units/meson.build | 4 + units/systemd-userdb-load-credentials.service | 31 ++ units/systemd-userdbd.service.in | 1 + 11 files changed, 441 insertions(+), 17 deletions(-) create mode 100644 units/systemd-userdb-load-credentials.service diff --git a/man/systemd.system-credentials.xml b/man/systemd.system-credentials.xml index 59819865487..73c36c19153 100644 --- a/man/systemd.system-credentials.xml +++ b/man/systemd.system-credentials.xml @@ -471,6 +471,40 @@ + + + userdb.user.* + userdb.group.* + + + Configure JSON user and group records. Read by + systemd-userdb-load-credentials.service, which invokes + userdbctl load-credentials. These credentials directly translate to + matching + JSON User and + JSON Group records. Example: the contents of a + credential userdb.user.foobar will be copied into a file + /etc/userdb/foobar.user, and + userdb.group.foobar will be copied into a file + /etc/userdb/foobar.group. Symlinks for the uid/gid will also be created in + /etc/userdb/, as well as the corresponding.membership + files. See + systemd-userdb7, + nss-systemd8, and + userdbctl1 + for details. + + Any passed user records must contain uid and gid fields. Any passed group records must + contain a gid field. For both user and group records, the credential suffix (for + userdb.user.foobar the suffix is foobar) must match the user + or group name field from the user or group record. + + Note that the records are created in /etc/userdb/ + (/etc/passwd and /etc/group are not modified). + + + + diff --git a/man/userdbctl.xml b/man/userdbctl.xml index d967783a2af..77d0b1ffaa0 100644 --- a/man/userdbctl.xml +++ b/man/userdbctl.xml @@ -332,6 +332,36 @@ + + + load-credentials + + When specified, the following credentials are used when passed in: + + + + userdb.user.* + userdb.group.* + + These credentials should contain valid + JSON User and + JSON Group records. For each matching + credential, various files are created in /etc/userdb/, implementing the + interface described in + nss-systemd8. + Any passed user records must contain uid and gid fields. Any passed group records must + contain a gid field. For both user and group records, the credential suffix (for + userdb.user.foobar the suffix is foobar) must match the + user or group name encoded in the record. + + + + + + + + + diff --git a/src/nspawn/nspawn-bind-user.c b/src/nspawn/nspawn-bind-user.c index 373a05af3e9..3e402ac33a9 100644 --- a/src/nspawn/nspawn-bind-user.c +++ b/src/nspawn/nspawn-bind-user.c @@ -369,18 +369,10 @@ int bind_user_setup( const char *root) { static const UserRecordLoadFlags strip_flags = /* Removes privileged info */ - USER_RECORD_REQUIRE_REGULAR| - USER_RECORD_STRIP_PRIVILEGED| - USER_RECORD_ALLOW_PER_MACHINE| - USER_RECORD_ALLOW_BINDING| - USER_RECORD_ALLOW_SIGNATURE| + USER_RECORD_LOAD_MASK_PRIVILEGED| USER_RECORD_PERMISSIVE; static const UserRecordLoadFlags shadow_flags = /* Extracts privileged info */ - USER_RECORD_STRIP_REGULAR| - USER_RECORD_ALLOW_PRIVILEGED| - USER_RECORD_STRIP_PER_MACHINE| - USER_RECORD_STRIP_BINDING| - USER_RECORD_STRIP_SIGNATURE| + USER_RECORD_EXTRACT_PRIVILEGED| USER_RECORD_EMPTY_OK| USER_RECORD_PERMISSIVE; int r; diff --git a/src/shared/group-record.c b/src/shared/group-record.c index 04989eb63aa..d4acc1a2620 100644 --- a/src/shared/group-record.c +++ b/src/shared/group-record.c @@ -372,3 +372,15 @@ bool group_record_match(GroupRecord *h, const UserDBMatch *match) { return true; } + +bool group_record_is_root(const GroupRecord *g) { + assert(g); + + return g->gid == 0 || streq_ptr(g->group_name, "root"); +} + +bool group_record_is_nobody(const GroupRecord *g) { + assert(g); + + return g->gid == GID_NOBODY || STRPTR_IN_SET(g->group_name, NOBODY_GROUP_NAME, "nobody"); +} diff --git a/src/shared/group-record.h b/src/shared/group-record.h index ee8d3995779..95e70cf2694 100644 --- a/src/shared/group-record.h +++ b/src/shared/group-record.h @@ -49,3 +49,6 @@ const char* group_record_group_name_and_realm(GroupRecord *h); UserDisposition group_record_disposition(GroupRecord *h); bool group_record_matches_group_name(const GroupRecord *g, const char *groupname); + +bool group_record_is_root(const GroupRecord *g); +bool group_record_is_nobody(const GroupRecord *g); diff --git a/src/shared/user-record.c b/src/shared/user-record.c index 4ce48066f97..63d47ec9b1b 100644 --- a/src/shared/user-record.c +++ b/src/shared/user-record.c @@ -2717,13 +2717,13 @@ int user_record_test_password_change_required(UserRecord *h) { return change_permitted ? 0 : -EROFS; } -int user_record_is_root(const UserRecord *u) { +bool user_record_is_root(const UserRecord *u) { assert(u); return u->uid == 0 || streq_ptr(u->user_name, "root"); } -int user_record_is_nobody(const UserRecord *u) { +bool user_record_is_nobody(const UserRecord *u) { assert(u); return u->uid == UID_NOBODY || STRPTR_IN_SET(u->user_name, NOBODY_USER_NAME, "nobody"); diff --git a/src/shared/user-record.h b/src/shared/user-record.h index 24dc1310ef9..139651714b0 100644 --- a/src/shared/user-record.h +++ b/src/shared/user-record.h @@ -132,6 +132,18 @@ typedef enum UserRecordLoadFlags { USER_RECORD_STRIP_BINDING | USER_RECORD_STRIP_STATUS, + USER_RECORD_LOAD_MASK_PRIVILEGED = USER_RECORD_REQUIRE_REGULAR| + USER_RECORD_STRIP_PRIVILEGED| + USER_RECORD_ALLOW_PER_MACHINE| + USER_RECORD_ALLOW_BINDING| + USER_RECORD_ALLOW_SIGNATURE, + + USER_RECORD_EXTRACT_PRIVILEGED = USER_RECORD_STRIP_REGULAR| + USER_RECORD_ALLOW_PRIVILEGED| + USER_RECORD_STRIP_PER_MACHINE| + USER_RECORD_STRIP_BINDING| + USER_RECORD_STRIP_SIGNATURE, + /* Whether to log about loader errors beyond LOG_DEBUG */ USER_RECORD_LOG = 1U << 28, @@ -477,8 +489,8 @@ int user_record_masked_equal(UserRecord *a, UserRecord *b, UserRecordMask mask); int user_record_test_blocked(UserRecord *h); int user_record_test_password_change_required(UserRecord *h); -int user_record_is_root(const UserRecord *u); -int user_record_is_nobody(const UserRecord *u); +bool user_record_is_root(const UserRecord *u); +bool user_record_is_nobody(const UserRecord *u); /* The following six are user by group-record.c, that's why we export them here */ int json_dispatch_realm(const char *name, sd_json_variant *variant, sd_json_dispatch_flags_t flags, void *userdata); diff --git a/src/userdb/userdbctl.c b/src/userdb/userdbctl.c index 2e5a7fc3821..2bc26043186 100644 --- a/src/userdb/userdbctl.c +++ b/src/userdb/userdbctl.c @@ -4,22 +4,28 @@ #include "bitfield.h" #include "build.h" +#include "copy.h" +#include "creds-util.h" #include "dirent-util.h" #include "errno-list.h" #include "escape.h" #include "fd-util.h" +#include "fileio.h" #include "format-table.h" #include "format-util.h" #include "main-func.h" +#include "mkdir-label.h" #include "pager.h" #include "parse-argument.h" #include "parse-util.h" #include "pretty-print.h" +#include "recurse-dir.h" #include "socket-util.h" #include "strv.h" #include "terminal-util.h" #include "uid-classification.h" #include "uid-range.h" +#include "umask-util.h" #include "user-record-show.h" #include "user-util.h" #include "userdb.h" @@ -1164,6 +1170,306 @@ static int ssh_authorized_keys(int argc, char *argv[], void *userdata) { return r; } +static int load_credential_one(int credential_dir_fd, const char *name, int userdb_dir_fd) { + int r; + + assert(credential_dir_fd >= 0); + assert(name); + assert(userdb_dir_fd >= 0); + + const char *user = startswith(name, "userdb.user."); + const char *group = startswith(name, "userdb.group."); + if (!user && !group) + return 0; + + _cleanup_(sd_json_variant_unrefp) sd_json_variant *v = NULL; + unsigned line = 0, column = 0; + r = sd_json_parse_file_at(NULL, credential_dir_fd, name, SD_JSON_PARSE_SENSITIVE, &v, &line, &column); + if (r < 0) + return log_error_errno(r, "Failed to parse credential '%s' as JSON at %u:%u: %m", name, line, column); + + _cleanup_(user_record_unrefp) UserRecord *ur = NULL, *ur_stripped = NULL, *ur_privileged = NULL; + _cleanup_(group_record_unrefp) GroupRecord *gr = NULL, *gr_stripped = NULL, *gr_privileged = NULL; + _cleanup_free_ char *fn = NULL, *link = NULL; + + if (user) { + ur = user_record_new(); + if (!ur) + return log_oom(); + + r = user_record_load(ur, v, USER_RECORD_LOAD_MASK_SECRET|USER_RECORD_LOG); + if (r < 0) + return r; + + if (user_record_is_root(ur)) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Creating 'root' user from credentials is not supported."); + if (user_record_is_nobody(ur)) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Creating 'nobody' user from credentials is not supported."); + + if (!streq_ptr(user, ur->user_name)) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), + "Credential suffix '%s' does not match user record name '%s'", + user, strna(ur->user_name)); + + if (!uid_is_valid(ur->uid)) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "JSON user record missing uid field"); + + if (!gid_is_valid(user_record_gid(ur))) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "JSON user record missing gid field"); + + _cleanup_(user_record_unrefp) UserRecord *m = NULL; + r = userdb_by_name(ur->user_name, /* match= */ NULL, USERDB_SUPPRESS_SHADOW, &m); + if (r >= 0) { + if (m->uid != ur->uid) + return log_error_errno(SYNTHETIC_ERRNO(EEXIST), + "Cannot create user %s from credential %s as it already exists with UID " UID_FMT " instead of " UID_FMT, + ur->user_name, name, m->uid, ur->uid); + + log_info("User with name %s and UID " UID_FMT " already exists, not creating user from credential %s", ur->user_name, ur->uid, name); + return 0; + } + if (r != -ESRCH) + return log_error_errno(r, "Failed to check if user with name %s already exists: %m", ur->user_name); + + m = user_record_unref(m); + r = userdb_by_uid(ur->uid, /* match= */ NULL, USERDB_SUPPRESS_SHADOW, &m); + if (r >= 0) { + if (!streq_ptr(ur->user_name, m->user_name)) + return log_error_errno(SYNTHETIC_ERRNO(EEXIST), + "Cannot create user %s from credential %s as UID " UID_FMT " is already assigned to user %s", + ur->user_name, name, ur->uid, m->user_name); + + log_info("User with name %s and UID " UID_FMT " already exists, not creating user from credential %s", ur->user_name, ur->uid, name); + return 0; + } + if (r != -ESRCH) + return log_error_errno(r, "Failed to check if user with UID " UID_FMT " already exists: %m", ur->uid); + + r = user_record_clone(ur, USER_RECORD_LOAD_MASK_PRIVILEGED|USER_RECORD_LOG, &ur_stripped); + if (r < 0) + return r; + + r = user_record_clone(ur, USER_RECORD_EXTRACT_PRIVILEGED|USER_RECORD_EMPTY_OK|USER_RECORD_LOG, &ur_privileged); + if (r < 0) + return r; + + fn = strjoin(ur->user_name, ".user"); + if (!fn) + return log_oom(); + + if (asprintf(&link, UID_FMT ".user", ur->uid) < 0) + return log_oom(); + } else { + assert(group); + + gr = group_record_new(); + if (!gr) + return log_oom(); + + r = group_record_load(gr, v, USER_RECORD_LOAD_MASK_SECRET|USER_RECORD_LOG); + if (r < 0) + return r; + + if (group_record_is_root(gr)) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Creating 'root' group from credentials is not supported."); + if (group_record_is_nobody(gr)) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Creating 'nobody' group from credentials is not supported."); + + if (!streq_ptr(group, gr->group_name)) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), + "Credential suffix '%s' does not match group record name '%s'", + group, strna(gr->group_name)); + + if (!gid_is_valid(gr->gid)) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "JSON group record missing gid field"); + + _cleanup_(group_record_unrefp) GroupRecord *m = NULL; + r = groupdb_by_name(gr->group_name, /* match= */ NULL, USERDB_SUPPRESS_SHADOW, &m); + if (r >= 0) { + if (m->gid != gr->gid) + return log_error_errno(SYNTHETIC_ERRNO(EEXIST), + "Cannot create group %s from credential %s as it already exists with GID " GID_FMT " instead of " GID_FMT, + gr->group_name, name, m->gid, gr->gid); + + log_info("Group with name %s and GID " GID_FMT " already exists, not creating group from credential %s", gr->group_name, gr->gid, name); + return 0; + } + if (r != -ESRCH) + return log_error_errno(r, "Failed to check if group with name %s already exists: %m", gr->group_name); + + m = group_record_unref(m); + r = groupdb_by_gid(gr->gid, /* match= */ NULL, USERDB_SUPPRESS_SHADOW, &m); + if (r >= 0) { + if (!streq_ptr(gr->group_name, m->group_name)) + return log_error_errno(SYNTHETIC_ERRNO(EEXIST), + "Cannot create group %s from credential %s as GID " GID_FMT " is already assigned to group %s", + gr->group_name, name, gr->gid, m->group_name); + + log_info("Group with name %s and GID " GID_FMT " already exists, not creating group from credential %s", gr->group_name, gr->gid, name); + return 0; + } + if (r != -ESRCH) + return log_error_errno(r, "Failed to check if group with GID " GID_FMT " already exists: %m", gr->gid); + + r = group_record_clone(gr, USER_RECORD_LOAD_MASK_PRIVILEGED|USER_RECORD_LOG, &gr_stripped); + if (r < 0) + return r; + + r = group_record_clone(gr, USER_RECORD_EXTRACT_PRIVILEGED|USER_RECORD_EMPTY_OK|USER_RECORD_LOG, &gr_privileged); + if (r < 0) + return r; + + fn = strjoin(gr->group_name, ".group"); + if (!fn) + return log_oom(); + + if (asprintf(&link, GID_FMT ".group", gr->gid) < 0) + return log_oom(); + } + + if (!filename_is_valid(fn)) + return log_error_errno(SYNTHETIC_ERRNO(EINVAL), + "Passed credential '%s' would result in invalid filename '%s'.", + name, fn); + + _cleanup_free_ char *formatted = NULL; + r = sd_json_variant_format(ur ? ur_stripped->json : gr_stripped->json, SD_JSON_FORMAT_NEWLINE, &formatted); + if (r < 0) + return log_error_errno(r, "Failed to format JSON record: %m"); + + r = write_string_file_at(userdb_dir_fd, fn, formatted, WRITE_STRING_FILE_CREATE|WRITE_STRING_FILE_ATOMIC); + if (r < 0) + return log_error_errno(r, "Failed to write JSON record to /etc/userdb/%s: %m", fn); + + if (symlinkat(fn, userdb_dir_fd, link) < 0) + return log_error_errno(errno, "Failed to create symlink from %s to %s", link, fn); + + log_info("Installed /etc/userdb/%s from credential.", fn); + + if ((ur && !sd_json_variant_is_blank_object(ur_privileged->json)) || + (gr && !sd_json_variant_is_blank_object(gr_privileged->json))) { + fn = mfree(fn); + fn = strjoin(ur ? ur->user_name : gr->group_name, ur ? ".user-privileged" : ".group-privileged"); + if (!fn) + return log_oom(); + + formatted = mfree(formatted); + r = sd_json_variant_format(ur ? ur_privileged->json : gr_privileged->json, SD_JSON_FORMAT_NEWLINE, &formatted); + if (r < 0) + return log_error_errno(r, "Failed to format JSON record: %m"); + + r = write_string_file_at(userdb_dir_fd, fn, formatted, WRITE_STRING_FILE_CREATE|WRITE_STRING_FILE_ATOMIC|WRITE_STRING_FILE_MODE_0600); + if (r < 0) + return log_error_errno(r, "Failed to write JSON record to /etc/userdb/%s: %m", fn); + + link = mfree(link); + + if (ur) { + if (asprintf(&link, UID_FMT ".user-privileged", ur->uid) < 0) + return log_oom(); + } else { + if (asprintf(&link, GID_FMT ".group-privileged", gr->gid) < 0) + return log_oom(); + } + + if (symlinkat(fn, userdb_dir_fd, link) < 0) + return log_error_errno(errno, "Failed to create symlink from %s to %s", link, fn); + + log_info("Installed /etc/userdb/%s from credential.", fn); + } + + if (ur) + STRV_FOREACH(g, ur->member_of) { + _cleanup_free_ char *membership = strjoin(ur->user_name, ":", *g); + if (!membership) + return log_oom(); + + _cleanup_close_ int fd = openat(userdb_dir_fd, membership, O_WRONLY|O_CREAT|O_CLOEXEC, 0644); + if (fd < 0) + return log_error_errno(errno, "Failed to create %s: %m", membership); + + log_info("Installed /etc/userdb/%s from credential.", membership); + } + else + STRV_FOREACH(u, gr->members) { + _cleanup_free_ char *membership = strjoin(*u, ":", gr->group_name); + if (!membership) + return log_oom(); + + _cleanup_close_ int fd = openat(userdb_dir_fd, membership, O_WRONLY|O_CREAT|O_CLOEXEC, 0644); + if (fd < 0) + return log_error_errno(errno, "Failed to create %s: %m", membership); + + log_info("Installed /etc/userdb/%s from credential.", membership); + } + + if (ur && user_record_disposition(ur) == USER_REGULAR) { + const char *hd = user_record_home_directory(ur); + + r = RET_NERRNO(access(hd, F_OK)); + if (r < 0) { + if (r != -ENOENT) + return log_error_errno(r, "Failed to check if %s exists: %m", hd); + + WITH_UMASK(0000) { + r = mkdir_parents(hd, 0755); + if (r < 0) + return log_error_errno(r, "Failed to create parent directories of %s: %m", hd); + + if (mkdir(hd, 0700) < 0 && errno != EEXIST) + return log_error_errno(errno, "Failed to create %s: %m", hd); + } + + if (chown(hd, ur->uid, user_record_gid(ur)) < 0) + return log_error_errno(errno, "Failed to chown %s: %m", hd); + + r = copy_tree(user_record_skeleton_directory(ur), hd, ur->uid, user_record_gid(ur), + COPY_REFLINK|COPY_MERGE, /* denylist= */ NULL, /* subvolumes= */NULL); + if (r < 0 && r != -ENOENT) + return log_error_errno(r, "Failed to copy skeleton directory to %s: %m", hd); + } + } + + return 0; +} + +static int load_credentials(int argc, char *argv[], void *userdata) { + int r; + + _cleanup_close_ int credential_dir_fd = open_credentials_dir(); + if (IN_SET(credential_dir_fd, -ENXIO, -ENOENT)) { + /* Credential env var not set, or dir doesn't exist. */ + log_debug("No credentials found."); + return 0; + } + if (credential_dir_fd < 0) + return log_error_errno(credential_dir_fd, "Failed to open credentials directory: %m"); + + _cleanup_free_ DirectoryEntries *des = NULL; + r = readdir_all(credential_dir_fd, RECURSE_DIR_SORT|RECURSE_DIR_IGNORE_DOT|RECURSE_DIR_ENSURE_TYPE, &des); + if (r < 0) + return log_error_errno(r, "Failed to enumerate credentials: %m"); + + _cleanup_close_ int userdb_dir_fd = xopenat_full( + AT_FDCWD, "/etc/userdb", + /* open_flags= */ O_DIRECTORY|O_CREAT|O_CLOEXEC, + /* xopen_flags= */ XO_LABEL, + /* mode= */ 0755); + if (userdb_dir_fd < 0) + return log_error_errno(userdb_dir_fd, "Failed to open '/etc/userdb/': %m"); + + FOREACH_ARRAY(i, des->entries, des->n_entries) { + struct dirent *de = *i; + + if (de->d_type != DT_REG) + continue; + + RET_GATHER(r, load_credential_one(credential_dir_fd, de->d_name, userdb_dir_fd)); + } + + return r; +} + static int help(int argc, char *argv[], void *userdata) { _cleanup_free_ char *link = NULL; int r; @@ -1183,6 +1489,7 @@ static int help(int argc, char *argv[], void *userdata) { " groups-of-user [USER…] Show groups the specified users are members of\n" " services Show enabled database services\n" " ssh-authorized-keys USER Show SSH authorized keys for user\n" + " load-credentials Write static user/group records from credentials\n" "\nOptions:\n" " -h --help Show this help\n" " --version Show package version\n" @@ -1512,10 +1819,8 @@ static int run(int argc, char *argv[]) { { "users-in-group", VERB_ANY, VERB_ANY, 0, display_memberships }, { "groups-of-user", VERB_ANY, VERB_ANY, 0, display_memberships }, { "services", VERB_ANY, 1, 0, display_services }, - - /* This one is a helper for sshd_config's AuthorizedKeysCommand= setting, it's not a - * user-facing verb and thus should not appear in man pages or --help texts. */ { "ssh-authorized-keys", 2, VERB_ANY, 0, ssh_authorized_keys }, + { "load-credentials", VERB_ANY, 1, 0, load_credentials }, {} }; diff --git a/units/meson.build b/units/meson.build index b29bed068f6..1b92ccf8b22 100644 --- a/units/meson.build +++ b/units/meson.build @@ -813,6 +813,10 @@ units = [ 'conditions' : ['HAVE_PAM'], 'symlinks' : ['multi-user.target.wants/'], }, + { + 'file' : 'systemd-userdb-load-credentials.service', + 'conditions' : ['ENABLE_USERDB'], + }, { 'file' : 'systemd-userdbd.service.in', 'conditions' : ['ENABLE_USERDB'], diff --git a/units/systemd-userdb-load-credentials.service b/units/systemd-userdb-load-credentials.service new file mode 100644 index 00000000000..5bcd6d18966 --- /dev/null +++ b/units/systemd-userdb-load-credentials.service @@ -0,0 +1,31 @@ +# SPDX-License-Identifier: LGPL-2.1-or-later +# +# This file is part of systemd. +# +# systemd is free software; you can redistribute it and/or modify it +# under the terms of the GNU Lesser General Public License as published by +# the Free Software Foundation; either version 2.1 of the License, or +# (at your option) any later version. + +[Unit] +Description=Load JSON user/group Records from Credentials +Documentation=man:systemd-userdb(8) +Documentation=man:systemd.system-credentials(7) + +DefaultDependencies=no +Before=systemd-user-sessions.service nss-user-lookup.target +After=local-fs.target +Conflicts=shutdown.target +Before=shutdown.target + +ConditionPathExists=!/etc/initrd-release + +[Service] +Type=oneshot +RemainAfterExit=yes +ExecStart=userdbctl load-credentials +ImportCredential=userdb.user.* +ImportCredential=userdb.group.* + +[Install] +WantedBy=sysinit.target diff --git a/units/systemd-userdbd.service.in b/units/systemd-userdbd.service.in index 1c092654b99..e853bb0c6fc 100644 --- a/units/systemd-userdbd.service.in +++ b/units/systemd-userdbd.service.in @@ -13,6 +13,7 @@ Documentation=man:systemd-userdbd.service(8) Requires=systemd-userdbd.socket After=systemd-userdbd.socket Before=sysinit.target +Wants=systemd-userdb-load-credentials.service DefaultDependencies=no [Service] -- 2.47.3