The Control Plane Lie: When "Protected" Serves 200
TI-2026-073B ยท Series: The Agent Frontier (Part B) ยท TLP:WHITE ยท 2026-07-10
Assessment (HIGH confidence). An identity manager can insist a service sits behind two-factor authentication while the router never forwards a single request to the authenticator. Three services โ an OpenWebUI LLM interface, a RAGFlow document-AI platform, and a certificate portal โ lived in exactly that state: registered
two_factorin the control plane, correct access-control rules generated, and yet served open HTTP 200 to the internet for months. The most exposed of them drew 140 distinct IPs of active hunting over five weeks, part of the documented 2026 wave against exposed LLM infrastructure. No account was ever compromised โ by margin, not by design โ and all three are now closed. The lesson is the title: the control plane's word is not the data plane's behaviour.
1. The lie
Every dashboard said protected. The identity manager listed all three services with auth_mode = forwardauth and a two_factor policy for their user groups. The generated Authelia access-control list carried a rule for every one of their hostnames. If you asked the system "is this behind 2FA?", every layer answered yes.
The internet answered differently. A plain, unauthenticated GET / returned 200 OK and the application's own front-end โ no redirect to a login, no challenge, nothing. The single component that turns "policy" into "enforcement" โ the one line on the Traefik route that forwards a request to the authenticator โ was missing. Everything described the protection. Nothing performed it.
This is the gap between the control plane (what the system says should happen) and the data plane (what actually happens to a packet). When they disagree and no one is checking, the control plane's confidence becomes a liability: it is precisely the reassurance that stops anyone from looking.
2. What "protected" was supposed to mean
The architecture is forward-authentication: the router intercepts each request, calls an authenticator, and only proxies to the application if a valid session (and second factor) exists. For that to happen, the route must reference the auth middleware. Miss that reference and the router proxies straight to the app โ the authenticator is never consulted, and the app's own login (if any) is the only thing standing.
For an LLM interface that is a thin wall. OpenWebUI, like most self-hosted AI front-ends, ships with open signup enabled by default. So the exposure was not merely "the app is reachable" โ it was "anyone on the internet can create an account on a retrieval-augmented LLM," with whatever documents that RAG had indexed one approval (or zero, depending on default role) away.
3. Root cause: an omission, not a regression
Git history is unambiguous, and it is more uncomfortable than a simple "someone turned auth off." Auth was never wired on these routes.
At the commit that migrated the LLM interface onto its public hostname, the new route was created carrying only a timeout middleware. In the same file, its two sibling routes each referenced the auth middleware. The middleware was defined, available, and used two lines away โ it was simply left off the one route that most needed it. The RAG platform and the certificate portal repeated the pattern independently.
That is the anatomy of the most dangerous class of misconfiguration: it looks protected from every angle except the load-bearing one.
- The auth middleware is defined in the same manifest.
- Sibling routes use it โ skim the file and it reads as an auth-protected stack.
- The manager registers the service as 2FA.
- The generated ACL has the rule.
Four independent signals say secure. Only the route's middlewares: list โ the sole thing that causes forwarding to the authenticator โ disagrees. A reviewer reading for intent sees protection. Only a check that compares the live route to the policy sees the hole. Worse: months later, an exploit-path-blocking middleware was added to one of these routes โ real hardening โ which deepened the impression the route was "being secured" while still never adding auth.
4. The guardrail that watched the wrong thing
There was a consistency check. It did not catch this โ and understanding why is the systemic finding.
The manager reconciles its registry against the cluster: it verifies that a registered service has a live route. It does not verify that the live route carries the auth middleware its policy requires. Those are different invariants, and the difference is the entire vulnerability. A one-pass cross-check of all 115 auth-registered domains against their live route middlewares surfaced the three drifted services in seconds โ an assertion the existing check never made.
A guardrail that verifies the wrong invariant is worse than none: it manufactures confidence. The whole reason three services sat open for months is that a system everyone trusted kept reporting them closed.
5. Who came knocking
The exposed LLM interface was not sitting in obscurity. Over five weeks it logged 4,243 requests from 140 distinct IPs, and the intent breakdown reads like a field guide to how exposed AI apps get hunted:
| Actor (representative) | Network | What they did | Outcome |
|---|---|---|---|
3.144.156.150 | AWS (AS16509) | Swept 100+ .env variants (/.env, /.env.aws, /create_env.py) | 404 (path-blocked); banned |
20.46.179.93 | Microsoft Azure (AS8075) | Hunted PHP webshells (/1.php, /404.php, /NewFile.php) | 301 (blocked); banned |
Advin cluster (207.241.173.236โฆ) | AS22295 | .env / AI-key harvest (documented operator) | 404; folded into known chain |
98.159.234.202 | CDNEXT/Datacamp (AS212238), GB | Loaded the actual login UI in a browser over 3 weeks | reached login; not banned |
The first three are commodity: automated sweeps for credential files and pre-planted shells, absorbed by an exploit-path-blocking middleware and swept into the auto-ban pipeline. The fourth is the one that matters. From a Datacamp anonymising VPN, a client spent three weeks loading the OpenWebUI single-page app and its login interface โ manifest.json, the UI component stylesheets, the real front-end โ engaging the application, not scanning for files. It reached the login surface. It did not register and did not authenticate. That is the shape of reconnaissance that precedes an account-creation or credential attack, run through a VPN, against the one service that should never have answered it.
6. The backdrop: this is a named target class in 2026
None of this is exotic. The exposed-LLM surface is one of the most actively mapped things on the internet this year:
- 175,000 publicly exposed Ollama servers across 130 countries, per a joint SentinelLABS + Censys investigation โ roughly a third in China. Ollama ships with no authentication by default.
- A Kaspersky honeypot impersonating Ollama, LM Studio, AutoGPT, LangServe and text-generation-webui logged 113,000 probe requests in a single month from thousands of IPs.
- Sysdig documented LLMjacking evolved: an actor wiring a misconfigured Ollama server in as the reasoning engine of an automated offensive tool that scans, matches CVEs, writes proof-of-concept exploits, and attempts intrusion. Pillar Security and the Cloud Security Alliance describe a commercial LLMjacking market that scans for unauthenticated LLM endpoints and resells the access.
Against that, 140 IPs on one exposed interface is not bad luck โ it is the expected rate. Exposure of an AI front-end is measured in days to discovery, not months.
7. Did anyone get in?
No โ and the forensic answer matters more than the reassurance. The LLM's user store held seven accounts, every one belonging to the operator or a known associate, the newest created before the probing window even opened. Despite open signup and 140 probers, not one external account was created. Commodity file and shell scanning met the exploit-path filter (404/301) and the aggressive scanners were already banned at the network edge.
But read the margin honestly. The only thing between 140 hunters and a self-registered account on a RAG-backed LLM was that none of them happened to complete a signup before the door was shut. That is luck reinforcing a lesson, not a control doing its job. Which is exactly why the fix was not "watch it more closely" but "close it."
8. Steelmanning the shrug
The counter-narrative: "Nothing happened. The scanners got 404s, no account was created, the app had its own login anyway. This is a non-event."
It fails on three points. (1) The app's own login is not the control you registered โ you registered two_factor forward-auth; open app-signup behind no proxy auth is a strictly weaker posture, and the whole point of the identity plane is not to depend on each app's defaults. (2) The absence of compromise was unmonitored โ nobody was watching the signup endpoint; "no rogue account" is a fact discovered after the window, not a control that would have stopped one. (3) The same drift hit three services and the guardrail never fired โ a pattern that recurs silently is not a non-event; it is an unbounded one until the invariant is fixed. The comfortable reading is the one the control-plane lie is built to produce.
9. Read between the lines
The quiet, generalisable truth here is that security posture is a property of the data plane, not the control plane โ and almost every tool we build to reason about posture reasons about the control plane, because that is what is legible. Registries, policy files, dashboards, "compliance" states: all describe intent. The packet does not read any of them. When you audit, audit the response an anonymous client actually receives, not the configuration that says what it should receive. The 302 that was missing is the only fact that ever mattered.
10. Detection & defence
- Assert the load-bearing invariant, not a proxy for it. For every service registered as forward-auth, verify its live route references an auth middleware. "Has a route" is not "is protected."
- Audit from the outside. Periodically fetch
/as an anonymous client on every public hostname and alert on any200where policy saystwo_factor. A 302-to-auth is the pass condition; a served app is a finding. - Never rely on an app's default auth as the edge control. Treat open-signup defaults (OpenWebUI, and most self-hosted AI front-ends) as world-writable until a proxy gate proves otherwise.
- Make deploy commits legible. Auto-commit pipelines that fold config changes into unrelated deploy snapshots hide exactly this kind of one-line drift from review. Security-relevant route changes deserve their own diff.
- Assume the LLM surface is hunted continuously. 175,000 exposed Ollama servers and a 113,000-probe/month honeypot are the ambient reality. Days to discovery, not months.
11. Sources
- Web-threats host + IP telemetry โ the exposed interface (140 IPs) and the reconnaissance actor
98.159.234.202. LSN web-threats platform, 2026-06-04 โ 2026-07-10. - Git history (K3S manifests) โ auth middleware never wired at the domain migration; sibling routes retained it.
- authelia-manager-v2 registry + generated
acl.ymlโ all three services registeredforwardauth/two_factor; consistency check did not flag missing route middleware. - The Hacker News / SentinelLABS + Censys โ 175,000 publicly exposed Ollama servers (Jan 2026).
- Kaspersky โ LLM honeypot, 113,000 probes in one month (May 2026).
- Sysdig / Cloud Security Alliance / Pillar Security โ LLMjacking evolved and the LLMjacking-as-a-service market (June 2026).
Confidence: HIGH on the drift, the root cause, the consistency-check blind spot, and the remediation (direct evidence: live verification, git history, registry state). HIGH on the external backdrop (multi-source corroboration). MEDIUM on the single reconnaissance actor's intent. Fact is tagged [DOCUMENTED]; hypothesis [INFERRED]. This is a caught-and-closed case study published as a defensive lesson: the exposure is remediated, and no exploitable detail or secret is disclosed.