Big Project · AI's EYE 23
인증 경계: 사용자, Worker, 로컬 개발 모드
이미 Firebase token 검증이 있다면 모든 서비스가 같은 token을 사용하거나, 개발할 때는 인증을 완전히 지우는 것이 단순해 보인다.
근거 · 프로젝트 문서 · 코드 · 테스트
1장: 사용자 토큰, 왜 내부 Worker에 그대로 쓸 수 없을까?
솔라는 모니터 한구석에 띄워 둔 architecture.md 파일의 한 문장에서 눈을 떼지 못했다.
Vision Worker, replay와 AICC의 내부 호출은 사람이 로그인한 세션이 아니다.
문장은 간결했지만, 솔라의 머릿속에는 명쾌함 대신 물음표가 떠올랐다. 이미 사용자가 로그인할 때 Firebase로 인증하고, 발급된 ID 토큰으로 모든 API 요청의 권한을 확인하는 시스템이 잘 돌아가고 있었다. 그런데 왜 굳이 내부 서비스가 호출할 때는 다른 방식을 써야 할까?
“언니, 이것 좀 봐봐.”
솔라가 화면을 가리키자, 옆에서 코드를 보던 루나가 고개를 돌렸다.
“사용자 인증은 Firebase 토큰으로 하고, 내부 서비스 호출은 X-Internal-API-Key라는 걸로 인증한다고 되어 있어. 이미 잘 만든 출입 카드가 있는데, 왜 내부 직원용 카드를 또 만드는 것처럼 일을 두 번 하는 거지? 그냥 Firebase 토큰 하나로 전부 인증하면 단순하잖아.”
솔라의 말에는 그럴듯한 논리가 있었다. 강력한 인증 수단 하나로 모든 주체를 관리하는 것. 단순하고 효율적으로 들렸다.
루나는 솔라의 질문에 바로 답하는 대신, 잠시 생각하더니 다른 질문을 던졌다.
“단순한 게 항상 최선일까? 한번 상상해 보자. 우리 시스템의 Vision Worker가 있다고 쳐. 이 Worker는 수만 장의 이미지를 밤새 처리해야 해. 그런데 이 Worker가 솔라, 바로 네 사용자 토큰을 써서 서버에 데이터를 저장한다고 해볼까?”
“내 토큰을?”
“응. Worker를 실행시킨 사람이 솔라니까, 네가 로그인할 때 받은 토큰을 Worker에 복사해서 넘겨주는 거야. 그럼 무슨 일이 생길까?”
솔라는 잠시 Worker가 자신의 ‘대리인’처럼 행동하는 그림을 그렸다. 처음에는 별문제 없어 보였다. 하지만 곧 이상한 점들이 떠오르기 시작했다.
“일단… 내 Firebase 토큰은 유효 시간이 한 시간밖에 안 되잖아. Worker는 밤새 돌아가야 하는데, 한 시간 뒤에 토큰이 만료되면 멈추겠네.”
“그렇지. 그럼 어떻게 해야 할까? Worker가 계속 일하게 하려면.”
“음… 내 아이디랑 비밀번호를 Worker에 알려줘서, 토큰이 만료될 때마다 새로 로그인해서 받아오게 하면…?”
말을 내뱉자마자 솔라는 스스로 고개를 저었다. 서버에서 돌아가는 자동화된 프로그램에 자신의 로그인 정보를 그대로 저장하는 건 생각만 해도 끔찍했다. 비밀번호가 유출되면 누군가 내 계정으로 마음대로 로그인할 수 있게 된다.
“그건 절대 안 되지. 그럼 아주아주 긴 유효 시간을 가진 토큰을 나한테만 특별히 발급해 주면?”
“좋아, 그럼 두 번째 문제가 생겨. Worker가 새벽 3시에 데이터를 처리하다가 뭔가 잘못해서 시스템에 오류를 일으켰다고 해보자. 서버 로그에는 뭐라고 찍힐까?”
루나의 말에 솔라는 다시 상상했다. 서버에 남는 기록. 모든 API 요청은 ‘솔라의 토큰’을 사용했으니, 로그에는 이렇게 남을 것이다. [ERROR] User 'sola_kim' failed to process image 'img_12345.jpg'.
”…내가 한 것처럼 기록되겠네. 나는 잠자고 있었는데도.”
“맞아. 시스템 입장에서는 사람인 솔라와 기계인 Worker를 전혀 구분할 수 없게 돼. 누구의 잘못인지, 어떤 요청이 정상이고 어떤 게 비정상인지 추적하기가 아주 어려워지지. 이걸 ‘신원’의 문제라고 해. Worker는 솔라가 아니잖아. 별개의 신원을 가져야 해.”
루나는 텅 빈 컵 두 개를 가져와 책상 위에 놓았다. 하나는 솔라 앞에, 다른 하나는 조금 떨어진 곳에 두었다.
“이건 사용자 솔라. 이건 Vision Worker. 둘은 우리 시스템에 접근하는 완전히 다른 주체야. 사람인 솔라는 짧은 시간 접속했다가 떠나지만, Worker는 서버 안에서 오랫동안 정해진 임무를 수행하지. 보관되는 장소도 달라. 네 토큰은 네 컴퓨터 브라우저에만 안전하게 있지만, Worker의 인증 정보는 서버 환경 변수 같은 곳에 저장되어야 하니까.”
솔라는 두 개의 컵을 번갈아 보았다. 처음에는 그저 ‘인증’이라는 하나의 문제로 보였던 것이, 이제는 전혀 다른 특성을 가진 두 주체의 문제로 분리되어 보이기 시작했다. 사람 사용자와 내부 Worker. 신원도, 활동하는 시간(수명)도, 존재하는 공간(배포 위치)도 전부 달랐다.
“아… ‘사람이 로그인한 세션이 아니다’라는 말이 단순히 사람이 아니라는 뜻이 아니라, 인증 주체의 특성 자체가 다르다는 의미였구나. 출입 목적과 시간이 다른 사람에게 똑같은 만능 키를 줘선 안 되는 거였어.”
솔라가 고개를 끄덕였다. 사용자 토큰을 재활용하자는 처음의 생각은 편리해 보였지만, 서로 다른 것을 억지로 하나로 묶으려 할 때 발생하는 혼란과 위험을 간과한 것이었다. 왜 별도의 인증 수단이 필요한지에 대한 의문은 풀렸다. 이제 궁금증은 다른 방향으로 향했다.
“알겠어. 둘을 분리해야 한다는 건 확실히 이해했어. 그렇다면 내부 서비스들을 위한 신분증, 그 X-Internal-API-Key라는 건 구체적으로 어떻게 다른 거지? 사용자 토큰이 가진 문제들을 어떻게 해결하는데?“
2장: X-Internal-API-Key: 내부 서비스의 ‘신분증’이 따로 필요한 이유
솔라의 질문이 채 끝나기도 전에, 루나는 책상 위를 정리하고 커다란 노트 한 장을 펼쳤다. 그리고는 가만히 두 개의 평행선을 그었다. 마치 두 개의 다른 인생 경로를 그리듯, 선들은 나란히 시작되었지만 각자의 길이를 намекнула. 루나는 위쪽 선의 시작점에 ‘사용자 토큰’이라고 적고, 아래쪽 선에는 ‘내부 API 키’라고 이름 붙였다.
이 기묘한 다이어그램은 솔라가 방금 던진 질문—사용자 토큰과 내부 키가 구체적으로 어떻게 다른지—에 대한 무언의 대답처럼 보였다. 솔라는 처음의 생각, 즉 X-Internal-API-Key 역시 Firebase 토큰처럼 그저 또 하나의 ‘비밀 문자열’에 불과하다는 생각을 붙들고 있었다. 하지만 언니가 그린 두 개의 선은 무언가 근본적인 차이가 있음을 암시했다.
“자, 어제 우리가 이야기했던 두 주체의 라이프사이클을 한번 따라가 보자.”
루나는 ‘사용자 토큰’ 선의 시작 지점을 펜으로 콕 찍었다.
“이건 언제, 누가 만들지?”
“음… 내가 구글 계정으로 로그인 버튼을 누를 때, Firebase가 나한테 발급해 주지.”
“맞아. 그리고 유효 시간은?”
“한 시간. 만료되면 리프레시 토큰으로 다시 받아오고.”
루나는 짧은 선분을 그리고 ‘1시간 (갱신 가능)’이라고 썼다. 이어서 아래 ‘내부 API 키’ 선의 시작점을 가리켰다.
“그럼 이건? Vision Worker가 쓸 이 키는 누가, 언제 만들까? Worker가 스스로 어딘가에 로그인이라도 하나?”
“아니, 그럴 리는 없지.” 솔라는 잠시 고민했다. “개발팀 누군가가 만들어서… 서버에 설정해 두겠지? 한 번 만들어두면 계속 쓸 테고.”
루나는 고개를 끄덕이며 아래쪽 선을 길게 그었다. 그리고 ‘수개월 (주기적 교체 필요)’이라고 적었다. 시작점부터 달랐다. 하나는 사용자의 행동에 따라 수시로 발급되는 동적인 값, 다른 하나는 시스템 운영을 위해 의도적으로 생성되고 관리되는 정적인 값이었다.
“좋아, 그럼 가장 중요한 차이. ‘신원’과 ‘권한’.”
루나는 다이어그램 중앙에 새로운 항목을 추가했다.
| 구분 | 사용자 토큰 (Firebase ID Token) | 내부 API 키 (X-Internal-API-Key) |
|---|---|---|
| 신원 | sola_kim (특정 사람 사용자) | VisionWorker (특정 내부 서비스) |
| 수명 | 1시간 (짧고, 자동 갱신) | 수개월 (길고, 수동 교체) |
| 발급 | 사용자 로그인 시 Firebase가 동적 발급 | 관리자가 생성 후 비밀 관리 도구에 저장 |
| 권한 | ’sola_kim’이 할 수 있는 모든 것 (글 읽기, 쓰기, 자기 정보 수정) | VisionWorker에게 필요한 최소한의 작업 (e.g. 이미지 분석 결과 DB 저장) |
| 탈취 시 | ’sola_kim’ 계정의 세션 탈취 | 내부 시스템의 특정 기능 악용 |
표가 완성되자, 솔라는 두 인증 수단의 차이를 한눈에 볼 수 있었다. 특히 ‘권한’ 항목에서 눈을 떼지 못했다.
“내 토큰은 ‘나’로서 할 수 있는 모든 권한을 다 가지고 있구나. 내가 관리자라면 관리자 권한까지 포함해서. 하지만 VisionWorker의 키는… 딱 자기가 할 일만 할 수 있는 권한을 갖는 거네. 예를 들어 VisionWorker가 내 계정을 지우거나 다른 사람 글을 수정할 수는 없는 거지?”
“바로 그거야. 그걸 ‘최소 권한의 원칙’이라고 불러.”
루나가 말했다.
“VisionWorker에겐 이미지 분석 결과를 데이터베이스 특정 테이블에 쓸 수 있는 권한만 주면 돼. 설령 이 키가 어떤 경로로든 유출되더라도, 공격자가 할 수 있는 일은 가짜 분석 결과를 써넣는 정도에 그치게 되지. 시스템 전체를 마비시키거나 모든 사용자 정보를 빼돌릴 수는 없어. 하지만 만약 우리가 네 관리자 계정 토큰을 Worker에 넣어뒀다면? 그걸 탈취당하는 순간, 시스템 전체의 관리자 권한을 넘겨주는 셈이야.”
솔라는 등골이 서늘해지는 것을 느꼈다. 편리함을 위해 사용자 토큰을 재활용하자는 생각이 얼마나 위험했는지 깨달았다. 그것은 단순한 키의 재활용이 아니었다. 사람의 모든 권한을 기계에 무분별하게 위임하고, 그 기계가 뚫렸을 때의 모든 책임을 시스템 전체가 짊어지는 구조였다. X-Internal-API-Key는 단지 또 다른 비밀번호가 아니었다. 그것은 시스템의 특정 역할을 수행하는 기계에게 발급된, 목적과 권한이 명확히 제한된 ‘업무용 신분증’이었다.
이제 솔라는 아키텍처 문서의 그 문장을 완전히 새로운 눈으로 볼 수 있었다. ‘내부 write API에 X-Internal-API-Key를 적용해 사용자 token을 장기 Worker 비밀처럼 재사용하지 않게 했다.’ 이 결정은 단순히 토큰의 수명 문제를 해결한 것을 넘어, 서비스 간 통신에 ‘최소 권한’이라는 보안의 경계를 명확히 설정한 것이었다.
“알겠어. 이제 확실히 알겠어. 사람과 기계는 신분증부터 달라야 하는 거였어. 운영 서버에서는 이게 정말 중요하겠구나.”
자신감이 붙은 솔라는 자신의 노트북을 열어 로컬 개발 환경의 설정 파일을 살펴보았다. 운영 환경의 복잡하고 안전한 구조를 이해하고 나니, 자신의 개발 환경이 궁금해졌다. 그리고 곧, 익숙하지만 낯설게 느껴지는 한 줄을 발견했다.
AUTH_REQUIRED=false
“언니, 근데 이건 뭐야? 운영 환경은 이렇게 철저하게 사용자랑 내부 Worker를 분리하는데, 내 컴퓨터에서 개발할 땐 그냥 인증을 꺼버리네. 이게 훨씬 편하긴 한데… 이렇게 완전히 다른 방식을 써도 괜찮은 거야?“
3장: 로컬 개발과 운영: ‘다른 목적’이 ‘다른 인증’을 만든다
솔라의 질문이 끝나자, 잠시 정적이 흘렀다. 루나는 솔라의 노트북 화면에 떠 있는 AUTH_REQUIRED=false라는 한 줄의 코드를 말없이 응시했다. 운영 환경의 인증 경계를 세우기 위해 나눴던 그 모든 논의가 무색하게, 너무나 단순하고 허술해 보이는 설정이었다. 솔라의 생각처럼, 개발의 편의를 위해 잠시 운영 환경의 규칙을 구부려도 괜찮다는 유혹에 빠지기 쉬운 지점이었다.
루나는 바로 대답하는 대신, 솔라의 노트북을 자기 쪽으로 살짝 돌렸다. 그리고는 새 텍스트 편집기 창을 열어 화면을 둘로 나누고, 간단한 상자 두 개를 그렸다.
+------------------------+ +--------------------------+
| 로컬 개발 환경 | | 운영 환경 |
| (내 컴퓨터 안) | | (실제 서비스 서버) |
| | | |
| - 사용자: 나 자신 | | - 사용자: 수천 명의 고객 |
| - 데이터: 테스트용 가짜| | - 데이터: 실제 고객 정보 |
| - 접근: 나만 가능 | | - 접근: 인터넷 전체에 노출|
+------------------------+ +--------------------------+
화면에 그려진 두 개의 명백히 분리된 세계. 솔라는 이 그림이 무엇을 의미하는지 어렴풋이 짐작했지만, AUTH_REQUIRED=false라는 스위치가 이 두 세계 사이에서 어떻게 작동해야 하는지에 대한 명확한 답을 찾지는 못했다.
“자, 솔라. 네가 찾은 이 스위치가 마법의 문이라고 해보자.”
루나가 로컬 개발 환경 상자 아래에 AUTH_REQUIRED=false 라고 적으며 말했다.
“내 컴퓨터 안에서는 이 문을 열어두면 인증 절차 없이 바로 개발 중인 기능으로 달려갈 수 있어. 아주 빠르고 편하지. 그런데 만약… 이 문이 열린 상태 그대로 운영 환경으로 옮겨진다면 어떻게 될까? 실수로, 이 설정 파일을 그대로 복사해서 배포한 거야.”
솔라는 상상해 보았다. 개발의 편의를 위해 열어둔 뒷문이 수천 명의 사용자가 쓰는 실제 서비스 한복판에 그대로 남겨진 상황을.
“음… 일단 누구든 로그인 없이 서비스를 쓸 수 있게 되겠네. 모든 API가 그냥 열려버리는 거니까.”
“누구든. 예를 들면 어떤 API가?” 루나의 질문은 솔라의 생각을 더 깊은 곳으로 이끌었다.
“사용자가 글을 쓰는 API, 정보를 수정하는 API… 아!”
솔라의 얼굴이 굳어졌다. 방금 전까지 논의했던 내용이 스쳐 지나갔다.
“그것뿐만이 아니네. 우리가 VisionWorker 같은 내부 서비스만 호출할 수 있도록 X-Internal-API-Key로 막아뒀던 그 내부 API들까지 전부… 그냥 열리는구나. 사용자 토큰도, 내부 API 키도 아무 의미가 없어지는 거야. AUTH_REQUIRED=false는 ‘모든 인증이 필요 없다’는 선언이니까.”
“맞아. 우리가 애써 세운 두 개의 단단한 벽, 즉 ‘사람 사용자와 내부 Worker를 구분하는 벽’과 ‘Worker에게 최소한의 권한만 주는 벽’이 한 번에 무너져 내리는 거지.”
솔라는 등골이 서늘해지는 것을 느꼈다. 편의를 위한 설정 하나가 실수로 잘못된 장소에 놓였을 때, 시스템 전체의 보안 체계를 어떻게 한순간에 무력화시킬 수 있는지 눈앞에 그려졌다. 그것은 단순히 문 하나가 열리는 수준의 문제가 아니었다. 성의 모든 문과 금고를 열어두는 것과 같았다.
“목적이 다르기 때문이야.”
루나가 말했다.
“로컬 개발 환경의 목적은 ‘빠른 개발’이야. 환경 전체가 개발자인 네 통제하에 있고, 데이터도 가짜지. 가장 큰 위협은 버그 정도일 거야. 그래서 인증을 건너뛰는 비용보다 개발 속도를 높이는 이득이 더 커. 하지만 운영 환경의 목적은 ‘안정적이고 안전한 서비스’야. 불특정 다수에게 열려있고, 진짜 고객 데이터가 흐르지. 외부 공격, 내부 실수 등 온갖 위협이 도사리고 있어. 여기서는 어떤 편의성보다 보안이 최우선이야. 두 환경은 위협 모델 자체가 달라.”
‘환경별 위협 모델’. 솔라는 그제야 AUTH_REQUIRED=false라는 설정이 그 자체로 나쁜 것이 아님을 깨달았다. 그것은 특정 목적(빠른 개발)과 특정 환경(안전한 로컬) 안에서만 유효한, 매우 강력한 도구였다. 문제는 그 도구를 사용하기에 적합하지 않은 환경에 그대로 가져다 놓으려 하는 생각이었다.
이제 솔라는 세 개의 분리 원칙이 하나의 큰 그림으로 맞춰지는 것을 느꼈다. 첫째, 인증의 주체(사람/기계)를 분리한다. 둘째, 기계에는 최소한의 권한만 부여한다. 그리고 마지막 셋째, 개발과 운영 환경의 목적에 맞춰 인증 전략 자체를 분리하고 절대 섞지 않는다.
잠시 후, 솔라는 고개를 끄덕이며 자신의 노트북을 다시 가져왔다. 그리고는 architecture.md 파일을 여는 대신, 새로운 feature-aicc-replay-design.md 파일을 생성했다. 곧 개발에 착수할 새로운 내부 서비스, ‘AICC Replay Worker’의 설계 초안이었다. 솔라는 키보드에 손을 올리고, 망설임 없이 ‘인증 설계’ 섹션을 작성하기 시작했다.
### 3. 인증 설계 원칙
새로운 `AICC Replay Worker`는 다음 세 가지 원칙에 따라 인증을 구현한다.
1. **주체 분리:** `AICC Replay Worker`는 내부 서비스 주체이며, 사람 사용자와 명확히 구분된다. 따라서 사용자 인증에 사용되는 Firebase ID 토큰을 재사용하지 않는다.
2. **최소 권한:** Worker 전용 `X-Internal-API-Key`를 발급하여 인증한다. 이 키는 Replay 데이터 생성 API에 대한 쓰기 권한'만'을 가지며, 다른 API에 대한 접근은 원천적으로 차단된다. 키는 운영 환경 변수를 통해 안전하게 주입한다.
3. **환경 분리:**
* **운영/스테이징 환경:** `AUTH_REQUIRED=true`를 강제하며, 모든 API 호출은 위 원칙에 따라 검증된다.
* **로컬 개발 환경:** 빠른 개발을 위해 `AUTH_REQUIRED=false` 옵션을 허용한다. 단, 이 설정 값은 환경 변수에 의해서만 제어되며, 운영 환경 배포 스크립트는 해당 변수 설정을 원천적으로 포함하지 않도록 구성하여 설정값 유출을 방지한다.
루나는 솔라가 작성한 문서를 조용히 지켜보았다. 더 이상의 설명은 필요 없었다. 솔라는 이제 단순히 규칙을 따르는 것을 넘어, 왜 그런 경계들이 필요한지 이해하고 새로운 시스템에 그 원칙을 스스로 적용하고 있었다. 안전한 시스템을 만드는 것은 단순히 강력한 자물쇠 하나를 채우는 일이 아니었다. 목적이 다른 공간들을 명확히 구분하고, 각 공간에 맞는 각기 다른 열쇠와 규칙을 설계하는 일이었다.