Проверенный коммит проходит тесты и ограниченный ключ перед публикацией на сервер
DevOps

Автоматический deploy сайта через GitHub Actions без лишних прав

Строим понятный CI-процесс: отдельно проверяем проект, ограничиваем production-окружение и SSH-ключ, запрещаем параллельные deploy и проверяем релиз.

Содержание

CI, или непрерывная интеграция, — автоматический запуск проверок для конкретного коммита. Когда к проверкам добавляют публикацию, получается цепочка: получить исходники, установить зависимости, собрать сайт, проверить артефакт и только затем обратиться к production-серверу. Автоматизация полезна лишь тогда, когда не превращает любую ошибку workflow в полный доступ к VPS.

Разделите проверку и публикацию

Первое задание должно работать без production-секретов: оно читает репозиторий, запускает тесты и создаёт сборку. Второе получает доступ к окружению production только после успеха первого. Для pull request достаточно проверки; публикацию запускают из защищённой ветки или вручную по принятому правилу.

GitHub Actions выполняет задания на runner — временной машине GitHub или вашем отдельном исполнителе. Код репозитория выполняется на runner с правами задания, поэтому сторонний action и установочный сценарий зависимости следует считать исполняемым кодом, а не безобидной декларацией.

Подготовьте отдельную учётную запись на VPS

Deploy-пользователь не должен входить как root и менять произвольные системные файлы. Ему нужны права только на рабочую копию проекта, каталоги релизов и запуск штатного сценария публикации. Конфигурация Nginx, SSH и firewall меняется отдельной административной процедурой.

На сервере полезно ограничить открытый ключ в authorized_keys принудительной командой. Строка имеет следующий вид; PUBLIC_KEY_MATERIAL заменяется открытой частью отдельного deploy-ключа:

restrict,command="/home/deploy/bin/update-site" ssh-ed25519 PUBLIC_KEY_MATERIAL ci-production

Параметр restrict отключает перенаправление портов, X11 и агент. command заставляет SSH запускать только указанный сценарий, даже если клиент запросил другую команду. Сам /home/deploy/bin/update-site должен принадлежать администратору или быть недоступен deploy-пользователю на запись, иначе ограничение можно обойти правкой файла.

Сначала протестируйте ключ вручную из отдельного терминала и убедитесь, что он запускает только ожидаемую публикацию. Не отключайте парольный доступ или старый рабочий ключ, пока новый путь не проверен во второй сессии.

Ограничьте production-окружение в GitHub

Создайте GitHub Environment с именем production. Разрешите публикацию только из нужной ветки; при необходимости включите ручное подтверждение. В secrets окружения поместите:

  • DEPLOY_SSH_KEY — отдельный приватный ключ без других назначений;
  • DEPLOY_HOST — адрес VPS;
  • DEPLOY_USER — имя ограниченного пользователя;
  • SSH_KNOWN_HOSTS — заранее проверенную строку ключа хоста.

Ключ хоста нельзя бездумно получать через ssh-keyscan в том же недоверенном соединении, которое предстоит защищать. Сверьте fingerprint через панель хостера или уже доверенную SSH-сессию и сохраните подтверждённую строку.

Опишите workflow с минимальными правами

Создайте .github/workflows/deploy.yml. Пример использует официальные actions GitHub, разделяет задания и запрещает двум production-публикациям выполняться одновременно:

name: Verify and deploy

on:
  push:
    branches: [main]
  workflow_dispatch:

permissions:
  contents: read

concurrency:
  group: production-deploy
  cancel-in-progress: false

jobs:
  verify:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version-file: .nvmrc
          cache: npm
      - run: npm ci
      - run: npm run check
      - run: npm run build

  deploy:
    needs: verify
    runs-on: ubuntu-latest
    environment: production
    steps:
      - name: Configure SSH
        env:
          DEPLOY_SSH_KEY: ${{ secrets.DEPLOY_SSH_KEY }}
          SSH_KNOWN_HOSTS: ${{ secrets.SSH_KNOWN_HOSTS }}
        run: |
          install -d -m 0700 ~/.ssh
          printf '%s\n' "$DEPLOY_SSH_KEY" > ~/.ssh/id_ed25519
          chmod 0600 ~/.ssh/id_ed25519
          printf '%s\n' "$SSH_KNOWN_HOSTS" > ~/.ssh/known_hosts
          chmod 0644 ~/.ssh/known_hosts

      - name: Deploy verified main
        env:
          DEPLOY_HOST: ${{ secrets.DEPLOY_HOST }}
          DEPLOY_USER: ${{ secrets.DEPLOY_USER }}
        run: |
          ssh -i ~/.ssh/id_ed25519 \
            -o BatchMode=yes \
            -o StrictHostKeyChecking=yes \
            "$DEPLOY_USER@$DEPLOY_HOST"

permissions: contents: read не даёт временному GITHUB_TOKEN права записи. needs: verify не запускает deploy после неуспешной проверки. Environment открывает свои secrets только заданию публикации, а concurrency оставляет один последовательный поток изменения production.

Файл .nvmrc должен содержать поддерживаемую проектом версию Node.js. Если его нет, задайте проверенную версию в workflow и обновляйте её отдельным изменением. Не используйте плавающие зависимости без lock-файла.

Сделайте серверный сценарий проверяемым

Принудительная команда на сервере должна сама определить допустимый репозиторий и ветку, получить только fast-forward изменение, выполнить штатные проверки и вызвать существующий deploy. Она не должна принимать путь удаления, shell-команду или имя репозитория из SSH-аргумента.

В начале сценария включите остановку при ошибке, установите предсказуемый PATH и перейдите в зафиксированный абсолютный каталог. Перед публикацией проверьте чистое рабочее дерево и ожидаемую ветку. После переключения выполните smoke-тест, а идентификатор релиза свяжите с полным хешем коммита.

Не копируйте универсальный серверный скрипт без учёта владельцев каталогов и действующего deploy. Если проект уже имеет npm run deploy или scripts/deploy.sh, CI должен вызывать именно этот проверенный путь, а не создавать второй способ публикации.

Проверьте отказ, а не только успешный запуск

До включения автоматического deploy проверьте четыре ситуации:

  1. Ошибка теста не запускает задание публикации.
  2. Ключ не позволяет выполнить произвольную SSH-команду.
  3. Два запуска в одной группе не переключают сайт одновременно.
  4. Неуспешный smoke-тест оставляет понятный журнал и возвращает совместимый релиз по принятой политике.

Проверьте также ротацию ключа: добавьте новый открытый ключ, выполните один успешный deploy, удалите старый и убедитесь, что старый приватный ключ больше не входит.

Что делать при компрометации workflow

Остановите environment или отключите workflow, удалите открытый ключ с VPS и отзовите связанные токены. Затем проверьте журнал GitHub Actions, SSH-входы, хеш опубликованного релиза и изменения на сервере. Простого исправления YAML недостаточно, если злоумышленник уже получил секрет или выполнил команду.

Подготовку Git выполняйте по проверочному списку перед публикацией, сервер должен использовать атомарные каталоги релизов, а итог подтверждается smoke-тестом.

Источники

Рекламное местоВаша компания здесьРазместить рекламу

Самопроверка

Проверьте, что материал усвоен

Ответьте на все вопросы. Результат сохранится только в этом браузере и будет учтён в статистике прочитанных материалов.

01Какие права нужны стандартному GITHUB_TOKEN для проверки и SSH-deploy?
02Зачем задавать concurrency для production?
03Где должен выполняться rollback?

Разбираем коротко

Частые вопросы

Что такое CI в процессе публикации?

Это автоматический запуск одинаковых проверок для выбранного коммита. Deploy добавляется отдельным заданием только после успешной сборки и принятых правил доступа.

Почему deploy-задание нужно отделять от проверки pull request?

Непроверенный код не должен получать production-секреты. Проверки могут выполняться для pull request, а доступ к окружению production выдаётся только заданию из разрешённой ветки после защитных правил.

Можно ли хранить приватный SSH-ключ прямо в workflow-файле?

Нет. Workflow хранится в Git. Ключ помещают в secret окружения, ограничивают на сервере и регулярно заменяют; открытая часть добавляется только нужной учётной записи deploy.