]> git.ipfire.org Git - thirdparty/systemd.git/commitdiff
networkd/wireguard: support network.wireguard.* credentials 30826/head
authorMike Yuan <me@yhndnzj.com>
Wed, 27 Dec 2023 13:38:32 +0000 (21:38 +0800)
committerMike Yuan <me@yhndnzj.com>
Tue, 9 Jan 2024 07:25:30 +0000 (15:25 +0800)
Closes #26702

man/systemd.netdev.xml
man/systemd.system-credentials.xml
src/network/netdev/wireguard.c
test/test-network/conf/25-wireguard-endpoint-peer0-cred.txt [new file with mode: 0644]
test/test-network/conf/25-wireguard-no-peer-private-key-cred.txt [new file with mode: 0644]
test/test-network/conf/25-wireguard-no-peer.netdev
test/test-network/conf/25-wireguard-preshared-key-peer2-cred.txt [new file with mode: 0644]
test/test-network/conf/25-wireguard.netdev
test/test-network/conf/25-wireguard.netdev.d/peer2.conf
test/test-network/systemd-networkd-tests.py
units/systemd-networkd.service.in

index cd77e725bc49b266aeacb60fdb1408cc55968c0e..bf3b5c21daef51a7eb175f402c45a2b7aa672c0e 100644 (file)
       <varlistentry>
         <term><varname>PrivateKey=</varname></term>
         <listitem>
-          <para>The Base64 encoded private key for the interface. It can be
-          generated using the <command>wg genkey</command> command
+          <para>The Base64 encoded private key for the interface. It can be generated using
+          the <command>wg genkey</command> command
           (see <citerefentry project="wireguard"><refentrytitle>wg</refentrytitle><manvolnum>8</manvolnum></citerefentry>).
-          This option or <varname>PrivateKeyFile=</varname> is mandatory to use WireGuard.
-          Note that because this information is secret, you may want to set
-          the permissions of the .netdev file to be owned by <literal>root:systemd-network</literal>
-          with a <literal>0640</literal> file mode.</para>
+          Specially, if the specified key is prefixed with <literal>@</literal>, it is interpreted as
+          the name of the credential from which the actual key shall be read. <command>systemd-networkd.service</command>
+          automatically imports credentials matching <literal>network.wireguard.*</literal>. For more details
+          on credentials, refer to
+          <citerefentry><refentrytitle>systemd.exec</refentrytitle><manvolnum>5</manvolnum></citerefentry>.
+          A private key is mandatory to use WireGuard. If not set, the credential
+          <literal>network.wireguard.private.<replaceable>netdev</replaceable></literal> is used if exists.
+          I.e. for <filename>50-foobar.netdev</filename>, <literal>network.wireguard.private.50-foobar</literal>
+          is tried.</para>
+
+          <para>Note that because this information is secret, it's strongly recommended to use an (encrypted)
+          credential. Alternatively, you may want to set the permissions of the .netdev file to be owned
+          by <literal>root:systemd-network</literal> with a <literal>0640</literal> file mode.</para>
 
           <xi:include href="version-info.xml" xpointer="v237"/>
         </listitem>
         <listitem>
           <para>Sets a Base64 encoded public key calculated by <command>wg pubkey</command>
           (see <citerefentry project="wireguard"><refentrytitle>wg</refentrytitle><manvolnum>8</manvolnum></citerefentry>)
-          from a private key, and usually transmitted out of band to the
-          author of the configuration file. This option is mandatory for this
-          section.</para>
+          from a private key, and usually transmitted out of band to the author of the configuration file.
+          This option honors the <literal>@</literal> prefix in the same way as the <option>PrivateKey=</option>
+          setting of the <option>[WireGuard]</option> section. This option is mandatory for this section.</para>
 
         <xi:include href="version-info.xml" xpointer="v237"/>
         </listitem>
       <varlistentry>
         <term><varname>PresharedKey=</varname></term>
         <listitem>
-          <para>Optional preshared key for the interface. It can be generated
-          by the <command>wg genpsk</command> command. This option adds an
-          additional layer of symmetric-key cryptography to be mixed into the
-          already existing public-key cryptography, for post-quantum
-          resistance.
-          Note that because this information is secret, you may want to set
-          the permissions of the .netdev file to be owned by <literal>root:systemd-network</literal>
-          with a <literal>0640</literal> file mode.</para>
+          <para>Optional preshared key for the interface. It can be generated by the <command>wg genpsk</command>
+          command. This option adds an additional layer of symmetric-key cryptography to be mixed into the
+          already existing public-key cryptography, for post-quantum resistance.
+          This option honors the <literal>@</literal> prefix in the same way as the <option>PrivateKey=</option>
+          setting of the <option>[WireGuard]</option> section.</para>
+
+          <para>Note that because this information is secret, it's strongly recommended to use an (encrypted)
+          credential. Alternatively, you may want to set the permissions of the .netdev file to be owned
+          by <literal>root:systemd-network</literal> with a <literal>0640</literal> file mode.</para>
 
           <xi:include href="version-info.xml" xpointer="v237"/>
         </listitem>
       <varlistentry>
         <term><varname>Endpoint=</varname></term>
         <listitem>
-          <para>Sets an endpoint IP address or hostname, followed by a colon, and then
-          a port number. IPv6 address must be in the square brackets. For example,
-          <literal>111.222.333.444:51820</literal> for IPv4 and <literal>[1111:2222::3333]:51820</literal>
-          for IPv6 address. This endpoint will be updated automatically once to
-          the most recent source IP address and port of correctly
+          <para>Sets an endpoint IP address or hostname, followed by a colon, and then a port number.
+          IPv6 address must be in the square brackets. For example, <literal>111.222.333.444:51820</literal>
+          for IPv4 and <literal>[1111:2222::3333]:51820</literal> for IPv6 address. This endpoint will be
+          updated automatically once to the most recent source IP address and port of correctly
           authenticated packets from the peer at configuration time.</para>
 
+          <para>This option honors the <literal>@</literal> prefix in the same way as the <option>PrivateKey=</option>
+          setting of the <option>[WireGuard]</option> section.</para>
+
           <xi:include href="version-info.xml" xpointer="v237"/>
         </listitem>
       </varlistentry>
index b2d491fe5813aa49a3a94536f50229ac0733078b..eb4c94c47f1a3ad276ea9fb3497d128c278081c6 100644 (file)
 
           <para>Note that the resulting files are created world-readable, it's hence recommended to not include
           secrets in these credentials, but supply them via separate credentials directly to
-          <filename>systemd-networkd.service</filename>.</para>
+          <filename>systemd-networkd.service</filename>, e.g. <varname>network.wireguard.*</varname>
+          as described below.</para>
+
+          <xi:include href="version-info.xml" xpointer="v256"/>
+        </listitem>
+      </varlistentry>
+
+      <varlistentry>
+        <term><varname>network.wireguard.*</varname></term>
+        <listitem>
+          <para>Configures secrets for WireGuard netdevs. Read by
+          <citerefentry><refentrytitle>systemd-networkd.service</refentrytitle><manvolnum>8</manvolnum></citerefentry>.
+          For more information, refer to the <option>[WireGuard]</option> section of
+          <citerefentry><refentrytitle>systemd.netdev</refentrytitle><manvolnum>5</manvolnum></citerefentry>.
+          </para>
 
           <xi:include href="version-info.xml" xpointer="v256"/>
         </listitem>
index 4c7d837c412c76609029830888c37707cd8c0ee1..57c3923c1b1764b755ba77c8ce64f010fe8be59c 100644 (file)
@@ -12,6 +12,7 @@
 #include "sd-resolve.h"
 
 #include "alloc-util.h"
+#include "creds-util.h"
 #include "dns-domain.h"
 #include "event-util.h"
 #include "fd-util.h"
@@ -25,6 +26,7 @@
 #include "networkd-util.h"
 #include "parse-helpers.h"
 #include "parse-util.h"
+#include "path-util.h"
 #include "random-util.h"
 #include "resolve-private.h"
 #include "string-util.h"
@@ -480,6 +482,8 @@ static int wireguard_decode_key_and_warn(
                 const char *lvalue) {
 
         _cleanup_(erase_and_freep) void *key = NULL;
+        _cleanup_(erase_and_freep) char *cred = NULL;
+        const char *cred_name;
         size_t len;
         int r;
 
@@ -493,10 +497,22 @@ static int wireguard_decode_key_and_warn(
                 return 0;
         }
 
-        if (!streq(lvalue, "PublicKey"))
+        cred_name = startswith(rvalue, "@");
+        if (cred_name) {
+                r = read_credential(cred_name, (void**) &cred, /* ret_size = */ NULL);
+                if (r == -ENOMEM)
+                        return log_oom();
+                if (r < 0) {
+                        log_syntax(unit, LOG_WARNING, filename, line, r,
+                                   "Failed to read credential for wireguard key (%s=), ignoring assignment: %m",
+                                   lvalue);
+                        return 0;
+                }
+
+        } else if (!streq(lvalue, "PublicKey"))
                 (void) warn_file_is_world_accessible(filename, NULL, unit, line);
 
-        r = unbase64mem_full(rvalue, strlen(rvalue), true, &key, &len);
+        r = unbase64mem_full(cred ?: rvalue, SIZE_MAX, /* secure = */ true, &key, &len);
         if (r == -ENOMEM)
                 return log_oom();
         if (r < 0) {
@@ -721,23 +737,39 @@ int config_parse_wireguard_endpoint(
                 void *data,
                 void *userdata) {
 
-        assert(filename);
-        assert(rvalue);
-        assert(userdata);
-
         Wireguard *w = WIREGUARD(userdata);
         _cleanup_(wireguard_peer_free_or_set_invalidp) WireguardPeer *peer = NULL;
-        _cleanup_free_ char *host = NULL;
-        union in_addr_union addr;
-        const char *p;
+        _cleanup_free_ char *cred = NULL;
+        const char *cred_name, *endpoint;
         uint16_t port;
-        int family, r;
+        int r;
+
+        assert(filename);
+        assert(rvalue);
 
         r = wireguard_peer_new_static(w, filename, section_line, &peer);
         if (r < 0)
                 return log_oom();
 
-        r = in_addr_port_ifindex_name_from_string_auto(rvalue, &family, &addr, &port, NULL, NULL);
+        cred_name = startswith(rvalue, "@");
+        if (cred_name) {
+                r = read_credential(cred_name, (void**) &cred, /* ret_size = */ NULL);
+                if (r == -ENOMEM)
+                        return log_oom();
+                if (r < 0) {
+                        log_syntax(unit, LOG_WARNING, filename, line, r,
+                                   "Failed to read credential for wireguard endpoint, ignoring assignment: %m");
+                        return 0;
+                }
+
+                endpoint = strstrip(cred);
+        } else
+                endpoint = rvalue;
+
+        union in_addr_union addr;
+        int family;
+
+        r = in_addr_port_ifindex_name_from_string_auto(endpoint, &family, &addr, &port, NULL, NULL);
         if (r >= 0) {
                 if (family == AF_INET)
                         peer->endpoint.in = (struct sockaddr_in) {
@@ -761,17 +793,23 @@ int config_parse_wireguard_endpoint(
                 return 0;
         }
 
-        p = strrchr(rvalue, ':');
+        _cleanup_free_ char *host = NULL;
+        const char *p;
+
+        p = strrchr(endpoint, ':');
         if (!p) {
                 log_syntax(unit, LOG_WARNING, filename, line, 0,
                            "Unable to find port of endpoint, ignoring assignment: %s",
-                           rvalue);
+                           rvalue); /* We log the original assignment instead of resolved credential here,
+                                       as the latter might be previously encrypted and we'd expose them in
+                                       unprotected logs otherwise. */
                 return 0;
         }
 
-        host = strndup(rvalue, p - rvalue);
+        host = strndup(endpoint, p - endpoint);
         if (!host)
                 return log_oom();
+        p++;
 
         if (!dns_name_is_valid(host)) {
                 log_syntax(unit, LOG_WARNING, filename, line, 0,
@@ -780,7 +818,6 @@ int config_parse_wireguard_endpoint(
                 return 0;
         }
 
-        p++;
         r = parse_ip_port(p, &port);
         if (r < 0) {
                 log_syntax(unit, LOG_WARNING, filename, line, r,
@@ -1078,6 +1115,55 @@ static int wireguard_peer_verify(WireguardPeer *peer) {
         return 0;
 }
 
+static int wireguard_read_default_key_cred(NetDev *netdev, const char *filename) {
+        Wireguard *w = WIREGUARD(netdev);
+        _cleanup_free_ char *config_name = NULL;
+        int r;
+
+        assert(filename);
+
+        r = path_extract_filename(filename, &config_name);
+        if (r < 0)
+                return log_netdev_error_errno(netdev, r,
+                                              "%s: Failed to extract config name, ignoring network device: %m",
+                                              filename);
+
+        char *p = endswith(config_name, ".netdev");
+        if (!p)
+                /* Fuzzer run? Then we just ignore this device. */
+                return log_netdev_error_errno(netdev, SYNTHETIC_ERRNO(EINVAL),
+                                              "%s: Invalid netdev config name, refusing default key lookup.",
+                                              filename);
+        *p = '\0';
+
+        _cleanup_(erase_and_freep) char *cred = NULL;
+
+        r = read_credential(strjoina("network.wireguard.private.", config_name), (void**) &cred, /* ret_size = */ NULL);
+        if (r < 0)
+                return log_netdev_error_errno(netdev, r,
+                                              "%s: No private key specified and default key isn't available, "
+                                              "ignoring network device: %m",
+                                              filename);
+
+        _cleanup_(erase_and_freep) void *key = NULL;
+        size_t len;
+
+        r = unbase64mem_full(cred, SIZE_MAX, /* secure = */ true, &key, &len);
+        if (r < 0)
+                return log_netdev_error_errno(netdev, r,
+                                              "%s: No private key specified and default key cannot be parsed, "
+                                              "ignoring network device: %m",
+                                              filename);
+        if (len != WG_KEY_LEN)
+                return log_netdev_error_errno(netdev, SYNTHETIC_ERRNO(EINVAL),
+                                              "%s: No private key specified and default key is invalid. "
+                                              "Ignoring network device.",
+                                              filename);
+
+        memcpy(w->private_key, key, WG_KEY_LEN);
+        return 0;
+}
+
 static int wireguard_verify(NetDev *netdev, const char *filename) {
         Wireguard *w = WIREGUARD(netdev);
         int r;
@@ -1088,10 +1174,11 @@ static int wireguard_verify(NetDev *netdev, const char *filename) {
                                               "Failed to read private key from %s. Ignoring network device.",
                                               w->private_key_file);
 
-        if (eqzero(w->private_key))
-                return log_netdev_error_errno(netdev, SYNTHETIC_ERRNO(EINVAL),
-                                              "%s: Missing PrivateKey= or PrivateKeyFile=, "
-                                              "Ignoring network device.", filename);
+        if (eqzero(w->private_key)) {
+                r = wireguard_read_default_key_cred(netdev, filename);
+                if (r < 0)
+                        return r;
+        }
 
         LIST_FOREACH(peers, peer, w->peers) {
                 if (wireguard_peer_verify(peer) < 0) {
diff --git a/test/test-network/conf/25-wireguard-endpoint-peer0-cred.txt b/test/test-network/conf/25-wireguard-endpoint-peer0-cred.txt
new file mode 100644 (file)
index 0000000..b4251c3
--- /dev/null
@@ -0,0 +1 @@
+192.168.27.3:51820
diff --git a/test/test-network/conf/25-wireguard-no-peer-private-key-cred.txt b/test/test-network/conf/25-wireguard-no-peer-private-key-cred.txt
new file mode 100644 (file)
index 0000000..8011c64
--- /dev/null
@@ -0,0 +1 @@
+EEGlnEPYJV//kbvvIqxKkQwOiS+UENyPncC4bF46ong=
index ce3b31a5cecc134bb3254a25bff5863fd38193bf..8c90735bc7657bec6bbbe6b9a7c92f007c123122 100644 (file)
@@ -4,6 +4,6 @@ Name=wg97
 Kind=wireguard
 
 [WireGuard]
-PrivateKey=EEGlnEPYJV//kbvvIqxKkQwOiS+UENyPncC4bF46ong=
+#PrivateKey=EEGlnEPYJV//kbvvIqxKkQwOiS+UENyPncC4bF46ong=
 ListenPort=51821
 FwMark=1235
diff --git a/test/test-network/conf/25-wireguard-preshared-key-peer2-cred.txt b/test/test-network/conf/25-wireguard-preshared-key-peer2-cred.txt
new file mode 100644 (file)
index 0000000..5e79c19
--- /dev/null
@@ -0,0 +1 @@
+6Fsg8XN0DE6aPQgAX4r2oazEYJOGqyHUz3QRH/jCB+I=
index 4fed38e57a1ea2ef89a37ff2239793b04b0ec1d5..6a2bb88c2e88090a6df8b739e8cca6e956b894b9 100644 (file)
@@ -13,8 +13,8 @@ RouteMetric=456
 [WireGuardPeer]
 PublicKey=RDf+LSpeEre7YEIKaxg+wbpsNV7du+ktR99uBEtIiCA=
 AllowedIPs=fd31:bf08:57cb::/48,192.168.26.3/24
-#Endpoint=wireguard.example.com:51820
-Endpoint=192.168.27.3:51820
+#Endpoint=192.168.27.3:51820
+Endpoint=@network.wireguard.peer0.endpoint
 PresharedKey=IIWIV17wutHv7t4cR6pOT91z6NSz/T8Arh0yaywhw3M=
 PersistentKeepalive=20
 RouteTable=1234
index bf99a5ab0f8967f16c22a1a64446b636e878d044..f3440df28f163658abc5a29663b9635213b17f3a 100644 (file)
@@ -1,5 +1,5 @@
 [WireGuardPeer]
 PublicKey=9uioxkGzjvGjkse3V35I9AhorWfIjBcrf3UPMS0bw2c=
-PresharedKey=6Fsg8XN0DE6aPQgAX4r2oazEYJOGqyHUz3QRH/jCB+I=
+PresharedKey=@network.wireguard.peer2.psk
 
 AllowedIPs=192.168.124.3
index 491dcca9fa3b0d741885aef8a332e96f9f4e8726..b122e0a491b93ae563eb0bef509cbfb072875094 100755 (executable)
@@ -27,6 +27,7 @@ network_unit_dir = '/run/systemd/network'
 networkd_conf_dropin_dir = '/run/systemd/networkd.conf.d'
 networkd_ci_temp_dir = '/run/networkd-ci'
 udev_rules_dir = '/run/udev/rules.d'
+credstore_dir = '/run/credstore'
 
 dnsmasq_pid_file = '/run/networkd-ci/test-dnsmasq.pid'
 dnsmasq_log_file = '/run/networkd-ci/test-dnsmasq.log'
@@ -298,6 +299,11 @@ def copy_network_unit(*units, copy_dropins=True):
     if has_link:
         udev_reload()
 
+def copy_credential(src, target):
+        mkdir_p(credstore_dir)
+        cp(os.path.join(networkd_ci_temp_dir, src),
+           os.path.join(credstore_dir, target))
+
 def remove_network_unit(*units):
     """
     Remove previously copied unit files from the testbed.
@@ -1784,6 +1790,10 @@ class NetworkdNetDevTests(unittest.TestCase, Utilities):
 
     @expectedFailureIfModuleIsNotAvailable('wireguard')
     def test_wireguard(self):
+        copy_credential('25-wireguard-endpoint-peer0-cred.txt', 'network.wireguard.peer0.endpoint')
+        copy_credential('25-wireguard-preshared-key-peer2-cred.txt', 'network.wireguard.peer2.psk')
+        copy_credential('25-wireguard-no-peer-private-key-cred.txt', 'network.wireguard.private.25-wireguard-no-peer')
+
         copy_network_unit('25-wireguard.netdev', '25-wireguard.network',
                           '25-wireguard-23-peers.netdev', '25-wireguard-23-peers.network',
                           '25-wireguard-preshared-key.txt', '25-wireguard-private-key.txt',
index 3608458aa57aff363e905107f34e64568e9dbfa2..32b6e9fa2ffed33562b84cf031503b63d25d99e1 100644 (file)
@@ -50,6 +50,7 @@ SystemCallErrorNumber=EPERM
 SystemCallFilter=@system-service
 Type=notify-reload
 User=systemd-network
+ImportCredential=network.wireguard.*
 {{SERVICE_WATCHDOG}}
 
 [Install]