Implement initial containers module

This commit is contained in:
Jack O'Sullivan 2022-03-26 14:20:30 +00:00
parent 5ef6684df4
commit 67114c1336
16 changed files with 372 additions and 73 deletions

6
flake.lock generated
View File

@ -150,11 +150,11 @@
},
"impermanence": {
"locked": {
"lastModified": 1644623728,
"narHash": "sha256-aG+JnIaFXTM9YqcE5uyBgPlPrkmX4bs+yY5YCfA/vBQ=",
"lastModified": 1647189769,
"narHash": "sha256-6PJ4wqDuFMIw34gM/LxQ9qZPw8vPls4xC7UCeweSvKs=",
"owner": "devplayer0",
"repo": "impermanence",
"rev": "74be13a87a3bbcbbaf94aea66f9576a1163db4f0",
"rev": "723c1a7535b7cd194c3a2a693a2566ba1e047a89",
"type": "github"
},
"original": {

View File

@ -92,9 +92,11 @@
configs = [
# Systems
nixos/boxes/colony.nix
nixos/installer.nix
nixos/boxes/colony.nix
nixos/containers/vaultwarden.nix
# Homes
home-manager/configs/castle.nix
home-manager/configs/macsimum.nix
@ -109,6 +111,7 @@
};
nixos.secretsPath = ./secrets;
deploy-rs.deploy.sshOpts = [ "-i" ".keys/deploy.key" ];
}
# Not an internal part of the module system apparently, but it doesn't have any dependencies other than lib

View File

@ -134,6 +134,6 @@ rec {
sshKeyFiles = {
me = .keys/me.pub;
deploy = .keys/deploy.key;
deploy = .keys/deploy.pub;
};
}

View File

@ -1,9 +1,8 @@
{
nixos.systems.colony = {
system = "x86_64-linux";
nixpkgs = "stable";
nixpkgs = "unstable";
home-manager = "unstable";
docCustom = false;
configuration = { lib, pkgs, modulesPath, config, ... }:
let
@ -32,9 +31,10 @@
};
};
server.enable = true;
tmproot.unsaved.ignore = [
"/var/db/dhcpcd/enp1s0.lease"
];
containers = {
instances.vaultwarden = {};
};
};
fileSystems = {
@ -58,6 +58,8 @@
enp1s0.useDHCP = true;
};
};
#systemd.services.systemd-networkd.environment.SYSTEMD_LOG_LEVEL = "debug";
};
};
}

View File

@ -0,0 +1,61 @@
{
nixos.systems.vaultwarden = {
system = "x86_64-linux";
nixpkgs = "unstable";
configuration = { lib, config, ... }:
let
inherit (lib) mkMerge mkIf mkForce;
vwData = "/var/lib/vaultwarden";
vwSecrets = "vaultwarden.env";
in
{
config = mkMerge [
{
my = {
server.enable = true;
secrets = {
files."${vwSecrets}" = {};
};
firewall = {
tcp.allowed = [ 80 3012 ];
};
};
systemd.services.vaultwarden.serviceConfig.StateDirectory = mkForce "vaultwarden";
services = {
vaultwarden = {
enable = true;
config = {
dataFolder = vwData;
webVaultEnabled = true;
rocketPort = 80;
websocketEnabled = true;
websocketPort = 3012;
};
environmentFile = config.age.secrets."${vwSecrets}".path;
};
};
}
(mkIf config.my.build.isDevVM {
my.tmproot.persistence.config.directories = [
{
directory = vwData;
user = config.users.users.vaultwarden.name;
group = config.users.groups.vaultwarden.name;
}
];
virtualisation = {
forwardPorts = [
{ from = "host"; host.port = 8080; guest.port = 80; }
];
};
})
];
};
};
}

View File

@ -37,7 +37,7 @@ let
lib = pkgs.lib;
# Put the inputs in specialArgs to avoid infinite recursion when modules try to do imports
specialArgs = { inherit inputs; };
specialArgs = { inherit inputs; inherit (cfg) systems; };
# `baseModules` informs the manual which modules to document
baseModules =

View File

@ -9,5 +9,6 @@
server = ./server.nix;
deploy-rs = ./deploy-rs.nix;
secrets = ./secrets.nix;
containers = ./containers.nix;
};
}

View File

@ -31,6 +31,15 @@ let
}
];
};
asContainer = extendModules {
# TODO: see previous
specialArgs = { inherit baseModules; };
modules = [
{
boot.isContainer = true;
}
];
};
in
{
options = with lib.types; {
@ -46,13 +55,19 @@ in
inherit (asDevVM) type;
default = { };
visible = "shallow";
description = "Configuration as a development VM";
description = "Configuration as a development VM.";
};
asISO = mkOption {
inherit (asISO) type;
default = { };
visible = "shallow";
description = "Configuration as a bootable .iso image";
description = "Configuration as a bootable .iso image.";
};
asContainer = mkOption {
inherit (asContainer) type;
default = { };
visible = "shallow";
description = "Configuration as a container.";
};
buildAs = options.system.build;
@ -76,6 +91,7 @@ in
# The meta.mainProgram should probably be set upstream but oh well...
devVM = recursiveUpdate config.my.asDevVM.system.build.vm { meta.mainProgram = "run-${config.system.name}-vm"; };
iso = config.my.asISO.system.build.isoImage;
container = config.my.asContainer.system.build.toplevel;
};
};
};

View File

@ -97,18 +97,15 @@ in
networking = {
domain = mkDefault "int.nul.ie";
useDHCP = mkDefault false;
useDHCP = false;
enableIPv6 = mkDefault true;
};
virtualisation = {
forwardPorts = flatten [
(optional config.services.openssh.openFirewall { from = "host"; host.port = 2222; guest.port = 22; })
];
useNetworkd = mkDefault true;
};
environment.systemPackages = with pkgs; [
bash-completion
vim
ldns
];
programs = {
@ -146,6 +143,11 @@ in
})
(mkIf config.my.build.isDevVM {
networking.interfaces.eth0.useDHCP = mkDefault true;
virtualisation = {
forwardPorts = flatten [
(optional config.services.openssh.openFirewall { from = "host"; host.port = 2222; guest.port = 22; })
];
};
})
];

View File

@ -0,0 +1,161 @@
{ lib, options, config, systems, ... }:
let
inherit (builtins) attrNames attrValues mapAttrs;
inherit (lib) concatMapStringsSep filterAttrs mkDefault mkIf mkMerge mkAliasDefinitions mkVMOverride mkAfter;
inherit (lib.my) mkOpt';
cfg = config.my.containers;
devVMKeyPath = "/run/dev.key";
containerOpts = with lib.types; { name, ... }: {
options = {
system = mkOpt' unspecified systems."${name}".configuration.config.my.buildAs.container
"Top-level system configuration.";
opts = mkOpt' lib.my.naiveModule { } "Options to pass to `containers.*name*`.";
};
};
in
{
options.my.containers = with lib.types; {
networking = {
bridgeName = mkOpt' str "containers" "Name of host bridge.";
hostAddresses = mkOpt' (either str (listOf str)) "172.16.137.1/24" "Addresses for the host bridge.";
};
persistDir = mkOpt' str "/persist/containers" "Where to store container persistence data.";
instances = mkOpt' (attrsOf (submodule containerOpts)) { } "Individual containers.";
};
config = mkMerge [
(mkIf (cfg.instances != { }) {
assertions = [
{
assertion = config.systemd.network.enable;
message = "Containers currently require systemd-networkd!";
}
];
my.firewall.trustedInterfaces = [ cfg.networking.bridgeName ];
systemd = {
network = {
netdevs."25-container-bridge".netdevConfig = {
Name = cfg.networking.bridgeName;
Kind = "bridge";
};
# Based on the pre-installed 80-container-vz
networks."80-container-vb" = {
matchConfig = {
Name = "vb-*";
Driver = "veth";
};
networkConfig = {
# systemd LLDP doesn't work on bridge interfaces
LLDP = true;
EmitLLDP = "customer-bridge";
# Although nspawn will set the veth's master, systemd will clear it (systemd 250 adds a `KeepMaster`
# to avoid this)
Bridge = cfg.networking.bridgeName;
};
};
networks."80-containers-bridge" = {
matchConfig = {
Name = cfg.networking.bridgeName;
Driver = "bridge";
};
networkConfig = {
Address = cfg.networking.hostAddresses;
DHCPServer = true;
# TODO: Configuration for routed IPv6 (and maybe IPv4)
IPMasquerade = "both";
IPv6SendRA = true;
};
};
};
tmpfiles.rules = map (n: "d ${cfg.persistDir}/${n} 0755 root root") (attrNames cfg.instances);
};
containers = mapAttrs (n: c: mkMerge [
{
path = "/nix/var/nix/profiles/per-container/${n}";
ephemeral = true;
autoStart = mkDefault true;
bindMounts = {
"/persist" = {
hostPath = "${cfg.persistDir}/${n}";
isReadOnly = false;
};
};
privateNetwork = true;
hostBridge = cfg.networking.bridgeName;
additionalCapabilities = [ "CAP_NET_ADMIN" ];
}
c.opts
(mkIf config.my.build.isDevVM {
path = mkVMOverride c.system;
bindMounts."${devVMKeyPath}" = {
hostPath = config.my.secrets.vmKeyPath;
isReadOnly = true;
};
})
]) cfg.instances;
})
# Inside container
(mkIf config.boot.isContainer {
assertions = [
{
assertion = config.systemd.network.enable;
message = "Containers currently require systemd-networkd!";
}
];
my = {
tmproot.enable = true;
};
system.activationScripts = {
# impermanence will throw a fit and bail the whole activation script if this already exists (the container
# start script pre-creates it for some reason)
clearMachineId.text = "rm -f /etc/machine-id";
createPersistentStorageDirs.deps = [ "clearMachineId" ];
# Ordinarily I think the Nix daemon does this but ofc it doesn't in the container
createNixPerUserDirs = {
text =
let
users = attrValues (filterAttrs (_: u: u.isNormalUser) config.users.users);
in
concatMapStringsSep "\n"
(u: ''install -d -o ${u.name} -g ${u.group} /nix/var/nix/{profiles,gcroots}/per-user/"${u.name}"'') users;
deps = [ "users" "groups" ];
};
};
networking = {
useHostResolvConf = false;
};
# Based on the pre-installed 80-container-host0
systemd.network.networks."80-container-eth0" = {
matchConfig = {
Name = "eth0";
Virtualization = "container";
};
networkConfig = {
DHCP = "yes";
LLDP = true;
EmitLLDP = "customer-bridge";
};
dhcpConfig = {
UseTimezone = true;
};
};
# If the host is a dev VM
age.identityPaths = [ devVMKeyPath ];
})
];
}

View File

@ -4,6 +4,7 @@ let
inherit (lib.my) parseIPPort mkOpt' mkBoolOpt';
cfg = config.my.firewall;
iptCfg = config.networking.firewall;
in
{
options.my.firewall = with lib.types; {
@ -31,9 +32,9 @@ in
enable = true;
ruleset =
let
trusted' = "{ ${concatStringsSep ", " cfg.trustedInterfaces} }";
openTCP = cfg.tcp.allowed ++ config.networking.firewall.allowedTCPPorts;
openUDP = cfg.udp.allowed ++ config.networking.firewall.allowedUDPPorts;
trusted' = "{ ${concatStringsSep ", " (cfg.trustedInterfaces ++ iptCfg.trustedInterfaces)} }";
openTCP = cfg.tcp.allowed ++ iptCfg.allowedTCPPorts;
openUDP = cfg.udp.allowed ++ iptCfg.allowedUDPPorts;
in
''
table inet filter {

View File

@ -8,6 +8,7 @@ let
in
{
options.my.secrets = with lib.types; {
vmKeyPath = mkOpt' str "/tmp/xchg/dev.key" "Path to dev key when in a dev VM.";
key = mkOpt' (nullOr str) null "Public key that secrets for this system should be encrypted for.";
files = mkOpt' (attrsOf unspecified) { } "Secrets to decrypt with agenix.";
};
@ -19,7 +20,7 @@ in
} // opts) cfg.files;
}
(mkIf config.my.build.isDevVM {
age.identityPaths = [ "/tmp/xchg/dev.key" ];
age.identityPaths = [ cfg.vmKeyPath ];
})
];
}

View File

@ -1,7 +1,7 @@
{ config, lib, ... }:
{ lib, pkgs, config, ... }:
let
inherit (lib) mkIf mkDefault;
inherit (lib.my) mkBoolOpt';
inherit (lib.my) mkBoolOpt' mkDefault';
cfg = config.my.server;
uname = if config.my.user.enable then config.my.user.config.name else "root";
@ -17,6 +17,12 @@ in
my.user.homeConfig = {
my.gui.enable = false;
};
documentation.nixos.enable = mkDefault' false;
environment.systemPackages = with pkgs; [
tcpdump
];
};
meta.buildDocsInSandbox = false;

View File

@ -1,10 +1,12 @@
{ lib, pkgs, config, ... }:
{ lib, pkgs, options, config, ... }:
let
inherit (lib) optionalString concatStringsSep concatMap concatMapStringsSep mkIf mkDefault mkMerge mkVMOverride;
inherit (lib)
optionalString concatStringsSep concatMap concatMapStringsSep mkIf mkDefault mkMerge mkForce mkVMOverride
mkAliasDefinitions;
inherit (lib.my) mkOpt' mkBoolOpt' mkVMOverride';
cfg = config.my.tmproot;
enablePersistence = cfg.persistDir != null;
enablePersistence = cfg.persistence.dir != null;
showUnsaved =
''
@ -58,8 +60,11 @@ in
options = with lib.types; {
my.tmproot = {
enable = mkBoolOpt' true "Whether to enable tmproot.";
persistDir = mkOpt' (nullOr str) "/persist" "Path where persisted files are stored.";
size = mkOpt' str "2G" "Size of tmpfs root";
persistence = {
dir = mkOpt' (nullOr str) "/persist" "Path where persisted files are stored.";
config = mkOpt' options.environment.persistence.type.nestedTypes.elemType { } "Persistence configuration";
};
unsaved = {
showMotd = mkBoolOpt' true "Whether to show unsaved files with `dynamic-motd`.";
ignore = mkOpt' (listOf str) [ ] "Path prefixes to ignore if unsaved.";
@ -77,33 +82,53 @@ in
}
];
my.tmproot.unsaved.ignore = [
"/tmp"
my.tmproot = {
unsaved.ignore = [
"/tmp"
# setup-etc.pl will create this for us
"/etc/NIXOS"
# setup-etc.pl will create this for us
"/etc/NIXOS"
# Once mutableUsers is disabled, we should be all clear here
"/etc/passwd"
"/etc/group"
"/etc/shadow"
"/etc/subuid"
"/etc/subgid"
# Once mutableUsers is disabled, we should be all clear here
"/etc/passwd"
"/etc/group"
"/etc/shadow"
"/etc/subuid"
"/etc/subgid"
# Lock file for /etc/{passwd,shadow}
"/etc/.pwd.lock"
# Lock file for /etc/{passwd,shadow}
"/etc/.pwd.lock"
# systemd last updated? I presume they'll get updated on boot...
"/etc/.updated"
"/var/.updated"
# systemd last updated? I presume they'll get updated on boot...
"/etc/.updated"
"/var/.updated"
# Specifies obsolete files that should be deleted on activation - we'll never have those!
"/etc/.clean"
# Specifies obsolete files that should be deleted on activation - we'll never have those!
"/etc/.clean"
# These are set in environment.etc by the sshd module, but because their mode needs to be changed,
# setup-etc will copy them instead of symlinking
"/etc/ssh/authorized_keys.d"
];
# These are set in environment.etc by the sshd module, but because their mode needs to be changed,
# setup-etc will copy them instead of symlinking
"/etc/ssh/authorized_keys.d"
];
persistence.config = {
# In impermanence the key in `environment.persistence.*` (aka name passed the attrsOf submodule) sets the
# default value, so we need to override it when we mkAliasDefinitions
_module.args.name = mkForce cfg.persistence.dir;
hideMounts = mkDefault true;
directories = [
"/var/log"
# In theory we'd include only the files needed individually (i.e. the {U,G}ID map files that track deleted
# users and groups), but `update-users-groups.pl` actually deletes the original files for "atomic update".
# Also the script runs before impermanence does.
"/var/lib/nixos"
"/var/lib/systemd"
];
files = [
"/etc/machine-id"
];
};
};
environment.systemPackages = [
(pkgs.writeScriptBin "tmproot-unsaved" showUnsaved)
@ -119,7 +144,7 @@ in
echo -e "\t\e[31;1;4mWarning:\e[0m $count file(s) on / will be lost on shutdown!"
echo -e '\tTo see them, run `tmproot-unsaved` as root.'
${optionalString enablePersistence ''
echo -e '\tAdd these files to `environment.persistence."${cfg.persistDir}"` to keep them!'
echo -e '\tAdd these files to `my.tmproot.persistence.config` to keep them!'
''}
echo -e "\tIf they don't need to be kept, add them to \`my.tmproot.unsaved.ignore\`."
echo
@ -149,8 +174,8 @@ in
{
assertions = [
{
assertion = config.fileSystems ? "${cfg.persistDir}";
message = "The 'fileSystems' option does not specify your persistence file system (${cfg.persistDir}).";
assertion = (config.fileSystems ? "${cfg.persistence.dir}") || config.boot.isContainer;
message = "The 'fileSystems' option does not specify your persistence file system (${cfg.persistence.dir}).";
}
];
@ -172,7 +197,7 @@ in
sourceDir = "${d.persistentStoragePath}${d.directory}";
in
''([ "$device" = "/mnt-root${sourceDir}" ] && ensurePersistSource "${sourceDir}" "${d.mode}")'')
config.environment.persistence."${cfg.persistDir}".directories}
cfg.persistence.config.directories}
waitDevice "$@"
}
@ -181,33 +206,20 @@ in
alias waitDevice=_waitDevice
'';
environment.persistence."${cfg.persistDir}" = {
hideMounts = mkDefault true;
directories = [
"/var/log"
# In theory we'd include only the files needed individually (i.e. the {U,G}ID map files that track deleted
# users and groups), but `update-users-groups.pl` actually deletes the original files for "atomic update".
# Also the script runs before impermanence does.
"/var/lib/nixos"
"/var/lib/systemd"
];
files = [
"/etc/machine-id"
];
};
environment.persistence."${cfg.persistence.dir}" = mkAliasDefinitions options.my.tmproot.persistence.config;
virtualisation = {
diskImage = "./.vms/${config.system.name}-persist.qcow2";
};
}
(mkIf config.services.openssh.enable {
environment.persistence."${cfg.persistDir}".files =
my.tmproot.persistence.config.files =
concatMap (k: [ k.path "${k.path}.pub" ]) config.services.openssh.hostKeys;
})
(mkIf config.my.build.isDevVM {
fileSystems = mkVMOverride {
# Hijack the "root" device for persistence in the VM
"${cfg.persistDir}" = {
"${cfg.persistence.dir}" = {
device = config.virtualisation.bootDevice;
neededForBoot = true;
};

View File

@ -5,6 +5,7 @@ let
cfg = config.my.user;
user' = cfg.config;
user = config.users.users.${user'.name};
in
{
options.my.user = with lib.types; {
@ -37,9 +38,33 @@ in
in mkIf (shell != null) (mkDefault' shell);
openssh.authorizedKeys.keyFiles = [ lib.my.sshKeyFiles.me ];
};
# In order for this option to evaluate on its own, home-manager expects the `name` (which is derived from the
# parent attr name) to be the users name, aka `home-manager.users.<name>`
homeConfig = { _module.args.name = lib.mkForce user'.name; };
homeConfig = {
# In order for this option to evaluate on its own, home-manager expects the `name` (which is derived from the
# parent attr name) to be the users name, aka `home-manager.users.<name>`
_module.args.name = lib.mkForce user'.name;
};
};
tmproot.persistence.config =
let
perms = {
mode = "0700";
user = user.name;
group = user.group;
};
in {
files = map (file: {
inherit file;
parentDirectory = perms;
}) [
"/home/${user'.name}/.bash_history"
];
directories = map (directory: {
inherit directory;
} // perms) [
# Persist all of fish; it's not easy to persist just the history fish won't let you move it to a different
# directory. Also it does some funny stuff and can't really be a symlink it seems.
"/home/${user'.name}/.local/share/fish"
];
};
};

View File

@ -0,0 +1,8 @@
age-encryption.org/v1
-> X25519 Lm6m9mqSeFYvQ3bo73i9KrAzADgWLRcxmUg31JwqgWw
FXbd6LUIA9OlCiMb1Us3T3/RkbQbxWD3pZ77/y3UuDM
-> C3L/E-grease -7Y+*Gh
UEBPiPpYXfbZltNeUQrX4ahsDakgciN6sSLLHkPsX69oGtLuGRQeoDC6tvEtG2Ws
wJEX57JORoAWfZsUtF0Oj+hN++ANcCm1andG45Yf
--- 1Pr1sAqpDFUZBGe97NYMyN3AEgv/EJgBl9DK4Ga93oc
•Ö”0˜ü`LVoÖ\åÃoÐ¥ëÜyÍýmè ÃÃn^Ä×ûär|Q@†µáq{ÄÖ…f¬ƒéîÏ<){ucÈu<75>ÞÅÆH¾ð<C2BE>»!U+7FYhh°W¶ÅkˆÐ;RO¶