diff --git a/nixos/modules/module-list.nix b/nixos/modules/module-list.nix
index cf25ae3157e6..2ba034735a33 100644
--- a/nixos/modules/module-list.nix
+++ b/nixos/modules/module-list.nix
@@ -937,6 +937,7 @@
./system/boot/grow-partition.nix
./system/boot/initrd-network.nix
./system/boot/initrd-ssh.nix
+ ./system/boot/initrd-openvpn.nix
./system/boot/kernel.nix
./system/boot/kexec.nix
./system/boot/loader/efi.nix
diff --git a/nixos/modules/system/boot/initrd-openvpn.nix b/nixos/modules/system/boot/initrd-openvpn.nix
new file mode 100644
index 000000000000..7553c2aebb10
--- /dev/null
+++ b/nixos/modules/system/boot/initrd-openvpn.nix
@@ -0,0 +1,81 @@
+{ config, lib, pkgs, ... }:
+
+with lib;
+
+let
+
+ cfg = config.boot.initrd.network.openvpn;
+
+in
+
+{
+
+ options = {
+
+ boot.initrd.network.openvpn.enable = mkOption {
+ type = types.bool;
+ default = false;
+ description = ''
+ Starts an OpenVPN client during initrd boot. It can be used to e.g.
+ remotely accessing the SSH service controlled by
+ or other network services
+ included. Service is killed when stage-1 boot is finished.
+ '';
+ };
+
+ boot.initrd.network.openvpn.configuration = mkOption {
+ type = types.path; # Same type as boot.initrd.secrets
+ description = ''
+ The configuration file for OpenVPN.
+
+
+
+ Unless your bootloader supports initrd secrets, this configuration
+ is stored insecurely in the global Nix store.
+
+
+ '';
+ example = "./configuration.ovpn";
+ };
+
+ };
+
+ config = mkIf (config.boot.initrd.network.enable && cfg.enable) {
+ assertions = [
+ {
+ assertion = cfg.configuration != null;
+ message = "You should specify a configuration for initrd OpenVPN";
+ }
+ ];
+
+ # Add kernel modules needed for OpenVPN
+ boot.initrd.kernelModules = [ "tun" "tap" ];
+
+ # Add openvpn and ip binaries to the initrd
+ # The shared libraries are required for DNS resolution
+ boot.initrd.extraUtilsCommands = ''
+ copy_bin_and_libs ${pkgs.openvpn}/bin/openvpn
+ copy_bin_and_libs ${pkgs.iproute}/bin/ip
+
+ cp -pv ${pkgs.glibc}/lib/libresolv.so.2 $out/lib
+ cp -pv ${pkgs.glibc}/lib/libnss_dns.so.2 $out/lib
+ '';
+
+ boot.initrd.secrets = {
+ "/etc/initrd.ovpn" = cfg.configuration;
+ };
+
+ # openvpn --version would exit with 1 instead of 0
+ boot.initrd.extraUtilsCommandsTest = ''
+ $out/bin/openvpn --show-gateway
+ '';
+
+ # Add `iproute /bin/ip` to the config, to ensure that openvpn
+ # is able to set the routes
+ boot.initrd.network.postCommands = ''
+ (cat /etc/initrd.ovpn; echo -e '\niproute /bin/ip') | \
+ openvpn /dev/stdin &
+ '';
+ };
+
+}
diff --git a/nixos/tests/all-tests.nix b/nixos/tests/all-tests.nix
index debc60a21d01..94fba23675df 100644
--- a/nixos/tests/all-tests.nix
+++ b/nixos/tests/all-tests.nix
@@ -150,6 +150,7 @@ in
incron = handleTest ./incron.nix {};
influxdb = handleTest ./influxdb.nix {};
initrd-network-ssh = handleTest ./initrd-network-ssh {};
+ initrd-network-openvpn = handleTest ./initrd-network-openvpn {};
initrdNetwork = handleTest ./initrd-network.nix {};
installer = handleTest ./installer.nix {};
iodine = handleTest ./iodine.nix {};
diff --git a/nixos/tests/initrd-network-openvpn/default.nix b/nixos/tests/initrd-network-openvpn/default.nix
new file mode 100644
index 000000000000..bb4c41e6d709
--- /dev/null
+++ b/nixos/tests/initrd-network-openvpn/default.nix
@@ -0,0 +1,145 @@
+import ../make-test-python.nix ({ lib, ...}:
+
+{
+ name = "initrd-network-openvpn";
+
+ nodes =
+ let
+
+ # Inlining of the shared secret for the
+ # OpenVPN server and client
+ secretblock = ''
+ secret [inline]
+
+ ${lib.readFile ./shared.key}
+
+ '';
+
+ in
+ {
+
+ # Minimal test case to check a successful boot, even with invalid config
+ minimalboot =
+ { ... }:
+ {
+ boot.initrd.network = {
+ enable = true;
+ openvpn = {
+ enable = true;
+ configuration = "/dev/null";
+ };
+ };
+ };
+
+ # initrd VPN client
+ ovpnclient =
+ { ... }:
+ {
+ virtualisation.useBootLoader = true;
+ virtualisation.vlans = [ 1 ];
+
+ boot.initrd = {
+ # This command does not fork to keep the VM in the state where
+ # only the initramfs is loaded
+ preLVMCommands =
+ ''
+ /bin/nc -p 1234 -lke /bin/echo TESTVALUE
+ '';
+
+ network = {
+ enable = true;
+
+ # Work around udhcpc only getting a lease on eth0
+ postCommands = ''
+ /bin/ip addr add 192.168.1.2/24 dev eth1
+ '';
+
+ # Example configuration for OpenVPN
+ # This is the main reason for this test
+ openvpn = {
+ enable = true;
+ configuration = "${./initrd.ovpn}";
+ };
+ };
+ };
+ };
+
+ # VPN server and gateway for ovpnclient between vlan 1 and 2
+ ovpnserver =
+ { ... }:
+ {
+ virtualisation.vlans = [ 1 2 ];
+
+ # Enable NAT and forward port 12345 to port 1234
+ networking.nat = {
+ enable = true;
+ internalInterfaces = [ "tun0" ];
+ externalInterface = "eth2";
+ forwardPorts = [ { destination = "10.8.0.2:1234";
+ sourcePort = 12345; } ];
+ };
+
+ # Trust tun0 and allow the VPN Server to be reached
+ networking.firewall = {
+ trustedInterfaces = [ "tun0" ];
+ allowedUDPPorts = [ 1194 ];
+ };
+
+ # Minimal OpenVPN server configuration
+ services.openvpn.servers.testserver =
+ {
+ config = ''
+ dev tun0
+ ifconfig 10.8.0.1 10.8.0.2
+ ${secretblock}
+ '';
+ };
+ };
+
+ # Client that resides in the "external" VLAN
+ testclient =
+ { ... }:
+ {
+ virtualisation.vlans = [ 2 ];
+ };
+ };
+
+
+ testScript =
+ ''
+ # Minimal test case, checks whether enabling (with invalid config) harms
+ # the boot process
+ with subtest("Check for successful boot with broken openvpn config"):
+ minimalboot.start()
+ # If we get to multi-user.target, we booted successfully
+ minimalboot.wait_for_unit("multi-user.target")
+ minimalboot.shutdown()
+
+ # Elaborated test case where the ovpnclient (where this module is used)
+ # can be reached by testclient only over ovpnserver.
+ # This is an indirect test for success.
+ with subtest("Check for connection from initrd VPN client, config as file"):
+ ovpnserver.start()
+ testclient.start()
+ ovpnclient.start()
+
+ # Wait until the OpenVPN Server is available
+ ovpnserver.wait_for_unit("openvpn-testserver.service")
+ ovpnserver.succeed("ping -c 1 10.8.0.1")
+
+ # Wait for the client to connect
+ ovpnserver.wait_until_succeeds("ping -c 1 10.8.0.2")
+
+ # Wait until the testclient has network
+ testclient.wait_for_unit("network.target")
+
+ # Check that ovpnclient is reachable over vlan 1
+ ovpnserver.succeed("nc -w 2 192.168.1.2 1234 | grep -q TESTVALUE")
+
+ # Check that ovpnclient is reachable over tun0
+ ovpnserver.succeed("nc -w 2 10.8.0.2 1234 | grep -q TESTVALUE")
+
+ # Check that ovpnclient is reachable from testclient over the gateway
+ testclient.succeed("nc -w 2 192.168.2.3 12345 | grep -q TESTVALUE")
+ '';
+})
diff --git a/nixos/tests/initrd-network-openvpn/initrd.ovpn b/nixos/tests/initrd-network-openvpn/initrd.ovpn
new file mode 100644
index 000000000000..5926a48af00f
--- /dev/null
+++ b/nixos/tests/initrd-network-openvpn/initrd.ovpn
@@ -0,0 +1,29 @@
+remote 192.168.1.3
+dev tun
+ifconfig 10.8.0.2 10.8.0.1
+# Only force VLAN 2 through the VPN
+route 192.168.2.0 255.255.255.0 10.8.0.1
+secret [inline]
+
+#
+# 2048 bit OpenVPN static key
+#
+-----BEGIN OpenVPN Static key V1-----
+553aabe853acdfe51cd6fcfea93dcbb0
+c8797deadd1187606b1ea8f2315eb5e6
+67c0d7e830f50df45686063b189d6c6b
+aab8bb3430cc78f7bb1f78628d5c3742
+0cef4f53a5acab2894905f4499f95d8e
+e69b7b6748b17016f89e19e91481a9fd
+bf8c10651f41a1d4fdf5f438925a6733
+13cec8f04701eb47b8f7ffc48bc3d7af
+65f07bce766015b87c3db4d668c655ff
+be5a69522a8e60ccb217f8521681b45d
+27c0b70bdfbfbb426c7646d80adf7482
+3ddac58b25cb1c1bb100de974478b4c6
+8b45a94261a2405e99810cb2b3abd49f
+21b3198ada87ff3c4e656a008e540a8d
+e7811584363597599cce2040a68ac00e
+f2125540e0f7f4adc37cb3f0d922eeb7
+-----END OpenVPN Static key V1-----
+
\ No newline at end of file
diff --git a/nixos/tests/initrd-network-openvpn/shared.key b/nixos/tests/initrd-network-openvpn/shared.key
new file mode 100644
index 000000000000..248a91a3e3d5
--- /dev/null
+++ b/nixos/tests/initrd-network-openvpn/shared.key
@@ -0,0 +1,21 @@
+#
+# 2048 bit OpenVPN static key
+#
+-----BEGIN OpenVPN Static key V1-----
+553aabe853acdfe51cd6fcfea93dcbb0
+c8797deadd1187606b1ea8f2315eb5e6
+67c0d7e830f50df45686063b189d6c6b
+aab8bb3430cc78f7bb1f78628d5c3742
+0cef4f53a5acab2894905f4499f95d8e
+e69b7b6748b17016f89e19e91481a9fd
+bf8c10651f41a1d4fdf5f438925a6733
+13cec8f04701eb47b8f7ffc48bc3d7af
+65f07bce766015b87c3db4d668c655ff
+be5a69522a8e60ccb217f8521681b45d
+27c0b70bdfbfbb426c7646d80adf7482
+3ddac58b25cb1c1bb100de974478b4c6
+8b45a94261a2405e99810cb2b3abd49f
+21b3198ada87ff3c4e656a008e540a8d
+e7811584363597599cce2040a68ac00e
+f2125540e0f7f4adc37cb3f0d922eeb7
+-----END OpenVPN Static key V1-----