Every post in this series so far has been about getting an identity to an application. Certificates so the connection is trusted, forward auth so a proxy can decide who you are, a tunnel and a policy so the identity provider is reachable and its console is not.
All of that ends at the same place: nginx makes a subrequest, authentik answers, and the request goes upstream carrying a header that says who just authenticated.
Whether the application does anything with that header is entirely up to the application. Tautulli does nothing with it. There is no header authentication mode, no trusted proxy user setting, no plugin. The header arrives and is discarded, and Tautulli goes on showing whatever it shows to whoever asked.
That leaves a choice, and this post is about picking the uncomfortable option.
Which proxy serves what
The above quoted lines are nginx directives, and the previous posts built a Caddyfile. Both are running, and the split between them is geographic.
nginx lives on my application host and predates all of this. It already fronted the services running there, with a vhost per hostname and certificates from the certbot setup. Caddy lives on the authentik host, inside the same Compose stack as authentik, and fronts the services that do not run on the application host, along with the internal TLS endpoints from part three.
Migrating working nginx vhosts to a proxy on a different machine would add a network hop to every request and buy nothing, so I left them where they are. Tautulli runs beside nginx, so nginx serves it.
Two Tautulli settings follow from that arrangement. nginx terminates TLS and speaks plain HTTP to the container, so Tautulli holds no certificate of its own and sees what looks like an unencrypted request:
The second one tells Tautulli to believe the X-Forwarded-Proto and X-Forwarded-Ssl headers nginx sends. Leave it off and Tautulli builds absolute URLs with the wrong scheme, which shows up in notification links and newsletter templates long before it shows up in the interface you use every day.
Caddy has not left the picture. When nginx needs an authentication decision it makes a subrequest to Caddy over verified TLS, and Caddy hands that to authentik’s embedded outpost. The browser talks to nginx, nginx talks to Caddy, Caddy talks to the outpost. Two front doors, one identity provider behind both.
An application that cannot be told who you are
Tautulli is a monitoring and analytics front end for Plex. It polls the Plex API on a schedule, keeps its own database of what was played and by whom, and turns that into watch history, per-user statistics, graphs of library growth, and notifications when a stream starts or the server goes offline. Plex ships some of this. Tautulli does considerably more of it, and turns up in most Plex setups that start to get serious.
Being read-only with respect to Plex makes it sound harmless. Consider what it holds. A complete viewing history for every account on the server, with timestamps, device names and IP addresses. Including how many times my nephew watched the same Bluey episode in one day. That is a more sensitive dataset than most of what I run, and it belongs behind something.
The interface also exposes its own settings pages, and those include the API key in plain text, the Plex server token, and the notification agents wired up to Discord and email. An unauthenticated Tautulli is not a read-only curiosity. It is a viewing history, a set of credentials, and a way to send messages as me.
Tautulli’s own authentication is a username and password stored in its config file, with an option to accept a Plex admin sign-in as well. Both are local. Neither reads a header, and no amount of searching the settings pages will produce one.
A user hitting tautulli.homelabdomain.xyz behind forward auth therefore gets challenged twice. Once by authentik, and then again by Tautulli, which wants its own credentials and has no opinion about what authentik just decided. The second login proves nothing the first did not already prove. It exists because Tautulli has no way to be told that the question was already settled.
Two logins for one identity buys nothing. The second is a password in a manager that nobody rotates, on an account that has no relationship to any directory, protecting an application that a proxy already refused to let strangers reach.
Plenty of self-hosted applications have a setting that says trust the proxy. Tautulli has nothing equivalent, so the obvious pair of options is disable its login and accept that anything reaching the container directly gets in, or keep the login and prompt twice.
Trying to inject the password instead
There is a third shape, and I spent a bit on it. If something in the request path already knows Tautulli’s credentials, it can present them on the user’s behalf, and the user sees one prompt while Tautulli sees the login it insists on. Two components in my path could do that.
authentik looked like the right one. Proxy providers carry a basic_auth_enabled flag with username and password attributes, which reads credentials off the authentik user and presents them upstream. The credential lives in the identity provider, where credentials belong.
It does nothing in forward auth mode. In forward auth, authentik never touches the upstream request. It answers a yes or no on a subrequest and nginx does the proxying, so there is no request for authentik to attach an Authorization header to. I measured that rather than assuming it, with an nginx map and a custom log_format recording only whether the header was present, never its contents.
| |
Absent on every request. The feature is real. It belongs to proxy mode, where the outpost is itself the reverse proxy.
nginx was the other candidate. It is the component doing the proxying, so it can present the credential itself:
| |
Only an authorised request ever reaches the proxy_pass, so authentik is still the gate. This worked. 401 without the header, 303 with it.
Getting there surfaced something about Tautulli that cost me the first attempt. Its Basic auth path wires CherryPy’s checkpassword_dict, and that compares the supplied password in the clear. The PBKDF2 hash it stores, sha256 at 600,000 iterations, gets consulted only by the form login. I put the hash in the header and got a 401 for everybody, including the correct password.
Then I reverted it, because of where the credential ended up. A reverse proxy holding an application password becomes a bigger prize than the traffic passing through it, and secret storage is the identity provider’s job. Solving an authentication problem by moving a password into the proxy is not solving it.
What the tidy version would cost
Doing it properly means proxy mode, and proxy mode means an outpost that can reach the application. The embedded outpost runs on the authentik host. Tautulli sits inside a Docker network on a different machine, so it would need a second proxy outpost deployed next to it and joined to that network:
nginx would hold nothing, which is the property I wanted. The price is a second outpost with its own token and lifecycle, version-locked to the core, and one application whose traffic takes a different path from everything else on that proxy. For a single service, since the others on it cannot use HTTP Basic at all.
Both mechanisms only work against HTTP Basic, and that is the ceiling on the whole idea. Entra’s password-based SSO handles arbitrary login forms. It does so with a browser extension performing client-side autofill, that you would push to your organization. For an application with a form login sitting behind a reverse proxy, there is no clean answer: disable the application’s login, accept two prompts, or hand the problem to a password manager. authentik has an open issue for exactly this gap.
I took the first one, and removed Tautulli’s login entirely to let the proxy do all of the gatekeeping. Blank the username in the config, and Tautulli stops asking:
Mine has been in that state since I got tired of the second prompt. You can confirm it from the host by going straight to the container and skipping nginx altogether:
| |
A redirect to /home and then a 200. No login page, no challenge, no credentials of any kind. Anything that can open a TCP connection to that address gets the full interface, including the settings pages and the API key printed in them.
Which should worry you. It does not worry me, for reasons that have nothing to do with Tautulli.
Why deleting the password was safe here
Removing an application’s own authentication is either a reasonable simplification or a serious hole, and nothing about the application tells you which. The answer lives entirely in the network.
Tautulli publishes no ports in my config. It sits on an internal Docker network at 172.30.0.19, an address I have now typed enough times to have memorised, and nginx reaches it there:
Both come back empty. There is no 8181 on the host, no mapping in the compose file, nothing bound to a LAN address. The only route to that container is nginx, and nginx will not pass a request through without a valid authentik session.
That condition is fragile in a specific way. Add ports: - "8181:8181" to that service, for five minutes of debugging, and you have published an unauthenticated administrative interface to your LAN. The application did not change. Its exposure did. Nothing in Tautulli’s configuration would warn you, because from Tautulli’s point of view nothing happened.
So the rule I would give somebody copying this: you may delete an application’s login only when the proxy is the sole path to it, and you have to keep that true afterwards. If you cannot say with confidence that nothing else can reach the service, keep the local account.
There is a related habit that costs nothing. The check above goes to the container address directly. Testing through the proxy only tells you the proxy works.
Letting the dashboard past the gate
One hole is open deliberately:
| |
No auth_request in that block. Requests to /api/ skip authentik completely.
They have to. My dashboard polls Tautulli for current streams, and a dashboard widget is a script with an API key, not a browser with a session cookie. Put forward auth in front of /api/ and the widget gets a 302 to a login page it cannot complete, and the tile sits empty forever. Took me longer to work that out than I would like to admit.
Every forward auth deployment eventually meets this. Browsers carry cookies and machines carry keys, and one proxy configuration has to serve both. The usual answer is a path carve-out. You decide that some part of the application is defended by something other than your identity provider.
For this path, that something is Tautulli’s own API key. The API requires it and the browser session has nothing to do with it. Asking without one gets you a refusal from Tautulli rather than from nginx:
| |
The carve-out is a prefix, so it covers /api/ and nothing above it: the settings pages, the history, the graphs and the rest of the interface still sit behind forward auth. The source-address allowlist is also declared at the server level, outside any location block, so it applies to the API path too:
An API key on a path reachable only from my LAN and my Tailscale range is a defensible trade. The same carve-out on an internet-facing hostname would be a single leaked key away from an open door, and Tautulli’s API key does not expire, cannot be scoped, and appears in plain text on a settings page that anybody with a session can read.
If you copy the pattern, put the carve-out on the narrowest prefix the client needs and check what else lives under it. /api/ is clean here. Plenty of applications serve their interface and their API from overlapping paths, and those are a different problem.
What this application taught me about the others
Tautulli is the first application in this series that could not participate in its own authentication, and it will not be the last. Sorting the things behind my proxy by what they can do with an identity gives three groups.
A few read a header and log the user in, the arrangement forward auth exists to produce. Others speak OIDC and would prefer a redirect to an identity provider, which is better still and is where I am taking this stack next. The rest do neither, leaving two logins or none.
Picking “none” is a decision about your network more than about the application. It was right here because one proxy is the only route in, and it would be wrong the moment that stopped being true.
Get it
There is no repository artifact for this one. Tautulli’s configuration is a file it manages itself, and the nginx blocks above are excerpts from a vhost specific to my services. Both need your own addresses, your own hostnames, and your own allowlist before they do anything for you.
The authentik and Caddy side that these blocks talk to is in my TechbyJeff repo at Docker/authentik.
Sources
- Tautulli, the project
- Tautulli: frequently asked questions, reverse proxy configuration
- Tautulli: API reference
- authentik: proxy providers and forward auth
- nginx:
ngx_http_auth_request_module
Part five of six on the identity stack in my homelab: authentik, certificates, forward auth, Cloudflare, Tautulli, OpenMediaVault.
