에이전트 플랫폼의 'Fail Open': ID 공급자가 없다는 것은 접근 불가를 의미해야 한다
요약
AWS Loom 플랫폼의 보안 취약점 세 가지를 분석하며, 특히 Identity Provider 미구성 시 모든 클라이언트에게 관리자 권한이 부여되는 'Fail Open' 문제를 지적합니다. 이는 설정 부재 자체가 심각한 보안 취약점이 될 수 있음을 보여줍니다.
핵심 포인트
- ID 공급자가 없을 때 기본값이 개방(Fail-open) 상태인 것이 위험함.
- 설정의 누락이나 미구성은 인증 실패를 의미해서는 안 됨.
- Loom은 '실패 시 개방' 패턴을 가지므로, 이 패턴들을 코드 레벨에서 점검해야 함.
AWS Loom을 아무런 Identity Provider가 구성되지 않은 상태로 출시했을 때, 모든 네트워크 클라이언트가 관리자 권한을 갖게 되었다. 이 버그는 특정하다. 패턴은 아마도 당신의 코드에도 있을 것이다.
다음은 여러분이 아마 배포했을 법한 설정 상태이다: 인증(auth) 통합이 선택 사항이며, 배포 가이드에서는 나중에 완료할 수 있는 단계로 목록에 올리고 있고, 서비스는 그것 없이도 아무 문제 없이 시작된다. 아무것도 충돌하지 않는다. 경고하는 것도 없다. 헬스 체크는 녹색을 반환하고, 데모는 작동하며, 누가 들어올지 결정해야 할 하나의 제어 장치가 조용히 모두가 들어와도 된다고 결정했다.
이것은 가정이 아니다. 2026년 10월 2일, AWS는 오픈 소스 AI 에이전트 오케스트레이션 플랫폼인 Loom의 세 가지 문제를 다루는 Security Bulletin 2026-124를 발표했다. 첫 번째 문제인 CVE-2026-103956는 주목할 만하다. 1.6.1 버전 이전에서는 Identity Provider가 구성되지 않은 배포만으로도 모든 네트워크 클라이언트가 애플리케이션 API에 대한 요청을 통해 에이전트 제어 평면(agent control plane)에 대한 완전한 관리자 권한을 얻을 수 있었다. 완전한 관리자 권한이란 도구 서버 등록, 저장된 통합 자격 증명 읽기, 그리고 관리되는 에이전트 역할에 첨부된 IAM 역할 정책 재작성을 의미했다. 토큰도, 비밀번호도, 익스플로잇 체인(exploit chain)도 필요 없었다. 설정의 부재 자체가 취약점이었다.
나머지 두 가지 문제는 그림을 완성합니다. CVE-2026-103957은 인증된 사용자가 OAuth2 discovery URL을 공격자 엔드포인트로 지정하여 클라이언트 시크릿이나 다른 사용자의 액세스 토큰을 받게 합니다. CVE-2026-103958은 인증된 사용자가 컨테이너 자격 증명 엔드포인트를 포함한 임의의 내부 주소로 툴 서버 및 원격 에이전트 연결 요청을 직접 보내 응답을 읽게 합니다. AWS는 Loom 1.7.0에서 이 문제를 수정했으며, 운영자들에게 OAuth2 클라이언트 시크릿을 로테이션하고, 활성 액세스 토큰을 취소하며, 컨테이너 역할 자격 증명이 도달했을 수 있는 경우 CloudTrail을 검토할 것을 권고했습니다.
이 공지사항에 대한 대부분의 분석 글은 '1.7.0으로 업그레이드'에서 멈추지만, Loom 운영자들에게는 이것이 올바른 첫 번째 조치입니다. 모든 설명자가 건너뛰는 부분은 그 아래에 깔린 패턴입니다. Loom은 하나의 코드베이스입니다. 실패 시 개방(Fail-open) 인증, 허용 목록 없이 가져온 사용자 제공 URL, 그리고 프로덕션까지 살아남은 로컬 개발 탈출구(escape hatch)는 모두 패턴이며, 이 패턴들은 Loom이라는 것을 한 번도 들어본 적 없는 코드베이스로 복제됩니다. 본 글에서는 이 세 가지 패턴을 정확히 명명하고, 각각의 패턴을 실행 가능한 코드로 축소하여 보여주며, 오후 안에 막을 수 있는 점검 항목들을 제공합니다.
실패 원인을 정확하게 명명하기: 인증 누락은 기본값이 아니다
CVE 레코드는 CVE-2026-103956에 CWE-306(임계 기능의 인증 누락)과 CWE-1188(자원의 안전하지 않은 기본 초기화)을 태그로 지정했습니다. 두 번째 태그가 정직한 설명입니다. 이 버그는 깨진 로그인 페이지나 우회 가능한 토큰 검사가 아니었습니다. 초기화 기본값이 잘못된 것이었습니다. 즉, ID 공급자가 없을 때 시스템이 가장 덜 허용적인 상태(least permissive one)가 아닌 가장 많이 허용하는 상태(most permissive state)로 초기화되었습니다.
이러한 기본값이 존재하는 이유를 생각해 보세요. 왜냐하면 여러분의 코드베이스도 같은 압력을 받고 있기 때문입니다. 인증(Authentication)은 개발 과정에서 마찰을 일으킵니다. 저장소(repo)를 복제하는 신입 엔지니어는 제어 평면(control plane)이 시작되기를 원하고, 데모는 영업 통화에서 실행되기를 원하며, 통합 테스트는 토큰 처리 과정을 건너뛰기를 원합니다. 그래서 누군가 인증을 조건부로 만듭니다. 이 조건부는 노트북에서는 합리적입니다. 문제는 코드의 어느 부분도 노트북과 배포 환경을 구별하지 못하고, 이 조건부가 아티팩트와 함께 이동한다는 것입니다. AWS 자체의 임시방편은 탈출구(escape hatch)를 직접적으로 명명합니다: 어떤 배포 환경에서든 LOOM_ALLOW_UNAUTHENTICATED_LOCAL_DEV 환경 변수가 설정되어 있지 않은지 확인하는 것입니다. 오직 로컬 편의성만을 목적으로 했던 플래그가 보안 게시판에 한 줄을 차지할 만큼 중요한 프로덕션 설정이 된 것입니다.
구성되지 않았을 때 스스로 꺼지는 제어는, 구성 옵션을 가진 제어가 아닙니다. 그것은 하나의 바이너리를 공유하는 두 개의 다른 시스템이며, 여러분의 배포가 어느 쪽으로 실행될지 선택할 수 없습니다.
이를 방지하는 규칙은 코드 리뷰 체크리스트에 넣기에는 충분히 짧습니다: 설정의 부재는 권한이 아니라 중단 조건(stop condition)이어야 합니다. 만약 ID 공급자(identity provider)가 구성되지 않았다면, 서비스는 시작을 거부하거나, 루프백(loopback) 이상의 아무것도 제공하지 않으며, 그 제한 사항은 시작 로그에 인쇄됩니다. 어떤 플래그 조합에서도 인증되지 않은 네트워크 호출자에게 관리 엔드포인트(administrative endpoints)를 절대 제공해서는 안 됩니다.
버그가 발생하는 것을 지켜보고, 그리고 그것을 닫으세요
예시를 하나 들어드리겠습니다. 이것은 취약한 형태와 수정된 형태를 하나의 stdlib 전용 Python 파일에 나란히 보여줍니다. 이는 Loom의 재구현이 아니라 축소판(reduction)이지만, 모든 줄은 실제 사고에서 내린 결정에 해당합니다.
# fail_closed.py -- stdlib only: the fail-open bug, and the fixed version
import ipaddress
from urllib.parse import urlparse
...
파일 하단에서 시나리오를 실행하면 다음과 같은 결과가 나옵니다:
vulnerable, no token -> {'principal': 'anonymous', 'scopes': ['admin']}
vulnerable, can register a tool server: True
fixed, same config -> startup refused: refusing to start: no identity provider configured
...
이 예시에서 주목할 몇 가지 사항이 있습니다. 첫째, 취약한 함수는 악의적으로 보이지 않습니다. 마치 누군가 2주 차에 추가한 편의 기능 분기처럼 보여서 검토를 통과하기 쉽습니다. 둘째, 수정된 부분이 authorize 내에서 더 나은 분기가 아닙니다. 이 수정은 결정을 시작 단계로 옮깁니다. 즉, 잘못 구성된 배포는 공격자 앞에서 요청 시간에 조용히 실패하는 대신, 운영자 앞에서 부팅 시점에 크게 실패하게 만듭니다. 셋째, 유효한 리더 토큰에 대해 수정된 authorize가 무엇을 반환하는지 주목하십시오. 읽기 범위(read scope)만입니다. 취약한 버전의 익명 주체는 관리자 권한(admin)을 받았는데, 이는 실패 개방 경로(fail-open path)가 단순히 인증되지 않은 수준이 아니라 최대 권한을 가졌다는 것을 의미합니다. 자체 코드를 감사할 때 찾아야 할 조합은 바로 이것입니다: 모든 것이 작동하도록 필요했던 개발자를 위해 분기가 작성되었기 때문에 광범위한 범위에 도달하는 비인증 경로(no-auth paths).
사용자에게 받은 모든 URL을 로드된 요청으로 취급하라
두 번째와 세 번째 Loom 이슈는 하나의 메커니즘을 공유합니다. 서버가 사용자가 제공한 URL을 가져왔고, 응답 또는 연결 자체가 페이로드(payload)가 된 것입니다. 디스커버리 문서(discovery document)는 백엔드에 비밀 정보를 보낼 위치를 알려주었습니다. 도구 서버 연결 요청은 169.254.169.254의 컨테이너 자격 증명 엔드포인트에 도달하여 반환된 내용을 읽어왔습니다. 이것은 에이전트 복장을 한 서버 측 요청 위조(server-side request forgery)이며, 에이전트 플랫폼들은 제품 자체가 사용자가 지정한 도구 서버 및 원격 에이전트에 연결하는 것이기 때문에 끊임없이 이를 유도합니다.
일반적인 에이전트 플랫폼의 파이프라인은 다음과 같습니다:
user configures a URL --> server fetches it --> response is parsed as trusted config
| | |
discovery doc, no allowlist, secrets sent to the
...
각 단계는 개입할 수 있는 지점이며, 각 단계가 다르게 실패하기 때문에 최소 두 곳 이상에서 검증해야 합니다. 구성(configuration) 시에는 URL을 허용 목록(allowlist)의 호스트와 비교하여 유효성을 검사하고 HTTPS를 요구해야 합니다. 가져오기(fetch) 시에는 호스트를 해석하고 사설(private), 루프백(loopback), 링크-로컬 주소는 거부하며, 리디렉션 후에도 재검증해야 합니다. 왜냐하면 169.254.169.254로 리디렉션하는 공개 URL은 이 속임수의 가장 오래된 버전이기 때문입니다. 응답(response) 시에는 가져온 문서를 신뢰할 수 없는 입력으로 취급해야 합니다. 발견 문서가 엔드포인트를 명시할 수는 있지만, 클라이언트 비밀 정보는 허용 목록에 있는 엔드포인트로만 전송되어야 하며, 문서 자체가 제공한 주소로는 절대 전송되어서는 안 됩니다. 위에서 설명한 check_outbound_url 함수가 앞의 두 단계를 축소하여 다루고 있습니다. 이 함수 자체만으로는 DNS 리바인딩 공격(DNS rebinding attack)을 막지 못하므로, IP 검사보다는 허용 목록이 핵심 통제 장치입니다.
참고로, 다른 사람이 시도하기 전에 자신의 배포 환경에 대한 감사 명령(audit commands)을 실행해 보세요:
# 배포된 환경에서 인증되지 않은 모드가 활성화되어 있지는 않은가?
env | grep -i "ALLOW_UNAUTHENTICATED\|AUTH_DISABLED\|DEV_MODE"
grep -rn "allow_unauthenticated\|auth.*optional\|skip_auth" --include="*.py" --include="*.ts" .
...
이 단 하나의 curl 명령어가 이 전체 내용에서 가장 저렴한 보안 테스트입니다. 이를 CI(지속적 통합)에 통합 검증(integration assertion)으로 추가하세요. 서비스가 운영 환경과 유사한 구성으로 부팅되도록 하고, 한 개의 인증되지 않은 요청을 관리 엔드포인트로 보내며, 응답이 401 또는 403 코드가 아닌 경우 빌드를 실패시키세요. 이렇게 하면 '실패 시 열림(fail-open)' 회귀가 CVE(공개 취약점)가 되는 대신 빨간색 빌드로 표시됩니다.
이 부분이 깨지는 지점 (Where This Breaks)
- 시작 거부(Startup refusal)는 접근 차단으로 이어질 수 있습니다. 만약 사용자의 ID 공급자(IdP)가 다운되었고, 제어 평면(control plane)이 이를 없이 시작하는 것을 거부한다면, IdP 장애는 플랫폼 전체의 장애로 이어집니다. 이 트레이드오프를 의식적으로 받아들이세요: 명시된 소유자, 시간 제한, 감사 기록을 갖춘 비상 절차(break-glass procedure)가 답이며, 조용한 인증되지 않은 모드는 아닙니다.
- IP 확인만으로는 충분하지 않습니다. 호스트 이름은 해결되고, 리디렉션이 이동하며, DNS 응답은 확인 시점과 가져오는 시점 사이에 변경됩니다. 허용 목록(allowlist)이 주요 제어 장치이며, 주소 확인은 공격의 느슨한 버전을 잡아냅니다.
- 허용 목록 유지보수는 실제 작업입니다. 모든 합법적인 새로운 도구 서버는 변경 요청(change request)입니다. 만약 그 마찰이 너무 크다면, 운영자들은 와일드카드(wildcard)를 요구할 것이고, 이 와일드카드는 조용히 원래의 버그를 복원합니다.
- 스코프가 지정된 라이터(Scoped writers)는 세 가지 CVE 중 두 개를 유발했습니다. CVE-2026-103957 및 CVE-2026-103958은 쓰기 스코프(write scopes)를 가진 인증된 사용자를 필요로 했습니다. 실패 시 폐쇄(Fail-closed) 인증 방식으로는 배포가 이러한 문제로부터 보호되지 못했을 것입니다. 아웃바운드 검증과 최소 권한 원칙에 따른 스코핑은 별개의 제어 장치이며, 둘 다 개별적으로 필요합니다.
- 포크(Forks)는 수정 사항을 상속받지 않습니다. AWS는 포크되거나 파생된 Loom 코드를 가진 모든 사용자에게 직접 패치할 것을 권고했습니다. 이는 여러분이 지금까지 판매한 인증 미들웨어(auth middleware)의 모든 내부 사본에도 적용됩니다.
구축해야 할 경우, 건너뛰어도 되는 경우 (Build It If, Skip It If)
에이전트의 자격 증명, 도구 등록 또는 역할 정책을 중개하는 제어 평면(control plane)을 운영한다면, 특히 단일 개발자 노트북을 넘어 접근 가능한 경우라면 전체 버전을 구축해야 합니다. 시작 거부, CI에서의 인증되지 않은 요청 테스트, 아웃바운드 허용 목록 지정, 그리고 문서화된 비상 절차는 완전한 세트이며, 이 중 어느 것도 플랫폼 프로젝트가 아닙니다.
에이전트 도구가 네트워크 리스너나 저장된 자격 증명이 없는 단일 프로세스 로컬 스크립트에 불과하다면 정교한 버전은 건너뛰어도 됩니다. 그 경우의 위험은 다르며, 아무것도 가져오지 않는 프로세스의 URL을 허용 목록 지정하는 데 보낸 주말은 낭비된 주말입니다.
최소 실행 가능 버전(minimal viable version)은 오후 안에 완성할 수 있습니다. 루프백 외부의 ID 공급자 없이는 부팅을 거부하는 스타트업 단언문 하나, 인증되지 않은 관리자 요청이 401 에러를 반환한다고 주장하는 CI 테스트 하나, 그리고 배포된 환경에서 UNAUTHENTICATED라는 이름이 포함된 플래그를 검색하는 grep 명령어 하나면 충분합니다. 이 세 가지 작은 결과물 각각은 Loom의 이야기를 단순한 공지사항에서 실패한 배포로 바꿀 수 있었을 것입니다.
curl부터 시작해 보세요. 오늘 운영하고 있는 제어 평면(control plane)에 대해 실행해 보고, 상태 코드를 소리 내어 읽으세요. 그리고 설정 파일이 약속하는 것보다는 그 숫자가 말하는 것에 따라 배포를 수정하세요. 토큰 자체가 전혀 없는 요청이 도착했을 때 당신의 제어 평면은 뭐라고 답할까요?
참고 자료 (Resources)
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기