
Автоматический 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 проверьте четыре ситуации:
- Ошибка теста не запускает задание публикации.
- Ключ не позволяет выполнить произвольную SSH-команду.
- Два запуска в одной группе не переключают сайт одновременно.
- Неуспешный smoke-тест оставляет понятный журнал и возвращает совместимый релиз по принятой политике.
Проверьте также ротацию ключа: добавьте новый открытый ключ, выполните один успешный deploy, удалите старый и убедитесь, что старый приватный ключ больше не входит.
Что делать при компрометации workflow
Остановите environment или отключите workflow, удалите открытый ключ с VPS и отзовите связанные токены. Затем проверьте журнал GitHub Actions, SSH-входы, хеш опубликованного релиза и изменения на сервере. Простого исправления YAML недостаточно, если злоумышленник уже получил секрет или выполнил команду.
Подготовку Git выполняйте по проверочному списку перед публикацией, сервер должен использовать атомарные каталоги релизов, а итог подтверждается smoke-тестом.
Источники
Самопроверка
Проверьте, что материал усвоен
Ответьте на все вопросы. Результат сохранится только в этом браузере и будет учтён в статистике прочитанных материалов.
Разбираем коротко
Частые вопросы
Что такое CI в процессе публикации?
Это автоматический запуск одинаковых проверок для выбранного коммита. Deploy добавляется отдельным заданием только после успешной сборки и принятых правил доступа.
Почему deploy-задание нужно отделять от проверки pull request?
Непроверенный код не должен получать production-секреты. Проверки могут выполняться для pull request, а доступ к окружению production выдаётся только заданию из разрешённой ветки после защитных правил.
Можно ли хранить приватный SSH-ключ прямо в workflow-файле?
Нет. Workflow хранится в Git. Ключ помещают в secret окружения, ограничивают на сервере и регулярно заменяют; открытая часть добавляется только нужной учётной записи deploy.


