安全地使用 SSH 进行持续部署

在 GitHub Actions 等 CI/CD (持续集成/持续部署) 服务中,如果需要部署到远程服务器,我们通常会使用 SSH 连接。然而如果不加以限制,存储在 CI/CD 服务提供商中的凭证一旦泄露将对服务器造成巨大的安全风险。遵循最小权限原则,本文将利用 SSHD 为用户强制指定命令的功能,限制 CI/CD (以 GitHub Actions 为例) 所使用的用户只能执行特定的命令,从而实现安全的持续部署。

通过 ForceCommandcommand 强制指定命令

SSHD 允许通过两种方式指定一个用户登录时执行的命令:

  1. sshd_config 中的 ForceCommand 选项
  2. authorized_keys 文件中的 command 选项

两者作用类似,当用户登录时,用户提供的命令(如果有)将被忽略,而是运行 ForceCommandcommand 中指定的命令。最初提供的命令将作为环境变量 SSH_ORIGINAL_COMMAND 传递给 ForceCommandcommand 指定的命令。这样用户将无法访问 shell, command 或 subsystem,可以通过编写脚本并将其放在 ForceCommandcommand 中来限制用户的操作。

二者区别在于 ForceCommand 只能作用于全体用户或特定用户,而 command 可以作用于特定密钥。ForceCommand 将会覆盖 command

细节

  • 除非明确禁止,用户仍然可以进行端口转发或 X11 转发,因此你可能需要指定 DisableForwarding 或者 no-port-forwarding 等限制
  • 该命令是使用用户的登录 Shell 加以 -c 选项执行的,例如 bash -c <specified command>,因此你不可以禁用登录 Shell (比如设置为 /bin/false)

使用 GitHub Actions 安全地进行持续部署

这一章将会以 GitHub Actions 为例,展示如何安全地使用 SSH 进行持续部署。

创建专用用户和密钥

首先,我们需要在服务器上创建一个专用用户:

1
sudo adduser deployuser

在本地机器上创建该用户的密钥对:

1
ssh-keygen -t rsa -b 4096 -C "deployuser@your-server"

创建完成后,将其公钥添加到 deployuserauthorized_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_IPdeployuser@your-server,这允许你在不暴露服务器 IP 的情况下部署到服务器

参考资料

  1. sshd_config(5) - OpenBSD manual pages
  2. sshd(8) - OpenBSD manual pages
  3. The Little known SSH ForceCommand – SHANER.LIFE
  4. Using environments for deployment - GitHub Docs
作者

Cao Mingjun

发布于

2024-06-04

更新于

2024-06-04

许可协议

评论