Add playbooks/us4-firewalld.yml, a narrow reconciliation of the audited us4 public zone: fails closed on drift or unknown allowances, never reloads or restarts firewalld, and does not manage Docker rules. Requires explicit apply + provider-console confirmations, backs up the firewalld config and ruleset, schedules an automatic 15-minute rollback via at, and verifies SSH, HTTPS routes, containers, Fail2ban jails, and the WireGuard health check before cancelling rollback. Pins ansible.posix 2.2.2 in requirements.yml. Also expand hosts/us4.wsvc.info.md with deployment config and a live audit snapshot (2026-08-12).
9.5 KiB
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 endpoint | us4.wsvc.info:51820/udp; DNS publishes only A 185.201.226.122 (no native AAAA) |
| 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 |
Upstream image documentation: LinuxServer.io WireGuard.
Deployment configuration
The repository-owned, non-secret Compose declaration is rendered from
ansible/templates/wireguard-compose.yml.j2. The live declaration was verified
on 2026-08-12 with these core settings:
| Setting | Live value / intent |
|---|---|
| Image | lscr.io/linuxserver/wireguard@sha256:ac43e1226878d2611315172d6ea357a95cb326ee73124b91108118efc8666889 |
| Image version | 1.0.20260223-r0-ls119 (build 2026-07-30) |
| Required capability | NET_ADMIN only; host kernel already supplies WireGuard/iptables, so SYS_MODULE and /lib/modules are not granted |
| Filesystem | Read-only container root; executable tmpfs at /run; writable bind mount /opt/wireguard/config:/config |
| Restart | unless-stopped |
| Server mode | Named peers ha, phone, mbp; runtime and configured peer counts both 3 |
| Client DNS | 1.1.1.1 |
| Tunnel routing | IPv4 full tunnel, 0.0.0.0/0; no client IPv6 tunnel |
| Runtime interface | wg0, server address 10.13.13.1/32, listen port 51820 |
| Forwarding/NAT | IPv4 forwarding enabled in the container namespace; wg0 forwarding allowed and egress masqueraded on eth+; IPv6 forwarding disabled |
Docker binds UDP 51820 on both host socket families, but the public hostname
has no AAAA record. Clients using us4.wsvc.info therefore reach the server over
IPv4.
Other host services and firewall (2026-08-12)
This host also carries the windy.me secondary MX and several web applications;
do not build its firewall allowlist from the WireGuard role alone.
| Port | Owner / purpose | Effective public state |
|---|---|---|
TCP 22 |
SSH management | Open |
TCP 25 |
Postfix, mx.windy.me (MX priority 30) |
Open; retain until the secondary-MX role is explicitly retired |
TCP 80, 443 |
Traefik for update.wsvc.info, us4-gate.wsvc.info, and trlm.wsvc.info |
Open |
TCP 3000 |
Semaphore UI direct Docker publish | Open; redundant with the Traefik route and should be removed or bound to loopback |
TCP 8080 |
Traefik direct Docker publish | Open; redundant with the authenticated dashboard route and should be removed or bound to loopback |
UDP 51820 |
WireGuard | Required public endpoint |
TCP 9443 |
Host nghttpx-to-Squid proxy | Listening but blocked by the current firewall |
UDP 123 |
ntpsec | Listening but blocked by the current firewall |
PostgreSQL (5433/5434/5435), MariaDB (3306), and the host Squid TCP
listener (3128) are loopback-only. Squid also owns wildcard UDP sockets, which
are not allowed by the current public zone.
UFW is not installed. Firewalld 2.3.1 is active with nftables. On 2026-08-12,
the reviewed ansible/playbooks/us4-firewalld.yml reconciliation removed the
stale imap, imaps, smtp-submission, and smtps services plus TCP 24,
6443, and 8443 without reloading or restarting firewalld. Runtime and
permanent public-zone state now match exactly: services dhcpv6-client, http,
https, smtp, and ssh, with no explicit ports.
Docker-published ports are accepted through Docker's DNAT/FORWARD chains, so
the public-zone cleanup does not close 3000 or 8080. Their Compose bindings
remain a separate, staged follow-up after the required observation window.
Firewalld logged Docker chain/policy conflicts during the 2026-08-10 boots;
treat any firewall reload or service restart as a maintenance-window operation
and reverify Docker routing. Tracking: Linear W1N-60.
mx.windy.me also publishes AAAA 2602:f9f3:0:2::878, while the host currently
has no global IPv6 address or IPv6 default route. Treat that as a separate
secondary-MX reachability issue.
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 mode0700, archives mode0600). They contain private keys, are not an off-host disaster-recovery backup, and must never leave the server. - Live private keys, preshared keys, generated peer configs, QR images, and
wg0.confare mode0600. Template-onlypeer.confandserver.conffiles are mode0644and do not contain generated key material. /opt/wireguard/configis mode0755, but its sensitive files are0600. The current files are owned by the image's numeric UID/GID rather than the declaredPUID=1000/PGID=1000; the root-run WireGuard processes can use them, but reconcile ownership only after a protected backup and maintenance review.LOG_CONFSis currently unset and the inspected container log contained no QR-code/config banners. Do not enable config logging; generated QR images are credentials.- Do not delete, move, or regenerate
/opt/wireguard/configduring maintenance. - Before a container recreation, validate
docker compose configand retain a verified SSH session for rollback. Do not rundocker compose down -v.
Routine operations
Run read-only checks through Ansible:
cd ansible
ansible-playbook playbooks/health-report.yml --limit wireguard
Preview the narrow, fail-closed public-zone reconciliation:
ansible-galaxy collection install -r requirements.yml
ansible-playbook playbooks/us4-firewalld.yml --limit us4 --check --diff
Apply it only after testing the provider console and keeping an independent SSH rollback session open. The playbook creates a protected server-local backup and a 15-minute automatic rollback before changing rules; it cancels that rollback only after SSH, HTTPS, SMTP, Docker, Fail2ban, and WireGuard checks pass:
ansible-playbook playbooks/us4-firewalld.yml --limit us4 \
-e '{"us4_firewalld_confirm": true, "us4_console_confirm": true}'
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:
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:
ansible-playbook playbooks/wireguard-harden.yml --limit wireguard \
-e '{"wireguard_harden_confirm": true}'
Verification
- Container
wireguardis running and interfacewg0exists. - UDP
51820is 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.
Live audit snapshot (2026-08-12)
The WireGuard service itself is healthy and its installation is broadly reasonable:
- The sanitized Ansible health report returned
status=ok; Compose is valid, the container is running with zero restarts,wg0exists, and UDP51820is listening. - One of three peers had a current handshake during the audit. Two peers had not handshaken since the current container/interface start; confirm those clients only if they are expected to be active.
- The image is immutable-digest pinned, key-bearing files are protected, the
container root is read-only, and the container has
NET_ADMINwithout the broaderSYS_MODULEcapability. - Debian
13.6, kernel6.12.101+deb13-amd64, Docker Engine29.7.2, and Docker Composev5.4.0were observed. No Debian package updates or reboot requirement were pending.
Open host-level follow-up (do not conflate these with a WireGuard outage):
- Disk capacity:
/was 90% used with about 3.4 GiB free. Docker reported about 2.48 GB of reclaimable images and the system journal used about 1.9 GB, but do not prune or vacuum without reviewing retention and rollback needs first. - Docker exposure: the firewalld public-zone cleanup is complete, but
Docker still publishes
3000and8080outside the ordinary host INPUT path. Remove those redundant Compose bindings in separate maintenance units after the observation window, and confirm provider firewall rules first. - Image maintenance: the upstream
latestamd64 image had advanced to1.0.20260223-r0-ls120(build 2026-08-06). Review and pin its immutable digest in a maintenance window rather than updating unattended. - Host hygiene:
apache2.service,certbot.service, andpostgresql@9.6-main.servicewere in a failed state while unrelated Docker workloads remained active. Establish ownership and remove or repair stale units separately. - Resource/log limits: the WireGuard container has no memory, CPU, or PID
limit and uses Docker's
json-filelog driver without a per-container rotation setting. Current log size was small, but limits/rotation should be considered during a reviewed Compose update.