The API took a session cookie, a proxy's word or the shared admin token, so a script or an agent working for one person had to sign in with their password and carry the cookie, or be given the admin token. Settings now makes named tokens, ipx_ and 256 random bits, sent as Authorization: Bearer. A token is its owner and no more. Only its SHA-256 is kept, in the new api_tokens table, with when it was made and last used; it is shown once and revoked from the same list. An unknown or revoked one gets a 401 rather than falling through to a cookie. Cloudflare Access still stands in front of the tunnel, so from outside a token needs an Access service token beside it; docs/sso.md says how. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
252 lines
13 KiB
Markdown
252 lines
13 KiB
Markdown
# Signing in through Cloudflare Access and Authentik
|
|
|
|
ipx can take the signed-in identity from whatever sits in front of it, instead of asking for a
|
|
password itself. The proxy authenticates the person and passes the result to ipx in a **header**;
|
|
ipx reads it, 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.
|
|
|
|
---
|
|
|
|
## How ipodderx.sdf1.net does it
|
|
|
|
Checked end to end on 2026-09-12. An earlier version of this page had never been tried against a
|
|
real setup and pointed at the wrong address.
|
|
|
|
```
|
|
browser ─► Cloudflare Access, app "ipodderx" ─── sign in ───► Authentik (OpenID Connect)
|
|
─► tunnel "rays-unraid" (the cloudflared container on Tower)
|
|
─► http://192.168.1.130:8099 ─► ipx
|
|
```
|
|
|
|
Authentik is not in the request path. It is the identity provider Cloudflare Access asks. Access
|
|
then adds `Cf-Access-Authenticated-User-Email`, the email address Authentik gave it, to every
|
|
request it forwards through the tunnel, and ipx signs that person in.
|
|
|
|
| Piece | Where | Setting |
|
|
|---|---|---|
|
|
| Identity provider | Zero Trust → Settings → Authentication | `Authentik`, OpenID Connect; scopes `openid email profile` |
|
|
| Access application | Zero Trust → Access → Applications → `ipodderx` | Domain `ipodderx.sdf1.net`; identity providers: Authentik only, with instant auth; session 730h; policy *Require Login* allows a list of email addresses |
|
|
| Tunnel route | Zero Trust → Networks → Tunnels → `rays-unraid` → Public hostnames | `ipodderx.sdf1.net` → HTTP `192.168.1.130:8099` |
|
|
| DNS | `sdf1.net` | `ipodderx` CNAME to the tunnel, proxied |
|
|
| ipx | `/mnt/fast/appdata/ipx/config.toml`, `[web]` | below |
|
|
|
|
```toml
|
|
[web]
|
|
enabled = true
|
|
bind = "0.0.0.0:8099"
|
|
trusted_header = "Cf-Access-Authenticated-User-Email"
|
|
trusted_proxies = ["127.0.0.1", "::1", "192.168.16.1"]
|
|
access_team = "rays-sdf1.cloudflareaccess.com"
|
|
access_aud = "8bfe73dfbc8c548d1cb5dc11c6db6887bcaf4f5144840396f83a620a140e1c4f"
|
|
auto_create_users = true
|
|
sign_out_url = "/cdn-cgi/access/logout"
|
|
session_days = 30
|
|
```
|
|
|
|
The last two turn on the token check described under [Verifying Cloudflare's token](#verifying-cloudflares-token),
|
|
on since 2026-09-19. Both can be read without the dashboard: a request to the site while signed
|
|
out is sent to `https://<team domain>/cdn-cgi/access/login/ipodderx.sdf1.net?kid=<AUD tag>&...`.
|
|
|
|
Restart ipx after editing it: `docker compose -f /mnt/fast/arcane/projects/content/compose.yaml
|
|
restart ipx`.
|
|
|
|
### What was missing
|
|
|
|
Cloudflare and Authentik were already right. Three things on the ipx side were not:
|
|
|
|
1. **`trusted_header` was empty**, which switches the whole proxy path off. ipx ignored the header
|
|
and asked for a password.
|
|
2. **`trusted_proxies` listed only `127.0.0.1`.** The tunnel's requests do not come from there;
|
|
see the next section.
|
|
3. **The account had the wrong name.** It was made by hand as `rays`, but the header carries
|
|
`rays@sdf1.net`. With `auto_create_users` on, the first visit would have made a second, empty
|
|
account. `ipx user rename rays rays@sdf1.net` fixed that without losing anything.
|
|
|
|
### The address to trust, and why it is 192.168.16.1
|
|
|
|
`cloudflared` runs in its own container and reaches ipx through the host's published port. Docker
|
|
(iptables firewall backend) masquerades traffic between its bridge networks, so the tunnel's
|
|
requests arrive from the **gateway of ipx's own network**, `content_default`:
|
|
|
|
```sh
|
|
docker network inspect content_default -f '{{range .IPAM.Config}}{{.Gateway}}{{end}}'
|
|
```
|
|
|
|
That was measured, not assumed. ipx does not log where a request came from, so the addresses were
|
|
read from the kernel's connection table inside the container while the site was open. (`/proc/net/tcp`
|
|
lists them in hex.)
|
|
|
|
If the `content` project's network is ever recreated, its gateway can change. Check it again, and
|
|
update `trusted_proxies` to match.
|
|
|
|
### Names
|
|
|
|
The username is the email address, lower-cased: `rays@sdf1.net`. To sign in at `/login` with a
|
|
password from the LAN, use that name too.
|
|
|
|
To let someone else in, add their address to the Access policy; they need an Authentik account with
|
|
that email. With `auto_create_users = true` they get an ipx account on their first visit, as an
|
|
ordinary user with no feeds. An account made before the proxy can be given the name the proxy will
|
|
send:
|
|
|
|
```sh
|
|
docker exec iPX ipx user rename <old name> <email address>
|
|
```
|
|
|
|
### Signing out
|
|
|
|
**Sign out** sends someone the proxy signed in to `sign_out_url`, here Cloudflare's
|
|
`/cdn-cgi/access/logout`. That ends your Access session for **every** Access application,
|
|
`code.sdf1.net` included: Cloudflare has no way to end just one, and its sign-out page does not send
|
|
you anywhere afterwards. The next visit goes back through Authentik, which lets you straight in if
|
|
you are still signed in there. Signing out of Authentik itself is Authentik's own sign-out.
|
|
|
|
ipx never shows its password page to someone the proxy vouches for: `/login` sends them on to their
|
|
feeds.
|
|
|
|
### The tile in Authentik's library
|
|
|
|
Authentik's library lists Authentik's own applications, and ipx signs in through the one
|
|
called `Cloudflare Access`, so ipx needs a bookmark of its own to show up there. It is
|
|
Applications → Applications → `iPX` (slug `ipodderx`): no provider, launch URL
|
|
`https://ipodderx.sdf1.net`, and web/logo.svg at 512px as its icon, uploaded again when the logo
|
|
changes. The library keeps each person's list of tiles in a cache that an edit to the application
|
|
does not clear, so after one, clear it (System → Policies → Clear cache, or
|
|
`POST /api/v3/policies/all/cache_clear/`) or the old name and icon stay up. It has no policy
|
|
bindings, so everyone in Authentik sees the tile. Who actually gets in is still up to the Access
|
|
policy.
|
|
|
|
### Check it
|
|
|
|
```sh
|
|
# From Tower itself: not a trusted address, so the header is ignored.
|
|
curl -s -H 'Accept: application/json' -H 'Cf-Access-Authenticated-User-Email: rays@sdf1.net' \
|
|
http://192.168.1.130:8099/api/me # -> sign in
|
|
|
|
# From a container on a Docker bridge, as cloudflared is: believed.
|
|
docker run --rm --network bridge mirror.gcr.io/library/busybox wget -qO- \
|
|
--header 'Accept: application/json' --header 'Cf-Access-Authenticated-User-Email: rays@sdf1.net' \
|
|
http://192.168.1.130:8099/api/me # -> {"admin":true,"name":"rays@sdf1.net"}
|
|
```
|
|
|
|
Then open `https://ipodderx.sdf1.net` in a private window. Authentik should ask who you are, and
|
|
ipx should show `rays@sdf1.net` in the sidebar footer without asking for a password.
|
|
|
|
---
|
|
|
|
## The ipx settings
|
|
|
|
| Key | What it does |
|
|
|---|---|
|
|
| `trusted_header` | The header the proxy sets. Empty, the default, turns the proxy path off. |
|
|
| `trusted_proxies` | The addresses allowed to set it. Nothing else is believed. |
|
|
| `auto_create_users` | Make an account the first time the proxy vouches for a name ipx has not seen. |
|
|
| `sign_out_url` | Where Sign out sends someone the proxy signed in: the proxy's own sign-out. Empty sends them to the sign-in page, where the proxy signs them straight back in. |
|
|
| `session_days` | How long a password sign-in lasts without use. |
|
|
|
|
The first account ever created is an admin. Every later one is an ordinary user, who 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 is theirs alone; see [users.md](users.md).
|
|
|
|
Local sign-in at `/login` keeps working alongside the proxy, 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 and is the
|
|
way back in if you lock yourself out. A brand new database starts with **admin / ipodderx**;
|
|
change it.
|
|
|
|
---
|
|
|
|
## Authentik in the request path instead
|
|
|
|
Not what ipodderx.sdf1.net uses, and **not verified**. Authentik can also sit in front of ipx
|
|
itself, with a **Proxy Provider** and an **outpost** that adds `X-authentik-username`:
|
|
|
|
- Applications → Providers → Create → Proxy Provider; mode **Proxy** (the outpost talks to ipx) or
|
|
**Forward auth** (an existing reverse proxy asks the outpost).
|
|
- Applications → Create, bound to that provider, with a policy; add the provider to an outpost.
|
|
- In ipx: `trusted_header = "X-authentik-username"`, and the outpost's or reverse proxy's address
|
|
in `trusted_proxies`. Measure that address as above rather than guessing it.
|
|
|
|
---
|
|
|
|
## 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. That is the whole security boundary.
|
|
|
|
With the tunnel reaching ipx through the host's port, `192.168.16.1` means **any container on Tower
|
|
that connects to `192.168.1.130:8099`**, not only `cloudflared`. Machines on the LAN, and Tower
|
|
itself, arrive under their own addresses and cannot set the header; the checks above show both
|
|
sides. Never list a LAN address or range: anyone there could then send
|
|
`Cf-Access-Authenticated-User-Email: rays@sdf1.net` and be you.
|
|
|
|
**Unless the token is checked.** With `access_team` and `access_aud` set (next section), the
|
|
header is not enough on its own: the request has to carry the token Cloudflare Access signed, and
|
|
a container on Tower cannot make one.
|
|
|
|
### Verifying Cloudflare's token
|
|
|
|
Access adds `Cf-Access-Jwt-Assertion` to every request it forwards: a JWT naming the person,
|
|
signed with keys only Cloudflare holds. With these two settings ipx checks it on every proxied
|
|
request, and takes the name from its `email` claim.
|
|
|
|
```toml
|
|
[web]
|
|
access_team = "<team>.cloudflareaccess.com" # Zero Trust → Settings: the team domain
|
|
access_aud = "…" # Access → Applications → ipodderx → Overview: Application Audience (AUD) Tag
|
|
```
|
|
|
|
ipx fetches the public keys from `https://<access_team>/cdn-cgi/access/certs` when it starts, and
|
|
again when a token names a key it has not seen (Cloudflare rotates them every six weeks or so), at
|
|
most once a minute. It checks the signature (RS256 only), that the audience is this application's
|
|
tag, the issuer, and the expiry. Anything else is refused, and so is every proxied request while
|
|
the keys cannot be fetched; password and token sign-in still work then.
|
|
|
|
`trusted_header` and `trusted_proxies` still apply: the check is added to them, not put in their
|
|
place.
|
|
|
|
Check it: the busybox request under [Check it](#check-it), which sends the email header without a
|
|
token from the Docker bridge, now gets `sign in`, and the site still signs you in through Authentik.
|
|
|
|
**Turning it off:** clear `trusted_header` and restart. Proxy-made 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, how each signs in, and when
|
|
echo -n 'secret123' | ipx user add sam # local account, password on stdin
|
|
ipx user add sam@example.com --no-password # proxy-only account, made ahead of time
|
|
ipx user rename sam sam@example.com # give an account the name the proxy sends
|
|
echo -n 'newsecret' | ipx user passwd sam # change a password
|
|
ipx user rm sam # remove the account
|
|
```
|
|
|
|
In the container, put `docker exec iPX` in front, and `docker exec -i iPX` for the ones
|
|
that read a password.
|
|
|
|
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. Make people ahead of time instead, with
|
|
the exact name the header will carry.
|
|
|
|
## API tokens for scripts and agents
|
|
|
|
Anyone can make API tokens for their own account in Settings. A token is sent as
|
|
`Authorization: Bearer ipx_...` and acts as the person who made it, admin only if they are. Only
|
|
its SHA-256 is kept; it is shown once, when made, and revoked there too.
|
|
|
|
Through the tunnel, Cloudflare Access turns a request with no Access sign-in towards Authentik
|
|
before ipx sees it, so a token alone does not get in that way. On the LAN, `http://192.168.1.130:8099`
|
|
takes it directly. From outside, make an Access service token, add a Service Auth policy for it to
|
|
the `ipodderx` application, and send `CF-Access-Client-Id` and `CF-Access-Client-Secret` beside the
|
|
`Authorization` header: Access lets the request through, vouches for no name, and ipx takes the
|
|
API token as who is asking. Do not bypass Access for `/api/*` instead; the token would then be the
|
|
only thing between the internet and the API.
|
|
|
|
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.
|