Files
ipodderx-rs/docs/configuration.md
rays c1187a7926 Verify Cloudflare Access's signed token before trusting the proxy
The proxy sign-in believed Cf-Access-Authenticated-User-Email from any
address in trusted_proxies. On Tower that address is the Docker gateway,
so any container there could name itself anyone (docs/sso.md said as
much, and CLAUDE.md listed it as a known gap).

With [web] access_team and access_aud set, a proxied request must also
carry a Cf-Access-Jwt-Assertion that verifies against Cloudflare's keys
(RS256 only, this application's audience, the team's issuer, not
expired), and the name comes from its email claim. The keys are fetched
at start and again when a token names an unseen key, at most once a
minute, so made-up key ids cannot make every request a request to
Cloudflare. While the keys cannot be had, proxied sign-in is refused;
password and token sign-in are unaffected. Both settings empty, nothing
changes.

jsonwebtoken does the checking, on the aws-lc-rs backend already in the
tree through rustls. Tests sign with throwaway keys in tests/data: a
valid token, another app's audience, expired, a forged signature, HS256,
alg none, the refetch limit, and keys that cannot be fetched. Checked
live on a scratch daemon: the header alone and a forged token got 401,
the admin token still signed in.

vouched_name takes the peer and headers rather than the request: a
&Request held across the new await made the auth middleware's future
unsendable, as a body is not Sync.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-19 14:38:25 +00:00

7.8 KiB

Configuration

Two places. config.toml holds what ipx needs before it reaches its database, and what decides who gets in: where things are (download_dir, socket, organize), [torrent] and [web]. The database holds the catalogue of feeds ([feeds.<id>] below) and the server settings the admin page edits (schedule, max_total_gb, max_age_days, max_new_per_check, media_types). Change those in the web UI, or with ipx add, ipx rm and ipx import; they take effect without a restart.

The first time ipx meets a database that holds no catalogue, it takes the feeds and those settings from config.toml, then rewrites config.toml without them, keeping the original beside it as config.toml.pre-database. After that, feeds or those settings written into config.toml are ignored, with a warning in the log saying so. The sections below describe them as they were written in config.toml, which is still how a fresh install begins.

config.toml's default location is $XDG_CONFIG_HOME/ipx/config.toml (~/.config/ipx/config.toml), overridden with --config or $IPX_CONFIG.

What Where Override
Config ~/.config/ipx/config.toml --config, $IPX_CONFIG
Database ~/.local/share/ipx/state.db $IPX_DATA_DIR
Control socket $XDG_RUNTIME_DIR/ipx.sock [general] socket
Downloads [general] download_dir

~ is expanded in paths. The database is SQLite in WAL mode unless IPX_DATABASE_URL names a Postgres database instead. Back SQLite up by copying state.db while the daemon is stopped, or with sqlite3 state.db .backup; back Postgres up with pg_dump. ipx copy-db <state.db> copies a SQLite database into the empty Postgres one IPX_DATABASE_URL names.

[general]

[general]
download_dir      = "~/Podcasts"
socket            = "/run/user/1000/ipx.sock"
schedule          = "every 1h"      # "every 30m", "every 4h", "2d", "90" (minutes)
organize          = "feed"          # "feed" | "date"
max_total_gb      = 50              # 0 = unlimited
max_age_days      = 30              # 0 = keep forever
max_new_per_check = 3               # per feed, per scan. 0 = unlimited
media_types       = ["audio", "video"]

schedule, max_total_gb, max_age_days, max_new_per_check and media_types move into the database as described above; download_dir, socket and organize stay in config.toml.

  • schedule — how often feeds are re-checked. A feed's own <ttl> still wins when it asks to be polled less often, and a per-feed schedule overrides both. Admin-only from the UI.
  • organizefeed files downloads under the feed's folder; date under YYYY-MM-DD.
  • max_total_gb — the reaper deletes to get back under this, oldest first, keeping a 50 MB pad. Kept items are never deleted, and a file only counts as read once every subscriber has read it. 0 disables it entirely.
  • max_age_days — items older than this with no file on disk are pruned from the database. Kept ones stay. 0 disables it.
  • max_new_per_check — the cap that stops a new subscription pulling a whole back catalogue. 0 means unlimited, which is rarely what you want: subscribing to an OPML of 80 feeds with no cap fetched 216 files and 22 GB in one scan.
  • media_types — top-level MIME types taken automatically. Anything else is still listed and can be fetched by hand; blog feeds put each article's header image in an <enclosure>, and without this the disk fills with artwork. Empty takes everything.

[torrent]

[torrent]
enabled        = true
seed_ratio     = 1.0      # stop seeding at this ratio ...
seed_time_mins = 60       # ... or after this long, whichever comes first
port_range     = "6881-6889"
stall_mins     = 30       # give up on a torrent making no progress

A .torrent body is handed to the torrent path whatever MIME type it was advertised as. Torrents run on their own tasks (two at a time) so a slow swarm never blocks a scan.

[web]

[web]
enabled           = true
bind              = "0.0.0.0:8099"        # 127.0.0.1:8080 by default
token             = ""                    # generated and saved on first run
trusted_header    = ""                    # e.g. "Cf-Access-Authenticated-User-Email"
trusted_proxies   = ["127.0.0.1", "::1"]
access_team       = ""                    # e.g. "<team>.cloudflareaccess.com"
access_aud        = ""                    # the Access application's AUD tag
auto_create_users = true
sign_out_url      = ""                    # e.g. "/cdn-cgi/access/logout"
session_days      = 30
  • token — the shared secret, which signs in as the admin. ?token=… sets a cookie, so you paste it once per browser. It is what the Docker healthcheck and any scripts use.
  • trusted_header — a header naming the signed-in user, set by whatever fronts ipx. Empty disables that path. See sso.md.
  • trusted_proxies — addresses allowed to assert that header, and the entire security boundary for it. Name the proxy, never a subnet.
  • access_team, access_aud — with both set, a request through the proxy also has to carry the Cf-Access-Jwt-Assertion Cloudflare Access signed for this application, and the name comes from that token instead of the header. See sso.md.
  • auto_create_users — create an account the first time the proxy vouches for a new name.
  • sign_out_url — where Sign out sends someone the proxy signed in: the proxy's own sign-out, /cdn-cgi/access/logout behind Cloudflare Access. Empty sends them to the sign-in page, where the proxy signs them straight back in.
  • session_days — sign a session out after this long without a request.

It is plain HTTP. On a LAN bind everything crosses the network in the clear — and a feed URL can itself carry a credential. Put TLS in front of it if that matters.

[feeds.<id>]

Kept in the database once ipx has moved them in: a feed's settings are changed in the web UI, and feeds come and go with ipx add, ipx rm and ipx import. The table key is the feed id: stable, human-readable, and used in paths and the API. ipx add derives it from the feed title.

[feeds.atp]
url                = "https://atp.fm/rss"
folder             = "Accidental Tech Podcast"   # default: the feed title
schedule           = "every 6h"                  # overrides [general] for this feed
media_types        = ["audio"]                   # overrides [general] for this feed
category           = "Technology"                # the Directory's, if the feed names none
username           = "ray"                       # HTTP basic auth
password_env       = "IPX_ATP_PASS"              # preferred over a literal `password`

With more than one account, keywords, auto_download, allow_explicit and max_new_per_check live on each person's subscription in the database, not here — the values in config.toml are the fallback for a feed nobody has claimed. The keys above describe the feed itself and are the same for everyone. See users.md.

Feeds derived from a subscribed OPML are not in the catalogue: the OPML is the source of truth and they are re-derived on every scan. Editing one in the UI promotes it to a catalogue entry.

Environment

Variable Effect
IPX_CONFIG Config file path
IPX_DATA_DIR Directory holding state.db
IPX_DATABASE_URL A postgres://user:password@host:port/database URL: use that database instead of state.db
IPX_TEST_DATABASE_URL For cargo test: run the database tests on this Postgres database too, each in a schema of its own
IPX_LOG What reaches stderr (ipx=debug, ipx::scan=debug, …)
IPX_UI_LOG What the in-process log buffer captures for the UI's Log view
http_proxy / https_proxy Honoured for feed and enclosure fetches