NixOS base module

This module includes configuration that is automatically enabled for any NixOS system I use.

Unlike most of my modules, there's no "mjm.foo" option to enable the behavior.

{
  config,
  pkgs,
  lib,
  inputs,
  ...
}:
{
  imports = [
    "${inputs.home-manager}/nixos"

    ../../common/base

    <<nixos-submodules>>
  ];

  config = {
    <<nixos-misc>>
  };

  _class = "nixos";
}

Users

./users.nix
{ pkgs, ... }: {
  <<nixos-users-config>>

  _class = "nixos";
}

This module contains configuration related to Unix users and permissions.

users.mutableUsers = false;

Disabling mutable users is a more natural fit. In my opinion, this should be the default, but I suppose it's a compromise to those more used to other Unix systems. With this setting, only users declared in the NixOS config will exist: add users at runtime is not possible.

users.manageLingering = false;

I don't use lingering for any users, so I'd rather not have the mechanism present for managing it.

security.sudo.enable = false;
security.run0.enable = true;
security.run0.wheelNeedsPassword = false;
security.run0.enableSudoAlias = true;

I use run0 instead of sudo on all of my systems. This avoids another setuid binary, closing off a class of vulnerabilities exposed by sudo.

users.users.mjm = {
  isNormalUser = true;
  description = "MJ";
  extraGroups = [ "wheel" ];
  shell = pkgs.fish;
  hashedPassword = "$y$j9T$tM/RKSjlb5ljgtpGT/Y8N1$3oXxWQh/q.KKCcJKoyVeIUVqjjt76EWX.uNEJRASt04";
};
nix.settings.trusted-users = [ "mjm" ];

I have one normal user defined on my machines. I use the fish shell, and I'm in the wheel group so that I can easily administer the system. For a similar reason, I add my user to the list of trusted users for nix. In particular, this avoids issues around trusting artifacts that I copy to servers via SSH.

networkd

./networkd.nix
{ config, lib, ... }:
let
  inherit (lib)
    mkIf
    mkOption
    types
    ;
  cfg = config.mjm.networkd;
in
{
  options.mjm.networkd = {
    enable = mkOption {
      type = types.bool;
      default = !config.networking.networkmanager.enable;
    };

    primaryLinkName = mkOption {
      type = types.str;
      default = "lan0";
    };

    secondaryLinkName = mkOption {
      type = types.nullOr types.str;
      default = null;
    };

    macvlan.enable = mkOption {
      type = types.bool;
      default = false;
    };

    primaryIface = mkOption {
      type = types.str;
      internal = true;
      readOnly = true;
    };

    bridgeParentName = mkOption {
      type = types.str;
      default = if cfg.secondaryLinkName == null then cfg.primaryLinkName else cfg.secondaryLinkName;
      readOnly = true;
    };
  };

  config = mkIf cfg.enable {
    <<networkd-config>>
  };

  _class = "nixos";
}

I use networkd to set up networking on any NixOS systems that don't have WiFi (in which case I use NetworkManager to be able to freely change networks more easily). I prefer to set my networkd configuration up explicitly, without using NixOS's networking.useNetworkd option that translates the various networking.* options to be backed by networkd.

My networkd configuration is a bit complex, primarily because it needs to optionally support using MACVLAN to provide networking for microVMs. I think the end result is pretty nice though.

There are three different network config structures I'm trying to support:

The complexity of this module is to be able to support all three of these.

systemd.network.enable = true;
networking.useDHCP = false;

To use networkd without using NixOS's networking.useNetworkd option, you have to disable the default DHCP option. There's an assertion for it, so evaluation will fail.

mjm.networkd.primaryIface =
  if cfg.secondaryLinkName == null && cfg.macvlan.enable then "mac0" else cfg.primaryLinkName;

The first thing to do is determine which interface is going to be the primary one: the one that this machine actually has an IP address on. The rest of the module will use this, as do some other unrelated modules. While there's three possible network shapes to consider, there's really only two cases here. If MACVLAN is enabled but there's only a single NIC for it, then we will have to create a "mac0" interface and use that as the primary. Otherwise, it's just the primary (or only) NIC, which defaults to "lan0".

systemd.network.networks."10-primary-lan" = {
  name = cfg.primaryIface;
  networkConfig = {
    DHCP = "ipv4";
    IPv6AcceptRA = true;
    IPv6PrivacyExtensions = true;
    MulticastDNS = "resolve";
  };
  dhcpV4Config = {
    UseDomains = true;
  };
  dhcpV6Config = {
    UseDNS = false;
  };
};

The primary interface needs to be configured to use DHCP to get its IPv4 address and SLAAC to get IPv6 addresses. I assign IPv4 addresses in my router's configuration, so even machines with static IPs get them through DHCP. The MulticastDNS option here is set to allow Avahi to be used without conflicting with systemd-resolved. Avahi is necessary to discover printers or other home devices.

UseDomains is enabled to make accessing my servers more convenient. The UseDNS setting for IPv6 is old enough that I don't actually remember its purpose. It might not be needed for my setup.

systemd.network.netdevs.mac0 = mkIf (cfg.primaryIface != cfg.primaryLinkName) {
  netdevConfig.Name = cfg.primaryIface;
  netdevConfig.Kind = "macvlan";
  macvlanConfig.Mode = "bridge";
};

If we're doing MACVLAN without a second NIC, then we'll need to create the MACVLAN interface that will be used as the primary interface for the host.

systemd.network.networks."10-bridge-lan" = mkIf cfg.macvlan.enable {
  name = cfg.bridgeParentName;
  networkConfig = {
    MACVLAN = mkIf (cfg.primaryIface != cfg.primaryLinkName) cfg.primaryIface;
    IPv6AcceptRA = false;
    LinkLocalAddressing = false;
  };
  linkConfig.RequiredForOnline = "carrier";
};

If MACVLAN is being used then the bridge interface for that needs to be configured. Router advertisements and link-local addressing are disabled, to avoid the device getting IP addresses that it isn't meant to use. Because it won't have an IP, "carrier" is the most online this interface is going to get, so letting networkd know that means its online status

is more correct.

The MACVLAN option is set if needed to connect this MACVLAN bridge to the mac0 primary interface, if it was created. The microVMs will handle creating their MACVTAP devices on this link when they start up, so that doesn't need to be configured here.

systemd.network.links = {
  "10-virtio" = {
    matchConfig.Driver = "virtio_net";
    linkConfig.Name = "lan0";
  };
  "10-rpi" = {
    matchConfig.Driver = "bcmgenet";
    linkConfig.Name = "lan0";
  };
  "10-vm-bridge" = {
    matchConfig.Property = "ID_VENDOR_ID=0x10ec ID_MODEL_ID=0x8125";
    linkConfig.Name = "lan1";
  };
  "15-other-ethernet" = {
    matchConfig.Driver = "e1000e r8169";
    linkConfig.Name = "lan0";
  };
};

Finally, I add a little configuration to rename my network interfaces to follow a consistent scheme. It pleases a weird and obsessive part of me, and it also makes configuration across machines easier. The main NIC will always be "lan0", and a second NIC if present will always be "lan1". I got the same PCIe NIC for all three VM hosts, so I can match on the exact vendor and model to match that device. Otherwise, I match by driver to fix the NICs for VMs, Raspberry Pis, and otherwise.

Note that the last rule has a higher number prefix, so that the "lan1" rule matches first. The secondary NICs use the same Realtek driver that is included in that last rule. This rule is only meant to match the built-in Ethernet port on the motherboard, which for my machines always uses either the Intel driver or the Realtek one.

kmscon

./kmscon.nix
{
  pkgs,
  config,
  lib,
  ...
}:
{
  <<kmscon-config>>

  _class = "nixos";
}

I've been using kmscon on my machines where possible to get a nicer console experience.

services.kmscon = {
  enable = lib.mkDefault true;
  config = {
    font-name = "Maple Mono";
    mouse = true;
    hwaccel = config.hardware.graphics.enable;
  };
};

I default to enabling kmscon, and only disable it on machines where it causes problems. Currently that's uranus, where it was breaking the display manager somehow, though I should try turning it back on to see if that issue has been resolved. The other settings are pretty straightforward.

fonts.packages = lib.mkIf config.services.kmscon.enable [
  pkgs.maple-mono.variable
];

Above I set the font to use for kmscon, but that only works if the font is actually installed. That will be true already for systems that use my desktop module, but for servers that don't use that, they need it added here.

Chrony

./chrony.nix
{ lib, ... }: {
  services.timesyncd.enable = lib.mkDefault false;
  services.chrony.enable = lib.mkDefault true;
  services.chrony.extraFlags = [ "-s" ];

  _class = "nixos";
}

I'm using chrony as an NTP client rather than systemd-timesyncd. The latter only supports Simple NTP, which is missing important features of ordinary NTP.

The -s flag is important for the Raspberry Pis, since they don't have a hardware clock. The flag causes chrony to remember the time from the last time it was running, which brings it closer to correct between boots.

=> chrony FAQ: Should I prefer chrony over timesyncd if I do not need to run a server?

Catppuccin

./catppuccin.nix
{ inputs, pkgs, ... }:
{
  imports = [
    "${inputs.catppuccin}/modules/nixos"
  ];

  catppuccin.enable = true;
  catppuccin.autoEnable = false;
  catppuccin.flavor = "macchiato";

  <<catppuccin-sources>>

  _class = "nixos";
}

I use catppuccin-nix to set up color schemes for many different things. I enable the module globally, but set autoEnable to false, so no catppuccin theming should be enabled that isn't explicitly mentioned in my configs. As for flavor, I prefer macchiato: dark, but not too dark.

catppuccin.sources = (import inputs.catppuccin { inherit pkgs; }).packages.overrideScope (
  _: _: {
    whiskers = pkgs.catppuccin-whiskers;
  }
);

Unfortunately, catppuccin-nix makes some use of IFD (import-from-derivation) to run the whiskers tool at evaluation time to produce some of the color themes. whiskers is a Rust tool, and Rust is not known for its fast compilation times. Needing to build whiskers can really slow things down, especially when building a system for another architecture using qemu-user. To make things worse, it doesn't end up getting cached in my personal attic cache because it doesn't end up in the closure of the final system.

The workaround for this is to override the catppuccin-nix module to pull whiskers from Nixpkgs. That version should be cached already.

Miscellaneous settings

time.timeZone = lib.mkDefault "Etc/UTC";

I want most of my machines to use UTC as the time zone. It makes more sense for servers to not have that kind of ambiguity around time. So I set it by default. Desktop machines will override this to my actual time zone.

programs.command-not-found.enable = false;

The common base module sets up nix-index, which provides its own command-not-found functionality. The default one provided by NixOS only works if the programs sqlite DB is present in the pkgs path, which is not the case if using a Git archive. So I disable it across the board in favor of nix-index.

documentation.enable = lib.mkDefault false;

I disable building documentation for servers and VMs, as it can sometimes be slow or broken. I can usually look up a man page on the workstation I'm using instead, as the desktop module will re-enable this.

networking.nftables.enable = lib.mkDefault true;

nftables is easier to work with than iptables, so I enable that by default. In some cases, it needs to be disabled due to a NixOS service module only supporting the iptables backend for NixOS's firewall, in which case I'll reluctantly re-enable it for just that machine.

nix.channel.enable = false;

I don't use nix-channel nor nixos-rebuild, so the paths that this manages are not needed for me.

environment.systemPackages = lib.mkIf (!config.mjm.minimal.enable) (
  lib.attrValues {
    inherit (pkgs) file pciutils usbutils;
  }
);

Install some useful utilities on machines that aren't microVMs.

zramSwap.enable = lib.mkDefault (!config.mjm.minimal.enable && !config.boot.zswap.enable);

I avoid doing fancy swap things on microVMs. I used to enable zram swap on all other machines, but I've come to learn that's maybe not a good idea a lot of the time. My impression now is that in most cases, a swap partition with zswap enabled gives more desirable performance characteristics when under memory pressure. So I've been moving to that configuration where I can (i.e. on systems where I have a swap partition already or the ability to create one).

So now I'm left with a configuration where zram swap is enabled by default unless a machine has opted in to zswap.

Proxy Information
Original URL
gemini://midna.dev/homelab/modules/nixos/base/
Status Code
Success (20)
Meta
text/gemini;lang=en-US
Capsule Response Time
26.355447 milliseconds
Gemini-to-HTML Time
0.829631 milliseconds

This content has been proxied by September (UNKNO).