Kết nối GitHub Secrets với VPS để CI/CD an toàn trong 10 phút

Thiết lập triển khai từ GitHub Actions lên VPS theo nguyên tắc tối thiểu quyền: tạo user deploy, SSH key riêng cho CI, dùng environment secrets và bảo vệ môi trường, pin known_hosts, verify sau deploy, audit và có quy trình rollback an toàn.

NHT
· 8 phút đọc
Kết nối GitHub Secrets với VPS để CI/CD an toàn trong 10 phút

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, IP 203.0.113.10, port 22. 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.pub

Khô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

  1. Repo → Settings → Environments → New environment → tạo production. Bật bảo vệ (Required reviewers/Wait timer) để kiểm soát ai được deploy.
  2. 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.10
      • VPS_PORT = 22
      • VPS_USER = deploy
      • APP_PATH = /var/www/app

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_configPasswordAuthentication 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ép AllowUsers deploy nế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.log hoặc journalctl -u ssh để theo dõi đăng nhập; có thể mở rộng command= trong authorized_keys để ghi log qua logger.

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 secret KNOWN_HOSTS khi đổ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.X thay vì ${{ secrets.X }}.
  • Không thể chạy pm2 hoặ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ần bash -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 pull từ 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