본문으로 건너뛰기

API Key

API Key는 CI 러너, Webhook, 스크립트, GitHub Action 같은 비대화형 클라이언트를 위한 자격증명입니다. 사용자의 JWT 세션을 소비하지 않고 머신 간 트래픽을 인증합니다.

대상 독자

소속 팀의 프로젝트에 대해 프로젝트 범위 Key를 발급하는 developer, 여기에 팀 범위 Key가 더해지는 team_admin, 조직 범위 Key까지 발급하는 super_admin.

/integrations UI로 관리하기

대부분의 사용자는 통합 페이지에서 본인의 Key를 직접 발급·폐기합니다. /integrations UI는 다음을 제공합니다.

  • 로그인한 사용자가 관리할 권한이 있는 모든 Key 나열.
  • Create 시 1회 노출 모달과 클립보드 복사 버튼, 전체 Key가 단 한 번만 표시된다는 강한 경고.
  • 행별 Revoke와 확인 다이얼로그 — 폐기는 ~5초 내에 전파.

/integrations — admin이 Key를 발급·폐기하는 API keys 섹션

생성 다이얼로그는 모든 역할이 동일한 화면을 사용합니다. scope 드롭다운에는 로그인한 사용자가 발급할 수 있는 scope만 나열되므로 developer에게는 project 하나만, super-admin에게는 셋 모두 보입니다.

/integrations — Label과 scope 입력이 있는 API key 생성 다이얼로그

이 페이지는 서버 측 동작 — Key 형태, 해싱, scope 시멘틱, 감사 로그, 회전 전략을 다룹니다. CI에 Key를 연결하기만 하면 되는 사용자는 통합 사용자 가이드에서 멈춰도 됩니다.

Key 형태

tos_<8-char-prefix>_<32-char-secret>

예시: tos_a1b2c3d4_eaff8b91d36c5e0a2f1c4d7e8a9b0c2d.

  • tos_ — 고정 prefix.
  • <8-char-prefix> — 랜덤이며 공개. 조회와 표시 라벨에 사용. 감사 로그에 노출.
  • <32-char-secret> — 랜덤이며 비공개. 서버에는 bcrypt 해시로만 저장하고, 전체 Key는 운영자에게 생성 시 한 번만 표시된 뒤 다시 보이지 않습니다.

조회는 prefix 전반에서 상수 시간이며, secret 비교는 타이밍 공격을 막기 위해 bcrypt.checkpw를 사용합니다.

Scope 모델

각 Key는 권한 경계를 결정하는 단일 리소스 scope를 가집니다.

  • org — 조직 전체에서 행위하며, 발급자가 호출할 수 있던 어떤 endpoint 든 호출 가능.
  • team — 특정 팀을 대신해 행위. 권한 상속이 켜져 있으면(기본값, 중첩 그룹 참고) 그 팀 아래 중첩된 모든 하위 그룹까지 도달하며, 도달 범위는 직접 소속됐을 때와 같습니다. 이 범위 밖 호출은 403으로 실패.
  • project — 특정 프로젝트에 묶이며, 해당 프로젝트 외의 호출은 403으로 실패.

각 scope 발급 권한:

Scope발급 가능 주체
orgsuper-admin 만
teamsuper-admin, team-admin
projectsuper-admin, team-admin, developer (소속 팀의 프로젝트 한정)

Key는 요청 시점에 발급한 사용자의 역할을 상속하되, 자신의 권한 범위(아래)로 그 역할을 좁힙니다. v0.10.0에는 이보다 더 세분화된 "effective role"이나 개별 동작 단위 허용 목록은 없습니다. 권한 검사는 JWT로 인증된 요청과 동일한 RBAC 코드 경로를 따릅니다.

권한 범위

각 Key는 읽기 전용(기본값)이거나 읽기 및 쓰기 중 하나입니다. 읽기 전용 Key는 스캔 실행을 포함해 무언가를 바꾸는 모든 요청을 거부당하므로, 결과를 조회하거나 보고서를 내려받기만 하는 파이프라인은 이 값으로 충분하지만 스캔을 실행하는 파이프라인은 그럴 수 없습니다. 권한 범위는 생애 주기 동안 좁아지는 방향으로만 바뀝니다. 읽기 및 쓰기 Key를 읽기 전용으로 낮출 수는 있어도 그 반대는 안 되므로, 넓혀야 한다면 새 Key를 발급하세요. 호출자가 실제로 쓰기를 해야 할 때만 읽기 및 쓰기를 고르세요. 대부분의 CI 스캔 트리거는 필요하지만, 이전 스캔의 SBOM이나 게이트 결과만 읽는 파이프라인은 필요하지 않습니다.

Key는 선택적 만료(TTL)를 지원합니다. 발급 시 expires_in_days(1-1825)를 지정하면 그만큼의 일수가 지난 뒤 인증이 거부되므로, 유출된 CI 키(파이프라인 로그, 포크 PR 러너)가 수동 폐기 없이 스스로 만료됩니다. 생략하면 만료 없는 Key(기존 기본값)가 됩니다. CI 키는 TTL을 설정하고 회전하는 것을 권장합니다. 위 읽기 전용/읽기 및 쓰기 구분보다 더 세분화된 allowed_actions taxonomy(scan:trigger, scan:read, report:download 등)는 여전히 로드맵입니다.

curl -sS -X POST "https://trustedoss.example.com/v1/api-keys" \
-H "Authorization: Bearer ${JWT}" -H "Content-Type: application/json" \
-d '{"name": "ci-key", "scope": "project", "project_id": "<uuid>", "permission_breadth": "read_write", "expires_in_days": 90}'

Key 발급

통합 페이지에서

  1. /integrations(로그인한 모든 사용자가 쓸 수 있는 최상위 사이드바 항목)을 엽니다.
  2. API keys 섹션으로 스크롤합니다.
  3. Create API key 클릭. 이 버튼이 없는 경우는 어느 팀에도 속하지 않아 발급 가능한 scope가 하나도 없을 때뿐입니다.
  4. 채우기:
    • Name(예: github-action-checkout-service)
    • Scopeproject(기본). 팀 관리자라면 team도 함께 표시됩니다
    • Project ID — scope가 project일 때 필수. scope가 team이면 Team ID
    • 이 키가 할 수 있는 일 — Read only(기본값, 권장) 또는 Read and write. 스캔을 실행할 Key라면 읽기 및 쓰기를 고릅니다
    • ExpirationNever expires(기본값) 또는 30 / 90 / 180 / 365일 프리셋. expires_in_days로 전송됩니다
  5. Create.

전체 Key는 모달에서 단 한 번 표시됩니다. 복사해 CI 시크릿 저장소(GitHub secrets, GitLab CI variables, Jenkins credentials)에 보관하세요. 모달을 닫으면 UI에서는 prefix만 보이고 전체 Key는 복구 불가입니다.

Super-admin으로

같은 /integrations 흐름이지만, 팀 경계를 넘는 Key는 scope를 org로 설정할 수 있습니다(드묾 — 대부분의 CI 통합은 team 또는 project scope에 머물러야 함).

API Key 사용

Authorization 헤더에 Bearer 토큰으로 Key를 전달:

curl -sS -H "Authorization: Bearer ${TRUSTEDOSS_API_KEY}" \
https://trustedoss.example.com/v1/projects

포털은 추적성을 돕기 위해 모든 요청에 prefix를 로깅합니다.

회전

회전 사유

  • 침해 — Key가 공개 레포에 커밋되었거나 CI 러너가 침해됨. 즉시 폐기.
  • 인사 변경 — Key를 발급한 team admin이 떠나는 경우. 새 Key 발급 후 CI 시크릿 교체, 옛 Key 폐기.
  • 정책 — 분기별 회전을 심층 방어 조치로 진행.

무중단 회전 방법

  1. 같은 scope로 새 Key 발급.
  2. CI 시크릿을 새 Key로 갱신.
  3. 한 번의 CI 사이클을 기다려 새 Key 동작 확인.
  4. 옛 Key를 폐기.

폐기 후 ~5초 이내(인증 캐시 TTL)에 옛 Key가 거절됩니다.

폐기

  1. /integrations → API keys → Key 행 → Revoke.
  2. 확인.

폐기는 즉시이며 되돌릴 수 없습니다. Key를 되살리려면 새로 발급하세요.

Key 목록

UI는 라벨, prefix, scope(org / team / project), 권한 범위(읽기 전용 / 읽기 및 쓰기), 발급자, 생성 시각, 마지막 사용 시각, 만료(expires_at, 만료 없으면 null), 폐기 상태를 표시합니다. 기존 Key의 secret을 복구할 방법은 없습니다. 의도된 설계입니다. 마지막 사용 IP 컬럼과 위에서 말한 더 세분화된 allowed_actions taxonomy는 로드맵입니다.

마지막 사용 시각은 API_KEY_LAST_USED_AT_UPDATE_INTERVAL_SECONDS(기본 15분) 단위로 반올림됩니다. 인증할 때마다 정확한 순간을 기록하지 않습니다 — "그 구간 안에서 사용됨"을 뜻하지, "바로 이 초에 사용됨"을 뜻하지 않습니다. 하나의 CI 실행이 20분 동안 같은 Key로 수십 번 폴링해도 시각 갱신은 한두 번만 일어납니다. 이 정도 해상도면 "이 Key가 아직 쓰이고 있는가"라는, 이 컬럼이 실제로 답하는 질문에는 충분합니다. 요청 단위 감사 기록이 필요하면 구조화된 백엔드 로그(api_key.auth_failed, 그리고 그 Key의 요청이 남기는 도메인 감사 행)를 참고하세요.

감사 로그

Key 라이프사이클 이벤트가 로그됩니다.

  • target_table=api_keys&action=create — Key 행이 insert될 때 ORM 리스너가 발신(actor, target prefix, scope).
  • api_key.revoked — 명시적 폐기 시 API Key 서비스가 발신하는 structlog 이벤트 전용(actor, target prefix). v0.10.0 에서는 audit_logs 행을 생성하지 않습니다. 단, ORM 리스너가 revoked_at 변경에 대한 api_keys.update 행을 여전히 기록하므로 폐기 자체는 감사 테이블에 캡처됩니다 — 다만 structlog 이벤트 이름이 아닌 target_table=api_keys&action=update로 기록됩니다.

현재 릴리스에서는 API Key 인증의 요청별 감사 행이 발행되지 않습니다(api_key.use 이벤트는 로드맵). API Key 요청으로 생성되는 감사 행은 도메인 액션(예: target_table=scans&action=create)을 그대로 기록하며, Key의 prefix는 요청에 대한 구조화된 로그에 캡처되지만 감사 행의 actor_user_id는 발급한 사용자입니다(Key 자체가 아님).

Webhook 시크릿 vs API Key

이 둘은 호환되지 않습니다. 포털은 다음을 구분합니다.

  • API Key — CI 클라이언트에서 포털 API로 향하는 아웃바운드.
  • Webhook 시크릿 — Webhook의 인바운드 HMAC 서명 검증에 사용(GitHub X-Hub-Signature-256, GitLab X-Gitlab-Token).

Webhook 흐름은 Webhooks 참고.

정상 동작 확인

Key 발급 후:

  1. curl -sS -H "Authorization: Bearer <key>" .../v1/projects가 200과 팀 프로젝트를 반환.
  1. 감사 로그가 prefix와 함께 target_table=api_keys&action=create 행을 기록. Admin UI 에서는 target_table=api_keys로 필터링할 수 없습니다 — api_keysAuditTargetTable 화이트리스트에 없기 때문입니다(감사 로그 → 필터 노출 vs raw 행 테이블 참고). 검증은 raw SQL 로:

    SELECT * FROM audit_logs
    WHERE target_table = 'api_keys'
    AND action = 'create'
    AND created_at > now() - interval '1 hour'
    ORDER BY created_at DESC;
  1. Key를 소비하는 CI 빌드가 첫 실행에 성공.

트러블슈팅

방금 만든 Key로 401

가장 흔한 두 가지 원인:

  • Key 값이 잘못됨. Bearer 값 앞뒤 공백은 인증 전에 제거되므로 우발적인 앞뒤 공백은 허용됩니다 — 다만 Key 내부의 공백은 인증을 깨뜨립니다. 원본 모달에서 다시 붙여넣으십시오. Key는 정확히 tos_ + 8 + _ + 32자입니다.
  • 포털은 두 실패 모드를 구분합니다:
    • 401 = 자격증명 문제(헤더 없음, 잘못된 Bearer 형식, 알 수 없는 prefix, 서명 불일치, 폐기됨, 만료됨).
    • 403 = 자격증명은 유효하지만 Key의 scope가 리소스를 커버하지 않음(예: team-scope Key가 org 전용 엔드포인트 호출).

"Key prefix exists but secret does not match"

누군가 secret을 brute-force 시도했거나 잘못된 형식의 Key가 전송됐습니다. 포털은 모든 미스를 api_key.auth_failed 이벤트(secret이 아니라 Key key_prefix 포함)로 구조화된 백엔드 로그에 기록합니다. brute-force 감지(단일 Key가 분당 N회 미스를 넘으면 Slack 알림)는 로드맵입니다 — 그때까지는 백엔드 로그에서 반복되는 api_key.auth_failed 라인을 주기적으로 grep하세요:

docker-compose -f docker-compose.yml logs --tail=2000 backend \
| grep api_key.auth_failed | sort | uniq -c | sort -rn | head

단일 prefix가 반복되면 즉시 폐기 후 회전.

로컬에선 동작하는데 CI에선 안 됨

확인 사항:

  • CI 시크릿이 적절한 환경 / 브랜치에 설정되어 있는지.
  • 러너의 아웃바운드 IP가 포털 방화벽에 차단되지 않았는지(일부 설치는 사무실 IP만 화이트리스트).
  • CI 트래픽이 통과하는 reverse proxy에서 Authorization 헤더가 보존되는지.

Roadmap

다음 기능들은 초기 문서에서 언급되지만 현재 릴리스에서는 출시되지 않았습니다.

  • 키별 역할 오버라이드(effective_role)와 세분화된 allowed_actions taxonomy(scan:trigger, scan:read, report:download, webhook:receive, *). 현재 Key는 발급자 역할과 전체 RBAC 표면을 상속.
  • Create API key 폼의 자유 입력 만료 값. 폼은 Never와 30 / 90 / 180 / 365일 프리셋을 제공하므로, API가 허용하는 1~1825일 가운데 그 밖의 값은 expires_in_days로 직접 발급해야 합니다.
  • actor_kind = api_key인 요청별 api_key.use 감사 이벤트. 현재 Key 라이프사이클(ORM 리스너의 insert와 명시적 api_key.revoked 액션)은 감사되지만 요청별 사용은 구조화된 로그에만 캡처됨.
  • 목록의 last_used_ip 컬럼.
  • brute-force secret-mismatch 알림(단일 Key가 60초 내 5회 미스 시 Slack 알림).

함께 보기