Triển khai thẳng từ GitHub Actions lên VPS có thể an toàn ở mức production nếu bạn áp dụng least privilege, tách khóa, dùng Environment secrets, pin known_hosts và có đường lui. Bài này đưa ra quy trình thực chiến, YAML chạy được, kèm verify, audit và rollback.
- TL;DR checklist
- Tạo user deploy riêng, không dùng root; cấp đúng quyền thư mục ứng dụng.
- Tạo SSH key riêng cho CI (khác với key cá nhân), chỉ nạp private key vào GitHub Secrets.
- Pin host key (known_hosts) đã xác thực, không ssh-keyscan mù.
- Dùng GitHub Environments + Environment secrets cho production, bật bảo vệ (review/approval).
- Workflow đặt permissions tối thiểu, tránh in secrets ra log.
- Verify sau deploy, ghi nhận audit; có kịch bản rollback.
Điều kiện, phạm vi và cảnh báo
- Đã có VPS với OpenSSH server bật sẵn; máy local có OpenSSH client. Tham chiếu cú pháp theo OpenSSH manual.
- Quyền quản trị repo GitHub để tạo Environments/Secrets và sửa workflow.
- Thông tin VPS: IP, user, port. Ví dụ dùng user
deploy, IP203.0.113.10, port22. Hãy thay bằng thông tin thật. - Phiên bản/thời điểm tham chiếu: quy trình kiểm thử với GitHub Actions (ubuntu-latest) và OpenSSH phổ biến, cập nhật đến 08/2026. Tên directive/command dựa trên OpenSSH manual.
- Cảnh báo rủi ro
- Sai cấu hình SSH/Firewall có thể mất kết nối. Luôn giữ một phiên SSH đang mở để rollback và nên có console/VNC của nhà cung cấp.
- Không dùng private key cá nhân trong CI. Dùng key chuyên biệt cho CI, hạn chế quyền bằng authorized_keys options.
- Không chèn lệnh in secrets (echo, set -x) vào log.
Bước 0: Tạo user deploy tối thiểu quyền (khuyến nghị)
# Đăng nhập với tài khoản có sudo trên VPS
sudo adduser --disabled-password --gecos "" deploy
# Thư mục ứng dụng ví dụ
sudo mkdir -p /var/www/app
sudo chown -R deploy:deploy /var/www/app
Giải thích: tách user deploy giúp giới hạn phạm vi tác động. Ứng dụng nên chạy bằng user này để tránh cần sudo trong deploy.
Bước 1: Tạo SSH key RIÊNG cho CI trên máy của bạn
Khuyên dùng Ed25519 (nhẹ, hiện đại). Không đặt passphrase cho key CI để runner không cần nhập tay (bù lại, bạn phải giới hạn quyền bằng options phía server).
ssh-keygen -t ed25519 -f ~/.ssh/ci_github_actions -C "ci@your-repo (GitHub Actions)"
# File tạo ra:
# Private: ~/.ssh/ci_github_actions
# Public : ~/.ssh/ci_github_actions.pub
Xem nội dung public key:
cat ~/.ssh/ci_github_actions.pubKhông chia sẻ private key (~/.ssh/ci_github_actions); chỉ public key (.pub) mới được phép công khai.
Bước 2: Nạp public key vào VPS và giới hạn quyền
Đăng nhập VPS bằng tài khoản deploy (lần đầu có thể qua mật khẩu hoặc sudo từ user khác):
ssh deploy@203.0.113.10 -p 22
mkdir -p ~/.ssh && chmod 700 ~/.ssh
nano ~/.ssh/authorized_keys # dán toàn bộ nội dung file ci_github_actions.pub
chmod 600 ~/.ssh/authorized_keys
Tăng an toàn bằng authorized_keys options (tham khảo sshd_config/authorized_keys trong OpenSSH manual). Ví dụ chặn port-forwarding/PTY và cố định lệnh triển khai:
# Ví dụ một dòng trong ~/.ssh/authorized_keys (rút ngắn phần key cho gọn dòng)
command="/usr/local/bin/deploy_app.sh",no-pty,no-agent-forwarding,no-port-forwarding ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAI... ci@your-repo
Tạo script cố định trên server (chỉ deploy làm được những gì bạn cho phép):
sudo tee /usr/local/bin/deploy_app.sh >/dev/null <<'SH'
#!/usr/bin/env bash
set -euo pipefail
APP_PATH=/var/www/app
cd "$APP_PATH"
prev=$(git rev-parse --verify HEAD || echo "")
trap 'if [ -n "$prev" ]; then echo "[rollback] $prev"; git reset --hard "$prev"; fi' ERR
# Pull và build ví dụ Node.js
if [ ! -d .git ]; then
git init
git remote add origin git@github.com:your-org/your-repo.git
git fetch --depth=1 origin main
git checkout -f FETCH_HEAD
else
git fetch --prune origin
git checkout -f main
git pull --ff-only origin main
fi
npm ci
npm run build
# Khởi động/reload theo stack của bạn
pm2 reload app || pm2 start ecosystem.config.js --only app
# Ghi nhận bản tốt gần nhất
git rev-parse HEAD > .last_good_sha
SH
sudo chmod 755 /usr/local/bin/deploy_app.sh
Kết quả mong đợi: user deploy có thể đăng nhập bằng key CI; khi kết nối, chỉ chạy được /usr/local/bin/deploy_app.sh, không mở port-forward/PTY.
Rollback nhanh nếu script hỏng: sửa/đổi tên deploy_app.sh qua phiên SSH còn mở, hoặc tạm thời bỏ command=... khỏi authorized_keys để thao tác thủ công (khôi phục lại ngay sau khi sửa xong).
Bước 3: Xác thực host key và kiểm tra đăng nhập
Lấy fingerprint host key ED25519 trên VPS (chạy trên VPS):
sudo ssh-keygen -lf /etc/ssh/ssh_host_ed25519_key.pub
# Ví dụ output: 256 SHA256:AbCdEf... hostname (ED25519)
Từ máy local, thu host key và so sánh fingerprint với kết quả trên:
ssh-keyscan -p 22 203.0.113.10 | ssh-keygen -lf -
Nếu khớp, lưu host key để pin trong pipeline (an toàn hơn TOFU):
ssh-keyscan -p 22 203.0.113.10 >> ~/.ssh/known_hosts
Kiểm tra đăng nhập không mật khẩu từ máy local:
ssh -i ~/.ssh/ci_github_actions -p 22 deploy@203.0.113.10
Kỳ vọng: vào được mà không hỏi mật khẩu; nếu sai sẽ gặp Permission denied (publickey).
Bước 4: Tạo Environment, Secrets và Vars trên GitHub
- Repo → Settings → Environments → New environment → tạo production. Bật bảo vệ (Required reviewers/Wait timer) để kiểm soát ai được deploy.
- Trong environment production, thêm
- Secrets:
SSH_PRIVATE_KEY: nội dung file~/.ssh/ci_github_actions(bao gồm BEGIN/END).KNOWN_HOSTS: nguyên dòng host key đã pin (từ bước 3), ví dụ203.0.113.10 ssh-ed25519 AAAA....
- Vars (Environment variables):
VPS_HOST=203.0.113.10VPS_PORT=22VPS_USER=deployAPP_PATH=/var/www/app
- Secrets:
Giải thích ngắn: Environment secrets chỉ hiển thị cho job chạy trong environment đó và có thể yêu cầu phê duyệt trước khi lộ cho runner (tham khảo tài liệu GitHub Actions secrets).
Bước 5: Workflow GitHub Actions (tối thiểu quyền, có verify và rollback)
Tạo file .github/workflows/deploy.yml:
name: Deploy
on:
push:
branches: ["main"]
permissions:
contents: read # tối thiểu để checkout code
concurrency:
group: deploy-production
cancel-in-progress: true
jobs:
deploy:
name: Deploy to production VPS
runs-on: ubuntu-latest
environment:
name: production
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Start ssh-agent and add key
uses: webfactory/ssh-agent@v0.9.0
with:
ssh-private-key: ${{ secrets.SSH_PRIVATE_KEY }}
- name: Pin known_hosts
run: |
mkdir -p ~/.ssh
echo "${{ secrets.KNOWN_HOSTS }}" >> ~/.ssh/known_hosts
chmod 644 ~/.ssh/known_hosts
- name: Deploy via SSH (idempotent, có rollback on error)
env:
HOST: ${{ vars.VPS_HOST }}
PORT: ${{ vars.VPS_PORT }}
USER: ${{ vars.VPS_USER }}
APP_PATH: ${{ vars.APP_PATH }}
run: |
set -euo pipefail
ssh -p "$PORT" -o StrictHostKeyChecking=yes "$USER@$HOST" "bash -lc '
set -euo pipefail
cd $APP_PATH
prev=\"\$(git rev-parse --verify HEAD || echo \"\")\"
trap '\''if [ -n \"$prev\" ]; then echo \"[rollback] $prev\"; git reset --hard \"$prev\"; fi'\'' ERR
git fetch --prune origin || true
if [ ! -d .git ] || ! git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
git init
git remote remove origin 2>/dev/null || true
git remote add origin git@github.com:your-org/your-repo.git
git fetch --depth=1 origin main
git checkout -f FETCH_HEAD
else
git checkout -f main || git checkout -f -b main
git pull --ff-only origin main
fi
npm ci
npm run build
pm2 reload app || pm2 start ecosystem.config.js --only app
git rev-parse HEAD > .last_good_sha
'"
- name: Smoke test
env:
HOST: ${{ vars.VPS_HOST }}
PORT: ${{ vars.VPS_PORT }}
USER: ${{ vars.VPS_USER }}
run: |
set -euo pipefail
# ví dụ healthcheck HTTP cục bộ qua SSH
ssh -p "$PORT" -o StrictHostKeyChecking=yes "$USER@$HOST" "curl -fsS http://127.0.0.1:3000/health || exit 1"
Kết quả mong đợi: workflow chỉ chạy khi push nhánh main; yêu cầu phê duyệt nếu bạn bật bảo vệ environment; deploy thành công và smoke test OK.
Verify nhanh: mở tab Actions → chọn run gần nhất → xem các bước Pin known_hosts, Deploy via SSH, Smoke test đều xanh. Trên server, kiểm tra /var/log/auth.log hoặc journalctl -u ssh để thấy phiên SSH từ GitHub runner.
Nâng cao bảo mật sshd và hệ thống
- Chỉ cho phép key: sửa
/etc/ssh/sshd_config→PasswordAuthentication no, sau đósudo systemctl restart sshd. Đừng đóng phiên SSH hiện tại cho tới khi xác nhận phiên mới đăng nhập được. - Cân nhắc
PermitRootLogin no, và chỉ cho phépAllowUsers deploynếu phù hợp (xem sshd_config trong manual). - Bật tường lửa (UFW) chỉ mở cổng cần thiết. Nếu bạn dùng Nginx để phục vụ web, tham khảo bài Setup website với Nginx trên VPS Ubuntu.
- Giới hạn tốc độ đăng nhập/Fail2ban để giảm brute-force.
Roll back khi mất kết nối vì chỉnh sshd: từ phiên SSH còn mở, hoàn lại cấu hình cũ và sudo systemctl restart sshd; nếu không, dùng console của nhà cung cấp để sửa sshd_config.
Kiểm toán (audit) và ghi nhận
- GitHub: Environment → xem Deployment history/Reviewers; Actions → xem run logs (ai trigger, SHA nào).
- Server:
/var/log/auth.loghoặcjournalctl -u sshđể theo dõi đăng nhập; có thể mở rộngcommand=trongauthorized_keysđể ghi log qualogger.
Lỗi thường gặp và cách xử lý
- Permission denied (publickey): kiểm tra đúng public key (.pub) đã vào
authorized_keys; quyền~/.ssh=700,authorized_keys=600; đúng user/thư mục. - Host key verification failed: bạn chưa pin hoặc host key thay đổi. So sánh fingerprint từ server với
ssh-keyscan | ssh-keygen -lf -; cập nhật secretKNOWN_HOSTSkhi đổi IP/máy (xem thêm: nếu thay IP trên distro cũ, tham khảo Đổi IP VPS CentOS 7). - Secret rỗng trong Actions: kiểm tra tên
${{ secrets.SSH_PRIVATE_KEY }}và job đã gán environment chứa secret đó. - YAML sai format: thụt lề không đúng, hoặc viết
$ secrets.Xthay vì${{ secrets.X }}. - Không thể chạy
pm2hoặc build: đảm bảo các tool đã cài trên VPS và PATH có sẵn cho user deploy (ví dụ, nvm cầnbash -lcđể load profile).
FAQ
- Khác nhau giữa deploy key của GitHub và SSH key để SSH vào VPS? Deploy key của GitHub cấp quyền đọc/ghi repo qua SSH cho máy chủ (để
git pulltừ VPS). Còn SSH key trong bài dùng để runner SSH vào VPS. Bạn có thể dùng cả hai đồng thời. - Dùng repository secrets hay environment secrets? Production nên dùng environment secrets để giới hạn phạm vi và bật phê duyệt. Repository secrets phù hợp cho dev/staging.
- Triển khai nhiều VPS? Tạo nhiều environment (staging, production-region) với vars/secrets riêng; cùng một workflow nhưng điều kiện nhánh/label khác.
- Có nên đổi port SSH? Có thể giúp giảm noise, nhưng không thay thế xác thực bằng key và hardening khác.
Tổng kết
- Tạo user deploy tối thiểu quyền → tạo SSH key riêng cho CI → nạp public key với authorized_keys options.
- Pin known_hosts đã xác thực → dùng Environment + Secrets để bảo vệ production.
- Workflow tối thiểu quyền, có verify và rollback → theo dõi audit ở GitHub và server.
Tài liệu tham khảo
- GitHub Docs: Using secrets in GitHub Actions
- OpenSSH Manual: Manual pages (xem thêm ssh-keygen(1), sshd_config(5), ssh-keyscan(1))