CI и публикация¶
Pipeline публикации¶
qa_portal_verify проверяет синтаксис Bash/JavaScript, запускает pytest, Ruff,
строгую сборку MkDocs и проверку artifact на symlink, hardlink и special files.
Результат хранится семь дней.
После успешной проверки pipeline защищённой ветки develop автоматически
запускает публикацию. Отдельная ручная job остаётся для диагностики:
| Job | Запуск | Действие | Меняет document root |
|---|---|---|---|
qa_portal_preflight |
Вручную | Проверяет HTTP marker, host key, lock, права, protocol и SHA-256 publisher | Нет |
qa_portal_publish |
Автоматически после qa_portal_verify |
Повторяет preflight, передаёт artifact, публикует и выполняет HTTP smoke | Да |
Preflight требует дополнительного подтверждения и имеет allow_failure: true.
Publish является обязательной job: ошибка доставки или внешнего HTTP smoke
завершает pipeline с ошибкой. Результат выпуска также фиксируется в deployment
environment qa/test-2.
Зафиксированная цель¶
Из CI нельзя передать другой host или document root:
- SSH host:
92.63.104.89:22; - public URL:
http://92.63.104.89/; - document root:
/var/www/test-2.business-pad.com; - отчёты:
/var/www/test-2.business-pad.com/allure; - private marker:
.businesspad-qa-portal-rootсо значениемtest-2.business-pad.comи одним переводом строки; - public marker:
/.well-known/businesspad-qa-portal-originс тем же точным значением.
Host и URL зафиксированы в tools/deploy_qa_portal.sh. Пути, markers и
допустимые SSH-команды зафиксированы в root-owned копии
tools/publish_qa_portal.py на сервере. Pipeline отправляет этому коду только
ограниченный несжатый tar-архив статических файлов и его SHA-256.
Перед SSH wrapper обращается к фиксированному IP по HTTP и требует точное содержимое public marker. SSH-публикация остаётся зашифрованной и проверяет pinned host key, но содержимое портала при просмотре по HTTP не защищено от чтения и подмены в сети. Поэтому портал не должен принимать или показывать реальные пароли, токены, персональные либо финансовые данные.
Переменные GitLab¶
В Settings → CI/CD → Variables создать три переменные. Для всех установить
Protect variable, отключить expansion и выбрать Environment scope
qa/test-2.
| Variable | Type | Visibility | Значение |
|---|---|---|---|
QA_PORTAL_DEPLOY_USER |
Variable | Visible | отдельный non-root пользователь, например bp_qa_portal |
QA_PORTAL_SSH_PRIVATE_KEY |
File | Visible | приватная часть отдельного deploy key без passphrase |
QA_PORTAL_SSH_KNOWN_HOSTS |
File | Visible | заранее проверенная запись именно для 92.63.104.89 |
Многострочный private key нельзя помечать Masked/Hidden: GitLab требует для такой File variable режим Visible. Последняя строка ключа должна заканчиваться LF. Значение по-прежнему защищено настройками protected branch, protected variable и environment scope; Visible не означает вывод в job log.
Пароль сервера, private key и другие секреты нельзя вводить в форму запуска
manual preflight. Значения из этой формы доступны пользователям, способным
повторно запустить job. Password-based SSH и StrictHostKeyChecking=no не
поддерживаются.
Одноразовая подготовка сервера¶
Подготовку выполняет администратор отдельно от CI. Сначала подтверждается
фактический Nginx root и снимается inventory:
find /var/www/test-2.business-pad.com -mindepth 1 -maxdepth 1 -printf '%f\n' | sort
Portal/unmanaged entries резервируются отдельно от большого allure; retention
Allure остаётся ответственностью report producer. Если уже есть index.html,
assets или другие имена будущей сборки, первый publish завершится ошибкой и
ничего не перезапишет. Миграцию таких файлов проводят вручную только после
проверенного backup и плана восстановления.
1. Отдельная учётная запись¶
Создать системного пользователя bp_qa_portal без sudo и с заблокированным
паролем. Обычный root account и его ключи в GitLab не используются:
useradd --system --user-group --create-home \
--home-dir /var/lib/bp_qa_portal --shell /bin/sh bp_qa_portal
passwd --lock bp_qa_portal
Если пользователь уже существует, его не пересоздают: сначала проверяют id,
passwd -S bp_qa_portal и sudo -l -U bp_qa_portal. В списке sudo не должно
быть разрешённых команд.
Document root должен быть root-owned, иметь точный режим 1775 и группу
bp_qa_portal, единственным участником которой является одноимённый deploy user.
Все ancestors (/, /var, /var/www) должны быть root-owned и не иметь group/
world write. Sticky bit не позволяет deploy user удалить root-owned markers или
каталог Allure, принадлежащий другому пользователю.
После проверки Nginx и backup подготавливаются root и markers:
install -d -o root -g bp_qa_portal -m 1775 /var/www/test-2.business-pad.com
install -d -o root -g root -m 0755 \
/var/www/test-2.business-pad.com/.well-known
printf 'test-2.business-pad.com\n' \
> /var/www/test-2.business-pad.com/.businesspad-qa-portal-root
printf 'test-2.business-pad.com\n' \
> /var/www/test-2.business-pad.com/.well-known/businesspad-qa-portal-origin
chown root:root \
/var/www/test-2.business-pad.com/.businesspad-qa-portal-root \
/var/www/test-2.business-pad.com/.well-known/businesspad-qa-portal-origin
chmod 0444 \
/var/www/test-2.business-pad.com/.businesspad-qa-portal-root \
/var/www/test-2.business-pad.com/.well-known/businesspad-qa-portal-origin
Каталог allure принадлежит отдельному report producer; bp_qa_portal не
должен владеть им или иметь право записи. Его owner/group нельзя менять
рекурсивной командой из этого runbook. Перед продолжением должны выполняться
условия:
owner(/var/www/test-2.business-pad.com) = root
group(/var/www/test-2.business-pad.com) = bp_qa_portal
members(group bp_qa_portal) = [bp_qa_portal]
mode(/var/www/test-2.business-pad.com) = 1775
owner(/, /var, /var/www) = root
group_or_world_write(/, /var, /var/www) = false
owner(.businesspad-qa-portal-root) = root
mode(.businesspad-qa-portal-root) = 0444
owner(.well-known) = root
mode(.well-known) = 0755
owner(public marker) = root
mode(public marker) = 0444
owner(allure) != bp_qa_portal
writable_by_bp_qa_portal(allure) = false
Проверка выполняется через namei -l, stat -c '%U:%G %a %n' и негативные
sudo -u bp_qa_portal test ! -w <path>. Отдельно подтверждается, что deploy user
может создать и удалить свой пробный файл в document root, но не может изменить
markers или allure.
CI намеренно не создаёт root, markers или allure и отклоняет запуск от root.
Для взаимного исключения всех forced-command процессов нужен отдельный lock вне document root. Каталог принадлежит root и недоступен для подмены deploy user, сам файл принадлежит только deploy user:
install -d -o root -g root -m 0755 /var/lib/businesspad-qa-portal
install -o bp_qa_portal -g bp_qa_portal -m 0600 /dev/null \
/var/lib/businesspad-qa-portal/publish.lock
stat -c '%U:%G %a %n' \
/var/lib/businesspad-qa-portal \
/var/lib/businesspad-qa-portal/publish.lock
Одновременно выполняется только один preflight или publish. Повторный процесс завершается сразу с ошибкой, а не конкурирует за manifest и rename-операции.
2. Root-owned forced command¶
Из проверенного checkout администратор устанавливает publisher вне document root и сверяет digest:
install -d -o root -g root -m 0755 /usr/local/lib/businesspad-qa-portal
sha256sum tools/publish_qa_portal.py
install -o root -g root -m 0555 tools/publish_qa_portal.py \
/usr/local/lib/businesspad-qa-portal/publish_qa_portal.py
sha256sum /usr/local/lib/businesspad-qa-portal/publish_qa_portal.py
Оба SHA-256 должны совпасть; digest установленной версии фиксируется в change record. Каждый CI preflight требует одновременно совпадения версии forced-command protocol и SHA-256 всей root-owned копии publisher. Поэтому после любого изменения файла его нужно переустановить до следующей публикации.
Для deploy key в /var/lib/bp_qa_portal/.ssh/authorized_keys указывается только
forced command:
restrict,command="/usr/bin/python3 -I /usr/local/lib/businesspad-qa-portal/publish_qa_portal.py --forced-command" ssh-ed25519 AAAA... businesspad-qa-portal-ci
Каталог .ssh должен иметь режим 0700, authorized_keys — 0600, оба должны
принадлежать bp_qa_portal. При стабильном исходящем IP runner к options ключа
добавляется from="<runner-ip>/32".
Forced command принимает только preflight и publish <sha256>. Он не даёт CI
shell, SCP, port forwarding или PTY. Изменение server-side publisher требует
отдельного административного review и повторной установки root-owned файла.
Негативная проверка ssh ... uname -a должна завершиться сообщением об
unsupported QA portal SSH command, не выполняя uname.
3. Public route и host key¶
Nginx должен отдавать public marker как text/plain по HTTP на фиксированном IP
без редиректа на другой host. Домен и сертификат не являются частью принятой
схемы публикации внутреннего QA-портала.
В HTTP server block фиксируются document root, portal 404 и доступ только к публичному marker среди dotfiles:
root /var/www/test-2.business-pad.com;
error_page 404 /404.html;
location / {
try_files $uri $uri/ =404;
}
location = /404.html {
internal;
}
location = /.well-known/businesspad-qa-portal-origin {
default_type text/plain;
try_files $uri =404;
}
location ~ /\. {
deny all;
}
После nginx -t проверяют, что неизвестный URL возвращает portal 404.html,
public marker доступен, а private root marker, manifest, staging и backup не
отдаются по HTTP.
ssh-keyscan можно использовать только для получения записи, но не как
доказательство её подлинности. Fingerprint сверяется через независимый доверенный
канал с администратором сервера, после чего строка для IP сохраняется в
QA_PORTAL_SSH_KNOWN_HOSTS. Wrapper дополнительно требует, чтобы File variable
содержала фиксированный host, игнорирует пользовательский SSH config и всегда
использует strict host checking.
Настройки GitLab CE¶
Текущий сервер использует GitLab Community Edition 18.10. Для проекта нужны:
- Ветка
developзащищена; merge в неё разрешён только тем, кто вправе публиковать портал. Защита обязательна: CI rule не запускает deploy-job для незащищённой ветки, а protected variables в неё не передаются. - SSH job выполняет project runner с тегами
dockerиbusinesspad-qa-deploy. Он отмечен как Protected, закреплён за доверенным project/group, не использует privileged mode/Docker socket и имеет исходящий доступ к container/package registries,92.63.104.89:22и92.63.104.89:80. Одна только текущая комбинация тегов не изолирует verify: runner с обоими тегами подходит и для job, требующей толькоdocker. Для физического разделения нужен второй runner и отдельный тегbusinesspad-qa-verify; после его регистрации тег добавляется в verify-job, а deploy runner не получает этот тег. Название тега не заменяет проверку фактического executor type. - Включена настройка Prevent outdated deployment jobs, а Allow job retries for rollback deployments отключена: возврат выполняется новым revert commit/pipeline, не повторным запуском старого artifact.
- Все три deploy variables имеют scope
qa/test-2. Обе remote-job объявляют этот environment; preflight используетaction: verifyи не создаёт deployment.
Protected Environments относятся к GitLab Premium/Ultimate и в текущем CE не
являются границей доступа. Если лицензия изменится, qa/test-2 дополнительно
защищается списком Allowed to deploy. Если разработчикам нужен merge в develop,
но не право публикации, deploy-job и deploy variables нужно вынести в отдельный
проект с более узким membership: одной YAML-настройкой текущего CE эти роли не
разделить.
Что происходит при публикации¶
- Verify-job создаёт artifact и проверяет его типы и лимиты.
- Publish-job повторяет локальную проверку, упаковывает только
site/в несжатый tar, считает SHA-256 и передаёт архив через stdin SSH forced command. - Сервер ограничивает размер всего tar, суммарный payload и число файлов, запрещает path traversal, symlink, hardlink, FIFO, socket и device.
- Перед изменением повторно проверяются exact root, private/public markers, режимы, владельцы и отсутствие коллизий с unmanaged entries.
- Publisher заменяет через private staging/rename только имена из собственного
manifest. При ошибке или обрабатываемом сигнале во время фазы замены до
commit операция откатывается;
allureи чужие имена не принимаются во владение автоматически. Если ошибка возникает уже во время rollback, publisher сохраняет private backup/staging, сообщает их имена и не удаляет единственную копию для ручного восстановления. Любое такое recovery-состояние блокирует следующие preflight/publish до решения администратора. - CI скачивает
site-version.jsonпо HTTP и сравнивает его SHA-256 с artifact. Если этот внешний smoke не проходит, job завершается ошибкой, но уже завершённая серверная транзакция автоматически не откатывается.
Artifact доступен семь дней. Для возврата к старой версии создаётся новый revert
commit в защищённой ветке develop и его новый pipeline; запуск старой deployment
job не используется.
Аварийное восстановление¶
Если publisher сообщает recovery state, новые публикации не запускают. Сначала
администратор сохраняет снимок document root и перечисляет только immediate
private-каталоги текущей транзакции:
find /var/www/test-2.business-pad.com -mindepth 1 -maxdepth 1 \
\( -name '.qa-portal-backup-*' -o -name '.qa-portal-staging-*' \) \
-printf '%M %u:%g %f\n'
Содержимое backup, staging, live root и manifest сопоставляют с сообщением job.
Нельзя автоматически удалять recovery-каталоги или повторять publish: backup
может быть единственной копией предыдущей версии. После ручного восстановления
index.html, всех manifest-owned entries и manifest проверяют markers, права и
HTTP site-version.json; только затем удаляют конкретные уже использованные
recovery-каталоги и повторяют preflight.
Текущая top-level схема даёт короткое окно 404/смешанных статических файлов между
rename старой и новой версии. Для портала это принятый компромисс; полная
read-atomic публикация потребует versioned release directories и одного
переключения Nginx current, при отдельном alias для /allure/.