Warn that deny-all takes unrelated notifications down with it

The page recommended a server-wide setting for one topic's sake and described
only the effect on that topic. Applied to a real server it closes every other
one, and one unreadable topic denies the rest along with it: the apps subscribe
to all of a server's topics over a single request, and ntfy authorises that by
looping the list and returning 403 for the whole request on the first topic the
account cannot read. Nothing reports which one. So a stray subscription takes
down alerts and notifications together, both correctly configured, and it reads
as the server being broken.

The symptom hides it further. A denied subscribe fails the WebSocket upgrade,
which the apps report as WebSocketNotSupportedException or a 403 on the upgrade
- which points at the reverse proxy. Publishing keeps working the whole time,
since that path carries a token, so a test alert arriving proves nothing about
whether anybody can read it.

The warning sits between the setting and the commands, where a reader meets it
before running them rather than after, and a test holds it in that order. The
grant block gives the reading account read on "*" rather than on the alert
topic alone - the narrow version is the configuration that took everything
else down, so the block somebody pastes should not be it.

Also stated, because it is the natural worry and the answer is not obvious: an
everyone grant does cover a logged-in user, on topics where that user has no
entry of their own. Logging in to read the alert topic does not cost you the
anonymous grant on dejima's.

Behaviour read from ntfy's source. The documentation covers neither the
multi-topic authorisation loop nor the everyone-versus-authenticated precedence.
This commit is contained in:
2026-08-29 00:43:32 -07:00
parent 40ebbecda4
commit b2e578c7e1
2 changed files with 97 additions and 3 deletions
+68 -3
View File
@@ -247,22 +247,87 @@ auth-file: /var/lib/ntfy/user.db
auth-default-access: deny-all
```
> ⚠️ **This is a server-wide switch, and on its own it will break notifications
> that have nothing to do with dejima.** It closes *every* topic on that server,
> and because the apps subscribe to all of a server's topics over one
> connection, a single topic your account cannot read returns 403 for **all** of
> them. Grant everything back before you turn it on, or the first thing you
> notice is unrelated alerts going quiet.
```sh
ntfy user add guard # role user, not admin
ntfy user add phone # you, subscribing
ntfy access guard dejima-alerts write-only
ntfy access phone dejima-alerts read-only
ntfy access phone "*" read-only # EVERY topic you subscribe to
ntfy access everyone dejima-notify read-write # dejima's topic, as it was
ntfy token add guard # prints tk_..., for secrets/config
ntfy access # read it back
```
`deny-all` is server-wide — there is no per-topic default — so anything else
publishing anonymously has to be granted back by name. That last line does it
for `dejima-notify-watch`, which has no token to offer, and leaves dejima's
publishing anonymously has to be granted back by name. The `everyone` line does
that for `dejima-notify-watch`, which has no token to offer, and leaves dejima's
topic exactly as open as it is today. **Nothing about dejima has to change**, and
the devbox still ends up with no access to `dejima-alerts` at all.
**The `"*"` for the reading account is doing real work**, and narrowing it is
where this goes wrong. Name each topic instead if you prefer, but name *all* of
them — every topic that account is subscribed to anywhere, not just the two
here. A single one left out denies the rest along with it.
Anything else that was publishing anonymously needs its own line. Wildcards are
allowed, so a family goes in one:
```sh
ntfy access everyone "home-*" read-write # whatever else lived on this server
```
The mechanism, since the symptom hides it: the apps subscribe over one request,
`/topic-a,topic-b,topic-c/ws`, and ntfy authorises it by looping the list and
returning 403 for the whole request on the first topic the account cannot read.
Nothing reports which one. That is why a single missing grant reads as the
server being broken rather than as one line being absent.
**An `everyone` grant covers a logged-in user too**, where that user has no
entry of their own for the topic: ntfy resolves permissions from the specific
user's entries *and* `everyone`'s, preferring the user's own, then the longer
topic pattern. Logging in does not lose you the anonymous grants — which is why
a topic left open for dejima stays readable by the phone account.
**Subscribers have to log in now, and the symptom does not say so.** A denied
subscribe fails the WebSocket upgrade, which surfaces in the apps as
`WebSocketNotSupportedException` or `websocket upgrade failed with HTTP 403` —
which reads as a broken reverse proxy, not as an ACL doing its job. Publishing
keeps working throughout, because that path carries a token, so a test alert
arriving proves nothing about whether anyone can read it.
Add the server with the `phone` credentials in the web app under
**Settings → Users**, or per-server in the Android app. Two commands separate
the ACL from the proxy without involving WebSockets at all:
```sh
curl -s -o /dev/null -w '%{http_code}\n' https://ntfy.example.invalid/dejima-alerts/json?poll=1
curl -s -o /dev/null -w '%{http_code}\n' -u phone:PASS https://ntfy.example.invalid/dejima-alerts/json?poll=1
```
403 then 200 is the ACL working. Anything else is the proxy. To find a stray,
ask about each topic on its own and then about all of them at once — the
combination failing while every part succeeds is the loop above:
```sh
for t in dejima-alerts dejima-notify whatever-else; do
printf '%s ' "$t"
curl -s -o /dev/null -w '%{http_code}\n' -u phone:PASS \
"https://ntfy.example.invalid/$t/json?poll=1"
done
```
A browser cannot set an `Authorization` header on a WebSocket, so anything
hand-rolled passes `?auth=`, whose value is base64 of the literal string
`Basic ` followed by base64 of `user:pass` — base64 of a base64, url-safe and
unpadded. The apps do this for you; it only matters if you are writing the
client.
**Neither account may be an `admin`.** An admin bypasses the ACL entirely and
has read-write everywhere, so an admin token is a key to the alert topic no
entry can take away. `ntfy user add` defaults to the `user` role, which is what
+29
View File
@@ -878,3 +878,32 @@ SSHEOF
# closed topic that is open.
grep -q 'admin bypasses the ACL' "$REPO/README.md"
}
@test "the deny-all advice warns that it closes the rest of the server too" {
# It is a server-wide setting recommended for one topic's sake. A reader who
# applies it and loses every other subscription was told to do that, and the
# symptom - a failed WebSocket upgrade - points at their reverse proxy
# rather than at the setting they just changed.
grep -q 'break notifications' "$REPO/README.md"
grep -q '403' "$REPO/README.md"
grep -qi 'log in' "$REPO/README.md"
# The warning has to sit with the setting, not below the commands that
# follow it. A reader who has already run the block has already broken the
# other notifications on that server.
yaml=$(grep -n 'auth-default-access: deny-all' "$REPO/README.md" | head -1 | cut -d: -f1)
warn=$(grep -n 'server-wide switch' "$REPO/README.md" | head -1 | cut -d: -f1)
cmds=$(grep -n 'ntfy user add guard' "$REPO/README.md" | head -1 | cut -d: -f1)
[ "$warn" -gt "$yaml" ] && [ "$warn" -lt "$cmds" ]
# And the block a reader pastes has to be one that works. Granting the
# reading account only the alert topic is the configuration that took down
# everything else on the server.
grep -q 'ntfy access phone "\*"' "$REPO/README.md"
# The apps subscribe to every topic over one request, and ntfy denies the
# whole request on the first topic the account cannot read. Without this,
# a single stray subscription looks like the server being broken and both
# correctly configured topics look like the thing that broke.
grep -q 'returning 403 for the whole request' "$REPO/README.md"
}