Aller au contenu principal

Déploiement distant privé

Cette architecture exécute Libre WebUI, Ollama et Cloudflare Tunnel sur un même hôte Docker sans publier les ports de l’application ni d’Ollama. Cloudflare Access constitue la frontière d’identité externe ; l’authentification Libre WebUI reste la frontière interne. Work et Watchtower sont des options distinctes, équivalentes à un accès root.

Ce modèle correspond à la topologie solo monoréplique : SQLite, blobs locaux chiffrés, vecteurs intégrés, coordination locale et worker de tâches intégré partagent le volume de données de l’application. N’en faites pas un déploiement en équipe en modifiant les sélecteurs du backend dans .env. Les déploiements en équipe doivent utiliser le fichier docker-compose.team.yml du dépôt (et docker-compose.team.work.yml lorsque Work est activé), qui provisionne comme une topologie coordonnée PostgreSQL/PGVector, un stockage S3 versionné, Redis, un worker externe et la passerelle.

Prenez comme point de départ deploy/private/docker-compose.yml. Il utilise par défaut l’image main :

LIBRE_WEBUI_IMAGE=ghcr.io/libre-webui/libre-webui:main

L’étiquette dev convient à une instance de développement explicitement choisie, pas à l’instance cliente par défaut.

Modèle de sécurité

  • Cloudflare Access protège l’intégralité du nom d’hôte, notamment /api/* et les mises à niveau WebSocket. N’ajoutez aucun chemin public de contournement.
  • Libre WebUI exige un compte valide pour les API de l’application. Les opérations de cycle de vie des modèles et de Work exigent que le rôle actuel dans la base de données soit administrateur.
  • L’application, Ollama, SearXNG et cloudflared utilisent uniquement un réseau Compose privé. L’hôte ne publie aucun port de l’application.
  • Le service SearXNG intégré alimente la recherche web facultative. Il est exclusivement interne et reste inactif jusqu’à ce qu’un administrateur active la recherche sous Paramètres > Recherche ; définissez SEARXNG_SECRET dans .env avant de démarrer la pile.
  • L’application s’exécute sans root, avec un système de fichiers racine en lecture seule, aucune capacité Linux, no-new-privileges et des limites de CPU, de mémoire et de PID.
  • Work est désactivé sauf si l’un de ses remplacements est inclus. Lorsqu’il est activé, ses conteneurs ajoutent leur propre système de fichiers racine en lecture seule, la suppression des capacités, des limites de ressources, un volume d’espace de travail et une politique réseau de refus par défaut.

La pile de base ne monte aucun socket Docker. L’activation de Work avec docker-compose.work-proxy.yml préserve ce principe : un proxy de socket sur un réseau interne détient le socket et ne transmet que les sections de l’API utilisées par Work (conteneurs, images, volumes, réseaux, exec, informations) ; les points de terminaison liés à swarm, aux secrets, au build et au système sont refusés par le proxy, et l’application n’a besoin ni de monter le socket ni d’appartenir à son groupe. Le proxy réduit la surface de l’API Docker, mais pas l’impact potentiel de ce qu’il transmet : quiconque peut créer des conteneurs peut encore monter des chemins de l’hôte. Considérez-le donc comme une véritable couche de renforcement, et non comme une isolation multilocataire.

Les solutions de remplacement avec socket brut restent la plus grande frontière de confiance : les remplacements docker-compose.work.yml et Watchtower donnent à un processus de conteneur la possibilité d’émettre n’importe quel appel à l’API Docker, et donc de contrôler l’hôte. Monter le socket en lecture seule ne rend pas l’accès à l’API Docker lui-même accessible en lecture seule. L’outil de sauvegarde intégré refuse d’hériter d’un socket Docker brut ; migrez Work vers le proxy filtré avant de vous fier aux sauvegardes intégrées planifiées.

Amorçage

  1. Créez un opérateur sudo sans root et vérifiez la connexion SSH par clé avant de désactiver la connexion SSH de root.
  2. Copiez deploy/private/.env.example vers /opt/libre-webui/.env, appliquez le mode 0600, générez des secrets uniques et dimensionnez BLOB_QUOTA_BYTES_PER_USER pour l’hôte. BLOB_QUOTA_RESERVATION_TTL_MS fait expirer les réservations de téléversement abandonnées ; sa valeur par défaut est d’une heure.
  3. Si Work doit être activé, définissez DOCKER_GID sur le groupe numérique propriétaire de /var/run/docker.sock.
  4. Stockez le jeton Cloudflare Tunnel dans /opt/libre-webui/secrets/tunnel-token, avec le mode 0640 ou plus strict.
  5. Créez une application auto-hébergée Cloudflare Access pour le nom d’hôte complet, utilisez une session de 24 heures et n’autorisez que les identités prévues. Activez Protect with Access sur la route du tunnel. Si la surveillance exige un contrôle d’état public, créez une application ou une politique distincte, limitée uniquement au chemin /health/live. N’ajoutez jamais une politique Bypass générale à l’application principale : les politiques Bypass correspondantes neutralisent sa politique Allow.
  6. Conservez ENABLE_SIGNUP=false. Une fois le nom d’hôte protégé par la liste d’autorisation Access, créez le premier administrateur local ; une base de données vide autorise automatiquement cet unique compte d’amorçage. N’activez l’inscription que pour une future période d’inscription délibérée.
  7. Configurez les restrictions de nom d’hôte Turnstile et définissez TURNSTILE_EXPECTED_HOSTNAME sur le nom d’hôte public exact.

Démarrez et vérifiez :

cd /opt/libre-webui
docker compose config --quiet
docker compose up -d
docker compose ps

Pour activer Work, incluez délibérément le remplacement par proxy de socket :

docker compose -f docker-compose.yml -f docker-compose.work-proxy.yml up -d

La variante avec socket brut (docker-compose.work.yml) reste disponible pour les déploiements qui en ont besoin, avec les conséquences de confiance décrites plus haut.

Une fois Access actif, les tests rapides en ligne de commande nécessitent un jeton de service Cloudflare Access, sauf si le chemin exact dispose d’un contournement étroit. Stockez les identifiants hors de l’historique du shell et envoyez les deux en-têtes :

curl --fail --silent --show-error \
-H "CF-Access-Client-Id: $CF_ACCESS_CLIENT_ID" \
-H "CF-Access-Client-Secret: $CF_ACCESS_CLIENT_SECRET" \
https://your-hostname.example/api/auth/system-info

Une requête non authentifiée à une API d’application protégée doit renvoyer 401 :

curl --output /dev/null --write-out '%{http_code}\n' \
-H "CF-Access-Client-Id: $CF_ACCESS_CLIENT_ID" \
-H "CF-Access-Client-Secret: $CF_ACCESS_CLIENT_SECRET" \
https://your-hostname.example/api/work/tasks

Renforcement de l’hôte

Le répertoire comprend un fragment de configuration sshd et une prison fail2ban. Avant d’appliquer le fragment sshd, vérifiez dans un autre terminal une session sudo distincte et sans root. Testez la configuration avec sshd -t avant de recharger SSH.

Utilisez UFW (ou un pare-feu équivalent) pour refuser par défaut le trafic entrant et n’autoriser que SSH avec limitation du débit. Dans ce modèle, Docker ne publie aucun port de service :

ufw default deny incoming
ufw default allow outgoing
ufw limit OpenSSH
ufw enable

Maintenez les mises à niveau de sécurité automatiques activées. Désactivez les transferts X11, d’agent et TCP, sauf nécessité documentée du déploiement.

Sauvegardes et reprise

Avant une sauvegarde, exécutez l’inventaire de reprise en lecture seule dans le conteneur du déploiement actif. Vous utiliserez ainsi exactement la version de l’application déployée, son environnement et son volume de données monté. Une commande lancée depuis une copie de travail sur l’hôte peut inspecter la mauvaise base de données ou exécuter un code source différent de l’image déployée.

docker exec libre-webui \
libre-webui recovery-check --json --data-dir /app/backend/data

L’état de sortie 0 indique qu’aucun obstacle à la préparation de la reprise n’a été détecté ; 1 signifie que le rapport JSON contient des obstacles et 2 que la commande n’a pas pu s’exécuter. Le rapport contient uniquement une empreinte de la clé de chiffrement et des indicateurs de présence des secrets ; il n’affiche jamais une clé ni la valeur d’un autre secret. Conservez l’inventaire avec la sauvegarde correspondante afin que les opérateurs puissent comparer la version de l’application, l’empreinte du schéma, les ressources Work attendues et les exclusions avant une restauration.

Créez des clés dédiées de chiffrement et de signature des sauvegardes au moyen de l’image exactement déployée. Conservez ce répertoire hors du volume de l’application et copiez la clé de chiffrement et la clé privée de signature vers un emplacement de reprise protégé distinct :

install -d -m 0700 /etc/libre-webui/backup-keys
image_ref=$(docker inspect libre-webui --format '{{.Image}}')
docker run --rm --user 0:0 --read-only --network none --cap-drop ALL \
--security-opt no-new-privileges \
--mount type=bind,src=/etc/libre-webui/backup-keys,dst=/backup-keys \
--entrypoint /usr/local/bin/libre-webui "$image_ref" \
backup keygen \
--directory /backup-keys

La génération de clés refuse les fichiers de sortie existants. Ne générez jamais de nouvelles clés sur un jeu de sauvegardes existant : la perte de la clé de chiffrement des archives ou de l’identité de signature rend la preuve de reprise correspondante inutilisable.

Installez les scripts de sauvegarde et de restauration ainsi que les unités systemd fournis, puis activez le minuteur :

install -d -m 0700 /var/backups/libre-webui
install -m 0750 deploy/private/libre-webui-backup \
/usr/local/sbin/libre-webui-backup
install -m 0750 deploy/private/libre-webui-restore \
/usr/local/sbin/libre-webui-restore
install -m 0644 deploy/private/libre-webui-backup.{service,timer} \
/etc/systemd/system/
systemctl daemon-reload
systemctl enable --now libre-webui-backup.timer

L’unité lit facultativement les remplacements réservés à la maintenance depuis /etc/libre-webui/backup.env ; elle ne charge pas le fichier .env de l’application. Ne créez le fichier en tant que root que si un remplacement est nécessaire :

install -d -m 0750 /etc/libre-webui
install -m 0600 /dev/null /etc/libre-webui/backup.env

LIBRE_WEBUI_STACK_DIR, LIBRE_WEBUI_BACKUP_RETENTION_DAYS, LIBRE_WEBUI_CONTAINER_NAME et LIBRE_WEBUI_BACKUP_KEY_DIR peuvent y être définis directement. Conservez le fichier appartenant à root et avec le mode 0600. Un répertoire de clés personnalisé doit rester lisible par root dans le bac à sable systemd.

Modifier LIBRE_WEBUI_BACKUP_DIR modifie aussi la frontière d’écriture de systemd. Le répertoire doit exister avant le démarrage du service et l’unité a besoin d’un fragment correspondant. Par exemple, après avoir défini LIBRE_WEBUI_BACKUP_DIR=/srv/backups/libre-webui dans backup.env :

install -d -m 0700 /srv/backups/libre-webui
systemctl edit libre-webui-backup.service

Ajoutez ce chemin exact dans l’éditeur, puis rechargez l’unité :

[Service]
ReadWritePaths=/srv/backups/libre-webui
systemctl daemon-reload
systemctl start libre-webui-backup.service

Sans l’entrée ReadWritePaths= correspondante, ProtectSystem=strict empêche à juste titre le minuteur d’écrire dans un emplacement personnalisé.

Le service de sauvegarde dispose d’un délai maximal de six heures pour les grandes archives. L’outil d’assistance acquiert un verrou sur l’hôte, n’arrête l’application que si elle était déjà en cours d’exécution et crée l’archive à partir du volume au repos avec l’image exactement déployée. L’archive possède un manifeste signé et une charge utile chiffrée par l’opérateur ; elle comprend le répertoire de données ainsi que la configuration d’exécution et les secrets nécessaires à l’ouverture de cet état. L’outil vérifie ensuite indépendamment l’intégralité de l’archive avant de publier atomiquement son rapport de métadonnées. Ses conteneurs de maintenance en lecture seule disposent d’un tmpfs /tmp privé et inscriptible pour l’inspection de SQLite et la vérification authentifiée de l’archive ; aucun texte en clair temporaire ne persiste dans la couche du conteneur. Copiez hors de l’hôte les deux fichiers et les clés de reprise protégées séparément.

Lorsque Work utilise docker-compose.work-proxy.yml, la reprise doit aussi prouver que chaque volume Work référencé par la base de données existe encore. L’outil lit le DOCKER_HOST de l’application déployée, localise le service de proxy de socket dans le même projet Compose actif et découvre leur unique réseau interne partagé à partir des connexions réseau réelles de Docker. Compose préfixe ce réseau avec le nom du projet : ne configurez ni n’inscrivez donc en dur un nom de réseau supposé. Seul le conteneur de création de l’archive rejoint ce réseau interne et peut atteindre le proxy filtré ; il ne reçoit aucun socket brut. La vérification indépendante de l’archive continue avec --network none. Un proxy absent, un point de terminaison inattendu, un réseau partagé externe ou ambigu, ou un montage de socket brut provoque un échec avant l’arrêt de l’application et avant la publication d’une archive.

Testez la reprise vers un nouveau volume sans remplacer le volume actif :

LIBRE_WEBUI_RESTORE_IMAGE="$image_ref" \
libre-webui-restore \
/var/backups/libre-webui/libre-webui-integrated-YYYYMMDDTHHMMSSZ.lwb \
libre-webui-restore-drill

L’outil de restauration refuse un volume ou une cible de configuration existants, vérifie l’archive et son inventaire de reprise interne dans un stockage jetable, puis copie les données vers le nouveau volume et écrit les fichiers récupérés runtime.json et secrets.json avec des autorisations privées. Il ne recâble ni ne démarre jamais la pile active. Examinez la configuration récupérée, mettez à jour délibérément les valeurs propres au déploiement et testez le volume restauré avec une pile isolée.

Les modèles Ollama peuvent être récupérés de nouveau. Les volumes Work Docker, les PVC Work Kubernetes et les dossiers Work liés à l’hôte ne font pas partie du répertoire de données de l’application et exigent leurs propres instantanés coordonnés et leur propre politique de conservation.

Mises à jour

Libre WebUI possède un état, même lorsque l’étiquette de son image est modifiable. Le fichier Compose de base marque en permanence l’application comme exclue de Watchtower. Ne la mettez à niveau que dans le cadre d’une action coordonnée de l’opérateur :

  1. Notez l’identifiant de l’image en cours d’exécution et résolvez le remplacement vérifié vers un digest immuable.
  2. Exécutez libre-webui recovery-check, démarrez le service de sauvegarde et exigez la création d’une archive et d’un rapport de vérification avant de poursuivre.
  3. Définissez LIBRE_WEBUI_IMAGE sur le digest vérifié, récupérez-le et recréez uniquement libre-webui avec Docker Compose. Ne supprimez ni ne recréez son volume de données.
  4. Exigez la réussite de /health/ready, de la connexion, de la session et de l’historique, de la récupération des documents et des tests rapides Work. Sinon, revenez au digest de l’image enregistré ; conservez l’état en échec et la sauvegarde vérifiée pour le diagnostic.

La séquence côté hôte est volontairement manuelle. Ne remplacez le digest qu’après l’avoir examiné et inspectez la paire .lwb et .json la plus récente avant la récupération :

docker inspect libre-webui --format '{{.Config.Image}} {{.Image}}'
docker exec libre-webui \
libre-webui recovery-check --json --data-dir /app/backend/data
systemctl start libre-webui-backup.service
systemctl --no-pager --full status libre-webui-backup.service
ls -lt /var/backups/libre-webui/libre-webui-integrated-* | head

# Set LIBRE_WEBUI_IMAGE=ghcr.io/libre-webui/libre-webui@sha256:REVIEWED_DIGEST
# in the root-owned .env, then recreate only the application.
docker compose pull libre-webui
docker compose up -d --no-deps libre-webui
docker inspect libre-webui --format '{{.State.Health.Status}} {{.Image}}'

Le remplacement Watchtower facultatif portant le socket reste disponible uniquement pour les sidecars explicitement étiquetés dans le fichier de base :

docker compose \
-f docker-compose.yml \
-f docker-compose.watchtower.yml \
up -d

Watchtower vérifie Ollama et SearXNG toutes les 30 minutes. Les données des modèles Ollama restent dans leur volume nommé et la configuration SearXNG reste dans son montage lié. Il ne met pas à jour Libre WebUI, cloudflared, le proxy du socket Work ni les bacs à sable Work. Un déploiement client suit main ; une instance expérimentale peut sélectionner :dev, mais l’application exige toujours la même mise à niveau manuelle conditionnée par une sauvegarde. Ne rattachez jamais cette pile privée individuelle à des services de persistance d’équipe ; déployez plutôt la topologie d’équipe complète.