Commit graph

105 commits

Author SHA1 Message Date
a42e2eeb7b
exclude manero-ca from most things 2026-06-25 16:52:20 -05:00
390eb2d835
cleanup 2026-06-22 16:17:33 -05:00
55451fe285
cleanup resolvers 2026-06-22 16:11:33 -05:00
c85be1d56c
flatcar resolvers 2026-06-22 15:52:51 -05:00
d6cf15165a
restore UISP server + caddy frontend, fix SSH host key regeneration, add tofu proxmox provider 2026-06-21 19:06:33 -05:00
216bd8e668
updates 2026-06-21 13:39:36 -05:00
763d7a938c
updates 2026-06-20 14:21:32 -05:00
1eb0b74fe8
updates 2026-06-19 17:24:54 -05:00
fb5803c866
add FreeBSD bootstrap, irc servers, hostname overrides, and cleanups
- bootstrap.yml: support pkg for FreeBSD sudo/python3 install
- base role: FreeBSD package sets, sudoers paths, host key regen, admin group, usermod process-in-use tolerance
- freebsd role: pkg update/upgrade tasks
- general role: standalone hostname tasks (ungated from network.skip), hostname_override support
- inspircd role: deploy configs for ca/us manero.org irc servers with vault-backed secrets
- uisp role: download and run installer on fresh hosts, skip if already running
- netbox role: stop services before usermod to avoid process-in-use failures
- hosts: add irc_servers group, ca/us.manero.org (Tailscale), drop staging.towerops.net
- all.yml: graham user gets static ed25519 key alongside github keys
2026-06-19 17:19:29 -05:00
b43aa56b5c
icinga2 host updates 2026-06-19 15:24:44 -05:00
b9a52c258a
updates 2026-06-19 09:33:27 -05:00
86ae10d963
resolvers: tune unbound for performance and reliability
- so-reuseport: yes — kernel hashes UDP across the per-thread sockets
  instead of waking all threads, balancing load and improving headroom
- edns-buffer-size: 1232 — DNS Flag Day 2020 default, avoids IP
  fragmentation (retransmits/TCP fallback)
- serve-expired (RFC 8767) with 1800ms client timeout and 86400s stale
  ttl — fast answers from stale cache during upstream slowness/outages
2026-06-07 16:43:33 -05:00
02390adff8
telegraf: disable PrivateMounts in containers (fix LXC 226/NAMESPACE)
The packaged telegraf.service sets PrivateMounts=true, which requires a
mount namespace that unprivileged LXC containers cannot create, so the
service failed to start on the LXC resolver with status 226/NAMESPACE.
Drop in PrivateMounts=false, gated to container guests so VM hosts keep
the unit's hardening.
2026-06-07 13:38:26 -05:00
66986e294b
monitoring: collect Unbound metrics via Telegraf into Prometheus + Grafana
Add a telegraf role (applied to the resolvers) that runs unbound-control
via the inputs.unbound plugin and exposes the stats as a Prometheus
/metrics endpoint on :9273, firewalled to the 204.110.188.0/22 scrape
network. Imports the InfluxData signing key (influxdata-archive.key; the
_compat key expired 2026-01-17) so dnf can validate the package.

- prom.w5isp.com: scrape job "unbound" for resolver1/2:9273
- grafana: "Unbound Resolvers" dashboard (queries, cache hit ratio,
  rcodes, query types, recursion time, request list, cache memory)
- resolvers: disable unused shm-enable to silence the startup warning
2026-06-07 13:34:24 -05:00
43730bf5e7
resolvers: fix empty sshd validation command in post-quantum task
The 'Validate sshd configuration' task had ansible.builtin.command with no
command, so it always errored (one of the following is required:
_raw_params, cmd, argv), tripping the rescue that reverts the drop-in. The
post-quantum KexAlgorithms hardening therefore never applied. Run sshd -t
to validate before restart as intended.
2026-06-07 10:38:46 -05:00
c64f59929e
resolvers: NXDOMAIN blocklist via RPZ; fix Ansible deprecations
Add an RPZ blocklist on the Unbound resolvers that returns NXDOMAIN for a
set of domains (and subdomains) and logs only those matches under rpz-log
tag "blocklist". Domains are templated from resolver_blocked_domains
(single source of truth) into /etc/unbound/blocklist.rpz; respip module
enabled for RPZ support.

Fix deprecation warnings across the Ansible tree:
- apt_repository -> deb822_repository (monitor, aprsc, uisp, grafana),
  removing stale .list files and adding update_cache where deb822 drops it
- community.mysql.* -> ansible.mysql.* (monitor) and requirements.yml
- top-level facts -> ansible_facts[...] (ansible_os_family,
  distribution_release, fqdn, architecture, default_ipv4) repo-wide
2026-06-07 10:32:42 -05:00
c5b658d653
remove findings 2026-06-04 15:25:00 -05:00
5e8823f4dd
fix: resolve all logic bugs
- Remove duplicate firewall include_role from base role (#18)
- Move remote_user from [ssh_connection] to [defaults] in ansible.cfg (#20)
- Add set -euo pipefail to run.sh (#21)
- Delete orphaned playbook_handlers.yml (#22)
- Fix changed_when: true in debian tasks (#23)
- Add tailscale_authkey env var lookup with TAILSCALE_KEY fallback (#25)
- Document -K requirement in bootstrap.yml (#26)
- Replace bare ansible_host=mail/git with IP addresses (#27)
- Update findings.md
2026-06-04 14:43:06 -05:00
ec372fd399
fix: add geerlingguy.postgresql to requirements.yml (used by db.w5isp.com) 2026-06-04 14:41:25 -05:00
8d50d412c6
docs: mark general.yml and docker targets findings as resolved 2026-06-04 14:40:22 -05:00
ffe1cc3820
docs: mark alpine role finding as resolved in findings.md 2026-06-04 14:40:02 -05:00
d81f910797
cleanup: remove orphaned host_vars/db.aprs.me.yml (superseded by db.w5isp.com) 2026-06-04 14:39:29 -05:00
118aa70f51
cleanup: remove broken makefile targets, fix general.yml references
- Remove all docker/build/push targets (no Dockerfile exists)
- Remove broken qc-web target (group doesn't exist)
- Remove clean, dirs, base-*, local, latest targets (docker-only)
- Fix bootstrap-network, run-all to use playbook.yml not general.yml
- Fix install-collections (deleted files referenced)
- Update findings.md
2026-06-04 14:37:55 -05:00
1b7077dd0d
fix: add template defaults, remove alpine ref, fix ssh -t flag
- Add |default() fallbacks for ntpserver, template_run_date,
  deb_mirror, and sshd.* to prevent template crash
- Remove include_role: name=alpine from base tasks (no such role)
- Fix SSH -t flag placement in Makefile reboot targets
- Update findings.md
2026-06-04 14:37:08 -05:00
44c109c6c9
cleanup: remove dead code and stale files
- Delete unreferenced roles: icinga2-container, debug, mailcow, syncthing
- Remove unused Galaxy deps from requirements.yml (docker, kubernetes)
- Delete roles/requirements.yml (graylog2.graylog, bertvv.bind, artis3n.tailscale)
- Merge netbox.netbox from collections/requirements.yml into root, delete file
- Delete 56 stale fact_cache files
- Remove unused inventory groups: home_cluster, dns_servers
- Update findings.md
2026-06-04 14:35:28 -05:00
ee72c64246
fix: resolve best practice and lint violations
- Convert ~290 short module names to FQCNs across all roles
- Change become: yes/no to become: true/false, gather_facts: no to false
- Remove deprecated inject_facts_as_vars from ansible.cfg
- Add missing task names to import_tasks/include_role calls
- Add become: yes to librenms clone task (partial-become fix)
- Add pipefail to risky shell command in udp-gro-fix.yml
- Quote all octal mode values
- Harden .ansible-lint with production profile and empty skip_list
- Update findings.md with all fixes
2026-06-04 14:34:23 -05:00
6f0d308fea
cleanup: remove forgejo, fix ssh key paths, fix tracking issues, update docs
- Remove forgejo role, play, group, host_vars, and DNS references
- Replace hardcoded /Users/graham/.ssh/ paths with env var lookup
- Remove broken setup-user Makefile target
- Add community.mysql collection to requirements.yml
- Add .ansible/ to .gitignore
- git rm --cached .DS_Store files and .vscode/settings.json
- Update CLAUDE.md and README.md to reflect actual state
- Add findings.md with audit results
2026-06-04 14:27:31 -05:00
46f55cdad8
fix 2026-06-02 14:46:50 -05:00
80943fc684
fix idempotency across roles
- firewall: remove ufw reset on every run; pre-check state before enable/disable/rule tasks
- debian: add regexp to fstab lineinfile; clean stale mariadb 12.2 repo
- librenms: use mariadb 11.8 (trixie-compatible)
- mailcow: idempotent git config, docker compose down/pull/up
- tailscale: don't redownload gpg key every run
2026-06-02 11:54:34 -05:00
38638e1134
update 2026-06-01 10:53:23 -05:00
a3bf64521b
updates 2026-05-20 12:06:54 -05:00
02df6fbbc6
resolvers: serve DoH via Caddy, add IPv6 records and PQ SSH
- unbound: add loopback DoH listener (127.0.0.1@8080, http-notls-downstream);
  interface-automatic-ports lists 8080 so interface-automatic does not silently
  drop the non-53 listener; access-control driven by resolver_allowed_netblocks
- caddy: add RedHat/COPR install path, caddy_template var, /var/log/caddy dir;
  new Caddyfile-resolver.j2 terminates LE TLS and h2c-proxies to unbound,
  enforcing the client allow-list via remote_ip
- resolvers: open 80/443 in firewall; prefer post-quantum SSH key exchange
  (sntrup761x25519-sha512) via validated sshd_config.d drop-in
- bind9: zone.j2 emits AAAA records; add ipv6 for resolver1/2 (2606:1c80::240/250)
- playbook: run caddy role on the resolvers group
2026-05-14 10:10:46 -05:00
1612910654
resolvers: enable IPv6 in unbound (listen, do-ip6, access-control)
Listen on ::0, set do-ip6: yes for IPv6 upstream resolution, and add
IPv6 access-control: default-deny ::0/0, allow ::1 and the VNTX
2606:1c80::/32 allocation.
2026-05-14 09:04:22 -05:00
5443e75d70
grafana: prune stale dashboard files from prom on each apply
Without this, a renamed dashboard left the old file on disk and the
provisioner refused to write any dashboards because of a duplicate UID
(seen with towerops-phoenix-live-view vs towerops-phoenix_live_view).
2026-05-11 10:39:35 -05:00
6fa9085b18
grafana: add per-app Loki log dashboards (argocd, aprs, towerops, prop, gridmap) 2026-05-11 10:36:07 -05:00
6d173d9fcb
loki: add log aggregation on prom + promtail daemonset in home-cluster
- new ansible loki role installs loki 3.5.5 on prometheus_servers as a
  systemd unit (single-binary, filesystem store, 30d retention)
- open tcp/3100 on prom.w5isp.com to 10.0.0.0/16
- grafana: provision loki datasource at http://127.0.0.1:3100
- k8s: promtail daemonset + rbac in monitoring namespace, ships pod logs
  to http://10.0.15.31:3100 with cluster=home-cluster label
- argocd: Application points at home/cluster/promtail
2026-05-11 10:24:30 -05:00
b13ae9bd5f
fix(grafana): correct PromEx status_info metric name in towerops dashboards
The dashboards referenced `towerops_prom_ex_status_info` but PromEx exports
this as `towerops_prom_ex_prom_ex_status_info`. The template job dropdown
was empty because Grafana couldn't find any job values for the wrong metric.
2026-05-11 08:15:35 -05:00
4191c0eebb
ansible/hosts: drop decommissioned proxmox + postgresql entries
Removes vm1-380 (10.0.0.1), db.towerops.net and db.aprs.me (10.0.15.22)
from the inventory.
2026-05-10 18:12:54 -05:00
04b3855874
k8s: add Starlink dish prometheus exporter + dashboard
joshuasing/starlink_exporter 0.9.0 (Docker Hub; ghcr.io lags behind
upstream releases). Targets the dish's gRPC management interface at
192.168.100.1:9200, reachable from cluster pods via 10.0.15.254 while
the Starlink router is in bypass mode.

Grafana dashboard "Starlink" covers throughput, PoP ping latency/drop,
obstruction state + 24h total, GPS lock, and dish position/info — synced
to /var/lib/grafana/dashboards/ by the existing grafana role.
2026-05-10 18:11:21 -05:00
8466e4590f
tmobile-exporter: switch to stock json_exporter + nginx sidecar
Replaces the vendored Go exporter with two pre-built upstream images:
- quay.io/prometheuscommunity/json-exporter:v0.7.0 maps the gateway JSON
  into prometheus metrics via JSONPath. The G4AR keys signal blocks as
  "5g" / "4g" (digit-prefixed), which the k8s client-go JSONPath dialect
  only reaches via bracket-with-single-quote notation.
- nginxinc/nginx-unprivileged:1.27-alpine-slim sidecar rewrites
  GET /metrics → GET /probe?module=tmhi&target=http://192.168.12.1/...
  so the existing kubernetes-services scrape job picks it up over the
  apiserver proxy with no Prometheus-side config changes.

Avoiding caddy here: its binary carries cap_net_bind_service file caps,
which the kernel refuses to exec under no_new_privs as a non-root user.

Dashboard "Gateway up" panel now reads up{service="tmobile-exporter"}
(Prometheus's built-in scrape-success gauge) since json_exporter's
/probe endpoint does not emit probe_success.
2026-05-10 17:57:13 -05:00
20649ba33d
grafana: add T-Mobile Home Internet dashboard
15 panels covering gateway up/uptime, 4G/5G bars, and per-band
RSRP/RSRQ/SNR/RSSI time series. Sources metrics from the new
tmobile-exporter (job=tmobile_gateway).
2026-05-10 17:32:06 -05:00
88bdbbe88f
ansible: add 'monitor' role mirroring live monitor.vntx.net (Icinga2 + Web 2)
Codifies the bare-metal Icinga2 master + Icinga Web 2 stack on
monitor.vntx.net (Ubuntu 22.04, Apache + mod_php, MariaDB 10.6, Icinga
2.16, IDO-MySQL backend). Re-running the role against the live host
should produce a near-identical configuration.

Role includes:
- Icinga apt repo + full package set (icinga2, ido-mysql, icingaweb2,
  monitoring-plugins, nagios-plugins-contrib, php + apache deps).
- MariaDB DB + user provisioning for both icinga2 (IDO daemon) and
  icingaweb2 (UI), plus a separate read-only `icinga` user that the UI
  uses to read the IDO. Schema bootstrapped only on first install.
- Templated icinga2 config (constants/zones/icinga2.conf), features-
  enabled symlinks (api/checker/command/ido-mysql/mainlog), and the
  three secret-bearing conf.d files (api-users, pagerduty x2) wired to
  vault vars.
- 23 dormant conf.d/* host + service definitions copied verbatim under
  files/. The live install has `include_recursive "conf.d"` commented
  out (a node-setup CLI artifact); role mirrors that. Set
  `icinga2_load_confd: true` in host_vars to rehydrate them.
- Icinga Web 2 ini files (config/auth/groups/roles/modules) + templated
  resources.ini for DB credentials. monitoring module enabled via
  symlink. icingaweb2 admin user bootstrapped on first install via a
  one-shot insert into icingaweb_user (gated on schema-not-yet-imported).
- Apache rewrite + ssl modules. Vhost itself is left to certbot --apache
  (Let's Encrypt cert is operator-managed, role doesn't manage it).

Cluster-wide cleanup:
- Gut roles/general/tasks/debian/icinga2.yml down to the uninstall
  safety net (kept the sweep that removes icinga2 from non-monitoring
  hosts). The previous master-side install + cert-distribution tasks
  referenced templates that never existed in the role; the new monitor
  role replaces them properly.

Operator action before first apply:
- Create host_vars/monitor.vntx.net/vault.yml with the seven required
  secrets (see roles/monitor/README.md). Live values can be pulled from
  /etc/icinga2/conf.d/{api-users,pagerduty-*,ido-mysql}.conf and
  /etc/icingaweb2/resources.ini on the live host.
- Generate the icingaweb2 admin password hash via
  `php -r 'echo password_hash("plaintext", PASSWORD_DEFAULT);'`.
- Run --check --diff before applying; the role is meant to be idempotent
  but hasn't been verified against the live install.
2026-05-08 13:00:53 -05:00
f3f10c3e36
ansible: rewrite netbox role to match live netbox.vntx.net (bare-metal install)
The previous role was netbox-community/netbox-docker (compose-based) but
the live host runs the upstream tarball install directly: system Postgres
17, system Redis 8, gunicorn under systemd, and Caddy reverse-proxy.
Re-running the old role would have torn the working install down.

This rewrite mirrors what's actually deployed:

- defaults: NetBox 4.6.0, /opt/netbox layout, gunicorn 5w/3t/120s, all
  configuration.py knobs exposed as ansible vars.
- tasks: install OS deps, ensure netbox user, create Postgres role + db,
  download + extract release tarball, run upgrade.sh, render
  configuration.py / gunicorn.py / systemd units, enable services.
- templates: configuration.py.j2 covers the same keys the live file sets;
  netbox.service / netbox-rq.service are byte-equivalent to the live
  units (modulo paths driven by the install-dir var).
- caddy role gets a per-host Caddyfile-netbox.vntx.net.j2 mirroring the
  live vhost (25MB body limit, /static/* file_server, reverse_proxy to
  127.0.0.1:8001).
- inventory: new netbox_servers group; netbox.vntx.net added to
  caddy_servers so the per-host Caddyfile is wired up.
- host_vars/netbox.vntx.net.yml: secrets reference vault_* vars; role
  refuses to render config.py until they're set.

Operator action: create host_vars/netbox.vntx.net/vault.yml with
netbox_secret_key, netbox_db_password, and netbox_api_token_peppers
(value visible via `sudo cat /opt/netbox/netbox/netbox/configuration.py`
on the live host). See roles/netbox/README.md.
2026-05-08 12:17:30 -05:00
2f85d55edd
ansible: drop disabled syncthing play stub 2026-05-08 12:03:40 -05:00
4a84b5b277
ansible: drop dead caddy_ansible dep + commented-out ns/netbox/tailscale_home plays
caddy_ansible.caddy_ansible was only referenced by the (commented) netbox play,
so removing the disabled play also makes the role unused. ns play was already
superseded by the BIND9 play. Tailscale-on-home-hosts was never re-enabled
post-Talos-migration — drop the stub; restore from git history if needed.
2026-05-08 12:02:35 -05:00
0970e29364
ansible: fix two latent bugs surfaced by repo audit
- grafana role: empty grafana_admin_password by default, and only emit
  admin_password into grafana.ini when the var is set. Previously the
  default 'admin' was rewritten on every run, clobbering passwords
  rotated via the Grafana UI.

- bootstrap.yml: probe for sudo state before attempting su-as-root.
  The first play was using become_method=su unconditionally; on hosts
  where graham already has passwordless sudo (or where -K isn't passed),
  ansible blocked indefinitely waiting for a root password prompt.
  Now the install-via-su task only runs when sudo is actually missing,
  and the python3 task uses sudo when available.

Also bring in latest microwaveprop dashboard tweaks.
2026-05-08 11:09:14 -05:00
be96483fb0
prom: add app-specific grafana dashboards + per-pod job override
Dashboards: aprs-me, towerops-{application,beam,ecto,oban,phoenix,phoenix-live-view}.

prometheus.yml.j2: kubernetes-pods job now honors a `prometheus.io/job`
pod annotation so apps can claim a named job label (e.g. towerops)
instead of bucketing under job=kubernetes-pods.
2026-05-08 10:25:17 -05:00
a8e1f2fb73
grafana: replace dashboard 13770 (Korean) with 6417 (English)
13770 'Kubernetes All-in-one Cluster Monitoring KR' has Korean panel titles.
Swap in 6417 'Kubernetes Cluster (Prometheus)' which is fully English.
2026-05-08 10:23:53 -05:00
e1274bc63b
prom: feed home-cluster metrics + provision standard k8s dashboards
Cluster-side:
- Deploy kube-state-metrics (kube-system) for cluster-state series.
- Deploy node-exporter DaemonSet on all 7 nodes via the new monitoring ns
  (privileged PSS, hostNetwork, read-only host mounts).
- Both managed by ArgoCD apps under home/cluster/argocd/apps/.

Prometheus-side:
- Replace direct pod-IP scraping (which can't reach the 10.244.0.0/16 pod
  network from outside the cluster) with API-server-proxy scraping for
  both Services and Pods. Now one target per pod, all healthy.
- Extend external Prometheus RBAC with services/proxy + pods/proxy.

Grafana-side:
- Add file-based dashboard provisioner; ship 5 standard dashboards
  (Node Exporter Full, k8s cluster-via-prometheus, k8s all-in-one,
  k8s monitoring, Prometheus 2.0 overview).

prom.w5isp.com:3000 now shows live data for all 7 cluster nodes plus
the prom host itself.
2026-05-08 10:08:32 -05:00
922552059f
prom: add prometheus stack (prometheus, alertmanager, grafana, node_exporter)
Provisions prom.w5isp.com (10.0.15.31) as the home-cluster monitoring host.
Prometheus 3.6.0 scrapes the Talos cluster API/nodes/cAdvisor via an external
SA token; Grafana ships with the Prometheus datasource pre-provisioned.

host_vars opens 9090/9093/9100/3000 to 10.0.0.0/16 plus SSH from the home LAN
so the firewall role doesn't lock out admin access.
2026-05-08 09:50:31 -05:00