넘겨받는 커머스의 코드는 GitHub 조직 하나에 있다. 처음 계획은 단순했다. 우리 계정을 owner 로 올리고 기존 owner 를 내보내면 된다고 봤다. 조직을 실측해 보니 owner 교체는 이관의 마지막 단계일 뿐이고, 그 앞에 닫아야 할 층이 넷 있었다.
Table of contents
Open Table of contents
배경 — 조직의 실측 스냅샷
gh api 로 조직을 조회한 결과는 다음과 같다.
| 항목 | 값 |
|---|---|
| plan | enterprise |
| seats | 11 / 11 |
owned_private_repos | 288 |
total_private_repos | 265 |
public_repos | 2 |
| 멤버 | 11명(사람 9명 + 우리 계정 1 + Machine User 1) |
| owner | 4명(사람 3명 + Machine User 1) |
우리 계정은 member 이고, 토큰 스코프는 gist·read:org·repo 라 admin:org 가 없다. 변경 API 는 하나도 부를 수 없는 상태다. 그리고 지금도 코드가 푸시되고 있다. 가장 최근 push 는 조회 당일이었다.
측정 — 보이지 않는 저장소 138~161개
우리 토큰으로 보이는 저장소는 129개다(private 102 · internal 25 · public 2, 그중 archived 19). 그런데 조직이 보고하는 private 저장소 수는 두 API 필드가 서로 다르다. owned_private_repos 는 288, total_private_repos 는 265 다. 어느 쪽을 기준으로 하든 private 저장소 138~161개가 우리 계정에 보이지 않는다. /orgs/{org}/repos 는 인증 주체가 접근할 수 있는 저장소만 돌려주기 때문이다.
보이지 않는 것이 무엇인지 표본 29개로 시험했다. 이름으로 짐작되는 관계사 쪽 저장소 16개 중 15개가 404 였고, 넘겨받는 서비스 쪽 저장소를 대조군으로 넣으면 200 이 나왔다. 문제는 그다음이었다. CI/CD 구성 요소인 common-devops-* 저장소 4개(alert · tekton-ci · helm · terraform-module)도 전부 404 였다. 넘겨받는 서비스의 배포에 필요한 자산인데 보이지 않는다. 「보이는 129개면 우리 것은 다 있다」고 볼 수 없다는 뜻이고, 이 4개는 우선 확보 목록에 올려야 한다. 보이는 129개는 전체 히스토리째 로컬에 받아 두었다(8.1GB).
원인 — owner 교체를 막는 네 층
- Enterprise 층. plan 이
enterprise이고internal저장소가 25개 있다.internal은 Enterprise Cloud 에서만 쓰는 가시성이므로 위에 Enterprise account 가 있고, 결제·SAML·정책이 거기 묶여 있다. 조직 owner 가 되어도 이 층은 건드릴 수 없고, 조직을 Enterprise 에서 떼어 내는 일은 Enterprise owner 만 할 수 있다 - 자기 승격 불가. member 는 스스로 owner 가 될 수 없다. 기존 owner 중 한 사람이 실행해야 한다
- Machine User 의존. owner 4명 중 하나는 사람이 아니라 CI/CD 전용 공용 계정이다. Argo CD 의 Git 저장소 연결, Tekton 파이프라인의 private 저장소 clone, 배포 API 로 가는 GitHub Webhook 이 이 계정에 묶여 있다. 지우면 배포 자동화가 전부 멈춘다
- 개인 휴대폰 2FA. 그 Machine User 의 2FA 가 특정 개인의 휴대폰 SMS 로 온다. 나머지 owner 가 모두 협조해도 이 계정에 로그인할 수 있는 사람은 그 휴대폰의 주인뿐이다
네 층 가운데 실제 병목은 마지막 둘이다. 앞의 둘은 상대에게 요청하면 풀리지만, 2FA 가 한 사람의 휴대폰에 있으면 그 사람의 협조 없이는 owner 교체도 자격증명 회수도 할 수 없다. 이 계정의 PAT 와 비밀번호는 문서에 평문으로 적혀 있었으므로, 이관 시점에 전량 폐기하고 다시 발급해야 한다.
판단 — 조직째 옮길지, 저장소별로 옮길지
옮기는 단위는 두 가지였다.
- 조직째 옮긴다. 넘겨받는 서비스의 자산이 빠짐없이 넘어오는 대신 양수도 대상이 아닌 관계사 저장소 160여 개가 함께 넘어온다. 관계사의 동의와 분리 협의가 먼저 필요하다
- 저장소별로 transfer 한다. 우리 것만 골라 옮기지만, 어느 것이 우리 자산인지 목록이 아직 없고 Enterprise 층·팀 권한·Machine User 의존을 건별로 풀어야 한다
지금은 둘 중 하나를 고르지 않기로 했다. 보류한 이유는 두 안 모두 Enterprise 구조 확인과 전체 저장소 목록이 있어야 비교할 수 있는데, 둘 다 owner 로 승격된 뒤에야 확정되기 때문이다. 반면 owner 교체 순서는 어느 안이든 같으므로 지금 정해 둘 수 있다. 근거 없이 하나를 고르면 1번은 남의 저장소를 떠안고, 2번은 우리 저장소를 빠뜨릴 위험이 있다. 이 보류의 대가는 그동안 우리가 볼 수 없는 저장소가 계속 남는다는 점이다.
owner 교체 순서는 미리 정해 두었다. 대상 계정을 owner 로 초대하고, 초대가 수락됐는지 확인한 뒤에야 기존 owner 를 내보내거나 member 로 내린다. 초대는 수락 전까지 pending 이라, pending 상태에서 기존 owner 를 지우면 owner 가 0명에 가까워진다. GitHub 이 마지막 owner 의 제거는 막아 주지만, 그 안전장치에 기대지 않는다.
남은 의문 — 승격 전에 닫을 것
- Enterprise account 의 이름과 owner, 결제 주체. 조직 API 로는 보이지 않는다
- Machine User 2FA 휴대폰의 주인과, 2FA 를 다른 수단으로 옮기는 방법
- 2FA 강제 여부와 SAML·SCIM 연동, Actions secrets·webhook·deploy key·GitHub Apps 목록. 모두
admin:org가 있어야 볼 수 있다 - 보이지 않는 private 저장소 138~161개의 목록과, 두 API 값 중 어느 쪽이 실제 수인지
결과 — 이전 방식을 미룬 대가와 얻은 것
- 선행 과제의 확정. owner 교체 전에 닫을 것을 Enterprise account 확인과 Machine User 2FA 이전 두 가지로 좁혀, 상대에게 확인받을 항목으로 정리했다
- 사고 예방 순서. 초대 → 수락 확인 → 기존 owner 정리의 순서를 미리 정해, owner 가 0명에 가까워지는 순간을 없앴다
- 보류의 대가. 조직째/저장소별 결정을 미룬 동안 보이지 않는 private 저장소 138~161개가 그대로 남고, CI/CD 저장소 4개는 우선 확보 대상으로 따로 걸어 두었다
정리
- 조직 owner 위에 Enterprise 층이 있으면 owner 교체로 통제권이 넘어오지 않는다. 그 층부터 확인한다
- 사람이 아닌 owner(Machine User)는 배포 자동화를 붙잡고 있다. 지우기 전에 무엇이 묶였는지 센다
- 2FA 가 한 사람의 휴대폰에 있으면, 그 사람이 이관의 병목이다
- 보이는 저장소 수는 인증 주체 기준이다. 보이지 않는 것을 표본으로 시험해 무엇이 빠졌는지 확인한다