Blog

The Ultimate NixOS Homelab Guide - Flakes, Modules and Fail2Ban w/ Cloudflare

Published on Jul 2, 2024

Welcome back everyone to the NixOS homelab guide, today we will be moving all our configurations into a very simple flake, setting up Vaultwarden with Fail2Ban and finally modularizing our configuration ready for future self hosted apps.

Contents

Flake Setup

Moving our configuration to a flake is pretty easy so let’s get straight to it.

Make a new directory in your home folder.

mkdir ~/.flake
cd ~/.flake

Now create a new flake.nix file at the root of that directory and open it in your text editor of choice.

vim flake.nix

Now fill out the file with the following, then I’ll explain and break it down.

{
  description = "A very basic flake";

  inputs = {
    nixpkgs.url = "github:nixos/nixpkgs/nixpkgs-unstable";
  };
  outputs = {
    self,
    nixpkgs,
    ...
  } @ inputs: let
    system = "x86_64-linux";
    version = "24.11";
    user = "your-username";
    hostname = "homelab";
    pkgs = import nixpkgs {
      inherit system;
      config = { allowUnfree = true; };
    };
    lib = nixpkgs.lib;
  in {
    nixosConfigurations = {
      ${hostname} = lib.nixosSystem {
        inherit system;
        specialArgs = { inherit user hostname version; };
        modules = [
          ./nix/configuration.nix
        ];
      };
    };
  };
}

We initialize a flake, add the unstable nixpkgs source, set some variables we can use through our entire config (make sure to set the hostname and user variables to whatever username and hostname you used in your configuration.nix, we set that file up in the last post) and then create the nixosSystem inheriting our variables and then importing configuration.nix which we will move into our flake directory right now.

mkdir ~/.flake/nix
sudo cp /etc/nixos/* ~/.flake/nix
sudo chown $USER:users ~/.flake/nix/*
sudo chmod a+rw ~/.flake/nix/*

Those commands will copy our existing configurations to our new flake directory and then make sure our user have all the write permissions.

You should now have this directory structure.

~/.flake
├── flake.nix
└── nix
    ├── configuration.nix
    └── hardware-configuration.nix

Now we need to update our configuration.nix to work with our flake and also add some utilities.

{ config, lib, pkgs, user, hostname, version, ... }:

{
  imports =
    [ # Include the results of the hardware scan.
      ./hardware-configuration.nix
    ];

  boot = {
    kernelPackages = pkgs.linuxPackages_latest;
    loader = {
      efi.canTouchEfiVariables = true;
      grub = {
        enable = true;
        efiSupport = true;
        device = "nodev";
      };
    };
  };

  nix = {
    # Enable flakes!
    settings = {
      experimental-features = [ "nix-command" "flakes" ];
      auto-optimise-store = true;
    };
  };

  nixpkgs.config.allowUnfree = true;

  networking = {
    hostName = "${hostname}";
    networkmanager = {
      enable = true;
    };
  };

  # Your timezone
  time.timeZone = "Australia/Sydney";

  users.users.${user} = {
    isNormalUser = true;
    extraGroups = [ "wheel" "docker" ]; # Enable ‘sudo’ for the user with wheel.
    home = "/home/${user}";
    shell = pkgs.zsh;
  };

  programs = {
    # Personal preference
    zsh.enable = true;

    nh = {
      enable = true;
      flake = "/home/${user}/.flake";
    };
  };

  environment.systemPackages = with pkgs; [
    vim
  ];

  # Enable the OpenSSH daemon.
  services.openssh.enable = true;

  system.stateVersion = "${version}";

}

Now our config will make use of the user, hostname and version variables we created in our flake.nix.

Now we are finally ready to apply our new config, run the below command. (replace homelab with whatever the hostname variable is in your flake.nix)

sudo nixos-rebuild boot --flake /home/$USER/.flake#homelab

If any errors come up please leave a comment below and I’ll be happy to help troubleshoot. If the command completes and there were no issues then you can just run sudo reboot.

Fail2Ban and Modularization

In this example I am setting up Fail2Ban for a local Vaultwarden server, you will need to adapt this based on the Fail2Ban instructions for your software.

Let’s start and creating a folder at ~/.flake/nix/modules

mkdir ~/.flake/nix/modules

In there let’s open up a new file and you can name it either fail2ban.nix or vaultwarden.nix if that is the service you are protecting. (name doesn’t matter that much its just semantics).

In that file start with this content:

{ pkgs, lib, config, user, ... }:

{
  services.fail2ban = {
    enable = true;
  };
}

Throughout this I’ll be referring to these pages: https://nixos.wiki/wiki/Fail2ban https://github.com/dani-garcia/vaultwarden/wiki/Fail2Ban-Setup https://github.com/fail2ban/fail2ban/blob/master/config/action.d/cloudflare.conf

Now I assume if your setting up Fail2Ban you understand how it works and what it is, so you may be asking, alright so how can I create stuff like Jails and custom actions and filters.

Well Nix has you covered. Let’s take a look at my Jail configuration for Vaultwarden.

{ pkgs, lib, config, user, ... }:

{
  services.fail2ban = {
    enable = true;
    jails = {
      vaultwarden.settings = {
        enabled = true;
        filter = "vaultwarden";
        action = ''
          cf
          iptables-allports
        '';
        # This is the path where I have my vaultwarden data
        logpath = "/home/${user}/vaultwarden/vw-data/vaultwarden.log";
        # User gets banned after 4 incorrect attempts
        maxretry = 4;
        bantime = "52w";
        findtime = "52w";
        chain = "FORWARD";
      };
    };
  };
}

“Ok awesome but what is the cf action and what about the vaultwarden filter, how do i set that up on NixOS when /etc is read-only?”

Fear not because here is environment.etc

{ pkgs, lib, config, user, ... }:

{
  environment.etc = {
    "fail2ban/filter.d/vaultwarden.local".text = pkgs.lib.mkDefault (pkgs.lib.mkAfter ''
      [INCLUDES]
      before = common.conf

      [Definition]
      failregex = ^.*Username or password is incorrect. Try again. IP: <ADDR>. Username:.*$
      ignoreregex =
    '');
  };

  services.fail2ban = {
    # ...
  };
}

You may recognise that string for the first child of etc it is the fail2ban path for custom filters!

Now let’s add the cf action which is for banning through Cloudflare. You only need this if you have made your Vaultwarden instance public via Cloudflare/-Tunnels.

  environment.etc = {
    "fail2ban/action.d/cf.conf".text = pkgs.lib.mkDefault (pkgs.lib.mkAfter ''
      [Definition]

      actionstart =
      actionstop =
      actioncheck =

      actionban = /run/current-system/sw/bin/curl -s -o /dev/null -X POST 
            -H "X-Auth-Email: <cfuser>" 
            -H "X-Auth-Key: <cftoken>" 
            -H "Content-Type: application/json" 
            -d '{"mode":"block","configuration":{"target":"ip","value":"<ip>"},"notes":"Fail2Ban <name>"}' 
            "https://api.cloudflare.com/client/v4/user/firewall/access_rules/rules"

      actionunban = /run/current-system/sw/bin/curl -s -o /dev/null -X DELETE -H 'X-Auth-Email: <cfuser>' -H 'X-Auth-Key: <cftoken>' 
            https://api.cloudflare.com/client/v4/user/firewall/access_rules/rules/$(/run/current-system/sw/bin/curl -s -X GET -H 'X-Auth-Email: <cfuser>' -H 'X-Auth-Key: <cftoken>' 
            'https://api.cloudflare.com/client/v4/user/firewall/access_rules/rules?mode=block&configuration_target=ip&configuration_value=&page=1&per_page=1' | tr -d '\n' | cut -d'"' -f6)

      [Init]
      cftoken = your-token

      cfuser = [email protected]

    '');
  };

Again you can see that the initial string just references the path as a child of /etc. It should be pretty conclusive from there how to add your own actions and filters. And if ever in doubt, check the NixOS Wiki and search.nixos.org for options relating to fail2ban.

As a side note, getting this to work took me literal days so yeah, haha.

Now to get this to work in our config add the following to your configuration.nix

{ config, lib, pkgs, user, hostname, version, ... }:

{
  imports = [
    ./hardware-configuration.nix
    ./modules/vaultwarden.nix
  ];
}

Now since we moved to a flake and I snuck nh into your configuration you can run the super simple command:

This will rebuild your system and switch over to it automatically based on the flake path we provided earlier, ~/.flake

Securing your Server

Well this is a pretty big post now isn’t it. The best way I secure my server is by disabling SSH via password and only allowing my personal private key.

This can be done as follows:

On your personal computer (the one with your private key)

You should also change your ~/.ssh/config on your PC to use that private key when connecting to your homeserver.

Host your.homelab.ip.address
  IdentityFile ~/.ssh/private

On your homeserver open up ~/.ssh/authorized_keys and copy the newly created string.

configuration.nix

{
  users.users.${user} = {
    # ...
    openssh.authorizedKeys = [
      # Paste your key into double quotes here
      # Example:
      "ssh-ed25519 AAAA------------ jasper@nixos"
    ];
  };

  services = {
    openssh = {
      enable = true;
      settings = {
        PasswordAuthentication = false;
      };
    };
  };
}

Then rebuild, this time though we will use the boot option and restart the server.

nh os boot
reboot

Now on your personal computer, open a new terminal and try to connect to the server via SSH. If you connect with the password being requested then hurray, you have just secured your server!

If you have any troubles please leave a comment or contact me on Discord (jasper_clarke) and I will try to help out!

This has been Jasper, until next time 👋