인증
gRPC 데이터플레인은 API 키, REST 컨트롤플레인은 관리자 JWT — 발급·저장·교체·폐기까지.
평면마다 자격증명이 하나씩
| 데이터플레인 (gRPC) | 컨트롤플레인 (REST) | |
|---|---|---|
| 자격증명 | API 키 | 관리자 JWT |
| 전달 방식 | x-api-key 메타데이터 | Authorization: Bearer … |
| 유효 기간 | 폐기할 때까지 | 기본 한 시간 |
| 귀속 대상 | 테넌트 | 테넌트에 묶인 관리자 로그인 |
| 쓰는 곳 | 여러분의 워커 | 대시보드와 운영 스크립트 |
둘은 서로 대체되지 않습니다. gRPC 서버는 자기 포트에서 자기 인터셉터를 돌리고 Authorization 을 아예 보지 않으며, 서블릿 보안 체인은 x-api-key 를 아예 보지 않습니다. 엉뚱한 쪽을 보내는 것은 아무것도 보내지 않은 것과 구별되지 않습니다.
API 키
발급
키는 테넌트별로 발급하며, 대시보드의 API 키 화면이나 POST /api/tenants/{tenantId}/api-keys 를 씁니다. 선택 항목인 label 은 나중에 키를 구분하기 위한 것입니다("worker-01", "staging") — 자격증명의 일부가 아닙니다.
{
"id": "5f1c2b40-…",
"rawToken": "rp_9Q3xK7bT…",
"label": "worker-01",
"prefix": "rp_9Q3xK7bT",
"createdAt": "2026-07-29T09:12:44Z"
}형식과 저장
원문 키는 rp_ 뒤에 암호학적 RNG 에서 뽑은 256 비트를 base64url 로 인코딩한 문자열입니다. rp_ 접두사는 키의 이름공간을 표시하고, 로그나 레포에 실수로 새어 나갔을 때 grep 으로 찾을 수 있게 해 줍니다.
저장되는 것은 의도적으로 인증에 쓸 수 없는 정보뿐입니다.
- 원문 키의 SHA-256 해시 — 조회할 때 보내온 값을 해시해서 해시끼리 비교하므로, 원문은 평문으로 저장되거나 비교되지 않습니다.
- 표시용 접두사(
rp_+ 8 자) — 비밀이 아니며, 목록 조회가 보여 주는 유일한 값입니다. - 레이블, 생성 시각, 폐기 시각.
원문 키는 정확히 한 번만 보여 줍니다
해시만 남기므로 키 값을 다시 돌려줄 수 있는 동작이 어디에도 없습니다 — API 도, 대시보드도, 데이터베이스도 못 합니다. 잃어버렸으면 새로 발급하세요. 그래서 키 목록 조회는 구조적으로 안전합니다. 정당한 관리자에게조차 키 원문을 흘릴 수 없습니다.
[
{"id":"5f1c2b40-…","label":"worker-01","prefix":"rp_9Q3xK7bT",
"createdAt":"2026-07-29T09:12:44Z","revokedAt":null},
{"id":"a08e77c1-…","label":"laptop","prefix":"rp_LmN4pQr8",
"createdAt":"2026-06-02T11:40:10Z","revokedAt":"2026-07-01T08:00:00Z"}
]키 교체
"rotate" 동작은 없고, 그건 의도적입니다 — 교체는 곧 발급 후 폐기이고, 그것이 아무것도 동작하지 않는 구간이 생기지 않는 유일한 순서입니다.
- 새 키를 발급합니다. 이제 두 키가 모두 유효합니다.
- 새 키를 워커에 배포하고 트래픽이 그 키로 흐르는지 확인합니다.
- 기존 키를
id로 폐기합니다.
배포 단위마다 레이블이 붙은 키를 따로 주세요. 그러면 워커 하나를 교체하거나 한 머신에서 새어 나간 키를 폐기해도 나머지가 끊기지 않습니다.
폐기는 즉시 적용됩니다
DELETE /api/tenants/{tenantId}/api-keys/{keyId} 는 키에 폐기 시각을 찍습니다. gRPC 조회는 폐기 시각이 없는 키만 보므로 그 키는 바로 다음 호출부터 통하지 않습니다 — 기다려야 할 캐시가 없습니다. 이미 폐기된 키나 존재하지 않는 키를 폐기하면 둘 중 어느 쪽인지 밝히지 않고 404 를 돌려줍니다.
거절된 호출은 어떻게 보이는가
| 상황 | gRPC 상태 | 이유 |
|---|---|---|
| 키 없음, 모르는 키, 폐기된 키 | UNAUTHENTICATED | 세 경우가 똑같이 응답됩니다 — 응답은 키의 존재 여부를 절대 알려 주지 않습니다. |
| 정지되거나 삭제된 테넌트의 키 | UNAUTHENTICATED | 조회는 활성 테넌트까지 함께 요구하므로, 키가 폐기되지 않았어도 동결된 테넌트의 트래픽은 멈춥니다. |
| 키 저장소에 닿을 수 없음 | UNAVAILABLE | 장애가 잘못된 자격증명으로 위장해서는 안 됩니다. UNAVAILABLE 은 진단 가능하고 재시도할 수 있는 답이고, 거짓 UNAUTHENTICATED 는 존재하지 않는 키 문제를 찾아 헤매게 만듭니다. |
대시보드 세션 (관리자 JWT)
컨트롤플레인은 토큰 전용이고 상태를 갖지 않습니다. 서버 세션도, 로그인 쿠키도 없으므로 CSRF 토큰도 없습니다 — 크로스사이트 요청이 얹혀 갈 암묵적 자격증명이 애초에 존재하지 않습니다. 자격증명을 한 번 JWT 로 바꾸고, 그 토큰을 요청마다 붙입니다.
curl -sS -X POST "https://$RP_HOST/api/auth/login" \
-H 'Content-Type: application/json' -d '{"username":"admin","password":"…"}'
# {"token":"eyJhbGciOiJIUzI1NiJ9…","tokenType":"Bearer","expiresInSeconds":3600}토큰은 서비스가 HS256 으로 서명하고, 의미 있는 클레임이 두 개 있습니다.
sub— 관리자 사용자명.tenant— 범위가 있는 모든 조회가 평가되는 테넌트 경계입니다. 로그인 시점에 서버가 정하며, 쿼리 파라미터·헤더·요청 본문에서 절대 가져오지 않습니다.
마지막 항목이 테넌시 규칙 전부입니다. 풀 조회, 이벤트 피드, 사용량이 모두 토큰 자신의 tenant 클레임으로 범위가 정해지므로, 다른 테넌트를 가리킬 수 있는 요청 형태가 없습니다. tenant 클레임이 없는 토큰은 기본값으로 채워지지 않고 거절됩니다.
다른 테넌트를 향한 요청은 조용히, 닫히는 방향으로 실패합니다
키 관리 엔드포인트는 경로에 tenantId 를 받습니다. 그것이 토큰이 묶인 테넌트가 아니면 답은 403 forbidden 이고, 이 검사는 "이 테넌트가 존재하는가" 검사보다 먼저 돕니다 — 403 과 404 의 차이로 어떤 테넌트가 존재하는지 알아낼 수 없습니다.
로그인은 출처 IP 별로 제한됩니다
v1 에는 관리자 계정이 하나뿐이므로 실패가 쌓였을 때 "계정"을 잠그는 것은 스스로 만드는 장애입니다. 그래서 제한 대상은 출처 IP 입니다. 15 분 안에 5 번 실패하면 그 IP 를 15 분 동안 막고, 429 와 Retry-After 헤더로 답합니다. 로그인이 성공하면 그 IP 의 카운터가 초기화되고, 여러 주소로 흩뿌린 시도에 대비한 전역 초당 상한이 그 뒤를 받칩니다.
만료
토큰은 기본 한 시간 유효하고 refresh 엔드포인트는 없습니다. 만료되면 모든 호출이 401 로 답하므로 다시 로그인합니다. 한 시간보다 오래 도는 스크립트는 토큰이 실행 내내 유효하다고 가정하지 말고 401 에서 재로그인하도록 만드세요. 대시보드가 바로 그렇게 동작하며, 로그인 화면으로 되돌립니다.
운영 체크리스트
- 배포 단위마다 레이블 붙은 API 키 하나 — 전체가 공유하는 키 하나가 아닙니다.
- 키는 발급 응답에서 바로 비밀 저장소로 넣습니다. 그 뒤에는 아무것도 되읽을 수 없습니다.
- 교체는 발급을 먼저, 폐기를 나중에 — 그래야 빈 구간이 없습니다.
- 키를 URL 에 절대 넣지 마세요. 키가 있어야 할 곳은
x-api-key메타데이터 헤더이고, 그건 접근 로그나 referer 에 남지 않습니다. UNAUTHENTICATED는 종단(키를 고쳐야 함),UNAVAILABLE은 재시도 대상(백오프)으로 다루세요.
다음: 이 자격증명들이 열어 주는 엔드포인트를 보려면 REST API 레퍼런스로.