如何让GitHub Actions安全SSH访问端口22受防火墙限制的Hetzner虚拟机?
我正在通过GitHub Actions结合SSH,将前端项目自动CI/CD部署到Hetzner云虚拟机(共享vCPU)。目前所有配置正常,仅防火墙配置存在问题:出于安全考虑,我把防火墙设置为仅允许自身IP访问SSH(22端口),但希望GitHub Actions能自动通过SSH部署到服务器。但GitHub Actions使用动态IP池,无法手动把所有IP加入白名单。
已尝试的操作:
- 在Windows系统执行命令
ssh-keygen -t ed25519 -C "user@computer" -f C:/Users/Admin/.ssh/hetzner-key生成SSH密钥对; - 将私钥添加到GitHub Secrets(命名为
SSH_KEY); - 将对应的公钥添加到Hetzner虚拟机的
~/.ssh/authorized_keys文件中; - 临时允许所有IPv4/IPv6访问防火墙时,部署可正常运行;
- 使用
appleboy/scp-action和appleboy/ssh-action传输并运行部署脚本。
防火墙限制仅自身IP访问SSH时,GitHub Actions部署失败,报错:
drone-scp error: error copy file to dest: ***,
error message: dial tcp ***:22: i/o timeout
2025/08/05 18:24:48 error copy file to dest: ***,
error message: dial tcp ***:22: i/o timeout
我的GitHub Actions工作流文件(deploy.yml):
name: Deploy Frontend to Hetzner on: push: branches: [main] jobs: deploy: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - name: Build Docker image run: | docker build -t frontend-app . docker save frontend-app > frontend.tar - name: Copy to Server uses: appleboy/scp-action@v1 with: host: ${{ secrets.SERVER_IP }} username: root key: ${{ secrets.SSH_KEY }} source: "frontend.tar" target: "/root/" - name: Deploy Container uses: appleboy/ssh-action@v1 with: host: ${{ secrets.SERVER_IP }} username: root key: ${{ secrets.SSH_KEY }} script: | docker load -i /root/frontend.tar docker stop frontend-container || true docker rm frontend-container || true docker run -d \ --name frontend-container \ -p 80:80 \ -p 443:443 \ -v /etc/letsencrypt:/etc/letsencrypt \ -v /var/lib/letsencrypt:/var/lib/letsencrypt \ -v /var/www/html:/var/www/html \ frontend-app
方案1:动态更新Hetzner防火墙规则
利用GitHub Actions Runner的临时出口IP,通过Hetzner Cloud API将该IP临时加入防火墙白名单,部署完成后自动移除,全程无需手动操作。
操作步骤:
- 在Hetzner Cloud控制台创建API Token,权限至少包含
firewall:read和firewall:write,将Token存入GitHub Secrets(命名为HETZNER_API_TOKEN)。 - 修改GitHub Actions工作流,新增步骤获取Runner出口IP、添加临时防火墙规则,部署结束后移除规则:
- name: 获取Runner出口IP id: get_ip run: echo "IP=$(curl -s https://api.ipify.org)" >> $GITHUB_OUTPUT - name: 将Runner IP加入Hetzner防火墙 run: | curl -X POST "https://api.hetzner.cloud/v1/firewalls/{你的防火墙ID}/actions/add_rules" \ -H "Authorization: Bearer ${{ secrets.HETZNER_API_TOKEN }}" \ -H "Content-Type: application/json" \ -d '{ "rules": [ { "direction": "in", "protocol": "tcp", "port": "22", "source_ips": ["${{ steps.get_ip.outputs.IP }}/32"] } ] }' # 保留原有的Build、Copy、Container部署步骤 - name: 从防火墙移除Runner IP if: always() run: | curl -X POST "https://api.hetzner.cloud/v1/firewalls/{你的防火墙ID}/actions/remove_rules" \ -H "Authorization: Bearer ${{ secrets.HETZNER_API_TOKEN }}" \ -H "Content-Type: application/json" \ -d '{ "rules": [ { "direction": "in", "protocol": "tcp", "port": "22", "source_ips": ["${{ steps.get_ip.outputs.IP }}/32"] } ] }'
if: always()确保无论部署成功或失败,都会清理临时IP规则,避免白名单残留。
方案2:专用部署用户+SSH命令限制
创建仅用于部署的专用用户,限制该用户只能执行指定的部署命令,同时在防火墙和SSH配置中区分管理员用户与部署用户的访问权限,兼顾安全与自动化需求。
操作步骤:
- 在服务器创建专用部署用户:
useradd -m deploy - 生成新的SSH密钥对,将公钥添加到
deploy用户的~/.ssh/authorized_keys,并添加命令限制(仅允许执行指定部署脚本):
command="bash /home/deploy/deploy-script.sh",no-port-forwarding,no-X11-forwarding,no-agent-forwarding,no-pty ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAA... deploy@github-actions
- 编写
/home/deploy/deploy-script.sh,仅包含部署所需的命令(如加载Docker镜像、重启容器等),禁止其他操作。 - 修改服务器
/etc/ssh/sshd_config,限制管理员用户仅允许自身IP访问,部署用户允许所有IP:
Match User root AllowUsers root@你的IP/32 Match User deploy AllowUsers deploy@*
- 在防火墙中开放22端口给所有IP,依赖SSH配置的用户访问限制保证安全。
方案3:反向SSH隧道
让Hetzner虚拟机主动发起SSH连接到GitHub Actions Runner,建立反向隧道,无需开放服务器22端口给外部,通过Runner的端口完成部署通信。
操作步骤:
- 在GitHub Secrets中存储服务器的SSH私钥(用于服务器主动连接Runner)。
- 在GitHub Actions工作流中启动临时SSH服务,然后触发服务器主动连接并建立反向隧道:
- name: 启动临时SSH服务 run: | mkdir -p ~/.ssh echo "${{ secrets.SERVER_SSH_KEY }}" > ~/.ssh/id_ed25519 chmod 600 ~/.ssh/id_ed25519 ssh-keygen -f ~/.ssh/id_ed25519 -y > ~/.ssh/id_ed25519.pub cat ~/.ssh/id_ed25519.pub >> ~/.ssh/authorized_keys sudo systemctl start ssh - name: 触发服务器建立反向隧道 run: | ssh -o StrictHostKeyChecking=no root@${{ secrets.SERVER_IP }} -R 2222:localhost:22 "sleep 300" & - name: 通过隧道传输文件 uses: appleboy/scp-action@v1 with: host: localhost port: 2222 username: root key: ${{ secrets.SSH_KEY }} source: "frontend.tar" target: "/root/" # 后续部署步骤同理,将host改为localhost,port改为2222
内容的提问来源于stack exchange,提问作者coder

