在 GitHub Actions 等 CI/CD (持续集成/持续部署) 服务中,如果需要部署到远程服务器,我们通常会使用 SSH 连接。然而如果不加以限制,存储在 CI/CD 服务提供商中的凭证一旦泄露将对服务器造成巨大的安全风险。遵循最小权限原则,本文将利用 SSHD 为用户强制指定命令的功能,限制 CI/CD (以 GitHub Actions 为例) 所使用的用户只能执行特定的命令,从而实现安全的持续部署。
通过 ForceCommand
或 command
强制指定命令
SSHD 允许通过两种方式指定一个用户登录时执行的命令:
sshd_config
中的 ForceCommand
选项
authorized_keys
文件中的 command
选项
两者作用类似,当用户登录时,用户提供的命令(如果有)将被忽略,而是运行 ForceCommand
或 command
中指定的命令。最初提供的命令将作为环境变量 SSH_ORIGINAL_COMMAND
传递给 ForceCommand
或 command
指定的命令。这样用户将无法访问 shell, command 或 subsystem,可以通过编写脚本并将其放在 ForceCommand
或 command
中来限制用户的操作。
二者区别在于 ForceCommand
只能作用于全体用户或特定用户,而 command
可以作用于特定密钥。ForceCommand
将会覆盖 command
。
- 除非明确禁止,用户仍然可以进行端口转发或 X11 转发,因此你可能需要指定
DisableForwarding
或者 no-port-forwarding
等限制
- 该命令是使用用户的登录 Shell 加以
-c
选项执行的,例如 bash -c <specified command>
,因此你不可以禁用登录 Shell (比如设置为 /bin/false
)
使用 GitHub Actions 安全地进行持续部署
这一章将会以 GitHub Actions 为例,展示如何安全地使用 SSH 进行持续部署。
创建专用用户和密钥
首先,我们需要在服务器上创建一个专用用户:
在本地机器上创建该用户的密钥对:
1
| ssh-keygen -t rsa -b 4096 -C "deployuser@your-server"
|
创建完成后,将其公钥添加到 deployuser
的 authorized_keys
文件中,并为 authorized_keys
文件添加权限:
1 2 3 4 5
| sudo mkdir -p /home/deployuser/.ssh sudo vim /home/deployuser/.ssh/authorized_keys sudo chmod 700 /home/deployuser/.ssh sudo chmod 600 /home/deployuser/.ssh/authorized_keys sudo chown -R deployuser:deployuser /home/deployuser/.ssh
|
配置 authorized_keys
我选择使用 command
选项,因为这样可以为每个密钥指定不同的命令。修改 authorized_keys
文件:
1
| command="/home/deployuser/deploy_script.sh",no-port-forwarding,no-X11-forwarding,no-agent-forwarding,no-pty,no-user-rc,restrict ssh-rsa AAAA... deployuser@your-server
|
其中:
command
指定了用户登录时执行的命令,这里是 /home/deployuser/deploy_script.sh
。
restrict
启用所有限制,包括禁用端口、代理和 X11 转发等。如果将来向 authorized_keys
文件添加任何限制功能,则它们将包含在其中。
no-port-forwarding,no-X11-forwarding,no-agent-forwarding,no-pty,no-user-rc
已经被包含在 restrict
中,它们是为了兼容尚未支持 restrict
的旧版本 SSH 服务器。
ssh-rsa AAAA... deployuser@your-server
是上一步中添加的公钥。
编写 deploy_script.sh
你可以根据自己的需求来编写 deploy_script.sh
,这里是一个简单的例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| #!/bin/sh
cd /path/to/repo case "$SSH_ORIGINAL_COMMAND" in "pull") git pull ;; "deploy") /path/to/deploy/script ;; *) echo "Only these commands are available to you:" echo "pull,deploy" exit 1 ;; esac
|
环境变量 SSH_ORIGINAL_COMMAND
是用户提供的命令,因此你应该小心处理它,以防止注入攻击。
例如你可能希望用户只能执行 git
,因此将脚本设置为 git $SSH_ORIGINAL_COMMAND
,但是用户可以通过传递 init; rm -rf /
给 SSH_ORIGINAL_COMMAND
来删除服务器上的所有文件。
请不要直接将 SSH_ORIGINAL_COMMAND
作为命令的一部分,而是使用 case
或其他方法来检查用户提供的命令。
最后不要忘了给 deploy_script.sh
添加执行权限:
1
| chmod +x /home/deployuser/deploy_script.sh
|
在 GitHub Actions 中使用
以下是一个 GitHub Workflow 的例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| name: Deploy to Server
on: push: branches: - main workflow_dispatch:
jobs: deploy: runs-on: ubuntu-latest environment: Server concurrency: deploy-server steps: - name: Checkout code uses: actions/checkout@v4
- name: Setup SSH run: | mkdir -p ~/.ssh echo "${{ secrets.SSH_KEY }}" > ~/.ssh/id_rsa chmod 600 ~/.ssh/id_rsa echo "${{ secrets.KNOWN_HOSTS }}" > ~/.ssh/known_hosts chmod 644 ~/.ssh/known_hosts
- name: Deploy to Server run: | ssh -i ~/.ssh/id_rsa "${{ secrets.SERVER_USER_IP }}" pull ssh -i ~/.ssh/id_rsa "${{ secrets.SERVER_USER_IP }}" deploy
|
其中:
- 我将这个 workflow 与
Server
环境相关联,这是可选的。如果关联了环境,你就可以在仓库主页上看到部署的状态并且可以管理部署。不关联不影响 workflow 的运行。
- 我通过指定
concurrency
来限制同时只能有一个 deploy-server
组的 workflow 在运行,避免多个部署任务同时运行引发并发问题。
- 我使用了三个 secret:
SSH_KEY
包含了第一步中生成的私钥
KNOWN_HOSTS
包含了服务器的公钥
SERVER_USER_IP
为 deployuser@your-server
,这允许你在不暴露服务器 IP 的情况下部署到服务器
参考资料
- sshd_config(5) - OpenBSD manual pages
- sshd(8) - OpenBSD manual pages
- The Little known SSH ForceCommand – SHANER.LIFE
- Using environments for deployment - GitHub Docs