LXC : un conteneur pour vaultwarden

Rédigé par Alexandre le 2023-03-07

#application #lxc #auto-hébergement

Récemment je me suis mis en tête d'équiper toute la famille d'un gestionnaire de mot de passe, mais ma solution de coeur, KeepassXC n'est pas des plus pratique. En effet, cette solution nécessite un outil tier pour synchroniser plusieurs appareils. Du coup, je me suis tourné vers Vaultwarden, le fork hébergeable de Bitwarden.

Dans cet article, je documente les étapes pour installer Vaultwarden dans un conteneur LXC via Podman.

Le conteneur LXC

Créer le conteneur en mode embarqué (nesting) :

$ lxc launch \
  images:debian/bullseye \
  ykn-vaultwarden-2310 \
  --config=security.nesting=true

Se connecter en SSH au conteneur ou via LXC :

$ lxc exec ykn-vaultwarden-2310 -- bash

Déployer Vaultwarden

Installer les prérequis :

# apt install ca-certificates

Installer podman :

# apt install podman --install-recommends

NB : je force l'installation des paquets recommandés pour contourner ma configuration d'apt qui désactive leur installation.

Créer un utilisateur dédié :

# adduser vaultwarden \
  --shell /bin/sh \
  --disabled-password

Créer un dossier pour les données :

# mkdir /srv/vaultwarden && \
  chown -R vaultwarden: /srv/vaultwarden

Utiliser l'utilisateur nouvellement créé :

# sudo -iu vaultwarden

Déployer le conteneur :

$ podman run \
  -v /srv/vaultwarden:/data \
  -e ADMIN_TOKEN="<jeton>" \
  -p 8080:80 -p 3012:3012 \
  --name vaultwarden \
  "docker.io/vaultwarden/server:latest"

NB : penser à remplacer <jeton> par un mot de passe complexe tout en le sauvegardant quelque part.

Sortir du conteneur en appuyant simultanément sur les touches Ctrl et C. Créer le service :

$ podman generate systemd \
  --new --name vaultwarden \
  > /tmp/container-vaultwarden.service

Repasser root :

$ exit

Déployer le service :

# mv /tmp/container-vaultwarden.service /etc/systemd/system/

Définir l'utilisateur du service :

# mkdir /etc/systemd/system/container-vaultwarden.service.d; \
  tee /etc/systemd/system/container-vaultwarden.service.d/override.conf <<EOF
[Service]
User=vaultwarden
Group=vaultwarden
RuntimeDirectory=user/%U
EOF

Changer l'emplacement du fichier de PID :

sed -i 's#\%t#/run/user/\%U#g' /etc/systemd/system/container-vaultwarden.service

Recharger systemd et démarrer le service :

# systemctl daemon-reload && \
  systemctl start container-vaultwarden.service

Si le service à correctement démarré, activer le service :

# systemctl enable container-vaultwarden.service

Pare-feu

Dans mon cas j'utilise nftables avec la configuration suivante :

# cat /etc/nftables.conf
#!/usr/sbin/nft -f
# Ansible managed

flush ruleset

table inet filter {
  chain input {
    type filter hook input priority 0;

    # accept any localhost traffic
    iif lo accept

    # accept traffic originated from us
    ct state established,related accept

    # accept neighbour discovery otherwise IPv6 connectivity breaks.
    ip6 nexthdr icmpv6 icmpv6 type {nd-neighbor-solicit,  nd-router-advert, nd-neighbor-advert} accept

    # include specifics rules
    include "/srv/nftables/rules_*.conf"

    # count and drop any other traffic
    counter drop
  }
}

Du coup, pour ouvrir les ports pour Vaultwarden :

# tee /srv/nftables/rules_vaultwarden.conf <<EOF
tcp dport 8080 accept
tcp dport 3012 accept
EOF

Recharger le pare-feu :

# systemctl reload nftables

Reverse proxy

Sur mon infrastructure, c'est nginx qui est en amont de Vaultwarden. Le vhost est le suivant :

server {
  listen 443 ssl http2;
  listen [::]:443 ssl http2;
  server_name vaultwarden.ykn.local;

  access_log /var/log/nginx/vaultwarden.ykn.local_access.log anonymize;
  error_log /var/log/nginx/vaultwarden.ykn.local_error.log;

  ssl_certificate /etc/ykn/rsync/letsencrypt/vaultwarden.ykn.local/fullchain.pem;
  ssl_certificate_key /etc/ykn/rsync/letsencrypt/vaultwarden.ykn.local/privkey.pem;
  ssl_trusted_certificate /etc/ykn/rsync/letsencrypt/vaultwarden.ykn.local/chain.pem;

  include /etc/nginx/snippets/ssl.conf;
  include /etc/nginx/snippets/header.conf;
  include /etc/nginx/snippets/maintenance.conf;
  include /etc/nginx/snippets/acme-challenge.conf;

  location / {
    include /etc/nginx/snippets/proxy.conf;
    proxy_pass http://ykn-vaultwarden-2310.nyx.ykn.local:8080;
  }

  location /notifications/hub {
    include /etc/nginx/snippets/proxy.conf;
    proxy_pass http://ykn-vaultwarden-2310.nyx.ykn.local:3012;
  }
}

Le contenu des différents snippets que j'utilise :

$ cat /etc/nginx/snippets/ssl.conf
ssl_session_timeout 1d;
ssl_session_cache shared:SSL:10m;
ssl_session_tickets off;

# intermediate configuration
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384;
ssl_prefer_server_ciphers off;

# OCSP stapling
ssl_stapling on;
ssl_stapling_verify on;

# replace with the IP address of your resolver
resolver 80.67.169.12 80.67.169.40 [2001:910:800::40] [2001:910:800::12] valid=60s;
resolver_timeout 2s;

# HSTS (ngx_http_headers_module is required) (15768000 seconds = 6 months)
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload";

$ cat /etc/nginx/snippets/header.conf
add_header	Referrer-Policy				"strict-origin"		always;
add_header	X-Frame-Options				"SAMEORIGIN"		always;
add_header	X-Xss-Protection			"1; mode=block"		always;
add_header	X-Content-Type-Options			"nosniff"		always;
add_header	Front-End-Https							on;

add_header	X-Download-Options			"noopen"		always;
add_header	X-Permitted-Cross-Domain-Policies	"none"			always;
#add_header	X-Robots-Tag				"none"			always;

$ cat /etc/nginx/snippets/maintenance.conf
proxy_intercept_errors on;
error_page 500 502 503 504 @maintenance;

location @maintenance {
  # Désactiver la journalisation
  access_log off;
  error_log off;

  root /var/www/maintenance;
  index /index.html;
  try_files /$uri /index.html =503;
}

$ cat /etc/nginx/snippets/acme-challenge.conf
location /.well-known/acme-challenge {
  proxy_pass http://letsencrypt.ykn.local/;
  proxy_set_header   Host infra-letsencrypt-2230.nyx.ykn.local;
}

$ cat /etc/nginx/snippets/proxy.conf
proxy_http_version	1.1;

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-Host	$host:$server_port;
proxy_set_header	X-Forwarded-Server	$host;
proxy_set_header	X-Forwarded-Port	$server_port;
proxy_set_header	X-Forwarded-Proto	$scheme;
proxy_set_header	Upgrade			$http_upgrade;
proxy_set_header	Connection		$connection_upgrade;
#add_header		Front-End-Https		on;

# Security
proxy_cookie_path	/                    "/; Secure; HttpOnly; SameSite=Strict";

Sauvegarde

Sur mon infrastructure, c'est borgmatic qui sauvegarde l'intérieur d'un conteneur. La configuration utilisée pour Vaultwarden est la suivante :

---

consistency:
  checks: 
    - frequency: 4 weeks
      name: repository
    - frequency: 2 weeks
      name: archives

hooks:
  healthchecks:
    ping_url: <url_healthcheck>
    send_logs: false

location:
  exclude_patterns: 
    - /srv/vaultwarden/icon_cache
    - /srv/vaultwarden/tmp
  repositories:
    - <dépot>
  source_directories: 
    - /srv/vaultwarden

retention:
  keep_daily: 7
  keep_monthly: 0
  keep_weekly: 4

storage:
  archive_name_format: ykn-vaultwarden-2310.nyx.ykn.local_{now}
  compression: lz4
  encryption_passphrase: <passphrase>
  ssh_command: ssh -i /etc/borgmatic/id_ed25519

NB: penser à modifier tout ce qui est entre <...>.

Mots de la fin

Afin de conclure cet article, je rappelle simplement que l'interface d'administration est accessible via /admin ; ce qui donne avec mon vhost : https://vaultwarden.ykn.local/admin. Je recommande de sécuriser cet emplacement via une authentification basique type htpasswd.