mirror of
https://github.com/MBombeck/HealthLog.git
synced 2026-11-10 13:22:14 +00:00
chore(compose): forward and document NOTIFICATION_PRIVATE_ORIGINS
Put the variable on the compose environment whitelist (without it a value in .env never reaches the process), on both example files, and in the env manifest so check-env reports it. The self-hosting notifications page gains a section on notification targets on a private network: the exact-origin grammar, what stays refused even when listed, and a table of which target is refused where so an operator knows which lever to pull. Refs #947
This commit is contained in:
1 parent
984a75d036
commit
9630b59973
5 files changed
+119
-9
No files matched your search
@@ -92,6 +92,17 @@ API_TOKEN_HMAC_KEY="REPLACE-WITH-OUTPUT-OF-openssl-rand-hex-32"
|
||||
# TELEGRAM_WEBHOOK_SECRET=""
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Notification targets on a private network (optional)
|
||||
# -----------------------------------------------------------------------------
|
||||
# Webhook (Gotify, Home Assistant, n8n) and ntfy targets on your own network,
|
||||
# comma-separated exact origins, e.g. "https://gotify.example.com,http://ntfy.lan:8080".
|
||||
# A target resolving to a private address is otherwise refused at send time.
|
||||
# Loopback, link-local and metadata addresses stay refused even when listed.
|
||||
# See docs/self-hosting/notifications.md.
|
||||
# NOTIFICATION_PRIVATE_ORIGINS=""
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# AI Provider (optional)
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
@@ -71,6 +71,20 @@ API_TOKEN_HMAC_KEY="<openssl rand -hex 32>"
|
||||
# NIGHTSCOUT_PRIVATE_ORIGINS=""
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# Notification targets on a private network -- optional
|
||||
# -----------------------------------------------------------------------------
|
||||
# Webhook (Gotify, Home Assistant, n8n, a Discord relay) and ntfy targets on
|
||||
# your own network, comma-separated exact origins (scheme://host[:port], no
|
||||
# path). A target that resolves to a private address is otherwise refused at
|
||||
# send time; the test button reports it as private_origin_not_approved. A
|
||||
# listed origin is still dialled through the pinned resolver with redirects
|
||||
# forbidden; loopback, link-local and metadata addresses stay refused even
|
||||
# when listed, and a malformed entry is logged once and grants nothing.
|
||||
# See docs/self-hosting/notifications.md.
|
||||
# NOTIFICATION_PRIVATE_ORIGINS=""
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# WHOOP -- optional integration
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
@@ -205,6 +205,13 @@ x-healthlog-env: &healthlog_env
|
||||
# validator should accept, comma-separated (e.g. "http://nightscout.lan").
|
||||
# Without it a LAN/private Nightscout URL is refused as an SSRF target.
|
||||
NIGHTSCOUT_PRIVATE_ORIGINS: "${NIGHTSCOUT_PRIVATE_ORIGINS:-}"
|
||||
# Webhook (Gotify, Home Assistant, n8n) and ntfy targets on your own
|
||||
# network, comma-separated exact origins (e.g. "https://gotify.example.com,
|
||||
# http://ntfy.lan:8080"). Without it a target that resolves to a private
|
||||
# address is refused at send time as an SSRF target. Loopback, link-local
|
||||
# and metadata addresses stay refused even when listed. See
|
||||
# docs/self-hosting/notifications.md.
|
||||
NOTIFICATION_PRIVATE_ORIGINS: "${NOTIFICATION_PRIVATE_ORIGINS:-}"
|
||||
# Open-Meteo endpoints for the environment/weather correlation. Default to
|
||||
# the hosted service; point them at your own Open-Meteo deployment to keep
|
||||
# the coordinate lookups off a third party.
|
||||
|
||||
@@ -180,10 +180,12 @@ HealthLog publishes to that topic.
|
||||
server if you self-host ntfy) and **Topic**, then save.
|
||||
|
||||
If your ntfy server requires auth, an access token can be attached; the
|
||||
server URL is validated as a public host and dialled through the egress
|
||||
guard (`src/lib/notifications/senders/ntfy.ts`). Urgent events are sent at
|
||||
ntfy's top priority (`5` / max) so they bypass batching and surface on the
|
||||
lock screen.
|
||||
server URL is validated at save time and dialled through the egress guard
|
||||
(`src/lib/notifications/senders/ntfy.ts`). A self-hosted ntfy on your own
|
||||
network needs the operator grant described under [Notification targets on
|
||||
a private network](#notification-targets-on-a-private-network). Urgent
|
||||
events are sent at ntfy's top priority (`5` / max) so they bypass batching
|
||||
and surface on the lock screen.
|
||||
|
||||
## Webhook
|
||||
|
||||
@@ -197,11 +199,76 @@ optionally, one custom header (e.g. an auth token).
|
||||
**header value**.
|
||||
2. Save. A Test button fires a one-off POST so you can confirm routing.
|
||||
|
||||
The URL is validated as a public host and dialled through
|
||||
`safeFetch({ requirePublicHost: true })` — the SSRF guard blocks private
|
||||
and loopback ranges because the target is user-supplied
|
||||
(`src/lib/notifications/senders/webhook.ts`). The webhook carries the
|
||||
event's urgency flag so you can route urgent events differently.
|
||||
The URL is validated at save time and dialled through `safeFetch` with
|
||||
the connect-time resolver pin — the SSRF guard refuses a target that
|
||||
resolves to a private or loopback address because the target is
|
||||
user-supplied (`src/lib/notifications/senders/webhook.ts`). A Gotify or
|
||||
Home Assistant on your own network needs the operator grant described in
|
||||
the next section. The webhook carries the event's urgency flag so you can
|
||||
route urgent events differently.
|
||||
|
||||
## Notification targets on a private network
|
||||
|
||||
Every webhook and ntfy target is resolved at send time and refused when
|
||||
the answer is a private, loopback, link-local or CGNAT address. That is
|
||||
the DNS-rebinding defence, and it applies to a public-looking name with a
|
||||
LAN A record (a Gotify behind Nginx Proxy Manager with a DNS-01
|
||||
certificate) exactly as it applies to `http://192.168.1.20`. The test
|
||||
button and the scheduled delivery take the same decision, so a test that
|
||||
fails with "Private network target" means nothing would have arrived
|
||||
either.
|
||||
|
||||
The operator opens a target by listing its exact origin in the server
|
||||
environment:
|
||||
|
||||
```env
|
||||
NOTIFICATION_PRIVATE_ORIGINS="https://gotify.example.com,http://ntfy.lan:8080"
|
||||
```
|
||||
|
||||
Each entry is one `scheme://host[:port]`, the same grammar as
|
||||
`NIGHTSCOUT_PRIVATE_ORIGINS`:
|
||||
|
||||
- only `http` and `https`; scheme, host and effective port must all match
|
||||
what the user saves (`https://gotify.example.com:8443` and
|
||||
`https://gotify.example.com` are two different grants);
|
||||
- no path, query, credentials, wildcard (`*.lan`) or CIDR (`10.0.0.0/8`) —
|
||||
a range grant would let a DNS rebinding to any address in the range pass
|
||||
the pin, so an operator with several relays lists several origins;
|
||||
- listing a host does not list its sub-hosts or sibling ports;
|
||||
- loopback (`127.0.0.0/8`, `::1`, `localhost`), the unspecified address,
|
||||
link-local and the cloud-metadata range (`169.254.0.0/16`, `fe80::/10`)
|
||||
cannot be granted: a literal entry is logged once and ignored, and a
|
||||
listed name that resolves there is refused at dial time. On a
|
||||
host-network deployment, list the LAN address of the relay instead of
|
||||
`localhost`;
|
||||
- RFC1918, IPv6 unique-local and CGNAT (a Tailscale address or MagicDNS
|
||||
name) are what the list is for;
|
||||
- a malformed entry is logged once at first use and grants nothing; the
|
||||
valid entries beside it keep working.
|
||||
|
||||
A listed origin is still dialled through the pinned resolver with
|
||||
redirects forbidden, so a relay that answers with a redirect to another
|
||||
host is refused. The variable has to be on the compose `environment:`
|
||||
whitelist to reach the container; the published `docker-compose.yml`
|
||||
lists it. Restart the app after changing it.
|
||||
|
||||
What is refused where, so you know which lever to pull:
|
||||
|
||||
| Target | Without the grant | With the exact origin listed |
|
||||
| ----------------------------------------------------------- | ----------------- | ---------------------------- |
|
||||
| Public name resolving to a public address | works | works, public pin |
|
||||
| Public name with a private A record (LAN reverse proxy) | refused at send | works |
|
||||
| `gotify.lan`, `gotify.home.arpa`, a Docker service name | refused at send | works |
|
||||
| `gotify.local` (mDNS) | refused at save | works |
|
||||
| Literal `192.168.x.x`, `10.x.x.x`, `100.64.x.x`, `fd00::/8` | refused at save | works |
|
||||
| `localhost`, `127.0.0.1`, `169.254.169.254`, `::1` | refused at save | still refused |
|
||||
|
||||
A private target that is not listed is refused when the card is saved
|
||||
(`422`, `meta.errorCode` = `private_origin_not_approved`, the message
|
||||
names the variable) or, for a DNS name the save cannot classify, when the
|
||||
test button or a reminder dials it. The resolved address is never shown
|
||||
to the user. Telegram, email and Web Push are not affected: their hosts
|
||||
are not user-supplied.
|
||||
|
||||
## Email (SMTP)
|
||||
|
||||
|
||||
@@ -226,6 +226,17 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Notification targets on a private network",
|
||||
"description": "Operator grant for webhook (Gotify, Home Assistant, n8n) and ntfy targets on the operator's own network. Unset, a target that resolves to a private address is refused at send time and the test button reports private_origin_not_approved. Optional; public targets never need it.",
|
||||
"required": false,
|
||||
"variables": [
|
||||
{
|
||||
"name": "NOTIFICATION_PRIVATE_ORIGINS",
|
||||
"purpose": "Comma-separated exact origins (scheme://host[:port], no path, no wildcard, no CIDR), e.g. \"https://gotify.example.com,http://ntfy.lan:8080\". Loopback, link-local and metadata addresses stay refused even when listed; a malformed entry is logged once and grants nothing."
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"name": "Off-host backups",
|
||||
"description": "Required ALL-OR-NONE: any of these without the rest disables backups silently.",
|
||||
|
||||
Reference in new issue
Block a user