Chuyển tới nội dung chính

Triển khai từ xa riêng tư

Mẫu này chạy Libre WebUI, Ollama và Cloudflare Tunnel trên một host Docker mà không publish port app hoặc Ollama. Cloudflare Access là boundary identity bên ngoài; xác thực Libre WebUI vẫn là boundary bên trong. Work và Watchtower là lựa chọn riêng với quyền tương đương root.

Đây là topology solo một replica: SQLite, blob local mã hóa, vector embedded, điều phối local và worker bền vững tích hợp dùng chung volume data app. Không biến nó thành deployment team bằng cách đổi selector backend trong .env. Deployment team phải dùng docker-compose.team.yml (và docker-compose.team.work.yml khi Work bật), cung cấp PostgreSQL/PGVector, storage S3 có version, Redis, worker ngoài và gateway như một topology phối hợp.

Dùng deploy/private/docker-compose.yml làm điểm bắt đầu. Image mặc định là main:

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

Tag dev chỉ phù hợp cho instance development được chọn rõ, không cho deployment client thường.

Mô hình bảo mật

  • Cloudflare Access bảo vệ toàn bộ hostname, gồm /api/* và WebSocket upgrade. Không thêm public bypass path.
  • API app cần account Libre WebUI active. Operation lifecycle model và Work cần database role administrator.
  • App, Ollama, SearXNG và cloudflared chỉ dùng network Compose private. Host không publish port app.
  • SearXNG đi kèm cung cấp tìm kiếm web tùy chọn. Service chỉ nội bộ và inactive đến khi administrator bật tìm kiếm trong Settings > Search; đặt SEARXNG_SECRET trong .env trước khi start stack.
  • App chạy non-root, filesystem root read-only, không Linux capability, có no-new-privileges và limit CPU, memory, PID.
  • Work tắt đến khi thêm một file override. Container Work nhận root filesystem read-only, capability đã drop, resource limit, workspace volume và policy network default-deny.

Stack cơ sở không mount Docker socket. Khi Work bật bằng docker-compose.work-proxy.yml, socket proxy trong network nội bộ giữ socket và chỉ chuyển các phần API Work dùng (containers, images, volumes, networks, exec, info). Swarm, secrets, build và system bị block; app không cần mount socket hay thuộc group. Proxy thu hẹp API surface Docker, không thu hẹp tác động của operation được chuyển: process tạo container vẫn có thể mount host path. Xem đây là lớp hardening, không phải isolation multi-tenant.

Lựa chọn raw socket vẫn là trust boundary rộng nhất: docker-compose.work.yml và override Watchtower cho container process có thể gọi Docker API tùy ý và control host. Mount socket read-only không làm access Docker API read-only. Backup helper tích hợp từ chối inherit raw socket; chuyển Work sang proxy được lọc trước khi dựa vào backup theo lịch.

Thiết lập ban đầu

  1. Tạo operator non-root có sudo và verify login SSH bằng key trước khi tắt SSH root.
  2. Copy deploy/private/.env.example đến /opt/libre-webui/.env, đặt mode 0600, tạo secret unique và định cỡ BLOB_QUOTA_BYTES_PER_USER cho host. BLOB_QUOTA_RESERVATION_TTL_MS giới hạn reservation upload bị bỏ; mặc định một giờ.
  3. Nếu bật Work, đặt DOCKER_GID thành ID số của group sở hữu /var/run/docker.sock.
  4. Lưu token Cloudflare Tunnel tại /opt/libre-webui/secrets/tunnel-token với mode 0640 hoặc chặt hơn.
  5. Tạo app self-hosted Cloudflare Access cho toàn hostname, session 24 giờ và chỉ cho identity mong muốn. Bật Protect with Access trên route Tunnel. Nếu monitoring cần health check public, tạo app/policy riêng chỉ cho /health/live. Không thêm policy Bypass rộng: Bypass khớp sẽ thắng policy Allow.
  6. Giữ ENABLE_SIGNUP=false. Sau khi allowlist Access bảo vệ hostname, tạo administrator local đầu; database rỗng cho phép account bootstrap duy nhất đó. Chỉ bật signup trong cửa sổ có chủ ý.
  7. Cấu hình hạn chế hostname Turnstile và đặt TURNSTILE_EXPECTED_HOSTNAME thành tên public chính xác.

Start và verify:

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

Với Work, thêm override socket proxy:

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

Biến thể raw socket (docker-compose.work.yml) vẫn có với hệ quả trust ở trên.

Sau khi Access active, test command line cần service token Cloudflare Access trừ khi path có bypass hẹp. Giữ credential ngoài shell history và gửi cả hai header:

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

Request chưa xác thực đến API được bảo vệ phải trả 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

Hardening host

Directory có config sshd bổ sung và jail fail2ban. Trước áp dụng, verify session operator non-root có sudo trong terminal khác. Test sshd -t trước reload SSH.

Dùng UFW hoặc firewall tương đương để mặc định từ chối ingress và chỉ cho SSH rate-limited. Docker không publish port service trong template:

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

Giữ update bảo mật tự động active. Tắt forwarding X11, agent và TCP trừ khi có nhu cầu documented.

Backup và recovery

Trước backup, chạy inventory recovery read-only trong container active. Việc này dùng version app, environment và volume thực sự deployed. Command từ checkout host có thể kiểm tra nhầm database hoặc source.

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

Code 0 nghĩa không blocker, 1 nghĩa JSON report có blocker và 2 nghĩa command lỗi. Report chỉ có fingerprint encryption key và dấu hiệu secret tồn tại; giá trị secret không in. Lưu inventory cùng backup để so version, schema fingerprint, resource Work và exception trước recovery.

Tạo encryption key và signing key riêng bằng image chính xác. Giữ directory ngoài volume app và copy private key đến vị trí recovery riêng:

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

Key generation từ chối file có sẵn. Không ghi đè backup set: mất encryption key hoặc signing identity làm recovery proof vô dụng.

Cài script backup/recovery và unit systemd rồi bật timer:

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

Unit có thể đọc maintenance override từ /etc/libre-webui/backup.env; .env của app không load. Chỉ tạo bằng root khi cần:

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

Tại đó có thể đặt LIBRE_WEBUI_STACK_DIR, LIBRE_WEBUI_BACKUP_RETENTION_DAYS, LIBRE_WEBUI_CONTAINER_NAMELIBRE_WEBUI_BACKUP_KEY_DIR. File phải thuộc root, mode 0600. Directory key riêng phải để root đọc được trong sandbox systemd.

Đổi LIBRE_WEBUI_BACKUP_DIR cũng đổi boundary write systemd. Directory phải tồn tại và unit cần drop-in khớp. Sau khi đặt LIBRE_WEBUI_BACKUP_DIR=/srv/backups/libre-webui trong backup.env:

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

Thêm path chính xác rồi reload unit:

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

Không có ReadWritePaths=, ProtectSystem=strict ngăn write vào vị trí custom.

Service cho tối đa sáu giờ với archive lớn. Helper lấy host lock, chỉ stop app nếu đang chạy và tạo archive từ volume yên bằng image chính xác. Archive có manifest ký và payload do operator mã hóa, gồm data, runtime và config secret. Archive được verify đầy đủ trước khi metadata report publish atomic. Container maintenance read-only nhận tmpfs /tmp private để check; plaintext tạm không persist. Copy file và key ra ngoài host.

Khi Work dùng docker-compose.work-proxy.yml, recovery cũng phải chứng minh từng volume Work. Helper đọc DOCKER_HOST, tìm socket-proxy trong project Compose active và xác định một network nội bộ chung từ attachment Docker thật. Không đoán tên network vì Compose thêm prefix project. Chỉ container tạo archive tham gia và tới proxy; không nhận raw socket. Verification độc lập dùng --network none. Thiếu proxy, endpoint bất ngờ, network ngoài/mơ hồ hoặc raw socket sẽ fail trước khi app dừng và archive publish.

Test recovery vào volume mới:

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

Helper từ chối volume hoặc target config có sẵn, verify archive và inventory trong storage disposable, rồi copy data vào volume mới và ghi runtime.json, secrets.json với permission private. Nó không đổi hay start stack active. Kiểm tra config và test bằng stack biệt lập.

Model Ollama có thể download lại. Volume Docker Work, PVC Kubernetes Work và folder Work host nằm ngoài directory app nên cần snapshot và retention riêng.

Cập nhật

Libre WebUI giữ state dù tag image mutable. Compose cơ sở luôn loại app khỏi Watchtower. Chỉ update như hành động operator phối hợp:

  1. Ghi ID image đang chạy và digest thay thế immutable đã review.
  2. Chạy libre-webui recovery-check, start backup và yêu cầu archive/report mới.
  3. Đặt LIBRE_WEBUI_IMAGE thành digest, pull và recreate chỉ libre-webui; không xóa data volume.
  4. Yêu cầu /health/ready, login, session/history, document và test Work. Nếu lỗi, quay lại digest và giữ state lỗi cùng backup.

Trình tự host cố ý manual. Kiểm tra cặp .lwb/.json mới nhất trước pull:

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}}'

Override Watchtower có socket chỉ dành cho sidecar được label:

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

Watchtower kiểm tra Ollama và SearXNG mỗi 30 phút. Data model Ollama giữ trong named volume và config SearXNG trong host mount. Watchtower không update Libre WebUI, cloudflared, socket proxy Work hoặc sandbox Work. Deployment client theo main; instance thử nghiệm có thể chọn :dev nhưng app vẫn cần update manual bắt buộc qua backup verified. Không nối stack solo private này vào service persistence team; triển khai topology team đầy đủ.