DEV Community

Cover image for The Ultimate NixOS Homelab Guide - Flakes, Modules and Fail2Ban w/ Cloudflare
Jasper
Jasper

Posted on • Edited on • Originally published at jasperclarke.com

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

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.

Sorry for the huge wait, I had some issues and personal problems that cropped up but I'm back!

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
        ];
      };
    };
  };
}
Enter fullscreen mode Exit fullscreen mode

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/*
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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}";

}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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;
  };
}
Enter fullscreen mode Exit fullscreen mode

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";
      };
    };
  };
}
Enter fullscreen mode Exit fullscreen mode

"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 = {
    # ...
  };
}
Enter fullscreen mode Exit fullscreen mode

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 = jasper-at-windswept@example.com

    '');
  };
Enter fullscreen mode Exit fullscreen mode

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
  ];
}
Enter fullscreen mode Exit fullscreen mode

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

  • nh os switch

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)

  • ssh-copy-id -i ~/.ssh/private_key.pub admin@your.homelab.ip.address

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
Enter fullscreen mode Exit fullscreen mode
  • KEEP A LOGGED IN TERMINAL TO YOUR SERVER OPEN BEFORE REBUILDING WITH PASSWORDS DISABLED AS IF ANYTHING GOES WRONG YOU WILL BE LOCKED OUT!!!

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;
      };
    };
  };
}
Enter fullscreen mode Exit fullscreen mode

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

nh os boot
reboot
Enter fullscreen mode Exit fullscreen mode

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-at-windswept) and I will try to help out!

That is all for this post, make sure to follow and like the post if you found it helpful and I will see you all in the next one.

Top comments (0)