Kontext CLI: AI 코딩 에이전트를 위한 Credential 보안 관리 도구

·

서론

평범한 화요일 오후, 한 스타트업의 시니어 개발자가 Claude Code에게 데이터베이스 스키마를 마이그레이션해달라고 요청했습니다. 30분 뒤, 그 에이전트는 실수로 프로덕션 데이터베이스를 날렸습니다. 더 끔찍한 건 이 사실을 그 누구도 몰랐다는 것입니다.

문제의 핵심은 권한 남용이 아닙니다. 바로 가시성 부족입니다. 대부분의 팀이 AI 코딩 에이전트에게 GitHub Personal Access Token, Stripe API Key, 데이터베이스 연결 문자열을 .env 파일이나 채팅창에 하드코딩해서 전달합니다. 이 순간, 감사(audit) 기능은 사라집니다. 어떤 개발자가 어떤 에이전트를 실행했는지, 그 에이전트가 정확히 무엇에 접근했는지, 그 접근이 적법했는지 추적할 방법이 없습니다.

Kontext CLI는 이 근본적인 문제를 해결합니다. 단순한 시크릿 매니저가 아니라, AI 에이전트를 위한 Credential Broker입니다. OIDC 인증, RFC 8693 토큰 교환, 세션 기반 단기 토큰 발급을 통해 자율 에이전트 환경에서의 권한 통제와 컴플라이언스를 실현합니다.

본론

문제 본질: Secret Sprawl과 감사 공백

전통적인 credential 관리는 human-in-the-loop를 전제로 설계되었습니다. 개발자가 터미널에서 직접 API를 호출하면, 그 행위는 개발자의 의도와 연결됩니다. 하지만 AI 에이전트는 다릅니다.

전통적 접근 vs AI 에이전트 접근의 차이:

비교 항목전통적 개발자 워크플로우AI 코딩 에이전트 워크플로우
인증 주체개발자 본인에이전트 (개발자 대리)
호출 빈도분당 1~5회분당 수백 회
권한 범위필요 최소 권한과도한 권한 (편의상)
감사 추적쉘 히스토리, 로그추적 불가 (채팅 로그에 묻힘)
Credential 수명장기 (90일~1년)동일 (장기 토큰 재사용)
회전(Rotation) 영향제한적전체 에이전트 세션 중단

핵심 문제는 장기 수명 API 키가 자율 프로세스에 직접 전달된다는 점입니다. 이 키가 유출되면, 공격자는 원래 개발자와 동일한 권한을 얻습니다. 더 심각한 건, 내부 위협 시나리오에서 악의적 에이전트가 권한을 남용해도 이를 감지할 수단이 없다는 것입니다.

Kontext CLI 아키텍처: Credential Broker 모델

Kontext는 AWS STS(Security Token Service)의 개념을 AI 에이전트 환경에 맞게 재해석했습니다.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
graph TD
    A[Developer] -->|kontext start --agent claude| B[Kontext CLI]
    B -->|OIDC Authentication| C[Identity Provider]
    C -->|ID Token| B
    B -->|Token Exchange Request| D[Kontext Backend]
    D -->|RFC 8693 Token Exchange| E[Service Provider]
    E -->|Short-lived Access Token| D
    D -->|Session Token| B
    B -->|Scoped Credentials in Memory| F[AI Agent Runtime]
    F -->|Tool Calls with Session Token| G[External Services]
    F -->|Audit Stream| H[Audit Log]

이 흐름의 핵심은 토큰 교환 패턴입니다. 에이전트는 원본 API 키를 절대 보지 않습니다. 대신 세션에 한정된 단기 토큰을 받습니다.

작동 원리 심층 분석

1. 선언적 Credential 매핑

프로젝트 루트에 .env.kontext 파일을 생성합니다:

1
2
3
4
5
# .env.kontext - 프로젝트별 credential 요구사항 선언
GITHUB_TOKEN={{kontext:github}}
STRIPE_KEY={{kontext:stripe}}
LINEAR_TOKEN={{kontext:linear}}
DATABASE_URL={{kontext:postgres-prod}}

이 파일은 credential 자체가 아닙니다. 요구사항 선언입니다. “이 프로젝트는 GitHub, Stripe, Linear, PostgreSQL에 접근해야 한다"는 메타데이터일 뿐, 실제 시크릿은 포함하지 않습니다.

2. 세션 시작 및 OIDC 인증

에이전트를 실행할 때 Kontext를 통해 시작합니다:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
# Claude Code를 Kontext로 시작
kontext start --agent claude

# 출력 예시:
# ✓ Authenticated via OIDC (user: seungbin@company.com)
# ✓ Session ID: sess_abc123def456
# ✓ Exchanging tokens for 4 services...
# ✓ GITHUB_TOKEN: short-lived token (TTL: 1h)
# ✓ STRIPE_KEY: short-lived token (TTL: 1h)
# ✓ LINEAR_TOKEN: short-lived token (TTL: 1h)
# ✓ DATABASE_URL: injected credential (memory-only)
# ✓ Audit logging enabled
# → Claude Code launched with scoped credentials

3. RFC 8693 Token Exchange 메커니즘

OAuth를 지원하는 서비스(GitHub, Stripe 등)의 경우, Kontext는 RFC 8693 OAuth 2.0 Token Exchange를 사용합니다:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
// Simplified token exchange flow (conceptual)
package main

import (
    "context"
    "fmt"
    "time"
)

// TokenExchangeRequest represents RFC 8693 request
type TokenExchangeRequest struct {
    GrantType          string // "urn:ietf:params:oauth:grant-type:token-exchange"
    SubjectToken       string // OIDC ID token from identity provider
    SubjectTokenType   string // "urn:ietf:params:oauth:token-type:id_token"
    ActorToken         string // Optional: delegating user's token
    ActorTokenType     string
    Resource           string // Target service identifier
    Audience           string // "https://github.com" or "https://api.stripe.com"
    Scope              string // Requested permissions
}

// SessionCredential holds the short-lived token
type SessionCredential struct {
    AccessToken string
    ExpiresAt   time.Time
    SessionID   string
    Scope       []string
    Service     string
}

func ExchangeToken(ctx context.Context, req TokenExchangeRequest) (*SessionCredential, error) {
    // 1. Validate subject token (OIDC ID token)
    claims, err := validateOIDCToken(req.SubjectToken)
    if err != nil {
        return nil, fmt.Errorf("invalid subject token: %w", err)
    }

    // 2. Check user's permission for requested resource
    if !hasPermission(claims.UserID, req.Resource, req.Scope) {
        return nil, fmt.Errorf("access denied: user %s lacks permission for %s",
            claims.UserID, req.Resource)
    }

    // 3. Mint short-lived access token scoped to session
    sessionToken := &SessionCredential{
        AccessToken: generateShortLivedToken(1 * time.Hour),
        ExpiresAt:   time.Now().Add(1 * time.Hour),
        SessionID:   generateSessionID(),
        Scope:       req.Scope,
        Service:     req.Resource,
    }

    // 4. Never persist to disk - memory only
    // Token is injected directly into agent's runtime environment

    return sessionToken, nil
}

핵심 보안 속성:

속성설명보안 이점
단기 수명토큰 TTL: 1시간유출 시 노출 창 최소화
범위 제한세션별 최소 권한lateral movement 방지
메모리 전용디스크 기록 금지at-rest 유출 차단
세션 바인딩특정 세션에 종속토큰 재사용 공격 방지

4. 감사 로깅 및 접근 추적

모든 도구 호출은 실시간으로 스트리밍됩니다:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
{
  "timestamp": "2025-01-15T14:32:07.891Z",
  "session_id": "sess_abc123def456",
  "user": "seungbin@company.com",
  "agent": "claude-code",
  "tool_call": {
    "service": "github",
    "operation": "create_pull_request",
    "repository": "company/backend-api",
    "parameters": {
      "title": "Fix: Update database schema migration",
      "branch": "feature/db-migration"
    }
  },
  "decision": "allowed",
  "policy": "default-allow",
  "token_scope": ["repo:write", "pr:write"]
}

Step-by-Step: Kontext CLI 설정 가이드

1단계: 설치

1
2
3
4
5
6
7
8
9
# macOS (Homebrew)
brew install kontext-dev/tap/kontext

# 또는 Go install
go install github.com/kontext-dev/kontext-cli@latest

# 버전 확인
kontext version
# kontext version 0.1.0 (build: abc123)

2단계: 초기 인증

1
2
3
4
5
6
7
# OIDC 제공자로 인증
kontext auth login

# 브라우저가 열리며 SSO 로그인 진행
# 성공 시:
# ✓ Authenticated as seungbin@company.com
# ✓ Credentials stored in system keyring

3단계: 프로젝트 credential 선언

1
2
3
4
5
6
7
8
# 프로젝트 루트에서
cat > .env.kontext << 'EOF'
GITHUB_TOKEN={{kontext:github}}
STRIPE_KEY={{kontext:stripe}}
EOF

# .env.kontext를 .gitignore에 추가
echo ".env.kontext" >> .gitignore

4단계: 에이전트 실행

1
2
3
4
5
# Claude Code를 Kontext로 실행
kontext start --agent claude

# 에이전트는 환경 변수로 단기 토큰을 받음
# echo $GITHUB_TOKEN → gho_shortlivedtoken...

5단계: 세션 감사 로그 확인

1
2
3
4
5
6
7
8
9
# 현재 세션의 접근 기록 조회
kontext audit list --session current

# 출력:
# TIME                 SERVICE   OPERATION          DECISION
# 2025-01-15 14:32:07  github    create_pull_request  allowed
# 2025-01-15 14:32:15  github    push_commits         allowed
# 2025-01-15 14:33:01  stripe    list_customers       allowed
# 2025-01-15 14:33:45  github    delete_branch        denied (policy)

공격 시나리오와 Kontext의 방어

⚠️ 윤리적 경고: 다음 공격 시나리오는 교육 및 방어 목적으로만 제공됩니다. 실제 시스템에 대한 무단 접근은 불법입니다.

시나리오 1: 에이전트 프로세스 메모리 덤프

공격자가 에이전트 프로세스의 메모리를 덤프하려고 시도합니다:

1
2
3
4
5
6
# 공격자 시도 (실패)
gcore <agent_pid>
strings core.<pid> | grep -E 'ghp_|sk_live_'

# 결과: 단기 토큰만 발견됨 (1시간 내 만료)
# 원본 API 키는 메모리에 존재하지 않음

Kontext 없었다면, 장기 수명 API 키(ghp_..., sk_live_...)가 메모리에 노출되었을 것입니다.

시나리오 2: .env 파일 유출

1
2
3
4
5
6
7
8
# 공격자 시도
cat /project/.env
# 결과: 파일 없음 (Kontext는 메모리 주입만 사용)

cat /project/.env.kontext
# 결과: placeholder만 존재
# GITHUB_TOKEN={{kontext:github}}
# → 실제 토큰 값 없음

시나리오 3: 권한 상승 시도

에이전트가 허용되지 않은 리소스에 접근하려는 경우:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
{
  "tool_call": {
    "service": "github",
    "operation": "delete_repository",
    "repository": "company/production-config"
  },
  "decision": "denied",
  "policy": "repo-deletion-restricted",
  "reason": "Repository deletion requires explicit approval"
}

서버 측 정책 강제(roadmap)가 구현되면, 이러한 시도는 자동으로 차단됩니다.

성능 고려사항

Kontext CLI는 Go로 작성되어 도구 호출당 약 5ms의 오버헤드를 가집니다. ConnectRPC를 사용하여 백엔드와 통신하며, 인증 정보는 시스템 키링(keyring)에 안전하게 저장합니다.

성능 벤치마크 (참고):

메트릭측정값비고
훅 오버헤드~5ms/call도구 호출당 추가 시간
세션 시작 시간~2초토큰 교환 포함
메모리 사용량~15MBCLI 프로세스 기준
토큰 크기~1KB세션 토큰 당

한계점 및 개선 방향

현재 Kontext CLI에는 몇 가지 한계가 있습니다:

  1. 지원 에이전트 제한: 현재 Claude Code만 지원. Codex 지원은 예정
  2. 서버 측 정책 미완성: 도구 호출 거부 인프라는 구축되었으나, 루프가 완전히 닫히지 않음
  3. 백엔드 의존성: 자체 호스팅 옵션 명확성 부족
  4. OAuth 미지원 서비스: 정적 API 키의 경우, 단기 토큰 발급 불가

개선 제안:

1
2
3
4
5
6
7
8
# 정적 API 키에 대한 추가 보호 (제안)
# HashiCorp Vault와 통합하여 동적 시크릿 생성
kontext config set vault-address https://vault.company.com
kontext config set vault-path secret/ai-agents

# 세션 격리 강화 (제안)
# 네트워크 정책과 연동하여 토큰별 허용 IP 제한
kontext start --agent claude --network-policy restricted

결론

핵심 요약

Kontext CLI는 AI 코딩 에이전트 시대의 credential 관리 문제에 대한 실용적 해결책입니다:

  1. 장기 API 키 제거: 에이전트는 단기 세션 토큰만 수신
  2. 메모리 전용 저장: 디스크에 시크릿 기록 금지
  3. 포괄적 감사: 모든 도구 호출의 사용자, 세션, 조직 귀속 추적

출처: https://github.com/kontext-dev/kontext-cli