본문으로 건너뛰기

© 2026 Molayo

Dev.to헤드라인2026. 06. 24. 03:32

당신의 AI 에이전트가 자신의 API 키를 로그에 남겼습니다. 저는 40줄짜리 Redactor(비식별화 도구)를 작성했습니다.

요약

AI 에이전트의 도구 호출 과정에서 API 키와 같은 민감한 정보가 로그에 평문으로 남는 보안 문제를 다룹니다. 이를 방지하기 위해 정규 표현식과 샤논 엔트로피를 활용한 40줄 규모의 Python 비식별화(Redactor) 도구 구현 방법을 소개합니다.

핵심 포인트

  • 에이전트의 도구 호출 인자가 로그 파이프라인을 통해 외부로 유출될 수 있음
  • 모델의 직접적인 유출이 아닌, 시스템의 로깅 및 관측성(Observability) 인프라 문제임
  • 정규 표현식과 Shannon-entropy를 활용해 민감 정보를 탐지하고 비식별화 가능
  • CWE-532(로그 파일에 민감한 정보 삽입) 문제를 에이전트 환경에서 예방해야 함

모델이 당신의 키를 직접 말한 것이 아닙니다. 당신의 트레이싱(tracing)이 그랬습니다. 에이전트가 도구 호출(tool call)을 수행했고, 프레임워크가 인자(arguments)와 함께 해당 호출을 로그에 남겼으며, 그 인자 중 하나가 인증에 사용된 API 키였습니다. 그 한 줄은 stdout으로 전달되었고, 그다음 로그 전달자(log shipper)로, 그다음에는 당신의 관측성(observability) 데이터를 저장하는 제3자 서비스로 전달되었습니다. 평문(plaintext) 상태로 말이죠. 모델이 무언가를 유출하는 것을 본 사람은 아무도 없습니다. 모델은 유출하지 않았기 때문입니다. 배관(plumbing)이 유출했습니다.

에이전트는 도구 호출을 로그에 남기며, 여기에는 인자가 포함됩니다. 그 인자 중 하나는 당신의 API 키, Bearer 토큰, 또는 연결 문자열(connection string)에 포함된 데이터베이스 비밀번호일 수 있습니다. 당신의 로그 파이프라인은 이를 평문 상태로 벤더(vendor)에 전달합니다. 로그 경계에 있는 Redaction(비식별화) 레이어는 프로세스를 떠나기 전에 이를 포착합니다. 알려진 키 형태를 위한 정규 표현식(regex), 알려지지 않은 형태를 위한 Shannon-entropy(샤논 엔트로피) 네트워크를 사용하는 방식입니다. 아래는 표준 라이브러리(stdlib) Python으로 작성된 약 40줄 분량의 전체 코드이며, 결정론적 실행(deterministic run)과 이 코드가 놓칠 수 있는 부분들에 대한 솔직한 목록을 포함하고 있습니다.

이것은 공격이 아닙니다. 당신의 로깅 문제입니다.

대부분의 에이전트 보안 관련 글은 공격자에 대해 다룹니다. 명령을 숨긴 스크래핑된 페이지(페이지가 에이전트에게 무엇을 할지 지시했습니다). 조용히 스키마를 변경한 도구. fetch 시점의 SSRF 프로브(웹 fetch 경계에서 차단됨). 이 모든 경우에는 외부의 무언가가 안으로 들어오려고 시도합니다.

이것은 정반대의 방향입니다. 악의적인 일은 일어나지 않았습니다. 입력값은 괜찮았습니다. 에이전트는 자신의 할 일을 했습니다. 그리고 자격 증명(credential)이 옆문으로 빠져나가 당신의 텔레메트리(telemetry)로 흘러 들어갔습니다. 스택에서 가장 지루한 코드 한 줄, 즉 디버그 로그를 작성하는 코드가 도구 호출 인자를 그대로 복사했기 때문입니다. 유출은 유입(inbound)이 아닙니다. 그것은 측면(sideways)에서 발생했으며, 당신이 직접 벤더에게 그것을 건네준 것입니다.

그 부분이 제가 진지하게 받아들이기까지 시간이 좀 걸렸던 지점입니다. 자신의 대시보드에서 직접 확인할 수 있는 유출은 유출처럼 느껴집니다. 하지만 SaaS로 전달한 로그 라인에 키가 포함되어 있는 것은 아무것도 아닌 것처럼 느껴집니다. 대시보드는 초록색이고 요청은 성공했기 때문입니다. 하지만 그 키는 이제 다른 누군가의 저장소에 들어가 있으며, 인덱싱(indexed)되고, 그들의 정책에 따라 보관되며, 해당 계정에 접근 권한이 있는 누구든 읽을 수 있는 상태가 됩니다. CWE-532는 이를 부르는 이름이 있습니다: 로그 파일에 민감한 정보 삽입 (insertion of sensitive information into a log file). 이는 오래된 문제입니다. 에이전트가 등장하기 20년 전부터 존재했습니다. 에이전트는 그저 이를 일상적인 일로 만들었을 뿐입니다. 왜냐하면 에이전트는 모든 것(everything)을 로그로 남기며, 그들이 끊임없이 전달하는 것 중 하나가 바로 인증(auth) 정보이기 때문입니다.

왜 에이전트가 일반적인 앱보다 상황을 악화시키는가

일반적인 서비스는 에지(edge)에서 한 번 인증을 수행하고 그 토큰을 다시는 로그에 남기지 않습니다. 에이전트는 다릅니다. 에이전트는 도구(tools)를 호출하는 루프(loop)이며, 그 도구들 중 상당수는 자체적인 자격 증명(credentials)을 필요로 합니다: LLM API 키, 검색 키, 데이터베이스 DSN, 무언가를 클론하기 위한 GitHub 토큰, 결과를 게시하기 위한 Slack 토큰 등입니다. 모든 호출은 로그 라인이 될 후보입니다. 모든 로그 라인은 유출의 후보입니다.

제 자신의 설정에서 가져온 구체적인 사례를 들어보겠습니다. 저는 운영 환경에서 스크래퍼(scrapers)를 실행합니다: 32개의 공개된 액터(actors)에 걸쳐 2,190회의 실행이 이루어지며, 가장 바쁜 것은 962회 실행되는 Trustpilot 리뷰 스크래퍼이고, 이어서 이메일 추출기가 138회, Reddit 스크래퍼가 92회 실행됩니다. 이 실행들 중 단 하나도 빠짐없이 그 호출 정보(액터, 입력값, 호출 내용)를 로그에 기록합니다. 제 환경에서 키가 유출되었다고 주장하는 것이 아닙니다; 제 입력값에는 키가 포함되어 있지 않으며, 만약 포함되어 있다면 말씀드렸을 것입니다. 핵심은 구조적인 문제입니다. 이는 수천 개의 로그 레코드(log records)를 의미하며, 각 레코드는 인자(argument)가 우연히 비밀 정보인지 아닌지를 알지도 못하고 신경 쓰지도 않는 코드에 의해 작성됩니다. 그 인자 중 하나가 비밀 정보가 되는 날, 로거(logger)는 눈 하나 깜짝하지 않고 그것을 실어 보냅니다. 규모(scale)가 바로 "아마 괜찮을 거야"라는 생각이 나쁜 도박이 되게 만드는 요인입니다. 대규모 환경에서 "아마도"라는 말은 당신이 이미 유출해 버린 유출 사고를 의미합니다.

그리고 프레임워크의 기본 설정은 당신에게 불리하게 작용합니다. 대부분의 에이전트(Agent) 및 트레이싱(Tracing) 라이브러리는 디버깅을 위해 필요한 기능이기 때문에 기본적으로 도구 입력값(tool inputs)을 로그에 남깁니다. 실패한 실행을 재현할 수 있게 해주는 바로 그 기능이 당신의 키를 디스크에 기록하는 기능이기도 합니다. 누구도 이를 유출하도록 설계하지 않았습니다. 유용함의 부작용으로서 유출되는 것입니다.

해결책: 소스가 아닌 경계에서 비식별화(Redact) 하세요

소스 단계에서 비밀 정보를 제거하려고 시도할 수도 있습니다. 도구 인자(tool argument)로 키를 절대 전달하지 않거나, 도구 내부에서 환경 변수로부터 키를 가져오거나, 트레이스(trace)에 포함되지 않도록 하는 식입니다. 가능한 곳에서는 그렇게 하십시오. 하지만 모든 경우를 잡아낼 수는 없습니다. 왜냐하면 당신이 모든 도구, 모든 라이브러리, 그리고 새로운 것을 로그에 남길 미래의 모든 코드 경로를 제어할 수는 없기 때문입니다. 따라서 데이터가 당신의 통제를 벗어나는 바로 그 지점, 즉 로그 경계(log boundary)에 두 번째 방어선을 구축해야 합니다. 단 하나의 함수를 만들고, 로그가 기록되거나 전송되기 전에 모든 라인이 그 함수를 통과하게 만드십시오.

이 Redactor(비식별화 도구)는 두 번의 패스(pass)를 거칩니다.

첫 번째 패스: 알려진 형태(known shapes). 대부분의 가치 높은 비밀 정보는 공개된 접두사(prefix)를 가지고 있습니다. OpenAI 키는 sk-로 시작합니다. AWS 액세스 키 ID(access key IDs)는 AKIA로 시작합니다. GitHub 클래식 개인 액세스 토큰(personal access tokens)은 ghp_로 시작합니다. GitHub의 공식 문서에 따르면

두 번째 단계: 엔트로피 네트워크 (entropy net). 정규표현식 (Regex)은 당신이 예상한 형태만을 잡아낼 수 있습니다. 벤더의 세션 토큰, 내부 서비스 키, 혹은 새로운 접두사(prefix)를 달고 나오는 다음 API 키 등은 당신의 리스트에 없을 것입니다. 따라서 길쭉한 토큰처럼 보이는 모든 것에 대해, 문자당 비트(bits per character) 단위로 Shannon 엔트로피 (Shannon entropy)를 계산합니다. 무작위로 보이는 문자열은 높은 점수를 받습니다. 영어 단어나 슬러그 (slugs)는 낮은 점수를 받습니다. 토큰이 임계값 (threshold)을 넘으면 마스킹 (mask) 처리합니다. 이것은 제가 발명한 것이 아닙니다. Yelp/detect-secrets 라이브러리가 정확히 이 아이디어를 제공하며, Base64HighEntropyStringHexHighEntropyString 플러그인을 통해 각각 기본 제한값을 4.5와 3.0 bits per character로 설정하고 있습니다. 이는 제가 지어낸 영리한 속임수가 아니라, 업계에서 인정받는 휴리스틱 (heuristic)입니다.

이것이 설계의 전부입니다. 정밀도 (precision)를 위한 알려진 형태, 그리고 미지의 대상을 위한 재현율 (recall)을 위한 엔트로피. 이제 코드를 보겠습니다.

데모 (The demo)

표준 라이브러리 (stdlib)만 사용합니다: remath. 네트워크, 무작위성 (randomness), 시계 (clock), os.environ, 서브프로세스 (subprocess)를 사용하지 않습니다. 테스트 데이터 (fixtures)는 하드코딩된 10개의 합성 로그 라인입니다. 6개는 알려진 형식의 비밀 정보를 포함하고, 1개는 알려지지 않은 형식의 토큰을 포함하며, 나머지 3개는 Redactor가 무엇을 잘못 잡아내는지 확인하기 위해 의도적으로 배치한 무해한 미끼 (decoys)입니다. 이것들은 실제 로그 덤프도 실제 자격 증명 (credentials)도 아닙니다. sk- / AKIA / ghp_ 문자열은 공개된 접두사 형태에 맞게 만들어진 것입니다. 무작위성이나 시계가 없기 때문에 출력은 실행할 때마다 바이트 단위로 동일하며, 덕분에 MD5 해시를 고정할 수 있습니다. 이는 실행 가능한 로컬 (runnable local) 환경입니다. redact 함수는 실제 로깅 필터 (logging filter)로 바로 적용할 수 있습니다.

"""
secret_redactor.py — 로그 라인을 위한 결정론적 (deterministic) stdlib 전용 비밀 정보 Redactor.

...

python3 -I secret_redactor.py로 실행하면 다음과 같이 바이트 단위로 일치하는 결과를 얻을 수 있습니다:

NAIVE LOGGER  (tool-call 인자를 그대로 작성하여 벤더로 전송함)
  WROTE: tool_call=openai args={"model":"gpt-4o","api_key":"sk-Hd83kfJ20alsKDi39fKDoeQ1xZ77bQp"}
  WROTE: tool_call=s3_put env={"AWS_ACCESS_KEY_ID":"AKIA7Q2KX9PLMNOP4RST"}
...

출력을 읽어보세요, Redactor를 당혹스럽게 만드는 두 줄을 포함하여

단순한 로거(logger)는 10줄 모두를 있는 그대로 기록합니다. 그중 6줄은 알려진 형식의 비밀 정보를 그대로 외부(egress)로 유출합니다. 이것이 기준점입니다. 아무것도 하지 않으면 6개를 유출하게 됩니다. 비식별화된(redacted) 로거는 6개 모두를 형태(shape)에 따라 마스킹(masking)하고, 1개의 알려지지 않은 형식의 토큰을 엔트로피(entropy)를 통해 마스킹하며, 나머지 2개의 무해한 줄은 깨끗하게 남겨둡니다. [REDACTED:openai_key][REDACTED:basic_auth] 레이블은 비밀 정보 자체를 보여주지 않으면서도 그곳에 어떤 종류의 비밀이 있었는지를 보여줍니다. 이것이 바로 실질적인 승리입니다.

이제 이 도구가 요새(fortress)가 아닌 바닥(floor) 수준에 머물게 만드는 두 줄을 살펴보겠습니다.

trustpilot-review-scraper-v2-prod라는 슬러그(slug)가 마스킹되었습니다. 이것은 비밀 정보가 아닙니다. 제가 실제로 사용하는 액터(actor) 중 하나의 이름입니다. 이 문자열은 문자당 3.78비트를 기록하며, 이는 제 임계값(threshold)인 3.6을 약간 상회합니다. 영어를 읽을 수 없는 함수에게는 무작위처럼 보일 정도로 문자와 숫자, 대시(-)가 밀도 있게 섞여 있기 때문입니다. 이는 완벽하게 무해한 로그 필드에 대한 오탐(false positive)이며, 운영 환경에서 뼈아프게 다가오는 실패 모드입니다. 더 많은 실제 비밀 정보를 잡기 위해 임계값을 낮추면 여러분의 슬러그, ID, 요청 해시(request hash)가 더 많이 마스킹될 것이고, 임계값을 높이면 실제 비밀 정보가 빠져나가게 됩니다. 명확한 경계선은 없습니다. 데모의 함수는 이 과정이 일어나는 것을 직접 확인할 수 있도록 점수(score)를 출력하기까지 합니다.

deadbeef 다이제스트(digest)는 마스킹되지 않았습니다. 이는 32글자의 충분히 긴 문자열이지만, 동일한 8글자가 네 번 반복되는 구조이기에 엔트로피가 문자당 2.16비트에 불과하며 임계값보다 훨씬 낮습니다. 이 경우에는 정답입니다. 이것은 비밀 정보가 아니라 미끼(decoy)이기 때문입니다. 하지만 여기서 얻는 교훈은 반대 방향을 가리킵니다. 엔트로피가 낮은 실제 비밀 정보도 정확히 똑같은 통로를 통해 빠져나갑니다. 취약한 사전식 비밀번호나 누군가 재사용한 짧은 공유 토큰 같은 것들 말입니다. 엔트로피 그물은 이들을 절대 잡아낼 수 없습니다. 오직 그 특정한 형태를 겨냥한 정규 표현식(regex)만이 잡아낼 수 있지만, 여러분은 예상하지 못한 형태에 대한 정규 표현식을 작성할 수 없습니다.

따라서 정직한 성적표는 "10줄의 코드, 0건의 유출, 완료"가 아닙니다. 실제 결과는 다음과 같습니다: 형태(shape)를 통해 포착된 6개의 알려진 형식의 비밀 정보, 엔트로피(entropy)를 통해 포착된 1개의 알 수 없는 비밀 정보, 잘못 마스킹된 1개의 정상적인 필드, 그리고 그대로 통과해 버릴 구조적으로 보이지 않는 유형의 비밀 정보 1개입니다. 이 Redactor(비식별화 도구)는 가장 흔하고 가장 당혹스러운 유출을 제거하는 저렴하고 결정론적인(deterministic) 방어벽입니다. 이것은 보증 수표가 아니며, 데모는 스스로의 실패 사례를 출력함으로써 여러분이 이를 보증이라고 가장할 수 없게 만듭니다.

이 도구를 신뢰하기 전에, 실패 모드(failure modes)의 명칭을 확인하세요

정상적인 고엔트로피(high-entropy) 필드에 대한 오탐(False positives). 이것은 여러분이 가장 먼저 체감하게 될 문제입니다. 슬러그(Slugs), UUID, 콘텐츠 해시(content hashes), 요청 ID(request IDs), 무해한 데이터의 base64 블롭(blobs): 비밀 정보가 아닌 것들 중에도 무작위로 보이는 것이 아주 많습니다. 이들 각각은 잘못 마스킹될 후보가 됩니다. 분주한 로그 환경에서 이는 일부 실제 디버깅 컨텍스트가 [REDACTED:high-entropy] 뒤로 사라진다는 것을 의미하며, 이제 여러분은 자신이 만든 Redactor에 짜증을 느끼게 될 것입니다. 해결책은 마법 같은 임계값(threshold)이 아닙니다. 그것은 알려진 안전한 패턴(여러분의 슬러그 형식, y

AI 자동 생성 콘텐츠

본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.

원문 바로가기
0

댓글

0