Skip to content

ci(deploy): harden production deploy auth and add approval gate - #179

Open
cefothe wants to merge 3 commits into
midt-bg:mainfrom
cefothe:ci/harden-deploy-auth
Open

ci(deploy): harden production deploy auth and add approval gate#179
cefothe wants to merge 3 commits into
midt-bg:mainfrom
cefothe:ci/harden-deploy-auth

Conversation

@cefothe

@cefothe cefothe commented Jun 30, 2026

Copy link
Copy Markdown
Contributor

Резюме

Подсилва production deploy пътя с гейт за човешко одобрение и затяга хигиената на CI job-овете. Адресира #89.

  • Approval gate за production (задължителен). Един v* таг сам по себе си вече не пуска прод автоматично. Deploy job-ът спира на „Review deployments“ преди да стигне runner (т.е. преди Cloudflare автентикацията изобщо да се изпълни). Кодифицирано в нов идемпотентен скрипт — scripts/provision-environments.sh — така че protection rule-ите на средата (required reviewers, prevent_self_review, v* deployment tag policy) са възпроизводими и одитируеми вместо невидими кликове в UI-я.
  • Маскиране на секрети. LOG_IP_KEY се регистрира с ::add-mask:: веднага след генерирането му, преди първата му употреба, така че по-късен случаен set -x/echo не може да го изтече.
  • Изричен job timeout. deploy job-ът вече се ограничава до 10 мин вместо 360-мин default на GitHub, така че заклещен wrangler или блокирала мрежа се провалят бързо. Изчакването за одобрение не влиза в таймера (той тръгва чак щом job-ът хване runner).
  • Документация. docs/deploy.md описва approval gate-а, изтичането на token-а + тримесечната ротация, four-eyes уговорката, и защо все още се ползват дълголетни token-и (а не OIDC) — Cloudflare няма OIDC/workload-identity federation за Wrangler към юни 2026 (workers-sdk#11434).

Файлове

Файл Промяна
scripts/provision-environments.sh Нов — кодифицира production required reviewers + v* tag policy
.github/workflows/deploy.yml timeout-minutes: 10, ::add-mask:: за LOG_IP_KEY, бележка за workflow_dispatch tag policy
docs/deploy.md Approval gate, ротация/изтичане на token, four-eyes caveat, OIDC обосновка

Тази репо няма preview.yml/preview-reap.yml, затова preview-частта от референтния подход не се прилага тук.

Какво трябва да се направи ръчно

Protection rule-ите на средата живеят в repo Settings, не в YAML — затова не се прилагат автоматично при merge. След merge изпълнете провизиониращия скрипт веднъж с admin права:

REVIEWER_TEAMS="midt-bg/maintainers" ./scripts/provision-environments.sh
# или с конкретни хора:
REVIEWER_USERS="alice,bob" ./scripts/provision-environments.sh

Изисквания: gh CLI, автентикиран с admin права върху repo-то, и jq. Проверка след това: Settings → Environments → production.

Acceptance (#89)

  • Прод деплой изисква човешко одобрение през Environment (скрипт за required reviewers + prevent_self_review).
  • Cloudflare credential-ите — минимални скоупове вече документирани; добавена ротация + обосновка защо OIDC не е възможен.
  • Deploy job-ът има изричен timeout (timeout-minutes: 10).

Addresses midt-bg#89: tighten Cloudflare deploy credentials and add release
governance to the production deploy path.

- timeout-minutes: 10 on the deploy job (was the 360-min default), so a
  wedged wrangler or stalled network fails fast instead of holding a
  runner for hours; the required-reviewers wait does not count against it
- ::add-mask:: the generated LOG_IP_KEY before it is used, belt-and-
  suspenders against accidental log leakage in a later edit
- scripts/provision-environments.sh: codify the production Environment
  required-reviewers gate + v* deployment tag policy (reproducible and
  auditable, not a hidden UI click); idempotent, converges to exactly one
  v* tag policy and validates reviewer refs
- deploy.yml: note that workflow_dispatch to production only works from a
  v* tag ref once the tag policy is provisioned
- docs/deploy.md: make the production approval gate mandatory, document
  token expiry/quarterly rotation, the four-eyes caveat, and record that
  Cloudflare has no OIDC for Wrangler (so the mitigation is minimal-scope
  + rotation, not keyless auth)
@cefothe
cefothe force-pushed the ci/harden-deploy-auth branch from 5f65bae to 6ff9b56 Compare June 30, 2026 07:34

@lyubomir-bozhinov lyubomir-bozhinov left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Силна стъпка — кодифицираният approval gate + възпроизводимите protection rules са точно каквото иска #89, а скриптът е чист (parameterized jq, валидация на login-ите преди interpolation, отказва празен reviewers[], prevent_self_review). Гейтът е вързан правилно (environment: на deploy job-а).

Едно за provisioning-а — не по кода, а по това кой държи ключовете за прод деплоя: нека required reviewers да са екип (REVIEWER_TEAMS="midt-bg/maintainers"), а не отделен потребител (нищо, че на практика днес това значи само Тодор).

  • Реален four-eyes: както сам отбелязваш, prevent_self_review пази само инициатора. Един-единствен reviewer концентрира одобрението на прод деплоя в едно лице — и може да заключи деплоя, ако този човек е и инициаторът.
  • Прод деплоят е по-добре да минава през група, а не през конкретен личен акаунт.

Скриптът вече го поддържа — само да не се provision-не с примерния единичен reviewer от README-то.

Address review on midt-bg#179: a lone required reviewer concentrates prod-deploy
approval in one person and can lock the deploy if that person is also the
initiator (prevent_self_review only guards the initiator). Lead the docs +
script usage with REVIEWER_TEAMS=midt-bg/maintainers and add an explicit
note not to provision production with the single-user example.
@cefothe

cefothe commented Jun 30, 2026

Copy link
Copy Markdown
Contributor Author

Благодаря! Точна забележка — обнових документацията и скрипта да водят с екип, не с личен акаунт (5cd41d2):

  • docs/deploy.md — добавена изрична бележка „Provision-вайте с екип, не с личен акаунт“ точно до командата: прод одобрението минава през REVIEWER_TEAMS="midt-bg/maintainers", а единичният REVIEWER_USERS пример е само илюстрация на синтаксиса — не за прод.
  • scripts/provision-environments.sh — usage хедърът вече води с REVIEWER_TEAMS=… (маркиран „recommended“); многопотребителската форма е отбелязана като „≥2 за реален four-eyes“.

Самата operational стъпка (подаване на екипа при изпълнение на скрипта след merge) остава ръчна по дизайн — не може да се enforce-не в YAML — но документацията вече го прави недвусмислено.

@lyubomir-bozhinov lyubomir-bozhinov left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Бележката за reviewer-екип е адресирана (commit 5cd41d2) — скриптът и docs вече водят с REVIEWER_TEAMS="midt-bg/maintainers", единичният примерен reviewer отпадна, four-eyes е изричен. Реалното подаване на екипа при provision остава ръчна admin стъпка — коректно, не може да се enforce-не в YAML.

Останалото е чисто: approval gate-ът е вързан правилно (environment: на deploy job-а, v* таг спира на „Review deployments" преди CF auth), скриптът е injection-safe (parameterized jq, валидиран login преди interpolation, отказва празен reviewers[], prevent_self_review), LOG_IP_KEY е маскиран преди употреба, timeout е изричен, а обосновката за дълголетен token вместо OIDC е вярна към юни 2026.

Одобрявам (advisory; финалното одобрение/merge е на maintainer).

@ydimitrof

Copy link
Copy Markdown
Contributor

Прегледах подробно от страна на сигурност, интегритет на данните и OWASP — с фокус върху инжекции, работа с тайни и евентуален зловреден код. Промяната е чиста и подобрява сигурността.

Какво е добре:

  • Няма повърхност за инжекция. Reviewer login-ите/екипите се валидират с regex преди да се интерполират в gh api пътя (изричен guard срещу path traversal), а JSON тялото се сглобява само през параметризиран jq плюс валидиран като цяло число wait_timer. Никъде няма SQL (само CI), няма command injection.
  • Маскирането на тайната е коректно. ::add-mask:: се регистрира веднага след openssl rand и преди стойността да се ползва, а ключът стига до wrangler по stdin — никога като аргумент или логнат ред.
  • Няма зловреден / supply-chain риск. Всички API заявки са към собствената среда на репото през автентикиран CLI; няма външен трафик, няма обфускация; action-ите остават SHA-pinned; няма промени по зависимостите.
  • Идемпотентен и защитен: провал при LIST е фатален преди конвергиране на tag policy-то, пагинацията е обработена, празен/над-6 набор reviewer-и се отхвърля.
  • Покрива чисто CI/CD: затягане на Cloudflare deploy credentials + approval gate за production #89: approval gate (required reviewers + prevent_self_review), изричен timeout-minutes: 10 и добре обоснован отказ от OIDC.

Дребни, незадължителни:

  1. Гейтът става реален едва щом admin изпълни provision-environments.sh след merge — дотогава v* таг още пуска прод без гейт. Това е присъщо на GitHub и PR-ът го документира; само да се уверим, че някой поема тази стъпка след merge.
  2. $REPO се интерполира в API пътищата без regex guard-а, който прилагаш за потребители/екипи. Нисък риск (admin подава стойността), но симетрична проверка би била по-спретната.

Благодаря, че го кодифицира вместо да остане като невидимо UI състояние.

Заключение: Одобрявам — няма проблеми със сигурността, интегритета на данните или инжекции; двете бележки по-горе са незадължителна шлифовка.

Address security review on midt-bg#179 (nit 2): $REPO was interpolated into the
gh api paths without the regex guard applied to reviewer logins/teams. Add a
symmetric owner/name check so every interpolated path segment is validated the
same way — rejects extra '/', path traversal, and malformed refs. Low risk
(admin-supplied) but keeps the guard surface uniform.
@cefothe

cefothe commented Jun 30, 2026

Copy link
Copy Markdown
Contributor Author

Благодаря за подробния security преглед! По двете бележки:

2 ($REPO guard) — оправено в 8bc6543. Добавих симетрична проверка веднага след резолвинга на REPO: ^[A-Za-z0-9-]+/[A-Za-z0-9._-]+$ (owner = букви/цифри/тире; име = плюс . и _). Така всеки сегмент, който се интерполира в gh api пътя, минава през същия guard — отхвърля допълнително /, path traversal и малформирани стойности. Нисък риск, както отбеляза (admin подава стойността), но повърхността на guard-овете вече е еднаква.

1 (гейтът става реален едва след provision-environments.sh) — присъщо на GitHub, документирано. Protection rule-ите живеят в repo Settings, не в YAML, така че не могат да се enforce-нат при merge. docs/deploy.md го описва в „Какво трябва да се направи ръчно“ и в Approval gate секцията (включително че workflow_dispatch/v* към прод минава гейта едва след provisioning-а). Като owner на стъпката след merge: admin изпълнява веднъж

REVIEWER_TEAMS="midt-bg/maintainers" ./scripts/provision-environments.sh

и проверява Settings → Environments → production. Ще се погрижа това да се случи веднага след merge.

Двете бележки са затворени — благодаря за approve-а.

@cefothe

cefothe commented Jun 30, 2026

Copy link
Copy Markdown
Contributor Author

Всички бележки от прегледите са адресирани и затворени:

  • Reviewer team вместо единичен потребител (от @lyubomir-bozhinov) — 5cd41d2: документацията и скриптът вече водят с REVIEWER_TEAMS="midt-bg/maintainers", с изрична бележка да не се provision-ва прод с единичен REVIEWER_USERS.
  • $REPO guard (от @ydimitrof, бележка 2) — 8bc6543: добавена симетрична валидация ^[A-Za-z0-9-]+/[A-Za-z0-9._-]+$ преди интерполиране в gh api пътищата.
  • Ръчна provisioning стъпка след merge (от @ydimitrof, бележка 1) — присъщо на GitHub (protection rules са в repo Settings, не в YAML); документирано в docs/deploy.md, а стъпката ще се изпълни веднага след merge от admin.

Няма отворени въпроси по сигурност, интегритет или инжекции. CI е зелено и PR-ът е готов за merge. Благодаря за прегледите!

@ydimitrof

Copy link
Copy Markdown
Contributor

Прегледах PR #179 задълбочено — с фокус върху сигурност, интегритет на данните, инжекции, работа с тайни и евентуален зловреден/supply-chain код — и локално по скрипта, workflow-а и документацията. Съпоставих и с acceptance критериите на #89.

Обхват на промяната

  • .github/workflows/deploy.ymltimeout-minutes: 10 на deploy job-а, ::add-mask:: за LOG_IP_KEY, коментар за v* tag policy при workflow_dispatch.
  • docs/deploy.md — approval gate, ротация/изтичане на token, four-eyes уговорка, обосновка за отказа от OIDC.
  • scripts/provision-environments.sh — нов идемпотентен скрипт за required reviewers + v* tag policy.

Сигурност и OWASP

  • Няма повърхност за инжекция (A03). Всяка стойност, която се интерполира в gh api път, минава през строг regex преди употреба: REPO (^[A-Za-z0-9-]+/[A-Za-z0-9._-]+$), reviewer login-ите (^[A-Za-z0-9-]+$), org/slug на екипите. Отхвърля допълнително /, path traversal и малформирани стойности. JSON тялото се сглобява само през параметризиран jq (--arg/--argjson), а id е валидиран като цяло число преди --argjson, така че няма JSON/command injection. Няма SQL (чист CI).
  • Коректна работа с тайни (A02/A09). ::add-mask::$key се регистрира веднага след openssl rand -hex 32 и преди първата употреба; ключът стига до wrangler по stdin, никога като аргумент или логнат ред. В стъпката няма set -x, който да предхожда маскирането.
  • Fail-safe контроли на достъпа (A01/A05). Празен или над-6 набор reviewer-и се отхвърля (гейтът не може мълчаливо да изчезне), провал при LIST е фатален преди конвергиране на tag policy-то, пагинацията е обработена. prevent_self_review + препоръката за екип вместо единичен акаунт са коректни; four-eyes ограничението е изрично документирано.
  • Няма зловреден / supply-chain риск. Всички заявки са към собствената среда на репото през автентикиран CLI — няма външен трафик, няма обфускация, няма промени по зависимостите; GitHub action-ите остават SHA-pinned. Няма закачени секрети или .env стойности.

Съответствие с #89
Трите acceptance критерия са покрити: (1) approval gate през Environment с required reviewers + prevent_self_review; (2) минимални scope-ове + документирана тримесечна ротация и обоснован извод, че Cloudflare няма OIDC за Wrangler към юни 2026; (3) изричен timeout-minutes: 10. Двете предходни бележки (reviewer-екип, $REPO guard) са адресирани в 5cd41d2 и 8bc6543.

Незадължителни наблюдения (не блокират)

  1. Гейтът е реален едва след като admin изпълни provision-environments.sh след merge — присъщо на GitHub, документирано; само да се потвърди, че някой поема стъпката веднага след merge.
  2. PUT /environments включва custom_branch_policies: true преди добавянето на v* policy-то. Ако скриптът прекъсне между двете, production остава напълно блокиран за деплой, докато не се пусне повторно — посоката е fail-closed (безопасна), но си струва да се знае оперативно при прекъснат run.
  3. wait_timer се нулира при всяко изпълнение без WAIT_TIMER — вече документирано; добре е да остане отбелязано, за да не изненада при повторно provisioning.

Промяната е чиста, тясно скоупната и подобрява сигурността на release веригата. Благодаря, че кодифицира protection rule-ите вместо да останат като невидимо UI състояние.

Заключение: Одобрявам — няма проблеми със сигурността, интегритета на данните или инжекции; кодът е OWASP-съобразен, а трите наблюдения по-горе са незадължителна шлифовка.

@lyubomir-bozhinov

Copy link
Copy Markdown
Collaborator

Ре-проверих новия връх 8bc6543 (rebase + hardening след approve-а). Чисто:

  • provision-environments.sh валидира всеки сегмент преди интерполация в gh api път — REPO (^[A-Za-z0-9-]+/[A-Za-z0-9._-]+$), потребители (^[A-Za-z0-9-]+$), WAIT_TIMER (само цифри) — и пада шумно при липсваща/невалидна стойност. Path-traversal / command-injection през ref-овете е затворен.

Approve-ът остава. Единствено: branch-ът е behind main — update преди merge.

@ydimitrof ydimitrof left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Преглед на PR: „ci(deploy): harden production deploy auth and add approval gate“

Фаза 0 — Security-Critical сканиране: ✅ ЧИСТО

  • Твърдо кодирани тайни: няма. LOG_IP_KEY се генерира по време на изпълнение с openssl rand -hex 32 и веднага се маскира — не се комитва нищо.
  • URL промени: единствената нова връзка е towards GitHub discussion (workers-sdk#11434) в документацията — безобидна, whitelisted-домейн (github.com).
  • Зловредни шаблони: няма backdoors, code injection или обфускация. gh api извикванията са конструирани върху валидиран вход.
  • Зависимости: няма нови пакети.
    Заключение: не се блокира, продължавам с ревюто.

Обща оценка

Много добре обмислена и щателно документирана промяна. Целта (issue #89) — да се пренесе управлението на production деплоя от невидими UI кликове към възпроизводим, одитируем скрипт + approval gate — е постигната. Валидацията на вход в provision-environments.sh (path-traversal guard за REPO, потребители и екипи; проверка за брой reviewer-и ≤6; отказ при 0 разрешени reviewer-а) е образцова. Логиката за конвергенция на tag policy е идемпотентна и обработва пагинация, а LIST провалът е фатален — правилно решение.

Силни страни

  • echo "::add-mask::$key" е поставено на реда непосредствено след генерирането — коректно (маскирането не е ретроактивно в рамките на ред).
  • timeout-minutes: 10 вместо 360-мин default — разумен fail-fast.
  • Документацията ясно обяснява ограниченията на prevent_self_review и защо е нужен екип/≥2 reviewer-а за реален four-eyes контрол — точно описание на поведението на GitHub (изисква се само 1 одобрение, но инициаторът не може да е одобряващият).
  • Обяснението „защо все още long-lived token, а не OIDC“ е добре обосновано.

Забележки (не-блокиращи)

  1. Прозорец на заключване при прекъсване (provision-environments.sh). PUT-ът задава custom_branch_policies: true, а v* tag policy се създава чак с последващия POST. Ако скриптът прекъсне между двете стъпки, production остава с включени custom policies, но без нито една валидна policy → GitHub блокира всички деплои към production, докато скриптът не бъде изпълнен отново. Коментарът „просто изпълнете отново“ смекчава, но си струва изрична бележка в лога/документацията, че до повторното изпълнение прод деплоите са спрени.
  2. Reviewer-ите трябва да имат write/admin достъп до repo-то, иначе PUT-ът връща 422. Скриптът резолвва id-та, но не проверява правата — препоръчвам по-ясно съобщение при 422 или бележка в prerequisites.
  3. Остатъчен риск при set -x. Коментарът около add-mask пази бъдещи редакции, но ако някой добави set -x в стъпката, xtrace на реда за генериране на ключа ще изтече стойността преди маскирането да влезе в сила. Струва си изрична забрана за set -x/ACTIONS_STEP_DEBUG в тази стъпка.

Оценка спрямо quality gates

  • Security (agent-level): 1.0/1.0 — валидация на вход, липса на injection, коректно маскиране.
  • Code Quality: 2.0/2.0 — последователен стил, ясни коментари, реуз на shell идиоми.
  • Documentation: 2.0/2.0 — изчерпателна, точна, с примери и предупреждения.
  • Performance: 2.0/2.0 — без регресии; timeout подобрява поведението.
  • Tests: 1.0/3.0 — ⚠ Няма автоматизирани проверки (напр. shellcheck в CI, bats тестове за валидациите на вход). За CI/infra скрипт е разбираемо, но строгите gates изискват покритие; поне добавяне на shellcheck към pipeline-а би било лесна печалба.

Композитна оценка: ~9.0/10 — сваля я единствено липсата на автоматизирано валидиране/тестове за новия скрипт.

Препоръка: COMMENT

Промяната е солидна, сигурна и готова за merge по същество. Не давам пълно APPROVE само защото gate-овете изискват автоматизирано тестово покритие/shellcheck за новия provision-environments.sh и защото прозорецът на заключване (забележка 1) заслужава изрична документация. Нито едно от тези не е блокиращо за функционалността.

# runs in single-digit minutes, so a job past 10 min is hung (stalled network I/O, wedged
# wrangler) — fail fast rather than burn a 6-hour slot. The required-reviewers wait on
# `production` does NOT count against this; the timer starts only once the job hits a runner.
timeout-minutes: 10

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍 Разумен fail-fast лимит вместо 360-мин default. Коментарът коректно уточнява, че изчакването за одобрение на production не влиза в таймера (той тръгва чак щом job-ът хване runner). Единствен риск: ако легитимен деплой някога стане бавен (напр. голям build), 10 мин може да го убие — стойността е разумна, но следете дали не е твърде агресивна при растеж на проекта.

# Register the value as a secret with the runner BEFORE it is used anywhere else, so an
# accidental echo / set -x trace in a later edit can never leak it. Masking is not
# retroactive within a line, hence add-mask on the line right after generation.
echo "::add-mask::$key"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Коректно: add-mask е на реда непосредствено след генерирането, така че стойността се регистрира като secret преди първата ѝ употреба. Остатъчен риск: ако някой добави set -x (или включи ACTIONS_STEP_DEBUG) в тази стъпка, xtrace на реда key="$(openssl rand -hex 32)" ще изведе ключа преди маскирането да влезе в сила. Обмислете изрична бележка/забрана за xtrace в тази стъпка, за да е защитата пълна.

Comment thread docs/deploy.md
2. **Deployment tag policy `v*`** — GitHub сам отказва прод деплой, ако ref-ът не е release таг, дори
ако `detect` логиката в [deploy.yml](../.github/workflows/deploy.yml) някога сгреши (defense in
depth). Job-ът има и изричен `timeout-minutes: 10` вместо 360-мин default.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Обяснението на four-eyes е точно. Едно уточнение си струва да се добави: GitHub изисква само едно одобрение, дори когато са изброени няколко reviewer-а — така че REVIEWER_USERS="alice,bob" НЕ изисква и двамата да одобрят, а гарантира four-eyes само в комбинация с prevent_self_review (инициаторът не може да одобри). За екип това означава, че екипът трябва да има ≥2 членове с write достъп — иначе gate-ът може да се заключи. Текстът го подсказва, но изричното „изисква се 1 одобрение“ би премахнало двусмислие.

@lyubomir-bozhinov lyubomir-bozhinov left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

add-mask placement-ът е коректен (::add-mask::$key веднага след генерирането; ключът отива към wrangler по stdin, не в args — set -x не може да го изтече), а provision guard-овете (regex валидация на repo/logins/slugs, отказ да PUT-не празни reviewers) са издържани.

Дребно (defense-in-depth): detect job-ът интерполира ${{ inputs.environment }} / ${{ github.event_name }} / ${{ github.ref }} директно в run:. На практика не инжектират (choice input-ите са server-validated, ref-овете без кавички), но по-чисто е през env: + $VAR. И: production protection rule-ът не съществува докато provision-environments.sh не се пусне след merge — минимизирай прозореца merge→provision, че v* tag дотогава деплойва без approval (PR-ът го документира).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants