Linux

DIY Linux Router - Part 3 - Users, Security and Firewall

This is the third part of a multi-part series describing how to build your own Linux router.

In the first and second parts, we installed the operating system, configured the network, and set up the Mac Mini to work as a router.
In this part, we will increase security by creating users, changing SSH authentication, and hardening the firewall configuration.

Fire of wall
EAA AirVenture Oshkosh 2013 Wall of Fire

Table of Contents

Users

Create intended users. You can create whatever user you need. In my case, I will have three: one to act as an administrator user named admin, another for rootless containers as podman, and another named git to have a personal and private Git repository.

For the podman user, if you're using the impermanence Storage, you need to allocate the subuid and subgid ranges for the user.

1. Generate Hashed Password (optional)

This step is optional, as the intended way to authenticate on the server is through SSH using SSH Keys, but you can create a password if you want to ask for one when using sudo or authenticating locally.

Create the password for the users:

mkpasswd --method=SHA-512
Password: #type the password (hackme00)
$6$ZeLsWXraGkrm9IDL$Y0eTIK4lQm8D0Kj7tSVzdTJ/VbPKea4ZPf0YaJR68Uz4MWCbG1EJp2YBOfWHNSZprZpjpbUvCIozbkr8yPNM0.

2. SSH Keys

Generate the private and public key pair or use an existing one. More about it at this Github's article.

Execute the following SSH key generation steps on your computer.

ssh-keygen -t ed25519 -C "[email protected]" -f ~/.ssh/router-admin
Generating public/private ed25519 key pair.
Enter passphrase (empty for no passphrase): 
Enter the same passphrase again: 
Your identification has been saved in /root/.ssh/router-admin
Your public key has been saved in /root/.ssh/router-admin.pub
The key fingerprint is...

Retrieve your SSH public keys and copy the content:

cat ~/.ssh/router-admin.pub
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC... [email protected]

Keep your private key safe and do not share it with anyone.

Repeat the same process for every user you want to create.

3. Create `users.nix` in `/etc/nixos/modules/`

Access the server via SSH using the user root and the password defined during the installation in part 1 of this tutorial and do as follows:

Set the intended users by replacing the values ​​in openssh.authorizedKeys.keys. In the example, the key for the admin user should be filled with the value from ~/.ssh/router-admin.pub.

/etc/nixos/modules/users.nix

{ config, pkgs, ... }: {
  users.users.root.initialHashedPassword = "##HashedPa$$word"; # You can remove this line if you do not want to log directly with root user.

  users.users = {
    # Admin user
    admin = {
      uid = 1000;
      isNormalUser = true;
      description = "Administrator User";
      home = "/home/admin"; # Home directory
      extraGroups = [ "wheel" ]; # Add the user to the 'wheel' group for sudo access
      initialHashedPassword = "$6$rounds=656000$example$hashedpassword"; # Password, optional
      openssh.authorizedKeys.keys = [
        "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC..."  # Replace with the actual public key
      ];
    };

    # Podman User for rootless pods
    podman = {
      uid = 1001;
      subUidRanges = [{ startUid = 100000; count = 65536; }];
      subGidRanges = [{ startGid = 100000; count = 65536; }];
      isNormalUser = true;
      description = "Podman Rootless";
      home = "/home/podman";
      group = "containers";
      openssh.authorizedKeys.keys = [
        "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC..."  # Replace with the actual public key
      ];
      linger = true; # Lingering enables systemd user services to start up without logging into user account.
    };

    # Git user
    git = {
      uid = 1002;
      isNormalUser = true;
      description = "Git";
      home = "/home/git";
      openssh.authorizedKeys.keys = [
        "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC..."  # Replace with the actual public key
      ];
    };
  };
  users.groups.containers = {
    gid = 993;
    members = [ "podman" ];
  }

  # Enable sudo for users in the 'wheel' group
  security.sudo = {
    enable = true;
    wheelNeedsPassword = false;  # Optional: Requires a password to be entered for the sudo command. Set to true if you choose to create the admin user with a password.
  };
}

4. Disable password authentication over SSH

Disable password authentication and root login through SSH.

/etc/nixos/modules/services.nix

  services = { 
  ...
    openssh = {
      enable = true;
      settings.PermitRootLogin = "no"; # Was "yes". Change to "no" to disable
      settings.PasswordAuthentication = false;
    };
  ...
  };

5. Update the configuration and try to log in

Rebuild the config:

nixos-rebuild switch

Try to log in to the server using the admin using the private key generated earlier.

ssh -i ~/.ssh/router-admin [email protected]

6. Create the SSH configuration file

If you do not want to type ssh -i ~/.ssh/router-admin [email protected] everytime to authenticate into the serverm, on your computer, configure the file ~/.ssh/config as following:

Host router-admin
  Hostname 10.1.78.1
  user admin
  IdentityFile ~/.ssh/router-admin

Host router-podman
  Hostname 10.1.78.1
  user podman
  IdentityFile ~/.ssh/router-podman

Host router-git
  Hostname 10.1.78.1
  user git
  IdentityFile ~/.ssh/router-git

Try sshing to the server without providing the identity file as a parameter.

ssh router-admin

7. Lock the root Account (optional)

Since the SSH service is configured to prevent root logins, I don't see the need to lock the root account, but you can do so if you want.
If you configured your server with impermanence storage, remove the line users.users.root.initialHashedPassword = "##HashedPa$$word" from the users.nix to lock out the root password without further steps.

CAUTION Be aware that locking the root password without a password for the admin account will prevent you from logging in locally, but only via SSH. Also, make sure you have created the admin account and added it to the wheel group.

passwd -l root

Firewall

With our user configurations complete, it's time to enhance our firewall security.

Current Setup

So far, our firewall configuration includes:

  • Allowing all incoming traffic from the LAN network.
  • Blocking all incoming traffic from WAN/PPPoE, Guest, and IoT networks except for internet access.

While this setup provides a basic level of security, we can achieve better protection through more granular traffic control. By doing so, we ensure that if any service unintentionally opens additional ports on the server, unauthorized traffic will not be allowed through.

Planned Enhancements

We will refine our firewall to allow only the traffic necessary for each network:

  • LAN network: Allow DHCP and SSH services.
  • Guest and IoT networks: Allow only DHCP service.
  • WAN: Enable SSH to remote access.

Organizing the Firewall Configuration

To simplify management and ensure scalability, we will structure our firewall configuration into logical sections. This approach divides interfaces, rules, and services into zones and organizes the configuration across multiple files. The structure will look as follows:

**INET Table** (Main firewall rules)

  • sets.nft: Map interfaces to their respective zones.
  • services.nft: Define chains for service ports, such as SSH and HTTP.
  • zones.nft: Specify which services are allowed in each zone.
  • rules.nft: Configure rules for zones and manage traffic flow.

**NAT Table** (Network Address Translation rules)

  • nat_sets.nft: Map interfaces to their respective zones.
  • nat_chains.nft: Define NAT chains for tasks like port redirection.
  • nat_zones.nft: Associate NAT chains with zones.
  • nat_rules.nft: Configure NAT rules for zones.

This modular approach will make the firewall configuration more organized, easier to understand, and simpler to maintain or extend in the future.

Set up files

1. Remove the nftables.nft file

As we will split the nftables into discrete files, there's no need to use this file anymore. You can delete or just let inactive.

rm /etc/nixos/modules/nftables.nft

2. Create the the NFTables Configuration Files

Create the directory and all NFTables files needed.

mkdir -p /etc/nixos/nftables
touch /etc/nixos/nftables/{nat_chains,nat_rules,nat_sets,nat_zones,rules,services,sets,zones}.nft

Configure every intended NFTables file.

sets.nft

Let's make use of variables to address the interfaces name dinamically.

cat << EOF > /etc/nixos/nftables/sets.nft 
table inet filter {
  set WAN {
    type ifname;
    elements = { \$if_wan }
  }

  set LAN {
    type ifname;
    elements = { \$if_lan }
  }

  set GUEST {
    type ifname;
    elements = { \$if_guest }
  }

  set IOT {
    type ifname;
    elements = { \$if_iot }
  }
}
EOF
services.nft
cat << EOF > /etc/nixos/nftables/services.nft
table inet filter {
  chain dhcp_input {
    udp dport 67 ct state { new, established } counter accept comment "Allow DHCP"
  }

  chain echo_input {
    icmp type echo-request accept
    icmp type echo-reply accept
  }

  chain public_ssh_input {
    tcp dport ssh ct state { new, established } limit rate 10/minute burst 50 packets counter accept comment "Allow SSH traffic with rate limiting"
  }

  chain ssh_input {
    tcp dport ssh ct state { new, established } counter accept comment "Allow SSH"
  }
}
EOF 
zones.nft
cat << EOF > /etc/nixos/nftables/zones.nft
table inet filter {
  chain LAN_INPUT {
    jump dhcp_input
    jump echo_input
    jump ssh_input
  }

  chain GUEST_INPUT {
    jump dhcp_input
    jump echo_input
  }

  chain IOT_INPUT {
    jump dhcp_input
    jump echo_input
  }

  chain WAN_INPUT {
    jump public_ssh_input
  }
}
EOF 
rules.nft
cat << EOF > /etc/nixos/nftables/rules.nft
table inet filter {
  chain input {
    type filter hook input priority filter; policy drop;
    iifname "lo" counter accept

    iifname @LAN jump LAN_INPUT 
    iifname @GUEST jump GUEST_INPUT
    iifname @IOT jump IOT_INPUT
    iifname @WAN jump WAN_INPUT
 
  
    # Allow returning traffic from ppp0 and drop everything else
    iifname @LAN ct state { established, related } counter accept
    iifname @WAN ct state { established, related } counter accept
  }

  chain output {
    type filter hook output priority 100; policy accept;
  }

  chain forward {
    type filter hook forward priority filter; policy drop;
    iifname @LAN  oifname @WAN counter accept comment "Allow trusted LAN to WAN"
    iifname @WAN oifname @LAN ct state established,related counter accept comment "Allow established back to LANs"
    
    iifname @GUEST  oifname @WAN counter accept comment "Allow trusted GUEST to WAN"
    iifname @WAN oifname @GUEST ct state established,related counter accept comment "Allow established back to GUEST"
  
    iifname @IOT  oifname @WAN counter accept comment "Allow trusted IOT to WAN"
    iifname @WAN oifname @IOT ct state established,related counter accept comment "Allow established back to IOT"

    #Drop traffic between networks
    iifname @GUEST oifname @LAN drop comment "Drop connections from GUEST to LAN"
    iifname @IOT oifname @LAN drop comment "Drop connections from IOT to LAN"
    iifname @GUEST oifname @IOT drop comment "Drop connections from GUEST to IOT"
    iifname @IOT oifname @GUEST drop comment "Drop connection from IOT to GUEST"
    
    #MSS Clamp
    oifname @WAN tcp flags syn tcp option maxseg size set 1452
  }
}
EOF 
nat_sets.nft
cat << EOF > /etc/nixos/nftables/nat_sets.nft
table nat {
  set WAN {
    type ifname;
    elements = { \$if_wan }
  }

  set LAN {
    type ifname;
    elements = { \$if_lan }
  }

  set GUEST {
    type ifname;
    elements = { \$if_guest }
  }

  set IOT {
    type ifname;
    elements = { \$if_iot }
  }
}
EOF
nat_chains.nft

NAT Chains will be created as empty for now.

cat << EOF > /etc/nixos/nftables/nat_chains.nft 
table ip nat {
}
EOF
nat_zones.nft

As far as there's no redirect chains, NAT zones will be created with empty chains for now.

cat << EOF > /etc/nixos/nftables/nat_zones.nft
table ip nat {
  chain LAN_PREROUTING {
  }

  chain GUEST_PREROUTING {
  }

  chain IOT_PREROUTING {
  }

  chain WAN_PREROUTING {
  }
}
EOF
nat_rules.nft
cat << EOF > /etc/nixos/nftables/nat_rules.nft
table ip nat {
  chain prerouting {
    type nat hook prerouting priority filter; policy accept;
    iifname @LAN jump LAN_PREROUTING 
    iifname @GUEST jump GUEST_PREROUTING 
    iifname @IOT jump IOT_PREROUTING 
    iifname @WAN jump WAN_PREROUTING 
  }

  chain postrouting {
    type nat hook postrouting priority filter; policy accept;
    oifname @WAN tcp flags syn tcp option maxseg size set 1452
    oifname @WAN masquerade
  }
}
EOF

3. Update the networking.nix Configuration File

Edit the network section of networking.nix configuration file as follows:
Update just the networking section. Let the remaining of the file as is.

/etc/nixos/modules/networking.nix

...
  networking = {
    useDHCP = false;
    firewall.enable = false;
    nftables = {
      enable = true;
      rulesetFile = pkgs.writeText "ruleset.conf" ''
        define if_wan = "ppp0"
        define if_lan = "${lan}"
        define if_guest = "${guest}"
        define if_iot =  "${iot}"
        define ip_lan = "${ip_lan}"
        define ip_guest = "${ip_guest}"
        define ip_iot = "${ip_iot}"
        
        # Inet filter, services and rules
        include "${../nftables/sets.nft}"
        include "${../nftables/services.nft}"
        include "${../nftables/zones.nft}"
        include "${../nftables/rules.nft}"

        # Nat & redirect
        include "${../nftables/nat_sets.nft}"
        include "${../nftables/nat_chains.nft}"
        include "${../nftables/nat_zones.nft}"
        include "${../nftables/nat_rules.nft}"
      '';
      flattenRulesetFile = true;
    };
  };
...

4. Rebuild the configuration and test

nixos-rebuild switch

Conclusion

With this configuration, we have improved the security of this firewall. What has been done so far:

  • Reduced the need to use the root account for certain tasks.
  • Divided our firewall into zones, making it easier to manage.
  • Created more granular control over the traffic on our server.
    . In the next part, it is time to install Podman and configure our DNS Server with Unbound on it.
  • Part 4: Podman and Unbound

keywords: macmini • router • linux • nixos • pppoe • unifi • ubiquiti • apple • vlan • tl-sg108e

This article in other languages