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






-1868F2?logo=vagrant&logoColor=white)
[](LICENSE)
[](https://github.com/AcidDemon/ansible-ente-photos-debian/commits/main)
[](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, 容器化, 测试用例, 特权提升, 系统提示词, 自动化部署, 自定义请求头, 自托管