feat(ansible): add OpenWrt dlink AP configuration

Add community.openwrt collection, dlink host to inventory,
openwrt role with system/network/firewall tasks, and two
playbooks: dlink-init.yml for one-time bootstrap from factory
IP, and openwrt.yml for ongoing idempotent configuration.

Network: MGMT untagged + LAN (vlan2) tagged on WAN port trunk
to MikroTik ether3. Firewall zones replace factory WAN/LAN
with mgmt (input ACCEPT) and lan (forward ACCEPT, AP mode).
This commit is contained in:
2026-05-13 21:08:55 +02:00
parent 17db139125
commit 120547b1b8
13 changed files with 477 additions and 15 deletions
+87 -15
View File
@@ -1,20 +1,92 @@
## RouterOS Ansible
# Ansible
This directory contains the new Ansible automation for the MikroTik router.
Idempotent configuration management for the home-lab network devices.
- Transport: RouterOS API (`community.routeros` collection), not SSH CLI scraping.
- Layout: one playbook (`playbooks/routeros.yml`) importing domain task files from `tasks/`.
- Goal: idempotent convergence using `community.routeros.api_modify` for managed paths.
## Devices
### Quick start
| Host | Group | IP | Playbook |
|---|---|---|---|
| crs418 (MikroTik CRS418) | `mikrotik` | 192.168.255.10 | `playbooks/routeros.yml` |
| dlink (OpenWrt AP) | `openwrt` | 192.168.255.11 | `playbooks/openwrt.yml` |
1. Install dependencies:
- `ansible-galaxy collection install -r ansible/requirements.yml`
- `python -m pip install librouteros hvac`
2. Configure secret references in `ansible/vars/routeros-secrets.yml`.
3. Store required fields in OpenBao under configured KV path.
4. Export token (`OPENBAO_TOKEN` or `VAULT_TOKEN`).
5. Run:
- `ANSIBLE_CONFIG=ansible/ansible.cfg ansible-playbook ansible/playbooks/routeros.yml`
Both devices are reachable on the MGMT network (192.168.255.0/24) once fully set up.
More details and design rationale: `docs/ansible/routeros-design.md`.
## Dependencies
```bash
ansible-galaxy collection install -r requirements.yml
pip install librouteros hvac
```
Collections used:
- `community.routeros >= 3.16.0` — MikroTik API modules
- `community.hashi_vault >= 7.1.0` — OpenBao/Vault secret lookup
- `community.openwrt >= 1.0.0` — OpenWrt UCI and shell modules
## MikroTik (routeros)
Secrets are fetched at runtime from OpenBao. No credentials are stored in files.
```bash
export VAULT_TOKEN=... # or OPENBAO_TOKEN
ansible-playbook playbooks/routeros.yml
```
Secret layout expected in OpenBao (KVv2, mount `secret`):
| Path | Fields |
|---|---|
| `routeros_api` | `username`, `password` |
| `wan_pppoe` | `username`, `password` |
| `router_tailscale` | `container_password` |
## OpenWrt dlink AP
The dlink needs a one-time initialisation before it can be managed through MikroTik.
There are two playbooks:
### Step 1 — `dlink-init.yml` (once, PC directly connected)
Run this while your PC is plugged into one of the dlink **LAN ports** with the
device still on its factory IP (192.168.1.1). MikroTik must **not** be in the
picture yet.
What it does:
- Reconfigures switch0 so the **WAN port** becomes a VLAN trunk:
- untagged → VLAN 1 (MGMT, 192.168.255.0/24)
- tagged → VLAN 2 (LAN, 192.168.0.0/24)
- Adds `mgmt` interface: static 192.168.255.11/24, gateway 192.168.255.10
- Reconfigures `lan` to a bridge on eth0.2 with no IP (AP mode)
- Removes routed `wan`/`wan6` interfaces
- Commits and reloads network in the background
After the reload the device is no longer reachable at 192.168.1.1.
```bash
ansible-playbook playbooks/dlink-init.yml
```
### Step 2 — connect dlink WAN port to MikroTik ether3
Plug the **dlink WAN port** into **MikroTik ether3**.
If the MikroTik config hasn't been applied yet, do it now:
```bash
export VAULT_TOKEN=...
ansible-playbook playbooks/routeros.yml
```
MikroTik ether3 is configured to send MGMT traffic untagged and VLAN 2 (LAN)
tagged, which matches what dlink expects on its WAN port.
### Step 3 — `openwrt.yml` (ongoing, via MikroTik)
All subsequent runs connect to 192.168.255.11 through MikroTik:
```bash
ansible-playbook playbooks/openwrt.yml
```
This is the idempotent main playbook. Run it any time to converge configuration.
+6
View File
@@ -4,3 +4,9 @@ all:
hosts:
crs418:
ansible_host: 192.168.255.10
openwrt:
hosts:
dlink:
ansible_host: 192.168.255.11
ansible_user: root
ansible_ssh_port: 22
+125
View File
@@ -0,0 +1,125 @@
---
# One-time initialisation playbook for the dlink OpenWrt AP.
#
# Run this while your PC is directly connected to a dlink LAN port
# (factory IP 192.168.1.1, no MikroTik in the picture yet).
#
# What it does:
# - Replaces the entire network config (switch VLANs, devices, interfaces)
# - Replaces the entire firewall config (mgmt/lan zones, no WAN)
# - Reloads network and firewall in the background
#
# After this playbook finishes the device is no longer reachable at 192.168.1.1.
# Plug the WAN port into MikroTik ether3 and use playbooks/openwrt.yml for all
# further configuration.
- name: dlink — one-time network initialisation
hosts: openwrt
gather_facts: false
vars:
ansible_host: "192.168.1.1"
ansible_user: root
tasks:
- name: Verify connectivity
community.openwrt.ping:
- name: Configure network (switch VLANs, devices, interfaces)
community.openwrt.uci:
command: import
merge: false
config: network
value: |
package network
config interface 'loopback'
option device 'lo'
option proto 'static'
list ipaddr '127.0.0.1/8'
config globals 'globals'
option ula_prefix 'fd4d:508e:899a::/48'
config switch
option name 'switch0'
option reset '1'
option enable_vlan '1'
config switch_vlan
option device 'switch0'
option vlan '1'
option vid '1'
option description 'mgmt'
option ports '4 6t'
config switch_vlan
option device 'switch0'
option vlan '2'
option vid '2'
option description 'lan'
option ports '0 1 2 3 4t 6t'
config device
option name 'br-lan'
option type 'bridge'
list ports 'eth0.2'
config interface 'mgmt'
option device 'eth0.1'
option proto 'static'
option ipaddr '192.168.255.11/24'
option gateway '192.168.255.10'
option dns '192.168.0.1'
config interface 'lan'
option device 'br-lan'
option proto 'none'
- name: Commit network config
community.openwrt.uci:
command: commit
key: network
- name: Configure firewall (mgmt/lan zones, no WAN)
community.openwrt.uci:
command: import
merge: false
config: firewall
value: |
package firewall
config defaults
option syn_flood '1'
option input 'REJECT'
option output 'ACCEPT'
option forward 'REJECT'
config zone
option name 'mgmt'
list network 'mgmt'
option input 'ACCEPT'
option output 'ACCEPT'
option forward 'REJECT'
config zone
option name 'lan'
list network 'lan'
option input 'REJECT'
option output 'ACCEPT'
option forward 'ACCEPT'
config rule
option name 'Allow-ICMP-mgmt'
option src 'mgmt'
option proto 'icmp'
option target 'ACCEPT'
- name: Commit firewall config
community.openwrt.uci:
command: commit
key: firewall
- name: Reload network in background (device will drop off 192.168.1.1)
community.openwrt.nohup:
command: /etc/init.d/network reload
ignore_unreachable: true
+10
View File
@@ -0,0 +1,10 @@
---
# Main OpenWrt playbook. Connects to dlink on its permanent management IP
# (192.168.255.11 via MikroTik ether3). Run dlink-init.yml first if the
# device has not been initialised yet.
- name: Configure OpenWrt
hosts: openwrt
gather_facts: false
roles:
- role: openwrt
+2
View File
@@ -3,3 +3,5 @@ collections:
version: ">=3.16.0"
- name: community.hashi_vault
version: ">=7.1.0"
- name: community.openwrt
version: ">=1.0.0"
+27
View File
@@ -0,0 +1,27 @@
---
# Hostname for the AP
openwrt_hostname: dlink
# Timezone (POSIX TZ string used by OpenWrt)
openwrt_timezone: CET-1CEST,M3.5.0,M10.5.0/3
# Management interface and IP (statically assigned on VLAN 1 / eth0.1)
openwrt_mgmt_ip: 192.168.255.11
openwrt_mgmt_prefix: 24
openwrt_mgmt_gateway: 192.168.255.10
# DNS servers for the AP itself
openwrt_dns_servers:
- 192.168.0.1
# SSH authorised keys (list of public key strings)
openwrt_ssh_authorized_keys: []
# NTP servers
openwrt_ntp_servers:
- 0.pl.pool.ntp.org
- 1.pl.pool.ntp.org
# Packages to install
openwrt_packages: []
+14
View File
@@ -0,0 +1,14 @@
---
- name: Reload network
community.openwrt.nohup:
command: /etc/init.d/network reload
ignore_unreachable: true
- name: Reload firewall
community.openwrt.service:
name: firewall
state: restarted
- name: Reload wireless
community.openwrt.command:
cmd: wifi reload
+51
View File
@@ -0,0 +1,51 @@
---
# This device is a pure AP — no routing, no NAT, no internet-facing interface.
#
# Zones:
# mgmt — management interface (192.168.255.11)
# input: ACCEPT (SSH, ping reachable from MGMT network)
# forward: REJECT (nothing routes through mgmt)
#
# lan — client bridge (eth0.2, wireless clients)
# input: REJECT (clients cannot SSH into the AP itself)
# forward: ACCEPT (client traffic passes through to MikroTik,
# which does all actual firewalling)
#
# No forwarding rules between zones — traffic in/out of each zone goes
# directly to/from MikroTik over the trunk, not through this device.
- name: Configure firewall
community.openwrt.uci:
command: import
merge: false
config: firewall
value: |
package firewall
config defaults
option syn_flood '1'
option input 'REJECT'
option output 'ACCEPT'
option forward 'REJECT'
config zone
option name 'mgmt'
list network 'mgmt'
option input 'ACCEPT'
option output 'ACCEPT'
option forward 'REJECT'
config zone
option name 'lan'
list network 'lan'
option input 'REJECT'
option output 'ACCEPT'
option forward 'ACCEPT'
config rule
option name 'Allow-ICMP-mgmt'
option src 'mgmt'
option proto 'icmp'
option target 'ACCEPT'
notify: Reload firewall
+19
View File
@@ -0,0 +1,19 @@
---
- name: Preflight — verify connectivity
ansible.builtin.import_tasks: preflight.yml
- name: System configuration
ansible.builtin.import_tasks: system.yml
- name: Network configuration
ansible.builtin.import_tasks: network.yml
- name: Firewall configuration
ansible.builtin.import_tasks: firewall.yml
- name: Wireless configuration
ansible.builtin.import_tasks: wireless.yml
- name: Package management
ansible.builtin.import_tasks: packages.yml
when: openwrt_packages | length > 0
+88
View File
@@ -0,0 +1,88 @@
---
# Network layout:
# MikroTik ether3 ↔ dlink WAN port (switch0 port4)
# MikroTik sends MGMT traffic untagged, vlan2 (LAN) and vlan5 (IOT) tagged.
#
# switch0 VLAN table:
# VLAN 1 (MGMT): CPU(6) tagged, WAN(4) untagged → eth0.1 → mgmt
# VLAN 2 (LAN): CPU(6) tagged, WAN(4) tagged, LAN1-4(0-3) untagged → eth0.2 → br-lan → lan
# VLAN 5 (IOT): CPU(6) tagged, WAN(4) tagged → eth0.5 → br-iot → iot
#
# Interfaces:
# mgmt — static 192.168.255.11/24 on eth0.1, management
# lan — bridge (br-lan) on eth0.2, LAN clients via LAN ports
# iot — bridge (br-iot) on eth0.5, IoT clients via wifi only
- name: Configure network
community.openwrt.uci:
command: import
merge: false
config: network
value: |
package network
config interface 'loopback'
option device 'lo'
option proto 'static'
list ipaddr '127.0.0.1/8'
config globals 'globals'
option ula_prefix 'fd4d:508e:899a::/48'
config switch
option name 'switch0'
option reset '1'
option enable_vlan '1'
config switch_vlan
option device 'switch0'
option vlan '1'
option vid '1'
option description 'mgmt'
option ports '4 6t'
config switch_vlan
option device 'switch0'
option vlan '2'
option vid '2'
option description 'lan'
option ports '0 1 2 3 4t 6t'
config switch_vlan
option device 'switch0'
option vlan '5'
option vid '5'
option description 'iot'
option ports '4t 6t'
config device
option name 'br-lan'
option type 'bridge'
list ports 'eth0.2'
config interface 'mgmt'
option device 'eth0.1'
option proto 'static'
option ipaddr '{{ openwrt_mgmt_ip }}/{{ openwrt_mgmt_prefix }}'
option gateway '{{ openwrt_mgmt_gateway }}'
option dns '{{ openwrt_dns_servers | join(" ") }}'
config interface 'lan'
option device 'br-lan'
option proto 'none'
config device
option name 'br-iot'
option type 'bridge'
list ports 'eth0.5'
config interface 'iot'
option device 'br-iot'
option proto 'none'
notify: Reload network
- name: Commit network config
community.openwrt.uci:
command: commit
key: network
+7
View File
@@ -0,0 +1,7 @@
---
- name: Install packages
community.openwrt.opkg:
name: "{{ item }}"
state: present
update_cache: true
loop: "{{ openwrt_packages }}"
+11
View File
@@ -0,0 +1,11 @@
---
- name: Verify connectivity to OpenWrt device
community.openwrt.ping:
- name: Gather OpenWrt facts
community.openwrt.setup:
register: openwrt_facts
- name: Show device info
ansible.builtin.debug:
msg: "Managing {{ inventory_hostname }} ({{ openwrt_facts.ansible_facts.ansible_system | default('OpenWrt') }})"
+30
View File
@@ -0,0 +1,30 @@
---
- name: Set hostname
community.openwrt.uci:
command: set
key: system.@system[0].hostname
value: "{{ openwrt_hostname }}"
- name: Set timezone
community.openwrt.uci:
command: set
key: system.@system[0].timezone
value: "{{ openwrt_timezone }}"
- name: Configure NTP servers
community.openwrt.uci:
command: set
key: system.ntp.server
value: "{{ openwrt_ntp_servers }}"
- name: Commit system config
community.openwrt.uci:
command: commit
key: system
- name: Set SSH authorized keys
community.openwrt.uci:
command: set
key: "dropbear.@dropbear[0].authorized_keys"
value: "{{ openwrt_ssh_authorized_keys | join('\n') }}"
when: openwrt_ssh_authorized_keys | length > 0