nix defaults

Published as part of 'nix' series.

Sensible Nix Defaults

If you're using Nix you are probably more accustomed to nonsense, and I am no exception. In an effort to remain sane, I use flakes to create NixOS modules which I can resuse across my systems, and I'd like to share a very basic one today: the "defaults" flake. To clarify, this is used when building machines a la nixpkgs.lib.nixosSystem and not for packaging programs.

You can find this flake mirrored here:

Okay, let's first setup our flake, nothing too fancy here. If you're not familiar with NixOS and Flakes this post will not be explaining them. The nix-index-database is included so we can use the , syntax to run programs.

The Flake

Here it is in all it's glory. You can skip ahead to the bottom (or read the README.md in repo) if you just want to know how to install and configure it.

https://github.com/xvrqt/defaults-flake/

# flake.nix
{
  inputs = {
    # Used to keep the other inputs in lock-step
    nixpkgs.url = "github:NixOS/nixpkgs/nixos-25.11";

    # Used to track options
    nix-index-database.url = "github:nix-community/nix-index-database";
    nix-index-database.inputs.nixpkgs.follows = "nixpkgs";
  };

  outputs = { nixpkgs, nix-index-database, ... }:
    let
      forAllSystems = function:
        nixpkgs.lib.genAttrs [
          "x86_64-linux"
          "aarch64-linux"
        ] (system: function nixpkgs.legacyPackages.${system});
    in
    {
      nixosModules = forAllSystems
        (pkgs: {
          default = { lib, config, ... }: {
            imports = [
              nix-index-database.nixosModules.nix-index
              (import ./nixosModule.nix {
                inherit lib pkgs config;
              })
            ];
          };
        });
    };
}

The Module

All the interesting stuff happens in here, first, lets tell our system we're using Flakes lol.

NixOS Meta Config

Then only allow users with super user privileges to rebuild the system - for obvious reasons.

I also like to set up a daily garbage collection and optimization (optimisation??) job to prevent accumulating too much cruft over time. I do wish there wa a way to say "only keep the last N generations" but instead you must specify it by how old they are. It picked 30 days, but tweak to your heart's content.

Lastly I only ever want to go 10 generations back; otherwise a day of tinkering can bloat your system for a month until its cleaned up.

# nixosModule.nix
{ lib, pkgs, config, ... }: {
  config = {
    nix = {
      settings = {
        # Enable NixOS Flakes
        experimental-features = [
          "nix-command"
          "flakes"
        ];
        # Only allow those with admin privileges invoke Nix commands
        allowed-users = [ "@wheel" ];
      };
      # Optimise the Nix-Store once a day
      optimise = lib.mkDefault {
        automatic = true;
        dates = [ "daily" ];
      };
      # Automatically Clean-out the Nix-Store once a day
      gc = lib.mkDefault {
        automatic = true;
        options = "--delete-older-than 30d";
        dates = "daily";
      };
    };
    # Only keep 10 generations maximum
    boot.loader.systemd-boot.configurationLimit = lib.mkDefault 10;
  };
}

Timezones and Units

I have servers all over the world and it's nice to set the their clocks, and other units to correspond with which I am familiar. Adjust these to your particular situation.

# nixosModule.nix
{ lib, pkgs, config, ... }: {
  # Previous Sections
  # - Nix Settings

  # Timezone to the West Coast (Best Coast) by default
  time.timeZone = lib.mkDefault "America/Los_Angeles";

  # Select internationalisation properties.
  i18n.defaultLocale = lib.mkDefault "en_US.UTF-8";

  # Get specific with encoding
  i18n.extraLocaleSettings = lib.mkDefault {
    LC_ADDRESS = "en_US.UTF-8";
    LC_IDENTIFICATION = "en_US.UTF-8";
    LC_MEASUREMENT = "en_US.UTF-8";
    LC_MONETARY = "en_US.UTF-8";
    LC_NAME = "en_US.UTF-8";
    LC_NUMERIC = "en_US.UTF-8";
    LC_PAPER = "en_US.UTF-8";
    LC_TELEPHONE = "en_US.UTF-8";
    LC_TIME = "en_US.UTF-8";
  };
}

Default Programs

I try to keep this lean, so these are mostly meta programs, or programs required on virtually every system. nh1 stands for "nix-helper" and I like to use it to rebuild my system because the syntax is shorter (nh os switch .) and it pretty prints the dependency graph, and system diff both during and after it finishes running. It can make things a lot more clear when your system is taking a long time to rebuild.

comma2 allows you to run programs you don't have installed directly from the command line, and then vanishes them from you path. I find it indispensibly convenient, and way more ergnomic than nesting nix-shell -p <pkg> repreatedly. To use, simply $ , <pkg> and it will run it for you. Nix is so declaritive it can be annoying to have to do an entire rebuild just to use one program for the day, and this eases that pain. I have even used it to run a video editor so I could work on a project and then when I finished I could just close out and the program was gone from my system.

openssh is a must have, and I recompile it with all the compile time hardening flags on to help ensure this critical piece of security infrastructure is less likely to compromise me.

Lastly, please remember to install your shell (unless you're a sh user, good for you). I use zsh and I like it a lot. I will probably switch to fish one day, but it is not this day.

# nixosModule.nix
{ lib, pkgs, config, ... }: {
  # Previous Sections
  # - Nix Settings
  # - Timezones & Units

  programs = {
  # Nix helper functions
  nh = {
    enable = lib.mkDefault true;
    # Where I always mount my flake
    flake = lib.mkDefault "/key/flake";
  };

  # Run programs without nix-shell
  # Usage: $ , cowsay "Hello"
  # It will install cowsay temporily, run the program in your shell, and then
  # remove it from your PATH again afterwards.
  nix-index-database.comma.enable = lib.mkDefault true;

  # Set the ssh package to be the hardened version of itself
  ssh.package = lib.mkDefault (harden pkgs.openssh);

  # Ensure a shell is enabled
  zsh.enable = lib.mkDefault true;
};

Environment

I hate the nano editor so I ensure that my environment variables are set such that they use my default editor of chose, hx3 which is written in Rust and is like nvim but with all my favorite nvim plugins preinstalled (and much snappier). I choose to recompile this for my machine since I spend most of my day using it - but I have provided an option to disable (e.g. on my low powered servers where I am rarely editing things directly, and don't want to spend an hour compiling a text editor).

I also include hyfetch4 which is like neofetch but gayer. All my shells print the system information on login to help me remember which one I ssh'd into.

I include the package for the shell we enabled above as the only permissible shell, and also clear out all other default packages (looking at you, nano).

# nixosModule.nix
{ lib, pkgs, config, ... }: {
  # Previous Sections
  # - Nix Settings
  # - Timezones & Units
  # - Programs

  # Packages every system needs
  environment = {
    # So we don't get stuck with a shitty editor
    variables = {
      # We use mkOverried 990 because NixOS be default sets a default value
      # (nano; priority 1000) to these environment variables
      EDITOR = lib.mkOverride 990 "hx";
      VISUAL = lib.mkOverride 990 "hx";
      SUDO_EDITOR = lib.mkOverride 990 "hx";
    };
    # These packages are automatically available to all users
    systemPackages = [
      # Default text editor
      (optimizeRust pkgs.helix)
      # Pretty print system information upon shell login
      (optimizeRust pkgs.hyfetch)
    ];
    # Remove all other default packages so nothing sneaks in
    defaultPackages = lib.mkDefault [ ];
    # Permissible login shells (sh is implicitly included)
    shells = lib.mkDefault [ pkgs.zsh ];
  };
}

Services

The only service we run is openssh so we can login remotely. I actually use another module to auto configure this to allow my computers to talk with each other, but we can still set some sensible security defaults while we're here.

The usual stuff, like not allowing login as root, and only allowing authentication with keys. I also like to use DNS resolution since I run my own DNS and have my own search domains. If you hardcode all your IPs then I would disable this.

We don't set the package here, because by default it uses programs.ssh.package which we set above.

# nixosModule.nix
{ lib, pkgs, config, ... }: {
  # Previous Sections
  # - Nix Settings
  # - Timezones & Units
  # - Programs
  # - Environment

  services = {
    openssh = {
      enable = lib.mkDefault true;
      settings = {
        # Allows hostnames to be FQDN (sshd will check their DNS record matches)
        UseDns = lib.mkDefault true;
        # SSH should check the permissions of the identity files and directories
        StrictModes = lib.mkDefault true;
        # We don't need to log in as root
        PermitRootLogin = lib.mkDefault "no";
        # SSH Keys Only
        PasswordAuthentication = lib.mkDefault false;
      };
    };
  };
}

Security

A few final tweaks to security, such as enabling sudo and exempting my users from requiring a password each time I use it. Also, preventing anyone from logging in as root.

I also setup my preferred settings for syscall auditing, but this is disabled by default (i.e. I only enable it on servers, but not personal use computers). You can enable it with defaults.auditing = true; in your NixOS Module.

# nixosModule.nix
{ lib, pkgs, config, ... }: {
  # Previous Sections
  # - Nix Settings
  # - Timezones & Units
  # - Programs
  # - Environment
  # - Services

  # Allow sudoers to not require a password
  security = {
    sudo = {
      enable = lib.mkDefault true;
      # Don't challenge memebers of 'wheel'
      wheelNeedsPassword = lib.mkDefault false;
    };

    # Locked down root user as a default
    users = {
      users = {
        root = lib.mkDefault {
          # Default Shell
          shell = pkgs.zsh;
          # Added to the list of sudoers
          extraGroups = [ "networkmanager" "wheel" ];
          # Disable logging in as root
          hashedPassword = lib.mkDefault "!";
          initialHashedPassword = lib.mkDefault "!";
        };
      };
    };

    # Enable Linux Kernel Auditing
    auditd = lib.mkIf auditCheck {
      enable = lib.mkDefault true;
      settings = {
        # Number of log files to keep
        num_logs = lib.mkDefault 8;
        # Maximum logfile size, in MiB
        max_log_file = lib.mkDefault 32;
        # What to do when we're out of log files
        max_log_file_action = lib.mkDefault "rotate";
      };
    };
    audit = lib.mkIf auditCheck {
      enable = lib.mkDefault true;
      # -a exit,always -> Run audit when syscall is loaded, no matter what
      # -F arch=b64 -> Only log syscalls made by 64bit processes
      # -S execve -> Only monitor the execve system call flag
      # Typically invoked by a shell, this monitors every attempt for a
      # 64bit process to execute another program
      rules = lib.mkDefault [
        "-a exit,always -F arch=b64 -S execve"
      ];
    };
  };
}

Using the Flake

It's not difficult to use, just add it to your inputs, include the default NixOS Module in your modules list, and tweak the options as you see fit. If you fork and edit the default editor, you might want to change the function which optimizes it. I included on for gcc as well as the one I used for Rust, which you can find at the top of the nixosModule.nix file.

Installation

Add this flake to your NixOS Configuration list of modules flake inputs, and add its NixOS Module to the outputs:

{
  inputs = {
    nixpkgs.url = "github:NixOs/nixpkgs";
    defaults.url = "github:xvrqt/defaults-flake";
    # etc...
  };

  outputs = {defaults, ...} @ inputs: {
      nixos-configuration = nixpkgs.lib.nixosSystem {
        inherit pkgs;
        specialArgs = { inherit inputs; };
        modules = [
          defaults.nixosModules.${system}.default  # <-- Important Bit
          ./my-nix-configuration.nix
          # etc...
        ];
      };
  };
}

Options

There are three options, which can you set using the following NixOS module.

{
  defaults = {
    # Whether or not to enable system wide auditing.
    # Defaults to FALSE. I only enable it on servers.
    auditing = false;

    packages = {
      # Whether to recompile your editor and hyfetch
      # Defaults to TRUE.
      optimize= true;
      # Whether to recompile openssh with hardening options
      # Defaults to TRUE.
      hardening = true;
    };
  };
}
  1. https://github.com/nix-community/nh

  2. https://github.com/nix-community/comma

  3. https://github.com/helix-editor/helix

  4. https://github.com/hykilpikonna/hyfetch