nixos/system/boot/initrd-openvpn: Add openvpn options for initrd

nixos/tests/initrd-openvpn: Add test for openvpn in the initramfs

The module in this commit adds new options that allows the
integration of an OpenVPN client into the initrd.
This can be used e.g. to remotely unlock LUKS devices.

This commit also adds two tests for `boot.initrd.network.openvpn`.
The first one is a basic test to validate that a failing connection
does not prevent the machine from booting.

The second test validates that this module actually creates a valid
openvpn connection.
For this, it spawns three nodes:

  - The client that uses boot.initrd.network.openvpn
  - An OpenVPN server that acts as gateway and forwards a port
    to the client
  - A node that is external to the OpenVPN network

The client connects to the OpenVPN server and spawns a netcat instance
that echos a value to every client.
Afterwards, the external node checks if it receives this value over the
forwarded port on the OpenVPN gateway.
gstqt5
CRTified 2020-07-01 00:02:56 +02:00
parent db5bbef31f
commit c684398c6a
6 changed files with 278 additions and 0 deletions

View File

@ -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

View File

@ -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
<option>boot.initrd.network.ssh</option> 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.
<warning>
<para>
Unless your bootloader supports initrd secrets, this configuration
is stored insecurely in the global Nix store.
</para>
</warning>
'';
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 &
'';
};
}

View File

@ -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 {};

View File

@ -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]
<secret>
${lib.readFile ./shared.key}
</secret>
'';
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")
'';
})

View File

@ -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]
<secret>
#
# 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-----
</secret>

View File

@ -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-----