ARRFLIX/docs/09-wan-exposure.md
s8n 6bf78d66e1 Restrict non-admin users + 3 imports landed
- EnableUserPreferenceAccess=false on guest + 5 (hides Display, Home,
  Playback, Subtitles pref pages — owner controls UX centrally).
- Wrapper bin/add-jellyfin-user.sh updated to bake this into all future
  non-admin user creations.
- ROADMAP entries (added by sibling import agents):
  - Imported: The Incredible Hulk (2008), TMDB 1724, 4 images
  - Imported: Idiocracy (2006), TMDB 7512 (NOT 1542 = Office Space)
  - Imported: American Dad! (2005) S01-S04, 58 eps, TMDB 1433
- WAN exposure docs added (doc 09, 256 lines): Gandi A record live,
  no-guest middleware dropped, lockout=5 baked in. Owner still must
  port-forward 80/443 on home router for actual public access.
2026-05-08 03:18:58 +01:00

8.9 KiB
Raw Blame History

09 — WAN Exposure (Public Internet)

Status: server-side changes applied 2026-05-08. Owner router-side port-forward still TODO — until the router rules are added, the WAN A record points to a closed port and arrflix.s8n.ru remains LAN-only in practice.

This document covers the move from LAN-only to public-internet access for arrflix.s8n.ru. It records what was changed, what the owner must still do on their home router, the risks they accept by opening the service, and the rollback procedure.


1. What changed server-side (already applied)

1.1 Public DNS A record (Gandi LiveDNS)

A record added under s8n.ru:

Field Value
Name arrflix
Type A
Value 82.31.156.86 (current home WAN IP)
TTL 300

Applied via Gandi LiveDNS API:

curl -X PUT "https://api.gandi.net/v5/livedns/domains/s8n.ru/records/arrflix/A" \
  -H "Authorization: Bearer $GANDIV5_PERSONAL_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"rrset_values":["<wan-ip>"],"rrset_ttl":300}'

Verified with dig @1.1.1.1 arrflix.s8n.ru +short82.31.156.86.

The Pi-hole local override (192.168.0.100 arrflix.s8n.ru) is kept in place so LAN clients bypass NAT hairpin and hit the LAN IP directly — see feedback_s8n_hosts_override.md for why hairpin is problematic. Public users get the WAN IP via 1.1.1.1 / Gandi.

1.2 Traefik middleware change

/opt/docker/jellyfin/docker-compose.yml:

- "traefik.http.routers.jellyfin.middlewares=security-headers@file,no-guest@file"
+ "traefik.http.routers.jellyfin.middlewares=security-headers@file"

The no-guest@file middleware (which restricted source IPs to 127.0.0.0/8 + 192.168.0.0/24 + tailnet + 82.22.5.233) is dropped from the Jellyfin router. security-headers@file is kept.

Container recreated:

ssh user@192.168.0.100 'cd /opt/docker/jellyfin && docker compose up -d'

1.3 Brute-force lockout policy

POST /Users/{id}/Policy applied with LoginAttemptsBeforeLockout=5 on every existing user (s8n admin, guest, 5). After 5 failed login attempts the user is locked out and only an admin can unlock.

The wrapper bin/add-jellyfin-user.sh is updated to apply this automatically to every future user so we never forget it.


2. What the OWNER must still do (router-side)

The home router controls whether traffic from the public internet reaches nullstone. Until the owner adds the rules below, the public DNS record points at a closed port.

2.1 Port-forward rules

Two rules needed on the home router admin UI (typically reachable at http://192.168.0.1 or similar — varies by ISP-supplied router):

External port Protocol Internal IP Internal port Purpose
443 TCP 192.168.0.100 443 Traefik HTTPS (main entrypoint)
80 TCP 192.168.0.100 80 Traefik HTTP (LE redirect to HTTPS)

Generic steps (the labels vary by router brand):

  1. Log into the home router admin UI.
  2. Find the section called Port Forwarding, Virtual Servers, NAT, or Applications & Gaming.
  3. Add a new rule: external port 443 TCP → internal host 192.168.0.100 port 443.
  4. Add a second rule: external port 80 TCP → internal host 192.168.0.100 port 80.
  5. Save / apply.
  6. Reboot the router only if the UI doesn't apply rules live.

2.2 Do NOT forward

  • Port 22 (SSH) — never expose. Tailscale or LAN only.
  • Any port to anything other than 192.168.0.100.

2.3 Verify from outside the LAN

From a phone on mobile data (or any non-home network):

curl -I https://arrflix.s8n.ru/health
# expect: HTTP/2 200

If you get a connection timeout or refused, the port-forward isn't live.


3. Risks the owner must accept

By moving from LAN-only to public-internet:

3.1 Login page is on the open internet

Anyone on the internet can hit the login form. Mitigation: LoginAttemptsBeforeLockout=5 plus strong passwords. There is no fail2ban or rate-limiting at the Traefik layer — the lockout is purely application-level (Jellyfin's own counter). Consider adding a Traefik rate-limit middleware later if abuse shows up in logs.

3.2 Copyrighted media exposure

The library contains commercial movies and TV. Hosting it on a public-resolvable domain raises legal heat (rights-holder C&D, ISP terms-of-service action). This is an information note, not legal advice — only the owner can decide whether to accept it.

3.3 Dynamic WAN IP (Virgin Media)

Home WAN IPs on consumer ISPs change without notice. When the WAN IP changes, the Gandi A record must be refreshed or arrflix.s8n.ru will point at someone else's IP.

A simple cron-driven updater (run every 15 min on nullstone):

#!/usr/bin/env bash
# /opt/scripts/gandi-arrflix-updater.sh
# Refreshes arrflix.s8n.ru A record if WAN IP changed.
set -euo pipefail
TOKEN="$(grep ^GANDIV5_PERSONAL_ACCESS_TOKEN /opt/docker/traefik/.env | cut -d= -f2)"
WAN=$(curl -s --max-time 10 https://ifconfig.me)
[[ "$WAN" =~ ^[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+$ ]] || exit 1
CUR=$(dig @1.1.1.1 +short arrflix.s8n.ru | head -1)
[[ "$WAN" == "$CUR" ]] && exit 0
curl -s -X PUT "https://api.gandi.net/v5/livedns/domains/s8n.ru/records/arrflix/A" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d "{\"rrset_values\":[\"$WAN\"],\"rrset_ttl\":300}" \
  > /var/log/gandi-arrflix.log 2>&1
logger "gandi-arrflix-updater: changed $CUR -> $WAN"

Cron:

*/15 * * * * /opt/scripts/gandi-arrflix-updater.sh

The Gandi token in /opt/docker/traefik/.env is already scoped to LiveDNS-only on s8n.ru, so even if the script leaks it can't change nameservers or add other domains (per reference_gandi_api.md).

3.4 Weak password warning — read this

The new user 5 has password 1234. This is fine for a private LAN-only service shared with one friend. It is dangerous on the public internet:

  • A botnet hits common username/password combos within hours of the domain becoming reachable. 5/1234 is in every basic password list.
  • The lockout fires after 5 attempts but a distributed attacker rotates source IPs faster than that.
  • Even if Jellyfin login holds, weak creds normalise weak creds — the next person added gets 6/1234 and the bar drops further.

Recommendation before the owner opens the router:

  • Reset user 5's password to something strong (12+ random chars).
  • Reset the guest user's password as well (or disable it).
  • Consider whether the s8n admin account password is strong.
  • Owner decides whether to keep the 5/1234 username/pw memorable for the friend at the cost of being an obvious target.

The owner accepted 5/1234 for the Forgejo friend account, but Forgejo on git.s8n.ru holds no copyrighted media. Jellyfin does. Different threat profile, same friend, different decision.


4. Rollback (revert to LAN-only)

If the public exposure goes wrong (abuse, legal pressure, anything):

  1. Close the router ports. Remove the port-forward rules for 443/tcp and 80/tcp on the home router admin UI. This is the fastest single-action shutoff — no internet traffic reaches Traefik.

  2. Re-add the no-guest middleware. Edit /opt/docker/jellyfin/docker-compose.yml, change

    - "traefik.http.routers.jellyfin.middlewares=security-headers@file"
    

    back to

    - "traefik.http.routers.jellyfin.middlewares=security-headers@file,no-guest@file"
    

    then cd /opt/docker/jellyfin && docker compose up -d. Even if a port stays open by mistake, only LAN/tailnet/owner-IP hits succeed.

  3. Remove the Gandi A record.

    curl -X DELETE "https://api.gandi.net/v5/livedns/domains/s8n.ru/records/arrflix/A" \
      -H "Authorization: Bearer $GANDIV5_PERSONAL_ACCESS_TOKEN"
    

    Pi-hole's LAN override remains, so LAN clients keep working.

  4. Disable or delete the cron updater if installed.

  5. Optional: set LoginAttemptsBeforeLockout back to -1 if the lockout is causing friction and there's no public exposure to defend against. (Not required — keeping 5 is harmless.)

After steps 13 the service is back to its pre-exposure posture (LAN + tailnet + owner home IP only).


5. Change log

Date Change By
2026-05-08 Gandi A record arrflix.s8n.ru → 82.31.156.86 (TTL 300) server-side
2026-05-08 Drop no-guest@file middleware on Jellyfin router server-side
2026-05-08 LoginAttemptsBeforeLockout=5 on s8n / guest / 5 server-side
2026-05-08 Wrapper bin/add-jellyfin-user.sh bakes in lockout=5 server-side
TODO Home router port-forward 443+80 → 192.168.0.100 owner
TODO Strong passwords on user 5 and guest owner
TODO (Optional) Gandi WAN-IP cron updater on nullstone owner