最近在公司实习的期间,在我的腾讯云轻量服务器上跑着的一个项目,然后我算真正意义的完整尝试了 CICD 的流程,感觉非常震撼,好用,同样也遇到了今天的问题
前情提要
我在 codex 的辅助下设计了一套工作流,提交到了 github 都会触发一次 actions,具体 actions 内容如下。
name: Deploy production
on:
push:
branches: [main]
workflow_dispatch:
concurrency:
group: luopan-production
cancel-in-progress: false
permissions:
contents: read
jobs:
ci:
name: Validate code and deployment configuration
runs-on: ubuntu-latest
steps:
- name: Check out committed source
uses: actions/checkout@v6
- name: Set up Python
uses: actions/setup-python@v6
with:
python-version: '3.11'
- name: Install uv
run: |
curl -LsSf https://astral.sh/uv/0.11.29/install.sh | \
env UV_UNMANAGED_INSTALL="$HOME/.local/bin" sh
echo "$HOME/.local/bin" >> "$GITHUB_PATH"
- name: Sync locked Python environment
run: uv sync --locked --no-dev
- name: Run Python syntax and unit checks
run: |
uv run --locked python -m compileall -q apps tests
uv run --locked python -m unittest discover -s tests -v
- name: Run Rust tests
run: cargo test --workspace
- name: Set up Node
uses: actions/setup-node@v6
with:
node-version: '20'
- name: Set up pnpm
uses: pnpm/action-setup@v4
with:
version: 10.24.0
- name: Install frontend dependencies
working-directory: apps/web
run: pnpm install --frozen-lockfile
- name: Check and build frontend
working-directory: apps/web
run: |
pnpm check
pnpm build
- name: Check JavaScript and shell scripts
run: |
node --check web/static/app.js
bash -n ops/deploy.sh ops/backup.sh ops/ssh-deploy-wrapper.sh \
docker/start.sh docker/cron_scrape.sh docker/cron_inventory_sync.sh \
docker/run_daily.sh docker/test-deploy.sh \
scripts/run_daily.sh scripts/run_dashboard.sh scripts/run_inventory_sync.sh \
scripts/deploy_from_github.sh
- name: Validate Docker Compose production mounts
run: |
docker compose config >/dev/null
LUOPAN_DATA_DIR=/srv/luopan-data docker compose config | \
grep -q '/srv/luopan-data/state'
grep -q '^STORAGE_SYNC_AFTER_SCRAPE=true$' docker/crontab
grep -q 'STORAGE_SYNC_AFTER_SCRAPE:-true' docker/cron_scrape.sh
for runtime_dir in config logs output session state; do
grep -q -- "--exclude='${runtime_dir}/'" ops/deploy.sh
done
grep -q -- '--alias douyin-compass-collector' ops/deploy.sh
deploy:
needs: ci
runs-on: ubuntu-latest
steps:
- name: Configure restricted deployment key
env:
DEPLOY_KEY: ${{ secrets.LUOPAN_DEPLOY_SSH_KEY }}
KNOWN_HOSTS: ${{ secrets.LUOPAN_DEPLOY_KNOWN_HOSTS }}
run: |
install -m 700 -d ~/.ssh
printf '%s\n' "$DEPLOY_KEY" > ~/.ssh/id_ed25519
chmod 600 ~/.ssh/id_ed25519
printf '%s\n' "$KNOWN_HOSTS" > ~/.ssh/known_hosts
chmod 600 ~/.ssh/known_hosts
- name: Deploy committed main branch
env:
DEPLOY_HOST: ${{ secrets.LUOPAN_DEPLOY_HOST }}
DEPLOY_USER: ${{ secrets.LUOPAN_DEPLOY_USER }}
run: |
ssh -i ~/.ssh/id_ed25519 -o BatchMode=yes -o StrictHostKeyChecking=yes \
-o ServerAliveInterval=30 -o ServerAliveCountMax=20 \
"${DEPLOY_USER}@${DEPLOY_HOST}" "deploy ${GITHUB_SHA}"
其实对于我来说这个项目的 CI 基本上都是走个流程,因为如果本地运行不通过的话,我都不会提交上去。所以主要看 CD 部分。
deploy:
needs: ci
runs-on: ubuntu-latest
steps:
- name: Configure restricted deployment key
env:
DEPLOY_KEY: ${{ secrets.LUOPAN_DEPLOY_SSH_KEY }}
KNOWN_HOSTS: ${{ secrets.LUOPAN_DEPLOY_KNOWN_HOSTS }}
run: |
install -m 700 -d ~/.ssh
printf '%s\n' "$DEPLOY_KEY" > ~/.ssh/id_ed25519
chmod 600 ~/.ssh/id_ed25519
printf '%s\n' "$KNOWN_HOSTS" > ~/.ssh/known_hosts
chmod 600 ~/.ssh/known_hosts
- name: Deploy committed main branch
env:
DEPLOY_HOST: ${{ secrets.LUOPAN_DEPLOY_HOST }}
DEPLOY_USER: ${{ secrets.LUOPAN_DEPLOY_USER }}
run: |
ssh -i ~/.ssh/id_ed25519 -o BatchMode=yes -o StrictHostKeyChecking=yes \
-o ServerAliveInterval=30 -o ServerAliveCountMax=20 \
"${DEPLOY_USER}@${DEPLOY_HOST}" "deploy ${GITHUB_SHA}"注意,需要在本地生成一对公钥和私钥,公钥放到服务器,私钥则放到 GitHub 的 actions 的 env 中。通过以下命令来生成密钥对:
ssh-keygen -t ed25519 -f ~/.ssh/key_name -C "comment_message"
公钥要放到服务器的~/.ssh/authorized_keys,然后就可以免密登录了,也可以成功 CD 了。
问题描述
现在是能成功部署的,但是问题是,每次提交触发 actions 都给我弹这样的消息。

原因也很明显,GitHub actions 的部署是在 Azure 的服务器上,而这些服务器又是美国的,我的部署命令也是通过ssh访问服务器来实现的。频繁的部署就会频繁的发,这样还是有点烦的。
问题解决
这个问题如何解决呢,我问过很多的 ai,给出的方案都是差不多的,一个是指定CICD 使用的用户名(腾讯云上给这个用户添加到白名单),另一个就是通过 cloudflare tunnel 连接,本文中使用的是方法一。
确认报警源
第一步,你收到任何的报警信息要先确定是不是自己触发的,通过核对登录时间和 actions 操作时间来判断。
不要把陌生人的恶意访问当成 actions。
创建专用账号
连接到服务器,通过以下命令来创建一个全新的用户:
sudo adduser \
--disabled-password \
--gecos "" \
user-name
sudo passwd -l uaer-name值得注意的是,由于我们之后要将这个用户加到白名单,所有要放行一切登录行为,为了避免被恶意攻击,所以我们关闭密码登录功能,只能通过密钥来登录。
单独创建密钥
为了避免因为密钥泄露导致的无限制恶意访问,所以最好单独创建一个新的密钥。
在自己的电脑本地:
ssh-keygen \
-t ed25519 \
-f ~/.ssh/github_actions \
-C "github-actions" \
-N "" -f 是位置和密钥名字;-t 是密钥类型,通常使用ed25519 ;-C 是备注信息。
和上面一样会得到两个密钥
- 私钥 → GitHub Actions Secret
- 公钥 → 服务器的 authorized_keys

注意这里的 secrets 的 name 要和上面 actions.yml 中对应。
给新建用户提权
新建的用户默认是不可能有权限去运行另一个用户文件夹内的文件的,所以我们需要给这个用户赋予一定的权限。
有两种方式,一种是给他赋项目文件夹和 docker(我的项目需要启动 docker) 的权。通过下面这两个命令。
sudo usermod -aG docker user-name
sudo chown -R username /home/ubuntu/luopan-app但是这样的话权限就会有点过于大了,因为 docker 本身就是一个很容易越权的服务。
所以更推荐第二种方式,就是只给这个用户赋予deploy.sh的权限。
幸好我的项目是有单独的部署脚本,所以只需要用户有权利运行deploy.sh就可以完成部署。
deploy.sh的两种模式
deploy.sh默认会有接受 SHA 和不接受 SHA 的两种情况,在有 SHA 的时候会下载并部署指定 commit,而没有的时候会git fetch origin main → git reset --hard origin/main。这样的设计为的是方便两种部署方式,一种是 actions,另一种直接 ssh 连接部署。
于是我们的权限设计也可以照顾这样的两种模式(实际上不照顾也行,因为默认actions 是会自带 SHA 的,而常规情况也不会通过这个用户来登录服务器)。
权限赋予
我们可以通过这样的命令来赋予权限(必须带 SHA):
sudo tee /usr/local/sbin/luopan-ssh-gateway >/dev/null <<'EOF'
#!/usr/bin/env bash
set -Eeuo pipefail
readonly DEPLOY_SCRIPT="/home/ubuntu/luopan-app/ops/deploy.sh"
readonly RECEIVED_COMMAND="${SSH_ORIGINAL_COMMAND:-}"
if [[ "${RECEIVED_COMMAND}" =~ ^deploy\ ([0-9a-f]{40})$ ]]; then
readonly DEPLOY_SHA="${BASH_REMATCH[1]}"
exec sudo -n -u ubuntu -- "${DEPLOY_SCRIPT}" "${DEPLOY_SHA}"
fi
echo 'This SSH key only permits:' >&2
echo ' deploy <40-character lowercase Git SHA>' >&2
exit 126
EOF
sudo chown root:root /usr/local/sbin/luopan-ssh-gateway
sudo chmod 755 /usr/local/sbin/luopan-ssh-gateway这段代码的意思是在/usr/local/sbin 文件夹下创建luopan-ssh-gateway这样的网关脚本
set -Eeuo pipefail这句表明使用严格模式,有任何错误都会尽快退出。
readonly DEPLOY_SCRIPT="/home/ubuntu/luopan-app/ops/deploy.sh"
readonly RECEIVED_COMMAND="${SSH_ORIGINAL_COMMAND:-}"这两句则是定义了DEPLOY_SCRIPT 和RECEIVED_COMMAND ,前面的readonly 表明定义完之后不可变。
第二句中的SSH_ORIGINAL_COMMAND 就是在运行 openssh 的时候如果命令被截断(我会在下面定义 authorized_keys来截断),原本要执行的 command 都会注入到这个变量里面。
{变量:-}表示:
- 如果变量存在,使用它的值;
- 如果变量不存在或为空,使用空字符串。
比如有人尝试直接打开交互式 Shell:
ssh luopan-deploy@服务器此时没有提交具体命令,RECEIVED_COMMAND 就会是空字符串,随后被拒绝。
if [[ "${RECEIVED_COMMAND}" =~ ^deploy\ ([0-9a-f]{40})$ ]]; then
readonly DEPLOY_SHA="${BASH_REMATCH[1]}"
exec sudo -n -u ubuntu -- "${DEPLOY_SCRIPT}" "${DEPLOY_SHA}"
fi这一步则是正则匹配,并把匹配到的 SHA 存到DEPLOY_SHA ,然后则以 ubuntu(默认用户)来运行这个deploy.sh。这里就是提权的核心。
echo 'This SSH key only permits:' >&2
echo ' deploy <40-character lowercase Git SHA>' >&2最后这两句就是拒绝非法指令。
sudo chown root:root /usr/local/sbin/luopan-ssh-gateway
sudo chmod 755 /usr/local/sbin/luopan-ssh-gateway这两句大致是一个意思,就是让普通用户有权读取这个脚本,但是没权利修改。避免被用户不小心改掉。
授权执行部署脚本
上面提到的最后一步就是sudo -n -u ubuntu – "${DEPLOY_SCRIPT}" "${DEPLOY_SHA},而默认一个用户是无权以另一个用户的身份来执行任务的。
sudo visudo -f /etc/sudoers.d/luopan-deploy写入以下内容:
Cmnd_Alias LUOPAN_DEPLOY_COMMANDS = /home/ubuntu/luopan-app/ops/deploy.sh, /home/ubuntu/luopan-app/ops/deploy.sh *
luopan-deploy ALL=(ubuntu) NOPASSWD: LUOPAN_DEPLOY_COMMANDS赋权
sudo visudo -cf /etc/sudoers.d/luopan-deploy
sudo chmod 440 /etc/sudoers.d/luopan-deploy
sudo -l -U luopan-deploy服务器authorized_keys添加。
创建.ssh 目录
sudo install \
-d \
-m 700 \
-o luopan-deploy \
-g luopan-deploy \
/home/luopan-deploy/.ssh700权限就是rwx------ ,也就是只有当前用户有权限。-o和-g就是设定所有者和所属组,后面都是跟用户名。
最后得到的是这个:
drwx------ 2 luopan-deploy luopan-deploy ... /home/luopan-deploy/.ssh生成受限制的authorized_keys
{
printf 'restrict,command="/usr/local/sbin/luopan-ssh-gateway" '
cat /tmp/luopan_github_actions.pub
} | sudo tee /home/luopan-deploy/.ssh/authorized_keys >/dev/null这一段会创建以下文件
/home/luopan-deploy/.ssh/authorized_keys文件内容是:
restrict,command="/usr/local/sbin/luopan-ssh-gateway" ssh-ed25519 AAAA-----------------AAAA... github-actions-luopanhacker这里表明让用户登录之后,强制运行luopan-ssh-gateway。
添加权限
sudo chown -R luopan-deploy:luopan-deploy /home/luopan-deploy/.ssh
sudo chmod 700 /home/luopan-deploy/.ssh
sudo chmod 600 /home/luopan-deploy/.ssh/authorized_keys本地连接测试
ssh \
-i ~/.ssh/luopan_github_actions \
-o IdentitiesOnly=yes \
luopan-deploy@服务器IP \
"whoami"应该就在返回:
This SSH key only permits:
deploy
deploy <40-character lowercase Git SHA>腾讯云添加白名单

在白名单管理中添加指定用户的放行规则

总结
按照上面的步骤走完,就不会再提示报警信息了,而且同时安全性也能相当高。
其实今天我才真正理解了 linux 不同用户的设计是为什么了,之前我常常都是只使用一个用户或者 root,但是为了安全可靠,多用户设计是相当先进的。🥹
评论
注册或登录后即可评论。