跳至内容
liuzhen932 的小窝
返回

使用 HAProxy 代理 Forgejo 的 SSH 访问

前情提要: 在 Debian 12 上安装 Forgejo 配置私有 Git

随着开源协作平台的普及,Forgejo 因其轻量、易部署和强大的 Git 管理能力,正被越来越多企业与开发者团队选用。在生产环境中,为提升安全性、加速访问并统一入口管理,我们通常会借助反向代理服务(如 Cloudflare)来代理对 Forgejo Web 页面的访问。这不仅隐藏了后端服务器的真实 IP,还能利用 CDN 加速、DDoS 防护和 SSL/TLS 自动化等功能,让团队协作平台更加稳定可靠。

然而,这也带来了一个显著的问题,即对于团队中惯用 SSH 的开发者来说,无法使用熟悉的 git clone [email protected]:username/repo.git 方式进行代码克隆与推送 —— 因为 Cloudflare 默认仅代理 HTTP/HTTPS 流量,而 SSH 是基于 TCP 的独立协议,无法被其直接穿透或转发(也可以开 Pro 以使用 Spectrum)。这篇文章主要面向在反向代理/CDN 后部署了 Forgejo 中型实例的实例管理员,给出使用 HAProxy TCP 反向代理以增加 SSH 入口的方案。

本文仅考虑 Cloudflare Free Plan;本文部分内容并不适合所有读者,请按需选择;本文含有 AIGC 创作内容;本文使用 Forgejo 在线编辑器编写,并使用 Instance Commit Signing 确保文章是本人撰写提交。

0x00 破局

SSH 流量需要独立通道,不能与 HTTP 混用。无论你是否使用 Cloudflare 或其他 CDN 代理 Web 界面,SSH 都必须通过 TCP 层面的转发机制直达后端 Forgejo 实例。

在本文中,我将讲述一种较为稳妥的方式 —— 部署 HAProxy —— 它支持原生 TCP 透传、健康检查、负载均衡和连接限速,能完美承载 SSH 这类长连接协议,并被 Codeberg 等生产环境使用。你可以让 HAProxy 监听公网 IP 的 :22 端口,后端指向 Forgejo 的 SSH 服务地址,开发者仍可使用熟悉的 [email protected] 方式操作仓库,体验无感切换。

0x01 前提条件

在开始配置 HAProxy 代理 SSH 之前,请务必确认以下基础条件已满足:

  1. Forgejo 实例已部署并正常运行,且 Web 服务可正常使用
  2. 你拥有一台独立的服务器用于部署 HAProxy,该服务器需具备公网 IP 地址,并能通过内网或私有网络访问后端 Forgejo 实例
  3. 您已经在 Forgejo 中为自己配置好了 SSH 公钥,这可以加快测试速度

接下来我将画一张图表,展示我推荐的这种架构:

+----------+     +-----------------+     +------------+      +---------------+
|   You    | --> | git.example.com | --> | Cloudflare | -->  |               |
+----------+     +-----------------+     +------------+      |               |
                                                             |    Forgejo    |
+----------+     +---------------------+     +---------+     |               |
|   Bob    | --> | git-ssh.example.com | --> | HAProxy | --> |               |
+----------+     +---------------------+     +---------+     +---------------+

在完成架构设计后,下一步是让域名正确指向对应的代理入口:你需要为 git.example.comgit-ssh.example.com 分别配置两条独立的 DNS 记录,确保它们不互相干扰。

对于 git.example.com,如果你此前已经使用了任何的 CDN 服务,保持即可 —— 这样所有 Web 请求都会经过 CDN;而对于 git-ssh.example.com,则必须将其 A 记录直接指向 HAProxy 服务器的公网 IP,且不能开启 Cloudflare 等的代理,否则 SSH 流量会被阻断或无法穿透。

0x02 配置网络

我们需要将 HAProxy 机器和 Forgejo 机器放到一个内网中,使其能互相(加密)通信。你可以使用 Tailscale 等基于 WireGuard 的客户端进行组网,亦可使用 iBGP 给你的 Forgejo 机器分一段 IP 并单机部署 HAProxy。

0x03 安装 HAProxy

HAProxy 官网 - haproxy on debian

在开始配置之前,你必须先在一台拥有公网 IP 的服务器上完成 HAProxy 的安装。这台服务器将成为 SSH 流量的唯一入口,负责接收来自 git-ssh.example.com 的所有连接请求,并将其转发至后端 Forgejo 实例。安装过程极其简单,无论你使用的是 Ubuntu、Debian、CentOS、Rocky Linux 或 AlmaLinux,只需执行几条命令即可完成。

对于基于 Debian 的系统,请自行查看 haproxy.debian.net 获取运行命令:

curl https://haproxy.debian.net/haproxy-archive-keyring.gpg \
      --create-dirs --output /etc/apt/keyrings/haproxy-archive-keyring.gpg
echo deb "[signed-by=/etc/apt/keyrings/haproxy-archive-keyring.gpg]" \
      https://haproxy.debian.net trixie-backports-3.2 main \
      > /etc/apt/sources.list.d/haproxy.list
# 然后刷新
apt-get update
# 我们安装 3.2-stable LTS 版本
apt-get install haproxy=3.2.\*

对于基于 RHEL 的系统,则执行:

sudo dnf install haproxy

安装完成后,HAProxy 服务不会自动启动,系统会生成默认配置文件 /etc/haproxy/haproxy.cfg,其中包含示例 frontendbackend 配置。

0x04 配置 HAProxy

考虑到有一篇关于 HAProxy 配置的草稿文章并未发出来,这里还是提一下默认的配置:

# /etc/haproxy/haproxy.cfg
# ==========
# 全局配置
# ==========

global
    log /dev/log local0
    chroot /var/lib/haproxy
    stats socket /run/haproxy/admin.sock mode 660 level admin
    stats timeout 30s
    user haproxy
    group haproxy
    daemon

	# 最大连接数
	maxconn 50000

defaults
	log	global
	mode	tcp
	option	dontlognull

    # 超时设置
    timeout connect 5s
    timeout client  60s
    timeout server  60s
    timeout queue 10s

一般为了调试,我们还可以启用其内置的统计页面:

# /etc/haproxy/haproxy.cfg
# ==========
# HAPROXY 状态页面
# ==========
listen f_stats
    stats       enable
    bind        *:12345
    mode        http
    log         global
    maxconn     10
	# 刷新间隔
    stats       refresh          30s
	# 访问路径和 realm
    stats       uri              /
    stats       realm            you-are-robot
	# 访问认证
    stats       auth             user:password
	# 隐藏版本
    stats       hide-version
	# 始终允许管理员权限
    stats admin if TRUE

出于我的个人习惯,在 frontend 我会使用 f_ 前缀,b_ 等也是如此。接下来我们配置一个 SSH (TCP) 的前后端:

# /etc/haproxy/haproxy.cfg
# ==========
# SSH 前端
# ==========
frontend f_ssh_forgejo-production
  timeout client 5m
  bind *:22
  # 使用 TCP 模式
  mode tcp
  # 设置默认后端
  default_backend b_ssh_forgejo-production

# ==========
# SSH 后端
# ==========
backend b_ssh_forgejo-production
  timeout server 5m
  mode tcp

  # server:                    定义一个后端服务器
  # s_ssh_forgejo-production:  该服务器的唯一标识名
  # 127.0.0.1:2222:            后端服务的实际 IP 地址和端口
  # check:                     启用健康检查功能,HAProxy 会定期探测该服务器是否存活
  # inter 30s:                 健康检查间隔为 30 秒
  # rise 2:                    连续 2 次健康检查成功,才认为服务器从「不健康」恢复为「健康」
  # fall 3:                    连续 3 次健康检查失败,才认为服务器「不健康」并从负载均衡池中移除
  # maxconn 50:                该服务器最多同时处理 50 个并发连接
  # send-proxy-v2:             启用 PROXY Procotol v2,将客户端真实 IP 等信息传递给后端服务
  server s_ssh_forgejo-production forgejo:2222 check inter 30s rise 2 fall 3 maxconn 50 send-proxy-v2

这样我们就配置了一个 :22 指向 forgejo:2222 的反向代理,并通过 PROXY Procotol v2 将信息发送给后端服务。

接下来是最佳实践,添加一个速率限制避免被扫描:

# /etc/haproxy/haproxy.cfg
# ==========
# SSH 速率限制
# ==========
backend ratelimit_ssh
   # 最多记录 8k 条条目,过期时间为 n 秒,并存储过去 n 秒内的连接速率
   # 将数据保留时间设置为超过 rates() 统计时间范围并无额外收益,因此将 expire 设为相同或 +1 秒是最优选择
   stick-table type ip size 8k expire 300s store conn_rate(300s),conn_cur

## 我们还需要更新下方的 frontend 使用 ACL 拒掉这部分请求

frontend f_ssh_forgejo-production
  timeout client 5m
  bind *:22
  mode tcp

  # rate-limiting
  acl blocked src_conn_rate(ratelimit_ssh) ge 50 # max connetions per ratelimit timeframe
  acl blocked src_conn_cur(ratelimit_ssh) ge 5 # max open connections
  acl allowed src -f /etc/haproxy/ratelimit-whitelist.acl # 始终允许的 IP 列表,需要创建这个文件
  tcp-request connection reject if blocked !allowed
  tcp-request connection track-sc0 src table ratelimit_ssh

  default_backend b_ssh_forgejo-production

使用 touch /etc/haproxy/ratelimit-whitelist.acl 创建白名单,然后 haproxy -c -f /etc/haproxy/haproxy.cfg 检查配置,确认无误后可重启/重载 HAProxy 实例:systemctl restart haproxy

0x05 配置 Forgejo

前面我们使用了 HAProxy Procotol v2,这里也要在 Forgejo 配置中体现:

# /data/gitea/conf/app.ini
[server]
;; 是否使用内置的 SSH 服务器
START_SSH_SERVER = true
;;
;; 期望在连接到内置 SSH 服务器时接收 PROXY 协议头
SSH_SERVER_USE_PROXY_PROTOCOL = true
;;
;; 用于内置 SSH 服务器的用户名。如果为空,则使用 RUN_USER 的值
;BUILTIN_SSH_SERVER_USER = %(RUN_USER)s
;;
;; 在克隆 URL 中暴露的域名
SSH_DOMAIN = git-ssh.example.com
;;
;; 在克隆 URL 中显示的 SSH 用户名
;SSH_USER = %(BUILTIN_SSH_SERVER_USER)s
;;
;; 内置 SSH 服务器应监听的网络接口
;SSH_LISTEN_HOST =
;;
;; 在克隆 URL 中暴露的端口号
SSH_PORT = 22
;;
;; 内置 SSH 服务器应监听的端口号
SSH_LISTEN_PORT = 2222
;;
;; 是否向匿名访客暴露 SSH 克隆 URL,默认为 false
;SSH_EXPOSE_ANONYMOUS = false
;;
;; 对任何写入 SSH 连接的超时时间(设置为 -1 以禁用所有超时)
;; 将默认使用 PER_WRITE_TIMEOUT
;SSH_PER_WRITE_TIMEOUT = 30s
;;
;; 每写入 1KB 数据到 SSH 连接的超时时间
;; 将默认使用 PER_WRITE_PER_KB_TIMEOUT
;SSH_PER_WRITE_PER_KB_TIMEOUT = 30s

完成!现在我们可以重启 Forgejo 实例查看效果:

  1. 在 Web 界面中,我们可以查看到 HTTPS 克隆右侧出现了 SSH 选项:

支持 SSH 克隆

  1. 管理设置 -> 应用配置 -> 概况 -> SSH 配置 中可以看到 SSH 已启用:

后台配置中 SSH 已启用

  1. 配置 SSH 公钥,使用 SSH 尝试连接实例,注意到可以成功连接:

可以使用 SSH 连接到实例

  1. 可以正确克隆项目。

0x06 总结

至此,通过 HAProxy 的 TCP 代理,我们成功为部署在 CDN 后的 Forgejo 实例打通了 SSH 访问通道。整个方案的核心在于利用 HAProxy 原生支持 TCP 透传和 PROXY Protocol 的能力,将公网流量安全、稳定地转发至后端服务,同时通过速率限制等配置增强了系统安全性。最终,开发者可以像往常一样使用熟悉的 [email protected] 地址进行代码操作,实现了在复杂网络环境下对 Git 服务的无缝访问与管理。

本文就到这里,如有任何疑问请留言。下一篇文章我将讲解如何在 Forgejo 实例上配置 Renovate Bot


分享这篇文章:

上一篇
将 Forgejo 从 SQLite 迁移到 PostgreSQL
下一篇
在 Debian 13 (Trixie) 上使用 ntpd-rs 搭建 NTP 服务

人机验证:请刷新页面以加载评论区