feat(ops): alert when a port is published to the internet that nothing allows (stacked on #106) #107

Merged
jfraeys merged 2 commits from jfraeys/feat/exposure-drift-alert into main 2026-10-13 13:03:53 -04:00
Owner

Merge #106 first (stacked on the infra_metrics role).

The LLDAP admin panel sat on the public internet because UFW cannot see Docker-published ports, and only a staging check found it. This makes production tell you within an hour next time.

  • roles/infra_metrics: an hourly timer runs playbooks/files/published_ports.py --prom ... (the same script and the same allowlist staging's connectivity check uses) and writes node-exporter textfile metrics: infra_public_ports_unexpected{container,port}, the total, and a completion timestamp. If docker or iptables is unavailable it exits non-zero and leaves the old file to go stale.
  • One allowlist: infra_public_ports_allowed in inventory/group_vars/all.yml (80/tcp, 443/tcp, 3478/udp, 8443/tcp). validate-connectivity.yml now reads it, so staging and production cannot disagree about what is allowed.
  • Alerts: PublicPortUnexpected (critical, 5 minutes) and InfraExposureCheckStale (warning, no completed check for 3 hours).
  • published_ports.py refactored into functions (find_unexpected, prom_text, main) with identical default CLI output, which staging depends on.

Tests: 8 unit tests with faked docker/iptables (flags only what is neither allowed nor dropped; a DROP in either IP family counts; drops never excuse UDP; empty allowlist flags all public ports; default output unchanged; metrics written atomically with mode 0644; clean host writes zero; a failing check leaves the old file) plus 3 promtool scenarios (fires after the 5-minute window with the right labels, a clean host never alerts, a stopped timer goes stale), mutation-checked. All 36 rules pass promtool check. ansible-lint and yamllint clean. While writing the test I had the for: 5m boundary wrong (it fires at the second evaluation, not before); corrected the test, not the rule.

Not exercised on a real node: the timer and first run; check with systemctl list-timers infra-exposure.timer and ls /var/lib/node_exporter/textfile_collector/infra_exposure.prom after deploying with --tags infra_metrics,prometheus.

🤖 Generated with Claude Code

**Merge #106 first** (stacked on the infra_metrics role). The LLDAP admin panel sat on the public internet because UFW cannot see Docker-published ports, and only a staging check found it. This makes production tell you within an hour next time. - roles/infra_metrics: an hourly timer runs playbooks/files/published_ports.py --prom ... (the same script and the same allowlist staging's connectivity check uses) and writes node-exporter textfile metrics: infra_public_ports_unexpected{container,port}, the total, and a completion timestamp. If docker or iptables is unavailable it exits non-zero and leaves the old file to go stale. - One allowlist: infra_public_ports_allowed in inventory/group_vars/all.yml (80/tcp, 443/tcp, 3478/udp, 8443/tcp). validate-connectivity.yml now reads it, so staging and production cannot disagree about what is allowed. - Alerts: PublicPortUnexpected (critical, 5 minutes) and InfraExposureCheckStale (warning, no completed check for 3 hours). - published_ports.py refactored into functions (find_unexpected, prom_text, main) with identical default CLI output, which staging depends on. Tests: 8 unit tests with faked docker/iptables (flags only what is neither allowed nor dropped; a DROP in either IP family counts; drops never excuse UDP; empty allowlist flags all public ports; default output unchanged; metrics written atomically with mode 0644; clean host writes zero; a failing check leaves the old file) plus 3 promtool scenarios (fires after the 5-minute window with the right labels, a clean host never alerts, a stopped timer goes stale), mutation-checked. All 36 rules pass promtool check. ansible-lint and yamllint clean. While writing the test I had the for: 5m boundary wrong (it fires at the second evaluation, not before); corrected the test, not the rule. Not exercised on a real node: the timer and first run; check with systemctl list-timers infra-exposure.timer and ls /var/lib/node_exporter/textfile_collector/infra_exposure.prom after deploying with --tags infra_metrics,prometheus. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
feat(secrets): one expiry classifier and a manifest-only expiry report
All checks were successful
Lint / lint (pull_request) Successful in 1m13s
Secret scan / scan (pull_request) Successful in 14s
44ba182f43
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
feat(secrets): rotation lock for vault read-modify-write cycles
All checks were successful
Lint / lint (pull_request) Successful in 1m58s
Secret scan / scan (pull_request) Successful in 12s
b35100b7dc
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
feat(secrets): monthly expiry report workflow and the manifest facts the repo knows
All checks were successful
Lint / lint (pull_request) Successful in 1m42s
Secret scan / scan (pull_request) Successful in 24s
d05d8e0b96
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
feat(ops): tool version registry, monthly upstream survey, and the update/security runbook
Some checks failed
Lint / lint (pull_request) Has been cancelled
Secret scan / scan (pull_request) Has been cancelled
86986b754f
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
ops: tool versions report runs weekly (Mondays), one issue per month refreshed each week
All checks were successful
Lint / lint (pull_request) Successful in 1m47s
Secret scan / scan (pull_request) Successful in 17s
1e4d538f6d
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
feat(ops): secret expiry and tool version drift as Prometheus metrics and alerts
All checks were successful
Lint / lint (pull_request) Successful in 1m35s
Secret scan / scan (pull_request) Successful in 15s
168d0f4b02
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
feat(ops): hourly check for unexpectedly public ports, alerting through Prometheus
All checks were successful
Lint / lint (pull_request) Successful in 1m29s
Secret scan / scan (pull_request) Successful in 11s
253d2df8e4
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
jfraeys/infra!107
No description provided.