본문 바로가기
MegazoneCloud
← 전체 글
Anthropic AWS

API Key 없는 Claude 환경 — Okta + Claude apps Gateway on AWS 구축기


들어가며: 게이트웨이를 세웠는데도 정책은 여전히 단말에 있다고?

지난 글에서 Cognito 단독으로 LiteLLM 기반 LLM Gateway 를 세운 이야기를 정리했습니다. base_url 한 줄로 기존 코드를 Bedrock 으로 전환하고, 팀별 가상키로 예산을 통제하는 구성이었습니다. 그 결론은 지금도 유효합니다.

그런데 블로그 글을 게시 한 이후에 받은 질문에는 공통점이 있었습니다. “우리는 Claude Code 만 쓰는데, 그 범용성이 전부 필요한가요?”

와… 정말 핵심을 찔렀어요! (GPT풍)

그렇습니다. 범용 게이트웨이의 강점은 무엇이든 통과시킨다는 점인데, 이것을 반대로 생각하면 무엇을 통과시킬지 운영자가 계속 관리해야 한다는 뜻이 되기 때문입니다.

Claude Code 와 Claude Desktop 만 놓고 보면 다음 네 가지의 이슈 포인트가 남아 있습니다.

이슈구체적인 내용
헤더·요청 필드 allowlist 관리Claude Code 가 보내는 anthropic-beta 값과 요청 필드는 릴리스마다 바뀝니다. 게이트웨이 운영자가 통과 목록을 계속 갱신해야 하고, 갱신이 늦으면 어느 날 기능 하나가 갑자기 동작하지 않습니다.
정책이 여전히 단말에 존재모델 허용목록·권한·환경변수는 MDM 으로 각 PC 에 배포해야 합니다. 게이트웨이는 누가 무엇을 쓸 수 있는지를 알지 못하고, 통과시키거나 막을 뿐입니다.
가상키 수명 관리발급·회전·폐기 프로세스를 직접 운영해야 합니다. 퇴사자 처리가 IdP 작업이 아니라 게이트웨이 작업이 됩니다.
Claude Desktop 은 별도 설계CLI 와 설정 경로도 로그인 흐름도 다릅니다. 범용 게이트웨이 앞에서는 두 제품을 따로 설계해야 합니다.

이번 글에서는 이 네 가지가 Anthropic 이 직접 만든 게이트웨이에서 어떻게 달라지는지를, 사내 테스트 계정 및 필드에 실제로 배포하고 검증한 경험으로 정리한 것입니다.

이 글에서 다루는 것: Claude apps Gateway 의 구조와 LiteLLM 과의 역할 구분, AWS 레퍼런스 아키텍처(CDK 스택 7종), Okta 기준 구축 절차, Claude Desktop 연결, 실제로 걸렸던 실패 사례 9건, IdP 교체 시 일반화된 교훈 5가지

다루지 않는 것: Bedrock 프롬프트 엔지니어링, Claude Code 자체 사용법, 특정 고객사 환경의 세부 구성


1. Claude apps Gateway 란 무엇인가

1.1 이미 claude 바이너리 안에 들어 있다?

이 기능을 처음 확인하고 가장 놀랐던 지점은 ‘별도의 제품을 내려받지 않는다’ 라는 것이었습니다. PC에서 Claude Code 를 실행하는 그 claude 바이너리를 서버에서 다음과 같이 실행하면 게이트웨이 역할이 됩니다.

claude gateway --config gateway.yaml

공식 문서의 내용을 그대로 옮기면 “It is included in the claude binary, so the same executable that runs Claude Code on a laptop runs the gateway server” 입니다. 별도 라이선스나 시트 비용이 발생하지 않고, 추론 비용과 컴퓨트 비용만 부담합니다.

전체 구조를 정리하면 다음과 같습니다.

위치구성 요소
개발자 단말Claude Code (CLI), Claude Desktop, Desktop 이 띄우는 임베드 세션, Agent SDK
게이트웨이self-hosted · Linux 단일 바이너리 · stateless
업스트림Amazon Bedrock, Claude Platform on AWS, Google Cloud, Microsoft Foundry, Anthropic API

단말이 들고 있는 것은 API Key 가 아니라 게이트웨이가 발급한 Bearer 토큰이고, 기본 수명은 1시간입니다.

데이터 경계에 관한 내용에서, 공식 문서는 “The gateway’s own data plane sends nothing to Anthropic infrastructure unless the Anthropic API is a configured upstream” 이라고 명시합니다. 텔레메트리, 감사 로그, 관리 설정, 개발자의 IdP 신원은 전부 사용자(관리자)가 지정한 목적지로만 전송됩니다. 데이터 레지던시 요건을 검토하실 때 참고 할 수 있는 내용입니다.

1.2 Claude Code 전용이라서 가능한 6가지 기능

기능내용
자격증명 격리업스트림 키와 IAM 권한은 인프라에만 존재합니다. 개발자 PC 에는 단기 Bearer 토큰만 남습니다.
IdP 그룹이 곧 정책groups 클레임으로 모델·권한·환경변수를 분기합니다. 게이트웨이에 별도 사용자 DB 가 없습니다.
Managed Settings 전달게이트웨이가 관리 설정을 직접 배포합니다. GET /managed/settings 로 사용자별 제공되며 ETag/304 캐싱이 적용됩니다.
OTLP 팬아웃user.id · user.email · user.groups 가 스탬프된 채로 지정한 컬렉터로 릴레이됩니다.
Spend Limit일간/주간/월간 한도를 초과하면 429 를 반환합니다.
업스트림 라우팅리전·프로바이더·failover 순서를 개발자 재설정 없이 교체합니다.

물론 제약 사양도 있습니다. 게이트웨이 본체에는 Admin UI 가 없습니다. 공식 문서의 Availability 표에서는 Admin UI 항목을 “Not available. Configuration is the YAML file; redeploy to change it” 으로 기재하고 있습니다. 불편한 지점이지만, 반대로 보면 설정 전체가 코드로 관리되고 리뷰 대상이 된다는 의미이기도 합니다.

그리고 뒤에서 보시겠지만 AWS 샘플에서 관리자 콘솔이 별도 스택으로 구현 되어 있습니다.

이 밖에 명시된 제약은 다음과 같습니다.

  • SAML 과 LDAP 은 지원하지 않습니다. OIDC 만 지원하므로 OIDC 브릿지를 앞에 두어야 합니다.
  • 게이트웨이 하나에 issuer 는 하나입니다. 여러 issuer 가 필요하면 인스턴스를 분리해야 합니다.
  • OTLP 는 HTTP 만 지원하고 gRPC 는 지원하지 않습니다.
  • 서버는 Linux 에 배포 됩니다.

1.3 LiteLLM 과 비교하면

앞선 글에서 다룬 LiteLLM 기반 범용 게이트웨이와 비교하면 다음과 같은 차이점이 있습니다.

비교 항목LLM Gateway (LiteLLM)Claude apps Gateway
만든 주체커뮤니티 OSS, 100개 이상 프로바이더 지원Anthropic 자체 개발, Claude Code 릴리스마다 함께 테스트
인증 방식가상키(Bearer) + SSO 별도 연동IdP OIDC 직결, device grant, 기본 1시간 세션
정책 전달MDM 으로 단말에 별도 배포게이트웨이가 managed settings 를 직접 배포
모델 통제가상키 단위 allowlist/v1/messages 요청 시점에 서버가 400 으로 거부
클라이언트 범위OpenAI 호환 애플리케이션 전부Claude Code / Claude Desktop 전용
운영 부담헤더·파라미터 allowlist 유지보수 필요프로토콜 계약을 GET /protocol 로 공개

다섯 번째 행이 이 제품의 명확한 한계입니다. 사내 애플리케이션이 타사 LLM 을 호출하는 트래픽은 이 게이트웨이로 받을 수 없습니다.

1.4 그래서 무엇을 골라야 하는가

둘 중 하나만을 써야 한다는 배타적인 선택이 아닌, 트래픽 성격으로 목적을 나누는 편이 가장 현실적입니다.

선택이런 경우에 적합합니다
LiteLLM 유지여러 애플리케이션과 팀이 LLM 을 호출하는 경우, OpenAI 호환 API 가 필요한 경우, Bedrock 외 프로바이더를 혼용하는 경우, 이미 운영 중이고 잘 동작하는 경우
Claude apps Gateway 도입Claude Code 와 Desktop 이 중심인 경우, IdP 그룹 기반 거버넌스가 목적인 경우, 데이터 레지던시 요건이 있는 경우, 단말에 정책을 배포하기 어려운 경우
둘 다 운영애플리케이션·배치 트래픽은 LiteLLM 으로, 개발자 IDE 트래픽은 Claude apps Gateway 로 보냅니다. 백엔드가 Bedrock 한 곳이므로 비용도 한 곳에서 집계됩니다.

판단 기준을 한 문장으로 정리하자면, “누가 무엇을 쓸 수 있는지”를 IdP 로 관리하는 것이 목적이라면 Claude apps Gateway가 우위에 있고, “무엇이든 통과시키되 비용만 통제”하는 것이 목적이라면 범용 게이트웨이가 여전히 적합합니다.

Anthropic 공식 문서에도 “If you already run an LLM gateway or API gateway that meets your needs, keep using it” 이라고 적혀 있습니다.


2. 아키텍처

2.1 AWS 레퍼런스 구성

aws-samples/sample-claude-apps-gateway-on-aws 저장소를 기준으로 설명하겠습니다. Okta 와 Amazon Bedrock 구성입니다.

요청 경로는 다음과 같습니다.

아키텍처
출처:https://github.com/aws-samples/sample-claude-apps-gateway-on-aws

Okta (OIDC IdP)                          Amazon Bedrock
      ↑                                        ↑
      │ 브라우저 직접 로그인                      │ ECS Task Role (SigV4)
      │                                        │
개발자 단말 ──VPN──▶ Internal ALB ──▶ ECS Fargate (claude gateway)
  CLI / Desktop                                 │
                                                ├─▶ Aurora Serverless v2 (세션·spend)
                                                ├─▶ Secrets Manager (JWT · OIDC secret)
                                                ├─▶ CloudWatch Logs (감사 로그, stderr)
                                                └─▶ OTLP 텔레메트리 → 사내 컬렉터

여기서 확인할 포인트는 세 가지입니다.

첫째, 개발자 단말에서 화살표가 두 갈래로 갈립니다.

브라우저는 Okta 로 직접 가고, CLI 는 VPN 을 경유해 게이트웨이로 갑니다. 이 두 경로가 분리되어 있다는 점이 뒤에 나오는 트러블슈팅의 배경입니다.

둘째, 게이트웨이는 private 서브넷에 있어서 사내망을 거치지 않으면 도달할 수 없습니다.

이것은 제약이 아니라 의도적인 설계입니다. Claude Code 는 퍼블릭 주소로 해석되는 게이트웨이를 신뢰하지 않습니다.

셋째, 이 그림 어디에도 개발자의 자격증명이 없습니다.

Bedrock 호출은 ECS Task Role 로 수행됩니다.

2.2 CDK 스택 7종

npx cdk deploy --all 한 번으로 다음 일곱 개 스택이 생성됩니다.

#스택 이름내용
1ClaudeGatewayNetworkStackVPC, public/private 서브넷, NAT Gateway, 보안그룹 체인
2ClaudeGatewayDatabaseStackAurora Serverless v2 (PostgreSQL), 자동 일시정지, 저장 시 암호화
3ClaudeGatewaySecretsStackSecrets Manager 에 JWT 서명키와 OIDC client secret 주입
4ClaudeGatewayBuildMachineStack임시 EC2 빌드머신에서 이미지 빌드·푸시
5ClaudeGatewayStackECS Express Mode, private 서비스 + HTTPS 엔드포인트 자동 발급
6ClaudeGatewayAdminConsoleStackFastAPI 관리 콘솔, spend 대시보드·한도 CRUD·모델 접근 관리
7ClaudeGatewayVpnStackAWS Client VPN (상호 TLS)

네 번째와 다섯 번째 스택에서 실무적으로 유용한 설계가 두 가지 나옵니다.

BuildMachineStack 의 content-addressed 태그. gateway.yaml 의 내용이 바뀌면 이미지 태그가 바뀌고 태스크 정의가 자동으로 갱신됩니다. 설정을 고쳤는데 반영되지 않는 사고를 이 구조가 막아 줍니다.

GatewayStack 의 ECS Express Mode. cl-xxxxxxxx.ecs.<region>.on.aws 형태의 HTTPS 엔드포인트를 자동으로 발급하는데, 이 이름이 퍼블릭 DNS 에서 사설 IP 를 반환합니다. 그래서 Route 53 프라이빗 호스티드존 작업 없이도 /login 의 private 검사를 통과합니다.

2.3 인증 플로우 5단계

단계무슨 일이 일어나는가근거
1/login 진입. forceLoginMethod=gateway 가 배포된 단말은 Cloud gateway 화면으로 바로 진입합니다managed-settings.json
2게이트웨이가 user_code 와 verification_uri 를 발급하고 브라우저에서 /device 가 열립니다RFC 8628 Device Grant
3브라우저는 Okta 에 로그인하고, 게이트웨이는 Okta 와 백채널로 토큰을 교환합니다. id_token 의 email 과 groups 를 읽습니다Authorization Code + PKCE
4게이트웨이가 자체 서명한 세션 JWT 를 발급합니다session.jwt_secret, ttl_hours
5Bearer 를 달고 /v1/messages 로 요청합니다. 모델 allowlist 와 spend 를 검증한 뒤 ECS Task Role 로 Bedrock 을 호출합니다

단계 4의 포인트가 중요합니다. 게이트웨이는 Okta 가 발급한 토큰을 그대로 사용하지 않고, 자기가 서명한 토큰을 새로 발급합니다. 이 사실이 뒤에서 다룰 Claude Desktop 401 문제의 원인이 됩니다.

결과적으로 개발자 PC 에는 AWS 자격증명도 Anthropic API Key 도 남지 않습니다. 퇴사 처리는 Okta 에서 완결되고, 다음 세션 갱신이 실패하면서 접근이 끊깁니다.

2.4 gateway.yaml: 필수 5개 섹션

설정은 이 파일 하나가 전부입니다. 필수 섹션이 다섯 개이고 나머지는 모두 기본값을 가집니다.

listen:
  host: 0.0.0.0
  port: 8080
  public_url: https://claude-gw.internal.example.com
  trusted_proxies: [10.0.0.0/16]

oidc:
  issuer: https://<your-org>.okta.com
  client_id: 0oa1example2
  client_secret: ${OIDC_CLIENT_SECRET}
  allowed_email_domains: [example.com]
  userinfo_fallback: true                    # Okta org authorization server 에서는 필수
  scopes: [openid, profile, email, offline_access, groups]

session:
  jwt_secret: ${GATEWAY_JWT_SECRET}
  ttl_hours: 8                               # 기본값은 1

store:
  postgres_url: ${GATEWAY_POSTGRES_URL}

upstreams:
  - provider: bedrock
    region: us-east-1
    auth: {}                                 # AWS 기본 자격증명 체인 (ECS Task Role / IRSA)

각 섹션에서 실무적으로 중요한 지점만 짚겠습니다.

  • listen.public_url: 게이트웨이는 redirect_uri 와 discovery 문서를 오직 이 값으로만 생성합니다. X-Forwarded-Host 와 X-Forwarded-Proto 를 참조하지 않으므로, ALB 뒤에 있더라도 이 값이 기준입니다.
  • oidc.userinfo_fallback: 기본값은 false 입니다. id_token 이 email 이나 groups 를 생략할 때 /userinfo 에서 채워 옵니다. Okta org server, Keycloak 의 lightweight access token, ADFS 의 minimal token 에서 필요합니다.
  • oidc.scopes: 기본값은 [openid, profile, email, offline_access] 입니다. groups 는 기본값에 포함되지 않으므로 명시적으로 추가해야 합니다.
  • session.ttl_hours: 기본값은 1 입니다. 이 값이 곧 오프보딩 지연 시간이 됩니다. 짧을수록 회수가 빠르고, 길수록 IdP 왕복이 줄어듭니다.
  • store.postgres_url: PostgreSQL 14 이상이 필요합니다. device grant 랑데부 상태를 여기에 저장하므로 복제본 간 공유가 필수이고, 게이트웨이가 부팅 시 스키마 마이그레이션을 직접 수행하므로 CREATE TABLE 권한이 필요합니다.
  • upstreams[].auth: {}: AWS 기본 자격증명 체인을 사용한다는 의미입니다. ECS Task Role 이나 EKS IRSA 를 그대로 활용하므로 액세스 키를 명시하지 않습니다.

한 가지 더 짚어 둘 만한 설계가 있습니다. 알 수 없는 키가 있으면 부팅 자체가 실패합니다. 오타가 조용히 무시되지 않고 필드 이름을 지목하면서 종료됩니다. 그리고 시크릿은 전부 ${VAR} 확장으로 빠져 있으므로 이 파일 자체는 형상 관리에 포함해도 됩니다.

2.5 managed.policies: IdP 그룹이 곧 정책입니다

서두에서 “정책이 여전히 단말에 있다”를 강조 했었는데, 그것에 대한 해결책이 바로 이 블록입니다.

managed:
  policies:
    # 구체적인 그룹을 먼저 배치합니다
    - match: { groups: [eng-contractors] }
      cli:
        availableModels: [claude-sonnet-4-6]
        permissions:
          deny: ["WebFetch", "Read(./.env)"]
      desktop:
        isLocalDevMcpEnabled: false

    # catch-all 은 맨 마지막에 두고, base layer 로 동작합니다
    - match: {}
      cli:
        availableModels: [claude-opus-4-8, claude-sonnet-4-6, claude-haiku-4-5]
        env: { DISABLE_UPDATES: "1" }
      desktop: {}

여기서 실무자가 반드시 숙지해야 할 규칙은 네가지 입니다.

첫째, 평가 순서. 위에서부터 첫 매칭 하나만 선택되고, 그 정책이 match: {} catch-all 위에 병합됩니다. catch-all 이 base layer 역할을 하므로, 역할별 정책에는 조직 기본값과 다른 부분만 기재하면 됩니다.

둘째, 병합 규칙이 키 유형에 따라 다릅니다.

키 유형병합 방식
허용 목록 (availableModels 등)교체됩니다
permissions.deny · permissions.ask · hooks 배열 등base 와 정책의 합집합
env · modelOverrides · skillOverridesshallow merge

deny 와 hooks 가 합집합인 것은 전사 금지 규칙이나 감사 훅이 개별 역할 정책 때문에 실수로 해제되지 않도록 하려는 설계입니다.

셋째, 서버측 강제. availableModels 는 클라이언트에 전달되는 표시 목록에 그치지 않고 /v1/messages 요청 시점에도 검사됩니다. 클라이언트를 어떻게 수정하더라도 400 으로 거부됩니다.

넷째, 반영 시점이 두 가지로 나뉩니다. 이것을 모르면 반드시 혼란이 발생합니다.

무엇을 바꿨는가언제 반영되는가
정책 내용다음 managed settings 폴링, 최대 1시간 이내
사용자의 그룹 멤버십다음 세션 재발급, session.ttl_hours 로 제한

자주 나오는 질문 하나를 미리 답변드리자면, 이 게이트웨이는 SCIM 을 지원하지 않습니다. 실제로는 지원하지 않는 것이 아니라 동기화할 대상이 없습니다. 게이트웨이는 사용자 명단을 보관하지 않고 매 요청마다 토큰을 보고 판단할 뿐입니다. 생애주기 관리는 IdP 의 SCIM 에서 그대로 수행하시면 됩니다.

2.6 서버가 강제하는 것과 클라이언트가 적용하는 것

이 경계가 불분명하면 “정책을 배포했는데 적용되지 않는다”가 반복됩니다.

서버측 강제 (우회 불가)클라이언트 적용 (managed tier)
모델 허용목록 → 요청 시 400 거부permissions.deny / ask 규칙
Spend Limit → 429 반환env 환경변수 주입
세션 만료, IdP 그룹 판정hooks (PreToolUse 등)
이메일 도메인과 email_verified 검증sandbox 네트워크 규칙
감사 로그 기록모델 피커 표시 목록

왼쪽은 게이트웨이가 응답 코드로 차단하므로 클라이언트를 수정해도 통과할 수 없습니다. 오른쪽은 사용자 설정이나 프로젝트 설정보다 우선하는 managed 티어에 들어가지만, 실행 주체는 어디까지나 클라이언트입니다.

그리고 반드시 함께 설명해야 하는 사실이 있습니다. 게이트웨이는 유일한 경로를 강제하지 않습니다. 자기 API 키나 AWS 자격증명을 보유한 개발자는 여전히 프로바이더를 직접 호출할 수 있습니다. 그 경로를 차단하는 것은 게이트웨이가 아니라 네트워크 egress 정책의 몫입니다. 보안 검토에서 이 질문이 나오면 “게이트웨이는 정상 경로를 안전하게 만드는 장치이고, 우회 경로 차단은 별도 통제입니다”라고 답하시면 됩니다.


3. Okta 기준 구축하기

3.1 사전 준비 체크리스트

아래 체크 리스트 중 누락되는 것이 있으면 첫 로그인에서 막힐 수 있습니다.

#항목내용
P-1Claude Code 버전서버와 단말 모두 v2.1.195 이상. Claude Desktop 연동은 게이트웨이가 v2.1.203 이상
P-2Okta OIDC Web Appconfidential 앱, redirect URI https://<gateway>/oauth/callback
P-3Bedrock 모델 액세스콘솔 Model catalog 에서 Anthropic use case form 을 계정당 1회 제출
P-4네트워크2개 AZ 의 private 서브넷과 NAT Gateway. 게이트웨이는 Bedrock 과 IdP 양쪽으로 나가야 합니다
P-5PostgreSQL 14 이상샘플은 Aurora Serverless v2 를 자동 생성합니다
P-6로컬 도구Node.js 20 이상, AWS CDK CLI, AWS CLI v2, Docker
P-7private 호스트명/login 은 해석된 IP 가 하나라도 퍼블릭이면 거부합니다

P-3 은 IAM 권한만으로 해결되지 않습니다. Bedrock 콘솔에서 use case form 을 제출해야 하고, 제출하지 않으면 403 이 발생합니다. IAM 정책에는 교차 리전 추론 프로파일 ARN 과 파운데이션 모델 ARN 을 양쪽 다 부여해야 합니다.

{
  "Effect": "Allow",
  "Action": ["bedrock:InvokeModel", "bedrock:InvokeModelWithResponseStream"],
  "Resource": [
    "arn:aws:bedrock:<region>:<account-id>:inference-profile/us.anthropic.*",
    "arn:aws:bedrock:*::foundation-model/anthropic.*"
  ]
}

리전에 관한 주의 사항: 게이트웨이의 내장 모델 카탈로그는 us.anthropic.* 추론 프로파일로 해석되고, 위 IAM 정책도 그 ARN 을 기준으로 합니다. ap-northeast-2 (서울) 를 사용하시려면 models: 블록에 해당 지역의 추론 프로파일 ID 를 명시하고 IAM 정책의 ARN 접두사도 함께 변경하셔야 합니다. 이 작업을 놓치면 온디맨드 처리량을 지원하지 않는다는 ValidationException 이 표시됩니다.

3.2 Step 1: Okta OIDC 앱 등록

게이트웨이가 관여하는 유일한 IdP 설정입니다.

항목값
App 유형OIDC, Web Application (confidential)
Grant typeAuthorization Code + Refresh Token
Sign-in redirect URIhttps://<gateway>/oauth/callback
Assignments게이트웨이를 사용할 사용자와 그룹
Groups claim filterMatches regex .* 또는 필요한 그룹만
관리자 그룹claude-gateway-admins (CDK 의 -c adminOktaGroupName)

Okta 관련한 주의점이 세 가지 있습니다.

첫째, thin id_token. https://<org>.okta.com 을 issuer 로 사용하면 이것이 org authorization server 인데, email 과 groups 를 id_token 에 넣어 주지 않습니다. 로그인은 성공하지만 정책 매칭이 되지 않습니다. oidc.userinfo_fallback: true 를 반드시 활성화하셔야 합니다.

둘째, groups 는 조건이 두 개입니다. oidc.scopes 에 groups 를 요청해야 하고, 동시에 Okta 앱의 groups claim filter 가 허용해야 합니다. 그리고 userinfo_fallback 은 요청하지 않은 클레임을 채워 주지 못하므로, scope 를 설정하지 않은 채 fallback 만 켜면 여전히 빈 값입니다.

셋째, custom authorization server 를 사용하는 경우. https://<org>.okta.com/oauth2/default 는 email 과 groups 를 직접 발급하므로 userinfo_fallback 이 필요하지 않습니다.

여기에 순서 문제가 하나 더 있습니다. 게이트웨이 URL 은 배포 후에야 확정됩니다. ECS Express Mode 가 발급하기 때문입니다. 그래서 절차를 정리하면 다음과 같습니다.

  1. Okta 앱을 임시 redirect URI 로 먼저 생성합니다.
  2. CDK 로 배포합니다.
  3. 배포 결과로 나온 실제 URL 을 Okta 의 redirect URI 에 등록합니다.

세 번째 단계를 누락하면 Invalid parameter: redirect_uri 가 표시됩니다.

3.3 Step 1-B: Okta 그룹 설계 = 권한 설계

이 절은 실제 구축에서 자주 누락되었던 부분입니다. 그룹 하나가 세 곳에서 동시에 사용됩니다.

#그룹사용되는 위치역할
1claude-gateway-adminsCDK -c adminOktaGroupName관리자 콘솔 접근 허용
2사용자 그룹 (예: eng-all)gateway.yaml 의 managed.policiesmatch.groups 로 매칭되어 모델 허용목록·권한·환경변수가 갈립니다
3동일한 사용자 그룹관리자 콘솔 /spend/limitsscope = rbac_group 으로 그룹 단위 지출 한도를 설정합니다

참고로 AWS 샘플의 admin-console 코드에 claude-gateway-admins 라는 이름이 고정되어 있습니다. 변경하실 수는 있지만 코드도 함께 수정해야 하므로, 그대로 사용하시는 편을 권합니다.

온보딩과 오프보딩 절차는 다음과 같이 정리됩니다.

온보딩: 관리자 작업

  1. Okta 앱에 해당 사용자를 할당합니다.
  2. 필요한 그룹에 추가합니다. 정책과 한도가 따라옵니다.
  3. 개발자에게 온보딩 가이드와 VPN 프로파일을 전달합니다.

오프보딩: 오프보딩으로 접근 차단

  1. Okta 계정을 비활성화하거나 앱 할당을 해제합니다.
  2. 최대 session.ttl_hours 이내에 차단됩니다.
  3. 즉시 차단이 필요하면 금액 0 인 개인 한도를 부여합니다. 다음 요청부터 즉시 차단됩니다.

게이트웨이에는 회수할 키가 없습니다. 그룹에서 제거하면 다음 토큰 갱신에서 접근이 종료됩니다.

3.4 Step 2: CDK 배포

컨텍스트 파라미터는 총 4개 입니다.

git clone https://github.com/aws-samples/sample-claude-apps-gateway-on-aws.git
cd sample-claude-apps-gateway-on-aws/cdk && npm install

npx cdk bootstrap aws://<account-id>/<region>

npx cdk deploy --all \
  -c oidcIssuer=https://<your-org>.okta.com \
  -c oidcClientId=<Okta client id> \
  -c oidcClientSecret='<Okta client secret>' \
  -c adminOktaGroupName=claude-gateway-admins

# 배포 후 게이트웨이 URL 확인
aws cloudformation describe-stacks \
  --stack-name ClaudeGatewayStack \
  --query "Stacks[0].Outputs[?OutputKey=='GatewayEndpoint'].OutputValue" \
  --output text

소요 시간은 25~40분 정도입니다. 대부분 Aurora 생성, 빌드머신의 이미지 빌드와 푸시, ECS Express Mode 의 ALB 프로비저닝에 사용됩니다.

배포에서 주의할 포인트를 4가지로 정리 했습니다.

  • 시크릿은 작은따옴표로 감싸십시오. PowerShell 에서 큰따옴표를 사용하면 $ 가 변수로 치환되어 잘못된 값이 주입됩니다. 오류가 발생하지 않으므로 로그인 시점이 되어서야 문제를 알게 됩니다.
  • 재배포 전에 cdk diff 를 먼저 실행하십시오. OIDC 환경변수가 변경된다고 표시되면 컨텍스트 값이 잘못된 것입니다.
  • gateway.yaml 을 수정했다면 이미지에 COPY 되므로 ClaudeGatewayBuildMachineStack 과 ClaudeGatewayStack 을 재배포해야 합니다. 설정 파일만 바꾸고 서비스를 재시작하는 방식으로는 반영되지 않습니다.
  • 검증이 끝나면 npx cdk destroy --all 을 실행하십시오. 상시 과금 요소는 NAT Gateway(시간당 약 $0.045 와 처리량), Client VPN 엔드포인트(시간당 약 $0.10), ALB 2대, Aurora Serverless v2(유휴 시 자동 정지), 그리고 Bedrock 사용량입니다. Aurora 는 자동으로 정지되지만 NAT Gateway 와 Client VPN 엔드포인트는 아무도 사용하지 않아도 계속 과금됩니다. 단말 접근을 SSM 기반 bastion 으로 대체하면 Client VPN 엔드포인트 요금은 제거할 수 있습니다.

3.5 Step 3: 배포 후 조치와 검증

배포 직후에 반드시 수행해야 할 작업이 두 가지입니다.

  1. Okta 에 실제 redirect URI <GatewayEndpoint>/oauth/callback 를 등록합니다.
  2. Client VPN 프로파일을 배포합니다. ClaudeGatewayVpnStack 의 Outputs 를 확인하면 Secrets Manager 에 완성된 프로파일이 저장되어 있습니다.

검증 체크리스트는 다음과 같습니다. 여기를 통과하지 못한 상태로 단말 배포로 넘어가지 마시기 바랍니다.

#확인 항목기대값
V-0/healthz200
V-1/.well-known/oauth-authorization-serverissuer 가 게이트웨이 URL 과 문자열까지 완전히 일치
V-2CloudWatch 부팅 로그config.load → migration → listening on 순서
V-3게이트웨이 호스트명 해석모든 주소가 사설 (getent ahosts 로 확인)
V-4claude 실행 후 /logindevice code 승인 후 로그인 완료
V-5감사 로그session.mint result=success
V-6추론 1회 실행 후 감사 로그inference 이벤트 기록
# CloudWatch Logs Insights
fields @timestamp, @message
| filter @message like /session\.mint|desktop_bootstrap|inference|auth\.denied/
| sort @timestamp desc
| limit 50

진단은 앞에서부터 실패 지점을 찾는 방식이 가장 빠릅니다.

증상원인
/healthz 가 200 이 아님부팅 실패. stderr 의 마지막 줄이 원인을 지목합니다
device authorization 실패Postgres 도달 불가 또는 쓰기 권한 없음
브라우저가 Okta 까지 도달하지 못함redirect URI 불일치
Okta 로그인 후 돌아와서 실패감사 로그의 거부 사유를 확인합니다

한가지 팁을 드리자면, 부팅이 되지 않을 때는 로그를 위에서부터 찾지 말고 stderr 의 마지막 줄부터 확인하는것을 권장합니다. Postgres 도달 불가, DDL 권한 없음, OIDC discovery 실패, 설정 스키마 위반 중 어느 것인지 필드 이름과 함께 표시됩니다.

V-5 의 session.mint result=success 로그는 스크린샷으로 남겨 두시기를 권합니다. 나중에 문제가 발생했을 때 정상 기준선으로 활용할 수 있습니다.

3.6 Step 4: 개발자 단말에 게이트웨이 URL 배포

게이트웨이가 모두 기동했더라도 개발자는 아직 접속할 수 없습니다. URL 을 단말에 배포해야 합니다.

{
  "forceLoginMethod": "gateway",
  "forceLoginGatewayUrl": "https://claude-gw.internal.example.com",
  "parentSettingsBehavior": "merge"
}
OSmanaged-settings.json 배포 위치
macOS/Library/Application Support/ClaudeCode/
WindowsC:\Program Files\ClaudeCode\
Linux/etc/claude-code/

여기에서 중요하게 확인할 포인트가 네 가지 있습니다.

첫째, 개발자 본인의 설정 파일은 무시됩니다. forceLoginGatewayUrl 은 관리자 소스에서만 읽힙니다. 공식 문서도 “A developer can’t set this up manually. The login picker has no gateway option, and forceLoginGatewayUrl is ignored in a developer’s own settings files” 라고 명시합니다. MDM 으로 배포하거나 관리자 권한으로 직접 배치해야 합니다.

둘째, forceLoginMethod 만 있고 URL 이 없으면 “Contact your IT administrator” 메시지에서 멈춥니다.

셋째, 우선순위가 있습니다. macOS 의 managed preferences plist 와 Windows 의 HKLM 정책이 managed-settings.json 파일보다 우선하고, 게이트웨이의 원격 설정은 그보다 더 우선합니다.

넷째, parentSettingsBehavior: "merge" 는 Claude Desktop 이 띄우는 임베드 세션에 egress 허용목록을 전달하기 위해 필요합니다. 이 키가 없으면 Desktop 이 띄운 세션에는 네트워크 제한이 적용되지 않는데, 경고도 표시되지 않습니다.

3.7 관리자 콘솔

게이트웨이 본체에는 Admin UI 가 없지만, AWS 샘플의 ClaudeGatewayAdminConsoleStack 이 FastAPI 기반 콘솔을 구성하고 있습니다. 접속은 세 단계입니다.

  1. 브라우저에서 콘솔 URL 에 접속합니다.
  2. Okta 로 로그인합니다. 개발자가 CLI 에서 사용하는 것과 같은 계정, 같은 앱입니다.
  3. /spend/dashboard 로 자동 이동합니다. claude-gateway-admins 멤버가 아니면 /not-authorized 로 이동합니다.

이 콘솔의 구성에서 눈여겨볼 지점은 세 가지입니다.

  • VPN 이 필요하지 않습니다. 게이트웨이 본체는 private 서브넷에 있어 개발자는 VPN 을 경유하지만, 콘솔은 public 서브넷에 배치됩니다. 설정 하나 변경 하려고 굳이 VPN 에 연결하지 않도록 한 의도적인 설계입니다.
  • 권한은 네트워크가 아니라 그룹으로 결정됩니다. 퍼블릭에 있지만 claude-gateway-admins 그룹 멤버만 사용할 수 있습니다.
  • 콘솔은 자격증명을 보관하지 않습니다. 로그인한 관리자 본인의 게이트웨이 토큰으로 동작하므로, 권한 회수는 Okta 그룹에서 제거하는 것으로 완결되고 최대 1시간 이내에 반영됩니다.

콘솔 화면은 세 가지로 구성되어 있는데, 동작 방식이 서로 다릅니다. 이것을 모르면 반드시 혼란이 발생합니다.

화면무엇을 하는가반영 시점감사 귀속
/spend/dashboard조직·그룹·사용자별 기간 사용량 조회조회 전용해당 없음
/spend/limits한도 생성·조회·삭제 (org / rbac_group / user)즉시, 다음 요청부터가능 (oidc:<Okta sub>)
/modelsBedrock 실시간 카탈로그에서 제공 모델 선택재배포 후, 보통 2분불가. CloudTrail 에 태스크 롤로만 기록

/models 는 게이트웨이 API 를 호출하는 것이 아니라 ECS 서비스의 환경변수를 갱신하므로 실제 재배포가 발생합니다. 그리고 행위자가 콘솔 서비스 계정으로만 기록되어 누가 변경했는지 특정할 수 없으므로, 추적이 필요하면 변경 관리 티켓이나 팀 채널 공지를 병행하시기를 권합니다.

한도와 관련해 관리자 매뉴얼에 반드시 포함해야 할 내용이 세 가지 있습니다.

  • 적용 한도 우선순위: 개인 한도가 있으면 그 값, 없으면 소속 그룹 중 가장 낮은 값, 그것도 없으면 조직 기본값, 마지막이 무제한입니다.
  • 그룹·조직 한도는 공유 풀이 아니라 1인당 기본값입니다. 조직에 월 $500 을 설정하면 전사 합계 $500 이 아니라 구성원 각자 월 $500 입니다. 총액 상한이 필요하면 AWS Budgets 같은 별도 수단을 사용하셔야 합니다.
  • 대시보드의 금액은 청구서가 아닙니다. 토큰 수에 USD 정가를 곱한 추정치이므로, 정확한 금액은 Cost Explorer 로 대조하셔야 합니다. 회계용 집계가 아니라 과다 사용을 차단하는 장치로 이해하시는 편이 정확합니다.

마지막으로 개발자 온보딩 문서에 명시해 두면 좋은 사실이 하나 있습니다. 프롬프트와 응답 본문은 저장되지 않습니다. 기록되는 것은 신원, 토큰 수, 모델, 지연시간입니다. “회사가 내 대화를 보는가”라는 질문에 이 한 문장으로 답할 수 있고, 거버넌스 도입에 대한 저항도 눈에 띄게 줄어듭니다.


4. Claude Desktop 연결

4.1 Claude Desktop은 Claude CLI 와 완전히 다른 경로를 사용합니다

같은 게이트웨이를 사용하지만 설정 경로도 세션도 별개입니다. 섞이지 않습니다.

항목Claude Code (CLI)Claude Desktop
설정 키forceLoginMethod / forceLoginGatewayUrlbootstrapUrl 과 inference* 계열
Windows 위치C:\Program Files\ClaudeCode\managed-settings.jsonHKLM\SOFTWARE\Policies\Claude
로그인/login → Cloud gateway 화면앱 자체의 device-code 흐름
세션게이트웨이 세션 A게이트웨이 세션 B (CLI 에 로그인해도 별도 로그인 필요)
서버측 요건없음 (기본 동작)정책에 desktop 키 옵트인 + 게이트웨이 v2.1.203 이상

다행히 Okta 쪽은 손댈 것이 없습니다. Desktop 은 게이트웨이하고만 통신하고, Okta 와 통신하는 것은 게이트웨이입니다. CLI 가 이미 동작한다면 Okta 앱 등록은 redirect URI 를 포함해 그대로 재사용됩니다. IdP 담당 부서에 다시 요청할 일이 없다는 점은 대기업 환경에서 상당히 큰 이점입니다.

4.2 서버에 추가하는 한 줄과 단말에 추가하는 다섯가지 키

서버 측은 정책에 desktop 키를 추가하면 끝입니다.

managed:
  policies:
    - match: {}
      cli: { availableModels: [...] }
      desktop: {}      # 이 한 줄이 옵트인입니다

이 키가 없으면 /user/bootstrap 이 모든 사용자에게 404 를 반환합니다. 공식 문서의 표현은 “/user/bootstrap returns 404 unless the policy matching the user carries a desktop key” 입니다. gateway.yaml 은 이미지에 COPY 되므로 이미지 재빌드와 서비스 갱신이 필요합니다.

desktop 블록에 넣을 수 있는 옵션은 전부 선택 사항이며, 지정하지 않으면 Desktop 의 기본값이 적용됩니다.

      desktop:
        modelDiscoveryEnabled: false      # 정책의 모델 목록만 사용
        isLocalDevMcpEnabled: false       # 로컬 MCP 서버 차단
        disableAutoUpdates: true
        banner: { text: "사내 PoC 환경" }

알 수 없는 키는 부팅을 실패시키므로 문서에 있는 키만 사용하십시오.

단말 측은 HKLM\SOFTWARE\Policies\Claude 아래에 REG_SZ 값 다섯 개를 설정합니다.

키값
inferenceProvidergateway
inferenceGatewayBaseUrl게이트웨이 오리진 (경로를 붙이지 않습니다)
inferenceCredentialKindinteractive
inferenceGatewayAuthSchemebearer
bootstrapUrl<위 오리진>/user/bootstrap

그리고 반드시 존재하면 안되는 키가 세 개 있습니다.

  • inferenceGatewayOidc
  • inferenceGatewayApiKey
  • inferenceCredentialHelper

이유는 2.3절에서 미리 언급한 내용과 연결됩니다. 이 게이트웨이는 자기가 session.jwt_secret 으로 서명한 세션 JWT 만 인정합니다. 그런데 inferenceGatewayOidc 는 IdP 에서 직접 받은 원본 토큰을 게이트웨이로 보내라는 설정입니다. LiteLLM 이나 Kong 처럼 게이트웨이가 IdP 의 JWT 를 직접 검증하는 제품을 위한 설정이므로, 이 게이트웨이에서는 401 invalid token 이 됩니다. inferenceGatewayApiKey 가 남아 있으면 그 문자열이 Bearer 로 전송되어 동일한 401 이 발생합니다.

이전 시도의 잔재로 남아 있는 경우가 많으므로 명시적으로 삭제까지 수행해야 합니다.

Windows Registry Editor Version 5.00

[HKEY_LOCAL_MACHINE\SOFTWARE\Policies\Claude]
"inferenceProvider"="gateway"
"inferenceGatewayBaseUrl"="https://REPLACE-ME.example.internal"
"inferenceCredentialKind"="interactive"
"inferenceGatewayAuthScheme"="bearer"
"bootstrapUrl"="https://REPLACE-ME.example.internal/user/bootstrap"
"inferenceGatewayOidc"=-
"inferenceGatewayApiKey"=-
"inferenceCredentialHelper"=-

[HKEY_CURRENT_USER\SOFTWARE\Policies\Claude]
"inferenceGatewayOidc"=-
"inferenceGatewayApiKey"=-
"inferenceCredentialHelper"=-

끝의 =- 는 값 삭제를 의미합니다. 그리고 이 .reg 파일은 UTF-8 BOM + CRLF 로 저장해야 regedit 가 정상적으로 받아들입니다.

관리 구성은 앱 기동 시 1회만 읽습니다. 창 닫기가 아닌, 트레이 아이콘에서 완전 종료한 뒤 다시 실행해야 합니다.

성공 여부는 감사 로그의 세 이벤트로 판정합니다.

이벤트의미
session.mint result=success로그인 성공
desktop_bootstrap.serveDesktop 이 정책을 수신
inference실제 추론 기록

5. 실전에서 막혔던 지점 9건

여기부터가 이 글의 핵심이라고 생각합니다. 공식 문서만 읽어서는 드러나지 않고, 실제로 배포하고 온보딩해 보면서 겪은 것들입니다.

5.1 개발자 온보딩에서 막히는 3곳

① 기존 로그인 흔적이 게이트웨이를 우회합니다

가장 많이 힘들었던 부분입니다. 설정은 전부 정확한데 게이트웨이를 거치지 않는 경우였습니다. forceLoginMethod 는 로그인 화면을 고정할 뿐이고, 이미 인증된 세션을 무효화하지 않기 때문입니다.

  • PC 에 ANTHROPIC_* 환경변수가 남아 있는 경우. Windows 는 User 범위와 Machine 범위를 양쪽 다 확인해야 합니다.
  • 개인 ~/.claude/settings.json 에 apiKeyHelper 가 있는 경우.
  • 기존 Pro / Max 로그인 세션이 살아 있는 경우.

조치 방법은 순서대로 1.환경변수 제거, 2.apiKeyHelper 삭제, 3./logout 입니다.

② 설정 파일 인코딩 때문에 아무 일도 일어나지 않습니다

PowerShell 의 Set-Content 나 메모장의 기본 저장이 BOM 을 붙이거나 UTF-16 으로 기록합니다. 그러면 CLI 가 파싱하지 못하고 조용히 명령어를 무시합니다. 오류도 표시되지 않습니다.

[System.IO.File]::WriteAllText($path, $content, (New-Object System.Text.UTF8Encoding($false)))

레거시 경로 C:\ProgramData\ClaudeCode\ 는 v2.1.75 부터 지원되지 않으므로, 예전에 생성해 둔 파일이 있으면 제거하십시오.

③ VPN 미연결 상태에서는 오류 메시지 없이 대기합니다

타임아웃 메시지조차 표시되지 않습니다. 고장난 것처럼 보이지만 정상 동작입니다. 게이트웨이가 private 서브넷에 있기 때문입니다. 그리고 게이트웨이가 응답하지 않으면 세션이 약 10초 뒤에 오류와 함께 종료됩니다. 정책 없이 실행되는 상황을 막으려는 의도적인 fail-closed 동작입니다.

VPN 을 먼저 연결하고 새 터미널에서 실행하십시오.

성공 판별 기준은 단 하나입니다. Claude Code 시작 배너 하단에 Cloud gateway 가 표시되어야 합니다. 응답이 정상적으로 오더라도 배너가 다르다면, 회사 게이트웨이가 아니라 개인 자격증명으로 동작하고 있는 상태입니다.

5.2 인프라와 연결에서 겪은 6건

① /login 이 거부됩니다

Gateway hosts must be on your organization's private network;
<host> resolves to the public (or unrecognized) address <ip>

원인은 듀얼스택 내부 ALB 가 퍼블릭 대역의 AAAA 레코드를 광고하기 때문입니다. A 레코드는 사설인데 AAAA 가 퍼블릭인 상황입니다. P-7 에서 “하나라도 퍼블릭이면 거부”라고 했던 조건이 이 경우에 해당합니다.

ALB 를 생성할 때 --ip-address-type ipv4 를 지정하십시오. 이미 생성했다면 다시 만들어야 합니다. EKS 에서는 alb.ingress.kubernetes.io/ip-address-type: ipv4 어노테이션을 사용합니다.

② Claude Desktop 이 계속 401 을 반환

4.2절에서 설명한 inferenceGatewayOidc 잔재입니다. 키를 삭제한 뒤 트레이에서 완전 종료하고 재실행해야 합니다. 창만 닫는 것으로는 반영되지 않습니다.

③ /user/bootstrap 이 404 를 반환

정책에 desktop 키가 없는 경우입니다. desktop: {} 를 추가하고 이미지를 재빌드한 뒤 ClaudeGatewayBuildMachineStack 과 ClaudeGatewayStack 을 재배포하십시오.

④ 401 을 옵트인 완료 근거로 잘못 판단한 경우

개인적으로 가장 어려웠던 점이었습니다. 로그인 전에 /user/bootstrap 을 호출해 보고 401 이 반환되기에 “인증만 하면 되겠구나, 옵트인은 되어 있구나”라고 판단했는데 틀렸습니다.

인증 검사가 정책 검사보다 먼저 수행됩니다. 그래서 로그인 전에는 정책과 무관하게 항상 401 입니다. 옵트인 여부를 판정하는 근거가 될 수 없습니다. 실제로 로그인한 뒤에야 404 가 나타났습니다. 판정은 감사 로그의 desktop_bootstrap 이벤트로 하셔야 하고, 거부된 경우 desktop_bootstrap.denied 의 reason 값(not_configured / policy_not_opted_in / no_policy_matched)을 확인하시면 됩니다.

⑤ 스트리밍이 중간에 끊김

ALB 의 idle timeout 기본값이 60초입니다. 토큰이 활발히 전송되는 동안에는 문제가 없지만, 첫 토큰이 나오기 전의 긴 침묵 구간이나 extended thinking 중에 연결이 끊깁니다.

aws elbv2 modify-load-balancer-attributes --load-balancer-arn "$ALB_ARN" \
  --attributes Key=idle_timeout.timeout_seconds,Value=3600

⑥ VDI 환경에서 Cowork 기능을 사용 할 수 없음

중첩 가상화가 필요한데 VDI 환경에서 제공되지 않는 경우가 있습니다. Chat 기능과 게이트웨이 인증은 정상 동작하고 Cowork 만 해당됩니다. 물리 단말이나 중첩 가상화를 지원하는 EC2 인스턴스를 사용하셔야 합니다.


6. IdP 를 교체한다면: 경험해서 정리한 포인트 5가지

이 글은 Okta 를 기준으로 설명했지만, Entra ID 나 Keycloak 을 사용하시는 분도 계실 것입니다. 결론부터 말씀드리면 바뀌는 것은 oidc 블록 하나입니다. 게이트웨이는 OIDC 표준만 참조하기 때문입니다.

실제로 IdP 를 교체하면서 확인한 포인트는 다섯 가지 입니다.

① 스코프 등록과 클레임 매퍼는 다른 작업입니다 (invalid_scope)

게이트웨이는 groups 를 scope 로 요청합니다. IdP 에 그 이름의 scope 가 등록되어 있지 않으면 invalid_scope 로 거부되어 로그인 자체가 실패합니다. 클레임을 채우는 매퍼를 만드는 작업과 scope 를 등록하는 작업은 별개이므로 혼동하기 쉽습니다.

② 그룹 경로 접두사 (match 불일치)

IdP 에 따라 그룹명이 /team-a 처럼 경로 접두사와 함께 전달됩니다. 그러면 정책의 match 가 성립하지 않습니다. 대소문자도 정확히 일치해야 합니다. 참고로 Entra ID 는 그룹 이름 대신 Object ID(GUID)를 발급하므로, managed.policies 가 GUID 로 매칭하거나 App Role 을 사용하면서 oidc.groups_claim: roles 를 설정해야 합니다.

③ email_verified 는 부재와 false 가 다릅니다

클레임이 아예 없는 것과 명시적으로 false 인 것은 다르게 처리됩니다. 명시적 false 는 거부됩니다. 그리고 allowed_email_domains 를 설정했다면 email 클레임 자체도 필요합니다.

④ refresh token 이 없으면 무음 갱신도 없습니다

offline_access 가 발급되지 않으면 session.ttl_hours 마다 브라우저 로그인을 다시 수행해야 합니다. 이런 경우 ttl_hours 를 8 이나 12 로 올리되, 오프보딩 지연이 그만큼 길어진다는 점을 감수하셔야 합니다.

⑤ IdP 노드 간 상태 공유 (invalid_grant)

IdP 를 여러 노드로 구성했는데 노드 간에 상태가 공유되지 않으면, authorization code 를 발급한 노드와 교환 요청을 받는 노드가 달라질 수 있습니다. 그러면 invalid_grant 가 발생합니다.

여기서 혼란스러운 지점은 브라우저 로그인은 정상적으로 동작한다는 것입니다. 브라우저는 쿠키로 같은 노드에 고정되기 때문입니다. 반면 게이트웨이의 백채널 요청은 쿠키가 없어 임의의 노드로 전달됩니다. “브라우저는 되는데 게이트웨이만 안 된다”는 증상이 이 경우에 해당합니다.


마치며

지금까지의 내용을 정리하자면, 크게 세가지로 정리 할 수 있습니다.

첫째, IdP 가 곧 접근 제어입니다. 온보딩은 그룹 추가이고 오프보딩은 그룹 제거입니다. 게이트웨이에는 사용자 명단도, 회수할 키도 없습니다. 지난 글에서 다룬 가상키 수명 관리라는 이슈가 이 구조에서는 사라집니다.

둘째, 정책을 서버가 배포합니다. MDM 으로 단말에 내려보내는 것은 게이트웨이 URL 한 줄이고, 나머지 정책은 게이트웨이가 최대 1시간 주기로 갱신합니다. 모델 허용목록은 /v1/messages 요청 시점에 서버가 강제하므로 클라이언트를 수정해도 우회할 수 없습니다.

셋째, 범용 게이트웨이와 배타적이지 않습니다. 애플리케이션과 배치 트래픽은 LiteLLM 으로, 개발자 IDE 트래픽은 Claude apps Gateway 로 보내는 구성이 현실적으로 가장 흔한 답입니다. 백엔드가 Bedrock 한 곳이므로 비용도 결국 한 곳에서 집계됩니다. 역할이 다른 것이지 우열이 있는 것이 아닙니다.

지난 글도 비슷한 문장으로 마무리했는데 이번에도 마찬가지로 정리 하겠습니다. 사내 AI 코딩 도구 도입은 “도입할 것인가”가 아니라 “어떻게 통제 가능한 형태로 확산할 것인가”의 문제입니다.

프로덕션 배포 전에는 반드시 자체 보안 검토를 수행하시기 바랍니다. 그리고 검증이 끝난 PoC 환경은 npx cdk destroy --all 로 정리하시기 바랍니다.


참고 자료

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다