diff --git a/ansible/README.md b/ansible/README.md index 7d1989c..88692cd 100644 --- a/ansible/README.md +++ b/ansible/README.md @@ -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. diff --git a/ansible/inventory/hosts.yml b/ansible/inventory/hosts.yml index 439fe32..2cfa642 100644 --- a/ansible/inventory/hosts.yml +++ b/ansible/inventory/hosts.yml @@ -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 diff --git a/ansible/playbooks/dlink-init.yml b/ansible/playbooks/dlink-init.yml new file mode 100644 index 0000000..b79fe4b --- /dev/null +++ b/ansible/playbooks/dlink-init.yml @@ -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 diff --git a/ansible/playbooks/openwrt.yml b/ansible/playbooks/openwrt.yml new file mode 100644 index 0000000..022d0f5 --- /dev/null +++ b/ansible/playbooks/openwrt.yml @@ -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 diff --git a/ansible/requirements.yml b/ansible/requirements.yml index 73c1860..7e148e7 100644 --- a/ansible/requirements.yml +++ b/ansible/requirements.yml @@ -3,3 +3,5 @@ collections: version: ">=3.16.0" - name: community.hashi_vault version: ">=7.1.0" + - name: community.openwrt + version: ">=1.0.0" diff --git a/ansible/roles/openwrt/defaults/main.yml b/ansible/roles/openwrt/defaults/main.yml new file mode 100644 index 0000000..1245a8c --- /dev/null +++ b/ansible/roles/openwrt/defaults/main.yml @@ -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: [] + diff --git a/ansible/roles/openwrt/handlers/main.yml b/ansible/roles/openwrt/handlers/main.yml new file mode 100644 index 0000000..fd59c00 --- /dev/null +++ b/ansible/roles/openwrt/handlers/main.yml @@ -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 diff --git a/ansible/roles/openwrt/tasks/firewall.yml b/ansible/roles/openwrt/tasks/firewall.yml new file mode 100644 index 0000000..0988d16 --- /dev/null +++ b/ansible/roles/openwrt/tasks/firewall.yml @@ -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 diff --git a/ansible/roles/openwrt/tasks/main.yml b/ansible/roles/openwrt/tasks/main.yml new file mode 100644 index 0000000..c1fd2f2 --- /dev/null +++ b/ansible/roles/openwrt/tasks/main.yml @@ -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 diff --git a/ansible/roles/openwrt/tasks/network.yml b/ansible/roles/openwrt/tasks/network.yml new file mode 100644 index 0000000..3249cc0 --- /dev/null +++ b/ansible/roles/openwrt/tasks/network.yml @@ -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 diff --git a/ansible/roles/openwrt/tasks/packages.yml b/ansible/roles/openwrt/tasks/packages.yml new file mode 100644 index 0000000..a6efc1f --- /dev/null +++ b/ansible/roles/openwrt/tasks/packages.yml @@ -0,0 +1,7 @@ +--- +- name: Install packages + community.openwrt.opkg: + name: "{{ item }}" + state: present + update_cache: true + loop: "{{ openwrt_packages }}" diff --git a/ansible/roles/openwrt/tasks/preflight.yml b/ansible/roles/openwrt/tasks/preflight.yml new file mode 100644 index 0000000..8f7070e --- /dev/null +++ b/ansible/roles/openwrt/tasks/preflight.yml @@ -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') }})" diff --git a/ansible/roles/openwrt/tasks/system.yml b/ansible/roles/openwrt/tasks/system.yml new file mode 100644 index 0000000..a5a713e --- /dev/null +++ b/ansible/roles/openwrt/tasks/system.yml @@ -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