Introduction

In this text, I present some useful personal observations I’ve figured with using NixOS for hosting a server. I think these may be useful for others, and as such, I’ve added them here for posterity.

This text was written on 02.02.2020, and reflects the conditions as present by then. Be sure to check the current state, as it may have changed in the meanwhile, rendering some of the information out of date or obsolete!

Observations

Running lvm-cache

Per this issue, LVM cached volumes are not quite yet properly supported, and the pull request fixing this is still pending.

Based on a workaround as implemented here by YorikSar, I implemented a method to ensure appropriate kernel modules are available for baseline SMQ caching.

# Ensure appropriate kernel modules are present. Extend this if you need further cache modules, e.g dm-cache-cleaner. Thin provisioning kernel modules should also be added separately.
boot.initrd.kernelModules = ["dm-cache" "dm-cache-smq" "dm-snapshot"];
# Ensure that appropriate tools are copied to initrd
boot.initrd.extraUtilsCommands = ''
for BIN in ${pkgs.thin-provisioning-tools}/{s,}bin/*; do
    copy_bin_and_libs $BIN
done
'';
# Before LVM commands are executed, ensure that LVM knows exactly where our cache and thin provisioning tools are
boot.initrd.preLVMCommands = ''
mkdir -p /etc/lvm
echo "global/thin_check_executable = \"$(which thin_check)\"" >> /etc/lvm/lvm.conf
echo "global/cache_check_executable = \"$(which cache_check)\"" >> /etc/lvm/lvm.conf
echo "global/cache_dump_executable = \"$(which cache_dump)\"" >> /etc/lvm/lvm.conf
echo "global/cache_repair_executable = \"$(which cache_repair)\"" >> /etc/lvm/lvm.conf
echo "global/thin_dump_executable = \"$(which thin_dump)\"" >> /etc/lvm/lvm.conf
echo "global/thin_repair_executable = \"$(which thin_repair)\"" >> /etc/lvm/lvm.conf
'';

Keeping Plex up to date

Plex Media Server has the habit of updating quite often. This can pose a problem as Nixpkgs will not automatically update to the latest Plex version. There is an easy way to ad-hoc patch this tho.

services.plex = {
    # Configure standard Plex options here
    enable = true;
    openFirewall = true;
    dataDir = "<your Plex data folder here>";

    # Manually override into version desired for the raw Plex package
    package = pkgs.plex.override {plexRaw = pkgs.plexRaw.overrideAttrs (oldAttrs: let
        version = "1.18.4.2171-ac2afe5f8"; # Update version here
        sha256 = "10x4cf1c826vj9gqr7r6k70rrjifmi36sd7imfi7pdw5swizjzqv"; # Update appropriate hash here
        in {
            name = "plex-override-${version}";
            src = pkgs.fetchurl {
                url = "https://downloads.plex.tv/plex-media-server-new/${version}/redhat/plexmediaserver-${version}.x86_64.rpm";
                inherit sha256;
            };
        }
    );};
};

Simple broken dependency override

When installing pgloader, I encountered an issue where it would fail to run due to The alien function "CRYPTO_num_locks" is undefined.. This apparently was caused by some sort of a dependency problem on certain OpenSSL versions. However, it is possible to manually override package dependencies like this, making sure that a certain package gets a specific version, without affecting other packages.

nixpkgs.config = {
    # Enable these if you need them
    # allowUnfree = true;
    # allowUnfreeRedistributable = true;
    # Override packages; in this case, downgrading pgloader's OpenSSL version
    packageOverrides = pkgs: rec {
      pgloader = pkgs.pgloader.override { openssl = pkgs.openssl_1_0_2;};
    };
  };

Geoblocking with NixOS standard firewall

NixOS integrated firewall is a relatively simple, iptables-based construction. Whilst not that featureful, it is on the flipside also relatively straightforward to extend to your needs. This example shows you how to geoblock your SSH port from outside Finland, as to limit the scope of possible attack attempts (with the beneficial side effect of reducing log spam).

Warnings

You are interacting with a firewall here. If you do it wrong, it is possible that you’ll lock yourself out, and have to resort to some other method of access - or worse, open unintended access avenues. You have been warned.

This geoblocking scheme is not complete protection - also enable Fail2Ban, and preferably disable password authentication entirely or even better, require 2FA.

Steps

First, get a list of IP networks you wish to allow. I sourced mine from FireHOL’s lists. Be sure to remove all comment lines - the helper script below will not handle those properly without modification! The file should only contain networks - e.g 10.0.0.0/8.

Create a helper script for adding appropriate rules. Per this example, it should be saved to /root/geoblock/geoblock_reload.sh, with the IP list saved to /root/geoblock/country_fi.netset, but these paths can be altered to suit better.

#!/bin/sh
set -e

ipset -exist create geofi hash:net
ipset flush geofi

while IFS= read -r ip
do
        ipset -A -! geofi "$ip";
done < "/root/geoblock/country_fi.netset"

iptables -A nixos-fw -p tcp --dport 22 -m set --match-set geofi src -j nixos-fw-accept

Alter your /etc/nixos/configuration.nix as follows:

# Run our geoblock reload command after rest of the firewall has been set up
networking.firewall.extraCommands = ''
     /root/geoblock/geoblock_reload.sh
'';
# Require that ipset is available to our firewall environment
networking.firewall.extraPackages = [pkgs.ipset];

# By default, the SSH service punches a hole to the firewall allowing SSH access. As we handle it ourself now, it is not desirable at all - and therefore disable this behavior
services.openssh.openFirewall = false;

Rebuild your system (preferably rebooting), and now you should have a geoblock-limited firewall. It should appear somewhat like this in iptables -L:

Chain nixos-fw (1 references)
target     prot opt source               destination         
nixos-fw-accept  all  --  anywhere             anywhere            
nixos-fw-accept  all  --  anywhere             anywhere             ctstate RELATED,ESTABLISHED

<snip snip>

nixos-fw-accept  tcp  --  anywhere             anywhere             tcp dpt:ssh match-set geofi src
nixos-fw-log-refuse  all  --  anywhere             anywhere  

Sudo, su and wheel group

It may be in some cases be preferable that sudo requires root password instead of user’s own password. This is how you do it.

In addition, this prevents SU (substitute user) from being used by non-wheel users. Regardless of what Richard Stallman says on the matter, I believe this is useful for a home server at least.

# Harden PAM configuration for SU - do not allow non-wheel to use.
security.pam.services.su.requireWheel = true;

# Sudo options
  security = {
    sudo = {
      enable = true;
      wheelNeedsPassword = true;
      extraConfig = ''
        Defaults rootpw
      '';
    };
  };

# Do not forget to give your users the wheel group. Have you done that yet with the extraGroups options for users?