Browse Source

Allow HTTP port 80 through UFW for certbot ACME validation

certbot standalone mode requires port 80 to be accessible for ACME HTTP-01
validation. UFW was configured with default deny and only SSH was allowed,
blocking certbot from obtaining certificates. Add UFW allow rule for port 80
before certbot attempts certificate provisioning.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
kotoyuuko 2 weeks ago
parent
commit
c828daf2f8

+ 2 - 0
openspec/changes/archive/2026-04-29-allow-http-port-for-certbot/.openspec.yaml

@@ -0,0 +1,2 @@
+schema: spec-driven
+created: 2026-04-29

+ 24 - 0
openspec/changes/archive/2026-04-29-allow-http-port-for-certbot/design.md

@@ -0,0 +1,24 @@
+## Context
+
+UFW 默认 deny incoming + 仅允许 SSH,导致 certbot standalone 的 80 端口 ACME 验证被防火墙阻断。
+
+## Goals / Non-Goals
+
+**Goals:**
+- 确保证书申请前 80 端口已被 UFW 放行
+
+**Non-Goals:**
+- 不修改 base role 的默认防火墙策略
+- 不引入临时端口规则(避免增加复杂度)
+
+## Decisions
+
+### 在 trojan role 中单独允许 80 端口
+
+- **Rationale**: base role 的 `allowed_ports` 机制在 base role 执行时生效,trojan role 在后执行时无法通过该机制追加端口。直接在 trojan role 中使用 UFW 模块允许 80 端口是最简洁的方案。
+- **Alternatives considered**: 在 base role 的 `group_vars/all.yml` 中增加 `allowed_ports: [80]` — 但这会为所有服务器(包括不部署 Trojan 的服务器)开放 80 端口,不符合最小权限原则。
+
+## Risks / Trade-offs
+
+- **[Risk]** 80 端口长期开放可能被扫描
+  - **Mitigation**: 80 端口本身不运行敏感服务,仅用于 HTTP 和 ACME 验证;nginx fallback 可提供基本响应

+ 22 - 0
openspec/changes/archive/2026-04-29-allow-http-port-for-certbot/proposal.md

@@ -0,0 +1,22 @@
+## Why
+
+certbot standalone 模式使用 ACME HTTP-01 验证,需要外部能访问服务器的 80 端口。当前 base role 配置 UFW 默认拒绝所有入站流量,仅允许 SSH 端口;trojan role 也只开放了 443 端口。这导致 certbot 无法通过 80 端口完成域名验证,证书申请失败。
+
+## What Changes
+
+- 在 trojan role 中 certbot 证书申请之前,通过 UFW 允许 80 端口入站
+- 保留 80 端口开放,HTTP 流量是正常的服务暴露
+
+## Capabilities
+
+### New Capabilities
+
+- 无新增能力
+
+### Modified Capabilities
+
+- `trojan-multiuser`: TLS 证书申请流程中增加 80 端口 UFW 放行步骤,确保 certbot 能够完成 ACME 验证。
+
+## Impact
+
+- `roles/trojan/tasks/main.yml` 中 certbot 步骤前增加 UFW 允许 80 端口的任务

+ 21 - 0
openspec/changes/archive/2026-04-29-allow-http-port-for-certbot/specs/trojan-multiuser/spec.md

@@ -0,0 +1,21 @@
+## MODIFIED Requirements
+
+### Requirement: TLS certificate is provisioned via Let's Encrypt
+The trojan role SHALL use certbot to obtain a TLS certificate for the domain configured on each individual host. The trojan role SHALL ensure port 80 is allowed through UFW before attempting certificate provisioning. After provisioning or renewal, the certificate and key SHALL be copied to `/etc/trojan-go/tls/` so the service user can read them.
+
+#### Scenario: Certificate provisioning with UFW allowing port 80
+- **WHEN** the trojan role runs on a host with `trojan_domain` configured
+- **THEN** port 80 is allowed through UFW before certbot attempts ACME validation
+- **THEN** certbot obtains a TLS certificate for the configured domain
+- **THEN** the certificate and key are copied to `/etc/trojan-go/tls/` owned by the trojan service user
+
+#### Scenario: Certificate provisioning on a second host with different domain
+- **WHEN** the trojan role runs on a host with `trojan_domain: "proxy2.example.com"`
+- **THEN** certbot obtains a TLS certificate for `proxy2.example.com`
+- **THEN** the certificate is independent from other hosts
+
+#### Scenario: Certificate auto-renewal
+- **WHEN** the certificate is within 30 days of expiry
+- **THEN** certbot renews it automatically
+- **THEN** a deploy-hook copies the renewed certs to `/etc/trojan-go/tls/`
+- **THEN** the Trojan service is reloaded after renewal

+ 7 - 0
openspec/changes/archive/2026-04-29-allow-http-port-for-certbot/tasks.md

@@ -0,0 +1,7 @@
+## 1. Implementation
+
+- [x] 1.1 Add UFW allow rule for port 80 in `roles/trojan/tasks/main.yml` before the certbot certificate provisioning step
+
+## 2. Validation
+
+- [x] 2.1 Run `ansible-playbook --syntax-check` to validate playbook integrity

+ 5 - 4
openspec/specs/trojan-multiuser/spec.md

@@ -29,11 +29,12 @@ The trojan role SHALL download and install the trojan-go binary from release art
 - **THEN** the Trojan service is restarted
 - **THEN** the Trojan service is restarted
 
 
 ### Requirement: TLS certificate is provisioned via Let's Encrypt
 ### Requirement: TLS certificate is provisioned via Let's Encrypt
-The trojan role SHALL use certbot to obtain a TLS certificate for the domain configured on each individual host. After provisioning or renewal, the certificate and key SHALL be copied to `/etc/trojan-go/tls/` so the service user can read them.
+The trojan role SHALL use certbot to obtain a TLS certificate for the domain configured on each individual host. The trojan role SHALL ensure port 80 is allowed through UFW before attempting certificate provisioning. After provisioning or renewal, the certificate and key SHALL be copied to `/etc/trojan-go/tls/` so the service user can read them.
 
 
-#### Scenario: Certificate provisioning with per-host domain
-- **WHEN** the trojan role runs on a host with `trojan_domain: "proxy1.example.com"`
-- **THEN** certbot obtains a TLS certificate for `proxy1.example.com`
+#### Scenario: Certificate provisioning with UFW allowing port 80
+- **WHEN** the trojan role runs on a host with `trojan_domain` configured
+- **THEN** port 80 is allowed through UFW before certbot attempts ACME validation
+- **THEN** certbot obtains a TLS certificate for the configured domain
 - **THEN** the certificate and key are copied to `/etc/trojan-go/tls/` owned by the trojan service user
 - **THEN** the certificate and key are copied to `/etc/trojan-go/tls/` owned by the trojan service user
 
 
 #### Scenario: Certificate provisioning on a second host with different domain
 #### Scenario: Certificate provisioning on a second host with different domain

+ 6 - 0
roles/trojan/tasks/main.yml

@@ -70,6 +70,12 @@
       - certbot
       - certbot
     state: present
     state: present
 
 
+- name: Allow HTTP port through UFW for certbot ACME validation
+  community.general.ufw:
+    rule: allow
+    port: 80
+    proto: tcp
+
 - name: Obtain Let's Encrypt certificate
 - name: Obtain Let's Encrypt certificate
   ansible.builtin.command:
   ansible.builtin.command:
     cmd: >
     cmd: >