Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .vscode/tasks.json
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,9 @@
"options": [
"common",
"git",
"nftables",
"podman",
"router",
"scripts",
"shell",
"ssh",
Expand Down
94 changes: 92 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,12 @@ SSH.

| Tag | Role | Configuration |
| --- | --- | --- |
| `common` | `common` | Base development packages and Node.js 20 |
| `systemd` | `systemd` | `systemd-resolved` and NetworkManager DNS integration |
| `common` | `common` | Base development packages and Node.js 24 LTS |
| `systemd` | `systemd` | `systemd-resolved`, mDNS, NSS, and NetworkManager DNS integration |
| `ssh` | `ssh_hardening` | OpenSSH server installation and ssh-audit-based cryptographic hardening |
| `podman` | `podman` | Rootless Podman and Podman Compose configuration |
| `nftables` | `nftables` | Default input firewall with SSH, ICMP, and mDNS access |
| `router` | `router` | Optional IPv4 router with DHCP, forwarding, and NAT |
| `shell` | `shell_config` | Fish, Starship, direnv, fzf, pyenv, fonts, and Konsole profile |
| `scripts` | `system_scripts` | Helper scripts in `/usr/local/bin` |
| `vscode` | `vscode` | VS Code repository, application, settings, and extensions |
Expand All @@ -24,6 +26,94 @@ overridden with `ssh_hardening_*` variables. Ensure key-based access works
before deploying the role remotely; it does not configure a firewall or
Fail2Ban.

## Testing DNS and mDNS

After deployment, verify that `systemd-resolved` is active and Avahi is not
running:

```bash
systemctl is-active systemd-resolved.service
systemctl is-active avahi-daemon.service
```

The first command must return `active`. The second must return `inactive` or
report that the unit does not exist.

Check the active network link, replacing `wlp2s0` with the appropriate
interface name:

```bash
resolvectl status wlp2s0
```

The output should list the DHCP-provided DNS server (for example,
`192.168.1.1`), the `fritz.box` search domain, and `+mDNS` under `Protocols`.

From another device on the same Layer-2 network segment, resolve the
workstation hostname:

```bash
resolvectl query workstation.local
```

Replace `workstation` with the target's hostname. The command should return
the target's IPv4 and/or IPv6 address. To confirm that the input filter loaded
the mDNS rules, run on the target:

```bash
sudo nft list chain inet filter input
```

The output must include UDP port `5353` rules for `224.0.0.251` (IPv4) and
`ff02::fb` (IPv6).

To inspect the mDNS traffic directly, run the following command on the target
while resolving its `.local` hostname from another device:

```bash
sudo tcpdump -ni wlp2s0 'udp port 5353'
```

Replace `wlp2s0` with the active network interface. The capture should show
UDP packets to `224.0.0.251.5353` (IPv4) and/or `ff02::fb.5353` (IPv6).
mDNS is link-local and does not cross routers; use unicast DNS for names in a
different IP subnet.

## Router Configuration

The `router` role is disabled by default to avoid changing a normal workstation
into a gateway. Enable it for the intended host in inventory variables and
configure the LAN interface that serves DHCP, the WAN interface used for NAT,
and the addresses announced to DHCP clients:

```yaml
router_enabled: true
router_lan_interface: usbnet
router_lan_mac_address: "3c:18:a0:0f:72:63"
router_wan_interface: wlp1s0
router_lan_address: 192.168.100.1/24
router_lan_subnet: 192.168.100.0/24
router_dhcp_gateway: 192.168.100.1
router_dhcp_dns_servers:
- 192.168.100.1
```

The `nftables` role is enabled by default. It configures a default-drop input
filter with access for established connections, loopback, ICMP/ICMPv6, SSH, and
mDNS. It writes `/etc/nftables.conf` with `flush ruleset` and includes
`/etc/nftables.d/*.nft`, creates `/etc/nftables.d/10-filter.nft`, and manages
the `nftables` service. To prevent conflicting input hooks, it disables
`firewalld` when that service is installed.

The optional `router` role assigns the USB LAN adapter a stable interface name
using its MAC address in `/etc/systemd/network/10-router-lan.link`, configures
IPv4 forwarding, writes `/etc/systemd/network/10-router-lan.network`, and manages
`systemd-networkd`. It creates `/etc/nftables.d/20-router.nft`, which permits
LAN access to the router and adds forwarding plus source NAT. The DHCP pool and
lease durations are configurable through the `router_dhcp_*` variables in the
role defaults. A new `.link` file takes effect when the adapter is next
initialized; reboot the host or reconnect the adapter after deployment.

## Requirements

- Debian or Ubuntu system
Expand Down
3 changes: 3 additions & 0 deletions ansible/inventories/production/host_vars/localhost.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
---
router_enabled: false
router_wan_interface: wlp2s0
4 changes: 4 additions & 0 deletions ansible/playbooks/workstation.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@
tags: [git]
- role: podman
tags: [podman]
- role: nftables
tags: [nftables, router]
- role: router
tags: [router]
- role: shell_config
tags: [shell]
- role: ssh_hardening
Expand Down
5 changes: 5 additions & 0 deletions ansible/roles/common/handlers/main.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
- name: Refresh NodeSource apt package cache
ansible.builtin.apt:
update_cache: true
become: true
6 changes: 5 additions & 1 deletion ansible/roles/common/tasks/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,16 @@
ansible.builtin.deb822_repository:
name: nodesource
types: deb
uris: https://deb.nodesource.com/node_20.x
uris: https://deb.nodesource.com/node_24.x
suites: nodistro
components: main
signed_by: /etc/apt/keyrings/nodesource.gpg
state: present
become: true
notify: Refresh NodeSource apt package cache

- name: Refresh NodeSource apt package cache before package installation
ansible.builtin.meta: flush_handlers

- name: Update apt package cache
ansible.builtin.apt:
Expand Down
6 changes: 6 additions & 0 deletions ansible/roles/nftables/handlers/main.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
- name: Restart nftables
ansible.builtin.systemd:
name: nftables
state: restarted
become: true
65 changes: 65 additions & 0 deletions ansible/roles/nftables/tasks/main.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
- name: Install nftables
ansible.builtin.apt:
name:
- nftables
state: present
update_cache: true
become: true

- name: Ensure nftables rules directory exists
ansible.builtin.file:
path: /etc/nftables.d
state: directory
owner: root
group: root
mode: "0755"
become: true

- name: Configure nftables base configuration
ansible.builtin.copy:
content: |
#!/usr/sbin/nft -f

flush ruleset

include "/etc/nftables.d/*.nft"
dest: /etc/nftables.conf
owner: root
group: root
mode: "0644"
become: true
notify: Restart nftables

- name: Configure nftables input filter
ansible.builtin.template:
src: 10-filter.nft.j2
dest: /etc/nftables.d/10-filter.nft
owner: root
group: root
mode: "0644"
become: true
notify: Restart nftables

- name: Validate assembled nftables configuration
ansible.builtin.command:
cmd: /usr/sbin/nft -c -f /etc/nftables.conf
become: true
changed_when: false

- name: Gather service facts
ansible.builtin.service_facts:

- name: Disable firewalld
ansible.builtin.systemd:
name: firewalld
enabled: false
state: stopped
become: true
when: ansible_facts.services['firewalld.service'] is defined

- name: Enable nftables
ansible.builtin.systemd:
name: nftables
enabled: true
state: started
become: true
17 changes: 17 additions & 0 deletions ansible/roles/nftables/templates/10-filter.nft.j2
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
add table inet filter

add chain inet filter input {
type filter hook input priority 0; policy drop;

ct state invalid drop
ct state { established, related } accept
iifname "lo" accept
ip protocol icmp accept
ip6 nexthdr icmpv6 accept
Comment thread
Copilot marked this conversation as resolved.
Resolved
meta nfproto ipv6 udp sport 547 udp dport 546 accept
# mDNS IPv4 multicast
ip daddr 224.0.0.251 udp dport 5353 accept
# mDNS IPv6 multicast
ip6 daddr ff02::fb udp dport 5353 accept
tcp dport 22 accept
}
24 changes: 24 additions & 0 deletions ansible/roles/router/defaults/main.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
---
# Set to true for hosts that should route a LAN through a WAN connection.
router_enabled: false

# The LAN interface runs the DHCP server; the WAN interface is used for NAT.
router_lan_interface: usbnet
router_lan_mac_address: "3c:18:a0:0f:72:63"
router_wan_interface: wlp1s0

# IPv4 configuration advertised to DHCP clients.
router_lan_address: 192.168.100.1/24
router_lan_subnet: 192.168.100.0/24
router_dhcp_gateway: 192.168.100.1
router_dhcp_dns_servers:
- "{{ router_dhcp_gateway }}"
Comment thread
embtom marked this conversation as resolved.
Resolved
router_dhcp_pool_offset: 100
router_dhcp_pool_size: 50
router_dhcp_default_lease_time: 12h
router_dhcp_max_lease_time: 24h

# Distribution packages that provide the RTL8153 USB Ethernet firmware patch.
router_rtl8153_firmware_packages:
Debian: firmware-realtek
Ubuntu: linux-firmware
25 changes: 25 additions & 0 deletions ansible/roles/router/handlers/main.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
---
- name: Restart NetworkManager
ansible.builtin.systemd:
name: NetworkManager
state: restarted
become: true

- name: Restart systemd-networkd
ansible.builtin.systemd:
name: systemd-networkd
state: restarted
become: true

- name: Restart systemd-resolved
ansible.builtin.systemd:
name: systemd-resolved
state: restarted
become: true

- name: Apply sysctl settings
ansible.builtin.command:
cmd: sysctl --system
become: true
changed_when: false
listen: Apply sysctl settings
Loading