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개월 작업의 진짜 결과다. 비용이 아닌 감사 가능성 의 변화.
그래서 언제 OIDC 로 옮겨야 하나
CI 에 박힌 장기 시크릿이 5개를 넘는 순간이 OIDC 워크로드 아이덴티티로 옮겨야 할 타이밍이다. 5개를 넘기면 인벤토리 자체가 무너지기 시작하고, 50개를 넘기면 사고는 시간 문제다. 팀이 작다고 미룰 이유도 없다. 작은 팀일수록 이주 비용 대비 감사 가치 가 크다.
비슷한 글
기획자가 코드를 배포하면, 장애는 누가 책임질까
AI로 제품 담당자가 직접 코드를 만들고 여러 에이전트가 동시에 작업하는 조직에서, 운영에 나가는 변경을 누가 무엇으로 확인해야 하는지. NIST SSDF, SLSA provenance, Google SRE 포스트모템 문화를 근거로 작성자와 분리된 검증·승인·복구의 역할 설계를 정리했다.
백재민
에이전트 시대의 Eclipse 플러그인 — 작업 컨텍스트는 20년 전에도 문제였다
Mylyn 이 2000년대에 풀려던 문제와 지금 에이전트 컨텍스트 문제는 같습니다. 달라진 건 그 컨텍스트를 읽는 쪽에 사람만 있는 게 아니라는 점입니다.
김영상
에이전트 시대의 VS Code 확장 — 개발자는 에디터에서 무엇을 보는가
코드 작성 비용이 무너진 뒤 개발자의 시간은 판단과 승인으로 옮겨갔습니다. 그 일들이 왜 에디터 밖에 있으면 안 되는지, VS Code 확장을 만들며 내린 선택들.
이승백