Compare commits

...

6 Commits

Author SHA1 Message Date
github-actions
12add9a534 GitHub bot : README updated 2026-08-25 17:10:35 +00:00
Alexandre
556eca95d3 fix(sabnzbd): stop forwarding X-Forwarded-For, which ingress 403s on (#3023)
Reported from a remote session: ingress answered
`403 External internet access denied - https://sabnzbd.org/access-denied`.

Root cause is `check_access()` in `sabnzbd/interface.py`:

    # Never check the XFF header unless access would have been granted
    # based on the remote IP alone!
    if is_allowed and cfg.verify_xff_header() and (xff_ips := ...):
        is_allowed = all(is_local_addr(ip) or is_loopback_addr(ip)
                         for ip in xff_ips)

nginx's own address is loopback, so the first test passes, and then every
address in X-Forwarded-For has to be local too. Supervisor puts the browser's
address in that header, so anyone reaching Home Assistant from outside the LAN
is refused. `verify_xff_header` defaults to on (`cfg.py:531`), so this is not a
configuration a user opted into.

Reproduced against the running add-on, `GET /config/general/`:

    no X-Forwarded-For                    200
    X-Forwarded-For: 81.164.12.7          403  External internet access denied
    X-Forwarded-For: 81.164.12.7, 172.30.32.2  403
    X-Forwarded-For: 192.168.1.44         200

which is why it worked on the LAN and not from outside. Verified the fix the
same way, running the shipped nginx.conf and ingress.conf in front of the live
add-on with both variants side by side: the current config 403s on a public
address, the fixed one answers 200 for all three chains, and redirects, static
roots and the API are unaffected. That instance has an empty `url_base`, so the
pass-through routing is now confirmed for both `url_base` values.

The header was forwarded because SABnzbd reads it — the wrong test, since what
it does with it is reject. Clearing it leaves SABnzbd looking at nginx's
loopback address, which is what it saw before the header was added; ingress is
gated by Home Assistant authentication before reaching this proxy either way.

The evidence.md entry records the methodology error, per the skill's own
feed-the-skill rule.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-08-25 16:15:13 +02:00
Alexandre
6ccd6a2eda Revert "Revert "feat(sabnzbd): enable Home Assistant ingress (#3019)" (#3021)" (#3022)
This reverts commit 4cf0e3aa23.
2026-08-25 15:54:59 +02:00
Alexandre
f307fdc462 docs(skill): correct the CHANGELOG heading date format (#3020)
SKILL.md's step 7 said to match `## X.Y (DD-MM-YYYY)`. The repo does not use
that: 7705 dated CHANGELOG headings are ISO `YYYY-MM-DD` against 363 in
`DD-MM-YYYY`, and the newest entry is ISO in 125 of 135 add-ons. Following the
instruction cost a Copilot review round on #3019.

`DD-MM-YYYY` is not invented, which is presumably how it got written down. It
is what `onpush_builder.yaml` inserts with `date '+%d-%m-%Y'` when a push
arrives with no heading for the config.yaml version, and it is the addons_updater
bot's default in `99-run.sh` — but that bot runs here with `date_iso8601: true`
(confirmed against the running add-on's options), which is why almost everything
on master is ISO. Neither is a reason to write `DD-MM-YYYY` by hand.

The traps.md entry also records that the builder's duplicate check is
`grep -q "^## ${version} ("` — keyed on the exact config.yaml version and blind
to the date — so an ISO heading you wrote yourself still suppresses the bot's
insertion.

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-08-25 15:52:22 +02:00
Alexandre
4cf0e3aa23 Revert "feat(sabnzbd): enable Home Assistant ingress (#3019)" (#3021)
This reverts commit 8341e542fb.
2026-08-25 15:49:26 +02:00
Alexandre
8341e542fb feat(sabnzbd): enable Home Assistant ingress (#3019)
* feat(sabnzbd): enable Home Assistant ingress

The add-on already carried a complete but disabled nginx ingress scaffold:
`etc/nginx/` with its includes and a `servers/ingress.conf`, a
`cont-init.d/32-nginx_ingress.sh` short-circuited by `exit 0`, and
`ENV PACKAGES="nginx"` in a Dockerfile byte-identical to nzbget's. Only
`ingress: true` and the s6 service that starts nginx were missing.

What SABnzbd 5.1.1 actually needs from the proxy, measured against the
running add-on rather than assumed:

- Its interface emits only relative links (`href="../../config/general/"`,
  `href="../../staticcfg/css/Auto.css"`, `action="./one"`), and grepping the
  5.1.1 source for `(href|src|action)="/` across `interfaces/{Glitter,Config,
  wizard}` and for absolute `url:` literals in the Glitter JavaScript returns
  nothing. A plain pass-through proxy preserves path depth, so no `sub_filter`
  is warranted. The previous config's `sub_filter /sabnzbd ...` would also have
  mangled the `https://sabnzbd.org/wiki/...` help links present on every
  config page.
- Redirects are the one exception: `Raiser()` prefixes `cfg.url_base()`, so
  `GET /` answers `303 Location: /sabnzbd/wizard/`. One `proxy_redirect`
  handles every observed case; all of them were path-absolute, never a full
  URL. Login redirects and logout go through the same `Raiser()`, and the
  session cookie's path is hardcoded to `/` (`interface.py:316`), so it is
  still sent under the ingress path.
- SABnzbd rejects a Host header that is not an IP literal:
  `Host: homeassistant` answers 403 "Hostname verification failed", while
  `Host: 192.168.1.5:8123` answers 200. nginx therefore sends `$proxy_host`
  instead of including the shared `proxy_params.conf`, which forwards
  `$http_host`.

`ingress_entry: sabnzbd` is dropped rather than kept: Supervisor appends it to
the ingress URL, which only resolves while the user's `url_base` is literally
`/sabnzbd`, and that is a setting they can change. SABnzbd serves the same
interface at `/` as under its `url_base` (verified for `/config/general/`,
`/static/`, `/staticcfg/` and `/wizard/`), so entering at the ingress root
works for any value, including the empty code default.

Ingress traffic reaches SABnzbd as `127.0.0.1:8080` and so is not filtered by
a user's host whitelist; direct ip:port access is unchanged and still is.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(sabnzbd): keep the ingress Location relative and scope the login cookie

Exercising the shipped config against the running add-on caught a bug that
reading it did not. With nginx's default `absolute_redirect on`, rewriting
`Location: /sabnzbd/wizard/` produced
`http://homeassistant.local:18099/api/hassio_ingress/<token>/sabnzbd/wizard/`
— nginx expands a scheme-less replacement using the browser's Host and its own
listen port, which is the add-on's internal ingress port and is not reachable
from the browser. `absolute_redirect off` keeps it a path, which the browser
resolves against the Home Assistant origin.

`proxy_cookie_path` comes from Codex's review of the diff. SABnzbd hardcodes
the login cookie to `Path=/` (`interface.py:316`), so on the shared ingress
origin the browser would send it to every other add-on's ingress path as well.

Verified end to end by running the shipped nginx.conf and ingress.conf against
the live add-on, with the browser Host set to a non-IP hostname throughout:
all five redirect cases return a path under the ingress entry, the config
pages, wizard, API and both static roots return 200, and a stub upstream
emitting `Path=/` comes back rewritten to the ingress path.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(sabnzbd): drop webui, which the add-on linter forbids alongside ingress

frenck/action-addon-linter fails the PR with "'webui' should be removed,
Ingress is enabled." No other ingress add-on in this repo keeps the key. The
"Open Web UI" button now opens ingress; the ports mapping is untouched, so
direct ip:port still works, it just has to be typed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(sabnzbd): make the nginx finish script actually work on s6-overlay v3

CodeRabbit is right that the execline finish script copied from nzbget is
inert on this image. s6-portable-utils dropped `s6-test` in favour of
execline's `eltest` — s6-overlay 3.2.1.0 ships no `s6-test` at all — so
execlineb cannot run the first `if` block and never reaches s6-svscanctl.
`/var/run/s6/services` is also the v2 scandir path; v3's legacy services.d
compatibility layer uses /run/service.

Rather than port it to eltest, use the shell form the scrutiny add-on already
ships: `kill -15 1` signals s6-overlay's init directly, so it depends on
neither the s6 tool set nor the scandir path, and it is three lines shorter.
The 0 and 256 exclusions are kept, so a normal shutdown does not trigger it.

Also fix the CHANGELOG date to YYYY-MM-DD per Copilot: that is what this file
and 7705 of the repo's 8068 dated headings use.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-08-25 15:46:36 +02:00
10 changed files with 83 additions and 26 deletions

View File

@@ -169,7 +169,9 @@ CI gates on a PR: **`CHANGELOG.md` updated** (hard fail), the **HA add-on linter
(`frenck/action-addon-linter`, blocking — not the weekly Super-Linter, which is non-blocking), and
the **add-on image build**. Bump `version` anyway (`X.Y.Z.N`, never `X.Y.Z-N`, see
`references/traps.md#versioning`) — Supervisor won't offer a rebuild without it. Update
`README.md` if you added options; match the CHANGELOG heading format `## X.Y (DD-MM-YYYY)`.
`README.md` if you added options; write the CHANGELOG heading as `## <version> (<date>)`,
matching the date format already in that file — almost always ISO `YYYY-MM-DD`, see
`references/traps.md#ci-and-review-bots`.
Write the body to a file, `gh pr create --body-file`: state what was measured, what changed,
**what is not verified**, and how to roll back the riskiest hunk alone.

View File

@@ -34,6 +34,12 @@ then generalising it to all hosts.**
would have deleted a user's hand-written configuration.
- A GPU probe created a hardware context — but that proved the driver worked, not that Chromium's
GPU path did.
- SABnzbd's source was grepped to see which proxy headers it reads, and `X-Forwarded-For` was
forwarded because it reads that one — but `verify_xff_header` is on by default and makes it
*reject* every address in the chain that is not local, so ingress answered 403 for anyone
reaching Home Assistant from outside the LAN (#3019, fixed in #3023). Every check ran from
inside the container, where no such header exists. **That an app reads a header is not a reason
to send it — find out what it does with it, and exercise the path a remote user takes.**
The pattern is always *inference standing in for detection*. Before changing a default, ask what
this is like on a host unlike yours. Prefer detecting the condition at runtime over asserting it.

View File

@@ -241,6 +241,16 @@ account. Note it and move on rather than guessing.
**Resolving a review thread requires GraphQL** (`resolveReviewThread`); the REST API cannot do it.
`scripts/pr_review.sh` wraps fetch / reply / resolve.
**CHANGELOG heading dates are ISO, whatever the bots' defaults say.** Match the format already in
the add-on's file. Repo-wide that is `## <version> (YYYY-MM-DD)`: 7705 dated headings against 363
in `DD-MM-YYYY`, and the newest entry is ISO in 125 of 135 add-ons. Copilot flags an ISO file that
gets a `DD-MM-YYYY` entry (#3019). `DD-MM-YYYY` is not invented — it is what `onpush_builder.yaml`
writes with `date '+%d-%m-%Y'` when it has to insert a heading you forgot, and what the
addons_updater bot writes when its `date_iso8601` option is off (`99-run.sh`; it is on in
production here) — but neither is a reason to write it yourself. The builder's duplicate check is
`grep -q "^## ${version} ("`, keyed on the exact `config.yaml` version and blind to the date, so
an ISO heading you wrote yourself still suppresses the bot's insertion.
**The repo's `.markdownlint.yaml` does not disable MD022/MD032**, so a CHANGELOG will show
dozens of pre-existing heading/list findings. They are noise because lint is `continue-on-error`,
not because the config exempts them — don't cite the config as a reason to ignore a finding.

View File

@@ -934,6 +934,7 @@ If you want to do add the repository manually, please follow the procedure highl
![Update](https://img.shields.io/badge/dynamic/json?label=Updated&query=%24.last_update&url=https%3A%2F%2Fraw.githubusercontent.com%2Falexbelgium%2Fhassio-addons%2Fmaster%2Fsabnzbd%2Fupdater.json)
![aarch64][aarch64-badge]
![amd64][amd64-badge]
![ingress][ingress-badge]
![smb][smb-badge]
![localdisks][localdisks-badge]

View File

@@ -1,4 +1,11 @@
## 5.1.1.3 (2026-08-25)
- Fixed ingress returning `403 External internet access denied` when Home Assistant is reached from outside the local network. SABnzbd's `verify_xff_header` option, which is on by default, refuses any request whose `X-Forwarded-For` chain contains a non-local address, so the proxy no longer forwards that header.
## 5.1.1.2 (2026-08-25)
- Ingress is now enabled: the WebUI opens directly in the Home Assistant sidebar, and the "Open Web UI" button now goes there. Access by ip:port is unchanged, but has to be typed rather than clicked, as Home Assistant does not allow an add-on to offer both.
- Note for users who set a "Host verification" whitelist in SABnzbd: ingress sends `Host: 127.0.0.1:8080` upstream, because SABnzbd rejects any Host that is not an IP literal. That whitelist therefore no longer filters the ingress route, which is gated by Home Assistant authentication instead. Direct ip:port access is unchanged and still filtered.
## 5.1.1 (2026-08-22)
- Update to latest version from linuxserver/docker-sabnzbd (changelog : https://github.com/linuxserver/docker-sabnzbd/releases)

View File

@@ -70,7 +70,7 @@ environment:
PGID: "0"
PUID: "0"
image: ghcr.io/alexbelgium/sabnzbd-{arch}
ingress_entry: sabnzbd
ingress: true
init: false
map:
- addon_config:rw
@@ -106,5 +106,4 @@ schema:
slug: sabnzbd
udev: true
url: https://github.com/alexbelgium/hassio-addons
version: "5.1.1"
webui: http://[HOST]:[PORT:8080]
version: "5.1.1.3"

View File

@@ -1,21 +1,14 @@
#!/usr/bin/with-contenv bashio
# shellcheck shell=bash
# shellcheck disable=SC2317
set -e
#################
# NGINX SETTING #
#################
exit 0
ingress_port=$(bashio::addon.ingress_port)
ingress_interface=$(bashio::addon.ip_address)
ingress_entry=$(bashio::addon.ingress_entry)
sed -i "s/%%port%%/${ingress_port}/g" /etc/nginx/servers/ingress.conf
sed -i "s/%%interface%%/${ingress_interface}/g" /etc/nginx/servers/ingress.conf
# Allows serving js
sed -i 's/<!-- %if-not-debug% -->/<!-- %if-not-debug% /g' /app/sabnzbd/webui/index.html
sed -i 's/<!-- %end% -->/ %end% -->/g' /app/sabnzbd/webui/index.html
sed -i 's/<!-- %if-debug%/<!-- %if-debug% -->/g' /app/sabnzbd/webui/index.html
sed -i 's/ %end% -->/<!-- %end% -->/g' /app/sabnzbd/webui/index.html
sed -i "s|%%ingress_entry%%|${ingress_entry}|g" /etc/nginx/servers/ingress.conf

View File

@@ -2,22 +2,41 @@ server {
listen %%interface%%:%%port%% default_server;
include /etc/nginx/includes/server_params.conf;
include /etc/nginx/includes/proxy_params.conf;
client_max_body_size 0;
location / {
add_header Access-Control-Allow-Origin *;
proxy_connect_timeout 30m;
proxy_send_timeout 30m;
proxy_read_timeout 30m;
proxy_pass http://127.0.0.1:8080;
location / {
proxy_pass http://127.0.0.1:8080;
proxy_set_header Accept-Encoding "";
# Correct url without port when using https
sub_filter_once off;
sub_filter_types *;
sub_filter /sabnzbd %%ingress_entry%%/sabnzbd;
}
# SABnzbd refuses any request whose Host is not an IP literal
# ("Access denied - Hostname verification failed"), so send the
# upstream socket rather than the browser's host.
proxy_set_header Host $proxy_host;
# X-Forwarded-For must NOT be forwarded. verify_xff_header defaults to
# on, and check_access() then requires every address in the header to
# be local, so Supervisor's copy of the browser's public address makes
# SABnzbd answer 403 "External internet access denied" for anyone
# reaching Home Assistant from outside the LAN. Clearing it leaves
# SABnzbd looking at nginx's own loopback address. Access is gated by
# Home Assistant authentication before it ever reaches this proxy.
proxy_set_header X-Forwarded-For "";
# The interface itself only emits relative links, so no body
# rewriting is needed. Redirects are the exception: Raiser() emits
# a path under url_base, so prefix them with the ingress entry.
# absolute_redirect must stay off, or nginx expands the rewritten
# Location into http://<browser host>:<ingress port>/..., a port the
# browser cannot reach.
proxy_redirect / %%ingress_entry%%/;
absolute_redirect off;
# The login cookie is hardcoded to Path=/, which on the ingress
# origin would send it to every other add-on's ingress path too.
proxy_cookie_path / %%ingress_entry%%/;
proxy_http_version 1.1;
proxy_read_timeout 86400s;
proxy_send_timeout 86400s;
}
}

View File

@@ -0,0 +1,9 @@
#!/usr/bin/with-contenv bashio
# shellcheck shell=bash
# ==============================================================================
# Stop the container when Nginx fails, so ingress does not silently go dead
# ==============================================================================
if [[ "$1" -ne 0 && "$1" -ne 256 ]]; then
bashio::log.error "Nginx exited with code $1"
kill -15 1
fi

View File

@@ -0,0 +1,11 @@
#!/usr/bin/with-contenv bashio
# shellcheck shell=bash
set -e
# ==============================================================================
# Wait for sabnzbd to become available
bashio::net.wait_for 8080 localhost 900
bashio::log.info "Starting NGinx..."
exec nginx