Skip to content
Open
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
/ansible/inventories/production/host_vars/localhost/vault.yml
1 change: 1 addition & 0 deletions .vscode/tasks.json
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@
"nftables",
"podman",
"router",
"scanner",
"scripts",
"shell",
"ssh",
Expand Down
51 changes: 51 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ SSH.
| `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 |
| `scanner` | `sane_airscan` | Static eSCL/WSD network-scanner configuration without Avahi discovery |
| `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 |
Expand Down Expand Up @@ -79,6 +80,26 @@ 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.

## Network Scanner Configuration

The optional `sane_airscan` role installs Skanpage and configures eSCL/WSD
scanners with static URLs. It disables automatic DNS-SD and WS-Discovery,
avoiding an Avahi requirement at XSane or Skanpage startup. Enable it per host
and define one or more scanner URLs:

```yaml
sane_airscan_enabled: true
sane_airscan_disable_hpaio: true
sane_airscan_devices:
- name: NPIC65336
url: http://npic65336.local/eSCL
protocol: escl
```

Set `sane_airscan_disable_hpaio: true` when the same device is configured via
eSCL and XSane should not load HPLIP's Avahi-dependent SANE backend. This does
not remove HPLIP's printing support.

## Router Configuration

The `router` role is disabled by default to avoid changing a normal workstation
Expand Down Expand Up @@ -114,6 +135,36 @@ 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.

To use a supported USB Wi-Fi adapter as the router LAN access point instead,
set the LAN adapter's permanent MAC address and enable hostapd:

```yaml
router_lan_interface: router-ap
router_lan_mac_address: "74:19:f8:17:a7:27"
router_access_point_enabled: true
router_access_point_ssid: toms_bridge
router_access_point_country_code: DE
```

The access point uses WPA2 and assigns clients addresses from the existing
`router_lan_*` subnet and DHCP pool. Store its 8 to 63 character passphrase in
an untracked, encrypted Vault file. Create it interactively with:

```bash
./scripts/create-router-access-point-vault
```

The script checks the WPA passphrase length, prompts for a Vault password, and
creates the following encrypted variable:

```yaml
router_access_point_passphrase: "<your WPA passphrase>"
```

When the Vault file exists, local deployments automatically prompt for its
password. Deploy with `./scripts/deploy --tag router`. The file is ignored by
Git and must never be committed.

## Requirements

- Debian or Ubuntu system
Expand Down
3 changes: 0 additions & 3 deletions ansible/inventories/production/host_vars/localhost.yml

This file was deleted.

15 changes: 15 additions & 0 deletions ansible/inventories/production/host_vars/localhost/main.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
---
router_enabled: false
router_wan_interface: wlp2s0
router_lan_interface: router-ap
router_lan_mac_address: "74:19:f8:17:a7:27"
router_access_point_enabled: true
router_access_point_ssid: toms_bridge
router_access_point_country_code: DE

sane_airscan_enabled: true
sane_airscan_disable_hpaio: true
sane_airscan_devices:
- name: NPIC65336
url: http://npic65336.local/eSCL
protocol: escl
2 changes: 2 additions & 0 deletions ansible/playbooks/workstation.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@
tags: [scripts]
- role: systemd
tags: [systemd]
- role: sane_airscan
tags: [scanner]
- role: vscode
tags: [vscode]
- role: yakuake
Expand Down
6 changes: 6 additions & 0 deletions ansible/roles/router/defaults/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,12 @@ router_dhcp_pool_size: 50
router_dhcp_default_lease_time: 12h
router_dhcp_max_lease_time: 24h

# Optional hostapd access point that replaces the wired LAN interface.
router_access_point_enabled: false
router_access_point_ssid: ""
router_access_point_country_code: DE
router_access_point_channel: 6

# Distribution packages that provide the RTL8153 USB Ethernet firmware patch.
router_rtl8153_firmware_packages:
Debian: firmware-realtek
Expand Down
7 changes: 7 additions & 0 deletions ansible/roles/router/handlers/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,10 @@
become: true
changed_when: false
listen: Apply sysctl settings

- name: Restart hostapd
ansible.builtin.systemd:
name: hostapd
state: restarted
become: true
when: router_lan_interface in ansible_facts.interfaces
73 changes: 70 additions & 3 deletions ansible/roles/router/tasks/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,28 @@
- router_dhcp_pool_offset | int > 0
- router_dhcp_pool_size | int > 0
- ansible_facts['distribution'] in router_rtl8153_firmware_packages
- >-
not (router_access_point_enabled | bool)
or (router_access_point_ssid | length > 0 and router_access_point_ssid | length <= 32)
- >-
not (router_access_point_enabled | bool)
or router_access_point_country_code is match('^[A-Z]{2}$')
- >-
not (router_access_point_enabled | bool)
or (router_access_point_channel | int >= 1 and router_access_point_channel | int <= 13)
- >-
not (router_access_point_enabled | bool)
or router_access_point_passphrase is defined
- >-
not (router_access_point_enabled | bool)
or (router_access_point_passphrase | default('') | length >= 8
and router_access_point_passphrase | default('') | length <= 63)
fail_msg: >-
Configure the router interface names, IPv4 addresses, and DHCP pool with
valid non-empty values. Add the target distribution's RTL8153 firmware
package to router_rtl8153_firmware_packages.
Configure the router interface names, IPv4 addresses, DHCP pool, and,
when enabled, access-point settings with valid values. Define an 8 to
63 character router_access_point_passphrase in an Ansible Vault file.
Add the target distribution's RTL8153 firmware package to
router_rtl8153_firmware_packages.

- name: Install router packages
ansible.builtin.apt:
Expand All @@ -34,6 +52,13 @@
update_cache: true
become: true

- name: Install access point package
ansible.builtin.apt:
name: hostapd
state: present
become: true
when: router_access_point_enabled | bool

- name: Ensure systemd-resolved configuration directory exists
ansible.builtin.file:
path: /etc/systemd/resolved.conf.d
Expand Down Expand Up @@ -113,6 +138,31 @@
become: true
notify: Restart systemd-networkd

- name: Configure access point
when: router_access_point_enabled | bool
block:
- name: Configure hostapd defaults
ansible.builtin.copy:
content: |
DAEMON_CONF="/etc/hostapd/hostapd.conf"
dest: /etc/default/hostapd
owner: root
group: root
mode: "0644"
become: true
notify: Restart hostapd

- name: Configure hostapd
ansible.builtin.template:
src: hostapd.conf.j2
dest: /etc/hostapd/hostapd.conf
owner: root
group: root
mode: "0600"
become: true
no_log: true
notify: Restart hostapd

- name: Configure router nftables rules
ansible.builtin.template:
src: "{{ item.src }}"
Expand Down Expand Up @@ -141,3 +191,20 @@
loop:
- systemd-networkd
- systemd-resolved

- name: Enable access point service
ansible.builtin.systemd:
name: hostapd
masked: false
enabled: true
become: true
when: router_access_point_enabled | bool

- name: Start access point service when LAN interface exists
ansible.builtin.systemd:
name: hostapd
state: started
become: true
when:
- router_access_point_enabled | bool
- router_lan_interface in ansible_facts.interfaces
14 changes: 14 additions & 0 deletions ansible/roles/router/templates/hostapd.conf.j2
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
interface={{ router_lan_interface }}
driver=nl80211
ssid={{ router_access_point_ssid }}
country_code={{ router_access_point_country_code }}
ieee80211d=1
hw_mode=g
channel={{ router_access_point_channel }}
ieee80211n=1
wmm_enabled=1
auth_algs=1
wpa=2
wpa_passphrase={{ router_access_point_passphrase }}
wpa_key_mgmt=WPA-PSK
rsn_pairwise=CCMP
4 changes: 4 additions & 0 deletions ansible/roles/sane_airscan/defaults/main.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
---
sane_airscan_enabled: false
sane_airscan_devices: []
sane_airscan_disable_hpaio: false
50 changes: 50 additions & 0 deletions ansible/roles/sane_airscan/tasks/main.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
---
- name: Configure static network scanners
when: sane_airscan_enabled | bool
block:
- name: Validate static network scanner configuration
ansible.builtin.assert:
that:
- sane_airscan_devices | length > 0
- sane_airscan_devices | selectattr('name', 'defined') | list | length
== sane_airscan_devices | length
- sane_airscan_devices | selectattr('url', 'defined') | list | length
== sane_airscan_devices | length
- sane_airscan_devices | selectattr('protocol', 'in', ['escl', 'wsd']) | list | length
== sane_airscan_devices | length
fail_msg: >-
Configure at least one scanner with a name, URL, and either the escl
or wsd protocol.

- name: Install scanner packages
ansible.builtin.apt:
name:
- sane-airscan
- skanpage
state: present
become: true

- name: Disable HPLIP SANE backend
ansible.builtin.file:
path: /etc/sane.d/dll.d/hplip
state: absent
become: true
when: sane_airscan_disable_hpaio | bool

- name: Ensure sane-airscan drop-in directory exists
ansible.builtin.file:
path: /etc/sane.d/airscan.d
state: directory
owner: root
group: root
mode: "0755"
become: true

- name: Configure static network scanners
ansible.builtin.template:
src: airscan.conf.j2
dest: /etc/sane.d/airscan.d/10-static-scanners.conf
owner: root
group: root
mode: "0644"
become: true
8 changes: 8 additions & 0 deletions ansible/roles/sane_airscan/templates/airscan.conf.j2
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# Managed by Ansible. Configure scanners in sane_airscan_devices.
[devices]
{% for device in sane_airscan_devices %}
"{{ device.name }}" = {{ device.url }}, {{ device.protocol }}
{% endfor %}

[options]
discovery = disable
42 changes: 42 additions & 0 deletions scripts/create-router-access-point-vault
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
#!/usr/bin/env bash
set -euo pipefail

SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
VAULT_FILE="${SCRIPT_DIR}/../ansible/inventories/production/host_vars/localhost/vault.yml"


if [[ $# -ne 0 ]]; then
usage >&2
exit 1
fi

if [[ -e "${VAULT_FILE}" ]]; then
echo "Vault file already exists: ${VAULT_FILE}" >&2
exit 1
fi

read -r -s -p "Router access point WPA passphrase: " passphrase
printf '\n'
read -r -s -p "Confirm WPA passphrase: " confirmation
printf '\n'

if [[ "${passphrase}" != "${confirmation}" ]]; then
echo "Passphrases do not match." >&2
exit 1
fi

vault_dir="$(dirname "${VAULT_FILE}")"
mkdir -p "${vault_dir}"
temp_file="$(mktemp)"
trap 'rm -f "${temp_file}"' EXIT
chmod 600 "${temp_file}"
escaped_passphrase="${passphrase//\\/\\\\}"
escaped_passphrase="${escaped_passphrase//\"/\\\"}"
printf 'router_access_point_passphrase: "%s"\n' "${escaped_passphrase}" >"${temp_file}"
unset passphrase confirmation
unset escaped_passphrase

ansible-vault encrypt --output "${VAULT_FILE}" "${temp_file}"
chmod 600 "${VAULT_FILE}"

echo "Created encrypted router access point vault: ${VAULT_FILE}"
Loading