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

256 lines
8.9 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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:
```bash
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 +short``82.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`:
```diff
- "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:
```bash
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):
```bash
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):
```bash
#!/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
```yaml
- "traefik.http.routers.jellyfin.middlewares=security-headers@file"
```
back to
```yaml
- "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.**
```bash
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 |