사내 그룹웨어: 전자결재와 LLM API 키 발급 통제
결재 없이 쓰이던 LLM API 키를 SSO·결재·발급·만료·대사가 하나로 이어지는 통제 루프로. 2주 만에 설계·구현.
- 역할
- 설계 · 백엔드 · 프론트엔드 · 운영 기반 (1차 구현 단독)
- 상태
- 1차 구현 뒤 클라이언트 결정으로 재세팅 중
- 기술
- NestJS 12 · TypeScript · PostgreSQL 17 · Kysely · node-pg-migrate (순수 SQL) · pg-boss · LiteLLM Proxy · Google OIDC (PKCE) · Next.js 16 · Docker Compose · Gitea Actions · gitleaks
- 2주
- M0~M4 설계·구현 (9/3 – 9/13)
- 37
- HTTP 엔드포인트 · 테이블 17
- 53
- 테스트 파일 (unit · web · e2e · int)
- 0
- 서버에 저장되는 raw API 키
조직 안에서 LLM API 키가 결재 없이 발급·공유되고, 만료와 회수는 사람 손에 달려 있었습니다. Google OIDC 로그인 위에 결재 엔진과 LiteLLM virtual key 발급·만료·대사 루프를 얹어, 키가 신청에서 폐기까지 추적되게 만들었습니다. 불변성은 앱 코드가 아니라 DB 권한이 지키도록 설계했습니다.
상황
사내 그룹웨어 요구는 전자결재, LLM API 키 발급 승인, 출결이었습니다. 핵심은 키였습니다. 누가 어떤 모델에 쓰는 키를 언제 승인받아 받았고 언제 만료되는지가 남지 않았고, 퇴사자 키가 살아 있을 수 있었습니다.
한 일
- 01
인증·세션을 API가 소유
Google OIDC(PKCE) 로그인을 API 서버가 직접 처리하고 DB 세션과 opaque 쿠키를 썼습니다. CSRF 가드, append-only 역할 이력, Directory 동기화 cron으로 조직 변경이 권한에 반영되게 했습니다.
- 02
결재 엔진
draft 없는 상신, 제출 시점 결재선 확정, 상호 승인, 대결(위임), 영업일 기준 escalation, doc type 레지스트리로 결재를 일반화했습니다. 제출된 문서의 불변성은 컬럼 단위 GRANT로 강제해, 앱에 버그가 있어도 DB가 변경을 거부합니다.
- 03
키 발급: raw 키는 한 번만
키 신청 결재가 승인되면 10분 이내 재인증을 거쳐 수령 시점에 LiteLLM virtual key를 동기 발급하고 raw 키는 응답에 딱 한 번만 실어 보냅니다. alias 멱등 saga로 재시도에 안전하고, 긴급 차단과 모델 목록 검증을 두었습니다.
- 04
생애주기와 대사
만료 30·7·1일 전 Google Chat 알림, 연장 결재, LiteLLM과의 정기 대사(reconcile) 잡, 오프보딩 시 자동 차단까지 pg-boss 워커로 돌립니다.
- 05
운영 기반
Docker Compose와 시크릿 파일 주입, lint·typecheck·test·build와 gitleaks·compose-lint를 CI 게이트로, pg_dump → age → rclone 백업과 restore drill 스크립트를 M0에 넣었습니다.
결과
- M0~M4 구현 완료: TypeScript 약 3.2만 줄, SQL 마이그레이션 5개·테이블 17개, 엔드포인트 37개, 테스트 파일 53개, 설계 결정 D1~D7 문서화.
- 키 신청 → 승인 → 발급 → 만료 알림 → 연장/회수 → 대사가 한 루프로 추적됨.
정직하게 적어 두는 것
- 1차 구현 이후 클라이언트 결정으로 2026.09 재세팅이 진행 중이며, 출결 모듈은 기획 단계입니다.
- 고객 내부 프로세스(결재선·키 정책)는 요약만 공개합니다.