PROGRESS.md becomes CHANGELOG.md with the finished step lists moved to an appendix. The README is an overview pointing at docs/: configuration, cli, users, sso (refreshed for accounts and admin-only settings), and architecture. CLAUDE.md collects what working on this code actually requires -- pkill -x not -f, the page being compiled in, the dead columns on entries, the Playwright worker that deleted its own database. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AdXho5tTkjFLeUXKbEjKBh
229 lines
9.2 KiB
Markdown
229 lines
9.2 KiB
Markdown
# Signing in through Cloudflare Zero Trust or Authentik
|
|
|
|
ipx can take the signed-in identity from whatever sits in front of it, instead of asking for a
|
|
password itself. Both products below do the same thing in the end: they authenticate the person and
|
|
pass the result to the origin in a **header**. ipx reads that header, finds (or creates) the
|
|
matching account, and gets on with it.
|
|
|
|
Read [How this is secured](#how-this-is-secured) before exposing anything. The short version: a
|
|
header is worth exactly as much as the hop that set it, so ipx only believes one from an address you
|
|
list.
|
|
|
|
---
|
|
|
|
## The ipx side (both setups)
|
|
|
|
```toml
|
|
[web]
|
|
enabled = true
|
|
bind = "0.0.0.0:8099"
|
|
token = "…" # keep it: it is the admin, used by the healthcheck
|
|
|
|
# The header your proxy sets. Empty (the default) disables this whole path.
|
|
trusted_header = "Cf-Access-Authenticated-User-Email" # Authentik: "X-authentik-username"
|
|
|
|
# Addresses allowed to assert that header -- the proxy, and nothing else.
|
|
trusted_proxies = ["127.0.0.1", "::1"]
|
|
|
|
# Create an account the first time the proxy vouches for a name ipx has not seen.
|
|
auto_create_users = true
|
|
|
|
session_days = 30
|
|
```
|
|
|
|
Restart the daemon after editing. Accounts made this way have **no password**: they can only ever
|
|
arrive through the proxy. `ipx user list` marks them `proxy only`.
|
|
|
|
The first account created is an admin. Every later one is an ordinary user, and an ordinary user
|
|
cannot change global settings, a feed's URL or folder, or how often feeds are scanned: the API
|
|
refuses those with a `403`, not just the UI. Everything else about a feed (which items they want,
|
|
whether to fetch them, how many at a time) is theirs alone; see [users.md](users.md).
|
|
|
|
Somebody arriving through the proxy for the first time starts with **no feeds**, because
|
|
subscriptions are per person. Adding a feed someone else already reads costs no second fetch and no
|
|
second copy on disk.
|
|
|
|
Promote someone with:
|
|
|
|
```sh
|
|
ipx user list
|
|
echo -n 'a good password' | ipx user passwd <name> # optional: also lets them sign in directly
|
|
```
|
|
|
|
Local sign-in at `/login` keeps working alongside all of this, which is how you get in from the LAN
|
|
when the tunnel is down. So does the shared `[web] token`, which signs in as the admin: that is
|
|
what the Docker healthcheck uses, and the way back in if you lock yourself out. A brand new database
|
|
starts with **admin / ipodderx** — change it.
|
|
|
|
---
|
|
|
|
## Cloudflare Zero Trust
|
|
|
|
This is what runs `ipodderx.sdf1.net`: a `cloudflared` tunnel to the origin, with an Access
|
|
application in front of it. Cloudflare authenticates the visitor and adds
|
|
`Cf-Access-Authenticated-User-Email` to every request it forwards.
|
|
|
|
### 1. The tunnel
|
|
|
|
In **Zero Trust → Networks → Tunnels**, either use the existing tunnel or create one, then add a
|
|
public hostname:
|
|
|
|
| Field | Value |
|
|
|---|---|
|
|
| Subdomain / domain | `ipodderx` / `sdf1.net` |
|
|
| Type | HTTP |
|
|
| URL | `localhost:8099` (or the LAN address of the box) |
|
|
|
|
Use `localhost` when `cloudflared` runs on the same machine as ipx — that keeps the origin request
|
|
coming from `127.0.0.1`, which is already in `trusted_proxies`. If `cloudflared` runs elsewhere (its
|
|
own container, another host), put **its** address in `trusted_proxies` instead, and make sure
|
|
nothing else can reach port 8099.
|
|
|
|
### 2. The Access application
|
|
|
|
**Zero Trust → Access → Applications → Add an application → Self-hosted**:
|
|
|
|
- Application domain: `ipodderx.sdf1.net`
|
|
- Session duration: whatever suits; ipx keeps its own 30-day session on top.
|
|
- Add a policy — *Allow*, with a rule such as `Emails` → your address, or `Emails ending in` →
|
|
your domain. Anyone this policy admits gets an ipx account when `auto_create_users` is on, so keep
|
|
the policy as narrow as the people you actually want reading your feeds.
|
|
|
|
### 3. Point ipx at the header
|
|
|
|
```toml
|
|
trusted_header = "Cf-Access-Authenticated-User-Email"
|
|
trusted_proxies = ["127.0.0.1", "::1"]
|
|
```
|
|
|
|
The username becomes the email address, lower-cased (`ray@example.com`). That is what shows in the
|
|
sidebar and what `ipx user list` prints.
|
|
|
|
### 4. Check it
|
|
|
|
```sh
|
|
# From the box itself: no header, no session -> the sign-in page.
|
|
curl -s -o /dev/null -w '%{http_code} %{redirect_url}\n' -H 'Accept: text/html' http://127.0.0.1:8099/
|
|
|
|
# Pretending to be the tunnel (only works because 127.0.0.1 is trusted):
|
|
curl -s -H 'Cf-Access-Authenticated-User-Email: you@example.com' http://127.0.0.1:8099/api/me
|
|
```
|
|
|
|
Then load `https://ipodderx.sdf1.net` in a browser: Cloudflare should ask who you are, and ipx
|
|
should show your address in the sidebar footer without ever asking for a password.
|
|
|
|
---
|
|
|
|
## Authentik
|
|
|
|
Authentik does this with a **Proxy Provider** plus an **outpost**, which sits in the request path and
|
|
adds `X-authentik-username` (also `X-authentik-email`, `X-authentik-name`, `X-authentik-groups`).
|
|
|
|
### 1. Provider
|
|
|
|
**Applications → Providers → Create → Proxy Provider**:
|
|
|
|
- Name: `ipx`
|
|
- Authorization flow: your usual (`default-provider-authorization-implicit-consent`)
|
|
- Mode: **Forward auth (single application)** if an existing reverse proxy fronts ipx, or
|
|
**Proxy** to let the outpost talk to ipx directly.
|
|
- External host: `https://ipodderx.example.net`
|
|
- Internal host (Proxy mode): `http://<ip of the ipx box>:8099`
|
|
|
|
### 2. Application and outpost
|
|
|
|
**Applications → Create**, bind it to that provider, and give it a policy so only the people you
|
|
mean are let through. Then add the provider to an outpost (**Applications → Outposts**, the embedded
|
|
one is fine).
|
|
|
|
### 3. Forward auth, if you use nginx/SWAG in front
|
|
|
|
In the server block for ipx:
|
|
|
|
```nginx
|
|
location /outpost.goauthentik.io {
|
|
proxy_pass http://authentik-server:9000/outpost.goauthentik.io;
|
|
proxy_set_header Host $host;
|
|
proxy_set_header X-Original-URL $scheme://$http_host$request_uri;
|
|
add_header Set-Cookie $auth_cookie;
|
|
auth_request_set $auth_cookie $upstream_http_set_cookie;
|
|
}
|
|
|
|
location / {
|
|
auth_request /outpost.goauthentik.io/auth/nginx;
|
|
error_page 401 = @goauthentik_proxy_signin;
|
|
auth_request_set $auth_cookie $upstream_http_set_cookie;
|
|
add_header Set-Cookie $auth_cookie;
|
|
|
|
# This is the line that matters to ipx.
|
|
auth_request_set $authentik_username $upstream_http_x_authentik_username;
|
|
proxy_set_header X-authentik-username $authentik_username;
|
|
|
|
proxy_pass http://ipx:8099;
|
|
}
|
|
```
|
|
|
|
### 4. Point ipx at the header
|
|
|
|
```toml
|
|
trusted_header = "X-authentik-username"
|
|
trusted_proxies = ["172.18.0.5"] # the outpost or nginx container, NOT a whole subnet
|
|
```
|
|
|
|
Usernames arrive as Authentik knows them (`ray`), lower-cased.
|
|
|
|
---
|
|
|
|
## How this is secured
|
|
|
|
**The header is only believed from `trusted_proxies`.** Every other source is ignored, and the
|
|
request falls through to a session cookie or the shared token. This is the whole security boundary,
|
|
so:
|
|
|
|
- List the **proxy's own address**, not a range. `["127.0.0.1"]` when the tunnel runs beside ipx;
|
|
the container's IP when it does not.
|
|
- Never list a LAN subnet. Anyone on your network could then send
|
|
`Cf-Access-Authenticated-User-Email: admin@…` and be your admin.
|
|
- Make sure the origin port is not reachable *around* the proxy by anyone you would not admit
|
|
through it. If it is, bind ipx to `127.0.0.1` and let only the proxy reach it.
|
|
|
|
Verify the refusal, don't assume it — set `trusted_proxies = ["10.9.9.9"]` briefly and confirm a
|
|
header from your machine gets a `401`:
|
|
|
|
```sh
|
|
curl -s -o /dev/null -w '%{http_code}\n' \
|
|
-H 'Cf-Access-Authenticated-User-Email: someone@example.com' http://127.0.0.1:8099/api/me
|
|
```
|
|
|
|
**What ipx does not do:** it does not verify Cloudflare's `Cf-Access-Jwt-Assertion` signature or
|
|
Authentik's session. It trusts the hop. That is a deliberate trade — it keeps the configuration to
|
|
three lines — and it is sound exactly as long as the point above holds.
|
|
|
|
**Turning it off:** clear `trusted_header`. Existing proxy-only accounts stay, but nobody can sign
|
|
in with them until they are given a password (`ipx user passwd <name>`).
|
|
|
|
---
|
|
|
|
## Everyday administration
|
|
|
|
```sh
|
|
ipx user list # who exists, and how each one signs in
|
|
echo -n 'secret123' | ipx user add sam # local account, password on stdin
|
|
ipx user add sam --no-password # proxy-only account, created ahead of time
|
|
echo -n 'newsecret' | ipx user passwd sam # change a password
|
|
ipx user rm sam # remove the account
|
|
```
|
|
|
|
Set `auto_create_users = false` once everyone who should have an account has one. After that the
|
|
proxy vouching for an unknown name is logged and refused, rather than quietly making an account.
|
|
Pre-create people instead with `ipx user add <name> --no-password`, using exactly the name the
|
|
header will carry (Cloudflare sends the email address, lower-cased).
|
|
|
|
Scanning intervals, the disk quota, retention, the download folder and a feed's URL are
|
|
**admin-only**: the Settings button is hidden for everyone else, and the API refuses the change even
|
|
if the request is made by hand. Everyone controls their own keywords, auto-download, explicit
|
|
setting and per-scan cap, along with their own read state and which feeds they see.
|
|
|
|
See also [users.md](users.md) for what several people share, [configuration.md](configuration.md)
|
|
for every `[web]` key, and [cli.md](cli.md) for the `ipx user` commands.
|