Problems running under domain.com/bitwarden #1333

Closed
opened 2026-02-05 00:39:10 +03:00 by OVERLORD · 0 comments
Owner

Originally created by @jlknoch on GitHub (Jul 28, 2022).

Hey all,
we are running a Kubernetes cluster and would like the Docker version of bitwardenrs/server:latest to be available via domain.com/bitwarden.

To do this, we instructed our traefik to point to our bitwarden server at /bitwarden. In order for the main page to load, we also had to add a middleware that stripped away the prefix bitwarden again.

The main page loads without errors.

We would also like to use the admin panel and make it accessible under /bitwarden/admin.

Again, the interaction of Traefik and bitwardenrs works and we can call /bitwarden/admin. However, the resources of the admin page cannot be loaded because absolute links and no relative links are specified in the source code.

When we call domain.com/bitwarden/admin:
image

The source code is:

<html>
<body>
<!--StartFragment-->

  | <!DOCTYPE html>
-- | --
  | <html lang="en">
  | <head>
  | <meta http-equiv="content-type" content="text/html; charset=UTF-8" />
  | <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" />
  | <meta name="robots" content="noindex,nofollow" />
  | <link rel="icon" type="image/png" href="/vw_static/vaultwarden-icon.png">
  | <title>Vaultwarden Admin Panel</title>
  | <link rel="stylesheet" href="/vw_static/bootstrap.css" />
  | <style>
  | body {
  | padding-top: 75px;
  | }
  | img {
  | width: 48px;
  | height: 48px;
  | }
  | .vaultwarden-icon {
  | height: 32px;
  | width: auto;
  | margin: -5px 0 0 0;
  | }
  | </style>
  | <script src="/vw_static/identicon.js"></script>
  | <script>
  | 'use strict';
  |  
  | function reload() { window.location.reload(); }
  | function msg(text, reload_page = true) {
  | text && alert(text);
  | reload_page && reload();
  | }
  | async function sha256(message) {
  | // https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/digest
  | const msgUint8 = new TextEncoder().encode(message);
  | const hashBuffer = await crypto.subtle.digest('SHA-256', msgUint8);
  | const hashArray = Array.from(new Uint8Array(hashBuffer));
  | const hashHex = hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
  | return hashHex;
  | }
  | async function identicon(email) {
  | const hash = await sha256(email);
  | const data = new Identicon(hash, { size: 48, format: 'svg' });
  | return "data:image/svg+xml;base64," + data.toString();
  | }
  | function toggleVis(input_id) {
  | const elem = document.getElementById(input_id);
  | const type = elem.getAttribute("type");
  | if (type === "text") {
  | elem.setAttribute("type", "password");
  | } else {
  | elem.setAttribute("type", "text");
  | }
  | return false;
  | }
  | function _post(url, successMsg, errMsg, body, reload_page = true) {
  | fetch(url, {
  | method: 'POST',
  | body: body,
  | mode: "same-origin",
  | credentials: "same-origin",
  | headers: { "Content-Type": "application/json" }
  | }).then( resp => {
  | if (resp.ok) { msg(successMsg, reload_page); return Promise.reject({error: false}); }
  | const respStatus = resp.status;
  | const respStatusText = resp.statusText;
  | return resp.text();
  | }).then( respText => {
  | try {
  | const respJson = JSON.parse(respText);
  | return respJson ? respJson.ErrorModel.Message : "Unknown error";
  | } catch (e) {
  | return Promise.reject({body:respStatus + ' - ' + respStatusText, error: true});
  | }
  | }).then( apiMsg => {
  | msg(errMsg + "\n" + apiMsg, reload_page);
  | }).catch( e => {
  | if (e.error === false) { return true; }
  | else { msg(errMsg + "\n" + e.body, reload_page); }
  | });
  | }
  | </script>
  | </head>
  |  
  | <body class="bg-light">
  | <nav class="navbar navbar-expand-md navbar-dark bg-dark mb-4 shadow fixed-top">
  | <div class="container-xl">
  | <a class="navbar-brand" href="/admin"><img class="vaultwarden-icon" src="/vw_static/vaultwarden-icon.png" alt="V">aultwarden Admin</a>
  | <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarCollapse"
  | aria-controls="navbarCollapse" aria-expanded="false" aria-label="Toggle navigation">
  | <span class="navbar-toggler-icon"></span>
  | </button>
  | <div class="collapse navbar-collapse" id="navbarCollapse">
  | <ul class="navbar-nav me-auto">
  | <li class="nav-item">
  | <a class="nav-link" href="/" target="_blank" rel="noreferrer">Vault</a>
  | </li>
  | </ul>
  |  
  | </div>
  | </div>
  | </nav>
  |  
  | <main class="container-xl">
  |  
  | <div class="align-items-center p-3 mb-3 text-white-50 bg-danger rounded shadow">
  | <div>
  | <h6 class="mb-0 text-white">Authentication key needed to continue</h6>
  | <small>Please provide it below:</small>
  |  
  | <form class="form-inline" method="post">
  | <input type="password" class="form-control w-50 mr-2" name="token" placeholder="Enter admin token">
  | <button type="submit" class="btn btn-primary">Enter</button>
  | </form>
  | </div>
  | </div>
  | </main>
  | <!-- This script needs to be at the bottom, else it will fail! -->
  | <script>
  | 'use strict';
  |  
  | // get current URL path and assign 'active' class to the correct nav-item
  | (() => {
  | const pathname = window.location.pathname;
  | if (pathname === "") return;
  | let navItem = document.querySelectorAll('.navbar-nav .nav-item a[href="'+pathname+'"]');
  | if (navItem.length === 1) {
  | navItem[0].className = navItem[0].className + ' active';
  | navItem[0].setAttribute('aria-current', 'page');
  | }
  | })();
  | </script>
  | <script src="/vw_static/bootstrap-native.js"></script>
  | </body>
  | </html>
  |  

<!--EndFragment-->
</body>
</html><!DOCTYPE html>
<html lang="en">
<head>
    <meta http-equiv="content-type" content="text/html; charset=UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" />
    <meta name="robots" content="noindex,nofollow" />
    <link rel="icon" type="image/png" href="[/vw_static/vaultwarden-icon.png](https://domain.com/vw_static/vaultwarden-icon.png)">
    <title>Vaultwarden Admin Panel</title>
    <link rel="stylesheet" href="[/vw_static/bootstrap.css](https://domain.com/vw_static/bootstrap.css)" />
    <style>
        body {
            padding-top: 75px;
        }
        img {
            width: 48px;
            height: 48px;
        }
        .vaultwarden-icon {
            height: 32px;
            width: auto;
            margin: -5px 0 0 0;
        }
    </style>
    <script src="[/vw_static/identicon.js](https://domain.com/vw_static/identicon.js)"></script>
    <script>
        'use strict';

        function reload() { window.location.reload(); }
        function msg(text, reload_page = true) {
            text && alert(text);
            reload_page && reload();
        }
        async function sha256(message) {
            // https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/digest
            const msgUint8 = new TextEncoder().encode(message);
            const hashBuffer = await crypto.subtle.digest('SHA-256', msgUint8);
            const hashArray = Array.from(new Uint8Array(hashBuffer));
            const hashHex = hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
            return hashHex;
        }
        async function identicon(email) {
            const hash = await sha256(email);
            const data = new Identicon(hash, { size: 48, format: 'svg' });
            return "data:image/svg+xml;base64," + data.toString();
        }
        function toggleVis(input_id) {
            const elem = document.getElementById(input_id);
            const type = elem.getAttribute("type");
            if (type === "text") {
                elem.setAttribute("type", "password");
            } else {
                elem.setAttribute("type", "text");
            }
            return false;
        }
        function _post(url, successMsg, errMsg, body, reload_page = true) {
            fetch(url, {
                method: 'POST',
                body: body,
                mode: "same-origin",
                credentials: "same-origin",
                headers: { "Content-Type": "application/json" }
            }).then( resp => {
                if (resp.ok) { msg(successMsg, reload_page); return Promise.reject({error: false}); }
                const respStatus = resp.status;
                const respStatusText = resp.statusText;
                return resp.text();
            }).then( respText => {
                try {
                    const respJson = JSON.parse(respText);
                    return respJson ? respJson.ErrorModel.Message : "Unknown error";
                } catch (e) {
                    return Promise.reject({body:respStatus + ' - ' + respStatusText, error: true});
                }
            }).then( apiMsg => {
                msg(errMsg + "\n" + apiMsg, reload_page);
            }).catch( e => {
                if (e.error === false) { return true; }
                else { msg(errMsg + "\n" + e.body, reload_page); }
            });
        }
    </script>
</head>

<body class="bg-light">
    <nav class="navbar navbar-expand-md navbar-dark bg-dark mb-4 shadow fixed-top">
        <div class="container-xl">
            <a class="navbar-brand" href="[/admin](https://domain.com/admin)"><img class="vaultwarden-icon" src="[/vw_static/vaultwarden-icon.png](https://domain.com/vw_static/vaultwarden-icon.png)" alt="V">aultwarden Admin</a>
            <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarCollapse"
                    aria-controls="navbarCollapse" aria-expanded="false" aria-label="Toggle navigation">
                <span class="navbar-toggler-icon"></span>
            </button>
            <div class="collapse navbar-collapse" id="navbarCollapse">
                <ul class="navbar-nav me-auto">
                    <li class="nav-item">
                        <a class="nav-link" href="[/](https://domain.com/)" target="_blank" rel="noreferrer">Vault</a>
                    </li>
                </ul>

            </div>
        </div>
    </nav>

<main class="container-xl">

    <div class="align-items-center p-3 mb-3 text-white-50 bg-danger rounded shadow">
        <div>
            <h6 class="mb-0 text-white">Authentication key needed to continue</h6>
            <small>Please provide it below:</small>

            <form class="form-inline" method="post">
                <input type="password" class="form-control w-50 mr-2" name="token" placeholder="Enter admin token">
                <button type="submit" class="btn btn-primary">Enter</button>
            </form>
        </div>
    </div>
</main>
    <!-- This script needs to be at the bottom, else it will fail! -->
    <script>
        'use strict';

        // get current URL path and assign 'active' class to the correct nav-item
        (() => {
            const pathname = window.location.pathname;
            if (pathname === "") return;
            let navItem = document.querySelectorAll('.navbar-nav .nav-item a[href="'+pathname+'"]');
            if (navItem.length === 1) {
                navItem[0].className = navItem[0].className + ' active';
                navItem[0].setAttribute('aria-current', 'page');
            }
        })();
    </script>
    <script src="[/vw_static/bootstrap-native.js](https://domain.com/vw_static/bootstrap-native.js)"></script>
</body>
</html>

In the source code of the admin panel absolute links are used like /vw_static/vaultwarden-icon.png. But these links should be named /bitwarden/vw_static/vaultwarden-icon.png to be reachable. So they should be relative.

How to solve the problem
In the code you could achieve this by omitting the / at the beginning of the link. Then instead of /vw_static/vaultwarden-icon.png now vw_static/vaultwarden-icon.png would be used. This link would then be automatically attached to suburls like /bitwarden and you could reach the resources.

Would it be possible to create an update for this? If not, how can we achieve this on our end?

Thanks in advance!

Originally created by @jlknoch on GitHub (Jul 28, 2022). Hey all, we are running a Kubernetes cluster and would like the Docker version of `bitwardenrs/server:latest` to be available via `domain.com/bitwarden`. To do this, we instructed our traefik to point to our bitwarden server at `/bitwarden`. In order for the main page to load, we also had to add a middleware that stripped away the prefix bitwarden again. **The main page loads without errors.** **We would also like to use the admin panel and make it accessible under `/bitwarden/admin`.** Again, the interaction of Traefik and bitwardenrs works and we can call `/bitwarden/admin`. However, the resources of the admin page cannot be loaded because absolute links and no relative links are specified in the source code. When we call `domain.com/bitwarden/admin`: ![image](https://user-images.githubusercontent.com/109718804/181450829-ffa227f3-85b1-41eb-96b3-881474bf2ec6.png) The source code is: ```html <html> <body> <!--StartFragment-->   | <!DOCTYPE html> -- | --   | <html lang="en">   | <head>   | <meta http-equiv="content-type" content="text/html; charset=UTF-8" />   | <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" />   | <meta name="robots" content="noindex,nofollow" />   | <link rel="icon" type="image/png" href="/vw_static/vaultwarden-icon.png">   | <title>Vaultwarden Admin Panel</title>   | <link rel="stylesheet" href="/vw_static/bootstrap.css" />   | <style>   | body {   | padding-top: 75px;   | }   | img {   | width: 48px;   | height: 48px;   | }   | .vaultwarden-icon {   | height: 32px;   | width: auto;   | margin: -5px 0 0 0;   | }   | </style>   | <script src="/vw_static/identicon.js"></script>   | <script>   | 'use strict';   |     | function reload() { window.location.reload(); }   | function msg(text, reload_page = true) {   | text && alert(text);   | reload_page && reload();   | }   | async function sha256(message) {   | // https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/digest   | const msgUint8 = new TextEncoder().encode(message);   | const hashBuffer = await crypto.subtle.digest('SHA-256', msgUint8);   | const hashArray = Array.from(new Uint8Array(hashBuffer));   | const hashHex = hashArray.map(b => b.toString(16).padStart(2, '0')).join('');   | return hashHex;   | }   | async function identicon(email) {   | const hash = await sha256(email);   | const data = new Identicon(hash, { size: 48, format: 'svg' });   | return "data:image/svg+xml;base64," + data.toString();   | }   | function toggleVis(input_id) {   | const elem = document.getElementById(input_id);   | const type = elem.getAttribute("type");   | if (type === "text") {   | elem.setAttribute("type", "password");   | } else {   | elem.setAttribute("type", "text");   | }   | return false;   | }   | function _post(url, successMsg, errMsg, body, reload_page = true) {   | fetch(url, {   | method: 'POST',   | body: body,   | mode: "same-origin",   | credentials: "same-origin",   | headers: { "Content-Type": "application/json" }   | }).then( resp => {   | if (resp.ok) { msg(successMsg, reload_page); return Promise.reject({error: false}); }   | const respStatus = resp.status;   | const respStatusText = resp.statusText;   | return resp.text();   | }).then( respText => {   | try {   | const respJson = JSON.parse(respText);   | return respJson ? respJson.ErrorModel.Message : "Unknown error";   | } catch (e) {   | return Promise.reject({body:respStatus + ' - ' + respStatusText, error: true});   | }   | }).then( apiMsg => {   | msg(errMsg + "\n" + apiMsg, reload_page);   | }).catch( e => {   | if (e.error === false) { return true; }   | else { msg(errMsg + "\n" + e.body, reload_page); }   | });   | }   | </script>   | </head>   |     | <body class="bg-light">   | <nav class="navbar navbar-expand-md navbar-dark bg-dark mb-4 shadow fixed-top">   | <div class="container-xl">   | <a class="navbar-brand" href="/admin"><img class="vaultwarden-icon" src="/vw_static/vaultwarden-icon.png" alt="V">aultwarden Admin</a>   | <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarCollapse"   | aria-controls="navbarCollapse" aria-expanded="false" aria-label="Toggle navigation">   | <span class="navbar-toggler-icon"></span>   | </button>   | <div class="collapse navbar-collapse" id="navbarCollapse">   | <ul class="navbar-nav me-auto">   | <li class="nav-item">   | <a class="nav-link" href="/" target="_blank" rel="noreferrer">Vault</a>   | </li>   | </ul>   |     | </div>   | </div>   | </nav>   |     | <main class="container-xl">   |     | <div class="align-items-center p-3 mb-3 text-white-50 bg-danger rounded shadow">   | <div>   | <h6 class="mb-0 text-white">Authentication key needed to continue</h6>   | <small>Please provide it below:</small>   |     | <form class="form-inline" method="post">   | <input type="password" class="form-control w-50 mr-2" name="token" placeholder="Enter admin token">   | <button type="submit" class="btn btn-primary">Enter</button>   | </form>   | </div>   | </div>   | </main>   | <!-- This script needs to be at the bottom, else it will fail! -->   | <script>   | 'use strict';   |     | // get current URL path and assign 'active' class to the correct nav-item   | (() => {   | const pathname = window.location.pathname;   | if (pathname === "") return;   | let navItem = document.querySelectorAll('.navbar-nav .nav-item a[href="'+pathname+'"]');   | if (navItem.length === 1) {   | navItem[0].className = navItem[0].className + ' active';   | navItem[0].setAttribute('aria-current', 'page');   | }   | })();   | </script>   | <script src="/vw_static/bootstrap-native.js"></script>   | </body>   | </html>   |   <!--EndFragment--> </body> </html><!DOCTYPE html> <html lang="en"> <head> <meta http-equiv="content-type" content="text/html; charset=UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no" /> <meta name="robots" content="noindex,nofollow" /> <link rel="icon" type="image/png" href="[/vw_static/vaultwarden-icon.png](https://domain.com/vw_static/vaultwarden-icon.png)"> <title>Vaultwarden Admin Panel</title> <link rel="stylesheet" href="[/vw_static/bootstrap.css](https://domain.com/vw_static/bootstrap.css)" /> <style> body { padding-top: 75px; } img { width: 48px; height: 48px; } .vaultwarden-icon { height: 32px; width: auto; margin: -5px 0 0 0; } </style> <script src="[/vw_static/identicon.js](https://domain.com/vw_static/identicon.js)"></script> <script> 'use strict'; function reload() { window.location.reload(); } function msg(text, reload_page = true) { text && alert(text); reload_page && reload(); } async function sha256(message) { // https://developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/digest const msgUint8 = new TextEncoder().encode(message); const hashBuffer = await crypto.subtle.digest('SHA-256', msgUint8); const hashArray = Array.from(new Uint8Array(hashBuffer)); const hashHex = hashArray.map(b => b.toString(16).padStart(2, '0')).join(''); return hashHex; } async function identicon(email) { const hash = await sha256(email); const data = new Identicon(hash, { size: 48, format: 'svg' }); return "data:image/svg+xml;base64," + data.toString(); } function toggleVis(input_id) { const elem = document.getElementById(input_id); const type = elem.getAttribute("type"); if (type === "text") { elem.setAttribute("type", "password"); } else { elem.setAttribute("type", "text"); } return false; } function _post(url, successMsg, errMsg, body, reload_page = true) { fetch(url, { method: 'POST', body: body, mode: "same-origin", credentials: "same-origin", headers: { "Content-Type": "application/json" } }).then( resp => { if (resp.ok) { msg(successMsg, reload_page); return Promise.reject({error: false}); } const respStatus = resp.status; const respStatusText = resp.statusText; return resp.text(); }).then( respText => { try { const respJson = JSON.parse(respText); return respJson ? respJson.ErrorModel.Message : "Unknown error"; } catch (e) { return Promise.reject({body:respStatus + ' - ' + respStatusText, error: true}); } }).then( apiMsg => { msg(errMsg + "\n" + apiMsg, reload_page); }).catch( e => { if (e.error === false) { return true; } else { msg(errMsg + "\n" + e.body, reload_page); } }); } </script> </head> <body class="bg-light"> <nav class="navbar navbar-expand-md navbar-dark bg-dark mb-4 shadow fixed-top"> <div class="container-xl"> <a class="navbar-brand" href="[/admin](https://domain.com/admin)"><img class="vaultwarden-icon" src="[/vw_static/vaultwarden-icon.png](https://domain.com/vw_static/vaultwarden-icon.png)" alt="V">aultwarden Admin</a> <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarCollapse" aria-controls="navbarCollapse" aria-expanded="false" aria-label="Toggle navigation"> <span class="navbar-toggler-icon"></span> </button> <div class="collapse navbar-collapse" id="navbarCollapse"> <ul class="navbar-nav me-auto"> <li class="nav-item"> <a class="nav-link" href="[/](https://domain.com/)" target="_blank" rel="noreferrer">Vault</a> </li> </ul> </div> </div> </nav> <main class="container-xl"> <div class="align-items-center p-3 mb-3 text-white-50 bg-danger rounded shadow"> <div> <h6 class="mb-0 text-white">Authentication key needed to continue</h6> <small>Please provide it below:</small> <form class="form-inline" method="post"> <input type="password" class="form-control w-50 mr-2" name="token" placeholder="Enter admin token"> <button type="submit" class="btn btn-primary">Enter</button> </form> </div> </div> </main> <!-- This script needs to be at the bottom, else it will fail! --> <script> 'use strict'; // get current URL path and assign 'active' class to the correct nav-item (() => { const pathname = window.location.pathname; if (pathname === "") return; let navItem = document.querySelectorAll('.navbar-nav .nav-item a[href="'+pathname+'"]'); if (navItem.length === 1) { navItem[0].className = navItem[0].className + ' active'; navItem[0].setAttribute('aria-current', 'page'); } })(); </script> <script src="[/vw_static/bootstrap-native.js](https://domain.com/vw_static/bootstrap-native.js)"></script> </body> </html> ``` In the source code of the admin panel absolute links are used like `/vw_static/vaultwarden-icon.png`. But these links should be named `/bitwarden/vw_static/vaultwarden-icon.png` to be reachable. So they should be relative. **How to solve the problem** In the code you could achieve this by omitting the `/` at the beginning of the link. Then instead of `/vw_static/vaultwarden-icon.png` now `vw_static/vaultwarden-icon.png` would be used. This link would then be automatically attached to suburls like `/bitwarden` and you could reach the resources. **Would it be possible to create an update for this? If not, how can we achieve this on our end?** Thanks in advance!
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: starred/vaultwarden#1333