Merge pull request #23170 from poettering/creds-copy

import system credentials from sd-stub + qemu fw_cfg + kernel cmdline explicitly in PID 1
This commit is contained in:
Lennart Poettering 2022-05-02 16:32:21 +02:00 committed by GitHub
commit 41be3b099f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
21 changed files with 1341 additions and 111 deletions

12
TODO
View file

@ -166,11 +166,6 @@ Features:
don't query this unnecessarily in entirely uninitialized
containers. (i.e. containers with empty /etc).
* systemd creds hookup with qemu fw_cfg. (Quite possibly might not need any
code at all, given the fw_cfg stuff are just files, but we should then
document how to use it). Goal: provide symmetric ways to pass creds to nspawn
containers and qemu VMs. (maybe also pick up env vars from fw_cfg?)
* beef up sd_notify() to support AV_VSOCK in $NOTIFY_SOCKET, so that VM
managers can get ready notifications from VMs, just like container managers
from their payload. Also pick up address from qemu/fw_cfg if set there.
@ -534,14 +529,7 @@ Features:
* expose MS_NOSYMFOLLOW in various places
* make LoadCredential= automatically find credentials in /etc/creds,
/run/creds, … and so on, if path component is unqualified
* teach LoadCredential=/LoadCredentialEncrypted= to load credentials from
kernel cmdline, maybe: LoadCredentialEncrypted=foobar:proc-cmdline:foobar
* credentials system:
- acquire from kernel command line
- acquire from EFI variable?
- acquire via via ask-password?
- acquire creds via keyring?

388
docs/CREDENTIALS.md Normal file
View file

@ -0,0 +1,388 @@
---
title: Credentials
category: Concepts
layout: default
SPDX-License-Identifier: LGPL-2.1-or-later
---
# System and Service Credentials
The `systemd` service manager supports a "credential" concept for securely
acquiring and passing credential data to systems and services. The precise
nature of the credential data is up to applications, but the concept is
intended to provide systems and services with potentially security sensitive
cryptographic keys, certificates, passwords, identity information and similar
types of information. It may also be used as generic infrastructure for
parameterizing systems and services.
Traditionally, data of this nature has often been provided to services via
environment variables (which is problematic because by default they are
inherited down the process tree, have size limitations, and issues with binary
data) or simple, unencrypted files on disk. `systemd`'s system and service
credentials are supposed to provide a better alternative for this
purpose. Specifically, the following features are provided:
1. Service credentials are acquired at the moment of service activation, and
released on service deactivation. They are immutable during the service
runtime.
2. Service credentials are accessible to service code as regular files, the
path to access them is derived from the environment variable
`$CREDENTIALS_DIRECTORY`.
3. Access to credentials is restricted to the service's user. Unlike
environment variables the credential data is not propagated down the process
tree. Instead each time a credential is accessed an access check is enforced
by the kernel. If the service is using file system namespacing the loaded
credential data is invisble to any other services.
4. Service credentials may be acquired from files on disk, specified as literal
strings in unit files, acquired from another service dynamically via an
`AF_UNIX` socket, or inherited from the system credentials the system itself
received.
5. Credentials may optionally be encrypted and authenticated, either with a key
derived from a local TPM2 chip, or one stored in `/var/`, or both. This
encryption is supposed to *just* *work*, and requires no manual setup. (That
is besides first encrypting relevant credentials with one simple command,
see below.)
6. Service credentials are placed in non-swappable memory. (If permissions
allow it, via `ramfs`.)
7. Credentials may be acquired from a hosting VM hypervisor (qemu `fw_cfg`), a
hosting container manager, the kernel command line, or from the UEFI
environment and the EFI System Partition (via `systemd-stub`). Such system
credentials may then be propagated into individual services as needed.
8. Credentials are an effective way to pass parameters into services that run
with `RootImage=` or `RootDirectory=` and thus cannot read these resources
directly from the host directory tree. Specifically, [Portable
Services](https://systemd.io/PORTABLE_SERVICES) may be parameterized this
way securely and robustly.
9. Credentials can be binary and relatively large (though currently an overall
size limit of 1M per service is enforced).
## Configuring per-Service Credentials
Within unit files, there are four settings to configure service credentials.
1. `LoadCredential=` may be used to load a credential from disk, from an
`AF_UNIX` socket, or propagate them from a system credential.
2. `SetCredential=` may be used to set a credential to a literal string encoded
in the unit file. Because unit files are world-readable (both on disk and
via D-Bus), this should only be used for credentials that aren't sensitive,
i.e. public keys/certificates but not private keys.
3. `LoadCredentialEncrypted=` is similar to `LoadCredential=` but will load an
encrypted credential, and decrypt it before passing it to the service. For
details on credential encryption, see below.
4. `SetCredentialEncrypted=` is similar to `SetCredential=` but expects an
encrypted credential to be specified literally. Unlike `SetCredential=` it
is thus safe to be used even for sensitive information, because even though
unit files are world readable, the ciphertext included in them cannot be
decoded unless access to TPM2/encryption key is available.
Each credential configured with these options carries a short name (suitable
for inclusion in a filename) in the unit file, under which the invoked service
code can then retrieve it. Each name should only be specified once.
For details about these four settings [see the man
page](https://www.freedesktop.org/software/systemd/man/systemd.exec.html#Credentials).
It is a good idea to also enable mount namespacing for services that process
credentials configured this way. If so, the runtime credential directory of the
specific service is not visible to any other service. Use `PrivateMounts=` as
minimal option to enable such namespacing. Note that many other sandboxing
settings (e.g. `ProtectSystem=`, `ReadOnlyPaths=` and similar) imply
`PrivateMounts=`, hence oftentimes it's not necessary to set this option
explicitly.
## Programming Interface from Service Code
When a service is invoked with one or more credentials set it will have an
environment variable `$CREDENTIALS_DIRECTORY` set. It contains an absolute path
to a directory the credentials are placed in. In this directory for each
configured credential one file is placed. In addition to the
`$CREDENTIALS_DIRECTORY` environment variable passed to the service processes
the `%d` specifier in unit files resolves to the service's credential
directory.
Example unit file:
```
[Service]
ExecStart=/usr/bin/myservice.sh
LoadCredential=foobar:/etc/myfoobarcredential.txt
Environment=FOOBARPATH=%d/foobar
```
Associated service shell script `/usr/bin/myservice.sh`:
```sh
#!/bin/sh
sha256sum $CREDENTIAL_PATH/foobar
sha256sum $FOOBARPATH
```
A service defined like this will get the contents of the file
`/etc/myfoobarcredential.txt` passed as credential `foobar`, which is hence
accessible under `$CREDENTIALS_DIRECTORY/foobar`. Since we additionally pass
the path to it as environment variable `$FOOBARPATH` the credential is also
accessible as the path in that environment variable. When invoked, the service
will hence show the same SHA256 hash value of `/etc/myfoobarcredential.txt`
twice.
In an ideal world, well-behaved service code would directly support credentials
passed this way, i.e. look for `$CREDENTIALS_DIRECTORY` and load the credential
data it needs from there. For daemons that do not support this but allow
passing credentials via a path supplied over the command line use
`${CREDENTIAL_PATH}` in the `ExecStart=` command line to reference the
credentials directory. For daemons that allow passing credentials via a path
supplied as environment variabe, use the `%d` specifier in the `Environment=`
setting to build valid paths to specific credentials.
## Tools
The
[`systemd-creds`](https://www.freedesktop.org/software/systemd/man/systemd-creds.html)
tool is provided to work with system and service credentials. It may be used to
access and enumerate system and service credentials, or to encrypt/decrypt credentials
(for details about the latter, see below).
When invoked from service context, `systemd-creds` passed without further
parameters will list passed credentials. The `systemd-creds cat xyz` command
may be used to write the contents of credential `xyz` to standard output. If
these calls are combined with the `--system` switch credentials passed to the
system as a whole are shown, instead of the those passed to the service the
command is invoked from.
Example use:
```sh
systemd-run -P --wait -p LoadCredential=abc:/etc/hosts systemd-creds cat abc
```
This will invoke a transient service with a credential `abc` sourced from the
system's `/etc/hosts` file. This credential is then written to standard output
via `systemd-creds cat`.
## Encryption
Credentials are supposed to be useful for carrying sensitive information, such
as cryptographic key material. For this kind of data (symmetric) encryption and
authentication is provided to make storage of the data at rest safer. The data
may be encrypted and authenticated with AES256-GCM. The encryption key can
either be one derived from the local TPM2 device, or one stored in
`/var/lib/systemd/credential.secret`, or a combination of both. If a TPM2
device is available and `/var/` resides on persistent storage the default
behaviour is to use the combination of both for encryption, thus ensuring that
credentials protected this way can only be decrypted and validated on the
local hardware and OS installation. Encrypted credentials stored on disk thus
cannot be decrypted without access to the TPM2 chip and the aforementioned key
file `/var/lib/systemd/credential.secret`. Moreover, credentials cannot be
prepared on another machine than the local one.
The `systemd-creds` tool provides the commands `encrypt` and `decrypt` to
encrypt and decrypt/authenticate credentials. Example:
```sh
systemd-creds encrypt plaintext.txt ciphertext.cred
shred -u plaintext-txt
systemd-run -P --wait -p LoadCredentialEncrypted=foobar:$(pwd)/ciphertext.cred systemd-creds cat foobar
```
This will first create an encrypted copy of the file `plaintext.txt` in the
encrypted credential file `ciphertext.cred`. It then securely removes the
source file. It then runs a transient service, that reads the encrypted file
and passes it as decrypted credential `foobar` to the invoked service binary
(which here is the `systemd-creds` tool, which just writes the data
it received to standard output).
Instead of storing the encrypted credential as a separate file on disk, it can
also be embedded in the unit file. Example:
```
systemd-creds encrypt -p --name=foobar plaintext.txt -
```
This will output a `SetCredentialEncrypted=` line that can directly be used in
a unit file. e.g.:
```
[Service]
ExecStart=/usr/bin/systemd-creds cat foobar
SetCredentialEncrypted=foobar: \
k6iUCUh0RJCQyvL8k8q1UyAAAAABAAAADAAAABAAAAC1lFmbWAqWZ8dCCQkAAAAAgAAAA \
AAAAAALACMA0AAAACAAAAAAfgAg9uNpGmj8LL2nHE0ixcycvM3XkpOCaf+9rwGscwmqRJ \
cAEO24kB08FMtd/hfkZBX8PqoHd/yPTzRxJQBoBsvo9VqolKdy9Wkvih0HQnQ6NkTKEdP \
HQ08+x8sv5sr+Mkv4ubp3YT1Jvv7CIPCbNhFtag1n5y9J7bTOKt2SQwBOAAgACwAAABIA \
ID8H3RbsT7rIBH02CIgm/Gv1ukSXO3DMHmVQkDG0wEciABAAII6LvrmL60uEZcp5qnEkx \
SuhUjsDoXrJs0rfSWX4QAx5PwfdFuxPusgEfTYIiCb8a/W6RJc7cMweZVCQMbTARyIAAA \
AAJt7Q9F/Gz0pBv1Lc4Dpn1WpebyBBm+vQ5N/lSKW2XSm8cONwCopxpDc7wJjXg7OTR6r \
xGCpIvGXLt3ibwJl81woLya2RRjIvc/R2zNm/yWzZAjiOLPih4SuHthqiX98ey8PUmZJB \
VGXglCZFjBx+d7eCqTIdghtp5pkDGwMJT6pjw4FfyFK2nJPawFKPAqzw9DK2iYttFeXi5 \
19xCfLBH9NKS/idlYXrhp+XIEtsr26s4lx5y10Goyc3qDOR3RD2cuZj0gHwV35hhhhcCz \
JaYytef1X/YL+7fYH5kuE4rxSksoUuA/LhtjszBeGbcbIT+O8SuvBJHLKTSHxPL8FTyk3 \
L4FSkEHs0rYwUIkKmnGohDdsYrMJ2fjH3yDNBP16aD1+f/Nuh75cjhUnGsDLt9K4hGg== \
```
## Inheritance from Container Managers, Hypervisors, Kernel Command Line, or the UEFI Boot Environment
Sometimes it is useful to parameterize whole systems the same way as services,
via `systemd` credentials. In particular, it might make sense to boot a
system with a set of credentials that are then propagated to individual
services where they are ultimately consumed.
`systemd` supports four ways to pass credentials to systems:
1. A container manager may set the `$CREDENTIALS_DIRECTORY` environment
variable for systemd running as PID 1 in the container, the same way as
systemd would set it for a service it
invokes. [`systemd-nspawn(1)`](https://www.freedesktop.org/software/systemd/man/systemd-nspawn.html#Credentials)'s
`--set-credential=` and `--load-credential=` switches implement this, in
order to pass arbitrary credentials from host to container payload. Also see
the [Container Interface](https://systemd.io/CONTAINER_INTERFACE)
documentation.
2. Quite similar, qemu VMs can be invoked with `-fw_cfg
name=opt/io.systemd.credentials/foo,string=bar` to pass credentials from
host through the hypervisor into the VM. (This specific switch would set
credential `foo` to `bar`.)
3. Credentials can also be passed into a system via the kernel command line,
via the `systemd.set-credential=` kernel command line option. Note though
that any data specified here is visible to any userspace application via
`/proc/cmdline`. This is hence typically not useful to pass sensitive
information.
4. Credentials may also be passed from the UEFI environment to userspace, if
the
[`systemd-stub`](https://www.freedesktop.org/software/systemd/man/systemd-stub.html)
UEFI kernel stub is used. This allows placing encrypted credentials in the
EFI System Partition, which are then picked up by `systemd-stub` and passed
to the kernel and ultimately userpace where systemd receives them. This is
useful to implement secure parameterization of vendor-built and signed
initial RAM disks, as userspace can place credentials next to these EFI
kernels, and be sure they can be accessed securely from initrd context.
Credentials passed to the system may be enumerated/displayed via `systemd-creds
--system`. They may also be propagated down to services, via the
`LoadCredential=` setting. Example:
```
systemd-nspawn --set-credential=mycred:supersecret -i test.raw -b
```
or
```
qemu-system-x86_64 \
-machine type=q35,accel=kvm,smm=on \
-smp 2 \
-m 1G \
-cpu host \
-nographic \
-nodefaults \
-serial mon:stdio \
-drive if=none,id=hd,file=test.raw,format=raw \
-device virtio-scsi-pci,id=scsi \
-device scsi-hd,drive=hd,bootindex=1 \
-fw_cfg name=opt/io.systemd.credentials/mycred,string=supersecret
```
Either of these lines will boot a disk image `test.raw`, once as container via
`systemd-nspawn`, and once as VM via `qemu`. In each case the credential
`mycred` is set to `supersecret`.
Inside of the system invoked that way the credential may then be viewed:
```sh
systemd-creds --system cat mycred
```
Or propagated to services further down:
```
systemd-run -p LoadCredential=mycred -P --wait systemd-creds cat mycred
```
## Well-Known Credentials
Various services shipped with `systemd` consume credentials for tweaking behaviour:
* [`systemd-sysusers(8)`](https://www.freedesktop.org/software/systemd/man/systemd-sysusers.html)
will look for the credentials `passwd.hashed-password.<username>`,
`passwd.plaintext-password.<username>` and `passwd.shell.<username>` to
configure the password (either in UNIX hashed form, or plaintext) or shell of
system users created. Replace `<username>` with the system user of your
choice, for example `root`.
* [`systemd-firstboot(1)`](https://www.freedesktop.org/software/systemd/man/systemd-firstboot.html)
will look for the credentials `firstboot.locale`, `firstboot.locale-messages`,
`firstboot.keymap`, `firstboot.timezone`, that configure locale, keymap or
timezone settings in case the data is not yet set in `/etc/`.
In future more services are likely to gain support for consuming credentials.
Example:
```
systemd-nspawn -i test.raw \
--set-credential=passwd.hashed-password.root:$(mkpasswd mysecret) \
--set-credential=firstboot.locale:C.UTF-8 \
-b
```
This boots the specified disk image as `systemd-nspawn` container, and passes
the root password `mysecret`and default locale `C.UTF-8` to use to it. This
data is then propagated by default to `systemd-sysusers.service` and
`systemd-firstboot.service`, where it is applied. (Note that these services
will only do so if these settings in `/etc/` are so far unset, i.e. they only
have an effect on *unprovisioned* systems, and will never override data already
established in `/etc/`.) A similar line for qemu is:
```
qemu-system-x86_64 \
-machine type=q35,accel=kvm,smm=on \
-smp 2 \
-m 1G \
-cpu host \
-nographic \
-nodefaults \
-serial mon:stdio \
-drive if=none,id=hd,file=test.raw,format=raw \
-device virtio-scsi-pci,id=scsi \
-device scsi-hd,drive=hd,bootindex=1 \
-fw_cfg name=opt/io.systemd.credentials/passwd.hashed-password.root,string=$(mkpasswd mysecret) \
-fw_cfg name=opt/io.systemd.credentials/firstboot.locale,string=C.UTF-8
```
## Relevant Paths
From *service* perspective the runtime path to find loaded credentials in is
provided in the `$CREDENTIALS_DIRECTORY` environment variable.
At runtime, credentials passed to the *system* are placed in
`/run/credentials/@system/` (for regular credentials, such as those passed from
a container manager or via qemu) and `/run/credentials/@encrypted/` (for
credentials that must be decrypted/validated before use, such as those from
`systemd-stub`).
The `LoadCredential=` and `LoadCredentialEncrypted=` settings when configured
with a relative source path will search for the source file to read the
credential from automatically. Primarily, these credentials are searched among
the credentials passed into the system. If not found there, they are searched
in `/etc/credstore/`, `/run/credstore/`,
`/usr/lib/credstore/`. `LoadCredentialEncrypted=` will also search
`/etc/credstore.encrypted/` and similar directories. These directories are
hence a great place to store credentials to load on the system.

View file

@ -73,6 +73,8 @@
<term><varname>systemd.machine_id=</varname></term>
<term><varname>systemd.unified_cgroup_hierarchy</varname></term>
<term><varname>systemd.legacy_systemd_cgroup_controller</varname></term>
<term><varname>systemd.set_credential=</varname></term>
<term><varname>systemd.import_credentials=</varname></term>
<listitem>
<para>Parameters understood by the system and service
manager to control system behavior. For details, see

View file

@ -41,6 +41,9 @@
<varname>SetCredentialEncrypted=</varname> settings, see
<citerefentry><refentrytitle>systemd.exec</refentrytitle><manvolnum>5</manvolnum></citerefentry> for
details.</para>
<para>For further information see <ulink url="https://systemd.io/CREDENTIALS">System and Service
Credentials</ulink> documentation.</para>
</refsect1>
<refsect1>

View file

@ -3039,20 +3039,31 @@ StandardInputData=SWNrIHNpdHplIGRhIHVuJyBlc3NlIEtsb3BzLAp1ZmYgZWVtYWwga2xvcHAncy
is absolute it is opened as regular file and the credential data is read from it. If the absolute
path refers to an <constant>AF_UNIX</constant> stream socket in the file system a connection is made
to it (only once at unit start-up) and the credential data read from the connection, providing an
easy IPC integration point for dynamically providing credentials from other services. If the
specified path is not absolute and itself qualifies as valid credential identifier it is understood
to refer to a credential that the service manager itself received via the
<varname>$CREDENTIALS_DIRECTORY</varname> environment variable, which may be used to propagate
credentials from an invoking environment (e.g. a container manager that invoked the service manager)
into a service. The contents of the file/socket may be arbitrary binary or textual data, including
newline characters and <constant>NUL</constant> bytes. If the file system path is omitted it is
chosen identical to the credential name, i.e. this is a terse way do declare credentials to inherit
from the service manager into a service. This option may be used multiple times, each time defining
an additional credential to pass to the unit. Alternatively, if the path is a directory, every file
in that directory will be loaded as a separate credential. The ID for each credential will be the
easy IPC integration point for dynamically transferring credentials from other services.</para>
<para>If the specified path is not absolute and itself qualifies as valid credential identifier it is
attempted to find a credential that the service manager itself received under the specified name —
which may be used to propagate credentials from an invoking environment (e.g. a container manager
that invoked the service manager) into a service. If no matching system credential is found, the
directories <filename>/etc/credstore/</filename>, <filename>/run/credstore/</filename> and
<filename>/usr/lib/credstore/</filename> are searched for files under the credential's name — which
hence are recommended locations for credential data on disk. If
<varname>LoadCredentialEncrypted=</varname> is used <filename>/run/credstore.encrypted/</filename>,
<filename>/etc/credstore.encrypted/</filename>, and
<filename>/usr/lib/credstore.encrypted/</filename> are searched as well.</para>
<para>If the file system path is omitted it is chosen identical to the credential name, i.e. this is
a terse way to declare credentials to inherit from the service manager into a service. This option
may be used multiple times, each time defining an additional credential to pass to the unit.</para>
<para>If an absolute path referring to a directory is specified, every file in that directory
(recursively) will be loaded as a separate credential. The ID for each credential will be the
provided ID suffixed with <literal>_$FILENAME</literal> (e.g., <literal>Key_file1</literal>). When
loading from a directory, symlinks will be ignored.</para>
<para>The contents of the file/socket may be arbitrary binary or textual data, including newline
characters and <constant>NUL</constant> bytes.</para>
<para>The <varname>LoadCredentialEncrypted=</varname> setting is identical to
<varname>LoadCredential=</varname>, except that the credential data is decrypted and authenticated
before being passed on to the executed processes. Specifically, the referenced path should refer to a
@ -3077,10 +3088,23 @@ StandardInputData=SWNrIHNpdHplIGRhIHVuJyBlc3NlIEtsb3BzLAp1ZmYgZWVtYWwga2xvcHAncy
<para>In order to reference the path a credential may be read from within a
<varname>ExecStart=</varname> command line use <literal>${CREDENTIALS_DIRECTORY}/mycred</literal>,
e.g. <literal>ExecStart=cat ${CREDENTIALS_DIRECTORY}/mycred</literal>.</para>
e.g. <literal>ExecStart=cat ${CREDENTIALS_DIRECTORY}/mycred</literal>. In order to reference the path
a credential may be read from within a <varname>Environment=</varname> line use
<literal>%d/mycred</literal>, e.g. <literal>Environment=MYCREDPATH=%d/mycred</literal>.</para>
<para>Currently, an accumulated credential size limit of 1 MB per unit is enforced.</para>
<para>The service manager itself may receive system credentials that can be propagated to services
from a hosting container manager or VM hypervisor. See the <ulink
url="https://systemd.io/CONTAINER_INTERFACE">Container Interface</ulink> documentation for details
about the former. For the latter, use the <command>qemu</command> <literal>fw_cfg</literal> node
<literal>opt/io.systemd.credentials/</literal>. Example qemu switch: <literal>-fw_cfg
name=opt/io.systemd.credentials/mycred,string=supersecret</literal>. They may also be specified on
the kernel command line using the <literal>systemd.set_credential=</literal> switch (see
<citerefentry><refentrytitle>systemd</refentrytitle><manvolnum>1</manvolnum></citerefentry>)
and from the UEFI firmware environment via
<citerefentry><refentrytitle>systemd-stub</refentrytitle><manvolnum>7</manvolnum></citerefentry>.</para>
<para>If referencing an <constant>AF_UNIX</constant> stream socket to connect to, the connection will
originate from an abstract namespace socket, that includes information about the unit and the
credential ID in its socket name. Use <citerefentry
@ -3094,7 +3118,10 @@ StandardInputData=SWNrIHNpdHplIGRhIHVuJyBlc3NlIEtsb3BzLAp1ZmYgZWVtYWwga2xvcHAncy
ID requested. Example: <literal>\0adf9d86b6eda275e/unit/foobar.service/credx</literal> in case the
credential <literal>credx</literal> is requested for a unit <literal>foobar.service</literal>. This
functionality is useful for using a single listening socket to serve credentials to multiple
consumers.</para></listitem>
consumers.</para>
<para>For further information see <ulink url="https://systemd.io/CREDENTIALS">System and Service
Credentials</ulink> documentation.</para></listitem>
</varlistentry>
<varlistentry>

View file

@ -944,6 +944,29 @@
</listitem>
</varlistentry>
<varlistentry>
<term><varname>systemd.set_credential=</varname></term>
<listitem><para>Sets a system credential, which can then be propagated to system services using the
<varname>LoadCredential=</varname> setting, see
<citerefentry><refentrytitle>systemd.exec</refentrytitle><manvolnum>5</manvolnum></citerefentry> for
details. Takes a pair of credential name and value, separated by a colon. Note that the kernel
command line is typically accessible by unprivileged programs in
<filename>/proc/cmdline</filename>. Thus, this mechanism is not suitable for transferring sensitive
data. Use it only for data that is not sensitive (e.g. public keys/certificates, rather than private
keys), or in testing/debugging environments.</para>
<para>For further information see <ulink url="https://systemd.io/CREDENTIALS">System and Service
Credentials</ulink> documentation.</para></listitem>
</varlistentry>
<varlistentry>
<term><varname>systemd.import_credentials=</varname></term>
<listitem><para>Takes a boolean argument. If false disables importing credentials from the kernel
command line, qemu_fw_cfg subsystem or the kernel command line.</para></listitem>
</varlistentry>
<varlistentry>
<term><varname>quiet</varname></term>

View file

@ -2595,6 +2595,42 @@ static int write_credential(
return 0;
}
static char **credential_search_path(
const ExecParameters *params,
bool encrypted) {
_cleanup_strv_free_ char **l = NULL;
assert(params);
/* Assemble a search path to find credentials in. We'll look in /etc/credstore/ (and similar
* directories in /usr/lib/ + /run/) for all types of credentials. If we are looking for encrypted
* credentials, also look in /etc/credstore.encrypted/ (and similar dirs). */
if (encrypted) {
if (strv_extend(&l, params->received_encrypted_credentials_directory) < 0)
return NULL;
if (strv_extend_strv(&l, CONF_PATHS_STRV("credstore.encrypted"), /* filter_duplicates= */ true) < 0)
return NULL;
}
if (params->received_credentials_directory)
if (strv_extend(&l, params->received_credentials_directory) < 0)
return NULL;
if (strv_extend_strv(&l, CONF_PATHS_STRV("credstore"), /* filter_duplicates= */ true) < 0)
return NULL;
if (DEBUG_LOGGING) {
_cleanup_free_ char *t = strv_join(l, ":");
log_debug("Credential search path is: %s", t);
}
return TAKE_PTR(l);
}
static int load_credential(
const ExecContext *context,
const ExecParameters *params,
@ -2609,11 +2645,12 @@ static int load_credential(
uint64_t *left) {
ReadFullFileFlags flags = READ_FULL_FILE_SECURE|READ_FULL_FILE_FAIL_WHEN_LARGER;
_cleanup_strv_free_ char **search_path = NULL;
_cleanup_(erase_and_freep) char *data = NULL;
_cleanup_free_ char *j = NULL, *bindname = NULL;
_cleanup_free_ char *bindname = NULL;
const char *source = NULL;
bool missing_ok = true;
const char *source;
size_t size, add;
size_t size, add, maxsz;
int r;
assert(context);
@ -2624,10 +2661,25 @@ static int load_credential(
assert(write_dfd >= 0);
assert(left);
if (path_is_absolute(path) || read_dfd >= 0) {
/* If this is an absolute path (or a directory fd is specifier relative which to read), read
* the data directly from it, and support AF_UNIX sockets */
if (read_dfd >= 0) {
/* If a directory fd is specified, then read the file directly from that dir. In this case we
* won't do AF_UNIX stuff (we simply don't want to recursively iterate down a tree of AF_UNIX
* IPC sockets). It's OK if a file vanishes here in the time we enumerate it and intend to
* open it. */
if (!filename_is_valid(path)) /* safety check */
return -EINVAL;
missing_ok = true;
source = path;
} else if (path_is_absolute(path)) {
/* If this is an absolute path, read the data directly from it, and support AF_UNIX
* sockets */
if (!path_is_valid(path)) /* safety check */
return -EINVAL;
flags |= READ_FULL_FILE_CONNECT_SOCKET;
/* Pass some minimal info about the unit and the credential name we are looking to acquire
@ -2636,25 +2688,50 @@ static int load_credential(
return -ENOMEM;
missing_ok = false;
source = path;
} else if (params->received_credentials) {
/* If this is a relative path, take it relative to the credentials we received
* ourselves. We don't support the AF_UNIX stuff in this mode, since we are operating
* on a credential store, i.e. this is guaranteed to be regular files. */
j = path_join(params->received_credentials, path);
if (!j)
} else if (credential_name_valid(path)) {
/* If this is a relative path, take it as credential name relative to the credentials
* directory we received ourselves. We don't support the AF_UNIX stuff in this mode, since we
* are operating on a credential store, i.e. this is guaranteed to be regular files. */
search_path = credential_search_path(params, encrypted);
if (!search_path)
return -ENOMEM;
source = j;
missing_ok = true;
} else
source = NULL;
if (source)
if (encrypted)
flags |= READ_FULL_FILE_UNBASE64;
maxsz = encrypted ? CREDENTIAL_ENCRYPTED_SIZE_MAX : CREDENTIAL_SIZE_MAX;
if (search_path) {
STRV_FOREACH(d, search_path) {
_cleanup_free_ char *j = NULL;
j = path_join(*d, path);
if (!j)
return -ENOMEM;
r = read_full_file_full(
AT_FDCWD, j, /* path is absolute, hence pass AT_FDCWD as nop dir fd here */
UINT64_MAX,
maxsz,
flags,
NULL,
&data, &size);
if (r != -ENOENT)
break;
}
} else if (source)
r = read_full_file_full(
read_dfd, source,
UINT64_MAX,
encrypted ? CREDENTIAL_ENCRYPTED_SIZE_MAX : CREDENTIAL_SIZE_MAX,
flags | (encrypted ? READ_FULL_FILE_UNBASE64 : 0),
maxsz,
flags,
bindname,
&data, &size);
else

View file

@ -406,7 +406,8 @@ struct ExecParameters {
const char *cgroup_path;
char **prefix;
const char *received_credentials;
const char *received_credentials_directory;
const char *received_encrypted_credentials_directory;
const char *confirm_spawn;

551
src/core/import-creds.c Normal file
View file

@ -0,0 +1,551 @@
/* SPDX-License-Identifier: LGPL-2.1-or-later */
#include <sys/mount.h>
#include "copy.h"
#include "creds-util.h"
#include "fileio.h"
#include "format-util.h"
#include "fs-util.h"
#include "import-creds.h"
#include "io-util.h"
#include "mkdir-label.h"
#include "mount-util.h"
#include "mountpoint-util.h"
#include "parse-util.h"
#include "path-util.h"
#include "proc-cmdline.h"
#include "recurse-dir.h"
#include "strv.h"
/* This imports credentials passed in from environments higher up (VM manager, boot loader, …) and rearranges
* them so that later code can access them using our regular credential protocol
* (i.e. $CREDENTIALS_DIRECTORY). It's supposed to be minimal glue to unify behaviour how PID 1 (and
* generators invoked by it) can acquire credentials from outside, to mimic how we support it for containers,
* but on VM/physical environments.
*
* This does three things:
*
* 1. It imports credentials picked up by sd-boot (and placed in the /.extra/credentials/ dir in the initrd)
* and puts them in /run/credentials/@encrypted/. Note that during the initrdhost transition the initrd root
* file system is cleaned out, thus it is essential we pick up these files before they are deleted. Note
* that these credentials originate from an untrusted source, i.e. the ESP and are not
* pre-authenticated. They still have to be authenticated before use.
*
* 2. It imports credentials from /proc/cmdline and puts them in /run/credentials/@system/. These come from a
* trusted environment (i.e. the boot loader), and are typically authenticated (if authentication is done
* at all). However, they are world-readable, which might be less than ideal. Hence only use this for data
* that doesn't require trust.
*
* 3. It imports credentials passed in through qemu's fw_cfg logic. Specifically, credential data passed in
* /sys/firmware/qemu_fw_cfg/by_name/opt/io.systemd.credentials/ is picked up and also placed in
* /run/credentials/@system/.
*
* If it picked up any credentials it will set the $CREDENTIALS_DIRECTORY and
* $ENCRYPTED_CREDENTIALS_DIRECTORY environment variables to point to these directories, so that processes
* can find them there later on. If "ramfs" is available $CREDENTIALS_DIRECTORY will be backed by it (but
* $ENCRYPTED_CREDENTIALS_DIRECTORY is just a regular tmpfs).
*
* Net result: the service manager can pick up trusted credentials from $CREDENTIALS_DIRECTORY afterwards,
* and untrusted ones from $ENCRYPTED_CREDENTIALS_DIRECTORY. */
typedef struct ImportCredentialContext {
int target_dir_fd;
size_t size_sum;
unsigned n_credentials;
} ImportCredentialContext;
static void import_credentials_context_free(ImportCredentialContext *c) {
assert(c);
c->target_dir_fd = safe_close(c->target_dir_fd);
}
static int acquire_encrypted_credential_directory(ImportCredentialContext *c) {
int r;
assert(c);
if (c->target_dir_fd >= 0)
return c->target_dir_fd;
r = mkdir_safe_label(ENCRYPTED_SYSTEM_CREDENTIALS_DIRECTORY, 0700, 0, 0, MKDIR_WARN_MODE);
if (r < 0)
return log_error_errno(r, "Failed to create " ENCRYPTED_SYSTEM_CREDENTIALS_DIRECTORY ": %m");
c->target_dir_fd = open(ENCRYPTED_SYSTEM_CREDENTIALS_DIRECTORY, O_RDONLY|O_DIRECTORY|O_CLOEXEC);
if (c->target_dir_fd < 0)
return log_error_errno(errno, "Failed to open " ENCRYPTED_SYSTEM_CREDENTIALS_DIRECTORY ": %m");
return c->target_dir_fd;
}
static int open_credential_file_for_write(int target_dir_fd, const char *dir_name, const char *n) {
int fd;
assert(target_dir_fd >= 0);
assert(dir_name);
assert(n);
fd = openat(target_dir_fd, n, O_WRONLY|O_CLOEXEC|O_CREAT|O_EXCL|O_NOFOLLOW, 0400);
if (fd < 0) {
if (errno == EEXIST) /* In case of EEXIST we'll only debug log! */
return log_debug_errno(errno, "Credential '%s' set twice, ignoring.", n);
return log_error_errno(errno, "Failed to create %s/%s: %m", dir_name, n);
}
return fd;
}
static bool credential_size_ok(ImportCredentialContext *c, const char *name, uint64_t size) {
assert(c);
assert(name);
if (size > CREDENTIAL_SIZE_MAX) {
log_warning("Credential '%s' is larger than allowed limit (%s > %s), skipping.", name, FORMAT_BYTES(size), FORMAT_BYTES(CREDENTIAL_SIZE_MAX));
return false;
}
if (size > CREDENTIALS_TOTAL_SIZE_MAX - c->size_sum) {
log_warning("Accumulated credential size would be above allowed limit (%s+%s > %s), skipping '%s'.",
FORMAT_BYTES(c->size_sum), FORMAT_BYTES(size), FORMAT_BYTES(CREDENTIALS_TOTAL_SIZE_MAX), name);
return false;
}
return true;
}
static int finalize_credentials_dir(const char *dir, const char *envvar) {
int r;
assert(dir);
assert(envvar);
/* Try to make the credentials directory read-only now */
r = make_mount_point(dir);
if (r < 0)
log_warning_errno(r, "Failed to make '%s' a mount point, ignoring: %m", dir);
else
(void) mount_nofollow_verbose(LOG_WARNING, NULL, dir, NULL, MS_BIND|MS_NODEV|MS_NOEXEC|MS_NOSUID|MS_RDONLY|MS_REMOUNT, NULL);
if (setenv(envvar, dir, /* overwrite= */ true) < 0)
return log_error_errno(errno, "Failed to set $%s environment variable: %m", envvar);
return 0;
}
static int import_credentials_boot(void) {
_cleanup_(import_credentials_context_free) ImportCredentialContext context = {
.target_dir_fd = -1,
};
int r;
/* systemd-stub will wrap sidecar *.cred files from the UEFI kernel image directory into initrd
* cpios, so that they unpack into /.extra/. We'll pick them up from there and copy them into /run/
* so that we can access them during the entire runtime (note that the initrd file system is erased
* during the initrd host transition). Note that these credentials originate from an untrusted
* source (i.e. the ESP typically) and thus need to be authenticated later. We thus put them in a
* directory separate from the usual credentials which are from a trusted source. */
if (!in_initrd())
return 0;
FOREACH_STRING(p,
"/.extra/credentials/", /* specific to this boot menu */
"/.extra/global_credentials/") { /* boot partition wide */
_cleanup_free_ DirectoryEntries *de = NULL;
_cleanup_close_ int source_dir_fd = -1;
source_dir_fd = open(p, O_RDONLY|O_DIRECTORY|O_CLOEXEC|O_NOFOLLOW);
if (source_dir_fd < 0) {
if (errno == ENOENT) {
log_debug("No credentials passed via %s.", p);
continue;
}
log_warning_errno(errno, "Failed to open '%s', ignoring: %m", p);
continue;
}
r = readdir_all(source_dir_fd, RECURSE_DIR_SORT|RECURSE_DIR_IGNORE_DOT, &de);
if (r < 0) {
log_warning_errno(r, "Failed to read '%s' contents, ignoring: %m", p);
continue;
}
for (size_t i = 0; i < de->n_entries; i++) {
const struct dirent *d = de->entries[i];
_cleanup_close_ int cfd = -1, nfd = -1;
_cleanup_free_ char *n = NULL;
const char *e;
struct stat st;
e = endswith(d->d_name, ".cred");
if (!e)
continue;
/* drop .cred suffix (which we want in the ESP sidecar dir, but not for our internal
* processing) */
n = strndup(d->d_name, e - d->d_name);
if (!n)
return log_oom();
if (!credential_name_valid(n)) {
log_warning("Credential '%s' has invalid name, ignoring.", d->d_name);
continue;
}
cfd = openat(source_dir_fd, d->d_name, O_RDONLY|O_CLOEXEC);
if (cfd < 0) {
log_warning_errno(errno, "Failed to open %s, ignoring: %m", d->d_name);
continue;
}
if (fstat(cfd, &st) < 0) {
log_warning_errno(errno, "Failed to stat %s, ignoring: %m", d->d_name);
continue;
}
r = stat_verify_regular(&st);
if (r < 0) {
log_warning_errno(r, "Credential file %s is not a regular file, ignoring: %m", d->d_name);
continue;
}
if (!credential_size_ok(&context, n, st.st_size))
continue;
r = acquire_encrypted_credential_directory(&context);
if (r < 0)
return r;
nfd = open_credential_file_for_write(context.target_dir_fd, ENCRYPTED_SYSTEM_CREDENTIALS_DIRECTORY, n);
if (nfd == -EEXIST)
continue;
if (nfd < 0)
return r;
r = copy_bytes(cfd, nfd, st.st_size, 0);
if (r < 0) {
(void) unlinkat(context.target_dir_fd, n, 0);
return log_error_errno(r, "Failed to create credential '%s': %m", n);
}
context.size_sum += st.st_size;
context.n_credentials++;
log_debug("Successfully copied boot credential '%s'.", n);
}
}
if (context.n_credentials > 0) {
log_debug("Imported %u credentials from boot loader.", context.n_credentials);
r = finalize_credentials_dir(ENCRYPTED_SYSTEM_CREDENTIALS_DIRECTORY, "ENCRYPTED_CREDENTIALS_DIRECTORY");
if (r < 0)
return r;
}
return 0;
}
static int acquire_credential_directory(ImportCredentialContext *c) {
int r;
assert(c);
if (c->target_dir_fd >= 0)
return c->target_dir_fd;
r = path_is_mount_point(SYSTEM_CREDENTIALS_DIRECTORY, NULL, 0);
if (r < 0) {
if (r != -ENOENT)
return log_error_errno(r, "Failed to determine if " SYSTEM_CREDENTIALS_DIRECTORY " is a mount point: %m");
r = mkdir_safe_label(SYSTEM_CREDENTIALS_DIRECTORY, 0700, 0, 0, MKDIR_WARN_MODE);
if (r < 0)
return log_error_errno(r, "Failed to create " SYSTEM_CREDENTIALS_DIRECTORY " mount point: %m");
r = 0; /* Now it exists and is not a mount point */
}
if (r == 0)
/* If not a mountpoint yet, try to mount a ramfs there (so that this stuff isn't swapped
* out), but if that doesn't work, let's just use the regular tmpfs it already is. */
(void) mount_nofollow_verbose(LOG_WARNING, "ramfs", SYSTEM_CREDENTIALS_DIRECTORY, "ramfs", MS_NODEV|MS_NOEXEC|MS_NOSUID, "mode=0700");
c->target_dir_fd = open(SYSTEM_CREDENTIALS_DIRECTORY, O_RDONLY|O_DIRECTORY|O_CLOEXEC);
if (c->target_dir_fd < 0)
return log_error_errno(errno, "Failed to open " SYSTEM_CREDENTIALS_DIRECTORY ": %m");
return c->target_dir_fd;
}
static int proc_cmdline_callback(const char *key, const char *value, void *data) {
ImportCredentialContext *c = ASSERT_PTR(data);
_cleanup_free_ char *n = NULL;
_cleanup_close_ int nfd = -1;
const char *colon;
size_t l;
int r;
assert(key);
if (!proc_cmdline_key_streq(key, "systemd.set_credential"))
return 0;
colon = value ? strchr(value, ':') : NULL;
if (!colon) {
log_warning("Credential assignment through kernel command line lacks ':' character, ignoring: %s", value);
return 0;
}
n = strndup(value, colon - value);
if (!n)
return log_oom();
if (!credential_name_valid(n)) {
log_warning("Credential name '%s' is invalid, ignoring.", n);
return 0;
}
colon++;
l = strlen(colon);
if (!credential_size_ok(c, n, l))
return 0;
r = acquire_credential_directory(c);
if (r < 0)
return r;
nfd = open_credential_file_for_write(c->target_dir_fd, SYSTEM_CREDENTIALS_DIRECTORY, n);
if (nfd == -EEXIST)
return 0;
if (nfd < 0)
return r;
r = loop_write(nfd, colon, l, /* do_poll= */ false);
if (r < 0) {
(void) unlinkat(c->target_dir_fd, n, 0);
return log_error_errno(r, "Failed to write credential: %m");
}
c->size_sum += l;
c->n_credentials++;
log_debug("Successfully processed kernel command line credential '%s'.", n);
return 0;
}
static int import_credentials_proc_cmdline(ImportCredentialContext *c) {
int r;
assert(c);
r = proc_cmdline_parse(proc_cmdline_callback, c, 0);
if (r < 0)
return log_error_errno(r, "Failed to parse /proc/cmdline: %m");
return 0;
}
#define QEMU_FWCFG_PATH "/sys/firmware/qemu_fw_cfg/by_name/opt/io.systemd.credentials"
static int import_credentials_qemu(ImportCredentialContext *c) {
_cleanup_free_ DirectoryEntries *de = NULL;
_cleanup_close_ int source_dir_fd = -1;
int r;
assert(c);
source_dir_fd = open(QEMU_FWCFG_PATH, O_RDONLY|O_DIRECTORY|O_CLOEXEC);
if (source_dir_fd < 0) {
if (errno == ENOENT) {
log_debug("No credentials passed via fw_cfg.");
return 0;
}
log_warning_errno(errno, "Failed to open '" QEMU_FWCFG_PATH "', ignoring: %m");
return 0;
}
r = readdir_all(source_dir_fd, RECURSE_DIR_SORT|RECURSE_DIR_IGNORE_DOT, &de);
if (r < 0) {
log_warning_errno(r, "Failed to read '" QEMU_FWCFG_PATH "' contents, ignoring: %m");
return 0;
}
for (size_t i = 0; i < de->n_entries; i++) {
const struct dirent *d = de->entries[i];
_cleanup_close_ int vfd = -1, rfd = -1, nfd = -1;
_cleanup_free_ char *szs = NULL;
uint64_t sz;
if (!credential_name_valid(d->d_name)) {
log_warning("Credential '%s' has invalid name, ignoring.", d->d_name);
continue;
}
vfd = openat(source_dir_fd, d->d_name, O_RDONLY|O_DIRECTORY|O_CLOEXEC);
if (vfd < 0) {
log_warning_errno(errno, "Failed to open '" QEMU_FWCFG_PATH "'/%s/, ignoring: %m", d->d_name);
continue;
}
r = read_virtual_file_at(vfd, "size", LINE_MAX, &szs, NULL);
if (r < 0) {
log_warning_errno(r, "Failed to read '" QEMU_FWCFG_PATH "'/%s/size, ignoring: %m", d->d_name);
continue;
}
r = safe_atou64(strstrip(szs), &sz);
if (r < 0) {
log_warning_errno(r, "Failed to parse size of credential '%s', ignoring: %s", d->d_name, szs);
continue;
}
if (!credential_size_ok(c, d->d_name, sz))
continue;
/* Ideally we'd just symlink the data here. Alas the kernel driver exports the raw file as
* having size zero, and we'd rather not have applications support such credential
* files. Let's hence copy the files to make them regular. */
rfd = openat(vfd, "raw", O_RDONLY|O_CLOEXEC);
if (rfd < 0) {
log_warning_errno(r, "Failed to open '" QEMU_FWCFG_PATH "'/%s/raw, ignoring: %m", d->d_name);
continue;
}
r = acquire_credential_directory(c);
if (r < 0)
return r;
nfd = open_credential_file_for_write(c->target_dir_fd, SYSTEM_CREDENTIALS_DIRECTORY, d->d_name);
if (nfd == -EEXIST)
continue;
if (nfd < 0)
return r;
r = copy_bytes(rfd, nfd, sz, 0);
if (r < 0) {
(void) unlinkat(c->target_dir_fd, d->d_name, 0);
return log_error_errno(r, "Failed to create credential '%s': %m", d->d_name);
}
c->size_sum += sz;
c->n_credentials++;
log_debug("Successfully copied qemu fw_cfg credential '%s'.", d->d_name);
}
return 0;
}
static int import_credentials_trusted(void) {
_cleanup_(import_credentials_context_free) ImportCredentialContext c = {
.target_dir_fd = -1,
};
int q, r;
r = import_credentials_qemu(&c);
q = import_credentials_proc_cmdline(&c);
if (c.n_credentials > 0) {
int z;
log_debug("Imported %u credentials from kernel command line/fw_cfg.", c.n_credentials);
z = finalize_credentials_dir(SYSTEM_CREDENTIALS_DIRECTORY, "CREDENTIALS_DIRECTORY");
if (z < 0)
return z;
}
return r < 0 ? r : q;
}
static int symlink_credential_dir(const char *envvar, const char *path, const char *where) {
int r;
assert(envvar);
assert(path);
assert(where);
if (!path_is_valid(path) || !path_is_absolute(path))
return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "String specified via $%s is not a valid absolute path, refusing: %s", envvar, path);
/* If the env var already points to where we intend to create the symlink, then most likely we
* already imported some creds earlier, and thus set the env var, and hence don't need to do
* anything. */
if (path_equal(path, where))
return 0;
r = symlink_idempotent(path, where, /* make_relative= */ true);
if (r < 0)
return log_error_errno(r, "Failed to link $%s to %s: %m", envvar, where);
return 0;
}
int import_credentials(void) {
const char *received_creds_dir = NULL, *received_encrypted_creds_dir = NULL;
bool envvar_set = false;
int r, q;
r = get_credentials_dir(&received_creds_dir);
if (r < 0 && r != -ENXIO) /* ENXIO → env var not set yet */
log_warning_errno(r, "Failed to determine credentials directory, ignoring: %m");
envvar_set = r >= 0;
r = get_encrypted_credentials_dir(&received_encrypted_creds_dir);
if (r < 0 && r != -ENXIO) /* ENXIO → env var not set yet */
log_warning_errno(r, "Failed to determine encrypted credentials directory, ignoring: %m");
envvar_set = envvar_set || r >= 0;
if (envvar_set) {
/* Maybe an earlier stage initrd already set this up? If so, don't try to import anything again. */
log_debug("Not importing credentials, $CREDENTIALS_DIRECTORY or $ENCRYPTED_CREDENTIALS_DIRECTORY already set.");
/* But, let's make sure the creds are available from our regular paths. */
if (received_creds_dir)
r = symlink_credential_dir("CREDENTIALS_DIRECTORY", received_creds_dir, SYSTEM_CREDENTIALS_DIRECTORY);
else
r = 0;
if (received_encrypted_creds_dir) {
q = symlink_credential_dir("ENCRYPTED_CREDENTIALS_DIRECTORY", received_encrypted_creds_dir, ENCRYPTED_SYSTEM_CREDENTIALS_DIRECTORY);
if (r >= 0)
r = q;
}
} else {
_cleanup_free_ char *v = NULL;
r = proc_cmdline_get_key("systemd.import_credentials", PROC_CMDLINE_STRIP_RD_PREFIX, &v);
if (r < 0)
log_debug_errno(r, "Failed to check if 'systemd.import_credentials=' kernel command line option is set, ignoring: %m");
else if (r > 0) {
r = parse_boolean(v);
if (r < 0)
log_debug_errno(r, "Failed to parse 'systemd.import_credentials=' parameter, ignoring: %m");
else if (r == 0) {
log_notice("systemd.import_credentials=no is set, skipping importing of credentials.");
return 0;
}
}
r = import_credentials_boot();
q = import_credentials_trusted();
if (r >= 0)
r = q;
}
return r;
}

4
src/core/import-creds.h Normal file
View file

@ -0,0 +1,4 @@
/* SPDX-License-Identifier: LGPL-2.1-or-later */
#pragma once
int import_credentials(void);

View file

@ -10,6 +10,7 @@
#include "macro.h"
#include "recurse-dir.h"
#include "string-util.h"
#include "virt.h"
#if HAVE_KMOD
#include "module-util.h"
@ -80,6 +81,10 @@ static bool has_virtio_rng(void) {
return r > 0;
}
static bool in_qemu(void) {
return IN_SET(detect_vm(), VIRTUALIZATION_KVM, VIRTUALIZATION_QEMU);
}
#endif
int kmod_setup(void) {
@ -109,6 +114,9 @@ int kmod_setup(void) {
#endif
/* virtio_rng would be loaded by udev later, but real entropy might be needed very early */
{ "virtio_rng", NULL, false, false, has_virtio_rng },
/* qemu_fw_cfg would be loaded by udev later, but we want to import credentials from it super early */
{ "qemu_fw_cfg", "/sys/firmware/qemu_fw_cfg", false, false, in_qemu },
};
_cleanup_(kmod_unrefp) struct kmod_ctx *ctx = NULL;
unsigned i;

View file

@ -51,6 +51,7 @@
#include "hexdecoct.h"
#include "hostname-setup.h"
#include "ima-setup.h"
#include "import-creds.h"
#include "killall.h"
#include "kmod-setup.h"
#include "limits-util.h"
@ -2180,6 +2181,10 @@ static int initialize_runtime(
(void) bump_rlimit_nofile(saved_rlimit_nofile);
(void) bump_rlimit_memlock(saved_rlimit_memlock);
/* Pull credentials from various sources into a common credential directory */
if (arg_system && !skip_setup)
(void) import_credentials();
return 0;
}

View file

@ -777,9 +777,37 @@ static int manager_setup_sigchld_event_source(Manager *m) {
return 0;
}
static int manager_find_credentials_dirs(Manager *m) {
const char *e;
int r;
assert(m);
r = get_credentials_dir(&e);
if (r < 0) {
if (r != -ENXIO)
log_debug_errno(r, "Failed to determine credentials directory, ignoring: %m");
} else {
m->received_credentials_directory = strdup(e);
if (!m->received_credentials_directory)
return -ENOMEM;
}
r = get_encrypted_credentials_dir(&e);
if (r < 0) {
if (r != -ENXIO)
log_debug_errno(r, "Failed to determine encrypted credentials directory, ignoring: %m");
} else {
m->received_encrypted_credentials_directory = strdup(e);
if (!m->received_encrypted_credentials_directory)
return -ENOMEM;
}
return 0;
}
int manager_new(LookupScope scope, ManagerTestRunFlags test_run_flags, Manager **_m) {
_cleanup_(manager_freep) Manager *m = NULL;
const char *e;
int r;
assert(_m);
@ -883,12 +911,9 @@ int manager_new(LookupScope scope, ManagerTestRunFlags test_run_flags, Manager *
if (r < 0)
return r;
r = get_credentials_dir(&e);
if (r >= 0) {
m->received_credentials = strdup(e);
if (!m->received_credentials)
return -ENOMEM;
}
r = manager_find_credentials_dirs(m);
if (r < 0)
return r;
r = sd_event_default(&m->event);
if (r < 0)
@ -1533,7 +1558,8 @@ Manager* manager_free(Manager *m) {
for (ExecDirectoryType dt = 0; dt < _EXEC_DIRECTORY_TYPE_MAX; dt++)
m->prefix[dt] = mfree(m->prefix[dt]);
free(m->received_credentials);
free(m->received_credentials_directory);
free(m->received_encrypted_credentials_directory);
free(m->watchdog_pretimeout_governor);
free(m->watchdog_pretimeout_governor_overridden);

View file

@ -438,7 +438,8 @@ struct Manager {
/* Prefixes of e.g. RuntimeDirectory= */
char *prefix[_EXEC_DIRECTORY_TYPE_MAX];
char *received_credentials;
char *received_credentials_directory;
char *received_encrypted_credentials_directory;
/* Used in the SIGCHLD and sd_notify() message invocation logic to avoid that we dispatch the same event
* multiple times on the same unit. */

View file

@ -73,6 +73,8 @@ libcore_sources = '''
generator-setup.h
ima-setup.c
ima-setup.h
import-creds.c
import-creds.h
job.c
job.h
kill.c

View file

@ -5032,7 +5032,8 @@ int unit_set_exec_params(Unit *u, ExecParameters *p) {
p->cgroup_path = u->cgroup_path;
SET_FLAG(p->flags, EXEC_CGROUP_DELEGATE, unit_cgroup_delegate(u));
p->received_credentials = u->manager->received_credentials;
p->received_credentials_directory = u->manager->received_credentials_directory;
p->received_encrypted_credentials_directory = u->manager->received_encrypted_credentials_directory;
return 0;
}

View file

@ -60,63 +60,76 @@ static const char* transcode_mode_table[_TRANSCODE_MAX] = {
DEFINE_PRIVATE_STRING_TABLE_LOOKUP_FROM_STRING(transcode_mode, TranscodeMode);
static int open_credential_directory(DIR **ret) {
_cleanup_free_ char *j = NULL;
static int open_credential_directory(
DIR **ret_dir,
const char **ret_prefix,
bool encrypted) {
const char *p;
DIR *d;
int r;
if (arg_system) {
_cleanup_free_ char *cd = NULL;
assert(ret_dir);
r = getenv_for_pid(1, "CREDENTIALS_DIRECTORY", &cd);
if (arg_system)
/* PID 1 ensures that system credentials are always accessible under the same fixed path. It
* will create symlinks if necessary to guarantee that. */
p = encrypted ?
ENCRYPTED_SYSTEM_CREDENTIALS_DIRECTORY :
SYSTEM_CREDENTIALS_DIRECTORY;
else {
/* Otherwise take the dirs from the env vars we got passed */
r = (encrypted ? get_encrypted_credentials_dir : get_credentials_dir)(&p);
if (r == -ENXIO) /* No environment variable? */
goto not_found;
if (r < 0)
return r;
if (!cd)
return -ENXIO;
if (!path_is_absolute(cd) || !path_is_normalized(cd))
return -EINVAL;
j = path_join("/proc/1/root", cd);
if (!j)
return -ENOMEM;
p = j;
} else {
r = get_credentials_dir(&p);
if (r < 0)
return r;
return log_error_errno(r, "Failed to get credentials directory: %m");
}
d = opendir(p);
if (!d)
return -errno;
if (!d) {
/* No such dir? Then no creds where passed. (We conditionalize this on arg_system, since for
* the per-service case a non-existing path would indicate an issue since the env var would
* be set incorrectly in that case.) */
if (arg_system && errno == ENOENT)
goto not_found;
return log_error_errno(errno, "Failed to open credentials directory '%s': %m", p);
}
*ret_dir = d;
if (ret_prefix)
*ret_prefix = p;
return 1;
not_found:
*ret_dir = NULL;
if (ret_prefix)
*ret_prefix = NULL;
*ret = d;
return 0;
}
static int verb_list(int argc, char **argv, void *userdata) {
_cleanup_(table_unrefp) Table *t = NULL;
static int add_credentials_to_table(Table *t, bool encrypted) {
_cleanup_(closedirp) DIR *d = NULL;
const char *prefix;
int r;
r = open_credential_directory(&d);
if (r == -ENXIO)
return log_error_errno(r, "No credentials received. (i.e. $CREDENTIALS_DIRECTORY not set or pointing to empty directory.)");
assert(t);
r = open_credential_directory(&d, &prefix, encrypted);
if (r < 0)
return log_error_errno(r, "Failed to open credentials directory: %m");
t = table_new("name", "secure", "size");
if (!t)
return log_oom();
(void) table_set_align_percent(t, table_get_cell(t, 0, 2), 100);
return r;
if (!d)
return 0; /* No creds dir set */
for (;;) {
_cleanup_close_ int fd = -1;
_cleanup_free_ char *j = NULL;
const char *secure, *secure_color = NULL;
_cleanup_close_ int fd = -1;
struct dirent *de;
struct stat st;
@ -149,7 +162,10 @@ static int verb_list(int argc, char **argv, void *userdata) {
if (!S_ISREG(st.st_mode))
continue;
if ((st.st_mode & 0377) != 0) {
if (encrypted) {
secure = "encrypted";
secure_color = ansi_highlight_green();
} else if ((st.st_mode & 0377) != 0) {
secure = "insecure"; /* Anything that is accessible more than read-only to its owner is insecure */
secure_color = ansi_highlight_red();
} else {
@ -161,16 +177,49 @@ static int verb_list(int argc, char **argv, void *userdata) {
secure_color = r ? ansi_highlight_green() : ansi_highlight_yellow4();
}
j = path_join(prefix, de->d_name);
if (!j)
return log_oom();
r = table_add_many(
t,
TABLE_STRING, de->d_name,
TABLE_STRING, secure,
TABLE_SET_COLOR, secure_color,
TABLE_SIZE, (uint64_t) st.st_size);
TABLE_SIZE, (uint64_t) st.st_size,
TABLE_STRING, j);
if (r < 0)
return table_log_add_error(r);
}
return 1; /* Creds dir set */
}
static int verb_list(int argc, char **argv, void *userdata) {
_cleanup_(table_unrefp) Table *t = NULL;
int r, q;
t = table_new("name", "secure", "size", "path");
if (!t)
return log_oom();
(void) table_set_align_percent(t, table_get_cell(t, 0, 2), 100);
r = add_credentials_to_table(t, /* encrypted= */ true);
if (r < 0)
return r;
q = add_credentials_to_table(t, /* encrypted= */ false);
if (q < 0)
return q;
if (r == 0 && q == 0) {
if (arg_system)
return log_error_errno(SYNTHETIC_ERRNO(ENXIO), "No credentials passed to system.");
return log_error_errno(SYNTHETIC_ERRNO(ENXIO), "No credentials passed. (i.e. $CREDENTIALS_DIRECTORY not set.)");
}
if ((arg_json_format_flags & JSON_FORMAT_OFF) && table_get_rows(t) <= 1) {
log_info("No credentials");
return 0;
@ -310,18 +359,15 @@ static int write_blob(FILE *f, const void *data, size_t size) {
}
static int verb_cat(int argc, char **argv, void *userdata) {
_cleanup_(closedirp) DIR *d = NULL;
usec_t timestamp;
int r, ret = 0;
r = open_credential_directory(&d);
if (r == -ENXIO)
return log_error_errno(r, "No credentials passed.");
if (r < 0)
return log_error_errno(r, "Failed to open credentials directory: %m");
timestamp = arg_timestamp != USEC_INFINITY ? arg_timestamp : now(CLOCK_REALTIME);
STRV_FOREACH(cn, strv_skip(argv, 1)) {
_cleanup_(erase_and_freep) void *data = NULL;
size_t size = 0;
int encrypted;
if (!credential_name_valid(*cn)) {
log_error("Credential name '%s' is not valid.", *cn);
@ -330,19 +376,58 @@ static int verb_cat(int argc, char **argv, void *userdata) {
continue;
}
r = read_full_file_full(
dirfd(d), *cn,
UINT64_MAX, SIZE_MAX,
READ_FULL_FILE_SECURE|READ_FULL_FILE_WARN_WORLD_READABLE,
NULL,
(char**) &data, &size);
if (r < 0) {
/* Look both in regular and in encrypted credentials */
for (encrypted = 0; encrypted < 2; encrypted ++) {
_cleanup_(closedirp) DIR *d = NULL;
r = open_credential_directory(&d, NULL, encrypted);
if (r < 0)
return log_error_errno(r, "Failed to open credentials directory: %m");
if (!d) /* Not set */
continue;
r = read_full_file_full(
dirfd(d), *cn,
UINT64_MAX, SIZE_MAX,
READ_FULL_FILE_SECURE|READ_FULL_FILE_WARN_WORLD_READABLE,
NULL,
(char**) &data, &size);
if (r == -ENOENT) /* Not found */
continue;
if (r >= 0) /* Found */
break;
log_error_errno(r, "Failed to read credential '%s': %m", *cn);
if (ret >= 0)
ret = r;
}
if (encrypted >= 2) { /* Found nowhere */
log_error_errno(SYNTHETIC_ERRNO(ENOENT), "Credential '%s' not set.", *cn);
if (ret >= 0)
ret = -ENOENT;
continue;
}
if (encrypted) {
_cleanup_(erase_and_freep) void *plaintext = NULL;
size_t plaintext_size;
r = decrypt_credential_and_warn(
*cn,
timestamp,
arg_tpm2_device,
data, size,
&plaintext, &plaintext_size);
if (r < 0)
return r;
erase_and_free(data);
data = TAKE_PTR(plaintext);
size = plaintext_size;
}
r = write_blob(stdout, data, size);
if (r < 0)
return r;

View file

@ -33,12 +33,12 @@ bool credential_name_valid(const char *s) {
return filename_is_valid(s) && fdname_is_valid(s);
}
int get_credentials_dir(const char **ret) {
static int get_credentials_dir_internal(const char *envvar, const char **ret) {
const char *e;
assert(ret);
e = secure_getenv("CREDENTIALS_DIRECTORY");
e = secure_getenv(envvar);
if (!e)
return -ENXIO;
@ -49,6 +49,14 @@ int get_credentials_dir(const char **ret) {
return 0;
}
int get_credentials_dir(const char **ret) {
return get_credentials_dir_internal("CREDENTIALS_DIRECTORY", ret);
}
int get_encrypted_credentials_dir(const char **ret) {
return get_credentials_dir_internal("ENCRYPTED_CREDENTIALS_DIRECTORY", ret);
}
int read_credential(const char *name, void **ret, size_t *ret_size) {
_cleanup_free_ char *fn = NULL;
const char *d;

View file

@ -26,7 +26,13 @@
bool credential_name_valid(const char *s);
/* Where creds have been passed to the local execution context */
int get_credentials_dir(const char **ret);
int get_encrypted_credentials_dir(const char **ret);
/* Where creds have been passed to the system */
#define SYSTEM_CREDENTIALS_DIRECTORY "/run/credentials/@system"
#define ENCRYPTED_SYSTEM_CREDENTIALS_DIRECTORY "/run/credentials/@encrypted"
int read_credential(const char *name, void **ret, size_t *ret_size);

View file

@ -3,9 +3,16 @@
set -e
TEST_DESCRIPTION="test credentials"
NSPAWN_ARGUMENTS="--set-credential=mynspawncredential:strangevalue"
NSPAWN_ARGUMENTS="${NSPAWN_ARGUMENTS:-} --set-credential=mynspawncredential:strangevalue"
QEMU_OPTIONS="${QEMU_OPTIONS:-} -fw_cfg name=opt/io.systemd.credentials/myqemucredential,string=othervalue"
KERNEL_APPEND="${KERNEL_APPEND:-} systemd.set_credential=kernelcmdlinecred:uff rd.systemd.import_credentials=no"
# shellcheck source=test/test-functions
. "${TEST_BASE_DIR:?}/test-functions"
test_append_files() {
instmods qemu_fw_cfg
generate_module_dependencies
}
do_test "$@"

View file

@ -23,17 +23,34 @@ rm /tmp/ts54-fallback
[ "$(systemd-run -p LoadCredential=paff:/tmp/ts54-fallback -p SetCredential=paff:poff --pipe --wait systemd-creds cat paff)" = "poff" ]
if systemd-detect-virt -q -c ; then
expected_credential=mynspawncredential
expected_value=strangevalue
elif [ -d /sys/firmware/qemu_fw_cfg/by_name ]; then
# Verify that passing creds through kernel cmdline works
[ "$(systemd-creds --system cat kernelcmdlinecred)" = "uff" ]
# If we aren't run in nspawn, we are run in qemu
systemd-detect-virt -q -v
expected_credential=myqemucredential
expected_value=othervalue
else
echo "qemu_fw_cfg support missing in kernel. Sniff!"
expected_credential=""
expected_value=""
fi
if [ "$expected_credential" != "" ] ; then
# If this test is run in nspawn a credential should have been passed to us. See test/TEST-54-CREDS/test.sh
[ "$(systemd-creds --system cat mynspawncredential)" = "strangevalue" ]
[ "$(systemd-creds --system cat "$expected_credential")" = "$expected_value" ]
# Test that propagation from system credential to service credential works
[ "$(systemd-run -p LoadCredential=mynspawncredential --pipe --wait systemd-creds cat mynspawncredential)" = "strangevalue" ]
[ "$(systemd-run -p LoadCredential="$expected_credential" --pipe --wait systemd-creds cat "$expected_credential")" = "$expected_value" ]
# Check it also works, if we rename it while propagating it
[ "$(systemd-run -p LoadCredential=miau:mynspawncredential --pipe --wait systemd-creds cat miau)" = "strangevalue" ]
[ "$(systemd-run -p LoadCredential=miau:"$expected_credential" --pipe --wait systemd-creds cat miau)" = "$expected_value" ]
# Combine it with a fallback (which should have no effect, given the cred should be passed down)
[ "$(systemd-run -p LoadCredential=mynspawncredential -p SetCredential=mynspawncredential:zzz --pipe --wait systemd-creds cat mynspawncredential)" = "strangevalue" ]
[ "$(systemd-run -p LoadCredential="$expected_credential" -p SetCredential="$expected_credential":zzz --pipe --wait systemd-creds cat "$expected_credential")" = "$expected_value" ]
fi
# Verify that the creds are immutable