
당신의 LLM API 키는 실제로 어디에 저장되어 있습니까?
요약
LLM 애플리케이션 개발 시 API 키가 저장되는 위치에 따른 보안 위협을 분석합니다. 애플리케이션 프로세스 내에 키를 두는 방식과 네트워크 프록시를 사용하는 방식의 차이를 통해 보안 사고 발생 시 폭발 반경을 줄이는 아키텍처 설계의 중요성을 설명합니다.
핵심 포인트
- 애플리케이션 프로세스 내 API 키 저장 시 해킹된 의존성이 키에 직접 접근 가능
- 네트워크 프록시 아키텍처를 통해 API 키를 분리하여 보안 위협 최소화 가능
- 완벽한 방어보다 사고 발생 시 피해 범위(blast radius)를 줄이는 설계가 중요
만약 오늘 누군가가 당신의 프로젝트 의존성(dependencies) 중 하나를 해킹한다면, 그들이 당신의 OpenAI, Anthropic 또는 Gemini API 키를 훔칠 수 있을까요?
그 답은 당신이 어떤 LLM 제공업체를 사용하는지 또는 코드베이스가 얼마나 안전한지에 달려 있지 않습니다. 그것은 대부분 많은 팀이 전혀 생각하지 못하는 하나의 아키텍처 결정, 즉 _애플리케이션이 실행되는 동안 제공업체의 API 키가 실제로 어디에 존재하는가_에 달려 있습니다.
만약 그 키가 애플리케이션 자체의 프로세스(process) 내부에 존재한다면, 해당 프로세스에서 실행되는 모든 의존성은 동일한 환경을 공유합니다. 해킹된 패키지는 당신의 인프라에 침입할 필요가 없습니다. 그저 당신의 애플리케이션과 동일한 권한으로 실행되어 당신의 코드가 접근할 수 있는 것과 동일한 자격 증명(credentials)에 접근할 수 있을 뿐입니다.
대신 제공업체 키가 별도의 네트워크 프록시(network proxy)에 존재한다면, 애플리케이션은 제공업체 자격 증명을 전혀 보유하지 않게 됩니다. 설령 의존성이 해킹되더라도, 공격자는 애플리케이션 프로세스 내에 존재하는 제한된 자격 증명에만 접근할 수 있습니다. 이것이 위험을 완전히 제거하지는 않지만, 문제가 발생했을 때 폭발 반경(blast radius)을 줄일 수 있습니다.
이 글에서는 두 가지 지배적인 LLM 게이트웨이(gateway) 아키텍처를 살펴보고, 각 설계에서 API 키가 정확히 어디에 존재하는지 검토하며, 실제 차이를 보여주는 재현 가능한 데모를 살펴보고, 왜 모든 가능한 공격을 제거하려고 노력하는 것보다 폭발 반경을 줄이는 것이 종종 더 중요한지에 대해 논의할 것입니다.
LLM 게이트웨이 아키텍처: 인프로세스(In-Process) vs 네트워크 프록시(Network Proxy)
모든 LLM 애플리케이션은 동일한 근본적인 작업을 수행합니다: 모델 제공업체에 요청을 보내고 API 키를 사용하여 해당 요청을 인증하는 것입니다.
중요한 부분은 애플리케이션이 API 키를 사용하는지 여부가 아닙니다. 요청이 생성되는 동안 그 키가 어디에 존재하는가입니다.
오늘날 대부분의 AI 애플리케이션은 두 가지 아키텍처 패턴 중 하나를 따릅니다.
아키텍처 1: 애플리케이션이 제공업체 키를 보유함
제공업체 키(provider key)는 앱 프로세스 내부에 존재하며, 임포트(import) 시 실행되는 모든 종속성(dependency)이 접근할 수 있는 범위에 있습니다.
이것은 대부분의 개발자에게 이미 익숙한 아키텍처입니다.
애플리케이션은 환경 변수(environment variable)로부터 제공업체 API 키를 로드하고, SDK 또는 게이트웨이 라이브러리(gateway library)를 초기화한 다음, OpenAI, Anthropic, Gemini 또는 다른 제공업체로 직접 요청을 보냅니다.
단순화된 버전은 흔히 다음과 같습니다:
api_key = os.environ["PROVIDER_API_KEY"]
client = OpenAI(api_key=api_key)
...
이 방식은 이해하기 쉽고 구현이 빠르며, 많은 프로젝트에서 매우 합리적입니다. 애플리케이션이 제공업체와 직접 통신하는 구성 요소이기 때문에 애플리케이션이 자격 증명(credential)을 소유합니다.
중요한 세부 사항은 제공업체 키가 이제 애플리케이션의 프로세스 내부에 존재한다는 점입니다. 해당 프로세스에서 실행되는 모든 패키지, 프레임워크, 플러그인 및 종속성은 동일한 권한 아래에서 실행됩니다. 만약 이러한 종속성 중 하나가 침해(compromised)된다면, 제공업체 키는 악성 코드와 동일한 환경에 존재하게 됩니다.
아키텍처 측면에서의 질문은 이것입니다:
애플리케이션 프로세스 내부의 무언가가 침해된다면, 그곳에서 어떤 비밀 정보(secrets)를 이용할 수 있는가?
아키텍처 2: 애플리케이션이 네트워크 프록시와 통신함
제공업체 키는 별도의 프록시 프로세스에 존재합니다. 앱 프로세스는 범위가 제한되고 교체 가능한(scoped, rotatable) 게이트웨이 토큰만을 보유합니다.
두 번째 패턴은 인증(authentication)을 애플리케이션 자체로부터 분리합니다.
모델 제공자에게 직접 요청을 보내는 대신, 애플리케이션은 게이트웨이(gateway) 또는 프록시(proxy)로 요청을 보냅니다. 프록시는 제공자의 API 키를 보유하며, 애플리케이션을 대신하여 업스트림(upstream) 요청을 수행합니다.
애플리케이션의 관점에서 보면 흐름은 거의 동일해 보입니다:
Application
│
▼
...
차이점은 애플리케이션이 무엇을 가지고 있지 않느냐 하는 것입니다.
제공자의 자격 증명(credential)을 저장하는 대신, 일반적으로 프록시를 통해 요청을 승인하는 범위가 제한된 게이트웨이 토큰(scoped gateway token)을 보유합니다. 프록시는 해당 토큰을 검증하고, 라우팅(routing) 또는 정책 결정을 적용한 다음, 요청을 업스트림으로 전달하기 전 자신의 프로세스 내부에서만 제공자의 API 키를 주입(inject)합니다.
이는 침해(compromise) 발생 시의 결과(consequences)를 변화시킵니다. 만약 악성 코드가 애플리케이션 프로세스 내부에서 실행된다면, 해당 코드는 애플리케이션이 보유한 자격 증명에 여전히 접근할 수 있습니다. 차이점은 제공자의 API 키가 더 이상 그중 하나가 아니라는 점입니다.
그렇다고 해서 애플리케이션이 무적(invulnerable)이 된다는 뜻은 아닙니다. 탈취된 게이트웨이 토큰 역시 보안 사고입니다. 하지만 제공자의 API 키와 달리, 게이트웨이 토큰은 범위를 좁게 설정(narrowly scoped)할 수 있고, 중앙에서 취소(centrally revoked)할 수 있으며, 애플리케이션을 재배포하지 않고도 교체(rotated)할 수 있고, 특정 작업으로 제한할 수 있습니다.
이러한 차이를 확인하는 가장 쉬운 방법은 동일하게 침해된 종속성(dependency)이 두 아키텍처 모두에서 실행되는 것을 관찰하는 것입니다. 그것이 우리가 다음에 할 일입니다.
공급망 공격(Supply Chain Attacks)이 LLM API 키를 노출하는 방식
더 중요한 질문은 애플리케이션이 실행되기 시작한 후에 어떤 일이 벌어지느냐 하는 것입니다.
프로세스가 실행되기 시작하면, 해당 프로세스가 필요로 하는 자격 증명은 그 프로세스에서 사용 가능한 상태가 됩니다. 만약 애플리케이션이 API 키를 읽을 수 있다면, 동일한 권한으로 실행되는 모든 코드도 잠재적으로 이를 읽을 수 있습니다.
이것이 바로 공급망 공격(supply chain attacks)이 매우 효과적이게 된 정확한 이유입니다.
공격자는 더 이상 당신의 애플리케이션에서 취약점을 찾을 필요가 없습니다. 대신, 그들은 당신의 의존성 트리 (dependency tree) 어딘가에 있는 패키지를 침해하고, 당신의 애플리케이션이 자신들을 대신하여 페이로드 (payload)를 실행하도록 만듭니다. 많은 경우, 해당 코드는 당신의 비즈니스 로직 (business logic)이 시작되기 훨씬 전인 설치 (installation) 또는 임포트 (import) 단계에서 실행됩니다.
import os
_INTERESTING = ("API_KEY", "SECRET", "TOKEN", "PASSWORD", "PRIVATE_KEY")
...
이 예시는 의도적으로 무해하게 작성되었습니다. 네트워크 요청을 보내거나, 파일을 쓰거나, 무언가를 유출하려고 시도하지 않습니다. 단순히 현재 프로세스 (process)에서 자격 증명 (credentials)을 스캔하고 발견된 것을 출력할 뿐입니다.
중요한 부분은 코드가 무엇을 하느냐가 아닙니다. 바로 코드가 어디에서 실행되는가입니다.
이 패키지가 당신의 의존성 그래프 (dependency graph)에서 여러 단계 깊숙이 자리 잡고 있다고 상상해 보십시오. 당신은 이를 직접 임포트하지 않으며, 그 소스 코드를 읽어본 적도 없습니다. 어느 날, 침해된 릴리스 (release)가 당신의 CI 파이프라인 (CI pipeline)에 도달하여 자동으로 설치되고, 정상적인 시작 시퀀스 (startup sequence)의 일부로 실행됩니다.
만약 당신의 애플리케이션이 제공자 (provider) API 키를 자체 환경 변수에 저장하고 있다면, 해당 의존성은 동일한 프로세스 내에 존재하기 때문에 그 키를 읽을 수 있습니다.
반대로, 제공자 자격 증명이 별도의 프록시 프로세스 (proxy process) 내에 존재하고 당신의 애플리케이션은 범위가 제한된 게이트웨이 토큰 (scoped gateway token)만을 보유하고 있다면, 정확히 동일한 의존성이 성공적으로 실행되더라도 발견할 제공자 키가 그곳에 존재하지 않게 됩니다.
이것이 바로 우리가 탐구하고자 하는 아키텍처적 차이 (architectural distinction)입니다.
또한 이것이 2026년 3월 LiteLLM 공급망 사고가 AI 생태계 전반에서 그토록 많은 관심을 끌었던 이유이기도 합니다. 이 사고가 중요했던 이유는 LiteLLM이 유독 취약했기 때문이 아닙니다. AI 인프라가 공격 대상으로서 얼마나 가치 있게 변했는지, 그리고 침해된 의존성이 실행 중인 애플리케이션 내부의 고가치 자격 증명에 얼마나 빠르게 도달할 수 있는지를 보여주었기 때문에 중요했습니다.
그 실제 사례를 살펴보기 전에, 그 차이점을 확인해 볼 가치가 있습니다.
다음의 재현 가능한 데모는 **동일한 침해된 종속성 (compromised dependency)**을 두 아키텍처 모두에 실행합니다. 종속성 자체에는 아무런 변화가 없습니다. 유일한 변수는 제공자 API 키가 어디에 위치하느냐입니다.
데모: 인프로세스 (In-Process) vs 프록시 LLM 게이트웨이 보안
이론은 유용하지만, 아키텍처적 위험이 발생하는 것을 직접 볼 수 있을 때 훨씬 더 이해하기 쉽습니다.
이 비교를 구체화하기 위해, 저는 두 아키텍처 모두에서 정확히 동일한 시나리오를 재현하는, 종속성이 없는 작은 데모(이 기사를 위해 Jonathan Hutchins가 제공)를 준비했습니다.
설정은 의도적으로 단순합니다:
- 애플리케이션 자체는 전혀 변하지 않습니다.
- 동일한 종속성이 두 예시 모두에서 임포트(import)됩니다.
- 유일하게 변하는 것은 제공자 API 키가 어디에 위치하느냐입니다.
이 데모는 Python의 표준 라이브러리만을 사용합니다. 외부 서비스, 제공자 계정, OpenAI 또는 Anthropic으로의 네트워크 호출, 그리고 실제 자격 증명(credentials)이 없습니다. 모든 것이 로컬에서 실행되므로 부작용을 걱정할 필요 없이 쉽게 재현할 수 있습니다.
"악성" 종속성 또한 매우 단순합니다. 이 종속성이 임포트될 때, 현재 프로세스를 스캔하여 자격 증명처럼 보이는 모든 것을 찾아냅니다.
import os
_INTERESTING = (
...
시나리오 A: 제공자 키가 애플리케이션 내부에 있는 경우
첫 번째 버전은 오늘날 많은 AI 애플리케이션이 사용하는 아키텍처를 따릅니다.
애플리케이션은 자신의 환경 변수에서 제공자 키를 읽습니다:
api_key = os.environ["PROVIDER_API_KEY"]
종속성이 임포트될 때, 이는 정확히 동일한 프로세스 내부에서 실행됩니다.
그 결과, 제공자 자격 증명을 즉시 발견합니다:
[malicious_dep@import]
EXFILTRATED PROVIDER_API_KEY = sk-provi...3xyz
여기서 특별히 영리한 기술이 사용된 것은 아닙니다.
종속성은 인증을 우회하거나, 메모리 오염 (memory corruption)을 악용하거나, 다른 서비스로 침입하지 않았습니다. 단지 자신이 실행되고 있는 프로세스 내에 이미 존재하는 데이터에 접근했을 뿐입니다.
공격자의 관점에서 보면, 그것만으로도 충분합니다.
시나리오 B: 제공자 키가 네트워크 프록시(network proxy) 내부에 존재하는 경우
이제 두 번째 아키텍처에 대해서도 정확히 동일한 종속성 공격을 실행해 보겠습니다.
이번에는 애플리케이션이 제공자 자격 증명(provider credential)을 전혀 받지 않습니다.
대신, 애플리케이션은 게이트웨이 토큰(gateway token)만을 보유합니다:
token = os.environ["GATEWAY_TOKEN"]
제공자 API 키는 프록시 프로세스 내부에만 존재하며, 이 프록시는 요청을 상위(upstream)로 전달하기 전에 게이트웨이 토큰을 검증합니다.
침해된 종속성이 실행될 때, 출력 결과는 매우 다르게 나타납니다:
[malicious_dep@import]
EXFILTRATED GATEWAY_TOKEN = gw-scope...-789
무엇이 나타나지 않았는지 주목하십시오.
제공자 API 키는 나타나지 않았습니다. 왜냐하면 애초에 애플리케이션의 프로세스 내부에 존재한 적이 없기 때문입니다.
애플리케이션은 여전히 성공적인 모델 응답을 받지만, LLM 제공자에 대한 인증은 애플리케이션 내부가 아닌 프록시 내부에서 이루어집니다.
이 시점에서 프록시가 문제를 "해결"했다고 결론짓고 싶은 유혹이 생길 수 있습니다.
하지만 그렇지 않습니다.
종속성은 여전히 자격 증명을 훔쳤습니다. 게이트웨이 토큰은 실제 토큰이며, 공격자가 이를 손에 넣는다면 여전히 프록시를 통해 요청을 보낼 수 있습니다. 그렇지 않은 척하는 것은 이 비교의 유용성을 떨어뜨릴 뿐입니다.
질문은 무언가가 유출되었느냐가 아닙니다. 무엇이 유출되었는지, 그 자격 증명으로 무엇을 할 수 있는지, 그리고 노출로부터 얼마나 빨리 복구할 수 있는지가 핵심입니다.
바로 이 지점에서 두 아키텍처는 훨씬 더 의미 있는 방식으로 갈라지기 시작합니다.
다음 데모 파트에서는 게이트웨이 토큰이 이미 도난당한 후에 정확히 어떤 일이 발생하는지, 그리고 왜 복구 과정이 침해된 제공자 API 키를 교체(rotating)하는 것과 매우 다른지를 보여줍니다.
프록시가 보호하는 것과 보호하지 못하는 것
이전 예시에서 침해된 종속성은 여전히 자격 증명을 훔쳤습니다.
단지 그것이 제공자 API 키가 아니었을 뿐입니다.
대신, 프록시(proxy)를 통해 요청을 허용하는 범위가 제한된 게이트웨이 토큰(scoped gateway token)을 획득했습니다. 이것 역시 여전히 보안 사고이며, 이 점을 미리 인정하는 것이 중요합니다. 보안 논의는 트레이드오프(trade-offs)를 설명할 때 더욱 유용해집니다.
흥미로운 부분은 침해 이후에 발생합니다. 데모의 rotate_demo.sh 스크립트는 복구 과정을 단계별로 보여줍니다.
초기에는 정당한 애플리케이션과 공격자 모두 동일한 게이트웨이 토큰을 보유하고 있으므로, 양쪽 모두 이를 성공적으로 사용할 수 있습니다. 이러한 일시적인 중첩은 운영자가 침해된 자격 증명(credential)을 취소할 때까지 발생하는 예상된 상황입니다.
그 후 운영자는 프록시의 토큰 저장소(token store)를 업데이트합니다.
기존 토큰은 취소됩니다.
새로운 범위 제한 토큰(scoped token)이 발급됩니다.
애플리케이션 코드에는 아무런 변화가 없습니다.
아무것도 재배포(redeployed)되지 않습니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기