add restricted network nixos modules
See README.md for explanation
This commit is contained in:
@@ -26,6 +26,7 @@ in
|
||||
{
|
||||
flake.nixosModules.hidden-ssh-announce = ./hidden-ssh-announce.nix;
|
||||
flake.nixosModules.bcachefs = ./bcachefs.nix;
|
||||
flake.nixosModules.user-firewall = ./user-firewall;
|
||||
flake.nixosModules.installer.imports = [
|
||||
./installer
|
||||
self.nixosModules.hidden-ssh-announce
|
||||
|
||||
179
nixosModules/user-firewall/README.md
Normal file
179
nixosModules/user-firewall/README.md
Normal file
@@ -0,0 +1,179 @@
|
||||
# User Firewall Module
|
||||
|
||||
This NixOS module provides network access restrictions for non-privileged users, ensuring they can only access local services and VPN interfaces while blocking direct internet access.
|
||||
|
||||
## Overview
|
||||
|
||||
The `user-firewall` module implements firewall rules that:
|
||||
- Block all outbound network traffic for normal (non-system) users by default
|
||||
- Allow specific users to bypass restrictions (exemptUsers)
|
||||
- Permit traffic on specific interfaces (like VPNs and localhost)
|
||||
- Support both iptables and nftables backends
|
||||
- Handle both IPv4 and IPv6 traffic
|
||||
|
||||
## Installation
|
||||
|
||||
Add the module to your NixOS configuration:
|
||||
|
||||
```nix
|
||||
{
|
||||
imports = [
|
||||
self.inputs.clan-core.nixosModules.user-firewall
|
||||
];
|
||||
}
|
||||
```
|
||||
|
||||
The module is automatically enabled once imported. It will immediately start restricting network access for all normal users except those listed in `exemptUsers`.
|
||||
|
||||
## Configuration
|
||||
|
||||
### Basic Usage
|
||||
|
||||
```nix
|
||||
{
|
||||
networking.user-firewall = {
|
||||
exemptUsers = [ "alice" ]; # Users who can access the internet
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
### Full Configuration Example
|
||||
|
||||
```nix
|
||||
{
|
||||
networking.user-firewall = {
|
||||
# Users who are exempt from network restrictions
|
||||
exemptUsers = [
|
||||
"alice"
|
||||
"admin"
|
||||
];
|
||||
|
||||
# Network interfaces that all users can use
|
||||
# Default includes common VPN interfaces
|
||||
allowedInterfaces = [
|
||||
"lo" # localhost (required for local services)
|
||||
"tun*" # OpenVPN, OpenConnect
|
||||
"wg*" # WireGuard (wg0, wg-home, etc.)
|
||||
"tailscale*" # Tailscale
|
||||
# Add custom interfaces as needed
|
||||
];
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
## How It Works
|
||||
|
||||
1. **User Classification**: The module automatically identifies all normal users (non-system users) and applies restrictions to those not in the `exemptUsers` list.
|
||||
|
||||
2. **Firewall Rules**:
|
||||
- For iptables: Creates a custom chain `user-firewall-output` in the OUTPUT table
|
||||
- For nftables: Creates a table `inet user-firewall` with an output chain
|
||||
- Rules check outgoing packets and reject those from restricted users
|
||||
|
||||
3. **Interface Patterns**: Supports wildcards in interface names:
|
||||
- `*` matches any characters (e.g., `wg*` matches `wg0`, `wg-home`)
|
||||
|
||||
## Default Allowed Interfaces
|
||||
|
||||
The module comes with sensible defaults for common VPN and overlay network interfaces:
|
||||
|
||||
- `lo` - Loopback (localhost access)
|
||||
- `tun*` - OpenVPN, OpenConnect
|
||||
- `tap*` - OpenVPN (bridged mode)
|
||||
- `wg*` - WireGuard
|
||||
- `tailscale*` - Tailscale
|
||||
- `zt*` - ZeroTier
|
||||
- `hyprspace` - Hyprpspace
|
||||
- `vpn*` - Generic VPN interfaces
|
||||
- `nebula*` - Nebula mesh network
|
||||
- `tinc*` - Tinc VPN
|
||||
- `edge*` - n2n
|
||||
- `ham0` - Hamachi
|
||||
- `easytier` - EasyTier
|
||||
- `mycelium` - Mycelium
|
||||
|
||||
## Use Cases
|
||||
|
||||
### 1. Public Kiosk Systems
|
||||
Restrict users to only access local services:
|
||||
```nix
|
||||
{
|
||||
networking.user-firewall = {
|
||||
allowedInterfaces = [ "lo" ]; # Only localhost
|
||||
exemptUsers = [ ]; # No exempt users
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Corporate Workstations
|
||||
Force all traffic through corporate VPN:
|
||||
```nix
|
||||
{
|
||||
networking.user-firewall = {
|
||||
allowedInterfaces = [ "lo" "wg-corp" ];
|
||||
exemptUsers = [ "sysadmin" ];
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
## Testing
|
||||
|
||||
The module includes comprehensive tests for both iptables and nftables backends:
|
||||
|
||||
```bash
|
||||
# Run iptables backend test
|
||||
nix build .#checks.x86_64-linux.user-firewall-iptables
|
||||
|
||||
# Run nftables backend test
|
||||
nix build .#checks.x86_64-linux.user-firewall-nftables
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Check Active Rules
|
||||
|
||||
The output includes package counters for each firewall rule, that can help to debug connectivity issues.
|
||||
|
||||
For iptables:
|
||||
```bash
|
||||
sudo iptables -L user-firewall-output -n -v
|
||||
sudo ip6tables -L user-firewall-output -n -v
|
||||
```
|
||||
|
||||
For nftables:
|
||||
```bash
|
||||
sudo nft list table inet user-firewall
|
||||
|
||||
# Watch counters in real-time
|
||||
sudo watch -n1 'nft list table inet user-firewall'
|
||||
```
|
||||
|
||||
Check which rule your VPN traffic is hitting. If packets are being rejected, verify:
|
||||
1. Your VPN interface name matches the patterns in `allowedInterfaces`
|
||||
2. Your user is listed in `exemptUsers` if needed
|
||||
|
||||
To see your current network interfaces:
|
||||
```bash
|
||||
ip link show | grep -E '^[0-9]+:'
|
||||
```
|
||||
|
||||
### Common Issues
|
||||
|
||||
1. **Service Connection Failures**: If local services fail to connect, ensure `lo` is in `allowedInterfaces`.
|
||||
|
||||
2. **VPN Not Working**: Check that your VPN interface name matches the patterns in `allowedInterfaces`. You can find your interface name with `ip link show`.
|
||||
|
||||
3. **User Still Has Access**: Verify the user is a normal user (not a system user) and not in `exemptUsers`.
|
||||
|
||||
## Security Considerations
|
||||
|
||||
- This module provides defense in depth but should not be the only security measure
|
||||
- System users (like `nginx`, `systemd-*`) are not restricted
|
||||
- Root user always has full network access
|
||||
- Restrictions apply at the packet filter level, not application level
|
||||
|
||||
## Limitations
|
||||
|
||||
- Requires `networking.firewall.enable = true`
|
||||
- Cannot restrict system users or root
|
||||
- Interface patterns are evaluated at rule creation time, not dynamically
|
||||
147
nixosModules/user-firewall/default.nix
Normal file
147
nixosModules/user-firewall/default.nix
Normal file
@@ -0,0 +1,147 @@
|
||||
{
|
||||
config,
|
||||
lib,
|
||||
pkgs,
|
||||
...
|
||||
}:
|
||||
let
|
||||
cfg = config.networking.user-firewall;
|
||||
|
||||
# Get all normal users (excluding system users)
|
||||
normalUsers = lib.filterAttrs (_name: user: user.isNormalUser) config.users.users;
|
||||
|
||||
# Get usernames for normal users who aren't exempt
|
||||
restrictedUsers = lib.attrNames (
|
||||
lib.filterAttrs (name: _user: !(lib.elem name cfg.exemptUsers)) normalUsers
|
||||
);
|
||||
|
||||
# Convert interface patterns for iptables
|
||||
# iptables uses + for one-or-more, but we use * in our interface
|
||||
toIptablesPattern =
|
||||
pattern:
|
||||
if lib.hasSuffix "*" pattern && pattern != "*" then
|
||||
# Convert "wg*" to "wg+" for iptables
|
||||
lib.removeSuffix "*" pattern + "+"
|
||||
else
|
||||
pattern;
|
||||
|
||||
# Build interface patterns for iptables with proper escaping
|
||||
interfaceRules = lib.concatMapStringsSep "\n " (
|
||||
iface:
|
||||
"ip46tables -A user-firewall-output -o ${lib.escapeShellArg (toIptablesPattern iface)} -j RETURN"
|
||||
) cfg.allowedInterfaces;
|
||||
|
||||
in
|
||||
{
|
||||
options.networking.user-firewall = {
|
||||
allowedInterfaces = lib.mkOption {
|
||||
type = lib.types.listOf lib.types.str;
|
||||
default = [
|
||||
"lo" # loopback (allows Tor on localhost)
|
||||
"tun*" # OpenVPN, OpenConnect
|
||||
"tap*" # OpenVPN (bridged mode)
|
||||
"wg*" # WireGuard (wg0, wg-home, etc.)
|
||||
"tailscale*" # Tailscale
|
||||
"zt*" # ZeroTier
|
||||
"vpn*" # Generic VPN interfaces
|
||||
"ipsec*" # IPSec
|
||||
"nebula*" # Nebula
|
||||
"tinc*" # Tinc
|
||||
"edge*" # n2n
|
||||
"hyprspace" # Hyprspace
|
||||
"ham0" # Hamachi
|
||||
"easytier" # EasyTier
|
||||
"mycelium" # Mycelium
|
||||
];
|
||||
description = ''
|
||||
Network interfaces that normal users can use.
|
||||
Supports wildcards: * (zero or more characters).
|
||||
'';
|
||||
};
|
||||
|
||||
exemptUsers = lib.mkOption {
|
||||
type = lib.types.listOf lib.types.str;
|
||||
default = [ ];
|
||||
description = ''
|
||||
Users exempt from network restrictions.
|
||||
'';
|
||||
};
|
||||
|
||||
};
|
||||
|
||||
config = {
|
||||
assertions = [
|
||||
{
|
||||
assertion = config.networking.firewall.enable;
|
||||
message = "networking.user-firewall requires networking.firewall.enable to be true";
|
||||
}
|
||||
];
|
||||
|
||||
# For nftables: create fake passwd file for build-time validation
|
||||
networking.nftables.checkRulesetRedirects = lib.mkIf config.networking.nftables.enable (
|
||||
lib.mkOptionDefault {
|
||||
"/etc/passwd" = pkgs.writeText "passwd" (
|
||||
let
|
||||
userList = lib.attrNames config.users.users;
|
||||
indexedUsers = lib.imap0 (
|
||||
i: name: "${name}:x:${toString (1000 + i)}:100::/home/${name}:/bin/sh"
|
||||
) userList;
|
||||
in
|
||||
lib.concatStringsSep "\n" indexedUsers
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
# For iptables backend
|
||||
networking.firewall.extraCommands = lib.mkIf (!config.networking.nftables.enable) ''
|
||||
# Create custom chain for user firewall output
|
||||
ip46tables -N user-firewall-output 2>/dev/null || true
|
||||
ip46tables -F user-firewall-output
|
||||
|
||||
# Allow traffic on permitted interfaces
|
||||
${interfaceRules}
|
||||
|
||||
|
||||
# Reject traffic from restricted users (TCP RST for TCP, ICMP for UDP)
|
||||
${lib.concatMapStringsSep "\n " (
|
||||
user: "ip46tables -A user-firewall-output -m owner --uid-owner ${lib.escapeShellArg user} -j REJECT"
|
||||
) restrictedUsers}
|
||||
|
||||
# Allow all other traffic
|
||||
ip46tables -A user-firewall-output -j RETURN
|
||||
|
||||
# Insert our chain at the beginning of OUTPUT
|
||||
ip46tables -D OUTPUT -j user-firewall-output 2>/dev/null || true
|
||||
ip46tables -I OUTPUT -j user-firewall-output
|
||||
'';
|
||||
|
||||
networking.firewall.extraStopCommands = lib.mkIf (!config.networking.nftables.enable) ''
|
||||
# Remove our custom chain
|
||||
ip46tables -D OUTPUT -j user-firewall-output 2>/dev/null || true
|
||||
ip46tables -F user-firewall-output 2>/dev/null || true
|
||||
ip46tables -X user-firewall-output 2>/dev/null || true
|
||||
'';
|
||||
|
||||
# For nftables backend
|
||||
networking.nftables.tables = lib.mkIf config.networking.nftables.enable {
|
||||
user-firewall = {
|
||||
family = "inet";
|
||||
content = ''
|
||||
chain output {
|
||||
type filter hook output priority 0; policy accept;
|
||||
|
||||
# Allow traffic on permitted interfaces
|
||||
${lib.concatMapStringsSep "\n " (
|
||||
iface: ''oifname "${iface}" counter accept comment "allow ${iface}"''
|
||||
) cfg.allowedInterfaces}
|
||||
|
||||
# Reject traffic from restricted users (TCP RST for TCP, ICMP for UDP)
|
||||
${lib.concatMapStringsSep "\n " (
|
||||
user: ''meta skuid "${user}" counter reject comment "blocked user ${user}"''
|
||||
) restrictedUsers}
|
||||
}
|
||||
'';
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user