feat: wip: add container backend to guests

This commit is contained in:
oddlama 2023-12-17 02:04:20 +01:00
parent 83f1908e21
commit abb8330d86
No known key found for this signature in database
GPG key ID: 14EFE510775FE39A
23 changed files with 256 additions and 208 deletions

View file

@ -30,7 +30,7 @@ Server related stuff:
- [loki](https://github.com/grafana/loki) and [promtail](https://grafana.com/docs/loki/latest/clients/promtail/) for logs - [loki](https://github.com/grafana/loki) and [promtail](https://grafana.com/docs/loki/latest/clients/promtail/) for logs
- Single-Sign-On for all services using oauth2 via [kanidm](https://github.com/kanidm/kanidm) - Single-Sign-On for all services using oauth2 via [kanidm](https://github.com/kanidm/kanidm)
- Zoned nftables firewall via [nixos-nftables-firewall](https://github.com/thelegy/nixos-nftables-firewall) - Zoned nftables firewall via [nixos-nftables-firewall](https://github.com/thelegy/nixos-nftables-firewall)
- Service isolation using [microvms](https://github.com/astro/microvm.nix) <!-- XXX: where possible, otherwise oci-containers --> - Service isolation using nixos-containers and [microvms](https://github.com/astro/microvm.nix)
<!-- <!--
XXX: todo, use details summary to show gallery of services XXX: todo, use details summary to show gallery of services
@ -43,7 +43,7 @@ XXX: todo, use details summary to show gallery of services
---|---|---|--- ---|---|---|---
💻 | nom | Gigabyte AERO 15-W8 (i7-8750H) | My laptop and my main portable development machine <sub>Framework when?</sub> 💻 | nom | Gigabyte AERO 15-W8 (i7-8750H) | My laptop and my main portable development machine <sub>Framework when?</sub>
🖥️ | kroma | PC (AMD Ryzen 9 5900X) | Main workstation and development machine, also for some occasional gaming 🖥️ | kroma | PC (AMD Ryzen 9 5900X) | Main workstation and development machine, also for some occasional gaming
🖥️ | ward | ODROID H3 | Energy efficient SBC for my home firewall and some lightweight services using microvms. 🖥️ | ward | ODROID H3 | Energy efficient SBC for my home firewall and some lightweight services using containers and microvms.
🥔 | zackbiene | ODROID N2+ | ARM SBC for home automation, isolating the sketchy stuff from my main network 🥔 | zackbiene | ODROID N2+ | ARM SBC for home automation, isolating the sketchy stuff from my main network
☁️ | envoy | Hetzner Cloud server | Mailserver ☁️ | envoy | Hetzner Cloud server | Mailserver
☁️ | sentinel | Hetzner Cloud server | Proxies and protects my local services ☁️ | sentinel | Hetzner Cloud server | Proxies and protects my local services
@ -123,7 +123,7 @@ Afterwards:
- Run `install-system` in the live environment and reboot - Run `install-system` in the live environment and reboot
- Retrieve the new host identity by using `ssh-keyscan <host/ip> | grep -o 'ssh-ed25519.*' > hosts/<host>/secrets/host.pub` - Retrieve the new host identity by using `ssh-keyscan <host/ip> | grep -o 'ssh-ed25519.*' > hosts/<host>/secrets/host.pub`
- (If the host has microvms, also retrieve their identities!) - (If the host has guests, also retrieve their identities!)
- Rekey the secrets for the new identity `nix run .#rekey` - Rekey the secrets for the new identity `nix run .#rekey`
- Deploy again - Deploy again

View file

@ -13,8 +13,8 @@ Make sure to utilize the github search if you know what you need!
- `host.pub` This host's public key (retrieved after initial setup). Used to rekey secrets so the host can access them at runtime. - `host.pub` This host's public key (retrieved after initial setup). Used to rekey secrets so the host can access them at runtime.
- `local.nix.age` Repository-wide local secrets. Decrypted on import, see `modules/repo/secrets.nix` for more information. - `local.nix.age` Repository-wide local secrets. Decrypted on import, see `modules/repo/secrets.nix` for more information.
Some hosts define microvms that run as virtualized guests. Their configuration is usually just a single file Some hosts define guests that run as containerized or virtualized guests. Their configuration is usually just a single file
stored in `microvms/<vm>.nix`. Their secrets are usually stored in a subfolder of the host's secrets. stored in `guests/<name>.nix`. Their secrets are usually stored in a subfolder of the host's secrets folder.
- `lib/` contains extra library functions that are needed throughout the config. - `lib/` contains extra library functions that are needed throughout the config.

View file

@ -141,14 +141,14 @@
inherit inherit
(import ./nix/hosts.nix inputs) (import ./nix/hosts.nix inputs)
hosts hosts
microvmConfigurations guestConfigs
nixosConfigurations nixosConfigurations
nixosConfigurationsMinimal nixosConfigurationsMinimal
; ;
# All nixosSystem instanciations are collected here, so that we can refer # All nixosSystem instanciations are collected here, so that we can refer
# to any system via nodes.<name> # to any system via nodes.<name>
nodes = self.nixosConfigurations // self.microvmConfigurations; nodes = self.nixosConfigurations // self.guestConfigs;
# Add a shorthand to easily target toplevel derivations # Add a shorthand to easily target toplevel derivations
"@" = mapAttrs (_: v: v.config.system.build.toplevel) self.nodes; "@" = mapAttrs (_: v: v.config.system.build.toplevel) self.nodes;

View file

@ -43,17 +43,13 @@
# TODO track my github stats # TODO track my github stats
# services.telegraf.extraConfig.inputs.github = {}; # services.telegraf.extraConfig.inputs.github = {};
meta.microvms.commonImports = [
../../modules
./microvms/common.nix
];
#guests.adguardhome = { #guests.adguardhome = {
# backend = "microvm"; # backend = "microvm";
# microvm = { # microvm = {
# system = "x86_64-linux"; # system = "x86_64-linux";
# autostart = true; # macvtapInterface = "lan";
# }; # };
# autostart = true;
# zfs = { # zfs = {
# enable = true; # enable = true;
# pool = "rpool"; # pool = "rpool";
@ -62,53 +58,47 @@
#}; #};
guests = let guests = let
mkMicrovm = system: module: { mkGuest = mainModule: {
backend = "microvm";
microvm = {
system = "x86_64-linux";
autostart = true;
};
zfs = {
enable = true;
pool = "rpool";
};
modules = [
../../modules
module
];
};
in {
adguardhome = mkMicrovm "x86_64-linux" ./guests/adguardhome.nix;
};
meta.microvms.vms = let
defaultConfig = name: {
system = "x86_64-linux";
autostart = true; autostart = true;
zfs = { zfs = {
enable = true; enable = true;
pool = "rpool"; pool = "rpool";
}; };
modules = [ modules = [
# XXX: this could be interpolated in-place but statix has a bug https://github.com/nerdypepper/statix/issues/75 ../../modules
(./microvms + "/${name}.nix") ./guests/common.nix
{node.secretsDir = ./secrets + "/${name}";} ({config, ...}: {node.secretsDir = ./secrets + "/${config.node.name}";})
mainModule
]; ];
}; };
mkMicrovm = system: mainModule:
mkGuest mainModule
// {
backend = "microvm";
microvm = {
system = "x86_64-linux";
macvtapInterface = "lan";
};
};
mkContainer = mainModule:
mkGuest mainModule
// {
backend = "container";
container.macvlan = "lan";
};
in in
lib.mkIf (!minimal) ( lib.mkIf (!minimal) {
lib.genAttrs [ adguardhome = mkContainer ./guests/adguardhome.nix;
"adguardhome" forgejo = mkContainer ./guests/forgejo.nix;
"forgejo" grafana = mkContainer ./guests/grafana.nix;
"grafana" influxdb = mkContainer ./guests/influxdb.nix;
"influxdb" kanidm = mkContainer ./guests/kanidm.nix;
"kanidm" loki = mkContainer ./guests/loki.nix;
"loki" paperless = mkContainer ./guests/paperless.nix;
"paperless" vaultwarden = mkContainer ./guests/vaultwarden.nix;
"vaultwarden" };
]
defaultConfig
);
#ddclient = defineVm; #ddclient = defineVm;
#samba+wsdd = defineVm; #samba+wsdd = defineVm;

View file

@ -6,9 +6,8 @@
sentinelCfg = nodes.sentinel.config; sentinelCfg = nodes.sentinel.config;
paperlessDomain = "paperless.${sentinelCfg.repo.secrets.local.personalDomain}"; paperlessDomain = "paperless.${sentinelCfg.repo.secrets.local.personalDomain}";
in { in {
microvm.mem = 1024 * 12; # XXX: remove microvm.mem = 1024 * 12;
# XXX: increase once real hardware is used # XXX: remove microvm.vcpu = 4;
microvm.vcpu = 4;
meta.wireguard-proxy.sentinel.allowedTCPPorts = [ meta.wireguard-proxy.sentinel.allowedTCPPorts = [
config.services.paperless.port config.services.paperless.port

View file

@ -45,12 +45,12 @@ in {
data = net.cidr.host 1 lanCidrv4; data = net.cidr.host 1 lanCidrv4;
} }
]; ];
reservations = [ # TODO reservations = [
{ # TODO {
hw-address = nodes.ward-adguardhome.config.lib.microvm.mac; # TODO hw-address = nodes.ward-adguardhome.config.lib.microvm.mac;
ip-address = dnsIp; # TODO ip-address = dnsIp;
} # TODO }
]; # TODO ];
} }
]; ];
}; };

View file

@ -124,11 +124,6 @@ in {
}; };
}; };
meta.microvms.networking = {
baseMac = config.repo.secrets.local.networking.interfaces.lan.mac;
macvtapInterface = "lan";
};
# Allow accessing influx # Allow accessing influx
meta.wireguard.proxy-sentinel.client.via = "sentinel"; meta.wireguard.proxy-sentinel.client.via = "sentinel";
} }

View file

@ -9,6 +9,6 @@
''This is \e{cyan}\n\e{reset} [\e{lightblue}\l\e{reset}] (\s \m \r)'' ''This is \e{cyan}\n\e{reset} [\e{lightblue}\l\e{reset}] (\s \m \r)''
] ]
# Disabled for guests because of frequent redraws (-> pushed to syslog on the host) # Disabled for guests because of frequent redraws (-> pushed to syslog on the host)
++ lib.optional (!config.guests.isGuest) ''\e{halfbright}\4\e{reset} \e{halfbright}\6\e{reset}'' ++ lib.optional (config.node.type == "host") ''\e{halfbright}\4\e{reset} \e{halfbright}\6\e{reset}''
++ [""]); ++ [""]);
} }

View file

@ -25,8 +25,9 @@
./config/system.nix ./config/system.nix
./config/users.nix ./config/users.nix
./guests
./meta/kanidm.nix ./meta/kanidm.nix
./meta/microvms.nix
./meta/nginx.nix ./meta/nginx.nix
./meta/oauth2-proxy.nix ./meta/oauth2-proxy.nix
./meta/promtail.nix ./meta/promtail.nix

View file

@ -0,0 +1,31 @@
_guestName: guestCfg: {lib, ...}: let
inherit (lib) mkForce;
in {
node.name = guestCfg.nodeName;
node.type = guestCfg.backend;
nix = {
settings.auto-optimise-store = mkForce false;
optimise.automatic = mkForce false;
gc.automatic = mkForce false;
};
systemd.network.networks = {
"10-${guestCfg.networking.mainLinkName}" = {
DHCP = "yes";
dhcpV4Config.UseDNS = false;
dhcpV6Config.UseDNS = false;
ipv6AcceptRAConfig.UseDNS = false;
networkConfig = {
IPv6PrivacyExtensions = "yes";
MulticastDNS = true;
IPv6AcceptRA = true;
};
linkConfig.RequiredForOnline = "routable";
};
};
networking.nftables.firewall = {
zones.untrusted.interfaces = [guestCfg.networking.mainLinkName];
};
}

View file

@ -0,0 +1,29 @@
guestName: guestCfg: {
config,
lib,
...
} @ attrs: let
inherit (lib) mkMerge;
in {
autoStart = guestCfg.autostart;
specialArgs =
attrs
// {
parentNode = config;
};
macvlans = [guestCfg.container.macvlan];
ephemeral = true;
privateNetwork = true;
config = mkMerge (guestCfg.modules
++ [
(import ./common-guest-config.nix guestName guestCfg)
{
systemd.network.networks = {
"10-${guestCfg.networking.mainLinkName}" = {
matchConfig.OriginalName = "mv-${guestCfg.container.macvlan}";
linkConfig.Name = guestCfg.networking.mainLinkName;
};
};
}
]);
}

View file

@ -4,12 +4,10 @@
lib, lib,
pkgs, pkgs,
utils, utils,
minimal,
... ...
}: let } @ attrs: let
inherit inherit
(lib) (lib)
attrNames
attrValues attrValues
any any
disko disko
@ -17,19 +15,13 @@
makeBinPath makeBinPath
mapAttrsToList mapAttrsToList
mergeToplevelConfigs mergeToplevelConfigs
mkDefault
mkEnableOption mkEnableOption
mkForce
mkIf mkIf
mkOption mkOption
net
optional
types types
; ;
cfg = config.guests;
nodeName = config.node.name; nodeName = config.node.name;
inherit (cfg) guests;
# Configuration required on the host for a specific guest # Configuration required on the host for a specific guest
defineGuest = guestName: guestCfg: { defineGuest = guestName: guestCfg: {
@ -79,115 +71,18 @@
requires = [fsMountUnit "zfs-chown-${utils.escapeSystemdPath guestCfg.zfs.mountpoint}.service"]; requires = [fsMountUnit "zfs-chown-${utils.escapeSystemdPath guestCfg.zfs.mountpoint}.service"];
after = [fsMountUnit "zfs-chown-${utils.escapeSystemdPath guestCfg.zfs.mountpoint}.service"]; after = [fsMountUnit "zfs-chown-${utils.escapeSystemdPath guestCfg.zfs.mountpoint}.service"];
}; };
};
microvm.vms.${guestName} = let "container@${guestName}" = mkIf (guestCfg.backend == "container") {
mac = (net.mac.assignMacs "02:01:27:00:00:00" 24 [] (attrNames guests)).${guestName}; requires = [fsMountUnit "zfs-chown-${utils.escapeSystemdPath guestCfg.zfs.mountpoint}.service"];
in after = [fsMountUnit "zfs-chown-${utils.escapeSystemdPath guestCfg.zfs.mountpoint}.service"];
mkIf (guestCfg.backend == "microvm") {
# Allow children microvms to know which node is their parent
specialArgs = {
parentNode = config;
inherit (inputs.self) nodes;
inherit (inputs.self.pkgs.${guestCfg.microvm.system}) lib;
inherit inputs;
inherit minimal;
};
pkgs = inputs.self.pkgs.${guestCfg.microvm.system};
inherit (guestCfg) autostart;
config = {
imports = guestCfg.modules;
node.name = guestCfg.nodeName;
node.isGuest = true;
# TODO needed because of https://github.com/NixOS/nixpkgs/issues/102137
environment.noXlibs = mkForce false;
lib.microvm.mac = mac;
microvm = {
hypervisor = mkDefault "qemu";
# Give them some juice by default
mem = mkDefault (2 * 1024);
# MACVTAP bridge to the host's network
interfaces = [
{
type = "macvtap";
id = "vm-${guestName}";
inherit mac;
macvtap = {
link = cfg.networking.macvtapInterface;
mode = "bridge";
};
}
];
shares =
[
# Share the nix-store of the host
{
source = "/nix/store";
mountPoint = "/nix/.ro-store";
tag = "ro-store";
proto = "virtiofs";
}
{
source = "/state/guests/${guestName}";
mountPoint = "/state";
tag = "state";
proto = "virtiofs";
}
]
# Mount persistent data from the host
++ optional guestCfg.zfs.enable {
source = guestCfg.zfs.mountpoint;
mountPoint = "/persist";
tag = "persist";
proto = "virtiofs";
};
};
# FIXME this should be changed in microvm.nix to mkDefault in oder to not require mkForce here
fileSystems."/state".neededForBoot = mkForce true;
fileSystems."/persist".neededForBoot = mkForce true;
# Add a writable store overlay, but since this is always ephemeral
# disable any store optimization from nix.
microvm.writableStoreOverlay = "/nix/.rw-store";
nix = {
settings.auto-optimise-store = mkForce false;
optimise.automatic = mkForce false;
gc.automatic = mkForce false;
};
networking.renameInterfacesByMac.${guestCfg.networking.mainLinkName} = mac;
systemd.network.networks = {
"10-${guestCfg.networking.mainLinkName}" = {
matchConfig.MACAddress = mac;
DHCP = "yes";
dhcpV4Config.UseDNS = false;
dhcpV6Config.UseDNS = false;
ipv6AcceptRAConfig.UseDNS = false;
networkConfig = {
IPv6PrivacyExtensions = "yes";
MulticastDNS = true;
IPv6AcceptRA = true;
};
linkConfig.RequiredForOnline = "routable";
};
};
networking.nftables.firewall = {
zones.untrusted.interfaces = [guestCfg.networking.mainLinkName];
};
}; };
}; };
microvm.vms.${guestName} =
mkIf (guestCfg.backend == "microvm") (import ./microvm.nix guestName guestCfg attrs);
containers.${guestName} = containers.${guestName} =
mkIf (guestCfg.backend == "microvm") { mkIf (guestCfg.backend == "container") (import ./container.nix guestName guestCfg attrs);
};
}; };
in { in {
imports = [ imports = [
@ -198,31 +93,16 @@ in {
microvm.host.enable = microvm.host.enable =
any any
(guestCfg: guestCfg.backend == "microvm") (guestCfg: guestCfg.backend == "microvm")
(attrValues guests); (attrValues config.guests);
} }
]; ];
options.node.isGuest = mkOption { options.node.type = mkOption {
type = types.bool; type = types.enum ["host" "microvm" "container"];
description = "Whether this machine is a guest on another machine."; description = "The type of this machine.";
default = false; default = "host";
}; };
# networking = {
# baseMac = mkOption {
# type = types.net.mac;
# description = ''
# This MAC address will be used as a base address to derive all MicroVM MAC addresses from.
# A good practise is to use the physical address of the macvtap interface.
# '';
# };
#
# macvtapInterface = mkOption {
# type = types.str;
# description = "The macvtap interface to which MicroVMs should be attached";
# };
# };
options.guests = mkOption { options.guests = mkOption {
default = {}; default = {};
description = "Defines the actual vms and handles the necessary base setup for them."; description = "Defines the actual vms and handles the necessary base setup for them.";
@ -252,6 +132,19 @@ in {
type = types.str; type = types.str;
description = "The system that this microvm should use"; description = "The system that this microvm should use";
}; };
macvtapInterface = mkOption {
type = types.str;
description = "The host macvtap interface to which the microvm should be attached";
};
};
# Options for the container backend
container = {
macvlan = mkOption {
type = types.str;
description = "The host interface to which the container should be attached";
};
}; };
networking = { networking = {
@ -298,5 +191,7 @@ in {
})); }));
}; };
config = mkIf (guests != {}) (mergeToplevelConfigs ["disko" "microvm" "systemd"] (mapAttrsToList defineGuest guests)); config =
mkIf (config.guests != {})
(mergeToplevelConfigs ["containers" "disko" "microvm" "systemd"] (mapAttrsToList defineGuest config.guests));
} }

101
modules/guests/microvm.nix Normal file
View file

@ -0,0 +1,101 @@
guestName: guestCfg: {
config,
inputs,
lib,
pkgs,
minimal,
...
}: let
inherit
(lib)
attrNames
mkDefault
mkForce
net
optional
;
mac = (net.mac.assignMacs "02:01:27:00:00:00" 24 [] (attrNames config.guests)).${guestName};
in {
specialArgs = {
parentNode = config;
inherit (inputs.self) nodes;
inherit (inputs.self.pkgs.${guestCfg.microvm.system}) lib;
inherit inputs;
inherit minimal;
};
pkgs = inputs.self.pkgs.${guestCfg.microvm.system};
inherit (guestCfg) autostart;
config = {
imports = guestCfg.modules ++ [(import ./common-guest-config.nix guestName guestCfg)];
# TODO needed because of https://github.com/NixOS/nixpkgs/issues/102137
environment.noXlibs = mkForce false;
lib.microvm.mac = mac;
microvm = {
hypervisor = mkDefault "qemu";
# Give them some juice by default
mem = mkDefault (2 * 1024);
# MACVTAP bridge to the host's network
interfaces = [
{
type = "macvtap";
id = "vm-${guestName}";
inherit mac;
macvtap = {
link = guestCfg.microvm.macvtapInterface;
mode = "bridge";
};
}
];
shares =
[
# Share the nix-store of the host
{
source = "/nix/store";
mountPoint = "/nix/.ro-store";
tag = "ro-store";
proto = "virtiofs";
}
{
source = "/state/guests/${guestName}";
mountPoint = "/state";
tag = "state";
proto = "virtiofs";
}
]
# Mount persistent data from the host
++ optional guestCfg.zfs.enable {
source = guestCfg.zfs.mountpoint;
mountPoint = "/persist";
tag = "persist";
proto = "virtiofs";
};
};
# FIXME this should be changed in microvm.nix to mkDefault in oder to not require mkForce here
fileSystems."/state".neededForBoot = mkForce true;
fileSystems."/persist".neededForBoot = mkForce true;
# Add a writable store overlay, but since this is always ephemeral
# disable any store optimization from nix.
microvm.writableStoreOverlay = "/nix/.rw-store";
nix = {
settings.auto-optimise-store = mkForce false;
optimise.automatic = mkForce false;
gc.automatic = mkForce false;
};
networking.renameInterfacesByMac.${guestCfg.networking.mainLinkName} = mac;
systemd.network.networks = {
"10-${guestCfg.networking.mainLinkName}" = {
matchConfig.MACAddress = mac;
};
};
};
}

View file

@ -48,17 +48,24 @@ inputs: let
nixosConfigurations = flip mapAttrs nixosHosts (mkHost {minimal = false;}); nixosConfigurations = flip mapAttrs nixosHosts (mkHost {minimal = false;});
nixosConfigurationsMinimal = flip mapAttrs nixosHosts (mkHost {minimal = true;}); nixosConfigurationsMinimal = flip mapAttrs nixosHosts (mkHost {minimal = true;});
# True NixOS nodes can define additional microvms (guest nodes) that are built # True NixOS nodes can define additional guest nodes that are built
# together with it. We collect all defined microvm nodes from each node here # together with it. We collect all defined guests from each node here
# to allow accessing any node via the unified attribute `nodes`. # to allow accessing any node via the unified attribute `nodes`.
microvmConfigurations = flip concatMapAttrs self.nixosConfigurations (_: node: guestConfigs = flip concatMapAttrs self.nixosConfigurations (_: node:
mapAttrs' flip mapAttrs' (node.config.guests or {}) (guestName: guestDef:
(vm: def: nameValuePair def.nodeName node.config.microvm.vms.${vm}.config) nameValuePair guestDef.nodeName (
(node.config.meta.microvms.vms or {})); if guestDef.backend == "microvm"
then node.config.microvm.vms.${guestName}.config
else {
# We can only access the .config part of nixosSystem here unfortunately,
# since the rest is not exposed by the nixos module.
inherit (node.config.containers.${guestName}) config;
}
)));
in { in {
inherit inherit
hosts hosts
microvmConfigurations guestConfigs
nixosConfigurations nixosConfigurations
nixosConfigurationsMinimal nixosConfigurationsMinimal
; ;