userdb: Add userdb.user.* and userdb.group.* credentials (#36740)

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 /run/userdb.
This commit is contained in:
Daan De Meyer
2025-03-19 10:30:52 +01:00
committed by GitHub
19 changed files with 467 additions and 36 deletions

View File

@@ -79,7 +79,7 @@ jobs:
vm: 0
skip: TEST-21-DFUZZER
- distro: fedora
release: "41"
release: "42"
sanitizers: address,undefined
llvm: 1
cflags: "-Og"

9
TODO
View File

@@ -383,13 +383,8 @@ Features:
* systemd-firstboot: optionally install an ssh key for root for offline use.
* add a small tool that reads user records/group records from a credential, and
then places them in the userdb drop-in dirs (either /run/ or /var/). While
doing so it processes them:
- split privileged part from unprivileged part (the way userdb dropins want
it
- write out membership files based on the listed group memberships
- maybe: also allocate a UID if none is included.
* Allocate UIDs/GIDs automatically in userdbctl load-credentials if none are
included in the user/group record credentials
* the ordering cycle log messages in transaction_verify_order_one() should
really be recognizable via a message id and come with an explanatory catalog

View File

@@ -471,6 +471,40 @@
<xi:include href="version-info.xml" xpointer="v257"/>
</listitem>
</varlistentry>
<varlistentry>
<term><varname>userdb.user.*</varname></term>
<term><varname>userdb.group.*</varname></term>
<listitem>
<para>Configure JSON user and group records. Read by
<filename>systemd-userdb-load-credentials.service</filename>, which invokes
<command>userdbctl load-credentials</command>. These credentials directly translate to
matching
<ulink url="https://systemd.io/USER_RECORD">JSON User</ulink> and
<ulink url="https://systemd.io/GROUP_RECORD">JSON Group</ulink> records. Example: the contents of a
credential <filename>userdb.user.foobar</filename> will be copied into a file
<filename>/etc/userdb/foobar.user</filename>, and
<filename>userdb.group.foobar</filename> will be copied into a file
<filename>/etc/userdb/foobar.group</filename>. Symlinks for the uid/gid will also be created in
<filename>/etc/userdb/</filename>, as well as the corresponding<filename>.membership</filename>
files. See
<citerefentry><refentrytitle>systemd-userdb</refentrytitle><manvolnum>7</manvolnum></citerefentry>,
<citerefentry><refentrytitle>nss-systemd</refentrytitle><manvolnum>8</manvolnum></citerefentry>, and
<citerefentry><refentrytitle>userdbctl</refentrytitle><manvolnum>1</manvolnum></citerefentry>
for details.</para>
<para>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
<literal>userdb.user.foobar</literal> the suffix is <literal>foobar</literal>) must match the user
or group name field from the user or group record.</para>
<para>Note that the records are created in <filename>/etc/userdb/</filename>
(<filename>/etc/passwd</filename> and <filename>/etc/group</filename> are not modified).</para>
<xi:include href="version-info.xml" xpointer="v258"/>
</listitem>
</varlistentry>
</variablelist>
</refsect1>

View File

@@ -332,6 +332,36 @@
<xi:include href="version-info.xml" xpointer="v245"/></listitem>
</varlistentry>
<varlistentry>
<term><command>load-credentials</command></term>
<listitem>
<para>When specified, the following credentials are used when passed in:</para>
<variablelist>
<varlistentry>
<term><varname>userdb.user.*</varname></term>
<term><varname>userdb.group.*</varname></term>
<listitem>
<para>These credentials should contain valid
<ulink url="https://systemd.io/USER_RECORD">JSON User</ulink> and
<ulink url="https://systemd.io/GROUP_RECORD">JSON Group</ulink> records. For each matching
credential, various files are created in <filename>/etc/userdb/</filename>, implementing the
interface described in
<citerefentry><refentrytitle>nss-systemd</refentrytitle><manvolnum>8</manvolnum></citerefentry>.
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
<literal>userdb.user.foobar</literal> the suffix is <literal>foobar</literal>) must match the
user or group name encoded in the record.</para>
<xi:include href="version-info.xml" xpointer="v258"/>
</listitem>
</varlistentry>
</variablelist>
<xi:include href="version-info.xml" xpointer="v258"/>
</listitem>
</varlistentry>
</variablelist>
</refsect1>

View File

@@ -0,0 +1,5 @@
{
"groupName": "testuser",
"gid": 4711,
"disposition": "regular"
}

View File

@@ -0,0 +1,14 @@
{
"userName": "testuser",
"uid": 4711,
"disposition": "regular",
"enforcePasswordPolicy": false,
"memberOf": [
"wheel",
"systemd-journal"
],
"shell": "/bin/bash",
"privileged": {
"hashedPassword": ["$1$kqp7NF1f$tNnQcshPX53CSfRKTQD0R1"]
}
}

View File

@@ -22,6 +22,9 @@ enable systemd-networkd-wait-online.service
# systemd-resolved is disable by default on CentOS so make sure it is enabled.
enable systemd-resolved.service
# systemd-userdbd.socket is disabled by default on OpenSUSE
enable systemd-userdbd.socket
# We install dnf in some images but it's only going to be used rarely,
# so let's not have dnf create its cache.
disable dnf-makecache.*

View File

@@ -1,3 +0,0 @@
# SPDX-License-Identifier: LGPL-2.1-or-later
z! /home/testuser 700 testuser testuser

View File

@@ -5,14 +5,6 @@ set -o nounset
find "$BUILDDIR" \( -name "*.rpm" -o -name "*.deb" -o -name "*.pkg.tar" -o -name systemd.raw \) -exec cp -t "$OUTPUTDIR" {} \;
useradd \
--uid 4711 \
--user-group \
--create-home \
--password "$(openssl passwd -1 testuser)" \
--shell /bin/bash \
testuser
if command -v authselect >/dev/null; then
# authselect 1.5.0 renamed the minimal profile to the local profile without keeping backwards compat so
# let's use the new name if it exists.

View File

@@ -82,6 +82,7 @@ wrap=(
su
tar
tgtd
unix_chkpwd
useradd
userdel
veritysetup

View File

@@ -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;

View File

@@ -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");
}

View File

@@ -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);

View File

@@ -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");

View File

@@ -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);

View File

@@ -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 },
{}
};

View File

@@ -818,6 +818,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'],

View File

@@ -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

View File

@@ -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]