Problems running under domain.com/bitwarden #2038

Closed
opened 2026-04-06 02:49:48 +02:00 by MrUnknownDE · 0 comments
Owner

Originally created by @jlknoch on 7/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 7/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.
No Label
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: github/vaultwarden#2038