AcidDemon/ansible-ente-photos-debian

GitHub: AcidDemon/ansible-ente-photos-debian

该项目提供一个 Ansible playbook,用于在 Proxmox VE + Debian 13 环境中自动部署带有分层加固架构的自托管端到端加密 Ente Photos 相册服务。

Stars: 0 | Forks: 0

# ansible-ente-photos-debian ![Ansible](https://img.shields.io/badge/Ansible-2.16%2B-EE0000?logo=ansible&logoColor=white) ![Debian](https://img.shields.io/badge/Debian-13%20trixie-A81D33?logo=debian&logoColor=white) ![Ente Photos](https://img.shields.io/badge/Ente-Photos-FA5C2F?logo=data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHZpZXdCb3g9IjAgMCAyNCAyNCI+PC9zdmc+&logoColor=white) ![Podman](https://img.shields.io/badge/rootless-Podman%20%2B%20Quadlet-892CA0?logo=podman&logoColor=white) ![gVisor + Coraza WAF](https://img.shields.io/badge/edge-gVisor%20%2B%20Coraza%20WAF-4B275F) ![Proxmox VE](https://img.shields.io/badge/Proxmox-VE-E57000?logo=proxmox&logoColor=white) ![Tested with Vagrant](https://img.shields.io/badge/tested%20with-Vagrant%20(static%20only)-1868F2?logo=vagrant&logoColor=white) [![License: MIT](https://img.shields.io/github/license/AcidDemon/ansible-ente-photos-debian?color=blue)](LICENSE) [![Last commit](https://img.shields.io/github/last-commit/AcidDemon/ansible-ente-photos-debian)](https://github.com/AcidDemon/ansible-ente-photos-debian/commits/main) [![Repo size](https://img.shields.io/github/repo-size/AcidDemon/ansible-ente-photos-debian)](https://github.com/AcidDemon/ansible-ente-photos-debian) 用于在运行于 Proxmox VE 上的 Debian 13 虚拟机中部署 **Ente Photos** 的 Ansible playbook。 **应用层采用 rootless Podman + Quadlet**(systemd *用户* 单元),以常驻的 `entestack` 用户(UID 1500)身份运行: Postgres (`ente-db`)、MinIO (`ente-minio`)、Ente museum API 服务器 (`ente-museum`),以及一个本地 nginx mTLS 入口 (`ente-web`),该入口托管静态 Ente Web 应用(photos、accounts、cast、albums)并将 `/api` 流量代理至 museum。 **边缘层是一个 rootful 的、由 gVisor 沙箱保护的 Caddy + Coraza WAF**(`edge`,即在 `runsc` 下运行的一个 *系统级* Quadlet),负责在 80/443 端口上终止 TLS。它必须以 rootful 方式运行:gVisor 需要 rootful cgroup,并且 rootless 的 `runsc` 在 cgroup v2 下无法正常工作。边缘节点运行在 podman 网桥上 —— **而不是** `--network host`,因为 `runsc` 在主机网络下没有可用的回环接口 —— 并通过 `host.containers.internal` 访问 rootless 应用层。所有 edge→app 流量均使用 mTLS(内部 CA)。应用层通过名称在 `ente-internal` Quadlet 网络上解析对等节点。 Ente Photos 是**客户端端到端加密**的:服务器永远不会看到明文照片或密钥。因此,该技术栈**不包含服务器端 ML、LDAP/OIDC SSO、ClamAV 防病毒以及文档编辑功能** —— 这些均属于客户端关注的事项或明确在范围之外。 ## 架构 ``` Internet │ TLS 443 (Caddy + Coraza WAF, gVisor/runsc, rootful) ▼ [edge container] ──mTLS──▶ [ente-web:8443] (rootless nginx, entestack user) │ ┌────┴─────────────────────────────────────────┐ │ photos / accounts / cast / albums (static) │ │ api.DOMAIN → reverse proxy → ente-museum │ └──────────────────────────────────────────────┘ │ │ [ente-museum] [ente-minio:9000] (museum API) (S3 photo storage) │ [ente-db:5432] (Postgres 17) DB + MinIO ports: internal ente-internal network only; NOT published externally. Edge ↔ ente-web: mTLS with internal CA (certs generated by internal_pki role). ``` ## 前置条件 - **Proxmox VE** 以及一个 Debian 13 cloud-init 模板(VMID 为 `9999`);请将 `pve_template_vmid` 设置为该模板。 - 位于 `files/ssh/ente-deploy`(私钥,已被 git 忽略)和 `files/ssh/ente-deploy.pub`(公钥,已提交)的**部署 SSH 密钥对**。使用以下命令生成: `ssh-keygen -t ed25519 -f files/ssh/ente-deploy`。 - **direnv**(执行 `direnv allow`)或手动设置: export ANSIBLE_CONFIG=$PWD/ansible.cfg unset ANSIBLE_ROLES_PATH ANSIBLE_COLLECTIONS_PATH ANSIBLE_COLLECTIONS_PATHS `.envrc` 会取消设置这些变量 —— 如果它们在你的 shell profile 中被设置,将会覆盖项目配置并破坏 collection/role 的解析。或者, 在执行每个 `ansible-playbook` 命令时添加前缀: env -u ANSIBLE_ROLES_PATH -u ANSIBLE_COLLECTIONS_PATH \ -u ANSIBLE_COLLECTIONS_PATHS ANSIBLE_CONFIG=$PWD/ansible.cfg - **Collections:** `ansible-galaxy collection install -r requirements.yml` - **虚拟机配置:** 至少需要 **8 GB 内存和 4 个 vCPU**。请参阅下方的配置说明。 ## 机密信息 编辑 `inventory/group_vars/all/vault.yml` —— 它作为一个**明文存根**发布,包含 `change-me-*` 的值。为每个字段设置真实值,然后进行加密: ``` ansible-vault encrypt inventory/group_vars/all/vault.yml ``` 如果仍存在任何 `change-me-*` 占位符,`ente` 角色中的预检 `assert.yml` 将拒绝运行。Vagrant 实验室不受此限制,因为 `inventory/vagrant.yml` 将每一个 `vault_*` 键覆盖为一次性实验值作为主机变量,因此在实验环境中不会有 `change-me-*` 占位符进入预检环节。 一旦加密,**每一次**运行都需要使用 `--ask-vault-pass`(包括 Vagrant 实验室),因为 `group_vars/all/vault.yml` 会为所有清单(inventory)加载。配置中**没有 `.vault_pass` 文件,也没有 `vault_password_file`**;密码仅在运行时提供。 | Vault 键 | 用途 | 生成方法 | |---|---|---| | `vault_pve_api_token_secret` | Proxmox API token secret(仅用于配置) | Proxmox UI → Datacenter → API Tokens | | `vault_ente_db_password` | Postgres 密码 | `openssl rand -base64 32` | | `vault_ente_minio_root_password` | MinIO root 密码 | `openssl rand -base64 32` | | `vault_ente_s3_access_key` | MinIO 限域访问密钥 ID | `openssl rand -hex 20` | | `vault_ente_s3_secret_key` | MinIO 限域密钥 | `openssl rand -base64 32` | | `vault_museum_key_encryption` | Museum 密钥加密密钥 | `openssl rand -base64 32` | | `vault_museum_key_hash` | Museum 密钥哈希密钥 | `openssl rand -base64 64` | | `vault_museum_jwt_secret` | Museum JWT 签名 secret | `openssl rand -base64 32` | | `vault_smtp_password` | SMTP 密码(仅在 `ente_smtp_enabled: true` 时使用) | 来源于你的 SMTP 提供商 | | `vault_ente_backup_password` | restic 备份加密密码 | `openssl rand -base64 32` | | `vault_tls_cert` / `vault_tls_key` | 自带(BYO)TLS 材料(仅在 `tls_mode: byo` 时) | 运维人员提供 | ## 运行矩阵 ### 本地 Vagrant (libvirt) ``` vagrant up ansible-playbook -i inventory/vagrant.yml site.yml --ask-vault-pass ``` ### 远程测试虚拟机 ``` ansible-playbook -i inventory/test.yml site.yml --ask-vault-pass ``` ### 生产环境(包括在 Proxmox 上创建虚拟机) ``` ansible-playbook -i inventory/hosts.yml site.yml \ -e provision_vm_enabled=true --ask-vault-pass ``` ### 按角色测试 有关完整列表以及每个测试的断言内容,请参阅 `tests/README.md`。运行全部测试: ``` for t in os_base podman_host internal_pki ente-storage ente \ caddy_edge fail2ban firewall_coexistence \ monitoring ente_backup; do ansible-playbook -i inventory/vagrant.yml tests/test-$t.yml --ask-vault-pass || break done ansible-playbook -i inventory/vagrant.yml tests/test-integration.yml --ask-vault-pass ``` ## 关键变量 所有变量都位于 `inventory/group_vars/all/main.yml`(唯一事实来源); 角色内部的可调参数位于 `roles//defaults/main.yml` 中。 | 变量 | 默认值 | 说明 | |---|---|---| | `ente_domain` | `photos.example.com` | 基础 FQDN;派生出五个子域名:`photos.`、`accounts.`、`cast.`、`albums.`、`api.` | | `tls_mode` | `acme` | `acme` = Let's Encrypt;`byo` = 运维人员通过 vault 提供证书/密钥;`selfsigned` = 实验室/Vagrant | | `edge_tls_profile` | `intermediate` | `intermediate`(TLS 1.2+)或 `modern`(仅限 TLS 1.3;需设置 `edge_tls_modern_attested: true`) | | `waf_mode` | `detection` | Coraza/CRS 模式 —— **在执行 `enforce` 前先在 `detection` 模式下浸泡观察** | | `gvisor_platform` | `systrap` | gVisor 平台 —— **切勿使用 `kvm`**(在克隆的虚拟机上没有嵌套虚拟化) | | `web_image` | `ghcr.io/ente/web@sha256:…`(已固定) | 官方预构建 Ente Web 应用镜像;在 `ente-web` mTLS nginx 背后提供服务 | | `ente_api_origin` | `https://api.{{ ente_domain }}` | 在容器启动时注入 Web 应用的公共 museum API URL(运行时为 `ENTE_API_ORIGIN`) | | `museum_image` | `ghcr.io/ente/server@sha256:…`(已固定) | Museum API 容器镜像 | | `minio_image` | `docker.io/minio/minio:latest` | MinIO 镜像 —— **在生产环境前请固定为 `@sha256`** | | `ente_smtp_enabled` | `false` | 启用 SMTP 以发送 OTP;为 false 时,OTP 验证码将显示在 `podman logs ente-museum` 中 | | `ente_s3_bucket` | `ente` | 用于照片存储的 MinIO bucket 名称 | | `ente_max_upload_bytes` | `5368709120` | 每个对象 5 GiB(nginx client_body + museum 限制) | | `fail2ban_enabled` | `true` | 通过 `ente-museum` 监狱进行暴力破解隔离 | | `monitoring_enabled` | `false` | 在 loopback 上开启 Prometheus exporters | | `ente_backup_enabled` | `false` | 对 `ente_backup_repo` 进行 restic 备份 | | `management_cidrs` | `["10.0.0.0/8"]` | 允许访问 SSH (22) 的 CIDR;同时也是 fail2ban 的 `ignoreip` | | `firewall_allow_tcp` | `[80, 443]` | base-debian 为边缘层开放的公共 TCP 端口 | | `firewall_trust_iifnames` | `["podman*"]` | base-debian 在 INPUT 链上信任的接口(podman 网桥) | | `firewall_forward_policy` | `accept` | base-debian FORWARD 策略(容器出口流量) | | `provision_vm_enabled` | `false` | 从 Proxmox 模板克隆并配置新虚拟机 | | `pve_vm_cores` / `pve_vm_memory` / `pve_vm_disk_size` | `4` / `8192` / `100G` | 虚拟机配置 —— 见说明 | | `pve_validate_certs` | `false` | 容忍 Proxmox 自签名的 API 证书;对于 CA 签发的 PVE 证书则设置为 `true` | ### 虚拟机配置(≥8 GB) 默认的 `pve_vm_memory: 8192`(8 GB)和 `pve_vm_cores: 4` 是**底线,而不是建议**。同时运行 Postgres + MinIO + museum + nginx 以及基于 gVisor 沙箱的 Caddy/Coraza 边缘节点,无法挤入 4 GB 内存中。**xcaddy 边缘构建**(Go 工具链 + 模块缓存)属于内存密集型任务,在首次运行 `ansible-playbook` 时峰值可能达到 1–2 GB;Web 层使用的是预构建的 `ghcr.io/ente/web` 镜像,因此不需要在虚拟机上进行 Node/Rust Web 构建。 **磁盘:** 在计算用户数据之前,请为系统磁盘预留至少 **20 GB** 空间。仅镜像构建(xcaddy Go 工具链缓存 + node_modules)和容器镜像层就会占用数 GB 空间。较小的根磁盘甚至无法构建边缘镜像 —— `podman build` 会因 `no space left on device` 而失败。**MinIO 负责存储照片库**,因此请根据你预期的照片集大小来调整 `pve_vm_disk_size(默认为 `100G`)。 ## 操作系统加固(外部)—— 关键集成限制 完整的操作系统加固由同类的 [`ansible-hardening-debian`](https://github.com/acidnetworks/ansible-hardening-debian) 代码库负责,并作为一个**独立的编排层**运行。本代码库仅附带一个浅层的 `os_base`。请按以下顺序运行: 1. `provision-vm.yml`(或使用现有的 Debian 13 主机)。 2. `ansible-hardening-debian`:先执行 `bootstrap.yml`,然后执行其 `site.yml`。 3. 本代码库的 `site.yml`,使用加固代码库的部署用户通过密钥进行连接。 ## 隔离与 rootless 说明 - **应用层(rootless):** `ente-db`、`ente-minio`、`ente-museum` 和 `ente-web` 以常驻的 `entestack` 用户(UID 1500)身份运行。它们的 Quadlets 位于 `/home/entestack/.config/containers/systemd/`;请以 `entestack` 用户身份使用 `systemctl --user` 进行管理。`podman-auto-update.timer` 在用户作用域内运行。 - **边缘层(rootful):** `edge` 单元是位于 `/etc/containers/systemd/` 中的**系统级** Quadlet;请以 root 身份使用 `systemctl` 进行管理。它直接绑定 80/443 端口(不需要 unprivileged-port sysctl)并在 `runsc` 下运行。 - **边缘层 ↔ 应用层网络:** rootful 边缘层通过主机网关(`host.containers.internal`)访问 rootless 应用层。`ente-web` 被发布在 `0.0.0.0:8443` —— 但是 base-debian 的 `base_filter` INPUT 链**会丢弃公共接口上该端口的流量**(只有内部 podman 网桥通过 `firewall_trust_iifnames` 才能访问它)。edge→`ente-web` 的跳转使用 mTLS。 **DB 和 MinIO 永远不会被发布(暴露端口)。** - `aardvark-dns` 被显式安装(在 Debian 上它只是 netavark 的一个 `Recommends` 依赖项);它是 `ente-internal` 上进行容器名称解析所必需的。 ## Ente 专属操作说明 ### 生产环境前的镜像及引用固定 `museum_image` 和 `minio_image` 默认使用基于标签的引用(`:latest` 和 `:17-alpine`),并在 `group_vars/all/main.yml` 中带有 `# TODO pin @sha256` 的注释。 如果 `tls_mode` 不为 `selfsigned`,`ente` 角色中的 `assert.yml` 预检**将强制要求固定摘要 —— 在非自签名部署中,如果任何一个镜像缺少 `@sha256:` 摘要,它将会中止运行。 `museum_image` 和 `web_image` 在发布时**已经被固定**为实时测试期间解析出的摘要。要在升级时重新固定: ``` # museum skopeo inspect docker://ghcr.io/ente/server:latest | jq -r .Digest # → @sha256:… # web skopeo inspect docker://ghcr.io/ente/web:latest | jq -r .Digest # → @sha256:… # minio (默认仍为基于 tag 的) skopeo inspect docker://docker.io/minio/minio:latest | jq -r .Digest ``` ### 在 Vagrant/libvirt 上进行了实时测试(2026-06-27) 完整的栈已部署,并且完整的测试套件在 Debian 13 Vagrant 虚拟机上运行成功(`tls_mode: selfsigned`)。已确认的端到端验证:museum 启动并且 `api./ping` 返回 `pong`;mTLS 入口强制执行边缘客户端证书验证(无证书 → HTTP 400,有证书 → 200);所有五个边缘 vhosts 均通过 TLS 响应;gVisor `runsc` 边缘运行 Caddy+Coraza;fail2ban 的三个 jails 处于活跃状态 (`caddy-coraza` jail 匹配 `_SYSTEMD_UNIT=edge.service`,museum jail 匹配 `_SYSTEMD_USER_UNIT=ente-museum.service`)。早期的“集成未知项”现在已在代码库中解决: - **Web 层** —— 从虚拟机上的源码构建切换为**官方预构建的 `ghcr.io/ente/web`** 镜像(源码构建需要 Rust+wasm-pack 工具链和 npm,且 endpoint 通过 `ENTE_API_ORIGIN` 在运行时注入)。 `ente-web` nginx 负责 mTLS 终止,并根据 Host 路由到其各自的应用端口。 - **Museum 配置** —— 已确认 museum 会读取 **`/museum.yaml`**(合并至镜像的 `local.yaml` 之上);请**不要**设置 `ENVIRONMENT=production`。健康检查路由为 `/ping`(通过 `wget` 检查;Alpine 镜像没有 `curl`)。 针对你自己的流量,仍有两个值得调整的项目(已在代码库中标出): 1. **Fail2ban museum failregex** (`roles/fail2ban/templates/ente-museum-filter.conf.j2`) 是刻意设置得比较宽松的 —— 一旦你观察到几次来自 `podman logs ente-museum` 的真实认证失败日志行,请针对它们进行收紧。 2. **Museum 指标端口**,用于可选的 Prometheus 抓取 (`museum_metrics_enabled`,默认关闭) —— 在启用前,请针对已固定的 `museum_image` 确认该端口。 ### 不使用 SMTP 的 OTP 当 `ente_smtp_enabled: false`(默认值)时,Ente Photos 仍然可以工作,但 账号验证和登录的 OTP 验证码**不会通过电子邮件发送** —— 它们会被打印到 museum 的 stdout 中。使用以下命令获取它们: ``` sudo -u entestack podman logs ente-museum 2>&1 | grep -i otp ``` 在生产环境中启用 SMTP(`ente_smtp_enabled: true` + `vault_smtp_password`),以便用户无需服务器访问权限即可注册和登录。 ### Proxmox Python 解释器 ``` [proxmox_hosts] pve.example.lan ansible_python_interpreter=/opt/ansible-venv/bin/python3 ``` 或者作为 `inventory/hosts.yml` 中的组变量: ``` all: vars: ansible_python_interpreter: /opt/ansible-venv/bin/python3 ``` ### 静态验证状态 本代码库已**通过静态验证**(YAML 语法、ansible-lint 以及所有 playbook 的语法检查)。针对真实 Vagrant/libvirt 环境的完整实时虚拟机运行属于运维人员的步骤,并在 `tests/README.md` 中有详细说明。请勿将静态验证徽章视为实时测试通过的声明。
标签:Ansible, Debian, Ente Photos, NIDS, Podman, 容器化, 测试用例, 特权提升, 系统提示词, 自动化部署, 自定义请求头, 自托管