From fa724cd52c1335d6b3225b74c1a9c801389997ba Mon Sep 17 00:00:00 2001 From: Mike Yuan Date: Wed, 27 Dec 2023 21:38:32 +0800 Subject: [PATCH] networkd/wireguard: support network.wireguard.* credentials Closes #26702 --- man/systemd.netdev.xml | 56 +++++--- man/systemd.system-credentials.xml | 16 ++- src/network/netdev/wireguard.c | 125 +++++++++++++++--- .../conf/25-wireguard-endpoint-peer0-cred.txt | 1 + .../25-wireguard-no-peer-private-key-cred.txt | 1 + .../conf/25-wireguard-no-peer.netdev | 2 +- .../25-wireguard-preshared-key-peer2-cred.txt | 1 + test/test-network/conf/25-wireguard.netdev | 4 +- .../conf/25-wireguard.netdev.d/peer2.conf | 2 +- test/test-network/systemd-networkd-tests.py | 10 ++ units/systemd-networkd.service.in | 1 + 11 files changed, 173 insertions(+), 46 deletions(-) create mode 100644 test/test-network/conf/25-wireguard-endpoint-peer0-cred.txt create mode 100644 test/test-network/conf/25-wireguard-no-peer-private-key-cred.txt create mode 100644 test/test-network/conf/25-wireguard-preshared-key-peer2-cred.txt diff --git a/man/systemd.netdev.xml b/man/systemd.netdev.xml index cd77e725bc..bf3b5c21da 100644 --- a/man/systemd.netdev.xml +++ b/man/systemd.netdev.xml @@ -1891,13 +1891,22 @@ PrivateKey= - The Base64 encoded private key for the interface. It can be - generated using the wg genkey command + The Base64 encoded private key for the interface. It can be generated using + the wg genkey command (see wg8). - This option or PrivateKeyFile= 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 root:systemd-network - with a 0640 file mode. + Specially, if the specified key is prefixed with @, it is interpreted as + the name of the credential from which the actual key shall be read. systemd-networkd.service + automatically imports credentials matching network.wireguard.*. For more details + on credentials, refer to + systemd.exec5. + A private key is mandatory to use WireGuard. If not set, the credential + network.wireguard.private.netdev is used if exists. + I.e. for 50-foobar.netdev, network.wireguard.private.50-foobar + is tried. + + 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 root:systemd-network with a 0640 file mode. @@ -1976,9 +1985,9 @@ Sets a Base64 encoded public key calculated by wg pubkey (see wg8) - from a private key, and usually transmitted out of band to the - author of the configuration file. This option is mandatory for this - section. + from a private key, and usually transmitted out of band to the author of the configuration file. + This option honors the @ prefix in the same way as the + setting of the section. This option is mandatory for this section. @@ -1986,14 +1995,15 @@ PresharedKey= - Optional preshared key for the interface. It can be generated - by the wg genpsk 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 root:systemd-network - with a 0640 file mode. + Optional preshared key for the interface. It can be generated by the wg genpsk + 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 @ prefix in the same way as the + setting of the section. + + 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 root:systemd-network with a 0640 file mode. @@ -2034,13 +2044,15 @@ Endpoint= - 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, - 111.222.333.444:51820 for IPv4 and [1111:2222::3333]:51820 - for IPv6 address. This endpoint will be updated automatically once to - the most recent source IP address and port of correctly + 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, 111.222.333.444:51820 + for IPv4 and [1111:2222::3333]:51820 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. + This option honors the @ prefix in the same way as the + setting of the section. + diff --git a/man/systemd.system-credentials.xml b/man/systemd.system-credentials.xml index b2d491fe58..eb4c94c47f 100644 --- a/man/systemd.system-credentials.xml +++ b/man/systemd.system-credentials.xml @@ -155,7 +155,21 @@ 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 - systemd-networkd.service. + systemd-networkd.service, e.g. network.wireguard.* + as described below. + + + + + + + network.wireguard.* + + Configures secrets for WireGuard netdevs. Read by + systemd-networkd.service8. + For more information, refer to the section of + systemd.netdev5. + diff --git a/src/network/netdev/wireguard.c b/src/network/netdev/wireguard.c index 4c7d837c41..57c3923c1b 100644 --- a/src/network/netdev/wireguard.c +++ b/src/network/netdev/wireguard.c @@ -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 index 0000000000..b4251c3dfd --- /dev/null +++ b/test/test-network/conf/25-wireguard-endpoint-peer0-cred.txt @@ -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 index 0000000000..8011c64115 --- /dev/null +++ b/test/test-network/conf/25-wireguard-no-peer-private-key-cred.txt @@ -0,0 +1 @@ +EEGlnEPYJV//kbvvIqxKkQwOiS+UENyPncC4bF46ong= diff --git a/test/test-network/conf/25-wireguard-no-peer.netdev b/test/test-network/conf/25-wireguard-no-peer.netdev index ce3b31a5ce..8c90735bc7 100644 --- a/test/test-network/conf/25-wireguard-no-peer.netdev +++ b/test/test-network/conf/25-wireguard-no-peer.netdev @@ -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 index 0000000000..5e79c1914c --- /dev/null +++ b/test/test-network/conf/25-wireguard-preshared-key-peer2-cred.txt @@ -0,0 +1 @@ +6Fsg8XN0DE6aPQgAX4r2oazEYJOGqyHUz3QRH/jCB+I= diff --git a/test/test-network/conf/25-wireguard.netdev b/test/test-network/conf/25-wireguard.netdev index 4fed38e57a..6a2bb88c2e 100644 --- a/test/test-network/conf/25-wireguard.netdev +++ b/test/test-network/conf/25-wireguard.netdev @@ -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 diff --git a/test/test-network/conf/25-wireguard.netdev.d/peer2.conf b/test/test-network/conf/25-wireguard.netdev.d/peer2.conf index bf99a5ab0f..f3440df28f 100644 --- a/test/test-network/conf/25-wireguard.netdev.d/peer2.conf +++ b/test/test-network/conf/25-wireguard.netdev.d/peer2.conf @@ -1,5 +1,5 @@ [WireGuardPeer] PublicKey=9uioxkGzjvGjkse3V35I9AhorWfIjBcrf3UPMS0bw2c= -PresharedKey=6Fsg8XN0DE6aPQgAX4r2oazEYJOGqyHUz3QRH/jCB+I= +PresharedKey=@network.wireguard.peer2.psk AllowedIPs=192.168.124.3 diff --git a/test/test-network/systemd-networkd-tests.py b/test/test-network/systemd-networkd-tests.py index 491dcca9fa..b122e0a491 100755 --- a/test/test-network/systemd-networkd-tests.py +++ b/test/test-network/systemd-networkd-tests.py @@ -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', diff --git a/units/systemd-networkd.service.in b/units/systemd-networkd.service.in index 3608458aa5..32b6e9fa2f 100644 --- a/units/systemd-networkd.service.in +++ b/units/systemd-networkd.service.in @@ -50,6 +50,7 @@ SystemCallErrorNumber=EPERM SystemCallFilter=@system-service Type=notify-reload User=systemd-network +ImportCredential=network.wireguard.* {{SERVICE_WATCHDOG}} [Install]