Skip to content

feat(sensitive): ntfy in-house, and Alertmanager repointed at it (#136) - #712

Merged
Gerrrt merged 1 commit into
mainfrom
gerrrt/self-hosted-ntfy-receiver-d447c2
Sep 28, 2026
Merged

Gerrrt merged 1 commit into
mainfrom
gerrrt/self-hosted-ntfy-receiver-d447c2

Conversation

@Gerrrt

@Gerrrt Gerrrt commented Sep 28, 2026 •

Copy link
Copy Markdown
Owner

What changed

  • ntfy on the sensitive tier. It runs on trinity behind Caddy at https://ntfy.matrix.elysium, deny-all. Users, ACLs and the token come from SOPS through NTFY_AUTH_*.
  • Alertmanager repointed. Its three real channels now deliver there, with a bearer token and the tier's root as a per-receiver ca_file. urgent and security also keep a second webhook to their existing ntfy.sh topics.
  • The heartbeat is unchanged. It stays on healthchecks.io, and check_alert_channels.py now fails any heartbeat pointed at a .matrix.elysium host.

Why

#136, with the decision it asked for:

Channel In-house ntfy ntfy.sh
default yes no
urgent yes yes
security yes yes
heartbeat never no (healthchecks.io)

A phone away from home cannot reach trinity: nothing on the tier is exposed, and WireGuard (ADR-0042) goes to the lab. So an in-house-only page would wait for Wi-Fi. For the iPhone, upstream-base-url sends ntfy.sh only the SHA-256 of the topic URL, never the alert.

A dead in-house ntfy still pages. A new http_2xx_tier_ca blackbox probe raises EndpointUnreachable. That alert and AlertmanagerNotificationsFailing are both critical, so they route to urgent and its ntfy.sh copy. validate.sh now pins the correctness route.

Blast radius

  • trinity: one new container. It has expose: only; no published port.

  • prometheus: Alertmanager's receivers change, and Alertmanager and blackbox-exporter gain a tier-ca.pem mount.

  • Deploy order matters. After pulling, make up refuses on each host until its secrets exist:

    • trinity needs the six NTFY_* keys.
    • prometheus needs ALERTMANAGER_{URGENT,SECURITY}_EXTERNAL_URL and ALERTMANAGER_NTFY_TOKEN, plus certificates/tier-ca.pem.

    Follow the cutover in docs/runbooks/verify-the-alert-path.md.

  • The probe target is commented out until cutover, because enabling it early would page urgent continuously.

  • No change to network segmentation or firewall rules. Alertmanager to trinity:443 is same-VLAN, trinity to ntfy.sh uses existing egress, and phones use the existing Hicks to 10.0.99.40:443 pass.

  • No new port published to a VLAN that could not already reach the service

  • No credential added outside secrets/*.sops.yaml. Topic names are treated as credentials too.

Verification

  • ntfy v2.28.0 booted under the compose file's hardening (read-only, cap_drop: ALL, no-new-privileges):

    • Healthy, with docker diff showing nothing written outside /var/lib/ntfy.
    • Auth: anonymous gets 403 both ways. The token can publish and gets 403 on read. phone can read and gets 403 on publish. Topics outside the ACL get 403.
    • A rotated token or hash gives 401 after restart.
    • ?template=alertmanager renders a readable title and summary.
  • Credential formats: make hash-password (Caddy bcrypt, cost 14) and make gen-secret topic names are both accepted.

  • Validation seeds: the seeded .env boots healthy through compose itself, in a throwaway project with the subnet moved. check_hardened_boot.sh cannot run on trinity because the live stack owns the subnet, so the new CI step is its first real run.

  • make validate: passes, including:

    • compose config
    • --probe (wget in the ntfy image)
    • check_caddyfile.sh
    • amtool routes (10 assertions)
    • promtool tests
    • docs and alert-channel checks
    • yamllint, shellcheck, actionlint
  • make validate passes

  • Deployed to the lab and confirmed working. Pending: the cutover runbook ends with a test page seen on Wi-Fi (in-house) and on mobile data (ntfy.sh), then a docker stop sensitive-ntfy drill. Sensitive tier: ntfy, to bring the notification path in-house #136 stays open until then.

  • Rebased onto main after Homepage (Sensitive tier: Homepage as the entry point to the estate #137) landed. Counts are now fourteen services and fifteen SOPS keys, and the build runbook gains Homepage's two keys and the home host-override name, which that PR had left out

  • Docs updated: stack README, the runbooks, security, observability, roadmap, architecture, changelog

Open: whether the Pixel's ntfy app trusts a user-installed root. The README asks for this to be recorded at cutover.

Refs #136

🤖 Generated with Claude Code

Adds ntfy to the sensitive tier, behind Caddy at ntfy.matrix.elysium, and
repoints Alertmanager's three real channels at it. urgent and security also
keep a second webhook to their ntfy.sh topics, because a phone away from
home cannot reach trinity: nothing on the tier is exposed, and WireGuard
goes to the lab. The heartbeat stays on healthchecks.io, and
check_alert_channels.py now fails a heartbeat pointed in-house.

ntfy is deny-all. Two users come from SOPS through NTFY_AUTH_*:
alertmanager may only write the three topics, by token, and phone may only
read them. Measured on v2.28.0 under the compose hardening: 403s where
expected, and a rotated token or hash is a 401 after restart, so the
volume is disposable. Alertmanager sends the token as a header, not
?auth=, because Caddy logs URLs. It verifies against the tier's root
through a per-receiver ca_file.

A dead ntfy still pages. A new http_2xx_tier_ca blackbox probe, written
out and enabled at cutover, raises EndpointUnreachable. That alert and
AlertmanagerNotificationsFailing are both critical, so they reach urgent
and its ntfy.sh copy; validate.sh pins the route.

Not deployed. The cutover is in docs/runbooks/verify-the-alert-path.md.

Refs #136

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@Gerrrt
Gerrrt force-pushed the gerrrt/self-hosted-ntfy-receiver-d447c2 branch from 3be05fc to 79fd90d Compare September 28, 2026 21:23
@Gerrrt
Gerrrt merged commit 7d0f58a into main Sep 28, 2026
8 checks passed
@Gerrrt
Gerrrt deleted the gerrrt/self-hosted-ntfy-receiver-d447c2 branch September 28, 2026 21:27
Gerrrt added a commit that referenced this pull request Sep 29, 2026
…e tier root (#136) (#715)

Enables the blackbox target for https://ntfy.matrix.elysium/v1/health,
which #712 wrote out commented. It was run first from the live exporter
on prometheus: probe_success 1, status 200, and a cert expiry of
2026-10-05T21:59:04Z. That is the same seven-day leaf Caddy serves on
trinity, so the chain verified against the tier's root end to end.

From now on a dead in-house ntfy fires EndpointUnreachable, which is
critical and so reaches urgent's ntfy.sh copy.

The stack README records the open question from #712: the Pixel's ntfy
app accepts a user-installed root. Both phones received a test publish
made with Alertmanager's token.

Refs #136

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Gerrrt added a commit that referenced this pull request Sep 29, 2026
…es (#136) (#718)

Enabling the ntfy probe (#715) paged `urgent` within ten minutes:
"ntfy's certificate has 7 days left", on a leaf Caddy had just issued.
Every sensitive-tier leaf is seven days from step-ca (ADR-0037), so it is
always inside TlsCertificateExpiryImminent's 7-day threshold and the
30-day one. It would have repeated every 4h indefinitely. The comment in
#712 that said the 30-day warning "should never fire here" was wrong.

- The ntfy target gains `renewal: acme`. Both existing expiry rules
  select renewal!="acme", which is a no-op for every target that lacks
  the label.
- New TlsAcmeRenewalStalled, critical, fires when an ACME leaf has under
  36h left for 30m. Caddy renews at a third of the lifetime, about 56h,
  so this means about 20h of failed renewals. Critical so it reaches
  urgent's ntfy.sh copy: when that leaf lapses the in-house path goes too.
- promtool cases: a healthy leaf at five days fires nothing; the same
  leaf at 30h fires only the new rule.
- Rule counts in the docs move from 102 to 103.

Refs #136

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant