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:
@@ -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
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user