Vaultwarden su VPS: password manager self-hosted
Guida a Vaultwarden e Docker su VPS: HTTPS, token admin, Fail2ban e backup testati. Evita errori TLS e proteggi il volume con tutte le password.
Cosa stai realizzando
Un password manager di cui hai il pieno controllo: Vaultwarden in un unico container di piccole dimensioni, dietro un reverse proxy che termina HTTPS, con le app ufficiali Bitwarden sul telefono, sul laptop e nel browser configurate per utilizzarlo. Vaultwarden reimplementa in Rust l'API server di Bitwarden e utilizza lo stesso protocollo di bitwarden.com, quindi ogni client ufficiale funziona senza modifiche, ma richiede circa 100 MB di RAM invece dello stack ufficiale composto da più container.
L'installazione richiede solo una dozzina di righe di Compose. Gli aspetti realmente importanti, e quelli che più spesso causano problemi, sono tre: TLS deve essere già disponibile prima di aprire il web vault, le registrazioni pubbliche devono essere disabilitate non appena hai creato il tuo account e il volume dei dati deve essere sottoposto a backup e a un ripristino di test, perché quella directory contiene tutte le password di cui sei responsabile.
Prerequisiti e aspetti critici da conoscere
- Una VPS con Docker Engine e il plugin Compose, su un'installazione pulita di Ubuntu 24.04 su KVM, con accesso root o sudo. 512 MB di RAM sono effettivamente sufficienti; 1 GB offre un margine più confortevole. È uno dei servizi più leggeri che si possano eseguire e si trova ai primi posti della selezione dei servizi che vale la pena ospitare autonomamente. Tuttavia, dimensiona la VPS in base agli altri servizi che condivideranno la macchina: eseguire una libreria fotografica self-hosted come PhotoPrism o Immich sulla stessa VPS porta il requisito minimo di RAM nell'ordine dei gigabyte, mentre Vaultwarden incide appena. Lo stesso calcolo vale per le interfacce multimediali che aggiungerai in seguito, perché trasformare una libreria Jellyfin in un videonoleggio degli anni 90 visitabile richiede un altro container sempre attivo e memoria aggiuntiva per il transcoding, mantenendo lo stesso budget.
- Un dominio con un record A (e un record AAAA se disponi di IPv6) che punti
vault.example.comalla VPS. Il certificato TLS viene emesso per questo nome esatto, quindi il DNS deve risolverlo prima di iniziare. - Le porte 80 e 443 devono essere aperte verso Internet e la terminazione deve essere gestita dal reverse proxy, mai direttamente da Vaultwarden. La porta 80 viene usata esclusivamente per la challenge del certificato ACME e per il redirect da HTTP a HTTPS.
- Il principale aspetto critico da considerare subito è il seguente: i client Bitwarden rifiutano di comunicare con un server che non usa HTTPS. Non esiste una fase di "prova iniziale su http": questo percorso non funziona, per un motivo preciso illustrato nella sezione successiva.
Perché Vaultwarden e non lo stack ufficiale di Bitwarden
Gli stessi client, con un consumo di risorse molto inferiore. La versione ufficiale self-hosted di Bitwarden viene distribuita come un insieme di container, tra cui MSSQL, Nginx, Identity, Api e Admin, e richiede circa 2 GB di RAM. Vaultwarden è un singolo binario che, per impostazione predefinita, archivia tutti i dati in un database SQLite e, quando è inattivo, utilizza poche decine di megabyte. Per una singola persona, una famiglia o un piccolo team è la scelta più semplice. Poiché implementa fedelmente l'API di Bitwarden, i dati restano portabili tra Vaultwarden e bitwarden.com.
Si rinuncia alla maggior parte delle funzionalità per gli ambienti enterprise: non è disponibile il provisioning SCIM, anche se il supporto sperimentale a OpenID Connect SSO è stato introdotto nella versione 1.35.0. Inoltre, l'operatore sei tu: aggiornamenti correttivi, HTTPS e backup sono a tuo carico. Questa guida tratta proprio queste tre attività.
Perché HTTPS non è facoltativo
Il vault web e le estensioni del browser di Bitwarden derivano le chiavi di crittografia nel browser tramite l’API Web Crypto (window.crypto.subtle). I browser espongono crypto.subtle soltanto in un contesto sicuro, tramite HTTPS oppure nel caso speciale di http://localhost. Con http://vault.example.com in chiaro, la funzionalità è undefined. Di conseguenza, non appena l’applicazione deriva una chiave, genera un errore e nella console viene visualizzato:
Uncaught (in promise) TypeError: Cannot read properties of undefined (reading 'importKey')La pagina si blocca oppure mostra un errore crittografico generico e nessun utente riesce ad accedere. I client desktop, mobili e del browser eseguono un controllo autonomo sull’URL self-hosted. Se l’endpoint usa http oppure non è raggiungibile, rifiutano la connessione con:
This is not a recognized Bitwarden server. You may need to check with your provider or update your server.La causa è la stessa in entrambi i casi: HTTPS valido assente. Per questo configuriamo prima TLS e non apriamo mai il vault tramite http, nemmeno una volta per un controllo rapido.
Passaggio 1: DNS e reverse proxy (prima TLS)
Punta il record al tuo VPS e verifica che venga risolto all'indirizzo corretto:
dig +short vault.example.comLa riga restituita deve contenere l'indirizzo IP del tuo VPS. Se è vuota o errata, correggi il DNS e attendi la scadenza del TTL: l'emissione del certificato fallisce se il nome non viene risolto.
Per il front end HTTPS, questa guida usa Traefik, che emette e rinnova automaticamente i certificati Let's Encrypt e si integra direttamente con Compose. Se non lo esegui già, segui prima la configurazione del reverse proxy Traefik e del TLS automatico; crea una rete Docker esterna (proxy qui sotto) e un resolver ACME (letsencrypt) a cui si collega il servizio Vaultwarden. Anche nginx con un certificato emesso manualmente funziona allo stesso modo dal lato Vaultwarden.
Preferisci nginx e Certbot invece di Traefik? Collega Vaultwarden a 127.0.0.1:8080 (aggiungi ports: ["127.0.0.1:8080:80"] al servizio e rimuovi le label Traefik), quindi emetti un certificato e configura il proxy verso il servizio. La parte relativa al certificato è descritta in emissione dei certificati Let's Encrypt con Certbot e nginx. L'elemento aggiuntivo fondamentale è l'upgrade WebSocket sul percorso delle notifiche:
server {
listen 443 ssl;
server_name vault.example.com;
client_max_body_size 525M;
location / {
proxy_pass http://127.0.0.1:8080;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
}Nota la riga X-Real-IP: consente a Fail2ban di identificare in seguito l'attaccante reale anziché 127.0.0.1. Tutto il resto di questa guida è identico, indipendentemente dal fatto che davanti venga utilizzato Traefik o nginx.
Passaggio 2: il file Compose
Create prima la directory del progetto. Questa guida usa /opt/vaultwarden, che rende prevedibile il nome del progetto Compose e quindi il volume dati vaultwarden_vw-data; i passaggi successivi relativi a Fail2ban e ai backup dipendono esattamente da questo nome.
sudo mkdir -p /opt/vaultwarden
cd /opt/vaultwardenCreate un .env per il secret dell'amministratore e il file Compose in questa directory.
# .env
ADMIN_TOKEN=paste-a-strong-token-hereGenerate il token con openssl rand -base64 48 e incollatelo nel file. (La forma con hash più robusto viene descritta nel passaggio successivo; per iniziare è sufficiente una stringa casuale lunga.)
# docker-compose.yml
services:
vaultwarden:
image: vaultwarden/server:latest
container_name: vaultwarden
restart: unless-stopped
environment:
DOMAIN: "https://vault.example.com"
SIGNUPS_ALLOWED: "true" # closed in Step 4, keep true just to register
ADMIN_TOKEN: "${ADMIN_TOKEN}"
IP_HEADER: "X-Forwarded-For" # X-Real-IP if your proxy sends that instead
LOG_FILE: "/data/vaultwarden.log"
LOG_LEVEL: "warn"
volumes:
- vw-data:/data
networks:
- proxy
labels:
- "traefik.enable=true"
- "traefik.http.routers.vw.rule=Host(`vault.example.com`)"
- "traefik.http.routers.vw.entrypoints=websecure"
- "traefik.http.routers.vw.tls.certresolver=letsencrypt"
- "traefik.http.services.vw.loadbalancer.server.port=80"
volumes:
vw-data:
networks:
proxy:
external: trueDue aspetti di questo file determinano l'intero design. Non esiste alcun mapping ports:, quindi Vaultwarden è raggiungibile soltanto tramite Traefik e la relativa terminazione TLS; pubblicare la sua porta sull'host significa esporre accidentalmente il vault tramite http. Inoltre, DOMAIN deve essere l'URL HTTPS pubblico completo: viene incorporato nei link degli allegati, nella 2FA WebAuthn e nell'endpoint delle notifiche. Un valore errato o http rompe queste funzioni anche quando il sito viene caricato. Il tag latest è un'eccezione deliberata alla consueta regola di non usare mai latest. Vaultwarden distribuisce le release stabili come un'unica immagine rolling, mentre :testing è il canale separato per le versioni preliminari. Eseguite quindi gli aggiornamenti intenzionalmente e consultate rapidamente le note di rilascio prima di eseguire il pull. L'eccezione è circoscritta: per la maggior parte dei container a lunga durata è preferibile usare un tag esatto, perché mantiene prevedibile un agent self-hosted sempre attivo sullo stesso VPS tra riavvii e pull.
Avviatelo e monitorate il log:
docker compose up -d
docker compose logs -f vaultwardenUn avvio corretto termina con una riga come Rocket has launched from http://0.0.0.0:80. Attendete alcuni secondi affinché Traefik recuperi il certificato, quindi caricate https://vault.example.com. Dovreste visualizzare il vault web di Bitwarden con un lucchetto valido e senza avvisi relativi al certificato.
Passaggio 3: un ADMIN_TOKEN robusto e il problema di $$
ADMIN_TOKEN protegge /admin, il pannello che può leggere ogni utente e ogni impostazione della tua istanza. Trattalo quindi come una password di root. Sono valide due modalità.
La modalità semplice usa la stringa casuale che hai già generato con openssl rand -base64 48. Poiché base64 non contiene mai $, puoi inserirla direttamente in .env senza eseguire l'escaping.
La modalità più sicura usa un hash PHC Argon2, quindi il token in chiaro non viene mai salvato su disco. Generane uno usando la stessa immagine:
docker run --rm -it vaultwarden/server /vaultwarden hash --preset owaspIl comando richiede il token due volte e stampa una stringa che inizia con $argon2id$v=19$.... Ecco il problema che può costare un'ora di lavoro: Docker Compose tratta $ come interpolazione di variabili. Quando incolli l'hash nel file Compose, devi quindi raddoppiare ogni $ in $$. Inseriscilo direttamente sotto environment:, senza passare da .env, e non racchiuderlo tra virgolette:
environment:
ADMIN_TOKEN: $$argon2id$$v=19$$m=19456,t=2,p=1$$c29tZXNhbHQ$$RdescudvJCsgt3ub+b+dWRWJTmaaJObGSe lasci i singoli segni $, Compose visualizza l'avviso The "argon2id" variable is not set e svuota il token. Di conseguenza, /admin rifiuta la password corretta. Esegui docker compose up -d e conserva nel tuo password manager il token in chiaro che hai digitato al prompt.
Passaggio 4: registra l’account, quindi chiudi l’accesso
Con SIGNUPS_ALLOWED: "true", apri https://vault.example.com, fai clic su Create account e registrati con il tuo indirizzo email e una password master complessa. Questa password master non è recuperabile e non esiste una procedura di reimpostazione. Salvala prima in un luogo affidabile.
Ora chiudi l’accesso. Modifica il file Compose per disabilitare le registrazioni:
SIGNUPS_ALLOWED: "false"Riapplica la configurazione con docker compose up -d. Non è un’attività di hardening da rimandare. Se le registrazioni restano aperte, chiunque trovi l’URL, inclusi i crawler, può creare un account sul server. Questi utenti non possono leggere il tuo vault, ma consumano risorse e trasformano la tua istanza privata in un servizio pubblico. Il segnale che hai lasciato l’opzione attiva è che /admin elenca account che non hai mai creato.
Per aggiungere in seguito familiari o membri del team senza riaprire le registrazioni pubbliche, usa il pulsante Invite User in /admin. Questo percorso richiede la configurazione di SMTP, in modo che l’invitato riceva il relativo link.
Passaggio 5: accesso a /admin
Aprire https://vault.example.com/admin e inserire il token di amministrazione in chiaro (la stringa casuale o la password di cui è stato calcolato l'hash, non l'hash stesso). Da questa pagina è possibile elencare gli utenti, modificare le impostazioni, inviare un'email di test e creare uno snapshot del database.
Se la pagina restituisce 404 Not Found, ADMIN_TOKEN è vuoto o non impostato e disabilita completamente il pannello. Questa è una scelta valida se il pannello non serve. Se la pagina viene caricata ma rifiuta il token, consultare il problema di escaping di $$ nell'elenco degli errori riportato di seguito. Token dimenticato? Non esiste una procedura di recupero: modificare .env oppure il file Compose, impostarne uno nuovo ed eseguire docker compose up -d.
Passaggio 6, collegare i client Bitwarden
Ogni client ufficiale può connettersi a un server self-hosted. Installa quindi il client Bitwarden per desktop, dispositivi mobili o browser dagli store consueti. Non serve una build speciale di Vaultwarden.
Prima di accedere, apri l'icona a forma di ingranaggio delle impostazioni nella schermata di accesso, indicata come Self-hosted o Region → Self-hosted. Imposta Server URL su https://vault.example.com e salva. Accedi quindi con l'indirizzo email e la master password registrati. Il client dovrebbe connettersi immediatamente e offrire il riempimento e il salvataggio delle credenziali.
Se un client mostra This is not a recognized Bitwarden server. You may need to check with your provider or update your server., l'URL è errato, usa http oppure il certificato non è considerato attendibile. Verifica innanzitutto che https://vault.example.com venga caricato correttamente in un browser. Gli aggiornamenti lenti sugli altri dispositivi dipendono dal push tramite WebSocket, descritto di seguito.
Passaggio 7: una jail Fail2ban per l’endpoint di accesso
Vaultwarden registra ogni accesso non riuscito nel file impostato da LOG_FILE, esattamente ciò che serve a un sistema di protezione dagli attacchi brute force. Se Fail2ban non è già in esecuzione, le istruzioni per l’installazione e la configurazione di base sono disponibili nella guida al rafforzamento di Fail2ban per SSH; qui aggiungiamo una jail per Vaultwarden.
Per prima cosa, individua il percorso del volume denominato sull’host, così Fail2ban può leggere il log:
docker volume inspect vaultwarden_vw-data --format '{{ .Mountpoint }}'Il comando restituisce un output simile a /var/lib/docker/volumes/vaultwarden_vw-data/_data; al suo interno, il log si trova in vaultwarden.log. Crea il filtro:
# /etc/fail2ban/filter.d/vaultwarden.conf
[Definition]
failregex = ^.*Username or password is incorrect\. Try again\. IP: <ADDR>\. Username:.*$
ignoreregex =Quindi crea la jail:
# /etc/fail2ban/jail.d/vaultwarden.local
[vaultwarden]
enabled = true
filter = vaultwarden
logpath = /var/lib/docker/volumes/vaultwarden_vw-data/_data/vaultwarden.log
banaction = iptables-allports
chain = DOCKER-USER
maxretry = 5
findtime = 600
bantime = 3600Ricarica la configurazione con sudo systemctl restart fail2ban e verifica il risultato con sudo fail2ban-client status vaultwarden.
Tre dettagli relativi a Docker determinano se questa protezione funziona. Primo: se il log mostra IP: 127.0.0.1 o l’indirizzo del proxy per ogni tentativo non riuscito, Vaultwarden sta bloccando il proxy. Imposta IP_HEADER sull’header realmente inviato dal proxy: X-Forwarded-For per Traefik, X-Real-IP per il blocco nginx precedente, CF-Connecting-IP quando il servizio è dietro Cloudflare. Secondo: la catena iptables corretta dipende dal proxy. Con Traefik in esecuzione come container con porte pubblicate, il traffico attraversa il percorso FORWARD di Docker, quindi il blocco deve essere applicato in DOCKER-USER, come nell’esempio precedente. Se invece hai scelto l’opzione host-nginx del Passaggio 1, le connessioni terminano su nginx nella catena INPUT dell’host e un blocco DOCKER-USER non le intercetta. In questo caso, elimina la riga chain = DOCKER-USER, in modo che Fail2ban utilizzi la catena predefinita INPUT. Terzo: usa banaction = iptables-allports invece del valore predefinito basato sulla porta. Questa jail non definisce alcuna porta e un blocco su tutte le porte in DOCKER-USER impedisce in modo ordinato all’autore dell’attacco di accedere a ogni servizio pubblicato sull’host.
Passaggio 8: eseguire il backup del vault e poi ripristinarlo davvero
Il volume vw-data è il tuo gestore di password. Contiene db.sqlite3 (ogni voce), le directory attachments/ e sends/, i file rsa_key.* che firmano le sessioni di accesso e config.json dal pannello di amministrazione. Un backup che esclude uno qualsiasi di questi elementi non funzionerà quando servirà.
Copiare db.sqlite3 mentre Vaultwarden sta scrivendo può produrre un file parzialmente scritto e corrotto. Esegui quindi uno snapshot a freddo; l'interruzione dura pochi secondi:
#!/usr/bin/env bash
set -euo pipefail
STAMP=$(date +%F)
DEST=/root/vw-backups
VOL=$(docker volume inspect vaultwarden_vw-data --format '{{ .Mountpoint }}')
mkdir -p "$DEST"
docker compose -f /opt/vaultwarden/docker-compose.yml stop vaultwarden
tar czf "$DEST/vw-$STAMP.tgz" -C "$VOL" .
docker compose -f /opt/vaultwarden/docker-compose.yml start vaultwardenEsegui il comando ogni notte tramite cron e copia .tgz fuori dal server. Un backup conservato solo sul server che stai proteggendo non è un backup. Il metodo corretto consiste nell'inviarlo tramite un backup notturno con restic su un altro server o in un object storage, che cifra l'archivio e deduplica automaticamente gli snapshot ripetuti. Il pulsante Backup Database del pannello di amministrazione è utile per creare uno snapshot a caldo del solo file SQLite, ma non include gli allegati né le chiavi.
Ora esegui la procedura che distingue un backup reale da una semplice ipotesi: ripristinalo una volta e verifica che funzioni:
mkdir -p /tmp/vw-restore
tar xzf /root/vw-backups/vw-2026-07-15.tgz -C /tmp/vw-restore
docker run --rm -p 127.0.0.1:8888:80 -v /tmp/vw-restore:/data vaultwarden/serverDal laptop, crea un tunnel verso il server con ssh -L 8888:127.0.0.1:8888 you@your-vps e apri http://localhost:8888. Poiché localhost è un contesto sicuro, crypto.subtle è disponibile e il vault viene decrittografato tramite http non cifrato in questo caso, l'unico in cui è consentito. Accedi con la password principale e verifica che le tue voci siano presenti: se lo sono, il database, le chiavi RSA e la password principale hanno superato correttamente il ciclo di backup e ripristino e puoi ricostruire l'ambiente su un nuovo VPS in pochi minuti. Arresta il container con Ctrl-C ed elimina /tmp/vw-restore. Mantieni questa abitudine di usare un tunnel per qualsiasi altra interfaccia di amministrazione sul server che non dovrebbe mai essere esposta a Internet. È anche il modo per raggiungere uno scanner di sicurezza open source self-hosted sulla porta 5173.
Modalità di errore e stringhe visualizzate
Cannot read properties of undefined (reading 'importKey') nella console del browser. Il vault è stato caricato tramite http, quindi crypto.subtle non è definito; accedervi soltanto tramite https:// e aggiungere il redirect da HTTP a HTTPS sul proxy.
This is not a recognized Bitwarden server... in un client. Il Server URL usa http, contiene un errore di digitazione oppure il certificato non è attendibile; verificare che https://vault.example.com mostri un lucchetto valido, quindi inserirlo nuovamente nelle impostazioni self-hosted del client.
/admin rifiuta la password corretta. L'hash Argon2 ha perso l'escape: ogni $ deve essere $$ in Compose, oppure è stato inserito l'hash invece del testo in chiaro che rappresenta.
Sincronizzazione lenta tra dispositivi; la console mostra WebSocket connection to 'wss://vault.example.com/notifications/hub' failed. Il proxy non inoltra gli header Upgrade/Connection; Traefik lo fa automaticamente, mentre nginx richiede le due righe di upgrade del Passaggio 1. Il vault continua a funzionare, ma la sincronizzazione avviene soltanto all'apertura. La vecchia porta dedicata 3012 non è più disponibile dalla v1.31.0, quindi non è necessaria una route WebSocket separata.
Fail2ban segnala un ban, ma l'attaccante continua a connettersi. Il ban viene applicato a 127.0.0.1 perché IP_HEADER è errato, oppure è stato inserito nella catena iptables sbagliata; impostare chain = DOCKER-USER e banaction = iptables-allports.
Aggiornamenti
Scarica la nuova immagine e ricrea il container; il volume denominato e tutti i dati persistono:
docker compose pull
docker compose up -dVaultwarden rilascia frequentemente nuove versioni. Consulta le note di rilascio del progetto invece di fissare una versione di patch, perché alcune release includono note sulla migrazione. Esegui un nuovo backup prima di ogni aggiornamento importante; puoi eseguire il rollback ripristinando l'archivio tar in un nuovo volume.
FAQ
Vaultwarden è uguale a Bitwarden?
È un server indipendente compatibile, non il server ufficiale. Vaultwarden reimplementa in Rust l'API del server Bitwarden, quindi i client ufficiali desktop, mobili, per browser e CLI funzionano tutti con Vaultwarden, usando una frazione delle risorse richieste dallo stack ufficiale. Il formato del vault è lo stesso, quindi puoi migrare in entrambe le direzioni esportando e importando i dati.
Ho davvero bisogno di HTTPS o posso usare http sulla mia LAN?
Ti serve HTTPS per qualsiasi uso che non sia un test localhost. Il vault web e le estensioni di Bitwarden usano la Web Crypto API del browser, disponibile solo in un contesto sicuro. Su http semplice, quindi, il client genera Cannot read properties of undefined e non completa mai l'accesso. L'unico indirizzo http funzionante è http://localhost. Per questo il test di ripristino del Passaggio 8 usa un tunnel SSH.
Come impedisco a persone estranee di registrarsi sul mio server?
Imposta SIGNUPS_ALLOWED: "false" nel file Compose ed esegui docker compose up -d subito dopo aver creato il tuo account. Da quel momento, aggiungi nuovi utenti tramite il pulsante Invite User in /admin. Questa funzione richiede SMTP configurato, così gli utenti ricevono il link di invito. Controlla periodicamente l'elenco degli utenti amministrativi per verificare che non siano comparsi account imprevisti.
Come eseguo il backup del mio vault Vaultwarden?
Arresta brevemente il container e archivia l'intero volume vw-data, db.sqlite3, attachments/, sends/, config.json e i file rsa_key.*. Poi copia l'archivio fuori dal server, idealmente tramite un cron notturno. Copiare il file SQLite mentre il server è in esecuzione può produrre uno snapshot corrotto, quindi esegui il backup a freddo. Soprattutto, ripristinalo una volta in un container temporaneo ed esegui l'accesso, così verifichi che il backup sia valido prima di farvi affidamento.
È davvero sicuro ospitare autonomamente le mie password?
Sì, se esegui le tre attività descritte in questa guida: HTTPS reale, registrazioni chiuse con un token amministrativo robusto e backup verificati. Il vault viene cifrato lato client con la password principale, quindi nemmeno il server vede le password in chiaro. Un db.sqlite3 sottratto è inutile senza di essa. Il compromesso è che aggiornamenti e backup diventano una tua responsabilità. Per questo Fail2ban e la procedura di ripristino non sono facoltativi. Dopo averli configurati, un esame più approfondito dei punti in cui un vault self-hosted può essere realmente attaccato è il passo successivo più utile. Poiché le singole voci sono cifrate sul client, ciò che resta da proteggere è il token amministrativo e l'archivio di backup.