CI 의 장기 시크릿을 OIDC 로 모두 교체하고 6개월 후
47 개의 장기 AWS access key 를 OIDC workload identity 로 갈아끼웠다. 6개월 후 감사 trail 이 한 배포당 47줄에서 1줄로 줄었다. 마이그레이션 6단계와 그 사이 함정.
작년 봄, 우리 고객 중 한 곳에서 기밀 사고 가 있었다. CI 에 박혀 있던 AWS IAM 사용자의 access key 가 어디선가 유출됐다. 어디서 유출됐는지를 알아내는 데 11일이 걸렸다 — 그 키는 3년 동안 회전한 적이 없었고, 사용한 사람·시스템·잡 ID 가 모든 로그에 같은 형태로 찍혀 있어서 분간이 불가능했다.
그 사고 이후 우리는 장기 시크릿 을 CI 에서 전부 추방하는 계획을 세웠다. 6개월 걸렸고, 47개의 access key 가 0개 가 됐다. 감사 trail 도 한 배포당 47줄에서 1줄로 줄었다. 이 글은 그 6단계의 이주와, 사이에 부서졌던 가정들이다.
OIDC workload identity 가 정확히 무엇인가
CI 잡이 그 잡임을 증명하는 단기 토큰 을 들고 가서, 클라우드 측이 그 토큰의 서명자(=ID provider) 와 클레임 을 검증하면 짧게 사용 가능한 자격증명 을 발급해 주는 흐름.
이 한 줄에 모든 게 있다. 시크릿이 미리 박혀 있지 않고, 잡 시작 시 토큰을 들고 가면 클라우드 측이 그 잡이 들어간 시점에만 유효한 자격 을 내준다.
CI 워커 IdP (예: GitHub OIDC) 클라우드 IAM
──────── ────────────────── ──────────────
잡 시작
↓
요청 (audience: aws)
─────────────────▶
서명된 JWT 발급
◀─────────────────
토큰 들고 클라우드에
────────────────────────────────────────────▶
서명자 검증
클레임 매칭
(repo, branch,
workflow 등)
IF OK:
임시 자격 발급
(보통 1시간)
◀────────────────────────────────────────────
자격으로 클라우드 호출
잡 종료 시 자격 만료 장기 시크릿이 어디에도 저장되지 않는다. 이게 핵심이다.
6단계 마이그레이션
1단계 — 인벤토리
CI 안에 어떤 시크릿이 어떤 잡에서 어떤 서비스에 쓰이는가. 이게 안 되면 시작이 불가능하다. 우리는 secret store grep + 빌드 로그 분석으로 47개를 찾았다 (정확하게는 49개 — 2개는 6개월간 한 번도 안 쓰인 죽은 키였다).
2단계 — Identity Provider 등록
각 클라우드 (AWS / GCP / Azure) 에 우리 CI 의 OIDC issuer 를 trusted IdP 로 등록. 우리는 GitHub Actions OIDC + 자체 CI 의 OIDC 둘 다 등록. 클라우드별로 audience 와 thumbprint 이 다른 게 함정.
3단계 — Trust Policy 작성
이게 핵심이다. 어떤 클레임의 토큰 만 어떤 IAM Role 을 assume 할 수 있는지를 명시.
{
"Effect": "Allow",
"Principal": { "Federated": "arn:aws:iam::123456789012:oidc-provider/token.actions.githubusercontent.com" },
"Action": "sts:AssumeRoleWithWebIdentity",
"Condition": {
"StringEquals": {
"token.actions.githubusercontent.com:aud": "sts.amazonaws.com",
"token.actions.githubusercontent.com:sub": "repo:collabops/web:ref:refs/heads/main"
}
}
}sub 클레임을 정확히 박는 게 결정적이다. 너무 느슨하면 다른 repo 가 같은 role 을 assume 할 수 있고, 너무 빡빡하면 합법적인 잡이 거부된다.
4단계 — 한 잡씩 이주
처음에 한 잡 을 골라 OIDC 로 옮긴다. 보통 덜 critical 한 빌드 잡 부터. 첫 잡이 안정화되는 데 보통 1~2주.
5단계 — 장기 시크릿 회전 후 폐기
OIDC 잡이 안정 후, 원래 access key 를 회전 시키고, 그 회전된 키도 폐기 한다. 이 단계가 가장 많이 빠진다 — OIDC 가 작동하면 안심 해서 옛 키를 그냥 두는 사례. 옛 키는 반드시 폐기.
6단계 — 모든 잡 이주 + 옛 IAM 사용자 삭제
마지막. 모든 잡이 OIDC 로 동작하면, 원래의 IAM 사용자 자체를 삭제. 이 시점에서 클라우드 콘솔에 사람도 자동화도 access key 가 없는 상태 가 된다.
사이에 부서진 가정 4가지
1. "토큰은 알아서 갱신되겠지." 안 된다. 우리 CI 는 잡 시작 시 한 번 토큰을 받는다. 잡이 1시간 이상 걸리면 token TTL 보다 길어져 자격이 만료. 해결: 긴 잡은 step 단위로 자격 재요청.
2. "claim 매칭은 정확히 가는 줄 알았다." OIDC sub 클레임 형식이 플랫폼마다 다르다. GitHub Actions 는 repo:<org>/<repo>:ref:refs/heads/<branch>. GitLab 은 다른 포맷. CollabOps 는 또 다른 포맷. 플랫폼 이주 가 일어나면 모든 trust policy 를 다시 써야 한다.
3. "rotation 이 안 필요한 줄 알았다." OIDC 자체는 회전 필요 없지만, IdP 의 서명 키 는 주기적으로 회전 한다. 클라우드의 등록된 thumbprint 가 오래되면 자동 거부됨. 분기 1회 점검 필요.
4. "단기 토큰이라 감사가 자동인 줄 알았다." 클라우드는 AssumeRoleWithWebIdentity 호출을 CloudTrail 에 기록하지만, 어떤 sub 클레임으로 들어왔는지 의 가시성이 도구마다 다르다. 우리는 자체 감사 layer 에 모든 OIDC token 발급 + assume role 호출 을 별도로 기록하기로 했다. 그 layer 가 진짜 감사 가치.
6개월 후 감사 trail
이전:
배포 한 번 = 47 라인의 access key 사용 로그
+ 어떤 잡에서 썼는지 grep
+ 시간순 정렬
+ 사람과 자동화 분리 (불가능에 가까움)
이후:
배포 한 번 = 1 라인 (OIDC token sub: <정확한 워크플로우 URL>)
자동화 vs 사람 분리 자동
어떤 repo, 어떤 branch, 어떤 workflow 인지 명시
token TTL 안에서만 유효 (자동 만료)이 한 표가 6개월 작업의 진짜 결과다. 비용이 아닌 감사 가능성 의 변화.
누가 이 글을 읽으면 좋은가
CI 에 장기 시크릿 이 5개 이상 박혀 있는 모든 팀. 5개를 넘기면 인벤토리 자체가 무너지기 시작한다. 50개를 넘기면 사고는 시간 문제다. 작은 팀일수록 OIDC 의 이주 비용 대비 감사 가치 가 크다.
비슷한 글
에이전틱 DevOps 12개월 후 — 첫 가설 중 무엇이 *맞았고* 무엇이 *틀렸나*
12개월 전 다음 10년의 DevOps는 에이전틱이다 의 가설들. 12개월의 데이터로 어느 가설이 맞고 어느 게 틀렸는지의 정직한 평가.
백재민
3 pillars 그 후 — 4 추가 신호의 *6개월 후* 운영 노트
3 pillars 가 더 이상 충분하지 않은 이유 발행 후 6개월. 4 추가 신호 (events / user journeys / deploy correlation / similarity) 가 운영에서 어떻게 작동했는지의 후속.
백재민
GitHub Actions vs 자체 호스팅 — *진짜 비용* 비교 (12개월 데이터)
GitHub Actions 가 *비싸 보임* 은 표면. 12개월 자체 호스팅 vs SaaS 비교 — 단순 *분당 비용* 이 아니라 *총 운영 비용* 으로.
백재민