Многие команды вешают self-hosted GitHub Runner на Cloud Mac или Mac mini и думают, что «нет queue, xcodebuild работает» достаточно — статья про queue и TCO закрывает этот слой.
Настоящая ловушка — следующий слой: общий workspace. Общий DerivedData, глобальные кэши зависимостей, один PAT для CI и Agent — пока workflow трогают только мейнтейнеры, это экономия минут. С fork PR, вредоносными post step и автоправкой .github/workflows через Claude Code / OpenHands self-hosted runner без изоляции открыт: секреты job A читает job B.
Отраслевой минимум 2026 совпадает здесь: one job, one workspace — эксклюзивный каталог на job, очистка при выходе, плюс ротация токенов. Это Cloud Mac AI Stack · L1 часть 3 (пререквизиты: ① движок выполнения · ② queue и компромиссы): откуда ловушка CI/CD и runbook для копирования. Индекс серии в § серия L1.
Перед чтением · серия L1 и вход в Stack
Фундамент L0: купить vs арендовать Cloud Mac · перенести AI-рабочую станцию в облако
Серия L1 (читать по порядку): ① движок выполнения → ② queue и TCO → ③ эта статья · безопасность CI/CD и one job, one workspace
Часто в том же stack (L3–L5): workflow Claude Code · настройка MCP · параллельное планирование Ollama · пайплайн OpenClaw
Краткий ответ
Self-hosted runner без изоляции workspace оставляет CI/CD открытым; базовый уровень 2026 — one job, one workspace + ротация токенов.
- Откуда ловушки: межjob-остатки файлов, отравление глобального кэша, долгоживущие PAT на диске
- Отраслевой базис: изолированный каталог на job, очистка
if: always()+ prune хоста - Тот же stack, что MCP least privilege и OpenHands: L4 управляет токенами Agent; L1 — границами диска Fact
Сравнение: старая конфигурация vs базовый уровень 2026 · где прячутся ловушки
При переходе с hosted macos-latest на self-hosted многие сохраняют только «нет queue» (см. L1 ②) и не наследуют hosted «новый диск на job». Таблица и диаграммы ниже отмечают, что казалось «удобным», а сейчас выглядит «открытым».
Вчерашнее удобство = сегодняшняя уязвимость — семь строк сразу
Старая распространённая практика Отраслевой базис 2026 Последствие ловушки
| Измерение | Старая распространённая практика (около 2024) | Отраслевой базис 2026 | Последствие ловушки |
|---|---|---|---|
| Рабочий каталог job | Несколько job делят _work, run dirs не удаляются |
one job, one workspace, очищается при завершении job | Job B читает .env job A, внедрённые скрипты |
| Кэш сборки | Общий DerivedData / глобальные ключи actions/cache |
Ключи cache с repo-scope + периодический prune | Отравление кэша; fork PR сканирует глобальный кэш |
| Учётные данные | Один PAT для CI + Agent; secrets ротируют, диск не стирают | Раздельные токены CI / MCP; ротация 30–90 дн. + wipe workspace | Копии PAT на диске остаются валидными |
| Материалы подписи | Временный keychain в $HOME, без post delete |
Keychain scope job, уничтожается с if: always() |
Следующий job или вредоносный step экспортирует сертификаты |
| Кто правит workflows | По умолчанию: только 2–3 мейнтейнера | Мейнтейнеры + fork PR + Agent правит CI | Поверхность атаки растёт от людей к полуавтономным процессам |
| Сегментация runner | Один Mac на все repo и PR | Prod / staging / fork используют разные labels | Low-trust workflow касается prod signing env |
| Миф hosted vs self-hosted | «self-hosted = моя машина, оптимизировать скорость» | self-hosted = вы сами рисуете границу безопасности (hosted VM изолирует автоматически) | Думали, self-hosted безопаснее; на деле более открыт |
Почему терпели старый способ? Приватный repo, нет fork PR, ежедневный CI только внутренний — общий диск экономил 3–8 минут. Почему в 2026 надо менять? На том же Cloud Mac часто крутятся Agent, MCP и job нескольких repo — любая старая привычка превращает удобство в инцидент безопасности.
Тот же self-hosted runner (тот же пользователь macOS) Job A · nightly подпись │ ├─► распаковка *.mobileprovision, временный keychain ├─► пишет в ~/Library/Developer/Xcode/DerivedData ← казалось «скорость» └─► PAT в ~/.netrc (ленивый скрипт) ← казалось «удобство» │ │ не очищено · остатки _work и кэша ▼ Job B · дневной fork PR «правка доков» │ └─► post step сканирует _work / DerivedData / .netrc ← ловушка: читает остатки job A Итог: self-hosted runner открыт (hosted macos-latest выбрасывает всю VM на job)
Job A ──► workspace A (_work/.../run-id) ──► очистка if: always() ──► ✓ Job B ──► workspace B (свежий каталог) ──► очистка if: always() ──► ✓ │ ├─ чувствительный кэш: ключи cache с префиксом repo или cron prune ├─ PAT: раздельные токены CI и MCP, плановая ротация └─ high-trust / low-trust job: разные runner labels (второй Cloud Mac при необходимости) Итог: аудируемый слой Fact; согласуется с «Claude Code производит Diff, Runner производит Fact»
Вкратце · раньше vs сейчас
- Старая ловушка: self-hosted как «более быстрый сервер» и шарить всё, что можно
- Базис 2026: self-hosted как «среду выполнения, которую вы дезинфицируете»
- Главный миф: миграция с
macos-latestи перенесли CPU, не перенесли изоляцию
Откуда открытость: три частые ловушки безопасности CI/CD на self-hosted runner
Hosted macos-latest получает свежий диск на job, поэтому при первом self-host многие думают «моя машина, оптимизировать удобство». Реальная цена self-hosted: границу безопасности рисуете вы — скрытый счёт помимо «нет queue» в L1 часть 2.
Три ошибки конфигурации ниже повторяются на Cloud Mac; попали в две — внедряйте one job, one workspace сразу.
Почему 2026 продвигает one job, one workspace повсюду
Не потому что GitHub выпустил новое правило — поверхность атаки выросла: workflow запускают мейнтейнеры, fork PR, supply-chain-скрипты и AI Agent на том же хосте. Три причины, почему «общий диск ради скорости» в 2026 не держится.
1. Межjob-загрязнение: секреты job A читаются в job B
Self-hosted _work не дезинфицируется автоматически между job. Сертификаты подписи из job A, .env от скриптов, .netrc от post step — job B читает через относительные пути или symlinks. В 2026 поверхность шире: Claude Code и OpenHands могут править .github/workflows на той же машине — аудируете не только diff, но и что остаётся на диске при смене CI.
2. Отравление глобального кэша: DerivedData / npm cache как общий backdoor
Шаринг ~/Library/Developer/Xcode/DerivedData или широких ключей actions/cache может работать в закрытом внутреннем repo; с fork PR вредоносный post сканирует глобальный кэш — hosted VM уничтожаются, self-hosted без cleanup — постоянная поверхность атаки. Типичный iOS-провал: nightly signing job и дневной PR «правка доков» на одном runner.
3. Долгоживущий PAT на диске: ротации secrets в UI недостаточно
Во многих Cloud Mac stack один GitHub PAT служит и доступу repo MCP, и push артефактов Runner. Утечка на стороне Agent валит и CI. Ротация secrets в UI GitHub без wipe workspace — как сменить цилиндр, но оставить копии на диске.
Роли Stack · слой Fact не может работать открыто
Слоган серии (из открытия L1): Claude Code производит Diff; GitHub Runner производит Fact. Красивые diff бессмысленны, если Fact зелёный в грязном workspace. L4 MCP least privilege управляет токенами Agent; L1 здесь — диском и границами job — оба слоя, не или-или.
Выровнять модель: что GitHub Actions оставляет на runner
Многие отождествляют «workspace» с checkout git-деревом — это лишь часть. После macOS job на диске может остаться:
| Путь / объект | Типичное содержимое | Риск без очистки |
|---|---|---|
_work/<repo>/<run-id>/ |
checkout, артефакты сборки, вывод тестов | следующий job читает сгенерированные файлы, вредоносные скрипты вне source |
~/.npm, ~/Library/Caches |
кэши зависимостей и инструментов | отравление кэша, путаница зависимостей между repo |
DerivedData, .swiftpm |
кэш сборки Xcode / Swift | утечка символов, встроенная устаревшая конфигурация подписи |
Временный keychain, *.mobileprovision |
материалы подписи | высокий риск: следующий job или вредоносный step экспортирует certs |
файлы injection env, .netrc |
учётные данные, записанные CI-скриптами | PAT в открытом виде сохраняется |
Hosted runner выбрасывают весь диск при завершении job; self-hosted — нет. На Agent stack хуже: файлы сессий Claude Code, веса Ollama и _work Runner могут делить один user home (планирование на одной машине в L2 параллельное планирование) — one job, one workspace значит: в начале считать среду загрязнённой Agent или другим job, в конце оставлять только слои, которые должны пересекать job (обычно никакие).
Отраслевой базис на практике: что значит one job, one workspace
Четыре слоя, от простого к сложному:
- Изоляция каталогов: каждый job использует свой
RUNNER_TEMP/ run dir; запрет скриптам писать в/tmp/sharedили «командные общие» папки вне repo. - Граница процессов: один процесс runner может выполнять job последовательно, но не должен делить неочищенное глобальное состояние между job (напр. API keys в
~/.zshrc). - Граница учётных данных: временные keychain подписи удалять в
postjob; secrets только через env, не на диск — если диск нужен, путь в run dir и удаление с job. - Граница ops: high-risk repo получают выделенные runner только для доверенных workflow (разделение labels), физически отдельно от runner fork PR — на Cloud Mac обычно второй узел, а не ставка на один Mac, где скрипты очистки никогда не упадут.
Связь с ephemeral runner
Ephemeral self-hosted runner GitHub Enterprise завершаются после каждого job и стартуют заново — автоматизируя one job, one workspace. При постоянном runner (типично на Cloud Mac) похожий эффект через скрипты + соглашения workflow.
Ротация токенов: почему стирания каталогов недостаточно
Очистка workspace убирает остатки файлов; ротация токенов гарантирует, что копии истекут даже при эксфильтрации. Ротируйте минимум три типа учётных данных:
- Токен регистрации runner: удалить и перерегистрировать runner (или ротировать по политике org), чтобы устаревшая регистрация на старых машинах не использовалась.
- GitHub PAT / App для CI: минимальные scopes (чтение repo vs запись packages), отдельно от политики PAT MCP — избегать одного токена для Agent + CI.
- Подпись Apple и API keys третьих сторон: краткоживущие credentials или injection из secrets на job; не писать в plist home runner.
Нет серебряной пули по частоте: приватный repo, нет fork PR, workflow правят только мейнтейнеры — 90 дней часто хватает; с открытыми контрибьюторами или Agent, автосабмитящим workflow, сократите до 30 дней и после инцидента сразу удалите и перерегистрируйте runner.
Runbook: встроить one job, one workspace в workflow
Вставьте эти сниппеты в существующие пайплайны, чтобы сделать базис аудируемым. Предполагается macOS self-hosted runner с layout _work по умолчанию. Если тот же Cloud Mac крутит Claude Code, предпочтите отдельного пользователя macOS для Runner. Подключение MCP: гайд настройки.
# фрагмент .github/workflows/ios-ci.yml jobs: build: runs-on: [self-hosted, macos, cloud-mac] steps: - uses: actions/checkout@v4 - name: Build iOS run: xcodebuild -scheme App -destination 'platform=iOS Simulator,name=iPhone 16' build - name: Scrub workspace (always) if: always() run: | rm -rf "$RUNNER_TEMP"/* rm -rf "$GITHUB_WORKSPACE"/build security delete-keychain ci_temp.keychain-db 2>/dev/null || true
#!/usr/bin/env bash # /usr/local/bin/runner-prune-work.sh · ежедневно 03:00 cron set -euo pipefail WORK_ROOT="${HOME}/actions-runner/_work" # Удалить run dirs старше 48ч (mtime) find "$WORK_ROOT" -mindepth 3 -maxdepth 3 -type d -mtime +2 -exec rm -rf {} + # Опционально: prune записей DerivedData старше 7 дней find ~/Library/Developer/Xcode/DerivedData -mindepth 1 -maxdepth 1 -type d -mtime +7 -exec rm -rf {} + 2>/dev/null || true
Проверка: в двух последовательных job выведите ls -la "$GITHUB_WORKSPACE/.." и ключевые пути кэша; убедитесь, что второй job не видит маркерные файлы первого (напр. touch /tmp/job-marker-$GITHUB_RUN_ID в post и проверка остатков).
Частая ошибка конфигурации
Использовать actions/cache для кэширования неподписанных сторонних бинарников под глобальным cache key без repo-scope — строит общий слой между job и repo на runner. Либо ужесточите ключи cache и ветки, либо включите каталог кэша в скрипты prune.
Совместное размещение Cloud Mac: граница безопасности Agent + Runner
Типичный Cloud Mac AI Stack: Claude Code (Diff) + Runner (Fact) + опционально Ollama на одном хосте. Экономит queue и git pull, но общие неочищаемые глобальные каталоги означают открытый CI втягивает сессии Agent в blast radius:
- Разделение пользователей: Runner под пользователем
runner, Agent под разработчиком; не смешиватьANTHROPIC_API_KEYи ключи подписи в одном~/.zshrc. - workspace Agent ≠ workspace CI: каталог проекта Claude Code не должен указывать на
_workRunner; патчи Agent идут через git, не прямыми записями в деревья кэша CI. - Конкуренция памяти ≠ общий диск: память Ollama vs Runner — параллельное планирование; высокий Swap не оправдание продолжать шарить DerivedData.
- Egress IP и labels: runner с доступом к internal staging не должен принимать fork PR; workflow от Agent сначала на labels с низкими привилегиями, prod runner после человеческого продвижения.
Когда можно отложить (и когда действовать сейчас)
| Сценарий | Можно отложить? | Примечания |
|---|---|---|
| Приватный monorepo, только 2–3 мейнтейнера, нет fork PR | Краткосрочно да | Всё равно monthly manual prune + квартальная ротация токенов |
| Open-source repo запускает Actions на внешних PR | Нет | Выделенный runner или возврат к hosted macOS |
| Claude Code / OpenHands / MCP пишут в repo | Нет | По умолчанию one job, one workspace; запрет общего чувствительного кэша |
| Сертификаты подписи расшифровываются в CI | Нет | Keychain scope job + post delete обязательны |
Чеклист перед запуском (для печати)
- У каждого job есть step очистки
if: always()или эквивалентный prune хоста - Временный keychain / файлы подписи не попадают в постоянные пути
$HOME - High-risk repo и low-trust workflow используют разные runner labels
- Токен регистрации runner и CI PAT имеют календарь ротации (рекомендуется 30–90 дней)
- Первый workflow PR нового контрибьютора — человеческий review, не прямой удар по prod runner
- Agent и CI используют разные PAT / App — не один токен для MCP и Runner
- Сверка с L1 часть 2: ② чинит «медленно»; ③ — «открыт ли self-hosted»
Серия L1 · как связаны слои Stack
Эта статья замыкает линию безопасности L1 (слой Fact): зачем Runner → стоит ли self-hosted → как приземлить базовый уровень 2026 one job, one workspace. Читайте таблицу по порядку; вертикально к L0, горизонтально к L3–L5.
| Часть | Тема | Статус |
|---|---|---|
| ① · Движок выполнения | Почему Runner — L1 Cloud Mac AI Stack (Diff → Fact) | Опубликовано |
| ② · Queue и TCO | Время queue macOS CI · self-hosted vs macos-latest | Опубликовано |
| ③ · эта статья | Безопасность self-hosted runner · базис one job, one workspace | Опубликовано |
| ④ · Пайплайн OpenClaw | Runner выполняет steps · OpenClaw оркестрирует триггеры и квитанции (расширение L1) | Опубликовано |
Вертикальные ссылки Stack (один вход на слой):
- L0 · фундамент: Mac mini vs Cloud Mac · облачная AI-рабочая станция
- L2 · inference: приватный inference Ollama · параллельное планирование с Runner
- L3 · Diff: workflow Claude Code на Cloud Mac · CodeGraph и пропущенные правки
- L4 · context: MCP triple-connect hub · least-privilege exposure
- L5 · workflow: платформа Agent OpenHands
После трилогии L1, если Agent и CI делят Cloud Mac, следующий слой обычно решения слоя L3 Diff (почему Claude Code заменяет традиционную IDE — иначе чем vs Cursor и статья workstation) — затем карта end-to-end L6 (планируется).
FAQ
Замедлит ли one job, one workspace CI?
Cold start станет медленнее — поэтому шарили диск. Баланс 2026: удалять только чувствительные артефакты и dirs job; возобновляемый кэш с ключами с префиксом repo, не никогда не prune-ный глобальный DerivedData.
Когда self-hosted runner «открыт»?
Когда несколько job/repo делят материалы подписи, .netrc или широкий глобальный кэш в одном user home без очистки if: always() — особенно с fork PR или workflow, отредактированными Agent.
Может ли MCP least privilege alone исправить общий диск runner?
Нет. MCP least privilege управляет вызовами инструментов Agent; Runner должен управлять файлами на диске. Вредоносные fork workflow обходят MCP и сканируют остатки _work.
Есть ли переключатель GitHub в один клик?
Hosted runner приближаются к «одному клику»; self-hosted нужны post steps workflow, cron хоста и опциональный ephemeral mode. Один флаг actions/checkout не заменяет всю границу.
Как это разделяется с оркестрацией OpenClaw?
OpenClaw управляет порядком триггеров и квитанциями; Runner выполняет steps. Изоляция живёт в workflow и хосте — не предполагайте, что OpenClaw стирает диск (см. L1 ④ · пайплайн OpenClaw).
С чего начать серию L1?
Рекомендуется: ① движок выполнения → ② queue → ③ эта статья. Полная таблица в § серия L1.
Трилогия L1 готова · следующий слой Diff
Слой Fact защищён — время для Claude Code
L1 отвечает, где CI выполняет Fact. Далее обычно L3: workflow Claude Code на Cloud Mac и логика замены IDE (не то же, что deep dive vs Cursor).
Читать workflow Claude Code