← Start of tutorial

WIUNIX ACADEMY

Self-host Vaultwarden with HTTPS, restricted admin access and consistent backups

Page 3 of 5. Review the earlier steps and prerequisites before continuing.

YOUR SYSTEM. YOUR STEPS.

Where are you working?

Choose your OS and version. The prerequisites, commands and troubleshooting follow your environment.

Ubuntu24.04 LTSReviewed against official documentation ·

USE YOUR OWN VALUES

Configure this tutorial

Set the example values for your environment; related explanations and copy panels update together. Never enter passwords, private keys or tokens. Values stay in this tab and are not sent to the server or share links. Check actual DNS, certificates and network policy before running commands.

Different IP addresses, domains or ports? Open the settings below, change the values and select “Apply to tutorial”, then copy the updated commands.

Set values for my environment7 editable values
07

LET’S BUILD

Validate already provisioned trusted TLS

Provision the certificate chain as fullchain.pem and private key as privkey.pem inside the new TLS directory using your existing protected certificate procedure. Keep both root-owned0600. Never paste key content into this article or a command argument. The paths are shown in the next command; it does not issue a certificate.

The checks confirm matching public keys, SAN and at least7 days remaining. They do not prove a valid public trust chain, DNS routing or future renewal. A private CA must already be trusted by every intended client; never use insecure TLS bypass flags.

bash

Dedicated Vaultwarden VM

set -eu
sudo test -s '/srv/academy-vaultwarden/tls/fullchain.pem'
sudo test -s '/srv/academy-vaultwarden/tls/privkey.pem'
sudo openssl x509 -in '/srv/academy-vaultwarden/tls/fullchain.pem' -noout -checkhost 'vault.example.com'
sudo openssl x509 -in '/srv/academy-vaultwarden/tls/fullchain.pem' -noout -checkend 604800
sudo python3 - <<'PY'
import subprocess,stat
from pathlib import Path
r=Path('/srv/academy-vaultwarden/tls')
for name in ('fullchain.pem','privkey.pem'):
    p=r/name;assert stat.S_ISREG(p.lstat().st_mode) and p.stat().st_uid==0 and not p.stat().st_mode & 0o077
cert=subprocess.check_output(['openssl','x509','-in',str(r/'fullchain.pem'),'-pubkey','-noout'])
certkey=subprocess.check_output(['openssl','pkey','-pubin','-outform','DER'],input=cert)
key=subprocess.check_output(['openssl','pkey','-in',str(r/'privkey.pem'),'-pubout','-outform','DER'])
assert certkey==key, 'Certificate/key mismatch'
print('SAN, expiry window, private modes and matching key: OK')
PY
How do I know it worked?

Correct SAN, matching key and sufficient remaining validity; private modes pass.

08

LET’S BUILD

Open the dedicated Nginx configuration

Open the precreated file and copy the whole next panel. This file belongs inside Nginx http context. The map has a fixed dedicated-service name; academy_vaultwarden selects the configuration filename and container. Admin requests are allowed only from 203.0.113.10/32, evaluated from the direct TCP peer. Do not add real_ip trust, a CDN or blanket allow as a shortcut.

bash

Dedicated Vaultwarden VM

sudo env HOME=/root nano --ignorercfiles --restricted --noread -- '/srv/academy-vaultwarden/config/nginx.conf'
How do I know it worked?

The full proxy file is saved with the chosen admin source network.

HTTPS proxy and restricted admin path

The proxy replaces client-supplied identity/scheme headers, preserves WebSocket upgrade on the same backend port and sets an explicit 32MiB HTTP-body ceiling. This is not a per-vault storage quota. Unknown Host returns421. Access logs are disabled here to avoid retaining vault-related paths; review protected error logs without exporting credentials.

nginx · /srv/academy-vaultwarden/config/nginx.conf
map $http_upgrade $academy_vaultwarden_upgrade {
    default upgrade;
    '' "";
}
server {
    listen 443 ssl;
    server_name vault.example.com;
    if ($host != vault.example.com) { return 421; }
    ssl_certificate /srv/academy-vaultwarden/tls/fullchain.pem;
    ssl_certificate_key /srv/academy-vaultwarden/tls/privkey.pem;
    ssl_protocols TLSv1.2 TLSv1.3;
    client_max_body_size 32m;
    access_log off;
    error_log /var/log/nginx/academy_vaultwarden-error.log error;
    proxy_http_version 1.1;
    proxy_set_header Host vault.example.com;
    proxy_set_header X-Forwarded-Proto https;
    proxy_set_header X-Forwarded-Host vault.example.com;
    proxy_set_header X-Forwarded-For $remote_addr;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header Forwarded "";
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection $academy_vaultwarden_upgrade;
    proxy_connect_timeout 5s;
    proxy_read_timeout 300s;
    proxy_send_timeout 300s;
    proxy_next_upstream off;
    location ~* ^/admin(?:/|$) {
        allow 203.0.113.10/32;
        deny all;
        proxy_pass http://127.0.0.1:8159;
    }
    location / {
        proxy_pass http://127.0.0.1:8159;
    }
}
09

LET’S BUILD

Install without overwriting, then test HTTPS

The exclusive copy refuses an existing Nginx filename. The configuration test must succeed before reload. A failed test leaves the running configuration unchanged; inspect and correct only this new file. Never skip certificate validation to make curl pass.

bash

Dedicated Vaultwarden VM

set -eu
sudo python3 - <<'PY'
import os,stat
from pathlib import Path
p=Path('/srv/academy-vaultwarden/config/nginx.conf')
assert stat.S_ISREG(p.lstat().st_mode) and p.stat().st_uid==0
fd=os.open('/etc/nginx/conf.d/academy_vaultwarden.conf',os.O_WRONLY|os.O_CREAT|os.O_EXCL|os.O_NOFOLLOW,0o644)
with os.fdopen(fd,'wb') as f: f.write(p.read_bytes())
os.chmod('/etc/nginx/conf.d/academy_vaultwarden.conf',0o644)
PY
sudo nginx -t
sudo systemctl reload nginx
curl --fail --silent --show-error 'https://vault.example.com/alive'
How do I know it worked?

Valid Nginx configuration and trusted HTTPS /alive200.

10

LET’S BUILD

Check the public and management boundaries

Run from an ordinary client outside 203.0.113.10/32: /admin/ must return403. Then run the same request from the allowed administration source: an unauthenticated login page may return200, but administrative content still needs the separate passphrase. Do not put it into curl argv, this article or browser configuration fields.

On the server, confirm the backend mapping is loopback and that no config.json was created. The public config response is intentionally public and does not include vault items. An HTTP200 health probe does not prove login, sync or recovery.

The public config version is an API compatibility value, not the binary release. Use the explicit binary version command below; the reviewed image reports Vaultwarden1.37.4 and Web-Vault2026.7.0. Loopback and Docker administration assume trusted host operators; untrusted local accounts require a separate reviewed isolation design.

bash

Dedicated Vaultwarden VM

set -eu
curl --fail --silent --show-error 'https://vault.example.com/api/config' | python3 -c 'import json,sys; d=json.load(sys.stdin); print({k:d.get(k) for k in ("version","environment","server")})'
curl --silent --show-error --output /dev/null --write-out '%{http_code}
' 'https://vault.example.com/admin/'
sudo docker exec 'academy_vaultwarden' sh -c 'test -r /run/secrets/admin.phc && test -w /data && test ! -e /data/config.json'
sudo docker exec 'academy_vaultwarden' /vaultwarden --version
sudo docker port 'academy_vaultwarden'
How do I know it worked?

Outside-source admin403, allowed-source login boundary, loopback-only backend and no config override.

Enroll deliberately before adding real secrets

Keep SIGNUPS_ALLOWED=false. The server admin can issue an explicit invitation even with ordinary invitations disabled; SMTP is not configured here. Follow the official invitation workflow using a controlled mailbox and verified SMTP before team enrollment. Do not briefly open unrestricted public registration as a convenience.

Use a disposable account and dummy item for your own manual acceptance: trusted HTTPS/secure context, fresh login, create/edit/sync a dummy record to a second clean client, logout/relogin and verify the saved value. Record exact client versions and server release. Configure user MFA and keep recovery codes outside the vault before real use; an admin PHC is not MFA. SMTP, invitations, MFA, SSO, passkeys and browser/mobile clients are documented acceptance tasks, not executed claims in this guide.

Do not save settings casually in /admin: config.json then overrides the environment. Keep a single reviewed configuration owner, and protect any resulting file and credentials in backups. Keep enrollment closed after invitations; test organization permissions separately before admitting other people.

Explore all tutorials ↗