From 29bdf06383b9719f4390bf730354b2df06880ba2 Mon Sep 17 00:00:00 2001 From: windyboy Date: Thu, 6 Aug 2026 10:20:43 +0800 Subject: [PATCH] Document network operations and WireGuard maintenance --- AGENTS.md | 1 + ansible/inventory/hosts.yml | 15 ++ ansible/playbooks/wireguard-harden.yml | 62 +++++++++ ansible/playbooks/wireguard-maintenance.yml | 114 ++++++++++++++++ ansible/roles/healthcheck/defaults/main.yml | 1 + .../templates/healthcheck-runner.sh.j2 | 5 +- .../healthcheck/templates/wireguard.sh.j2 | 35 +++++ ansible/templates/wireguard-compose.yml.j2 | 24 ++++ docs/edgerouter-x-configuration.md | 120 ++++++++++++++++ docs/unifi-network.md | 129 ++++++++++++++++++ hosts/gw.md | 102 ++++++++++++++ hosts/ubnt.md | 10 +- hosts/us4.wsvc.info.md | 61 +++++++++ inventory/hosts.md | 3 +- 14 files changed, 678 insertions(+), 4 deletions(-) create mode 100644 ansible/playbooks/wireguard-harden.yml create mode 100644 ansible/playbooks/wireguard-maintenance.yml create mode 100644 ansible/roles/healthcheck/templates/wireguard.sh.j2 create mode 100644 ansible/templates/wireguard-compose.yml.j2 create mode 100644 docs/edgerouter-x-configuration.md create mode 100644 docs/unifi-network.md create mode 100644 hosts/gw.md create mode 100644 hosts/us4.wsvc.info.md diff --git a/AGENTS.md b/AGENTS.md index e8ebc6c..8e1b992 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -28,6 +28,7 @@ Also readable as `agent.md` (symlink → this file). | **us2.wsvc.info** | Vaultwarden + Traefik (+ Soft Serve, …) | `ssh -4 windy@us2.wsvc.info` | [hosts/us2.wsvc.info.md](hosts/us2.wsvc.info.md) | | **hk2.chans.xyz** | PowerDNS auth ns1 (`/opt/pdns`) | `ssh -4 windy@hk2.chans.xyz` | [hosts/hk2.chans.xyz.md](hosts/hk2.chans.xyz.md) | | **gfw.windy.lan** | OpenWrt LAN gateway / OpenClash | `ssh -4 root@192.168.66.1` | [hosts/gfw.windy.lan.md](hosts/gfw.windy.lan.md) | +| **gw** | EdgeRouter X primary LAN gateway | `ssh -4 zhiqiang@192.168.66.254` | [hosts/gw.md](hosts/gw.md) | | **ubnt** | UniFi Network Controller | `ssh -4 windy@192.168.66.46` | [hosts/ubnt.md](hosts/ubnt.md) | ### Public services diff --git a/ansible/inventory/hosts.yml b/ansible/inventory/hosts.yml index 260aaf7..5f02007 100644 --- a/ansible/inventory/hosts.yml +++ b/ansible/inventory/hosts.yml @@ -45,6 +45,17 @@ all: backup: compose_args: [--no-deps, --force-recreate] service_reconcile_traefik_restart_targets: [poweradmin] + us4: + ansible_host: us4.wsvc.info + ansible_host_ipv4: 185.201.226.122 + service_role: wireguard + compose_project_dir: /opt/wireguard + healthcheck_profile: wireguard + wireguard_image: >- + lscr.io/linuxserver/wireguard@sha256:ac43e1226878d2611315172d6ea357a95cb326ee73124b91108118efc8666889 + service_reconcile_services: + wireguard: + compose_args: [--no-deps, --force-recreate] mailcow: hosts: mx2: @@ -54,11 +65,15 @@ all: powerdns: hosts: hk2: + wireguard: + hosts: + us4: docker_hosts: children: mailcow: vaultwarden: powerdns: + wireguard: # Matrix is a dedicated K3s node and intentionally remains outside the # Docker-oriented managed group. k3s_servers: diff --git a/ansible/playbooks/wireguard-harden.yml b/ansible/playbooks/wireguard-harden.yml new file mode 100644 index 0000000..e36082d --- /dev/null +++ b/ansible/playbooks/wireguard-harden.yml @@ -0,0 +1,62 @@ +--- +# This owns the non-secret Compose declaration only. WireGuard key material +# remains under the server-local /opt/wireguard/config bind mount. +- name: Apply the reviewed WireGuard Compose hardening + hosts: wireguard + become: true + gather_facts: false + serial: 1 + vars: + wireguard_harden_confirm: false + wireguard_compose_candidate: "{{ compose_project_dir }}/compose.yml.candidate" + tasks: + - name: Require explicit WireGuard hardening confirmation + ansible.builtin.assert: + that: + - wireguard_harden_confirm | bool + fail_msg: >- + This operation replaces the non-secret Compose declaration. Run only + during the approved maintenance window after a rollback archive exists. + + - name: Verify that a protected pre-change archive exists + ansible.builtin.find: + paths: "{{ compose_project_dir }}/backups" + patterns: 'pre-hardening-*.tar.gz' + file_type: file + register: wireguard_pre_hardening_archives + + - name: Require a pre-change rollback archive + ansible.builtin.assert: + that: + - wireguard_pre_hardening_archives.matched | int > 0 + fail_msg: >- + Create a protected pre-hardening archive before replacing compose.yml. + + - name: Render the candidate non-secret Compose declaration + ansible.builtin.template: + src: ../templates/wireguard-compose.yml.j2 + dest: "{{ wireguard_compose_candidate }}" + owner: root + group: root + mode: "0644" + + - name: Validate the candidate Compose declaration + ansible.builtin.command: + argv: + - docker + - compose + - --project-directory + - "{{ compose_project_dir }}" + - --file + - "{{ wireguard_compose_candidate }}" + - config + - --quiet + changed_when: false + + - name: Atomically activate the validated Compose declaration + ansible.builtin.command: + argv: + - mv + - "{{ wireguard_compose_candidate }}" + - "{{ compose_project_dir }}/compose.yml" + changed_when: true diff --git a/ansible/playbooks/wireguard-maintenance.yml b/ansible/playbooks/wireguard-maintenance.yml new file mode 100644 index 0000000..6462724 --- /dev/null +++ b/ansible/playbooks/wireguard-maintenance.yml @@ -0,0 +1,114 @@ +--- +# Deliberately narrow maintenance path for the us4 WireGuard service. The +# Compose file and all private key material remain on the server; this playbook +# only pulls its declared immutable image and recreates the named service. +- name: Update and recreate the approved WireGuard service + hosts: wireguard + become: true + gather_facts: false + serial: 1 + vars: + wireguard_maintenance_confirm: false + tasks: + - name: Require explicit WireGuard maintenance confirmation + ansible.builtin.assert: + that: + - wireguard_maintenance_confirm | bool + fail_msg: >- + This operation pulls the image declared in /opt/wireguard/compose.yml + and briefly recreates WireGuard. Set wireguard_maintenance_confirm=true + only during the approved maintenance window. + + - name: Create the local WireGuard rollback directory + ansible.builtin.file: + path: "{{ compose_project_dir }}/backups" + state: directory + owner: root + group: root + mode: "0700" + + - name: Create a timestamp for the rollback archive + ansible.builtin.command: + argv: [date, +%Y%m%dT%H%M%SZ] + changed_when: false + register: wireguard_backup_timestamp + + - name: Create a protected local rollback archive of WireGuard configuration + ansible.builtin.shell: + cmd: >- + umask 077 && tar --create --gzip + --file={{ compose_project_dir }}/backups/config-{{ wireguard_backup_timestamp.stdout }}.tar.gz + --directory={{ compose_project_dir }} config + executable: /bin/bash + changed_when: true + + - name: Verify the compose declaration uses an immutable image digest + ansible.builtin.command: + argv: + - docker + - compose + - --project-directory + - "{{ compose_project_dir }}" + - config + changed_when: false + register: wireguard_compose_config + + - name: Reject mutable WireGuard image declarations + ansible.builtin.assert: + that: + - "'@sha256:' in wireguard_compose_config.stdout" + fail_msg: >- + Pin the WireGuard image to an approved immutable digest before using + this maintenance playbook. + + - name: Pull the declared WireGuard image + ansible.builtin.command: + argv: + - docker + - compose + - --project-directory + - "{{ compose_project_dir }}" + - pull + - wireguard + changed_when: true + + - name: Recreate the WireGuard container + ansible.builtin.command: + argv: + - docker + - compose + - --project-directory + - "{{ compose_project_dir }}" + - up + - -d + - --no-deps + - --force-recreate + - wireguard + changed_when: true + + - name: Verify WireGuard container state + ansible.builtin.command: + argv: + - docker + - compose + - --project-directory + - "{{ compose_project_dir }}" + - ps + - --all + changed_when: false + register: wireguard_compose_ps + + - name: Display WireGuard container state + ansible.builtin.debug: + var: wireguard_compose_ps.stdout_lines + + - name: Run the deployed WireGuard health check + ansible.builtin.command: + argv: [/usr/local/lib/vps-health/run] + changed_when: false + register: wireguard_health_report + failed_when: wireguard_health_report.rc not in [0, 1] + + - name: Display sanitized WireGuard health result + ansible.builtin.debug: + msg: "{{ wireguard_health_report.stdout | default(wireguard_health_report.stderr, true) }}" diff --git a/ansible/roles/healthcheck/defaults/main.yml b/ansible/roles/healthcheck/defaults/main.yml index 5a937f0..b5b4c3e 100644 --- a/ansible/roles/healthcheck/defaults/main.yml +++ b/ansible/roles/healthcheck/defaults/main.yml @@ -11,3 +11,4 @@ healthcheck_profiles: mailcow: mailcow.sh vaultwarden: vaultwarden.sh pdns: pdns.sh + wireguard: wireguard.sh diff --git a/ansible/roles/healthcheck/templates/healthcheck-runner.sh.j2 b/ansible/roles/healthcheck/templates/healthcheck-runner.sh.j2 index 84e5987..d749fba 100644 --- a/ansible/roles/healthcheck/templates/healthcheck-runner.sh.j2 +++ b/ansible/roles/healthcheck/templates/healthcheck-runner.sh.j2 @@ -1,3 +1,4 @@ #!/usr/bin/env bash -set -uo pipefail -exec '{{ healthcheck_install_root }}/{{ healthcheck_profiles[healthcheck_profile] }}' >> '{{ healthcheck_log_dir }}/healthcheck.log' 2>&1 +set -o pipefail +'{{ healthcheck_install_root }}/{{ healthcheck_profiles[healthcheck_profile] }}' 2>&1 | tee -a '{{ healthcheck_log_dir }}/healthcheck.log' +exit "${PIPESTATUS[0]}" diff --git a/ansible/roles/healthcheck/templates/wireguard.sh.j2 b/ansible/roles/healthcheck/templates/wireguard.sh.j2 new file mode 100644 index 0000000..195d000 --- /dev/null +++ b/ansible/roles/healthcheck/templates/wireguard.sh.j2 @@ -0,0 +1,35 @@ +#!/usr/bin/env bash +set -uo pipefail +source '{{ healthcheck_install_root }}/health-common.sh' + +require_command docker +require_command ss +check_compose + +container='wireguard' +if ! docker inspect "$container" >/dev/null 2>&1; then + record critical 'wireguard_container_missing' +else + running="$(docker inspect "$container" --format '{{ '{{' }}.State.Running{{ '}}' }}' 2>/dev/null)" + [[ "$running" == true ]] || record critical 'wireguard_container_not_running' + + interface="$(docker exec "$container" wg show interfaces 2>/dev/null || true)" + grep -qw 'wg0' <<<"$interface" || record critical 'wireguard_wg0_missing' + + configured_peer_count="$(docker exec "$container" sh -c 'find /config -mindepth 1 -maxdepth 1 -type d -name "peer_*" | wc -l' 2>/dev/null || true)" + runtime_peer_count="$(docker exec "$container" wg show wg0 dump 2>/dev/null | awk 'NR > 1 { count++ } END { print count + 0 }')" + [[ "$configured_peer_count" =~ ^[0-9]+$ ]] || record unknown 'wireguard_configured_peer_count_unknown' + [[ "$runtime_peer_count" =~ ^[0-9]+$ ]] || record unknown 'wireguard_runtime_peer_count_unknown' + if [[ "$configured_peer_count" =~ ^[0-9]+$ && "$runtime_peer_count" =~ ^[0-9]+$ ]]; then + [[ "$configured_peer_count" -eq "$runtime_peer_count" ]] || record warning "wireguard_peer_count_mismatch_${configured_peer_count}_${runtime_peer_count}" + fi +fi + +if ss -H -uln 2>/dev/null | awk '{print $4}' | grep -Eq '(^|:)51820$'; then + record ok 'wireguard_udp_51820_listening' +else + record critical 'wireguard_udp_51820_missing' +fi + +emit_result +exit "$EXIT_CODE" diff --git a/ansible/templates/wireguard-compose.yml.j2 b/ansible/templates/wireguard-compose.yml.j2 new file mode 100644 index 0000000..9b57a6f --- /dev/null +++ b/ansible/templates/wireguard-compose.yml.j2 @@ -0,0 +1,24 @@ +services: + wireguard: + image: {{ wireguard_image }} + container_name: wireguard + cap_add: + - NET_ADMIN + environment: + - PUID=1000 + - PGID=1000 + - TZ=Asia/Shanghai + - SERVERURL=us4.wsvc.info + - SERVERPORT=51820 + - PEERS=ha,phone,mbp + - PEERDNS=1.1.1.1 + # Intentional: client IPv6 traffic is not tunneled. + - ALLOWEDIPS=0.0.0.0/0 + ports: + - 51820:51820/udp + volumes: + - ./config:/config + read_only: true + tmpfs: + - /run:exec + restart: unless-stopped diff --git a/docs/edgerouter-x-configuration.md b/docs/edgerouter-x-configuration.md new file mode 100644 index 0000000..370f994 --- /dev/null +++ b/docs/edgerouter-x-configuration.md @@ -0,0 +1,120 @@ +# EdgeRouter X configuration record + +This is a read-only, sanitized configuration record for +[gw](../hosts/gw.md), taken on 2026-08-05. It intentionally excludes PPPoE +credentials, user/password configuration, key material, and full DHCP client +or reservation inventories. + +## Platform and topology + +| Item | Current configuration | +|---|---| +| Appliance / OS | EdgeRouter X 5-Port; EdgeOS `v3.0.1` build `5862409` | +| WAN physical interface | `eth4`, described as `Internet (PPPoE)` | +| WAN routed interface | `pppoe0`; dynamic IPv4 default route; MTU `1492` | +| LAN66 | `eth0`, `192.168.66.254/24` | +| LAN55 | `switch0`, `192.168.55.254/24`; member ports `eth1`–`eth3` | +| IPv4 routing | Both LAN prefixes are directly connected; default route is through `pppoe0` | + +Despite its name, LAN55 is not currently an 802.1Q VLAN configuration: +`switch0` has `vlan-aware` disabled. It is a separate Layer-3 subnet carried +by the built-in switch ports, while LAN66 is connected through `eth0`. + +## IPv6 + +The PPPoE uplink requests a delegated `/60`, enables rapid commit and assigns +SLAAC-capable `/64` networks to the LANs: + +| Interface | Delegated prefix slot | Router address suffix | +|---|---|---| +| `eth0` (LAN66) | `:1` | `::1` | +| `switch0` (LAN55) | `:2` | `::1` | + +IPv6 has a default route through `pppoe0`. The currently delegated prefixes +are dynamic and are deliberately not treated as persistent configuration +facts. + +## Effective firewall policy + +| Ruleset | Direction / attachment | Effective policy | +|---|---|---| +| `WAN_IN` | `pppoe0`, IPv4 `in` | Active; default drop; permits established/related, drops invalid | +| `WAN_LOCAL` | `pppoe0`, IPv4 `local` | Active; default drop; permits established/related, drops invalid | +| `WANv6_IN` | `pppoe0`, IPv6 `in` | Active; default drop; permits established/related and ICMPv6; drops invalid | +| `WANv6_LOCAL` | `pppoe0`, IPv6 `local` | Active; default drop; permits established/related, ICMPv6, and DHCPv6 replies; drops invalid | +| `LAN_IN` | None | Inactive; configured default drop with explicit LAN55 ↔ LAN66 allows | +| `LAN_OUT` | None | Inactive; configured default drop with an Internet allow | + +Therefore, LAN55 and LAN66 currently have ordinary bidirectional routed +access. The configured `LAN_IN`/`LAN_OUT` rules do **not** enforce an access +boundary because neither is bound to an interface. From source +`192.168.55.254`, the controller at `192.168.66.46` replied to 3/3 ICMP probes +on 2026-08-05. The online LAN55 AP also confirms the operational Inform path +to `192.168.66.46:9080`. + +Global firewall settings enable normal ICMP echo replies, disable broadcast +ping and IPv4/IPv6 source routing, enable SYN cookies and martian logging, set +an MSS clamp of `1412`, and leave source validation disabled. Receive redirects +are disabled; IPv4 send redirects remain enabled. + +The [official EdgeOS User Guide](https://dl.ubnt.com/guides/edgemax/EdgeOS_UG.pdf) +describes the distinction: a firewall policy is a ruleset that must be applied +to its target interface and direction before it affects traffic. + +## NAT and inbound exposure + +IPv4 traffic leaving `pppoe0` is covered by NAT masquerade rule `5010`. +Port-forward auto-firewall and hairpin NAT are enabled. The configured WAN +forwards are below; their external reachability was not tested in this review. + +| WAN protocol / port | Internal destination | Purpose | +|---|---|---| +| TCP/UDP `8123` | `192.168.55.11:8123` | Home Assistant | +| TCP/UDP `51413` | `192.168.66.51:51413` | Transmission | +| TCP/UDP `5822` | `192.168.66.36:22` | SSH forward | +| TCP/UDP `1194` | `192.168.66.32:1194` | OpenVPN | + +Treat every forward as an intentional public attack surface. Changes require +the normal firewall/change review and external verification from a trusted +network. + +## LAN services + +| Service | LAN66 | LAN55 | +|---|---|---| +| DHCP pool | `.38`–`.243` | `.38`–`.243` | +| Default gateway | `.254` | `.254` | +| DNS server handed out | `192.168.66.36` | `192.168.66.36` | +| Lease duration | 24 hours | 24 hours | +| UniFi Controller DHCP option | `192.168.66.46` | `192.168.66.46` | + +DNS forwarding listens on `eth0` and `switch0` with a cache size of `512`. +Management services are SSH/TCP `22`, HTTP/TCP `80`, and HTTPS/TCP `443`. +Since no active LAN-local firewall is documented, treat access to this gateway +management plane from either LAN as permitted unless verified otherwise. + +Infrastructure devices, including the two APs and the UniFi controller, have +DHCP static mappings. Keep the authoritative full mapping list only in the +gateway configuration; do not copy unrelated household device inventories into +this operations repository. + +## Acceleration and maintenance implications + +At verification time, `show ubnt offload` reported that both IPSec and HWNAT +offload modules were not loaded. No `system offload` enablement was present in +the inspected configuration. This may constrain routing/NAT throughput under +load; it is an observation, not authorization to alter the router. + +Use these safe effective-state checks after any planned change: + +```bash +show interfaces +show ip route +show ipv6 route +show firewall +show nat rules +show ubnt offload +``` + +Avoid recording raw `show configuration commands` output: it includes PPPoE +credentials and may include other secrets. diff --git a/docs/unifi-network.md b/docs/unifi-network.md new file mode 100644 index 0000000..dabfa4e --- /dev/null +++ b/docs/unifi-network.md @@ -0,0 +1,129 @@ +# UniFi network configuration + +## Controller + +| Item | Value | +|---|---| +| Host | [ubnt](../hosts/ubnt.md) | +| Address | `192.168.66.46` | +| Deployment | Docker, `jacobalberty/unifi:v9.5.21`, host networking | +| Inform endpoint | `http://192.168.66.46:9080/inform` | +| MongoDB database / port | `ace` / `27117` | + +Although `8080` is a common UniFi inform port, this controller uses `9080`. +All AP adoption and recovery procedures must use the endpoint above. + +## Inter-VLAN reachability + +The AP management networks are routed by [gw](../hosts/gw.md), +`192.168.66.254`. On 2026-08-05, a probe sourced from the LAN55 gateway +(`192.168.55.254`) reached the controller (`192.168.66.46`) with 3/3 ICMP +replies. The UAP-AC-Lite is online through the `:9080/inform` endpoint, which +also confirms the required application path in practice. + +`LAN_IN` contains explicit allow rules for `192.168.55.0/24 ↔ +192.168.66.0/24`, but the firewall set is currently inactive (not attached to +an interface). Treat the networks as bidirectionally reachable by default; +do not rely on those inactive rules as an access boundary. Recheck this after +attaching any LAN firewall policy or changing VLAN routing. + +To inspect device records from the controller host: + +```bash +docker exec -it unifi-controller bash +mongo --port 27117 +``` + +```javascript +use ace +db.device.find( + { type: "uap" }, + { name: 1, ip: 1, model: 1, inform_url: 1 } +).pretty() +``` + +## Managed access points + +| Name | Model | Management IP | Firmware | Management network | Inform port | +|---|---|---|---|---|---| +| U6 Lite | U6 Lite (`UAL6`) | `192.168.66.6` | `6.7.54.15663` | LAN66 (`192.168.66.0/24`, gateway `192.168.66.254`) | `9080` | +| UAP-AC-Lite | `U7LT` | `192.168.55.5` | `6.8.2.15592` | LAN55 (`192.168.55.0/24`, gateway `192.168.55.254`) | `9080` | + +AP MAC addresses are recorded only where operationally needed: U6 Lite +`78:45:58:4d:cc:30`; UAP-AC-Lite `f0:9f:c2:20:04:e9`. + +## SSH administration + +Adopted APs use the SSH account configured in the controller, rather than the +factory `ubnt/ubnt` credential. The password is stored in the password manager +and must not be added to this repository. + +In UniFi Network `9.5.21`, configure it at: + +```text +Devices → Device Updates and Settings → Device SSH Settings +→ Device SSH Authentication +``` + +Use `ssh zhiqiangf@AP_IP` for the adopted-device account. Do not query or copy +the controller's `mgmt` database setting into logs or documentation: it can +contain the managed SSH password. + +On 2026-08-06, key-only IPv4 SSH was verified for both managed APs using the +`zhiqiangf` account. Verify future access without permitting password or +keyboard-interactive fallback: + +```bash +ssh -4 -o BatchMode=yes -o PasswordAuthentication=no \ + -o KbdInteractiveAuthentication=no -o StrictHostKeyChecking=yes \ + zhiqiangf@192.168.66.6 'mca-cli-op info' +ssh -4 -o BatchMode=yes -o PasswordAuthentication=no \ + -o KbdInteractiveAuthentication=no -o StrictHostKeyChecking=yes \ + zhiqiangf@192.168.55.5 'mca-cli-op info' +``` + +Both devices reported `Connected` to +`http://192.168.66.46:9080/inform` during that verification. The AP shell maps +this management account to UID 0, so treat it as administrative access. + +## Offline AP recovery + +An AP can be reachable via ICMP and SSH but offline in the controller when its +saved inform URL still points to port `8080`. This occurred for UAP-AC-Lite +after its inform configuration had diverged from the controller endpoint. + +From the AP, inspect the active endpoint and test controller reachability: + +```bash +info +nc 192.168.66.46 9080 +``` + +The AP's BusyBox `nc` may not support `-zv`; use the plain `nc` invocation +above when testing the TCP path. + +If `info` reports `http://192.168.66.46:8080/inform` (or another obsolete +endpoint), correct it. UniFi devices commonly require the command twice while +adoption is completed: + +```bash +set-inform http://192.168.66.46:9080/inform +set-inform http://192.168.66.46:9080/inform +``` + +Verify that the AP changes to connected/online in the controller. + +## Controller migration check + +After a controller migration or restore, inspect stored device records for a +stale default-port endpoint before treating an AP as faulty: + +```javascript +use ace +db.device.find( + { inform_url: /8080/ }, + { name: 1, ip: 1, inform_url: 1 } +) +``` + +Do not include credentials or controller database dumps in incident notes. diff --git a/hosts/gw.md b/hosts/gw.md new file mode 100644 index 0000000..d29fb23 --- /dev/null +++ b/hosts/gw.md @@ -0,0 +1,102 @@ +# gw — EdgeRouter X primary LAN gateway + +## Role and access + +| Item | Value | +|---|---| +| Role | Primary router for the `192.168.66.0/24` and `192.168.55.0/24` LANs | +| IPv4 | `192.168.66.254` | +| Verified hostname | `gw` | +| SSH | `ssh -4 zhiqiang@192.168.66.254` | +| Platform | Ubiquiti EdgeRouter X 5-Port, EdgeOS `v3.0.1` (Build `5862409`) | +| Kernel | `4.14.54-UBNT` | +| Time zone | `Asia/Shanghai` | + +Use `zhiqiang` as the default account for all routine gateway operations. Its +key-only SSH login and passwordless `sudo` were verified on 2026-08-04; use +`BatchMode=yes` for unattended read-only checks. `ubnt` remains an existing +administrative account, but is not the routine operations entry point. + +Do not store account passwords, PPPoE credentials, SSH keys, configuration +password hashes, or UISP/UNMS connection strings in this repository. Both +administrative accounts have EdgeOS configuration and sudo privileges. Verify a +new SSH host key out of band before accepting it. + +## Network topology + +| Interface | Address / role | +|---|---| +| `eth0` | `192.168.66.254/24`; LAN 66 | +| `switch0` (`eth1`–`eth3`) | `192.168.55.254/24`; LAN 55 | +| `eth4` | WAN physical port; PPPoE uplink | +| `pppoe0` | WAN default route; MTU `1492`; IPv6 prefix delegation `/60` | + +IPv6 prefix delegation assigns SLAAC-capable `/64` networks to both LANs. +`eth4` applies the WAN IPv4 and IPv6 firewall policies. + +Detailed effective configuration, including firewall binding and WAN exposure, +is recorded in [the EdgeRouter X configuration record](../docs/edgerouter-x-configuration.md). + +## Services and policy + +- DHCP serves both LANs with 24-hour leases. Client DNS is `192.168.66.36`. + Pools are `.38`–`.243` on both networks; selected infrastructure and client + addresses have static mappings. The UniFi controller is advertised as + `192.168.66.46`. +- DNS forwarding listens on `eth0` and `switch0` with a cache size of 512. +- WAN NAT masquerades all IPv4 traffic leaving `pppoe0`. +- `WAN_IN` and `WAN_LOCAL` default to drop, permitting established/related + sessions and rejecting invalid state. Equivalent IPv6 policies permit + necessary ICMPv6 and DHCPv6 traffic. +- `LAN_IN` defines a default drop plus explicit `55 → 66` and `66 → 55` + allows, but it is **not currently applied to an interface**. The active + inter-VLAN posture is therefore ordinary routed access: LAN55 and LAN66 can + communicate bidirectionally unless a different active policy intervenes. + `LAN_OUT` is likewise defined but inactive; WAN policies are active on + `pppoe0`. +- Management services: SSH on TCP 22; EdgeOS GUI on HTTP 80 and HTTPS 443. + +## Safe inspection + +Use the EdgeOS operational CLI for routine inspection. `show configuration +commands` can expose credentials and remote-management tokens, so either avoid +copying it or filter sensitive lines before recording output. + +```bash +ssh -4 -o BatchMode=yes zhiqiang@192.168.66.254 +show version +show interfaces +show ip route +show system uptime +show firewall +``` + +The [official EdgeOS User Guide](https://dl.ubnt.com/guides/edgemax/EdgeOS_UG.pdf) +explains that a firewall policy is a ruleset and must be applied to the +relevant interface/direction to take effect. Use the operational `show +firewall` output—not merely the configured rule definitions—to determine the +effective policy. + +## Maintenance notes + +- EdgeOS writes persistent changes through its configuration tree: enter + `configure`, make the intended change, then run `commit` and `save`. +- The current SSH service reported a non-post-quantum key-exchange warning. + Treat any cryptography or SSH-policy maintenance as a planned, lockout-safe + change following the repository SSH safety procedure. +- This device is distinct from `gfw.windy.lan` (`192.168.66.1`), the OpenWrt + OpenClash gateway. Do not apply OpenWrt procedures to this router. + +## Verification record + +Configuration and reachability were checked by read-only SSH on 2026-08-04. +No network policy changes were made during that inspection. The `zhiqiang` +account password was subsequently rotated using the EdgeOS configuration tree +and verified by a separate SSH login; the password is intentionally not +recorded here. + +Inter-VLAN access was rechecked by read-only SSH on 2026-08-05. A probe sourced +from `192.168.55.254` reached the UniFi controller at `192.168.66.46` with +3/3 ICMP replies. This supports the AP Inform path to +`192.168.66.46:9080`; the controller listener and an online LAN55 AP provide +the corresponding application-level evidence. No firewall changes were made. diff --git a/hosts/ubnt.md b/hosts/ubnt.md index 4499c77..d257502 100644 --- a/hosts/ubnt.md +++ b/hosts/ubnt.md @@ -25,6 +25,7 @@ Verify a changed key out of band before accepting it. | Image | `jacobalberty/unifi:v9.5.21` | | Network mode | host | | HTTPS UI | `https://192.168.66.46:8443` | +| Device inform endpoint | `http://192.168.66.46:9080/inform` | The controller binds `*:8443`. A local HTTPS request to `https://127.0.0.1:8443/` is the primary quick health check. @@ -43,6 +44,14 @@ Without this bypass, a controller request to `localhost:8443` can be sent to the OpenClash gateway instead of remaining local. Details and verification are in [the UniFi localhost proxy note](../docs/unifi-openclash-localhost.md). +## Managed access points + +The controller manages the APs listed in [the UniFi network record](../docs/unifi-network.md). +The inform endpoint is deliberately on port `9080`, not UniFi's common default +of `8080`. During adoption or recovery, use the documented `:9080/inform` URL; +an AP left on `:8080` can remain reachable by ping and SSH while showing +offline in the controller. + ## Safe reconciliation and verification ```bash @@ -55,4 +64,3 @@ curl -k -sS -o /dev/null -w '%{http_code}\n' https://127.0.0.1:8443/ Recreating the service briefly interrupts the controller. Do not use `docker compose down -v`; it can remove persistent data. - diff --git a/hosts/us4.wsvc.info.md b/hosts/us4.wsvc.info.md new file mode 100644 index 0000000..23cde3b --- /dev/null +++ b/hosts/us4.wsvc.info.md @@ -0,0 +1,61 @@ +# us4.wsvc.info + +| Item | Value | +|------|-------| +| Role | WireGuard VPN server (LinuxServer Docker image) | +| SSH | `ssh -4 windy@us4.wsvc.info` (prefer IPv4 from WSL) | +| IPv4 | `185.201.226.122` | +| Compose project | `/opt/wireguard` | +| Compose file | `/opt/wireguard/compose.yml` | +| Container | `wireguard` | +| Image policy | Immutable digest, updated only in an approved maintenance window | +| Public port | UDP `51820` on IPv4 and IPv6 | +| Tunnel subnet | `10.13.13.0/24` | +| Routing policy | IPv4-only full tunnel (`ALLOWEDIPS=0.0.0.0/0`); IPv6 traffic is not guaranteed to use the VPN | + +## Safety + +- Private keys, preshared keys, peer configuration files, and QR codes remain + only in `/opt/wireguard/config`; do not copy them to this repository or + Linear. +- Local rollback archives are stored in `/opt/wireguard/backups` (directory + mode `0700`, archives mode `0600`). They contain private keys, are not an + off-host disaster-recovery backup, and must never leave the server. +- Do not delete, move, or regenerate `/opt/wireguard/config` during + maintenance. +- Before a container recreation, validate `docker compose config` and retain a + verified SSH session for rollback. Do not run `docker compose down -v`. + +## Routine operations + +Run read-only checks through Ansible: + +```bash +cd ansible +ansible-playbook playbooks/health-report.yml --limit wireguard +``` + +The image update and recreate procedure is deliberately separate and requires +an immutable image digest in the server-side Compose file plus an explicit +maintenance-window confirmation: + +```bash +cd ansible +ansible-playbook playbooks/wireguard-maintenance.yml --limit wireguard \ + -e '{"wireguard_maintenance_confirm": true}' +``` + +Before that recreate, apply the reviewed Compose hardening from the repository: + +```bash +ansible-playbook playbooks/wireguard-harden.yml --limit wireguard \ + -e '{"wireguard_harden_confirm": true}' +``` + +## Verification + +- Container `wireguard` is running and interface `wg0` exists. +- UDP `51820` is listening on the host. +- Validate a known client can handshake and sends IPv4 traffic through the VPN. +- Do not treat inactive mobile peers as a failure solely because their latest + handshake is old. diff --git a/inventory/hosts.md b/inventory/hosts.md index cd27b7f..c7885df 100644 --- a/inventory/hosts.md +++ b/inventory/hosts.md @@ -13,7 +13,7 @@ diagnosis and procedures that are deliberately interactive or destructive; see | repo.windy.me | Soft Serve git (on us2) | `ssh -p 2222 windy@repo.windy.me` | 193.9.44.165 | stub | see us2 | | auth.wsvc.info | Vaultwarden public hostname | — (HTTPS) | → us2 | active | see us2 | | us1.wsvc.info | PowerDNS secondary (ns2 host) | TBD | 202.91.35.141 | stub | Auth 5.0.5; see hk2 | -| us4.wsvc.info | Traefik / services | TBD | TBD | stub | — | +| us4.wsvc.info | WireGuard VPN | `ssh -4 windy@us4.wsvc.info` | 185.201.226.122 | active | [hosts/us4.wsvc.info.md](../hosts/us4.wsvc.info.md) | | hk2.chans.xyz | PowerDNS auth (ns1) | `ssh -4 windy@hk2.chans.xyz` | 154.36.174.161 | active | [hosts/hk2.chans.xyz.md](../hosts/hk2.chans.xyz.md) | | ns1.wsvc.info | PowerDNS public NS name | — (DNS) | → hk2 `154.36.174.161` | active | see hk2 | | ns2.wsvc.info | Secondary NS (AXFR/NOTIFY peer) | — (DNS) | → us1 `202.91.35.141` | active | see hk2 | @@ -21,6 +21,7 @@ diagnosis and procedures that are deliberately interactive or destructive; see | pgweb.wsvc.info | PowerDNS Postgres UI | — (HTTPS) | → hk2 | active | see hk2 | | **synapse.chans.xyz** | Matrix homeserver (ESS: Synapse + MAS + Element) | `ssh -4 windy@synapse.chans.xyz` | `169.58.86.13` | **active** | [hosts/synapse.chans.xyz.md](../hosts/synapse.chans.xyz.md) | | **gfw.windy.lan** | OpenWrt LAN gateway / OpenClash | `ssh -4 root@192.168.66.1` | `192.168.66.1` | **active** | [hosts/gfw.windy.lan.md](../hosts/gfw.windy.lan.md) | +| **gw** | EdgeRouter X primary LAN gateway | `ssh -4 zhiqiang@192.168.66.254` | `192.168.66.254` | **active** | [hosts/gw.md](../hosts/gw.md) | | **ubnt** | UniFi Network Controller | `ssh -4 windy@192.168.66.46` | `192.168.66.46` | **active** | [hosts/ubnt.md](../hosts/ubnt.md) | `status: stub` = known to exist; fill `hosts/.md` when next touched.