RSS듀오랩스
보안

개인 액세스 토큰의 한계: 토큰은 권한이 아니라 신원

작성자
듀오랩스 대표·8분 읽기
- run: eas build --platform all --non-interactive
  env:
    EXPO_TOKEN: ${{ secrets.EXPO_TOKEN }}

어느 날 이 단계가 인증 오류로 멈춥니다. 시크릿은 그대로고, 권한 설정을 건드린 사람도 없고, 어제까지 잘 돌았습니다. 바뀐 것은 한 가지뿐입니다. 그 토큰을 발급한 사람이 팀을 떠났습니다.

흔히 토큰을 권한을 담은 문자열로 이해합니다

그러면 위 상황이 설명되지 않습니다. 아무도 권한을 회수하지 않았는데 접근이 사라졌으니까요.

토큰이 담는 것은 권한이 아니라 누구인가입니다. 권한은 그 신원에 딸려 있을 뿐입니다. 개인 액세스 토큰은 사람의 신원을 빌려 쓰는 것이고, 그래서 그 사람의 소속이 끝나면 토큰도 같이 끝납니다. 문자열은 멀쩡히 남아 있지만 가리키는 사람이 더는 그 조직에 없습니다.

이 구분이 흐려지는 이유는 화면에서 둘이 똑같이 생겼기 때문이라고 봅니다. 발급 화면도 비슷하고, 복사해서 CI 시크릿에 넣는 절차도 같고, 실패하기 전까지는 동작도 같습니다.

로봇 계정은 조직이 소유하는 신원입니다

그래서 나온 것이 사람이 아닌 신원입니다. 부르는 이름은 서비스마다 다릅니다. Expo 는 robot user, GitLab 은 프로젝트 액세스 토큰이 만드는 봇 사용자, GitHub 은 GitHub App 의 설치 토큰이 그 자리를 맡습니다.

공통점은 하나입니다. 그 신원의 주인이 조직이지 개인이 아닙니다. 누가 퇴사해도 계정은 남고, 역할은 조직이 정하고, 회수도 조직이 합니다.

여기서 실무적으로 갈리는 지점이 하나 더 있습니다. 감사 로그입니다. 개인 토큰으로 CI 가 배포하면 로그에는 그 사람 이름이 찍힙니다. 배포한 것은 파이프라인인데 기록은 사람을 가리키니, 나중에 "이 배포 누가 했나" 를 물으면 답이 틀립니다. 로봇 계정이면 로그가 "그 파이프라인" 이라고 정직하게 말합니다.

이름이 로봇이어도 사람 계정인 구현이 있습니다

여기서부터는 구현마다 다르고, 그 차이가 실무에서 제일 자주 발목을 잡습니다.

어떤 서비스는 로봇 계정이 유료 좌석을 차지합니다. 조직 요금제가 사용자 수 기준이면 자동화 신원 하나가 사람 한 명 값입니다. 그래서 "그냥 내 토큰 쓰지" 로 돌아가는 팀이 많고, 저는 그 선택 자체를 비난할 수 없다고 봅니다. 다만 그때는 떠날 때 무엇이 멈추는지를 적어 두는 편이 낫습니다.

또 어떤 서비스는 이름만 봇이고 실제로는 사람이 만든 개인 토큰에 라벨을 붙인 것입니다. 그러면 앞의 문제가 그대로 남습니다. 구분하는 방법은 하나입니다. 그 토큰을 만든 사람의 계정을 비활성화했을 때 토큰이 죽는지 보면 됩니다. 죽으면 사람 신원이고, 살아 있으면 조직 신원입니다.

이 검사는 문서만 읽어서는 확신하기 어렵습니다. 저도 서비스마다 다르게 알고 있고, 실제로 계정을 정지해 보기 전에는 단정하지 않는 편입니다.

수명도 같이 갈립니다

개인 토큰과 로봇 토큰은 보통 장기 토큰입니다. 만들면 폐기할 때까지 삽니다. 그래서 유출되면 발견될 때까지 계속 유효합니다.

반면 GitHub App 방식은 결이 다릅니다. 앱이 개인키로 짧은 수명의 JWT 를 만들고, 그것으로 설치 액세스 토큰을 받아 씁니다. 그 토큰은 만료됩니다. CI 로그에 실수로 찍혀도 창이 좁습니다.

대신 복잡합니다. 앱을 만들고, 설치하고, 개인키를 보관하고, 매번 토큰을 교환하는 단계가 붙습니다. 오래 사는 문자열 하나짧게 사는 토큰을 매번 만드는 절차 중 무엇을 고를지는 그 저장소가 무엇을 할 수 있는지에 달려 있습니다. 배포까지 하는 파이프라인이면 후자 쪽이 값어치를 합니다.

신원을 아예 저장하지 않는 길도 있습니다

여기까지는 "어떤 신원을 만들 것인가" 였습니다. 그런데 저장할 신원을 아예 안 만드는 방향도 있습니다.

OpenID Connect 페더레이션이 그것입니다. CI 러너가 실행 중에 "나는 이 저장소의 이 워크플로다" 라는 서명된 주장을 발급받고, 클라우드 쪽이 그 주장을 신뢰해 짧은 수명의 자격을 내줍니다. 저장소에 남는 비밀이 없습니다. 훔칠 문자열 자체가 없으니 유출이라는 사건이 성립하지 않습니다.

신원 관점에서 보면 이건 세 번째 선택지입니다. 사람의 신원을 빌리는 것도, 조직이 소유한 봇 신원을 만드는 것도 아니고, 파이프라인 자체가 신원입니다. 그래서 권한을 "이 저장소의 main 브랜치 워크플로" 처럼 좁게 줄 수 있습니다. 봇 계정으로는 그 정도로 못 좁힙니다. 봇은 여러 저장소에서 같은 얼굴이니까요.

다만 양쪽이 다 지원해야 성립합니다. CI 쪽이 OIDC 주장을 발급해야 하고, 받는 서비스가 그것을 신뢰 관계로 받아 줘야 합니다. 주요 클라우드는 대체로 되지만, 앱 빌드 서비스나 패키지 레지스트리 쪽은 아직 갈립니다. 저도 어느 서비스가 되고 안 되는지는 그때그때 문서를 확인합니다.

배포 키는 신원이 아니라 통로입니다

헷갈리기 쉬운 이웃이 하나 더 있습니다. 저장소에 붙이는 배포 키입니다.

배포 키는 사람도 봇도 아니고 저장소 하나에 붙은 접근 통로입니다. 그래서 퇴사와 무관하고 좌석도 안 먹습니다. 대신 할 수 있는 일이 그 저장소를 읽고 쓰는 것뿐이라, 조직 API 를 부르거나 여러 저장소를 오가는 일에는 못 씁니다.

셋을 한 줄로 갈라 보면 이렇습니다. 개인 토큰은 사람을 빌리고, 봇 토큰은 신원을 새로 만들고, 배포 키는 신원 없이 통로만 뚫습니다. 필요한 것이 저장소 하나라면 셋 중 마지막이 제일 좁고 제일 조용합니다.

설계에서 실제로 달라지는 것

토큰을 신원으로 보면 세 가지가 자연스럽게 정해집니다.

퇴사 절차에 토큰 목록이 들어갑니다. 계정 비활성화가 곧 파이프라인 정지라면, 그 사실을 그날 알아야지 다음 배포 때 알면 안 됩니다.

토큰마다 "누구인가" 를 적습니다. 볼트든 문서든, 그 값이 개인 신원인지 조직 신원인지가 값 자체보다 중요합니다. 회전 주기도 거기서 나옵니다. 사람에 묶인 토큰은 그 사람의 재직 기간이 사실상의 만료일입니다.

권한은 신원에 붙지 토큰에 붙지 않습니다. 그래서 "이 토큰의 권한을 줄이자" 는 대개 잘못된 질문입니다. 옳은 질문은 "이 신원에 무엇을 줄 것인가" 이고, 서비스가 신원별 역할을 지원하지 않으면 토큰을 아무리 나눠도 권한은 안 줄어듭니다.

여기까지가 확실한 부분

토큰이 신원을 담는다는 것, 그리고 개인 신원에 묶인 토큰은 그 사람의 소속과 수명을 공유한다는 것. 이 둘은 서비스와 무관하게 성립합니다.

그 위는 전부 서비스별입니다. 로봇 계정이 좌석을 먹는지, 역할을 얼마나 잘게 나눌 수 있는지, 이름만 봇인지, 짧은 수명 토큰을 지원하는지. 이건 각 서비스 문서를 봐야 하고, 문서가 애매하면 앞서 말한 검사를 직접 해 보는 수밖에 없습니다.

CI 시크릿에 토큰을 넣기 전에 물어볼 것은 하나입니다. 이게 누구입니까.

마지막 수정:

공유하실 때는 출처(Duolabs)와 원문 주소를 표시해 주세요.