[GH-ISSUE #3111] Traefik error handling intercepts backend HTTP 4xx JSON responses, breaking applications that rely on semantic error codes (e.g. Vaultwarden 2FA) #35739

Open
opened 2026-06-18 20:57:58 -05:00 by GiteaMirror · 34 comments
Owner

Originally created by @StefanSa on GitHub (May 19, 2026).
Original GitHub issue: https://github.com/fosrl/pangolin/issues/3111

Describe the Bug

When a backend application behind Pangolin returns an HTTP 400 (or other 4xx) response with a JSON body, Pangolin's Traefik layer intercepts the response and replaces the JSON body with its own error page (HTML). This breaks any application that uses 4xx status codes as part of its normal API flow.

The most prominent example is Vaultwarden (Bitwarden-compatible password manager). Its 2FA login is a two-step process:

  1. Client sends username + password via POST /identity/connect/token
  2. Vaultwarden validates credentials, sees 2FA is enabled, and intentionally returns 400 Bad Request with a JSON body containing the 2FA provider information and challenge data
  3. The client parses this JSON to determine which 2FA methods are available and prompts the user accordingly
  4. The client sends a second request including the 2FA token

When Pangolin/Traefik intercepts step 2 and replaces the JSON body with an HTML error page, the client cannot parse the 2FA challenge and login fails completely.

Actual Behavior

The JSON body of the 400 response is replaced with an HTML error page. The Bitwarden client cannot parse the response and fails with an error. The Vaultwarden server log shows:

[2026-05-19 14:06:42.827][request][INFO] POST /identity/connect/token
[2026-05-19 14:06:42.829][error][ERROR] 2FA token not provided
[2026-05-19 14:06:42.829][response][INFO] (login) POST /identity/connect/token => 400 Bad Request

This can be confirmed by inspecting the browser DevTools (Network tab): the response to the POST /identity/connect/token request contains HTML instead of the expected JSON.

Scope of Impact

This is not specific to Vaultwarden. Any backend application that uses HTTP 4xx status codes with meaningful response bodies as part of its API contract will be affected. Common examples include:

  • OAuth2/OIDC flows that return 400 with JSON error details
  • REST APIs that use 400, 401, 403, 409, 422 with structured error responses
  • WebAuthn/FIDO2 authentication flows

This issue has been independently reported and confirmed by multiple users across the Vaultwarden community, with Pangolin specifically identified as the cause:

Suggested Solution

Ideally, Pangolin should not replace response bodies from healthy backend connections. Error pages should only be served when the backend is unreachable or the connection itself fails — not when the backend intentionally returns a 4xx status code with a valid response body.

If that is not feasible as a default, a per-resource option to disable error page interception (e.g. "Pass through backend error responses") would resolve the issue.

Environment

  • Pangolin Version: v18.4

To Reproduce

  1. Set up Vaultwarden as a resource behind Pangolin
  2. Enable 2FA (TOTP) on a Vaultwarden user account
  3. Attempt to log in via the Vaultwarden web vault

Expected Behavior

The 400 Bad Request response from Vaultwarden should be passed through to the client unmodified, including the JSON body. The client then prompts for the 2FA code and completes the login.

Originally created by @StefanSa on GitHub (May 19, 2026). Original GitHub issue: https://github.com/fosrl/pangolin/issues/3111 ### Describe the Bug When a backend application behind Pangolin returns an HTTP 400 (or other 4xx) response **with a JSON body**, Pangolin's Traefik layer intercepts the response and replaces the JSON body with its own error page (HTML). This breaks any application that uses 4xx status codes as part of its normal API flow. The most prominent example is **Vaultwarden** (Bitwarden-compatible password manager). Its 2FA login is a two-step process: 1. Client sends username + password via `POST /identity/connect/token` 2. Vaultwarden validates credentials, sees 2FA is enabled, and **intentionally** returns `400 Bad Request` with a JSON body containing the 2FA provider information and challenge data 3. The client parses this JSON to determine which 2FA methods are available and prompts the user accordingly 4. The client sends a second request including the 2FA token When Pangolin/Traefik intercepts step 2 and replaces the JSON body with an HTML error page, the client cannot parse the 2FA challenge and login fails completely. ### Actual Behavior The JSON body of the 400 response is replaced with an HTML error page. The Bitwarden client cannot parse the response and fails with an error. The Vaultwarden server log shows: ``` [2026-05-19 14:06:42.827][request][INFO] POST /identity/connect/token [2026-05-19 14:06:42.829][error][ERROR] 2FA token not provided [2026-05-19 14:06:42.829][response][INFO] (login) POST /identity/connect/token => 400 Bad Request ``` This can be confirmed by inspecting the browser DevTools (Network tab): the response to the `POST /identity/connect/token` request contains HTML instead of the expected JSON. ### Scope of Impact This is not specific to Vaultwarden. Any backend application that uses HTTP 4xx status codes with meaningful response bodies as part of its API contract will be affected. Common examples include: - **OAuth2/OIDC flows** that return `400` with JSON error details - **REST APIs** that use `400`, `401`, `403`, `409`, `422` with structured error responses - **WebAuthn/FIDO2** authentication flows ### Related Upstream Reports This issue has been independently reported and confirmed by multiple users across the Vaultwarden community, with Pangolin specifically identified as the cause: - [dani-garcia/vaultwarden#6333](https://github.com/dani-garcia/vaultwarden/discussions/6333) — *"Vaultwarden breaks when Traefik error catcher includes status 400"* — explicitly mentions Pangolin - [dani-garcia/vaultwarden#4672](https://github.com/dani-garcia/vaultwarden/discussions/4672) — *"Nginx Proxy Manager and 2FA"* — same root cause with NPM custom error pages - [dani-garcia/vaultwarden#5088](https://github.com/dani-garcia/vaultwarden/discussions/5088) — *"Unable to login if 2FA enabled"* — confirmed caused by reverse proxy intercepting 400 responses ### Suggested Solution Ideally, Pangolin should **not replace response bodies from healthy backend connections**. Error pages should only be served when the backend is unreachable or the connection itself fails — not when the backend intentionally returns a 4xx status code with a valid response body. If that is not feasible as a default, a per-resource option to disable error page interception (e.g. "Pass through backend error responses") would resolve the issue. ### Environment - Pangolin Version: v18.4 ### To Reproduce 1. Set up Vaultwarden as a resource behind Pangolin 2. Enable 2FA (TOTP) on a Vaultwarden user account 3. Attempt to log in via the Vaultwarden web vault ### Expected Behavior The `400 Bad Request` response from Vaultwarden should be passed through to the client **unmodified**, including the JSON body. The client then prompts for the 2FA code and completes the login.
GiteaMirror added the stale label 2026-06-18 20:57:58 -05:00
Author
Owner

@AstralDestiny commented on GitHub (May 19, 2026):

Are you using pangolin auth or vaultwarden natively?

<!-- gh-comment-id:4490541186 --> @AstralDestiny commented on GitHub (May 19, 2026): Are you using pangolin auth or vaultwarden natively?
Author
Owner

@StefanSa commented on GitHub (May 19, 2026):

@AstralDestiny

Vaultwarden handles authentication natively — I am not using Pangolin’s built-in auth for this resource.

To rule out any interaction, I tested both configurations:

1.	Resource with no Pangolin auth rules (passthrough only)
2.	Resource with Pangolin auth rules enabled

The result is identical in both cases: the 400 Bad Request JSON response body from Vaultwarden’s 2FA challenge is replaced with HTML. The issue occurs at the Traefik error response handling level, independent of whether Pangolin auth is involved or not.

<!-- gh-comment-id:4490791047 --> @StefanSa commented on GitHub (May 19, 2026): @AstralDestiny Vaultwarden handles authentication natively — I am not using Pangolin’s built-in auth for this resource. To rule out any interaction, I tested both configurations: 1. Resource with no Pangolin auth rules (passthrough only) 2. Resource with Pangolin auth rules enabled The result is identical in both cases: the 400 Bad Request JSON response body from Vaultwarden’s 2FA challenge is replaced with HTML. The issue occurs at the Traefik error response handling level, independent of whether Pangolin auth is involved or not.
Author
Owner

@AstralDestiny commented on GitHub (May 19, 2026):

If it's traefik then it would have to be posted within the traefik issue github honestly.

Though I will spin up a test vaultwarden in a moment give me a bit.

<!-- gh-comment-id:4490875004 --> @AstralDestiny commented on GitHub (May 19, 2026): If it's traefik then it would have to be posted within the traefik issue github honestly. Though I will spin up a test vaultwarden in a moment give me a bit.
Author
Owner

@AstralDestiny commented on GitHub (May 19, 2026):

Which method? TOTP or security key?

<!-- gh-comment-id:4490953746 --> @AstralDestiny commented on GitHub (May 19, 2026): Which method? TOTP or security key?
Author
Owner

@StefanSa commented on GitHub (May 19, 2026):

security key

<!-- gh-comment-id:4490959567 --> @StefanSa commented on GitHub (May 19, 2026): security key
Author
Owner

@AstralDestiny commented on GitHub (May 19, 2026):

There was a problem reading the security key. Try again.

Fun.. give me a few. (Forgot to set basedomain within vw)

<!-- gh-comment-id:4490977776 --> @AstralDestiny commented on GitHub (May 19, 2026): There was a problem reading the security key. Try again. Fun.. give me a few. (Forgot to set basedomain within vw)
Author
Owner

@AstralDestiny commented on GitHub (May 19, 2026):

I'm able to login just fine with a pass key and a physical security key.

Are you differing at all from base deployment?

<!-- gh-comment-id:4491075334 --> @AstralDestiny commented on GitHub (May 19, 2026): I'm able to login just fine with a pass key and a physical security key. Are you differing at all from base deployment?
Author
Owner

@StefanSa commented on GitHub (May 19, 2026):

That's interesting. What are you doing differently from me and from the other cases i mentioned?

<!-- gh-comment-id:4491120733 --> @StefanSa commented on GitHub (May 19, 2026): That's interesting. What are you doing differently from me and from the other cases i mentioned?
Author
Owner

@AstralDestiny commented on GitHub (May 19, 2026):

Mine differs a bit but it was posted to https://discord.com/channels/1325658630518865980/1438910182372540536/1438910182372540536

Though I hope to get some time soon to publish the page on pangolin.net docs, But I doubt any of that should cause any breakage.

<!-- gh-comment-id:4491157321 --> @AstralDestiny commented on GitHub (May 19, 2026): Mine differs a bit but it was posted to https://discord.com/channels/1325658630518865980/1438910182372540536/1438910182372540536 Though I hope to get some time soon to publish the page on pangolin.net docs, But I doubt any of that should cause any breakage.
Author
Owner

@StefanSa commented on GitHub (May 19, 2026):

I actually tested WebAuthn with a physical security key as well. I was able to register the key via the Web UI without issues, but login with WebAuthn also failed due to the same 400 response body being replaced. Login was only possible again after disabling 2FA entirely.

So this affects all 2FA methods (TOTP and WebAuthn) — which makes sense, since both rely on the same POST /identity/connect/token → 400 Bad Request with JSON body flow for the initial challenge-response step.

This is a base Pangolin deployment with no custom Traefik middleware or error page configuration. Could you share your Pangolin version and check in DevTools (Network tab) whether your 400 response body on POST /identity/connect/token returns JSON or HTML?

<!-- gh-comment-id:4491168422 --> @StefanSa commented on GitHub (May 19, 2026): I actually tested WebAuthn with a physical security key as well. I was able to register the key via the Web UI without issues, but login with WebAuthn also failed due to the same 400 response body being replaced. Login was only possible again after disabling 2FA entirely. So this affects all 2FA methods (TOTP and WebAuthn) — which makes sense, since both rely on the same POST /identity/connect/token → 400 Bad Request with JSON body flow for the initial challenge-response step. This is a base Pangolin deployment with no custom Traefik middleware or error page configuration. Could you share your Pangolin version and check in DevTools (Network tab) whether your 400 response body on POST /identity/connect/token returns JSON or HTML?
Author
Owner

@AstralDestiny commented on GitHub (May 19, 2026):

I'm on EE-1.18.4, No error pages here as I think it's a waste of resources and has traefik respond to stuff it should otherwise not bother with.

My headers are changed drastically to align with security that most modern sites don't even bother with.

Request URL https://vw.domain4.com/identity/connect/token
Request Method POST
Status Code 400 Bad Request
Remote Address XX.XX.XX.XX:443
Referrer Policy same-origin
JSON format

Request URL https://vw.domain4.com/identity/connect/token
Request Method POST
Status Code 200 OK
Remote Address XX.XX.XX.XX:443
Referrer Policy same-origin
JSON format

Tried posting portion of the json it was just broken when previewed within github. But looks proper within webtools.

<!-- gh-comment-id:4491232153 --> @AstralDestiny commented on GitHub (May 19, 2026): I'm on EE-1.18.4, No error pages here as I think it's a waste of resources and has traefik respond to stuff it should otherwise not bother with. My headers are changed drastically to align with security that most modern sites don't even bother with. Request URL https://vw.domain4.com/identity/connect/token Request Method POST Status Code 400 Bad Request Remote Address XX.XX.XX.XX:443 Referrer Policy same-origin JSON format Request URL https://vw.domain4.com/identity/connect/token Request Method POST Status Code 200 OK Remote Address XX.XX.XX.XX:443 Referrer Policy same-origin JSON format Tried posting portion of the json it was just broken when previewed within github. But looks proper within webtools.
Author
Owner

@AstralDestiny commented on GitHub (May 19, 2026):

If you want if you can poke me on the discord or slack, see if it's a web browser issue or somewhere like I'll have you try against an instance here.

What's your traefik version by chance?

<!-- gh-comment-id:4491266010 --> @AstralDestiny commented on GitHub (May 19, 2026): If you want if you can poke me on the discord or slack, see if it's a web browser issue or somewhere like I'll have you try against an instance here. What's your traefik version by chance?
Author
Owner

@StefanSa commented on GitHub (May 19, 2026):

Thanks, that’s very helpful. So the key difference is clear: no error pages on your side → JSON passes through correctly. On my side → JSON gets replaced with HTML.

I did not explicitly configure any custom error pages either, so this might be a default middleware in the community edition base deployment. I’ll dig into my Traefik dynamic config and check for any errors middleware that might be intercepting 4xx responses by default.

By the way — you mentioned you’ve drastically changed your headers and optimized your Traefik config for security. If you’re open to sharing the relevant parts, that would be a great reference point for comparison. No pressure of course, but it would definitely help narrow this down.

<!-- gh-comment-id:4491333506 --> @StefanSa commented on GitHub (May 19, 2026): Thanks, that’s very helpful. So the key difference is clear: no error pages on your side → JSON passes through correctly. On my side → JSON gets replaced with HTML. I did not explicitly configure any custom error pages either, so this might be a default middleware in the community edition base deployment. I’ll dig into my Traefik dynamic config and check for any errors middleware that might be intercepting 4xx responses by default. By the way — you mentioned you’ve drastically changed your headers and optimized your Traefik config for security. If you’re open to sharing the relevant parts, that would be a great reference point for comparison. No pressure of course, but it would definitely help narrow this down.
Author
Owner

@AstralDestiny commented on GitHub (May 19, 2026):

Oh all of it is in that discord url I posted it's on the official discord. Well minus some of the dnssec and such stuff. and the read only and the apparmor/selinux profiles.

https://discord.gg/MZtgvEfNCc If it helps, I plan to add the stuff to the docs just been overly busy to sit down finish it.

<!-- gh-comment-id:4491422917 --> @AstralDestiny commented on GitHub (May 19, 2026): Oh all of it is in that discord url I posted it's on the official discord. Well minus some of the dnssec and such stuff. and the read only and the apparmor/selinux profiles. https://discord.gg/MZtgvEfNCc If it helps, I plan to add the stuff to the docs just been overly busy to sit down finish it.
Author
Owner

@StefanSa commented on GitHub (May 20, 2026):


Update — Correction after further testing:

After more thorough testing I need to narrow down the scope of this issue significantly.

Web UI works correctly: Login with 2FA (WebAuthn via Token2 security key) through the Vaultwarden Web Vault works without issues. The 400 Bad Request JSON response containing the 2FA challenge is passed through correctly by Pangolin/Traefik/badger, and the 2FA flow completes as expected.

The issue is limited to the native Bitwarden app. The app fails to complete the 2FA login flow against Vaultwarden behind Pangolin. The Vaultwarden server log still shows:

POST /identity/connect/token => 400 Bad Request
ERROR 2FA token not provided

This suggests the native app either handles the 2FA challenge-response flow differently than the Web UI, or sends different headers that cause a different interaction with the middleware chain.

The Traefik access log confirms that the backend response has downstream_Content-Type: application/json with a DownstreamContentSize of 351 bytes, which appears to be the correct JSON response from Vaultwarden. The question is whether the native app receives and parses this response correctly, or whether something in the middleware chain (badger) treats the native app's requests differently based on headers or client identification.

I apologize for the initial broad scope of this report. The core issue remains — 2FA login fails for native Bitwarden clients behind Pangolin — but it is not a general error page interception problem as originally described.


<!-- gh-comment-id:4495082411 --> @StefanSa commented on GitHub (May 20, 2026): --- **Update — Correction after further testing:** After more thorough testing I need to narrow down the scope of this issue significantly. **Web UI works correctly:** Login with 2FA (WebAuthn via Token2 security key) through the Vaultwarden Web Vault works without issues. The `400 Bad Request` JSON response containing the 2FA challenge is passed through correctly by Pangolin/Traefik/badger, and the 2FA flow completes as expected. **The issue is limited to the native Bitwarden app.** The app fails to complete the 2FA login flow against Vaultwarden behind Pangolin. The Vaultwarden server log still shows: ``` POST /identity/connect/token => 400 Bad Request ERROR 2FA token not provided ``` This suggests the native app either handles the 2FA challenge-response flow differently than the Web UI, or sends different headers that cause a different interaction with the middleware chain. The Traefik access log confirms that the backend response has `downstream_Content-Type: application/json` with a `DownstreamContentSize` of 351 bytes, which appears to be the correct JSON response from Vaultwarden. The question is whether the native app receives and parses this response correctly, or whether something in the middleware chain (badger) treats the native app's requests differently based on headers or client identification. I apologize for the initial broad scope of this report. The core issue remains — 2FA login fails for native Bitwarden clients behind Pangolin — but it is not a general error page interception problem as originally described. ---
Author
Owner

@AstralDestiny commented on GitHub (May 20, 2026):

Mm mind testing against one of my instances for that by chance?

<!-- gh-comment-id:4495110117 --> @AstralDestiny commented on GitHub (May 20, 2026): Mm mind testing against one of my instances for that by chance?
Author
Owner

@StefanSa commented on GitHub (May 20, 2026):

Sure, happy to test. Quick question first — are you also running the Bitwarden Windows Desktop App successfully with 2FA enabled against your Vaultwarden instance? That would help me understand if it's specific to my setup or a general Desktop App issue.

<!-- gh-comment-id:4495188872 --> @StefanSa commented on GitHub (May 20, 2026): Sure, happy to test. Quick question first — are you also running the Bitwarden Windows Desktop App successfully with 2FA enabled against your Vaultwarden instance? That would help me understand if it's specific to my setup or a general Desktop App issue.
Author
Owner

@AstralDestiny commented on GitHub (May 20, 2026):

I don't have vaultwarden deployed past the web side part of things. Seems I'm getting rate limited by github again.

<!-- gh-comment-id:4495223358 --> @AstralDestiny commented on GitHub (May 20, 2026): I don't have vaultwarden deployed past the web side part of things. Seems I'm getting rate limited by github again.
Author
Owner

@AstralDestiny commented on GitHub (May 20, 2026):

Mostly asking for you to test as the desktop app just seems to not be happy at all with me.

<!-- gh-comment-id:4495285790 --> @AstralDestiny commented on GitHub (May 20, 2026): Mostly asking for you to test as the desktop app just seems to not be happy at all with me.
Author
Owner

@LaurenceJJones commented on GitHub (May 20, 2026):

Are you using the cloud or self hosted?

If it's the cloud, we setup error middleware but we tried to minimize the scope as much as possible so it didn't intercept application responses.

If you are self hosted then you need to check if you setup the error pages middleware and if so you need to tune them as Pangolin has no control over that.

<!-- gh-comment-id:4495371815 --> @LaurenceJJones commented on GitHub (May 20, 2026): Are you using the cloud or self hosted? If it's the cloud, we setup error middleware but we tried to minimize the scope as much as possible so it didn't intercept application responses. If you are self hosted then you need to check if you setup the error pages middleware and if so you need to tune them as Pangolin has no control over that.
Author
Owner

@StefanSa commented on GitHub (May 20, 2026):

@LaurenceJJones @AstralDestiny

Self-hosted. And to clarify — after further testing, the issue turned out to be not about error pages intercepting 400 responses. The Web UI with 2FA (WebAuthn) works correctly through Pangolin. The problem is specifically with the Bitwarden Windows Desktop App failing to complete the 2FA flow.

The Vaultwarden admin diagnostic page reveals the actual cause — response header conflicts:

2FA Connector calls:
  Header: 'referrer-policy' does not contain 'same-origin'
  Header: 'x-xss-protection' does not contain '0'
  Header: 'x-frame-options' is present while it should not

These headers are injected by a Traefik security-headers middleware on the entrypoint level. The Web UI tolerates the mismatch, but the Desktop App (Electron) does not.

The challenge with Pangolin is that there's currently no clean way to solve this per-resource:

  1. Security-headers on the entrypoint apply globally to all resources — can't exclude Vaultwarden without removing them for everything
  2. Pangolin's UI only offers Custom Request Headers per resource, not Response Headers
  3. Creating a manual Traefik router in the dynamic config to override headers for Vaultwarden would bypass badger, losing Pangolin's auth layer
  4. The HTTP-provider generated config doesn't include any per-resource header overrides

A per-resource "Custom Response Headers" option in Pangolin (similar to the existing Request Headers field) would solve this cleanly. Would that be feasible?


<!-- gh-comment-id:4495749946 --> @StefanSa commented on GitHub (May 20, 2026): @LaurenceJJones @AstralDestiny --- Self-hosted. And to clarify — after further testing, the issue turned out to be **not** about error pages intercepting `400` responses. The Web UI with 2FA (WebAuthn) works correctly through Pangolin. The problem is specifically with the **Bitwarden Windows Desktop App** failing to complete the 2FA flow. The Vaultwarden admin diagnostic page reveals the actual cause — **response header conflicts**: ``` 2FA Connector calls: Header: 'referrer-policy' does not contain 'same-origin' Header: 'x-xss-protection' does not contain '0' Header: 'x-frame-options' is present while it should not ``` These headers are injected by a Traefik security-headers middleware on the entrypoint level. The Web UI tolerates the mismatch, but the Desktop App (Electron) does not. The challenge with Pangolin is that there's currently no clean way to solve this per-resource: 1. **Security-headers on the entrypoint** apply globally to all resources — can't exclude Vaultwarden without removing them for everything 2. **Pangolin's UI** only offers Custom Request Headers per resource, not Response Headers 3. **Creating a manual Traefik router** in the dynamic config to override headers for Vaultwarden would bypass badger, losing Pangolin's auth layer 4. **The HTTP-provider generated config** doesn't include any per-resource header overrides A per-resource "Custom Response Headers" option in Pangolin (similar to the existing Request Headers field) would solve this cleanly. Would that be feasible? ---
Author
Owner

@AstralDestiny commented on GitHub (May 20, 2026):

Did you attempt with my header changes found on the discord?

Also no the "Custom response" only applies between traefik and vaultwarden and not vaultwarden and user which those headers should be existing on though don't recall pangolin pushing headers.. be curious to know what the github is pushing right now as I've attempted to modify it a few times in the past but keeps being overwritten later.

<!-- gh-comment-id:4495809762 --> @AstralDestiny commented on GitHub (May 20, 2026): Did you attempt with my header changes found on the discord? Also no the "Custom response" only applies between traefik and vaultwarden and not vaultwarden and user which those headers should be existing on though don't recall pangolin pushing headers.. be curious to know what the github is pushing right now as I've attempted to modify it a few times in the past but keeps being overwritten later.
Author
Owner

@StefanSa commented on GitHub (May 20, 2026):

Here's what Pangolin currently generates via the HTTP provider (http://pangolin:3001/api/v1/traefik-config) for the Vaultwarden route:

json"15-vaultwarden-router": {
  "entryPoints": ["websecure"],
  "middlewares": ["badger"],
  "service": "15-vaultwarden-service",
  "rule": "Host(`vw.saxxxxx.org`)",
  "priority": 100,
  "tls": {"certResolver": "letsencrypt"}
}

So Pangolin itself only injects badger — no header middleware, no error pages. The problematic headers in my case were coming from my own security-headers middleware applied at the Traefik entrypoint level. After aligning those values with your config from Discord (referrerPolicy: "same-origin", browserXssFilter: false, customBrowserXSSValue: "0"), the API diagnostic errors are resolved.
Still testing whether this also fixes the Desktop App 2FA issue — will report back.

<!-- gh-comment-id:4495875333 --> @StefanSa commented on GitHub (May 20, 2026): Here's what Pangolin currently generates via the HTTP provider (http://pangolin:3001/api/v1/traefik-config) for the Vaultwarden route: ``` json"15-vaultwarden-router": { "entryPoints": ["websecure"], "middlewares": ["badger"], "service": "15-vaultwarden-service", "rule": "Host(`vw.saxxxxx.org`)", "priority": 100, "tls": {"certResolver": "letsencrypt"} } ``` So Pangolin itself only injects badger — no header middleware, no error pages. The problematic headers in my case were coming from my own security-headers middleware applied at the Traefik entrypoint level. After aligning those values with your config from Discord `(referrerPolicy: "same-origin"`, `browserXssFilter: false`, `customBrowserXSSValue: "0")`, the API diagnostic errors are resolved. Still testing whether this also fixes the Desktop App 2FA issue — will report back.
Author
Owner

@AstralDestiny commented on GitHub (May 20, 2026):

So happy? or?

<!-- gh-comment-id:4496927507 --> @AstralDestiny commented on GitHub (May 20, 2026): So happy? or?
Author
Owner

@StefanSa commented on GitHub (May 20, 2026):


Resolved — Root cause identified and fix documented.

After extensive debugging, this turned out to be a combination of issues rather than a single error page interception problem. Here's the full breakdown for anyone running Vaultwarden behind Pangolin:

The Problem

The Bitwarden Windows Desktop App (Electron) could not complete 2FA (WebAuthn) login against Vaultwarden behind Pangolin, while the Web UI worked fine.

Root Causes

1. X-Frame-Options: SAMEORIGIN blocking the WebAuthn connector
The Desktop App loads webauthn-connector.html from Vaultwarden in an iframe. The parent page is a local file:// URL inside the Electron app. A Traefik frameDeny: true middleware on the entrypoint added X-Frame-Options: SAMEORIGIN, which blocked the iframe because file://https://. This caused ERR_BLOCKED_BY_RESPONSE.

2. Vaultwarden CSP frame-ancestors missing file://*
Even after removing X-Frame-Options, the Electron iframe was blocked because Vaultwarden's own CSP frame-ancestors did not include file://*.

3. Security headers overriding Vaultwarden's expected headers
Global Traefik security headers (referrer-policy, x-xss-protection, content-security-policy) conflicted with what Vaultwarden expects, causing diagnostic failures.

4. Missing Pangolin rule for /connectors/*
The WebAuthn connector path needed an explicit allow rule in Pangolin.

Solution

Vaultwarden environment variable:

ALLOWED_IFRAME_ANCESTORS=file://*

Pangolin:

  • Add a rule allowing /connectors/*
Image

Traefik security-headers middleware (entrypoint level):

security-headers:
  headers:
    frameDeny: false                    # Do NOT set X-Frame-Options
    browserXssFilter: false
    customBrowserXSSValue: "0"
    referrerPolicy: "same-origin"
    # Do NOT set contentSecurityPolicy — let Vaultwarden set its own

What was NOT the cause

  • Pangolin's badger middleware does not intercept or modify backend response bodies. The 400 JSON response from Vaultwarden's 2FA challenge was passed through correctly.
  • Error page interception was not occurring in this setup.
  • CrowdSec bouncer was not involved.

Recommendation

A per-resource Custom Response Headers option in Pangolin would make this much easier to solve. Currently, disabling frameDeny or adjusting headers requires changing the global Traefik entrypoint config, which affects all resources. Being able to override response headers per resource (similar to the existing Custom Request Headers field) would allow targeted fixes without weakening security for other services.

Thanks to @AstralDestiny for helping narrow this down — the comparison with his hardened Traefik config was key to identifying the header conflicts.


<!-- gh-comment-id:4497528901 --> @StefanSa commented on GitHub (May 20, 2026): --- **Resolved — Root cause identified and fix documented.** After extensive debugging, this turned out to be a combination of issues rather than a single error page interception problem. Here's the full breakdown for anyone running Vaultwarden behind Pangolin: ### The Problem The Bitwarden **Windows Desktop App** (Electron) could not complete 2FA (WebAuthn) login against Vaultwarden behind Pangolin, while the **Web UI** worked fine. ### Root Causes **1. `X-Frame-Options: SAMEORIGIN` blocking the WebAuthn connector** The Desktop App loads `webauthn-connector.html` from Vaultwarden in an iframe. The parent page is a local `file://` URL inside the Electron app. A Traefik `frameDeny: true` middleware on the entrypoint added `X-Frame-Options: SAMEORIGIN`, which blocked the iframe because `file://` ≠ `https://`. This caused `ERR_BLOCKED_BY_RESPONSE`. **2. Vaultwarden CSP `frame-ancestors` missing `file://*`** Even after removing `X-Frame-Options`, the Electron iframe was blocked because Vaultwarden's own CSP `frame-ancestors` did not include `file://*`. **3. Security headers overriding Vaultwarden's expected headers** Global Traefik security headers (`referrer-policy`, `x-xss-protection`, `content-security-policy`) conflicted with what Vaultwarden expects, causing diagnostic failures. **4. Missing Pangolin rule for `/connectors/*`** The WebAuthn connector path needed an explicit allow rule in Pangolin. ### Solution **Vaultwarden environment variable:** ``` ALLOWED_IFRAME_ANCESTORS=file://* ``` **Pangolin:** - Add a rule allowing `/connectors/*` <img width="1508" height="726" alt="Image" src="https://github.com/user-attachments/assets/dc5396a8-a2af-487c-8d47-670dc0b21262" /> **Traefik security-headers middleware (entrypoint level):** ```yaml security-headers: headers: frameDeny: false # Do NOT set X-Frame-Options browserXssFilter: false customBrowserXSSValue: "0" referrerPolicy: "same-origin" # Do NOT set contentSecurityPolicy — let Vaultwarden set its own ``` ### What was NOT the cause - Pangolin's badger middleware does **not** intercept or modify backend response bodies. The 400 JSON response from Vaultwarden's 2FA challenge was passed through correctly. - Error page interception was not occurring in this setup. - CrowdSec bouncer was not involved. ### Recommendation A per-resource **Custom Response Headers** option in Pangolin would make this much easier to solve. Currently, disabling `frameDeny` or adjusting headers requires changing the global Traefik entrypoint config, which affects all resources. Being able to override response headers per resource (similar to the existing Custom Request Headers field) would allow targeted fixes without weakening security for other services. Thanks to @AstralDestiny for helping narrow this down — the comparison with his hardened Traefik config was key to identifying the header conflicts. ---
Author
Owner

@AstralDestiny commented on GitHub (May 20, 2026):

I don't go for legacy support, Also the CSP is fine, If an app provides it traefik will opt for vaultwarden's csp even if it's different from traefik.

Also would just use customFrameOptionsValue: SAMEORIGIN at that point.
Also if it's using file: for a reference that shouldn't break it.. I'll deep dive this latter, If it's really using that then it's violating some spec likely.

Also part of this reads like aI.

Also adding a rule doesn't do anything unless you have pangolin fronting the auth.

<!-- gh-comment-id:4497603029 --> @AstralDestiny commented on GitHub (May 20, 2026): I don't go for legacy support, Also the CSP is fine, If an app provides it traefik will opt for vaultwarden's csp even if it's different from traefik. Also would just use customFrameOptionsValue: SAMEORIGIN at that point. Also if it's using file: for a reference that shouldn't break it.. I'll deep dive this latter, If it's really using that then it's violating some spec likely. Also part of this reads like aI. Also adding a rule doesn't do anything unless you have pangolin fronting the auth.
Author
Owner

@StefanSa commented on GitHub (May 20, 2026):


Closing this — figured out what was going on.

The actual issue had nothing to do with error page interception. The 400 JSON responses from Vaultwarden were being passed through correctly by Pangolin/badger the whole time.

What actually broke the Desktop App (Electron) was a combination of things:

My Traefik security-headers middleware had frameDeny: true on the entrypoint, which added X-Frame-Options: SAMEORIGIN globally. The Desktop App loads webauthn-connector.html in an iframe, but the parent is a local file:// path inside the Electron app — so SAMEORIGIN blocks it because file://https://. That gave me ERR_BLOCKED_BY_RESPONSE in the Electron console.

After fixing that, the iframe loaded but the WebAuthn ceremony still didn't work. Turned out I also needed ALLOWED_IFRAME_ANCESTORS=file://* in my Vaultwarden config so the CSP frame-ancestors includes file:// for the Electron context. Plus a Pangolin rule for /connectors/*.

With all three changes the Desktop App works with WebAuthn 2FA now. Web UI was never affected since it doesn't rely on cross-origin iframes.

For anyone else running Vaultwarden behind Pangolin with the Desktop App and WebAuthn — don't set X-Frame-Options via your reverse proxy, let Vaultwarden handle framing via its own CSP, and set ALLOWED_IFRAME_ANCESTORS=file://*.

Thanks @AstralDestiny for the help debugging and the Traefik config comparison — that pointed me in the right direction with the header conflicts.


<!-- gh-comment-id:4497686120 --> @StefanSa commented on GitHub (May 20, 2026): --- Closing this — figured out what was going on. The actual issue had nothing to do with error page interception. The 400 JSON responses from Vaultwarden were being passed through correctly by Pangolin/badger the whole time. What actually broke the Desktop App (Electron) was a combination of things: My Traefik security-headers middleware had `frameDeny: true` on the entrypoint, which added `X-Frame-Options: SAMEORIGIN` globally. The Desktop App loads `webauthn-connector.html` in an iframe, but the parent is a local `file://` path inside the Electron app — so SAMEORIGIN blocks it because `file://` ≠ `https://`. That gave me `ERR_BLOCKED_BY_RESPONSE` in the Electron console. After fixing that, the iframe loaded but the WebAuthn ceremony still didn't work. Turned out I also needed `ALLOWED_IFRAME_ANCESTORS=file://*` in my Vaultwarden config so the CSP `frame-ancestors` includes `file://` for the Electron context. Plus a Pangolin rule for `/connectors/*`. With all three changes the Desktop App works with WebAuthn 2FA now. Web UI was never affected since it doesn't rely on cross-origin iframes. For anyone else running Vaultwarden behind Pangolin with the Desktop App and WebAuthn — don't set `X-Frame-Options` via your reverse proxy, let Vaultwarden handle framing via its own CSP, and set `ALLOWED_IFRAME_ANCESTORS=file://*`. Thanks @AstralDestiny for the help debugging and the Traefik config comparison — that pointed me in the right direction with the header conflicts. ---
Author
Owner

@AstralDestiny commented on GitHub (May 20, 2026):

Yeah but file:// is a violation for Fido2.. if anything it's a hack.. which bitwarden themselves shouldn't be using..

<!-- gh-comment-id:4497750089 --> @AstralDestiny commented on GitHub (May 20, 2026): Yeah but file:// is a violation for Fido2.. if anything it's a hack.. which bitwarden themselves shouldn't be using..
Author
Owner

@AstralDestiny commented on GitHub (May 20, 2026):

Also if you can, tell bitwarden about it. As that behavior is not fun if it does any loading via file:// as someone could tamper with that over it requesting from the server. Could lead to non noisy issues in security of the desktop application, putting that in the iframe also allows some abuse to a point too.

<!-- gh-comment-id:4497873485 --> @AstralDestiny commented on GitHub (May 20, 2026): Also if you can, tell bitwarden about it. As that behavior is not fun if it does any loading via file:// as someone could tamper with that over it requesting from the server. Could lead to non noisy issues in security of the desktop application, putting that in the iframe also allows some abuse to a point too.
Author
Owner

@StefanSa commented on GitHub (May 20, 2026):

Agreed — loading a security-critical WebAuthn ceremony via file:// and then requiring frame-ancestors file://* to make it work is questionable architecture. There are still open issues around Desktop App + WebAuthn, e.g. bitwarden/clients#18013. Might be worth raising the file:// framing concern there since Bitwarden's security team would be the right audience for that.

<!-- gh-comment-id:4497954814 --> @StefanSa commented on GitHub (May 20, 2026): Agreed — loading a security-critical WebAuthn ceremony via file:// and then requiring frame-ancestors file://* to make it work is questionable architecture. There are still open issues around Desktop App + WebAuthn, e.g. [bitwarden/clients#18013](https://github.com/bitwarden/clients/issues/18013). Might be worth raising the file:// framing concern there since Bitwarden's security team would be the right audience for that.
Author
Owner

@StefanSa commented on GitHub (May 20, 2026):

One more follow-up — I've opened an issue on the Bitwarden side about the underlying architectural problem that caused all of this: the Desktop App uses a file:// iframe to load webauthn-connector.html, which forces server admins to weaken their security headers to make WebAuthn 2FA work.
bitwarden/clients#20745
Hopefully they'll move to native Electron WebAuthn calls at some point, which would make the ALLOWED_IFRAME_ANCESTORS workaround and the X-Frame-Options removal unnecessary.
Thanks again @AstralDestiny for the help and the security observation that led to this.

<!-- gh-comment-id:4498130432 --> @StefanSa commented on GitHub (May 20, 2026): One more follow-up — I've opened an issue on the Bitwarden side about the underlying architectural problem that caused all of this: the Desktop App uses a file:// iframe to load webauthn-connector.html, which forces server admins to weaken their security headers to make WebAuthn 2FA work. bitwarden/clients#20745 Hopefully they'll move to native Electron WebAuthn calls at some point, which would make the ALLOWED_IFRAME_ANCESTORS workaround and the X-Frame-Options removal unnecessary. Thanks again @AstralDestiny for the help and the security observation that led to this.
Author
Owner

@AstralDestiny commented on GitHub (May 20, 2026):

No worries.

<!-- gh-comment-id:4498590943 --> @AstralDestiny commented on GitHub (May 20, 2026): No worries.
Author
Owner

@github-actions[bot] commented on GitHub (Jun 4, 2026):

This issue has been automatically marked as stale due to 14 days of inactivity. It will be closed in 14 days if no further activity occurs.

<!-- gh-comment-id:4618074235 --> @github-actions[bot] commented on GitHub (Jun 4, 2026): This issue has been automatically marked as stale due to 14 days of inactivity. It will be closed in 14 days if no further activity occurs.
Author
Owner

@github-actions[bot] commented on GitHub (Jun 19, 2026):

This issue has been automatically closed due to inactivity. If you believe this is still relevant, please open a new issue with up-to-date information.

<!-- gh-comment-id:4747409805 --> @github-actions[bot] commented on GitHub (Jun 19, 2026): This issue has been automatically closed due to inactivity. If you believe this is still relevant, please open a new issue with up-to-date information.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: github-starred/pangolin#35739