Codex CLI 401 Unauthorized: 테스트된 9가지 원인 및 유사 사례
요약
Codex CLI 사용 중 발생하는 401 Unauthorized 인증 오류의 9가지 원인과 진단 방법을 설명합니다. 상태 코드보다 에러 메시지 본문에 집중하여 환경 변수 누락, 잘못된 API 키, 헤더 누락 등을 구분하는 법을 다룹니다.
핵심 포인트
- 401 오류 발생 시 상태 코드보다 에러 메시지 본문을 먼저 확인해야 함
- 환경 변수 누락은 네트워크 호출 자체가 발생하지 않은 상태임
- API 키 검증 시 모델 카탈로그 엔드포인트 대신 추론 엔드포인트를 사용 권장
- 인증 실패 유형에 따라 즉시 수정, 경로 전환, 점검 중단 중 적절한 대응 필요
요약 (TL;DR): Codex CLI 인증은 아홉 가지의 서로 다른 방식으로 실패하며, 그중 일부만이 문자 그대로의 401로 나타납니다. 바로 이 점 때문에 상태 코드(status code)는 출력 내용 중 가장 유용하지 않은 부분입니다. 원인을 식별하는 것은 메시지 본문입니다. Missing bearer or basic authentication in header는 아무것도 전송되지 않았음을 의미하며, 여기서 놀라운 점은 기본 제공자(default provider)를 사용할 때 OPENAI_API_KEY를 내보내기(exporting) 해도 도움이 되지 않는다는 것입니다. Incorrect API key provided는 키가 전달되었으나 거부되었음을 의미합니다. You didn't provide an API key는 헤더가 전송 과정에서 누락되었음을 의미하며, 이는 거의 항상 값이 줄바꿈(newline)으로 끝나기 때문에 발생합니다. 로컬의 Missing environment variable 오류는 401이 아니며, /v1이 누락된 base_url로 인한 404 역시 마찬가지입니다. 아래의 모든 사례는 2026-07-30에 Codex CLI 0.146.0 버전에서 재현되었습니다.
30초 진단법
다음 세 가지 확인 사항을 이 순서대로 진행하세요. 대부분의 사람들은 첫 번째 단계를 건너뛰고, 멀쩡한 키를 재발급받느라 20분을 허비합니다.
| 확인 사항 | 명령어 | 알려주는 내용 |
|---|---|---|
| 1. 요청이 발생하기는 했는가? | 출력에서 Missing environment variable 확인 | 만약 존재한다면, Codex는 네트워크 호출을 전혀 하지 않은 것입니다. 설정이 설정되지 않았거나 비어 있는 변수를 가리키고 있습니다. |
| ... |
3단계에서 미리 언급할 만한 함정이 하나 있습니다. 인터넷상의 트러블슈팅 조언 절반이 이를 틀리게 설명하기 때문입니다: 애그리게이터(aggregator)를 키 확인 용도로 /v1/models를 사용하지 마세요. 2026-07-30에 테스트한 결과, https://api.ofox.ai/v1/models는 가짜 키를 보냈을 때나 Authorization 헤더를 아예 보내지 않았을 때도 전체 카탈로그와 함께 200을 반환합니다. 모델 카탈로그는 공개되어 있기 때문입니다. OpenAI 자체의 api.openai.com/v1/models는 키 없이 요청했을 때 401을 반환하며, 여기서 이러한 습관이 생겨났지만, 그 습관이 그대로 적용되지는 않습니다. 실제로 추론(inference)을 수행하는 엔드포인트를 사용하세요.
언제 수정하고, 언제 전환하며, 언제 멈춰야 하는가
인증 실패는 어떤 유형인지 알고 있을 때는 수정 비용이 저렴하지만, 추측에 의존할 때는 비용이 많이 듭니다. 대략적인 규칙은 다음과 같습니다:
- 즉시 수정 (Fix it): 메시지가
Missing environment variable(환경 변수 누락),Incorrect API key provided(잘못된 API 키 제공), 또는 리프레시 토큰 (refresh token)을 언급하는 등 구체적인 원인을 지칭하는 경우입니다. 이러한 오류는 결정론적인 단일 단계 해결책이 존재하며, 각각 2분 이내에 해결할 수 있습니다. - 인증 경로 전환 (Switch the auth path): ChatGPT 로그인 흐름에서 세 번 이상 시도했을 때입니다. API 키 경로는 구성 요소가 더 적으며 (리프레시 토큰 없음, 브라우저 왕복 없음, 8일의 리프레시 윈도우 없음), 무언가를 자동화하고 있다면 컨테이너 재시작 후에도 유지되는 유일한 경로입니다.
- 중단 후 다른 사항 점검 (Stop and check something else): 401 대신 404 오류가 발생하거나, CLI가 설정 오류 (config error)로 인해 실행을 거부하는 경우입니다. 이는 인증 문제가 아니며, 키를 아무리 교체해도 해결되지 않습니다. 오류가 권한 부여 (authorization)가 아닌 사용량 제한 (usage limit)을 언급하는 경우도 마찬가지입니다.
키를 재발급하는 것이 올바른 첫 번째 조치인 유일한 경우는, 오류에 표시된 마스킹된 접미사 (masked suffix)가 현재 사용 중이라고 생각하는 키와 일치하면서 Incorrect API key provided 오류가 발생하는 경우입니다. 만약 접미사가 일치하지 않는다면, 그것은 키의 문제가 아니라 설정 (config) 문제이며, 새 키를 발급받아도 동일하게 실패할 것입니다.
401 읽기: 세 가지 메시지, 세 가지 원인
이것이 핵심 표입니다. 모든 행은 실제 엔드포인트 (live endpoint)를 대상으로 재현되었습니다.
| Message body | 실제로 발생한 일 | 출처 | 해결 방법 |
|---|---|---|---|
Missing bearer or basic authentication in header | 요청에 자격 증명(credential)이 첨부되지 않음 | 로그인 정보가 없는 기본 OpenAI 제공자, 또는 OPENAI_API_KEY만 내보낸 경우 | `printenv OPENAI_API_KEY \ |
| ... | |||
테스트 실행에서 파악된 두 가지 세부 사항으로 출력 내용을 읽기 쉽게 만들었습니다. 기본 OpenAI 제공자의 경우, Codex는 먼저 wss://api.openai.com/v1/responses에 대해 WebSocket 전송을 시도하고 5번 재시도한 후, HTTPS로 폴백(fallback)하여 5번 더 재시도합니다. 따라서 단일 인증 실패가 실제 메시지가 나오기 전에 대략 열 개의 오류 줄을 생성합니다. 사용자 지정 제공자의 경우, WebSocket 전송이 비활성화되어 있습니다 (codex doctor에서 supports websockets: false 보고). 따라서 한 번의 재시도 루프와 더 깔끔한 실패를 얻게 됩니다. 만약 Reconnecting... 4/5라는 벽을 바라보고 있다면, 맨 아래로 스크롤하세요. 마지막 줄이 중요한 정보입니다. |
원인 1: OPENAI_API_KEY를 내보내고 충분하다고 가정함
가장 흔한 경우이며, 직관적이지 않기 때문에 가장 상단에 배치할 가치가 있습니다.
export OPENAI_API_KEY="sk-proj-..."
codex exec "say hi"
# 오류: 예상치 못한 상태 401 Unauthorized: Missing bearer or basic
...
서버가 말한 것을 주목하세요: Missing bearer.
printenv OPENAI_API_KEY | codex login --with-api-key
환경 변수 (Environment variables)가 작동하기는 하지만, 이는 커스텀 프로바이더 (custom provider)의 env_key 필드를 통해서만 가능하며, 이는 아래에서 더 자세히 다룰 다른 메커니즘입니다.
원인 2: 오래된 --api-key 플래그를 사용함
만약 2026년 중반 이전에 작성된 튜토리얼을 따랐다면 다음과 같은 상황일 수 있습니다:
codex login --api-key "sk-proj-..."
# --api-key 플래그는 더 이상 지원되지 않습니다. 대신 키를 파이프 (pipe)로 전달하세요.
# 예: `printenv OPENAI_API_KEY | codex login --with-api-key`.
메시지는 명확하지만, 아무것도 기록하지 않고 종료되며, 설정 스크립트 (setup script) 내에서는 출력이 빠르게 지나가 버립니다. 그러면 다음 명령어가 Missing bearer 오류와 함께 실패하며 키(key)가 원인으로 지목됩니다. auth.json 파일이 존재하며 예상한 내용을 포함하고 있는지 확인하세요:
cat ~/.codex/auth.json
# {
# "auth_mode": "apikey",
...
원인 3: env_key가 설정되지 않은 변수를 가리킴 (401 오류가 아님)
커스텀 프로바이더 (custom provider) 블록을 사용할 때, Codex는 지정된 환경 변수에서 키를 읽어옵니다:
model = "openai/gpt-5.5"
model_provider = "ofox"
...
만약 OFOX_API_KEY가 설정되어 있지 않다면, 401 오류가 발생하지 않습니다. 대신 로컬 에러가 발생하며 네트워크 호출 자체가 이루어지지 않습니다:
ERROR: Missing environment variable: `OFOX_API_KEY`.
빈 문자열 (empty string) 또한 동일한 에러를 생성하는데, 이는 소스 코드와 일치합니다: model-provider-info/src/lib.rs는 변수를 사용하기 전에 !v.trim().is_empty()로 필터링합니다. 따라서 Codex 입장에서는 export OFOX_API_KEY=""를 실행하는 것과 변수를 아예 내보내지 (export) 않는 것이 동일합니다.
이 문제는 Codex를 실행하는 셸 (shell)이 변수를 내보낸 셸과 다른 launchd, systemd, 그리고 Docker 환경에서 가장 빈번하게 발생하여 골칫거리가 됩니다.
원인 4: 키 값에 포함된 줄바꿈 (Newline)
이것은 이번 사례 중 가장 까다로운 원인입니다. 왜냐하면 printenv를 통해 눈으로 직접 확인했을 때는 키가 분명히 존재함에도 불구하고, 에러 메시지는 키를 제공하지 않았다고 비난하기 때문입니다.
export OFOX_API_KEY="$(cat ~/keys/ofox.txt)" # 파일 끝에 줄바꿈이 포함된 경우
codex exec "hi"
# ERROR: unexpected status 401 Unauthorized: You didn't provide an API key.
...
헤더(header) 내부에 줄바꿈(newline)이 포함되어 생성되었고, 이로 인해 헤더가 유실되었습니다. 서버는 실제로 자격 증명(credential)을 전혀 받지 못했으므로 서버의 메시지는 정확합니다. 다만 사용자 입장에서는 마치 아무것도 설정하지 않은 것처럼 들릴 뿐입니다.
여기서 원인이 아닌 것이 무엇인지 알아두는 것도 가치가 있습니다. 왜냐하면 가장 의심스럽지만 무죄인 항목이 있기 때문입니다. 바로 끝에 붙은 공백(trailing space)입니다. export OFOX_API_KEY="$REAL "로 테스트했을 때 요청은 성공했습니다. Codex가 이를 대신 정리(cleaning up)해주지 않는다는 점에 유의하세요. api_key() 함수 내의 trim()은 공백 제거가 아닌 비어 있는지 확인(emptiness check)하는 용도이며, 반환되는 값은 끝에 공백이 포함된 원본(raw) 값입니다. 후속 단계(downstream)의 무언가가 이를 허용하는 것입니다. 반면, 줄바꿈(newline)은 헤더를 완전히 깨뜨립니다. 어떤 경우든, 길을 잃은 공백을 찾느라 시간을 허비하지 마세요.
반대로, 따옴표로 감싸진 값(quoted value)은 다른 문구와 함께 실패합니다:
export OFOX_API_KEY='"sk-..."' # 값에 리터럴 따옴표 문자가 포함됨
# ERROR: unexpected status 401 Unauthorized: Invalid or expired API key
이런 경우는 따옴표를 유지하는 단순한 export $(cat .env | xargs) 방식으로 .env 파일을 로드할 때 흔히 발생합니다.
내보내기(export) 시점에 문제가 되는 바이트를 제거하고 길이를 확인하세요:
export OFOX_API_KEY="$(tr -d '\n\r"' < ~/keys/ofox.txt)"
printf '%s' "$OFOX_API_KEY" | wc -c # 바이트 수가 키 길이와 일치하는지 확인
원인 5: 프로바이더 블록(Provider Block)에 env_key가 완전히 누락됨
9가지 실패 사례 중 가장 조용히 발생하는 오류입니다. 위의 설정에서 한 줄을 삭제해 보겠습니다:
[model_providers.ofox]
name = "Ofox"
base_url = "https://api.ofox.ai/v1"
...
Codex는 불평하지 않습니다. 대신 auth.json에 있는 자격 증명으로 폴백(fallback)하며, 대부분의 사용자에게는 이것이 OpenAI 키이므로 이를 게이트웨이(gateway)로 전송합니다. 게이트웨이는 이를 거부합니다:
ERROR: unexpected status 401 Unauthorized: Invalid or expired API key,
url: https://api.ofox.ai/v1/responses
환경 변수(environment variable)가 올바르게 설정되어 있습니다. 귀하의 키는 유효합니다. 에러 메시지는 다른 키가 전송되었기 때문에 키가 유효하지 않다고 말하고 있습니다. 동일한 셸(shell)에서 두 가지 방식 모두 테스트했습니다: env_key가 존재할 때는 요청이 정상적인 완료(completion)를 반환하지만, 해당 라인을 삭제하면 401 에러가 발생합니다.
반대의 경우도 알아둘 가치가 있으며, 이는 좋은 소식입니다: env_key가 존재할 때, 그것이 auth.json보다 우선순위를 갖습니다. auth.json에는 의도적으로 가짜 키를 저장하고 환경 변수에는 유효한 키를 저장하여 테스트했을 때, 요청은 성공했습니다. 커스텀 프로바이더(custom provider)를 설정하기 전에 로그아웃할 필요는 없습니다.
원인 6: ChatGPT 로그인 경로 만료
API 키 대신 ChatGPT 구독을 통해 로그인한 경우, 실패 양상이 완전히 다릅니다:
ERROR: Your access token could not be refreshed. Please log out and sign in again.
주변 로그 라인에 나타난 엔드포인트(endpoint)를 주목하세요: api.openai.com이 아니라 wss://chatgpt.com/backend-api/codex/responses입니다. 두 로그인 모드는 서로 다른 백엔드(backend)와 통신하며, 이는 귀하가 실제로 어떤 모드를 사용 중인지 빠르게 파악할 수 있는 방법입니다.
Codex 0.146.0 버전은 여기서 다섯 가지 변체(variant) 중 하나를 출력하며, 이들은 서로 대체될 수 없습니다. rust-v0.146.0 태그의 login/src/auth/manager.rs 파일 내용에 따르면:
| 변체 (Variant) | 실제 의미 |
|---|---|
...because your refresh token has expired | 실제 만료됨. 다시 로그인하십시오. |
| ... |
"이미 사용됨(already used)" 변체는 사람들을 놀라게 하는 부분입니다. 리프레시 토큰(Refresh tokens)은 일회용이므로, auth.json을 Docker 이미지에 포함시키거나 ~/.codex를 두 대의 노트북 간에 동기화하면, 먼저 리프레시를 수행하는 기기에서는 작동하고 다른 기기에서는 인증이 깨지는 설정이 됩니다. 동일한 소스 파일에서 TOKEN_REFRESH_INTERVAL을 8일로 설정하므로, 일주일 이상 유휴 상태였던 기기는 다음 실행 시 선제적 리프레시(proactive refresh)를 시도하게 되며, 보통 이때 충돌이 발생합니다.
해결 방법은 하나이며, 매우 단순합니다:
codex logout
codex login # 브라우저 흐름 (browser flow)
# 또는, 자동화된 작업의 경우:
...
무인 환경 (unattended environments)의 경우, API 키 경로를 권장합니다. API 키는 동기화가 어긋날 수 있는 리프레시 세맨틱 (refresh semantics)이 없습니다.
원인 7: 모든 것이 괜찮다고 말하는 codex login status
이 명령은 두 가지 다른 방식으로 거짓 정보를 제공하며, 두 가지 경우 모두 재현되었습니다.
만료된 ChatGPT 토큰이 포함된 수동 제작 auth.json을 사용할 경우, 모든 요청은 위에서 언급한 리프레시 오류와 함께 실패하지만, 다음과 같이 표시됩니다:
codex login status
# Logged in using ChatGPT
또한 커스텀 프로바이더 (custom provider) 환경에서는 status가 auth.json에 저장된 키를 보고하지만, 정작 요청에 사용되는 키는 전혀 다를 수 있습니다:
codex login status
# Logged in using an API key - sk-proj-***n-999
두 출력 모두 파일의 내용을 설명할 뿐입니다. 네트워크 체크 (network check)를 수행하지는 않습니다. 이 명령들은 "자격 증명이 저장되어 있는가"에 대한 답으로 사용해야 하며, "내 인증이 작동하는가"에 대한 답으로 사용해서는 안 됩니다. 후자를 확인하려면 30초 진단 단계에서 사용했던 curl을 실행하거나, 단순히 codex exec "hi"를 실행한 뒤 마지막 줄을 읽으십시오.
이 상황에서는 codex doctor가 더 유용합니다. codex doctor의 Configuration 섹션은 어떤 config.toml이 로드되었는지와 파싱 (parsing) 여부, 인증 저장 모드 (auth storage mode), 그리고 어떤 인증 환경 변수 (auth environment variables)를 인식할 수 있는지를 보여줍니다. Connectivity 섹션은 활성 프로바이더 (active provider), 와이어 API (wire API), WebSocket 전송 (WebSocket transport) 적용 여부, 그리고 엔드포인트 (endpoint) 도달 가능 여부를 보고합니다. 여전히 자격 증명을 검증하지는 않지만, CODEX_HOME이 설정되어 있을 때 놀라울 정도로 자주 발생하는 근본 원인인 "Codex가 당신이 편집 중인 파일이 아닌 다른 설정 파일을 읽고 있는 상황"을 1초 내에 알려줍니다.
원인 8: auth.json 파일 손상 (401 오류가 아님)
드문 경우지만, 인증 문제와 전혀 닮지 않은 오류를 발생시키기 때문에 시간을 허비하게 만드는 주범입니다. 파일이 잘렸거나 수동으로 편집된 auth.json의 경우 다음과 같은 오류가 발생합니다:
codex exec "hi"
# EOF while parsing a value at line 2 column 0
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기