反向代理

在 Nginx、Traefik 或 Apache 之后发布 AnsibleForms,放在根路径或子路径下

  1. 何时使用
  2. 关键设置
  3. 在子路径下提供服务
  4. 超时、上传和实时输出
  5. 运行器 WebSocket
  6. 请求头大小
  7. 客户端地址
  8. 示例

何时使用

AnsibleForms 在同一个端口上提供界面、REST API 和 Swagger;在其前面放一个反向代理可以:

  • 在 443 端口上提供服务,并使用现有工具(ACME、企业 CA)管理的证书
  • 将 AnsibleForms 发布在子路径下,与其他应用共用一个主机名
  • 让应用端口和数据库不暴露在公共网络上

代理是可选的:应用本身也可以提供 HTTPS,参见 HTTPS。


关键设置

只有少数几项设置会影响 AnsibleForms 在代理之后的行为:

设置 在代理之后
PORT 代理转发到的端口,默认为 8000。docker-compose 项目将其设为 443。
HTTPS 当代理终止 TLS 并通过私有网络访问应用时设为 0;如果这一段也要加密,则设为 1。
BASE_URL 子路径,例如 /ansibleforms。当应用有自己的主机名时,保持为 /。
TRUST_PROXY 哪些代理可以在 X-Forwarded-For 中报告客户端地址,通常为 1。参见 客户端地址。
API_BODY_LIMIT_MB 应用接受的最大 JSON 请求,默认为 50 MB。代理允许的大小必须至少与此相同。
UPLOAD_MAX_GB 表单上传字段接受的最大文件,默认为 10 GB。代理也必须允许这个大小,否则上传会在代理处失败。
MAX_HTTP_HEADER_SIZE 应用接受的请求头总大小上限,默认为 16 KB,从 7.1.0 起。参见 请求头大小。
公共根 URL 在 系统 > 常规 的 服务器 选项卡中:用户访问的地址,包括子路径。它用于生成通知邮件、通道消息和审批请求中的链接。

对于 Entra ID 或 OIDC,请将公共地址注册为重定向 URI;如果设置了 BASE_URL,还要包括其子路径。


在子路径下提供服务

BASE_URL 把整个应用挂载在子路径下;代理必须 原样 转发路径。 当 BASE_URL=/ansibleforms 时:

  • 界面、/api/v2/... 和 Swagger 界面都在 /ansibleforms/ 下提供
  • 界面的 <base href> 会在启动时被改写,因此静态资源和 API 调用都在子路径下解析
  • 对 / 或 /ansibleforms(没有末尾斜杠)的请求会被重定向到 /ansibleforms/

请保留前缀:Traefik 中不要使用 StripPrefix,Nginx 的 proxy_pass 中不要带 URI,因为应用需要这个前缀。

状态 页面会显示当前生效的基础 URL。


超时、上传和实时输出

作业页面和作业计数器通过一个长连接的服务器发送事件流 /api/v2/events 获知变化,每个浏览器保持一个连接。该事件流会发送关闭代理缓冲的请求头 (X-Accel-Buffering: no),并每 15 秒发送一行保活数据,因此常见的 60 秒读取超时不会断开它; 即使代理仍然关闭了它,浏览器也会自行重新连接。启动后紧接着显示在表单下方的输出 则是每两秒读取一次。启动作业的请求在作业创建后立即返回,因此长时间运行的 playbook 不会一直占用 请求。

可能耗时较长的请求有:

  • 在 备份 页面进行的 备份和恢复,它们会等待导出或恢复命令完成,最长为 BACKUP_COMMAND_TIMEOUT_SECONDS(默认 3600)
  • 通过文件字段进行的 大文件上传,它们在一个请求中以流的方式传输整个文件
  • 启用 聊天助手 时的 聊天消息,它们会等待模型提供商应答

代理通常在 60 秒后超时,并限制请求体大小(Nginx 默认接受 1 MB)。请为 AnsibleForms 的路由同时提高这两项限制,如下面的示例所示。


运行器 WebSocket

从 7.3.0 起,出站运行器 会主动连接应用,并在 <BASE_URL>/api/v2/rte/socket 上保持一个打开的 WebSocket。代理必须转发协议升级,否则运行器永远无法连接,会不断重试:

代理 WebSocket 需要的配置
Nginx proxy_http_version 1.1 以及 Upgrade 和 Connection 请求头,Nginx 默认不转发它们
Traefik 无需配置:WebSocket 会原样通过
Apache 在 ProxyPass 行上使用 upgrade=websocket(2.4.47 及更高版本),更早的版本使用 mod_proxy_wstunnel

两端每 20 秒发送一次 ping,因此常见的 60 秒读取超时不会断开空闲的套接字。


请求头大小

每个请求都在其 Authorization 请求头中携带登录令牌,此外还有 cookie 和代理添加的请求头。 代理和应用都会限制请求头的大小,并在 AnsibleForms 看到请求之前拒绝超过上限的请求, 因此服务器日志中不会有任何记录。默认值如下:

组件 限制 默认值 拒绝时返回
AnsibleForms(Node.js) MAX_HTTP_HEADER_SIZE,所有请求头合计 16384 字节 431
Nginx large_client_header_buffers,每个请求头字段 4 8k:每个字段 8 KB 400
Traefik 入口点的 http.maxHeaderBytes,所有请求头合计 1 MB 431
HAProxy global 部分中的 tune.bufsize,整个请求头块,减去 tune.maxrewrite 预留(1024 字节) 16384 字节:15360 字节的请求头 400
Apache LimitRequestFieldSize,每个请求头字段 8190 字节 400

在 7.0.3 及更早版本中,登录令牌携带单点登录用户的所有组,可能会超出这些限制,参见 单点登录后表单和作业为空。 从 7.1.0 起,令牌保持精简,默认值就足够了。当其他因素使请求头变大时,例如 同一域名下其他应用的 cookie,或前面的单点登录网关,请同时提高这些限制:

  • AnsibleForms:MAX_HTTP_HEADER_SIZE=65536,7.1.0 之前为 NODE_OPTIONS=--max-http-header-size=65536;两者都在 启动时读取,因此修改后请重启应用
  • Nginx:在 http 或 server 块中设置 large_client_header_buffers 4 32k;
  • HAProxy:在 global 部分中设置 tune.bufsize 65536;每个流都会获得该大小的缓冲区,因此 HAProxy 建议 按相同倍数降低全局 maxconn,并且 HTTP/2 至少需要默认的 16384
  • Apache:在服务器配置或虚拟主机中设置 LimitRequestFieldSize 32768

客户端地址

在代理之后,每个连接都来自代理。没有 TRUST_PROXY 时, 审计记录会为每个操作记录代理的地址,按地址计算的 登录限制 也会把所有客户端算作同一个地址。 TRUST_PROXY 说明当代理在 X-Forwarded-For 中报告客户端时,应用可以相信哪些代理:

值 客户端地址
0(默认) 连接的地址。忽略 X-Forwarded-For;空值、false、off 和 no 含义相同。
一个数字,例如 1 信任应用前面这么多层代理:单个反向代理为 1,ingress 前面还有负载均衡器时为 2。
一个列表,例如 10.0.0.5,172.16.0.0/12 信任位于这些地址和 CIDR 范围内的代理。也接受预设值 loopback、linklocal 和 uniquelocal。
true 信任每一跳:使用 X-Forwarded-For 最左边的地址。

应用从右往左遍历 X-Forwarded-For,跳过受信任的代理,取第一个不属于它们的 地址。请使用准确的代理数量或它们的地址:

  • X-Forwarded-For 是任何客户端都可以发送的普通请求头。数字过大或使用 true,会让 客户端自行选择审计记录中的地址。只有当应用只能通过代理访问时 true 才是安全的, 设置它时服务器会在启动时记录一条 [SECURITY] 警告。
  • 代理必须添加它接收连接的来源地址,如下面的示例所示 (Nginx 中为 $proxy_add_x_forwarded_for;Traefik 和 Apache mod_proxy 默认会添加)。
  • 无效的值在设置页面上会被拒绝;如果在环境中设置,启动时会记录一条错误,并且 不信任任何代理。在设置页面上保存的修改无需重启即可生效。

示例

每个示例都将 AnsibleForms 发布在 https://forms.example.com/ansibleforms/,由代理终止 TLS, 应用以 HTTPS=0、PORT=8000、BASE_URL=/ansibleforms 和 TRUST_PROXY=1 运行:

proxy_pass 中没有 URI,因此 Nginx 会按原样转发路径,包括前缀。map 块放在 http 上下文中(conf.d/ 下的文件会被包含在那里),用于转发运行器的 WebSocket 升级:

map $http_upgrade $connection_upgrade {
    default upgrade;
    ''      close;
}

server {
    listen 443 ssl;
    server_name forms.example.com;

    ssl_certificate     /etc/nginx/certs/forms.example.com.crt;
    ssl_certificate_key /etc/nginx/certs/forms.example.com.key;

    location /ansibleforms/ {
        proxy_pass         http://10.0.0.20:8000;
        proxy_set_header   Host $host;
        proxy_set_header   X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header   X-Forwarded-Proto $scheme;

        proxy_http_version 1.1;                         # the Runners' WebSocket
        proxy_set_header   Upgrade $http_upgrade;
        proxy_set_header   Connection $connection_upgrade;

        client_max_body_size    10g;    # UPLOAD_MAX_GB
        proxy_request_buffering off;    # stream uploads instead of spooling them first
        proxy_read_timeout      3600s;  # BACKUP_COMMAND_TIMEOUT_SECONDS
        proxy_send_timeout      3600s;
    }
}

不使用子路径时,请使用 location /,并且不设置 BASE_URL。

要让代理与应用之间的这一段也保持加密,请以 HTTPS=1 运行应用,将代理指向 https://,并为应用配置一个代理信任的证书,参见 HTTPS。