많은 팀이 Cloud Mac이나 Mac mini에 self-hosted GitHub Runner를 올리고 "queue 없음, xcodebuild 동작"이면 충분하다고 생각합니다 — queue와 TCO 글이 다루는 층입니다.
진짜 함정은 다음 층입니다: 공유 workspace. DerivedData, 전역 의존성 캐시, CI와 Agent 겸용 PAT — workflow를 유지보수자만 건드릴 때는 몇 분 단축에 그칩니다. fork PR, 악의적 post step, Claude Code / OpenHands의 .github/workflows 자동 편집이 합류하면 격리 없는 self-hosted runner는 무방비입니다: job A의 secret을 job B가 읽습니다.
2026 업계 하한선도 여기에 맞춥니다: one job, one workspace — job별 전용 디렉터리, 종료 시 정리, token 로테이션. 이 글은 Cloud Mac AI Stack · L1 3편(선행: ① 실행 엔진 · ② queue와 트레이드오프): CI/CD 보안 함정의 출처와 복붙 runbook. 시리즈 목차는 § L1 시리즈.
읽기 전 · L1 시리즈와 Stack 진입
L0 기반: Cloud Mac 구매 vs 대여 · AI 워크스테이션 클라우드 이전
L1 시리즈(순서 권장): ① 실행 엔진 → ② queue와 TCO → ③ 이 글 · CI/CD 보안과 one job, one workspace
같은 Stack에서 자주 등장(L3–L5): Claude Code 워크플로 · MCP 설정 · Ollama 병렬 스케줄링 · OpenClaw 파이프라인
비교: 예전 구성 vs 2026 기준 · 함정은 어디에
Hosted macos-latest에서 self-hosted로 옮길 때 많은 팀이 「queue 없음」만 이어받고(L1 ②) Hosted의 「job마다 새 디스크」는 이어받지 않습니다. 아래 표와 다이어그램은 당시 「편리」했던 것이 지금은 「무방비」로 보이는 지점을 표시합니다.
어제의 편의 = 오늘의 노출 — 7행 한눈에
예전 일반적 관행 2026 업계 기준 함정 결과
| 관점 | 예전 일반적 관행(2024 전후) | 2026 업계 기준 | 함정 결과 |
|---|---|---|---|
| Job 작업 디렉터리 | 여러 job이 _work 공유, run 디렉터리 미삭제 |
one job, one workspace, job 종료 시 정리 | Job B가 Job A의 .env, 삽입 스크립트 읽기 |
| 빌드 캐시 | 공유 DerivedData / 전역 actions/cache key |
repo 범위 cache key + 정기 prune | 캐시 오염; fork PR이 전역 캐시 스캔 |
| 자격 증명 | CI + Agent 겸용 PAT; secrets는 로테하지만 디스크 미삭제 | CI / MCP 별 token; 30–90일 로테 + workspace 삭제 | 디스크 PAT 복사본이 유효 상태 유지 |
| 서명 자료 | $HOME 임시 keychain, post 삭제 없음 |
job 범위 keychain, if: always()로 파기 |
다음 job 또는 악의 step이 인증서 export |
| workflow 편집 주체 | 기본: 유지보수자 2–3명만 | 유지보수자 + fork PR + Agent CI 편집 | 공격면이 「사람」에서 「반자율 프로세스」로 확대 |
| Runner 세그먼트 | Mac 1대가 모든 repo, 모든 PR | prod / staging / fork는 별 label | 저신뢰 workflow가 prod 서명 환경 접촉 |
| Hosted vs self-hosted 오해 | 「self-hosted = 내 머신, 속도 최우선」 | self-hosted = 보안 경계를 직접 그린다(Hosted VM은 자동 격리) | self-hosted가 더 안전하다고 착각; 실제는 더 노출 |
예전 방식을 참을 수 있었던 이유? 비공개 repo, fork PR 없음, 일일 CI는 내부 job만 — 공유 디스크는 3–8분 단축에 그침. 2026에 바꿔야 하는 이유? 같은 Cloud Mac에서 Agent, MCP, 다중 repo job이 돌아감 — 옛 습관 하나가 「편의」를 보안 사건으로 바꿉니다.
동일 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는 job마다 VM 전체 폐기)
Job A ──► workspace A (_work/.../run-id) ──► if: always() 정리 ──► ✓ Job B ──► workspace B(신규 디렉터리) ──► if: always() 정리 ──► ✓ │ ├─ 민감 캐시: repo 접두사 cache key 또는 cron prune ├─ PAT: CI와 MCP 별 token, 정기 로테이션 └─ 고신뢰 / 저신뢰 job: 별 runner label(필요 시 두 번째 Cloud Mac) 결과: 감사 가능한 Fact 층; 「Claude Code가 Diff, Runner가 Fact」와 정합
한눈에 · 예전 vs 현재
- 예전 함정: self-hosted를 「더 빠른 서버」로 보고 공유 가능한 것은 모두 공유
- 2026 기준: self-hosted를 「직접 소독해야 하는 실행 환경」으로 봄
- 최대 오해:
macos-latest에서 이전할 때 CPU만 옮기고 격리는 안 옮김
무방비의 시작: self-hosted runner에서 흔한 CI/CD 보안 함정 3가지
Hosted macos-latest는 job마다 새 디스크에 가깝기 때문에 처음 self-host하는 팀은 「내 머신, 편의 우선」으로 오해합니다. self-hosted의 진짜 비용은 보안 경계를 직접 그리는 것 — L1 2편의 「queue 없음」 밖 숨은 청구서입니다.
아래 세 가지 오설정은 Cloud Mac 현장에서 반복됩니다. 두 가지 해당 시 즉시 one job, one workspace를 도입하세요.
2026이 one job, one workspace를 전면 추진하는 이유
GitHub가 새 규칙을 낸 게 아니라 공격면이 커졌기 때문입니다: workflow는 유지보수자, fork PR, 공급망 스크립트, 동일 호스트 AI Agent에서 실행됩니다. 「디스크 공유로 속도」는 2026에 성립하지 않습니다.
1. job 간 오염: job A secret을 job B가 읽음
self-hosted _work는 job 사이에 자동 소독되지 않습니다. job A에서 풀린 서명 인증서, 스크립트가 쓴 .env, post step의 .netrc — job B는 상대 경로나 심볼릭 링크로 읽을 수 있습니다. 2026에는 Claude Code와 OpenHands가 동일 머신에서 .github/workflows 편집 — diff뿐 아니라 CI 변경 시 디스크에 무엇이 남는지도 감사해야 합니다.
2. 전역 캐시 오염: DerivedData / npm 캐시가 공유 백도어
~/Library/Developer/Xcode/DerivedData나 넓은 actions/cache key 공유는 닫힌 내부 repo에서는 될 수 있습니다. fork PR이 오면 악의 post step이 전역 캐시 스캔 — Hosted VM은 파기, 정리 없는 self-hosted는 지속 공격면. 전형적 iOS 실패: nightly 서명 job과 주간 「문서 수정」PR이 동일 runner.
3. 디스크에 남은 장기 PAT: UI secrets 로테만으로는 부족
많은 Cloud Mac Stack에서 GitHub PAT 하나가 MCP repo 접근과 Runner 아티팩트 push에 쓰입니다. Agent 측 유출 시 CI도 함께 무너집니다. GitHub UI에서 secrets만 로테하고 workspace를 지우지 않으면 자물쇠만 바꾸고 디스크 복사본은 남기는 것과 같습니다.
모델 정렬: GitHub Actions가 runner에 남기는 것
많은 이가 「workspace」를 checkout된 git 트리와 동일시하지만 일부일 뿐입니다. macOS job 후 디스크에 남을 수 있는 것:
| 경로 / 객체 | 전형적 내용 | 미정리 시 위험 |
|---|---|---|
_work/<repo>/<run-id>/ |
checkout, 빌드 산출물, 테스트 출력 | 다음 job이 생성물·소스 밖 악의 스크립트 읽기 |
~/.npm, ~/Library/Caches |
의존성·도구 캐시 | 캐시 오염, repo 간 의존성 혼동 |
DerivedData, .swiftpm |
Xcode / Swift 빌드 캐시 | 심볼 유출, 구 서명 설정 임베드 |
임시 keychain, *.mobileprovision |
서명 자료 | 고위험: 다음 job 또는 악의 step이 인증서 export |
env 주입 파일, .netrc |
CI 스크립트가 쓴 자격 증명 | 평문 PAT 잔존 |
Hosted runner는 job 종료 시 디스크 전체 폐기; self-hosted는 아님. Agent Stack에서는 더 악화: Claude Code 세션 파일, Ollama 가중치, Runner _work가 동일 사용자 홈 공유(동일 머신 스케줄링은 L2 병렬 스케줄링) — one job, one workspace란: 시작 시 Agent나 다른 job으로 오염됐다고 가정하고, 종료 시 job을 넘는 층은 남기지 않음(보통 없음).
업계 기준 실천: one job, one workspace의 의미
4층, 쉬운 순:
- 디렉터리 격리: 각 job은 전용
RUNNER_TEMP/ run 디렉터리;/tmp/shared나 repo 밖 「팀 공유」폴더 쓰기 금지. - 프로세스 경계: runner 프로세스 하나가 job을 직렬 실행해도 job 사이 미정리 전역 상태(예:
~/.zshrc에 export한 API key) 공유 금지. - 자격 경계: 서명용 임시 keychain은 job
post에서 삭제; secrets는 env만, 디스크 금지 — 디스크 필요 시 run 디렉터리 내에서 job과 함께 삭제. - 운영 경계: 고위험 repo는 신뢰 workflow 전용 runner(label 분리), fork PR runner와 물리 분리 — Cloud Mac에서는 보통 두 번째 노드, 한 대에서 정리 스크립트가 영원히 맞다고 걸지 않음.
ephemeral runner와의 관계
GitHub Enterprise ephemeral self-hosted runner는 각 job 후 프로세스 종료·새 인스턴스 — one job, one workspace 자동화. 상주 runner(Cloud Mac에서 흔함)면 스크립트 + workflow 규약으로 유사 효과.
token 로테이션: 디렉터리 삭제만으로는 부족
workspace 정리는 파일 잔류 해결; token 로테이션은 유출돼도 복사본 만료. 최소 3종 로테:
- Runner 등록 token: runner 제거·재등록(또는 조직 정책 로테), 구 머신 등록 남용 방지.
- CI용 GitHub PAT / App: 최소 scope(repo 읽기 vs packages 쓰기), MCP PAT 정책과 분리 — Agent + CI 겸용 token 금지.
- Apple 서명·서드파티 API key: 단기 자격 또는 job별 secrets 주입; runner 홈 plist 쓰기 금지.
만능 주기는 없음: 비공개 repo, fork PR 없음, workflow는 유지보수자만 — 90일이면 충분한 경우 많음; 오픈 contributor나 Agent 자동 workflow 제출 시 30일로 단축, 사건 후 즉시 runner 제거·재등록.
Runbook: one job, one workspace를 workflow에 굽기
기존 파이프라인에 아래를 붙여 기준을 감사 가능하게. macOS self-hosted runner, 기본 _work 레이아웃 가정. 같은 Cloud Mac에서 Claude Code도 돌리면 Runner는 전용 macOS 사용자 권장. 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" # 48h 초과 run 디렉터리 삭제(mtime) find "$WORK_ROOT" -mindepth 3 -maxdepth 3 -type d -mtime +2 -exec rm -rf {} + # 선택: 7일 초과 DerivedData 항목 prune find ~/Library/Developer/Xcode/DerivedData -mindepth 1 -maxdepth 1 -type d -mtime +7 -exec rm -rf {} + 2>/dev/null || true
검증: 연속 2 job에서 ls -la "$GITHUB_WORKSPACE/.."와 주요 캐시 경로 출력; 두 번째 job이 첫 번째 마커 파일을 볼 수 없는지(post에서 touch /tmp/job-marker-$GITHUB_RUN_ID 후 잔류 확인).
흔한 오설정
actions/cache로 미서명 서드파티 바이너리를 전역 cache key로 캐시하고 repo 범위 없음 — runner에서 job·repo 횡단 공유층 생성. cache key·브랜치를 조이거나 캐시 디렉터리를 prune 스크립트에 포함.
Cloud Mac 동거: Agent + Runner 보안 경계
전형적 Cloud Mac AI Stack: Claude Code(Diff) + Runner(Fact) + 선택 Ollama 동일 호스트. queue와 git pull 절약하지만 지울 수 없는 전역 디렉터리 공유 시 무방비 CI가 Agent 세션까지 끌어들임:
- 사용자 분리: Runner는
runner사용자, Agent는 개발자 사용자;ANTHROPIC_API_KEY와 서명 키를 한~/.zshrc에 섞지 않음. - Agent workspace ≠ CI workspace: Claude Code project dir을 Runner
_work에 두지 않음; Agent patch는 git 경유, CI 캐시 트리 직접 쓰기 금지. - 메모리 경합 ≠ 디스크 공유: Ollama vs Runner 메모리는 병렬 스케줄링; 높은 Swap도 DerivedData 공유 핑계가 아님.
- 출구 IP와 label: 내부 staging에 닿는 runner는 fork PR도 받지 않음; Agent 제출 workflow는 먼저 저권한 label, prod runner는 사람 승격 후.
미뤄도 되는 경우(지금 해야 하는 경우)
| 시나리오 | 미뤄도 됨? | 메모 |
|---|---|---|
| 비공개 monorepo, 유지보수자 2–3명만, fork PR 없음 | 단기 가능 | 월간 manual prune + 분기 token 로테 권장 |
| 오픈소스가 외부 PR로 Actions 실행 | 불가 | 전용 runner 또는 Hosted macOS 복귀 |
| Claude Code / OpenHands / MCP가 repo에 쓰기 | 불가 | 기본 one job, one workspace; 민감 캐시 공유 금지 |
| 서명 인증서를 CI에서 복호화 | 불가 | job 범위 keychain + post 삭제 필수 |
출시 전 체크리스트(인쇄용)
- 모든 job에
if: always()정리 step 또는 동등 호스트 prune - 임시 keychain / 서명 파일을 영구
$HOME경로에 두지 않음 - 고위험 repo와 저신뢰 workflow는 별 runner label
- Runner 등록 token과 CI PAT 로테 캘린더(30–90일 권장)
- 신규 contributor 첫 workflow PR은 사람 리뷰, prod runner 직격 금지
- Agent와 CI는 별 PAT / App — MCP와 Runner 겸용 token 금지
- L1 2편 대조: ②는 「느림」, ③은 「self-hosted 무방비 여부」
L1 시리즈 · Stack 층 연결
이 글은 L1(Fact 층) 보안선을 마무리: Runner가 왜 필요한가 → self-hosted 가치 있는가 → 2026 one job, one workspace 기준 착지. 표 순서로 읽기; 세로 L0, 가로 L3–L5.
| 편 | 주제 | 상태 |
|---|---|---|
| ① · 실행 엔진 | Runner가 Cloud Mac AI Stack L1인 이유(Diff → Fact) | 게시됨 |
| ② · queue와 TCO | macOS CI queue 시간 · self-hosted vs macos-latest | 게시됨 |
| ③ · 이 글 | self-hosted runner 보안 · one job, one workspace 기준 | 게시됨 |
| ④ · OpenClaw 파이프라인 | Runner가 step 실행 · OpenClaw가 트리거·영수증 편성(L1 확장) | 게시됨 |
Stack 세로 링크(층당 진입 1개):
- L0 · 기반: Mac mini vs Cloud Mac · 클라우드 AI 워크스테이션
- L2 · 추론: Ollama 프라이빗 추론 · Runner 병렬 스케줄링
- L3 · Diff: Cloud Mac Claude Code 워크플로 · CodeGraph와 누락 편집
- L4 · 컨텍스트: MCP 트리플 커넥트 Hub · 최소 권한 노출
- L5 · 워크플로: OpenHands Agent 플랫폼
L1 3부작 후 Agent와 CI가 같은 Cloud Mac이면 다음은 보통 L3 Diff 층 결정(Claude Code가 전통 IDE를 대체하는 이유 — vs Cursor와 워크스테이션 글과 역할 분리) — 이후 L6 E2E 지도(예정).
FAQ
one job, one workspace가 CI를 느리게 하나?
콜드 스타트는 느려집니다 — 공유 디스크의 이유입니다. 2026 균형: 민감 산출물과 job dir만 삭제; 재생 가능 캐시는 repo 접두사 key, prune 안 하는 전역 DerivedData 아님.
self-hosted runner가 「무방비」인 경우는?
여러 job/repo가 서명 자료, .netrc, 넓은 전역 캐시를 한 사용자 홈에서 공유하고 if: always() 정리 없음 — 특히 fork PR이나 Agent 편집 workflow 시.
MCP 최소 권한만으로 runner 디스크 공유가 해결되나?
아니요. MCP 최소 권한은 Agent 도구 호출; Runner는 디스크 파일 관할. 악의 fork workflow는 MCP를 우회해 _work 잔류 스캔 가능.
GitHub 원클릭 전환 있나?
Hosted runner는 근사 「원클릭」; self-hosted는 workflow post, 호스트 cron, 선택 ephemeral 조합. actions/checkout 한 플래그로 전체 경계 대체 불가.
OpenClaw 편성 층과 분담은?
OpenClaw는 트리거 순서·영수증; Runner는 step 실행. 격리는 workflow·호스트에 기록 — OpenClaw가 디스크를 지운다고 가정하지 말 것(L1 ④ · OpenClaw 파이프라인).
L1 시리즈는 어디서 시작?
권장: ① 실행 엔진 → ② queue → ③ 이 글. 전체 표는 § L1 시리즈.
L1 3부작 완료 · 다음 Diff 층
Fact 층 확보 후 Claude Code로
L1은 CI가 어디서 Fact를 도는지. 다음은 보통 L3: Cloud Mac Claude Code 워크플로와 IDE 대체 로직(vs Cursor 심층과 다름).
Claude Code 워크플로 읽기