Git

GitHub Actions deploy qua SSH: thiết kế key, user và rollback an toàn

Hướng dẫn GitHub Actions deploy qua SSH cho VPS: tạo deploy key, user ít quyền, known_hosts, release bất biến, health check và rollback an toàn cho production.

NHT
· 8 phút đọc
GitHub Actions deploy qua SSH: thiết kế key, user và rollback an toàn

Deploy qua SSH từ GitHub Actions phù hợp với VPS nhỏ khi bạn muốn giữ quy trình đơn giản nhưng vẫn có kiểm thử, release version và rollback. Vấn đề không nằm ở việc chạy một lệnh ssh, mà ở cách thiết kế deploy key, user trên server, known_hosts, quyền thư mục, secrets, health check và trạng thái release để một lần deploy lỗi không làm website mất dịch vụ.

Bài viết này xây dựng một mô hình thực tế cho website PHP hoặc Node.js: GitHub Actions build và test, kết nối bằng deploy user không phải root, upload release vào thư mục riêng, cập nhật symlink current, chạy smoke test rồi rollback nếu health check thất bại. Các giá trị private key và host thật không xuất hiện trong repository.

1. Mô hình deploy SSH tối thiểu nhưng an toàn

Có ba thành phần cần tách biệt. GitHub Actions là control plane chạy workflow; máy chủ VPS là managed host; deploy user là tài khoản chỉ có quyền cần thiết để nhận release và điều khiển ứng dụng. Workflow không nên đăng nhập bằng tài khoản root, không nên copy toàn bộ workspace vào thư mục đang phục vụ traffic và không nên chạy migration không thể đảo ngược trước khi biết release mới hoạt động.

SSH chỉ là kênh vận chuyển và thực thi lệnh. An toàn của deployment phụ thuộc vào danh tính, quyền hạn, xác minh host và khả năng quay về release trước.Nguyên tắc vận hành tổng hợp từ GitHub Docs và OpenSSH manual.
Thành phầnTrách nhiệmKhông nên làm
GitHub ActionsCheckout, test, build artifact, gọi lệnh deployIn secret hoặc chạy deploy từ pull request không tin cậy
Deploy userGhi release, đổi symlink, reload service trong phạm vi cho phépCó toàn quyền root hoặc dùng chung với user quản trị
VPSPhục vụ traffic, lưu release và logGhi đè trực tiếp source đang chạy
known_hostsXác minh host key đã biếtTin mù quáng output ssh-keyscan chưa đối chiếu

2. Tạo deploy user và quyền thư mục

Trên VPS, tạo user riêng cho CI/CD. User này cần shell phù hợp nếu workflow phải chạy lệnh, thư mục home riêng và quyền trên release directory. Nếu service cần reload, có thể cấp một sudo rule giới hạn cho đúng lệnh systemctl hoặc dùng cơ chế owner/group thay vì cho sudo toàn quyền.

# Thực hiện một lần trên VPS bằng tài khoản quản trị
sudo adduser --disabled-password --gecos "" deploy
sudo install -d -o deploy -g deploy -m 0750 /srv/myapp/releases
sudo install -d -o deploy -g deploy -m 0750 /srv/myapp/shared
sudo install -d -o deploy -g deploy -m 0750 /srv/myapp/current

# Chỉ cấp lệnh cần thiết nếu thực sự phải reload service
# deploy ALL=(root) NOPASSWD: /usr/bin/systemctl reload myapp

Tùy ứng dụng, shared directory có thể chứa .env, uploads hoặc storage link. Các file này không nên bị xóa khi release mới được thay thế. Với Ghost, Laravel hoặc Node.js, hãy xác định rõ process manager, owner của file và lệnh reload trước khi mở quyền cho deploy user.

3. Tạo SSH key và lưu trong GitHub Secrets

Tạo một key pair riêng cho repository hoặc environment production. Private key chỉ được lưu trong GitHub Secrets, không commit vào source và không dán vào log. Public key đặt trong ~/.ssh/authorized_keys của deploy user với comment để audit. Nếu nhiều môi trường, dùng key hoặc environment secret tách biệt để giảm phạm vi ảnh hưởng khi một key bị thu hồi.

# Tạo key ở máy quản trị, không tạo private key trong workflow
ssh-keygen -t ed25519 -C "technt-deploy-prod" -f ./technt_deploy_prod

# Trên VPS, thêm public key cho deploy user
sudo install -d -m 0700 -o deploy -g deploy /home/deploy/.ssh
sudo sh -c 'cat technt_deploy_prod.pub >> /home/deploy/.ssh/authorized_keys'
sudo chown deploy:deploy /home/deploy/.ssh/authorized_keys
sudo chmod 0600 /home/deploy/.ssh/authorized_keys

Trong GitHub, tạo các secrets riêng như DEPLOY_HOST, DEPLOY_USER, DEPLOY_PRIVATE_KEY và DEPLOY_KNOWN_HOSTS. Host, port và thư mục đích có thể là variables nếu không nhạy cảm; private key và token phải là secrets. GitHub khuyến nghị principle of least privilege, mask dữ liệu nhạy cảm và rotate secret nếu bị lộ.

4. known_hosts và rủi ro MITM

SSH client cần biết host key của VPS. Cách an toàn hơn là quản trị viên lấy fingerprint từ console hoặc nhà cung cấp, đối chiếu ngoài kênh rồi lưu dòng known_hosts vào GitHub Secret. ssh-keyscan chỉ thu thập public host key; OpenBSD cảnh báo công cụ này không thể tự xác minh tính xác thực và output phải được kiểm tra out-of-band.

# Lấy key để quản trị viên đối chiếu fingerprint trước
ssh-keyscan -t ed25519 -p 22 example.com > known_hosts
ssh-keygen -lf known_hosts

# Trong workflow, không dùng StrictHostKeyChecking=no
mkdir -p ~/.ssh
printf '%s
' "$DEPLOY_KNOWN_HOSTS" > ~/.ssh/known_hosts
chmod 600 ~/.ssh/known_hosts

Không dùng StrictHostKeyChecking=no chỉ để tránh lỗi lần đầu kết nối. Cách đó biến việc xác minh host thành hình thức và có thể cho phép kết nối đến máy giả mạo. Nếu VPS thay key hợp lệ, cần quy trình cập nhật known_hosts có kiểm duyệt và ghi lại lý do.

5. Tách build, test và deploy trong workflow

Một workflow an toàn thường có job test trước, sau đó job deploy chỉ chạy khi test thành công và branch phù hợp. Không nên để mọi pull request tự động dùng secret production. Dùng environment production với required reviewers hoặc điều kiện branch để bảo vệ bước deploy. Quyền GITHUB_TOKEN cũng nên đặt read-only mặc định và tăng riêng cho job thực sự cần.

name: Deploy over SSH
on:
  push:
    branches: [main]

permissions:
  contents: read

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v6
      - run: ./ci/test.sh

  deploy:
    needs: test
    runs-on: ubuntu-latest
    environment: production
    steps:
      - uses: actions/checkout@v6
      - name: Build artifact
        run: ./ci/build.sh dist
      - name: Upload and activate release
        env:
          DEPLOY_HOST: ${{ secrets.DEPLOY_HOST }}
          DEPLOY_USER: ${{ secrets.DEPLOY_USER }}
          DEPLOY_KEY: ${{ secrets.DEPLOY_PRIVATE_KEY }}
        run: ./ci/deploy.sh

6. Upload release bất biến thay vì ghi đè current

Mỗi lần deploy nên có release id, chẳng hạn commit SHA hoặc timestamp. Upload artifact vào /srv/myapp/releases/, kiểm tra file rồi mới đổi symlink current. Process đang chạy có thể tiếp tục dùng release cũ trong khi release mới được chuẩn bị; sau khi smoke test đạt, reload service hoặc chuyển traffic theo cách ứng dụng yêu cầu.

set -Eeuo pipefail
release="${GITHUB_SHA:?missing sha}"
root=/srv/myapp
release_dir="$root/releases/$release"

ssh "$DEPLOY_USER@$DEPLOY_HOST" "mkdir -p '$release_dir'"
rsync -az --delete -e "ssh" dist/   "$DEPLOY_USER@$DEPLOY_HOST:$release_dir/"
ssh "$DEPLOY_USER@$DEPLOY_HOST"   "ln -sfn '$release_dir' '$root/current' && sudo systemctl reload myapp"

Tên release nên chỉ chứa ký tự được kiểm soát từ workflow, không lấy trực tiếp input tùy ý từ commit message hoặc pull request rồi nối vào shell command. Khi dùng rsync, kiểm tra kỹ source/destination và exclude shared data. Tách release khỏi shared storage giúp rollback không làm mất upload hoặc file cấu hình.

7. Health check sau deploy và rollback

Sau khi kích hoạt release, gọi endpoint health hoặc kiểm tra process, port và log. Nếu endpoint trả lỗi, symlink current phải quay về release trước; sau đó reload service và báo workflow thất bại. Rollback không thể khôi phục một migration database phá schema, vì vậy migration backward-compatible và backup database vẫn là phần riêng của kế hoạch release.

set -Eeuo pipefail
previous="$(ssh "$DEPLOY_USER@$DEPLOY_HOST"   "readlink -f /srv/myapp/current || true")"
release_dir="/srv/myapp/releases/$release"

ssh "$DEPLOY_USER@$DEPLOY_HOST"   "ln -sfn '$release_dir' /srv/myapp/current && sudo systemctl reload myapp"

if ! curl --fail --silent --show-error --max-time 10     "https://example.com/health"; then
  ssh "$DEPLOY_USER@$DEPLOY_HOST"     "ln -sfn '$previous' /srv/myapp/current && sudo systemctl reload myapp"
  exit 1
fi

Giữ lại một số release gần nhất theo retention, nhưng không xóa release trước khi smoke test và thời gian quan sát kết thúc. Log deploy cần ghi commit SHA, actor, thời điểm, release mới, release cũ, kết quả health check và kết quả rollback nếu có.

8. SSH command injection và secret trong log

Mọi biến từ commit, tag, branch hoặc input workflow đều có thể chứa ký tự shell. Đưa giá trị vào environment rồi trích dẫn đúng cách; tốt hơn nữa là dùng action hoặc script đã kiểm soát thay vì nối chuỗi command dài. Không echo private key, không dùng set -x quanh đoạn xử lý secret và kiểm tra log sau lần chạy thử.

  • Không đặt private key trong file repository hoặc artifact.
  • Không in toàn bộ environment khi debug workflow.
  • Không dùng input pull request làm tên thư mục hoặc lệnh shell mà không whitelist.
  • Không để production secrets truy cập từ workflow chạy trên mã chưa được review.
  • Không cho deploy user quyền root toàn hệ thống chỉ vì cần reload một service.

9. Rollback application khác rollback database

Rollback code thường chỉ là đổi symlink về release cũ. Rollback database phức tạp hơn vì schema mới có thể không tương thích với code cũ. Với Laravel, migration nên triển khai theo chiến lược expand-and-contract; với Node.js, API contract và queue payload cũng cần tương thích trong thời gian chuyển đổi. Nếu deploy có database change, hãy đặt bước backup, migration và kế hoạch phục hồi thành một runbook riêng.

Bài Laravel deploy không downtime có thể dùng làm phần tham chiếu cho migration và rollback database. Bài quản lý user và sudo trên Linux giúp rà lại mô hình quyền trước khi tạo deploy user.

10. Checklist trước khi bật deploy production

  1. Tạo deploy user riêng, không dùng root và giới hạn quyền thư mục.
  2. Tạo key riêng, lưu private key trong GitHub Secret và đặt public key đúng authorized_keys.
  3. Xác minh host fingerprint ngoài kênh trước khi tạo known_hosts.
  4. Tách test/build khỏi deploy và bảo vệ environment production.
  5. Upload vào release directory có id bất biến, không ghi đè current.
  6. Chạy health check sau activate và có lệnh rollback rõ ràng.
  7. Kiểm tra database migration, queue và shared storage trước release.
  8. Review log để chắc chắn secret không bị in hoặc biến input bị injection.
  9. Ghi nhận release SHA, người triển khai và kết quả rollback.

11. Kết luận

GitHub Actions deploy qua SSH có thể phù hợp với VPS nhỏ nếu pipeline coi SSH là một phần của mô hình bảo mật, không phải lối tắt. Key riêng, deploy user ít quyền, known_hosts được xác minh, release bất biến, health check và rollback là năm lớp cần có. Khi hệ thống lớn hơn, có thể chuyển sang image registry, agent hoặc managed deployment, nhưng các nguyên tắc này vẫn giữ nguyên.

Tài liệu tham khảo

  1. GitHub Docs — Secure use reference.
  2. GitHub Docs — Using secrets in GitHub Actions.
  3. OpenBSD manual — ssh-keyscan.