]> git.ipfire.org Git - thirdparty/systemd.git/commitdiff
imds: add "systemd-imds" tool that is a simple client to "systemd-imdsd"
authorLennart Poettering <lennart@amutable.com>
Wed, 4 Mar 2026 14:13:25 +0000 (15:13 +0100)
committerLennart Poettering <lennart@amutable.com>
Thu, 26 Mar 2026 09:54:15 +0000 (10:54 +0100)
This is a client tool to the systemd-imdsd@.service added in the
previous commit. It's mostly just a 1:1 IPC client via Varlink. It can
be used to query any IMDS key, but it's primary usecase is to acquire
the "userdata" from IMDS. Moreover, if invoked with the --import switch
it will check if the userdata contains a list of system credentials. If
so, it will import them into the local credstore. If the userdata does
not look like a list of system credentials no operation is executed,
under the assumption the data is intended for cloud-init instead.

It also imports a couple of other fields, if available and recogniuzed,
such as SSH keys and the hostname.

man/rules/meson.build
man/systemd-imds.xml [new file with mode: 0644]
src/imds/imds-tool.c [new file with mode: 0644]
src/imds/meson.build
units/meson.build
units/systemd-imds-import.service.in [new file with mode: 0644]

index 60fefdfb11cde913959e0dcfe672ff88ee80e25d..0ecf0db5d6957125e1e2969a43cc33cb69a28d73 100644 (file)
@@ -1045,6 +1045,7 @@ manpages = [
  ['systemd-hostnamed.service', '8', ['systemd-hostnamed'], 'ENABLE_HOSTNAMED'],
  ['systemd-hwdb', '8', [], 'ENABLE_HWDB'],
  ['systemd-id128', '1', [], ''],
+ ['systemd-imds', '1', ['systemd-imds-import.service'], 'ENABLE_IMDS'],
  ['systemd-imdsd@.service',
   '8',
   ['systemd-imdsd',
diff --git a/man/systemd-imds.xml b/man/systemd-imds.xml
new file mode 100644 (file)
index 0000000..3980c75
--- /dev/null
@@ -0,0 +1,174 @@
+<?xml version='1.0'?> <!--*-nxml-*-->
+<!DOCTYPE refentry PUBLIC "-//OASIS//DTD DocBook XML V4.5//EN"
+  "http://www.oasis-open.org/docbook/xml/4.5/docbookx.dtd">
+<!-- SPDX-License-Identifier: LGPL-2.1-or-later -->
+
+<refentry id="systemd-imds" conditional='ENABLE_IMDS'
+    xmlns:xi="http://www.w3.org/2001/XInclude">
+
+  <refentryinfo>
+    <title>systemd-imds</title>
+    <productname>systemd</productname>
+  </refentryinfo>
+
+  <refmeta>
+    <refentrytitle>systemd-imds</refentrytitle>
+    <manvolnum>1</manvolnum>
+  </refmeta>
+
+  <refnamediv>
+    <refname>systemd-imds</refname>
+    <refname>systemd-imds-import.service</refname>
+    <refpurpose>Cloud IMDS (Instance Metadata Service) tool</refpurpose>
+  </refnamediv>
+
+  <refsynopsisdiv>
+    <para><filename>systemd-imds-import.service</filename></para>
+    <cmdsynopsis>
+      <command>systemd-imds</command> <arg choice="opt" rep="repeat">OPTIONS</arg> <arg choice="opt">KEY</arg>
+    </cmdsynopsis>
+  </refsynopsisdiv>
+
+  <refsect1>
+    <title>Description</title>
+
+    <para><command>systemd-imds</command> is a tool for acquiring data from IMDS (Instance Metadata Service),
+    as provided in many cloud environments. It is a client to
+    <citerefentry><refentrytitle>systemd-imdsd@.service</refentrytitle><manvolnum>8</manvolnum></citerefentry>,
+    and provides access to IMDS data from shell environments.</para>
+
+    <para>The tool can operate in one of five modes:</para>
+
+    <itemizedlist>
+      <listitem><para>Without positional arguments (and without the <option>--well-known=</option> switch)
+      general IMDS service data and a few well known fields are displayed in human friendly
+      form.</para></listitem>
+
+      <listitem><para>With a positional argument (and without <option>--well-known=</option>) the IMDS data
+      referenced by the specified key is acquired and written to standard output, in unprocessed form. IMDS
+      keys are the part of the IMDS acquisition URL that are suffixed to the base URL. IMDS keys must begin
+      with a slash (<literal>/</literal>). Note that IMDS keys are typically
+      implementation-specific.</para></listitem>
+
+      <listitem><para>With the <option>--well-known=</option> option specified (see below), the indicated
+      well-known field is written to standard output, in unprocessed form. The concept of well-known fields
+      abstracts IMDS implementation differences to some level, exposing a unified interface for IMDS fields
+      that typically exist on many different implementations, but under implementation-specific
+      keys.</para></listitem>
+
+      <listitem><para>With the <option>--userdata</option> option specified (see below) the "userdata"
+      provided via IMDS is written to standard output. Under the hood this is similar to
+      <option>--well-known=userdata-base</option>, <option>--well-known=userdata</option> or
+      <option>--well-known=userdata-base64</option>. Each of the three is tried in turn (in this order), and
+      the first available is returned. For <option>--well-known=userdata-base</option> the
+      <literal>systemd-userdata</literal> userdata item is requested. For
+      <option>--well-known=userdata-base64</option> the returned data is automatically
+      Base64-decoded.</para></listitem>
+
+      <listitem><para>With the <option>--import</option> option specified, various well known and userdata
+      fields are imported into the local credential store, where they are used to configure and parameterize
+      the system. For details see below.</para></listitem>
+    </itemizedlist>
+  </refsect1>
+
+  <refsect1>
+    <title>Options and Commands</title>
+
+    <variablelist>
+      <varlistentry>
+        <term><option>--well-known=</option></term>
+        <term><option>-K</option></term>
+
+        <listitem><para>Takes one of <literal>hostname</literal>, <literal>region</literal>,
+        <literal>zone</literal>, <literal>ipv4-public</literal>, <literal>ipv6-public</literal>,
+        <literal>ssh-key</literal>, <literal>userdata</literal>, <literal>userdata-base</literal>,
+        <literal>userdata-base64</literal>. Acquires a specific "well-known" field from IMDS. Many of these
+        fields are commonly supported by various IMDS implementations, but typically some fields are
+        not. Note that if <option>--well-known=userdata-base</option> is used an additional subkey should be
+        specified as positional argument, which encodes the specific userdata item to acquire.</para>
+
+        <xi:include href="version-info.xml" xpointer="v261"/></listitem>
+      </varlistentry>
+
+      <varlistentry>
+        <term><option>--refresh=</option></term>
+
+        <listitem><para>Takes a time in seconds as argument, and indicates the required "freshness" of the
+        data, in case cached data is used.</para>
+
+        <xi:include href="version-info.xml" xpointer="v261"/></listitem>
+      </varlistentry>
+
+      <varlistentry>
+        <term><option>--cache=</option></term>
+
+        <listitem><para>Takes a boolean. If set to false local caching of IMDS is disabled, and the data is
+        always acquired fresh from the IMDS endpoint.</para>
+
+        <xi:include href="version-info.xml" xpointer="v261"/></listitem>
+      </varlistentry>
+
+      <varlistentry>
+        <term><option>--userdata</option></term>
+        <term><option>-u</option></term>
+
+        <listitem><para>Acquire this instance's IMDS user data, if available. See above for
+        details.</para>
+
+        <xi:include href="version-info.xml" xpointer="v261"/></listitem>
+      </varlistentry>
+
+      <varlistentry>
+        <term><option>--import</option></term>
+
+        <listitem><para>Acquires IMDS data and writes relevant fields as credentials to
+        <filename>/run/credstore/</filename>. This currently covers:</para>
+
+        <itemizedlist>
+          <listitem><para>If the IMDS user data is a valid JSON object containing a field
+          <varname>systemd.credentials</varname> (with a JSON array as value) it is processed, importing
+          arbitrary credentials listed in the array. Each array item must have a <varname>name</varname>
+          field indicating the credential name. It may have one <varname>text</varname>,
+          <varname>data</varname> or <varname>encrypted</varname> field, containing the credential data. If
+          <varname>text</varname> is used the value shall be a literal string of the credential value. If
+          <varname>data</varname> is used the value may be arbitrary binary data encoded in a Base64
+          string. If <varname>encrypted</varname> is used the value shall be a Base64 encoded encrypted
+          credential. See
+          <citerefentry><refentrytitle>systemd.system-credentials</refentrytitle><manvolnum>7</manvolnum></citerefentry>
+          for information about credentials that may be imported this way.</para></listitem>
+
+          <listitem><para>If the well-known <varname>ssh-key</varname> field is available, its value will be
+          imported into the <varname>ssh.authorized_keys.root</varname> credential.</para></listitem>
+
+          <listitem><para>If the well-known <varname>hostname</varname> field is available, its value will be
+          imported into the <varname>firstboot.hostname</varname> credential.</para></listitem>
+        </itemizedlist>
+
+        <para>This command is invoked by the <filename>systemd-imds-import.service</filename> run at
+        boot.</para>
+
+        <xi:include href="version-info.xml" xpointer="v261"/></listitem>
+      </varlistentry>
+
+      <xi:include href="standard-options.xml" xpointer="help" />
+      <xi:include href="standard-options.xml" xpointer="version" />
+    </variablelist>
+  </refsect1>
+
+  <refsect1>
+    <title>Exit status</title>
+
+    <para>On success, 0 is returned, a non-zero failure code otherwise.</para>
+  </refsect1>
+
+  <refsect1>
+    <title>See Also</title>
+    <para><simplelist type="inline">
+      <member><citerefentry><refentrytitle>systemd</refentrytitle><manvolnum>1</manvolnum></citerefentry></member>
+      <member><citerefentry><refentrytitle>systemd-imdsd@.service</refentrytitle><manvolnum>8</manvolnum></citerefentry></member>
+      <member><citerefentry><refentrytitle>systemd-imds-generator</refentrytitle><manvolnum>8</manvolnum></citerefentry></member>
+      <member><citerefentry><refentrytitle>systemd.system-credentials</refentrytitle><manvolnum>7</manvolnum></citerefentry></member>
+    </simplelist></para>
+  </refsect1>
+
+</refentry>
diff --git a/src/imds/imds-tool.c b/src/imds/imds-tool.c
new file mode 100644 (file)
index 0000000..d4a5b6b
--- /dev/null
@@ -0,0 +1,892 @@
+/* SPDX-License-Identifier: LGPL-2.1-or-later */
+
+#include <fcntl.h>
+#include <getopt.h>
+#include <sys/stat.h>
+#include <unistd.h>
+
+#include "sd-varlink.h"
+
+#include "alloc-util.h"
+#include "build.h"
+#include "build-path.h"
+#include "creds-util.h"
+#include "dns-rr.h"
+#include "errno-util.h"
+#include "fd-util.h"
+#include "fileio.h"
+#include "format-table.h"
+#include "format-util.h"
+#include "fs-util.h"
+#include "hexdecoct.h"
+#include "imds-util.h"
+#include "in-addr-util.h"
+#include "io-util.h"
+#include "iovec-util.h"
+#include "json-util.h"
+#include "log.h"
+#include "main-func.h"
+#include "parse-argument.h"
+#include "pretty-print.h"
+#include "string-util.h"
+#include "strv.h"
+#include "time-util.h"
+#include "tmpfile-util.h"
+
+static enum {
+        ACTION_SUMMARY,
+        ACTION_GET,
+        ACTION_USERDATA,
+        ACTION_IMPORT,
+        _ACTION_INVALID = -EINVAL,
+} arg_action = _ACTION_INVALID;
+static char *arg_key = NULL;
+static ImdsWellKnown arg_well_known = _IMDS_WELL_KNOWN_INVALID;
+static int arg_cache = -1;
+static usec_t arg_refresh_usec = 0;
+static bool arg_refresh_usec_set = false;
+
+STATIC_DESTRUCTOR_REGISTER(arg_key, freep);
+
+static int help(void) {
+        _cleanup_free_ char *link = NULL;
+        int r;
+
+        r = terminal_urlify_man("systemd-imds", "1", &link);
+        if (r < 0)
+                return log_oom();
+
+        printf("%s [OPTIONS...] [KEY]\n"
+               "\n%sIMDS data acquisition.%s\n\n"
+               "  -h --help            Show this help\n"
+               "     --version         Show package version\n"
+               "  -K --well-known=[hostname|region|zone|ipv4-public|ipv6-public|ssh-key|\n"
+               "                  userdata|userdata-base|userdata-base64]\n"
+               "                       Select well-known key/base\n"
+               "     --refresh=SEC     Set minimum freshness time for returned data\n"
+               "     --cache=no        Disable cache use\n"
+               "  -u --userdata        Dump user data\n"
+               "     --import          Import system credentials from IMDS userdata\n"
+               "                       and place them in /run/credstore/\n"
+               "\nSee the %s for details.\n",
+               program_invocation_short_name,
+               ansi_highlight(),
+               ansi_normal(),
+               link);
+
+        return 0;
+}
+
+static int parse_argv(int argc, char *argv[]) {
+
+        enum {
+                ARG_VERSION = 0x100,
+                ARG_REFRESH,
+                ARG_CACHE,
+                ARG_IMPORT,
+        };
+
+        static const struct option options[] = {
+                { "help",       no_argument,       NULL, 'h'         },
+                { "version",    no_argument,       NULL, ARG_VERSION },
+                { "well-known", required_argument, NULL, 'K'         },
+                { "refresh",    required_argument, NULL, ARG_REFRESH },
+                { "cache",      required_argument, NULL, ARG_CACHE   },
+                { "userdata",   no_argument,       NULL, 'u'         },
+                { "import",     no_argument,       NULL, ARG_IMPORT  },
+                {}
+        };
+
+        int c, r;
+
+        assert(argc >= 0);
+        assert(argv);
+
+        while ((c = getopt_long(argc, argv, "hK:u", options, NULL)) >= 0) {
+
+                switch (c) {
+
+                case 'h':
+                        return help();
+
+                case ARG_VERSION:
+                        return version();
+
+                case 'K': {
+                        if (isempty(optarg)) {
+                                arg_well_known = _IMDS_WELL_KNOWN_INVALID;
+                                break;
+                        }
+
+                        if (streq(optarg, "help"))
+                                return DUMP_STRING_TABLE(imds_well_known, ImdsWellKnown, _IMDS_WELL_KNOWN_MAX);
+
+                        ImdsWellKnown wk = imds_well_known_from_string(optarg);
+                        if (wk < 0)
+                                return log_error_errno(wk, "Failed to parse --well-known= argument: %s", optarg);
+
+                        arg_well_known = wk;
+                        break;
+                }
+
+                case ARG_CACHE:
+                        r = parse_tristate_argument_with_auto("--cache=", optarg, &arg_cache);
+                        if (r < 0)
+                                return r;
+
+                        break;
+
+                case ARG_REFRESH: {
+                        if (isempty(optarg)) {
+                                arg_refresh_usec_set = false;
+                                break;
+                        }
+
+                        usec_t t;
+                        r = parse_sec(optarg, &t);
+                        if (r < 0)
+                                return log_error_errno(r, "Failed to parse refresh timeout: %s", optarg);
+
+                        arg_refresh_usec = t;
+                        arg_refresh_usec_set = true;
+                        break;
+                }
+
+                case 'u':
+                        arg_action = ACTION_USERDATA;
+                        break;
+
+                case ARG_IMPORT:
+                        arg_action = ACTION_IMPORT;
+                        break;
+
+                case '?':
+                        return -EINVAL;
+
+                default:
+                        assert_not_reached();
+                }
+        }
+
+        if (IN_SET(arg_action, ACTION_USERDATA, ACTION_IMPORT)) {
+                if (argc != optind)
+                        return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "No parameters expected.");
+
+        } else {
+                assert(arg_action < 0);
+
+                if (argc > optind + 1)
+                        return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "None or one argument expected.");
+
+                if (argc == optind && arg_well_known < 0)
+                        arg_action = ACTION_SUMMARY;
+                else {
+                        if (arg_well_known < 0)
+                                arg_well_known = IMDS_BASE;
+
+                        if (argc > optind) {
+                                if (!imds_key_is_valid(argv[optind]))
+                                        return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Specified IMDS key is not valid, refusing: %s", argv[optind]);
+
+                                if (!imds_well_known_can_suffix(arg_well_known))
+                                        return log_error_errno(SYNTHETIC_ERRNO(EINVAL), "Well known key '%s' does not take a key suffix, refusing.", imds_well_known_to_string(arg_well_known));
+
+                                r = free_and_strdup_warn(&arg_key, argv[optind]);
+                                if (r < 0)
+                                        return r;
+                        }
+
+                        arg_action = ACTION_GET;
+                }
+        }
+
+        return 1;
+}
+
+static int acquire_imds_key(
+                sd_varlink *link,
+                ImdsWellKnown wk,
+                const char *key,
+                struct iovec *ret) {
+
+        int r;
+
+        assert(link);
+        assert(wk >= 0);
+        assert(wk < _IMDS_WELL_KNOWN_MAX);
+        assert(ret);
+
+        const char *error_id = NULL;
+        sd_json_variant *reply = NULL;
+        r = sd_varlink_callbo(
+                        link,
+                        "io.systemd.InstanceMetadata.Get",
+                        &reply,
+                        &error_id,
+                        SD_JSON_BUILD_PAIR_CONDITION(wk != IMDS_BASE, "wellKnown", JSON_BUILD_STRING_UNDERSCORIFY(imds_well_known_to_string(wk))),
+                        JSON_BUILD_PAIR_STRING_NON_EMPTY("key", key),
+                        SD_JSON_BUILD_PAIR_CONDITION(arg_refresh_usec_set, "refreshUSec", SD_JSON_BUILD_UNSIGNED(arg_refresh_usec)),
+                        SD_JSON_BUILD_PAIR_CONDITION(arg_cache >= 0, "cache", SD_JSON_BUILD_BOOLEAN(arg_cache)));
+        if (r < 0)
+                return log_error_errno(r, "Failed to issue io.systemd.InstanceMetadata.Get(): %m");
+        if (error_id) {
+                if (STR_IN_SET(error_id, "io.systemd.InstanceMetadata.KeyNotFound", "io.systemd.InstanceMetadata.WellKnownKeyUnset")) {
+                        *ret = (struct iovec) {};
+                        return 0;
+                }
+
+                return log_error_errno(sd_varlink_error_to_errno(error_id, reply), "Failed to issue io.systemd.InstanceMetadata.Get(): %s", error_id);
+        }
+
+        _cleanup_(iovec_done) struct iovec data = {};
+        static const sd_json_dispatch_field dispatch_table[] = {
+                { "data", SD_JSON_VARIANT_STRING, json_dispatch_unbase64_iovec, 0, SD_JSON_MANDATORY },
+                {},
+        };
+        r = sd_json_dispatch(reply, dispatch_table, SD_JSON_ALLOW_EXTENSIONS, &data);
+        if (r < 0)
+                return r;
+
+        *ret = TAKE_STRUCT(data);
+        return 1;
+}
+
+static int acquire_imds_key_as_string(
+                sd_varlink *link,
+                ImdsWellKnown wk,
+                const char *key,
+                char **ret) {
+
+        int r;
+
+        assert(link);
+        assert(wk >= 0);
+        assert(wk < _IMDS_WELL_KNOWN_MAX);
+        assert(ret);
+
+        _cleanup_(iovec_done) struct iovec data = {};
+        r = acquire_imds_key(link, wk, key, &data);
+        if (r < 0)
+                return r;
+        if (r == 0) {
+                *ret = NULL;
+                return 0;
+        }
+
+        _cleanup_free_ char *s = NULL;
+        r = make_cstring(data.iov_base, data.iov_len, MAKE_CSTRING_REFUSE_TRAILING_NUL, &s);
+        if (r < 0)
+                return r;
+
+        *ret = TAKE_PTR(s);
+        return 1;
+}
+
+static int acquire_imds_key_as_ip_address(
+                sd_varlink *link,
+                ImdsWellKnown wk,
+                const char *key,
+                int family,
+                union in_addr_union *ret) {
+        int r;
+
+        assert(link);
+        assert(wk >= 0);
+        assert(wk < _IMDS_WELL_KNOWN_MAX);
+        assert(ret);
+
+        _cleanup_free_ char *s = NULL;
+        r = acquire_imds_key_as_string(link, wk, key, &s);
+        if (r < 0)
+                return r;
+        if (r == 0 || isempty(s)) {
+                *ret = (union in_addr_union) {};
+                return 0;
+        }
+
+        r = in_addr_from_string(family, s, ret);
+        if (r < 0)
+                return r;
+
+        return 1;
+}
+
+static int action_summary(sd_varlink *link) {
+        int r;
+
+        assert(link);
+
+        _cleanup_(table_unrefp) Table *table = table_new_vertical();
+        if (!table)
+                return log_oom();
+
+        const char *error_id = NULL;
+        sd_json_variant *reply = NULL;
+        r = sd_varlink_call(
+                        link,
+                        "io.systemd.InstanceMetadata.GetVendorInfo",
+                        /* parameters= */ NULL,
+                        &reply,
+                        &error_id);
+        if (r < 0)
+                return log_error_errno(r, "Failed to issue io.systemd.InstanceMetadata.GetVendorInfo(): %m");
+        if (error_id)
+                return log_error_errno(sd_varlink_error_to_errno(error_id, reply), "Failed to issue io.systemd.InstanceMetadata.GetVendorInfo(): %s", error_id);
+
+        const char *vendor = NULL;
+        static const sd_json_dispatch_field dispatch_table[] = {
+                { "vendor",    SD_JSON_VARIANT_STRING, sd_json_dispatch_const_string, 0, 0 },
+                {}
+        };
+        r = sd_json_dispatch(reply, dispatch_table, SD_JSON_ALLOW_EXTENSIONS, &vendor);
+        if (r < 0)
+                return r;
+        if (vendor) {
+                r = table_add_many(table,
+                                   TABLE_FIELD, "Vendor",
+                                   TABLE_SET_JSON_FIELD_NAME, "vendor",
+                                   TABLE_STRING, vendor);
+                if (r < 0)
+                        return table_log_add_error(r);
+        }
+
+        static const struct {
+                ImdsWellKnown well_known;
+                const char *field;
+        } wktable[] = {
+                { IMDS_HOSTNAME,     "Hostname"            },
+                { IMDS_REGION,       "Region"              },
+                { IMDS_ZONE,         "Zone"                },
+                { IMDS_IPV4_PUBLIC,  "Public IPv4 Address" },
+                { IMDS_IPV6_PUBLIC,  "Public IPv6 Address" },
+        };
+        FOREACH_ELEMENT(i, wktable) {
+                _cleanup_free_ char *text = NULL;
+
+                r = acquire_imds_key_as_string(link, i->well_known, /* key= */ NULL, &text);
+                if (r < 0)
+                        return r;
+                if (r == 0 || isempty(text))
+                        continue;
+
+                r = table_add_many(table,
+                                   TABLE_FIELD, i->field,
+                                   TABLE_SET_JSON_FIELD_NAME, imds_well_known_to_string(i->well_known),
+                                   TABLE_STRING, text);
+                if (r < 0)
+                        return table_log_add_error(r);
+        }
+
+        if (table_isempty(table))
+                return log_error_errno(SYNTHETIC_ERRNO(EOPNOTSUPP), "No well-known IMDS data available.");
+
+        r = table_print(table, NULL);
+        if (r < 0)
+                return table_log_print_error(r);
+
+        return 0;
+}
+
+static const char *detect_json_object(const char *text) {
+        assert(text);
+
+        /* Checks if the provided text looks like a JSON object. It checks if the first non-whitespace
+         * characters are {" or {}. */
+
+        text += strspn(text, WHITESPACE);
+        if (*text != '{')
+                return NULL;
+
+        const char *e = text + 1;
+        e += strspn(e, WHITESPACE);
+        if (!IN_SET(*e, '"', '}'))
+                return NULL;
+
+        return text;
+}
+
+static int write_credential(const char *dir, const char *name, const struct iovec *data) {
+        int r;
+
+        assert(dir);
+        assert(name);
+
+        _cleanup_close_ int dfd = open_mkdir(dir, O_CLOEXEC|O_PATH, 0700);
+        if (dfd < 0)
+                return log_error_errno(dfd, "Failed to open credential directory '%s': %m", dir);
+
+        if (faccessat(dfd, name, F_OK, AT_SYMLINK_NOFOLLOW) < 0) {
+                if (errno != ENOENT)
+                        return log_error_errno(errno, "Failed to check if '%s' exists in credential directory '%s': %m", name, dir);
+        } else {
+                log_notice("Skipping importing of credential '%s', it already exists locally in '%s'.", name, dir);
+                return 0;
+        }
+
+        _cleanup_free_ char *t = NULL;
+        _cleanup_close_ int fd = open_tmpfile_linkable_at(dfd, name, O_WRONLY|O_CLOEXEC, &t);
+        if (fd < 0)
+                return log_error_errno(fd, "Failed to create credential file '%s/%s': %m", dir, name);
+
+        CLEANUP_TMPFILE_AT(dfd, t);
+
+        r = loop_write(fd, data->iov_base, data->iov_len);
+        if (r < 0)
+                return log_error_errno(r, "Failed to write credential file '%s/%s': %m", dir, name);
+
+        if (fchmod(fd, 0400) < 0)
+                return log_error_errno(errno, "Failed to set access mode on credential file '%s/%s': %m", dir, name);
+
+        r = link_tmpfile_at(fd, dfd, t, name, /* flags= */ 0);
+        if (r < 0)
+                return log_error_errno(r, "Failed to move credential file '%s/%s' into place: %m", dir, name);
+
+        t = mfree(t); /* Disarm auto-cleanup */
+        return 1;
+}
+
+typedef struct CredentialData {
+        const char *name;
+        const char *text;
+        struct iovec data, encrypted;
+} CredentialData;
+
+static void credential_data_done(CredentialData *d) {
+        assert(d);
+
+        iovec_done(&d->data);
+        iovec_done(&d->encrypted);
+}
+
+static int import_credential_one(CredentialData *d) {
+        int r;
+
+        assert(d);
+        assert(d->name);
+
+        log_debug("Importing credential '%s' from IMDS.", d->name);
+
+        const char *dir = "/run/credstore";
+        struct iovec *v, _v;
+        if (d->text) {
+                _v = IOVEC_MAKE_STRING(d->text);
+                v = &_v;
+        } else if (iovec_is_set(&d->data))
+                v = &d->data;
+        else if (iovec_is_set(&d->encrypted)) {
+                dir = "/run/credstore.encrypted";
+                v = &d->encrypted;
+        } else
+                assert_not_reached();
+
+        r = write_credential(dir, d->name, v);
+        if (r <= 0)
+                return r;
+
+        log_info("Imported credential '%s' from IMDS (%s).", d->name, FORMAT_BYTES(v->iov_len));
+        return 1;
+}
+
+static int import_credentials(const char *text) {
+        int r;
+
+        assert(text);
+
+        /* We cannot be sure if the data is actually intended for us. Hence let's be somewhat defensive, and
+         * accept data in two ways: either immediately as a JSON object, or alternatively marked with a first
+         * line of "#systemd-userdata". The latter mimics the markers cloud-init employs. */
+
+        const char *e = startswith(text, "#systemd-userdata\n");
+        if (!e) {
+                e = detect_json_object(text);
+                if (!e) {
+                        log_info("IMDS user data does not look like JSON or systemd userdata, not processing.");
+                        return 0;
+                }
+        }
+
+        log_debug("Detected JSON userdata");
+
+        _cleanup_(sd_json_variant_unrefp) sd_json_variant *j = NULL;
+        unsigned line = 0, column = 0;
+        r = sd_json_parse(e, /* flags= */ 0, &j, &line, &column);
+        if (r < 0) {
+                if (line > 0)
+                        log_syntax(/* unit= */ NULL, LOG_WARNING, /* filename= */ NULL, line, r, "JSON parse failure.");
+                else
+                        log_error_errno(r, "Failed to parse IMDS userdata JSON: %m");
+                return 0;
+        }
+
+        static const sd_json_dispatch_field top_table[] = {
+                { "systemd.credentials", SD_JSON_VARIANT_ARRAY, sd_json_dispatch_variant_noref, 0, 0 },
+                {},
+        };
+
+        sd_json_variant *creds = NULL;
+        r = sd_json_dispatch(j, top_table, SD_JSON_ALLOW_EXTENSIONS|SD_JSON_LOG, &creds);
+        if (r < 0)
+                return r;
+
+        unsigned n_imported = 0;
+        int ret = 0;
+        if (creds) {
+                log_debug("Found 'systemd.credentials' field");
+
+                sd_json_variant *c;
+                JSON_VARIANT_ARRAY_FOREACH(c, creds) {
+                        static const sd_json_dispatch_field credential_table[] = {
+                                { "name",      SD_JSON_VARIANT_STRING, sd_json_dispatch_const_string, offsetof(CredentialData, name),      SD_JSON_MANDATORY },
+                                { "text",      SD_JSON_VARIANT_STRING, sd_json_dispatch_const_string, offsetof(CredentialData, text),      0                 },
+                                { "data",      SD_JSON_VARIANT_STRING, json_dispatch_unbase64_iovec,  offsetof(CredentialData, data),      0                 },
+                                { "encrypted", SD_JSON_VARIANT_STRING, json_dispatch_unbase64_iovec,  offsetof(CredentialData, encrypted), 0                 },
+                                {},
+                        };
+
+                        _cleanup_(credential_data_done) CredentialData d = {};
+                        r = sd_json_dispatch(c, credential_table, SD_JSON_LOG|SD_JSON_WARNING, &d);
+                        if (r < 0) {
+                                RET_GATHER(ret, r);
+                                continue;
+                        }
+
+                        if (!credential_name_valid(d.name)) {
+                                RET_GATHER(ret, log_warning_errno(SYNTHETIC_ERRNO(EBADMSG), "Credential name '%s' is not valid, refusing.", d.name));
+                                continue;
+                        }
+
+                        if ((!!d.text + !!iovec_is_set(&d.data) + !!iovec_is_set(&d.encrypted)) != 1) {
+                                RET_GATHER(ret, log_warning_errno(SYNTHETIC_ERRNO(EBADMSG), "Exactly one of 'text', 'data', 'encrypted' must be set for credential '%s', refusing.", d.name));
+                                continue;
+                        }
+
+                        r = import_credential_one(&d);
+                        if (r < 0)
+                                RET_GATHER(ret, r);
+                        else if (r > 0)
+                                n_imported++;
+                }
+        }
+
+        log_full(n_imported == 0 ? LOG_DEBUG : LOG_INFO, "Imported %u credentials from IMDS.", n_imported);
+        return ret;
+}
+
+static int add_public_address_to_json_array(sd_json_variant **array, int family, const union in_addr_union *addr) {
+        int r;
+
+        assert(array);
+        assert(IN_SET(family, AF_INET, AF_INET6));
+        assert(addr);
+
+        if (in_addr_is_null(family, addr))
+                return 0;
+
+        _cleanup_(dns_resource_record_unrefp) DnsResourceRecord *rr = NULL;
+        if (dns_resource_record_new_address(&rr, family, addr, "_public") < 0)
+                return log_oom();
+
+        _cleanup_(sd_json_variant_unrefp) sd_json_variant *rrj = NULL;
+        r = dns_resource_record_to_json(rr, &rrj);
+        if (r < 0)
+                return log_error_errno(r, "Failed to convert A RR to JSON: %m");
+
+        r = sd_json_variant_append_array(array, rrj);
+        if (r < 0)
+                return log_error_errno(r, "Failed to append A RR to JSON array: %m");
+
+        log_debug("Writing IMDS RR for: %s", dns_resource_record_to_string(rr));
+        return 1;
+}
+
+static int import_imds_public_addresses(sd_varlink *link) {
+        int r, ret = 0;
+
+        assert(link);
+
+        /* Creates local RRs (honoured by systemd-resolved) for our public addresses. */
+
+        _cleanup_(sd_json_variant_unrefp) sd_json_variant *aj = NULL;
+
+        union in_addr_union u = {};
+        r = acquire_imds_key_as_ip_address(link, IMDS_IPV4_PUBLIC, /* key= */ NULL, AF_INET, &u);
+        if (r < 0)
+                RET_GATHER(ret, r);
+        else if (r > 0) {
+                r = add_public_address_to_json_array(&aj, AF_INET, &u);
+                if (r < 0)
+                        return r;
+        }
+
+        u = (union in_addr_union) {};
+        r = acquire_imds_key_as_ip_address(link, IMDS_IPV6_PUBLIC, /* key= */ NULL, AF_INET6, &u);
+        if (r < 0)
+                RET_GATHER(ret, r);
+        else if (r > 0) {
+                r = add_public_address_to_json_array(&aj, AF_INET6, &u);
+                if (r < 0)
+                        return r;
+        }
+
+        if (sd_json_variant_elements(aj) == 0) {
+                log_debug("No IMDS public addresses known, not writing our RRs.");
+                return 0;
+        }
+
+        _cleanup_free_ char *text = NULL;
+        r = sd_json_variant_format(aj, SD_JSON_FORMAT_NEWLINE, &text);
+        if (r < 0)
+                return log_error_errno(r, "Failed to format JSON text: %m");
+
+        r = write_string_file("/run/systemd/resolve/static.d/imds-public.rr", text, WRITE_STRING_FILE_CREATE|WRITE_STRING_FILE_ATOMIC|WRITE_STRING_FILE_MKDIR_0755);
+        if (r < 0)
+                return log_error_errno(r, "Failed to write IMDS RR data: %m");
+
+        log_debug("IMDS public addresses written out.");
+        return 1;
+}
+
+static int import_imds_ssh_key(sd_varlink *link) {
+        int r;
+
+        assert(link);
+
+        _cleanup_(iovec_done) struct iovec data = {};
+        r = acquire_imds_key(link, IMDS_SSH_KEY, /* key= */ NULL, &data);
+        if (r < 0)
+                return r;
+        if (r == 0 || !iovec_is_set(&data)) {
+                log_debug("No SSH key supplied via IMDS, not importing.");
+                return 0;
+        }
+
+        r = write_credential("/run/credstore", "ssh.authorized_keys.root", &data);
+        if (r <= 0)
+                return r;
+
+        log_info("Imported SSH key as credential 'ssh.authorized_keys.root'.");
+        return 0;
+}
+
+static int import_imds_hostname(sd_varlink *link) {
+        int r;
+
+        assert(link);
+
+        _cleanup_(iovec_done) struct iovec data = {};
+        r = acquire_imds_key(link, IMDS_HOSTNAME, /* key= */ NULL, &data);
+        if (r < 0)
+                return r;
+        if (r == 0 || !iovec_is_set(&data)) {
+                log_debug("No hostname supplied via IMDS, not importing.");
+                return 0;
+        }
+
+        r = write_credential("/run/credstore", "firstboot.hostname", &data);
+        if (r <= 0)
+                return r;
+
+        log_info("Imported hostname as credential 'firstboot.hostname'.");
+        return 0;
+}
+
+static int acquire_imds_userdata(sd_varlink *link, struct iovec *ret) {
+        int r;
+
+        assert(link);
+        assert(ret);
+
+        /* First try our private namespace, if the concept exists, and then fall back to the singleton */
+        _cleanup_(iovec_done) struct iovec data = {};
+        r = acquire_imds_key(link, IMDS_USERDATA_BASE, "/systemd-userdata", &data);
+        if (r == 0)
+                r = acquire_imds_key(link, IMDS_USERDATA, /* key= */ NULL, &data);
+        if (r < 0)
+                return r;
+        if (r > 0) {
+                if (!iovec_is_set(&data)) { /* Treat empty user data like empty */
+                        *ret = (struct iovec) {};
+                        return 0;
+                }
+
+                *ret = TAKE_STRUCT(data);
+                return 1;
+        }
+
+        r = acquire_imds_key(link, IMDS_USERDATA_BASE64, /* key= */ NULL, &data);
+        if (r < 0)
+                return r;
+        _cleanup_(iovec_done) struct iovec decoded = {};
+        if (r > 0) {
+                r = unbase64mem_full(data.iov_base, data.iov_len, /* secure= */ false, &decoded.iov_base, &decoded.iov_len);
+                if (r < 0)
+                        return r;
+        }
+
+        if (!iovec_is_set(&decoded)) { /* Treat empty user data like empty */
+                *ret = (struct iovec) {};
+                return 0;
+        }
+
+        *ret = TAKE_STRUCT(decoded);
+        return 1;
+}
+
+static int action_get(sd_varlink *link) {
+        int r;
+
+        assert(link);
+
+        _cleanup_(iovec_done) struct iovec data = {};
+        r = acquire_imds_key(link, arg_well_known, arg_key, &data);
+        if (r < 0)
+                return r;
+        if (r == 0)
+                return log_error_errno(SYNTHETIC_ERRNO(ENOENT), "Key not available.");
+
+        r = loop_write(STDOUT_FILENO, data.iov_base, data.iov_len);
+        if (r < 0)
+                return log_error_errno(r, "Failed to write data to standard output: %m");
+
+        return 0;
+}
+
+static int action_userdata(sd_varlink *link) {
+        int r;
+
+        assert(link);
+
+        _cleanup_(iovec_done) struct iovec data = {};
+        r = acquire_imds_userdata(link, &data);
+        if (r < 0)
+                return r;
+        if (r == 0)
+                return log_error_errno(SYNTHETIC_ERRNO(ENOENT), "User data not available.");
+
+        r = loop_write(STDOUT_FILENO, data.iov_base, data.iov_len);
+        if (r < 0)
+                return log_error_errno(r, "Failed to write data to standard output: %m");
+
+        return 0;
+}
+
+static int remove_userdata(const char *path) {
+        assert(path);
+
+        if (unlink(path) < 0) {
+
+                if (errno != ENOENT)
+                        log_debug_errno(errno, "Failed to remove '%s', ignoring: %m", path);
+
+                return 0;
+        }
+
+        log_debug("Removed '%s'.", path);
+        return 1;
+}
+
+static int save_userdata(const struct iovec *data, const char *path) {
+        int r;
+
+        assert(data);
+        assert(path);
+
+        if (!iovec_is_set(data))
+                return remove_userdata(path);
+
+        r = write_data_file_atomic_at(AT_FDCWD, path, data, WRITE_DATA_FILE_MKDIR_0755);
+        if (r < 0)
+                return log_error_errno(r, "Failed to save userdata to '%s': %m", path);
+
+        log_debug("Saved userdata to '%s'.", path);
+        return 1;
+}
+
+static int action_import(sd_varlink *link) {
+        int r;
+
+        assert(link);
+
+        int ret = 0;
+        RET_GATHER(ret, import_imds_public_addresses(link));
+        RET_GATHER(ret, import_imds_hostname(link));
+        RET_GATHER(ret, import_imds_ssh_key(link));
+
+        _cleanup_(iovec_done) struct iovec data = {};
+        r = acquire_imds_userdata(link, &data);
+        if (r < 0)
+                return RET_GATHER(ret, r);
+        if (r == 0) {
+                log_info("No IMDS data available, not importing credentials.");
+                (void) remove_userdata("/run/systemd/imds/userdata");
+                return ret;
+        }
+
+        /* Keep a pristine copy of the userdata we actually applied. (Note that this data is typically also
+         * kept as cached item on systemd-imdsd, but that one is possibly subject to cache invalidation,
+         * while this one is supposed to pin the data actually in effect.) */
+        (void) save_userdata(&data, "/run/systemd/imds/userdata");
+
+        /* Ensure no inner NUL byte */
+        if (memchr(data.iov_base, 0, data.iov_len)) {
+                log_info("IMDS user data contains NUL byte, not processing.");
+                return ret;
+        }
+
+        /* Turn this into a proper C string */
+        if (!iovec_append(&data, &IOVEC_MAKE_BYTE(0)))
+                return log_oom();
+
+        return RET_GATHER(ret, import_credentials(data.iov_base));
+}
+
+static int run(int argc, char* argv[]) {
+        int r;
+
+        log_setup();
+
+        r = parse_argv(argc, argv);
+        if (r <= 0)
+                return r;
+
+        _cleanup_(sd_varlink_unrefp) sd_varlink *link = NULL;
+        r = sd_varlink_connect_address(&link, "/run/systemd/io.systemd.InstanceMetadata");
+        if (r < 0) {
+                if (r != -ENOENT && !ERRNO_IS_NEG_DISCONNECT(r))
+                        return log_error_errno(r, "Failed to connect to systemd-imdsd: %m");
+
+                log_debug_errno(r, "Couldn't connect to /run/systemd/io.systemd.InstanceMetadata, will try to fork off systemd-imdsd as child now.");
+
+                /* Try to fork off systemd-imdsd as a child as a fallback. If we have privileges and the
+                 * SO_FWMARK trickery is not necessary, then this might just work. */
+                _cleanup_free_ char *p = NULL;
+                _cleanup_close_ int pin_fd =
+                        pin_callout_binary(LIBEXECDIR "/systemd-imdsd", &p);
+                if (pin_fd < 0)
+                        return log_error_errno(pin_fd, "Failed to pick up imdsd binary: %m");
+
+                r = sd_varlink_connect_exec(&link, p, /* argv[]= */ NULL);
+                if (r < 0)
+                        return log_error_errno(r, "Failed to connect to imdsd service: %m");
+        }
+
+        switch (arg_action) {
+
+        case ACTION_SUMMARY:
+                return action_summary(link);
+
+        case ACTION_GET:
+                return action_get(link);
+
+        case ACTION_USERDATA:
+                return action_userdata(link);
+
+        case ACTION_IMPORT:
+                return action_import(link);
+
+        default:
+                assert_not_reached();
+        }
+}
+
+DEFINE_MAIN_FUNCTION_WITH_POSITIVE_FAILURE(run);
index 79214890ea05c6ab3f242e02cf8ae2cb9635aa2d..a28dd0ca3a5103b35109139c25e1044e46335be6 100644 (file)
@@ -14,6 +14,14 @@ executables += [
                 ) + import_curl_util_c,
                 'dependencies' : [ libcurl ],
         },
+        libexec_template + {
+                'name' : 'systemd-imds',
+                'public' : true,
+                'sources' : files(
+                        'imds-tool.c',
+                        'imds-util.c'
+                ),
+        },
 ]
 
 install_data(
index 782d1ecadfbe4e6f8556cfd8d0a363b13451efc8..ca17237dd0b16793514f5d6df943ef099c8d22a4 100644 (file)
@@ -404,6 +404,10 @@ units = [
           'file' : 'systemd-imds-early-network.service.in',
           'conditions' : ['ENABLE_IMDS'],
         },
+        {
+          'file' : 'systemd-imds-import.service.in',
+          'conditions' : ['ENABLE_IMDS'],
+        },
         {
           'file' : 'systemd-importd.service.in',
           'conditions' : ['ENABLE_IMPORTD'],
diff --git a/units/systemd-imds-import.service.in b/units/systemd-imds-import.service.in
new file mode 100644 (file)
index 0000000..9704557
--- /dev/null
@@ -0,0 +1,25 @@
+#  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=Import System Credentials from IMDS
+Documentation=man:systemd-imds(1)
+Documentation=man:systemd.system-credentials(7)
+DefaultDependencies=no
+Wants=systemd-imdsd.socket network-online.target
+After=systemd-imdsd.socket network-online.target
+Before=sysinit.target systemd-firstboot.service
+Conflicts=shutdown.target
+Before=shutdown.target
+ConditionPathExists=/etc/initrd-release
+
+[Service]
+Type=oneshot
+RemainAfterExit=yes
+ExecStart={{LIBEXECDIR}}/systemd-imds --import