diff --git a/NEWS b/NEWS index cdf3c3a0ad9..c34c55603e4 100644 --- a/NEWS +++ b/NEWS @@ -13,6 +13,14 @@ CHANGES WITH 261 in spe: attestation environments which use hardware CC registers and not the TPM quote. + * By default networking to cloud IMDS services is now locked down, for + recognized clouds. This is recommended for secure installations, but + typically conflicts with traditional IMDS clients such as cloud-init, + which require direct IMDS access currently. The new meson option + "imds-network" can be used to change the default networking mode to + "unlocked" at build-time, for compatibility. This is probably what + general purpose distributions should set for now. + CHANGES WITH 260: Feature Removals and Incompatible Changes: diff --git a/man/rules/meson.build b/man/rules/meson.build index 0ecf0db5d69..5c14fb626bb 100644 --- a/man/rules/meson.build +++ b/man/rules/meson.build @@ -1045,6 +1045,7 @@ manpages = [ ['systemd-hostnamed.service', '8', ['systemd-hostnamed'], 'ENABLE_HOSTNAMED'], ['systemd-hwdb', '8', [], 'ENABLE_HWDB'], ['systemd-id128', '1', [], ''], + ['systemd-imds-generator', '8', [], 'ENABLE_IMDS'], ['systemd-imds', '1', ['systemd-imds-import.service'], 'ENABLE_IMDS'], ['systemd-imdsd@.service', '8', diff --git a/man/systemd-imds-generator.xml b/man/systemd-imds-generator.xml new file mode 100644 index 00000000000..d8e1f1aa05b --- /dev/null +++ b/man/systemd-imds-generator.xml @@ -0,0 +1,107 @@ + + + + + + + + systemd-imds-generator + systemd + + + + systemd-imds-generator + 8 + + + + systemd-imds-generator + Generator to automatically enable IMDS on supporting environments + + + + /usr/lib/systemd/system-generators/systemd-imds-generator + + + + Description + + systemd-imds-generator is a generator that enables IMDS (Instance Metadata + Service) functionality at boot on systems that support it. Specifically it does three things: + + + It pulls the systemd-imdsd.socket unit (which activates + systemd-imdsd@.service8) + into the initial transaction, which provides IMDS access to local applications via Varlink + IPC. + + It pulls the systemd-imds-early-network.service unit into the + initial transaction, which generates a suitable + systemd.network5 + network configuration file that allows early-boot network access to the IMDS + functionality. + + It pulls the systemd-imds-import.service unit into the initial + transaction, which automatically imports various credentials from IMDS into the local system, storing + them in /run/credstore/. + + + By default, whether to pull in these services or not is decided based on + hwdb7 information, + that detects various IMDS environments automatically. However, this logic may be overridden via + systemd.imds=, see below. + + systemd-imds-generator implements + systemd.generator7. + + + + Kernel Command Line + + systemd-imds-generator understands the following kernel command line + parameters: + + + + + systemd.imds= + + Takes a boolean argument or the special value auto, and may be used to + enable or disable the IMDS logic. Note that this controls only whether the relevant services (as + listed above) are automatically pulled into the initial transaction, it has no effect if some other + unit or the user explicitly activate the relevant units. If this option is not used (or set to + auto) automatic detection of IMDS is used, see above. + + + + + + + + + systemd.imds.import= + + Takes a boolean argument. If false the systemd-imds-import.service (see + above) is not pulled into the initial transaction, i.e. no credentials are imported from + IMDS. Defaults to true. + + + + + + + + + + See Also + + systemd1 + systemd-imds1 + systemd-imdsd@.service8 + systemd.system-credentials7 + + + + diff --git a/meson.build b/meson.build index e35237e452b..2893bea332f 100644 --- a/meson.build +++ b/meson.build @@ -1544,6 +1544,7 @@ have = get_option('imds').require( conf.get('HAVE_LIBCURL') == 1, error_message : 'curl required').allowed() conf.set10('ENABLE_IMDS', have) +conf.set10('IMDS_NETWORK_LOCKED_DEFAULT', get_option('imds-network') == 'locked') have = get_option('importd').require( conf.get('HAVE_LIBCURL') == 1 and @@ -3087,7 +3088,8 @@ summary({ 'default user $PATH' : default_user_path != '' ? default_user_path : '(same as system services)', 'systemd service watchdog' : service_watchdog == '' ? 'disabled' : service_watchdog, 'time epoch' : f'@time_epoch@ (@alt_time_epoch@)', - 'TPM2 nvpcr base' : run_command(sh, '-c', 'printf 0x%x @0@'.format(get_option('tpm2-nvpcr-base')), check : true).stdout() + 'TPM2 nvpcr base' : run_command(sh, '-c', 'printf 0x%x @0@'.format(get_option('tpm2-nvpcr-base')), check : true).stdout(), + 'IMDS networking' : get_option('imds-network'), }) # TODO: diff --git a/meson_options.txt b/meson_options.txt index 7835f716662..30c5fd3ab67 100644 --- a/meson_options.txt +++ b/meson_options.txt @@ -144,6 +144,8 @@ option('timesyncd', type : 'boolean', description : 'install the systemd-timesyncd daemon') option('imds', type : 'feature', description : 'install the systemd-imds stack') +option('imds-network', type : 'combo', choices : [ 'locked', 'unlocked' ], + description : 'whether to default to locked/unlocked IMDS network mode') option('journal-storage-default', type : 'combo', choices : ['persistent', 'auto', 'volatile', 'none'], description : 'default storage mode for journald (main namespace)') option('remote', type : 'feature', deprecated : { 'true' : 'enabled', 'false' : 'disabled' }, diff --git a/src/imds/imds-generator.c b/src/imds/imds-generator.c new file mode 100644 index 00000000000..d33e63bddd3 --- /dev/null +++ b/src/imds/imds-generator.c @@ -0,0 +1,193 @@ +/* SPDX-License-Identifier: LGPL-2.1-or-later */ + +#include "sd-hwdb.h" + +#include "dropin.h" +#include "fileio.h" +#include "generator.h" +#include "imds-util.h" +#include "log.h" +#include "parse-util.h" +#include "proc-cmdline.h" +#include "special.h" +#include "string-util.h" +#include "virt.h" + +static int arg_enabled = -1; /* Whether we shall offer local IMDS APIs */ +static bool arg_import = true; /* Whether we shall import IMDS credentials, SSH keys, … into the local system */ +static ImdsNetworkMode arg_network_mode = + IMDS_NETWORK_LOCKED_DEFAULT ? IMDS_NETWORK_LOCKED : IMDS_NETWORK_UNLOCKED; + +static int parse_proc_cmdline_item(const char *key, const char *value, void *data) { + int r; + + assert(key); + + if (proc_cmdline_key_streq(key, "systemd.imds")) { + if (proc_cmdline_value_missing(key, value)) + return 0; + + r = parse_tristate_full(value, "auto", &arg_enabled); + if (r < 0) + return log_warning_errno(r, "Failed to parse systemd.imds= value: %m"); + + } else if (proc_cmdline_key_streq(key, "systemd.imds.import")) { + if (proc_cmdline_value_missing(key, value)) + return 0; + + r = parse_boolean(value); + if (r < 0) + return log_warning_errno(r, "Failed to parse systemd.imds.import= value: %m"); + + arg_import = r; + } else if (proc_cmdline_key_streq(key, "systemd.imds.network")) { + if (proc_cmdline_value_missing(key, value)) + return 0; + + ImdsNetworkMode m = imds_network_mode_from_string(value); + if (m < 0) + return log_warning_errno(m, "Failed to parse systemd.imds.network= value: %m"); + + arg_network_mode = m; + } + + return 0; +} + +static int smbios_get_modalias(char **ret) { + int r; + + assert(ret); + + _cleanup_free_ char *modalias = NULL; + r = read_virtual_file("/sys/devices/virtual/dmi/id/modalias", SIZE_MAX, &modalias, /* ret_size= */ NULL); + if (r < 0) + return r; + + truncate_nl(modalias); + + /* To detect Azure we need to check the chassis asset tag. Unfortunately the kernel does not include + * it in the modalias string right now. Let's hence append it manually. This matches similar logic in + * rules.d/60-dmi-id.rules. */ + _cleanup_free_ char *cat = NULL; + r = read_virtual_file("/sys/devices/virtual/dmi/id/chassis_asset_tag", SIZE_MAX, &cat, /* ret_size= */ NULL); + if (r < 0) + log_debug_errno(r, "Failed to read chassis asset tag, ignoring: %m"); + else { + truncate_nl(cat); + + if (!string_has_cc(cat, /* ok= */ NULL) && !isempty(cat) && !strextend(&modalias, "cat", cat, ":")) + return -ENOMEM; + } + + log_debug("Constructed SMBIOS modalias string: %s", modalias); + *ret = TAKE_PTR(modalias); + return 0; +} + +static int smbios_query(void) { + int r; + + /* Let's check whether the DMI device's hwdb data suggests IMDS support is available. Note, we cannot + * ask udev for this, as we typically run long before udev. Hence we'll do the hwdb lookup via + * sd-hwdb directly. */ + + _cleanup_free_ char *modalias = NULL; + r = smbios_get_modalias(&modalias); + if (r == -ENOENT) { + log_debug("No DMI device found, assuming IMDS is not available."); + return false; + } + if (r < 0) + return log_error_errno(r, "Failed to read DMI modalias: %m"); + + _cleanup_(sd_hwdb_unrefp) sd_hwdb *hwdb = NULL; + r = sd_hwdb_new(&hwdb); + if (r < 0) + return log_error_errno(r, "Failed to open hwdb: %m"); + + r = sd_hwdb_seek(hwdb, modalias); + if (r < 0) + return log_error_errno(r, "Failed to seek in hwdb for '%s': %m", modalias); + + for (;;) { + const char *key, *value; + r = sd_hwdb_enumerate(hwdb, &key, &value); + if (r < 0) + return log_error_errno(r, "Failed to enumerate hwdb entry for '%s': %m", modalias); + if (r == 0) + break; + + if (streq(key, "IMDS_VENDOR")) + return true; + } + + log_debug("IMDS_VENDOR= property for DMI device not set, assuming IMDS is not available."); + return false; +} + +static int run(const char *dest, const char *dest_early, const char *dest_late) { + int r; + + r = proc_cmdline_parse(parse_proc_cmdline_item, /* userdata= */ NULL, PROC_CMDLINE_STRIP_RD_PREFIX); + if (r < 0) + log_warning_errno(r, "Failed to parse kernel command line, ignoring: %m"); + + if (arg_enabled < 0) { + Virtualization v = detect_container(); + if (v < 0) + log_debug_errno(v, "Container detection failed, ignoring: %m"); + if (v > 0) { + log_debug("Running in a container, disabling IMDS logic."); + arg_enabled = false; + } else { + r = smbios_query(); + if (r < 0) + return r; + arg_enabled = r > 0; + } + } + + if (!arg_enabled) { + log_debug("IMDS not enabled, skipping generator."); + return 0; + } + + log_info("IMDS support enabled, pull in IMDS units."); + + /* Enable IMDS early networking, so that we can actually reach the IMDS server. */ + if (arg_network_mode != IMDS_NETWORK_OFF) { + r = generator_add_symlink(dest_early, SPECIAL_SYSINIT_TARGET, "wants", SYSTEM_DATA_UNIT_DIR "/systemd-imds-early-network.service"); + if (r < 0) + return log_error_errno(r, "Failed to hook in systemd-imds-early-network.service: %m"); + } + + /* Enable the IMDS service socket */ + r = generator_add_symlink(dest_early, SPECIAL_SOCKETS_TARGET, "wants", SYSTEM_DATA_UNIT_DIR "/systemd-imdsd.socket"); + if (r < 0) + return log_error_errno(r, "Failed to hook in systemd-imdsd.socket: %m"); + + /* We now know the SMBIOS device exists, hence it's safe now to order the IMDS service after it, so + * that it has all properties properly initialized. */ + r = write_drop_in( + dest_early, + "systemd-imdsd@.service", + 50, "dmi-id", + "# Automatically generated by systemd-imds-generator\n\n" + "[Unit]\n" + "Wants=sys-devices-virtual-dmi-id.device\n" + "After=sys-devices-virtual-dmi-id.device\n"); + if (r < 0) + return log_error_errno(r, "Failed to hook DMI id device before systemd-imdsd@.service: %m"); + + if (arg_import) { + /* Enable that we import IMDS data */ + r = generator_add_symlink(dest_early, SPECIAL_SYSINIT_TARGET, "wants", SYSTEM_DATA_UNIT_DIR "/systemd-imds-import.service"); + if (r < 0) + return log_error_errno(r, "Failed to hook in systemd-imds-import.service: %m"); + } + + return 0; +} + +DEFINE_MAIN_GENERATOR_FUNCTION(run); diff --git a/src/imds/meson.build b/src/imds/meson.build index a28dd0ca3a5..f9fa9b5f0fb 100644 --- a/src/imds/meson.build +++ b/src/imds/meson.build @@ -22,6 +22,13 @@ executables += [ 'imds-util.c' ), }, + generator_template + { + 'name' : 'systemd-imds-generator', + 'sources' : files( + 'imds-generator.c', + 'imds-util.c' + ), + }, ] install_data(