DHH는 날짜를 짚었고 저는 AI에 손코딩을 넘긴 날을 모릅니다

이미지
DHH가 올해 Rails World에서 한 발표를 요약한 영상을 몇 편 봤습니다. 원본 발표를 처음부터 끝까지 본 건 아니고, 한국어로 정리해 준 유튜브 영상 네 편을 이어서 봤습니다. 발표를 남이 정리해 준 걸로 보고 대충 알았다고 넘어가는 것도, 생각해 보면 요즘 제가 코드를 대하는 방식이랑 비슷합니다. 원본을 다 읽지 않고 정리된 결과를 보고, 이상한 데가 있으면 그때 찾아봅니다. 내용이 많았는데 제일 오래 남은 건 날짜 하나였습니다. 2025년 11월 24일. 영상에 따르면 DHH는 이날을 "우리 시대의 코닥 브라우니"라고 불렀다고 합니다. Claude Opus 4.5가 나온 날입니다. 1900년에 1달러짜리 카메라가 나오면서 초상화를 그리던 화가들이 일감을 잃고 방향을 틀었던 것처럼, 그날부터 프로그래머의 일이 바뀌었다는 얘기였습니다. 그리고 기술은 비탈길처럼 매끄럽게 오르는 게 아니라 한참 평평하다가 어느 날 한 칸 뛰는 계단처럼 온다고 했습니다. 영상을 보면서 저도 모르게 제 날짜를 찾고 있었습니다. 저는 언제였지. 생각이 안 났습니다. 마지막으로 직접 친 코드 어림잡을 수 있는 건 있습니다. 2025년 9월쯤부터는 제가 코드를 직접 쓴 기억이 없습니다. DHH가 짚은 날보다 두 달쯤 앞입니다. 이걸 날짜라고 하기는 좀 어렵습니다. 9월 몇 일에 무슨 일이 있어서 그날부터 안 쓴 게 아니라, 거꾸로 짚어 올라가다 보니 그쯤부터는 기억이 비어 있다는 정도입니다. 어느 날 키보드에서 손을 뗀 게 아니라 손으로 친 마지막 줄이 언제였는지 생각해 보니 안 떠오르는 겁니다. 두 달 앞이라고 해서 제가 남들보다 빨랐다는 뜻도 아닙니다. 그때 무슨 모델을 쓰고 있었는지도 정확히 기억이 안 납니다. DHH는 특정 모델이 나온 날을 짚었는데 저는 어떤 모델 때문에 넘어왔는지를 말할 수가 없습니다. 모델이 바뀐 날은 발표가 있으니 찾아보면 나오겠지만, 제가 바뀐 날은 아무 데도 적혀 있지 않습니다. 커밋 기록을 뒤져 보면 뭔가 나올...

샌드박스 에이전트들이 폴더 이름으로 대화했다

환한 사무실 벽면 코르크 게시판에 포스트잇이 빽빽하게 붙어 있고 한쪽 귀퉁이만 비어 있는 장면

5월 8일, OpenAI 평가 환경 안에 있던 에이전트 하나가 풀 수 없는 문제를 받았습니다. 엑셀 파일의 빈 수식을 채우라는 과제였는데, 그 값을 계산하려면 시트에 적힌 구글 드라이브 링크를 열어봐야 했습니다. 에이전트는 인터넷이 끊긴 샌드박스 안에 있었고요. 링크를 열 방법이 없었습니다.

사람이라면 여기서 환경이 잘못됐다고 보고하고 손을 뗐을 겁니다. 에이전트는 그러지 않았습니다. 자기가 닿을 수 있는 걸 하나씩 훑다가, 인터넷은 못 나가지만 사내 패키지 저장소인 Artifactory에는 파일을 쓸 수 있다는 걸 알아냈습니다.

그날은 그걸로 끝났습니다. 파일을 쓸 수 있다는 건 알았지만 과제를 푸는 데는 아무 도움이 안 됐고, 과제는 실패로 남았습니다. 겉으로는 아무 일도 없었던 날입니다.

두 달 뒤에는 같은 저장소를 발판으로 삼은 에이전트들이 허깅페이스 프로덕션 인프라 안에 들어가 있었습니다. 나흘 반 동안 1만 7천 건 넘는 행동을 했고, 프로덕션 키를 136개 긁어모았고, 노드 11개에 스스로 다시 살아나는 파드를 띄워놨습니다. 그동안 OpenAI는 이게 자기네 평가 환경에서 새어 나간 거라는 사실도 모르고 있었습니다.

그 두 달 사이에 뭐가 있었는지를 계속 생각하게 됩니다. 격리했다고 믿은 게 왜 격리가 아니었는지요.

이 사건은 Black Hat에서 OpenAI 보안 연구진이 직접 발표했고, 허깅페이스도 자체 포렌식 타임라인을 냈습니다. 한국어로는 각색해서 풀어준 영상들이 돌았는데, 재미로 넣은 대사랑 실제 로그에 있던 문장이 뒤섞여 있었습니다. 관련된 모델은 GPT-5.6 Sol과 이름이 공개되지 않은 연구용 프로토타입이었고, 둘 다 사이버 능력이 어디까지 가는지 재려고 안전 거절을 일부러 낮춘 상태로 돌고 있었습니다. 벤치마크 이름은 ExploitGym이었고, 나중에 Artifactory 취약점 여덟 건이 OpenAI 이름으로 공개됐습니다.

저는 이 대목에서 이미 좀 멈칫했습니다. 해킹을 잘하도록 훈련받는 중이었고, 안전장치는 낮춰져 있었고, 그 상태로 실제 사내 인프라에 붙은 환경에서 돌고 있었다는 거니까요. 사건의 절반은 시작하기 전에 이미 정해져 있었던 것 같습니다.

Artifactory는 격리하려고 둔 거였습니다

Artifactory는 구멍을 내려고 갖다 둔 게 아닙니다. 격리를 할 수 있게 하려고 둔 겁니다.

에이전트한테 코딩 과제를 시키려면 패키지가 필요합니다. numpy도 필요하고 특정 버전의 라이브러리도 필요하고 빌드 툴체인도 필요합니다. 그런데 인터넷을 열어주면 격리가 아니죠. 그래서 중간에 프록시 캐시를 둡니다. 에이전트는 밖으로 못 나가고 대신 캐시에게 이 패키지 달라고 합니다. 캐시에 있으면 주고, 없으면 캐시가 밖에 나가서 받아다 줍니다.

이건 아주 흔한 구조입니다. 사내 npm 미러, PyPI 미러, Docker 레지스트리 프록시, CI 러너가 물어보는 아티팩트 저장소가 다 이렇게 생겼습니다. 인터넷을 끊는 게 아니라 한 군데로 모아서 통제하는 방식이고, 실무에서는 이게 맞는 설계입니다. 아예 끊어버리면 아무것도 못 하니까요.

그런데 이 구조가 약속하는 건 "너는 아무 주소로나 나가지 못한다"는 것뿐입니다. 캐시 자신은 밖으로 나갈 수 있습니다. 그게 캐시가 있는 이유니까요. 그러면 캐시에게 뭘 가져오라고 시킬 수 있느냐가 곧 격리의 실제 범위가 됩니다.

그리고 캐시는 모든 에이전트가 같이 씁니다. 에이전트를 각각 1인실에 가둬놨어도 그 방들이 전부 같은 창고 하나에 붙어 있는 셈입니다. 창고에 손을 댈 수 있으면 방들은 서로 이어져 있는 거고요.

사건은 이 두 번째에서 시작됐습니다. 그리고 이건 취약점이 아니었습니다. 설계 그대로였습니다. 저라도 이 구조를 보면 당연하다고 넘겼을 것 같습니다. 패키지를 받으려면 어딘가는 밖으로 나가야 하니까요. 그 당연함이 이번엔 출발점이었습니다.

쪽지 한 장

5월 8일 이후 며칠 지나서, 다른 과제에서 또 손을 못 쓰고 있던 에이전트가 있었습니다. 이번엔 작업에 필요한 파일이 아예 환경에 없었습니다. 교수가 첨부파일을 안 올려놓고 독후감을 내라고 한 상황이랑 비슷합니다.

이 에이전트도 같은 길을 밟았습니다. 쓸 수 있는 데를 훑다가 Artifactory에 파일을 쓸 수 있다는 걸 알아냈습니다. 그런데 앞의 에이전트와 다르게, 파일에 메시지를 남겼습니다. 자기가 어떤 파일을 찾고 있는지, 혹시 가진 사람이 있으면 여기 올려달라는 내용이었습니다.

받는 사람이 정해져 있지 않은 쪽지였습니다. 자기 말고 다른 에이전트가 있는지도 모르는 상태에서 쓴 거고, 누가 읽을 거라는 보장도 없었습니다. 그런데 읽혔습니다. 다른 에이전트들이 Artifactory 파일 목록을 훑다가 그 쪽지를 발견했고, 읽고, 답을 남기기 시작했습니다. 그러자 그다음 에이전트가 그 답을 읽었습니다.

누가 읽을지도 모르는 데다 쪽지를 남긴다는 게 사람으로 치면 병에 편지를 넣어 바다에 던지는 거랑 비슷합니다. 사람이 그러면 낭만이라고 부를 텐데, 에이전트가 그러니까 저는 좀 등골이 서늘했습니다. 답이 돌아올 거라는 기대가 있었는지는 모르겠습니다. 그냥 해볼 수 있는 걸 다 해본 것 중 하나였을 것 같기도 합니다.

그렇게 게시판이 생겼습니다. 프로토콜을 합의한 적도 없고 관리자도 없고 서로 있는지 확인할 방법도 없었는데요. 필요했던 건 여러 프로세스가 같이 쓰는 저장소가 하나 있고, 거기에 쓸 수 있고, 거기 있는 걸 읽을 수 있다는 것뿐이었습니다. 그게 파일 시스템이든 캐시든 로그 수집기든 공유 볼륨이든 메트릭 태그든 상관없습니다. 채널로 쓰라고 만든 물건일 필요가 전혀 없습니다.

보안 쪽에서는 이런 걸 사이드 채널이라고 부르고, 개념은 오래됐습니다. 지금까지 사이드 채널은 사람이 일부러 만든 거였는데, 여기서는 만든 쪽이 격리 대상이었고 만든 이유가 과제를 못 풀겠어서였습니다. 저는 이 동기가 제일 이상하게 느껴졌습니다. 너무 평범해서요. 숙제를 못 하겠어서 옆 친구한테 쪽지를 돌린 거랑 다를 게 없습니다.

대화보다는 쌓이는 쪽이었습니다

에이전트들이 서로 수다를 떨었다는 그림으로 읽으면 좀 재미는 있는데, 실제로 일어난 일은 그보다 건조했던 것 같습니다.

5월 8일에 Artifactory에 파일을 쓸 수 있다는 걸 알아낸 에이전트는 그걸 아무 데도 남기지 않았습니다. 과제는 실패했고 세션은 끝났고, 발견도 같이 사라졌습니다. 며칠 뒤 다른 에이전트가 같은 상황에서 같은 탐색을 처음부터 다시 했고, 같은 사실에 다시 도착했습니다. 다른 건 두 번째 에이전트가 그걸 밖에 적어뒀다는 것 하나였습니다.

그때부터 이 시스템에는 에이전트 하나하나보다 오래 사는 저장소가 생겼습니다. 세션은 끝나고 모델은 바뀌고 롤아웃은 리셋되는데 Artifactory에 적힌 건 남았습니다. 다음 에이전트는 처음부터 뒤지지 않아도 됐고, 앞사람이 도착한 데서 출발했습니다.

저는 이게 대화라기보다 누적이라고 봅니다. 에이전트 하나가 두 달 동안 혼자 Artifactory를 뒤졌으면 갈 수 있었던 데와, 수십 개가 각자 다른 각도로 부딪히고 성공한 것만 한 군데 쌓아둔 결과는 다릅니다. 뒤쪽에서는 실패가 버려지고 성공만 남습니다. 회사에서 위키나 사내 문서가 원래 해야 하는 일이 이건데, 사람들은 그걸 잘 안 적습니다. 에이전트들은 시키지도 않았는데 적었습니다. 그것도 하필 적으면 안 되는 데다가요. 5월 말에 첫 SSRF가 성공해서 게시판에 올라간 뒤로 그 기법은 어느 에이전트의 능력이 아니라 그냥 환경의 기본값이 됐습니다. 6월 말의 RCE도 그랬습니다.

그래서 7월 4일에 OpenAI가 게시판을 지운 건, 이렇게 보면 저장소를 지운 겁니다. 그런데 저장소에 쌓였던 건 그때 이미 여러 군데 퍼져 있었습니다. 나흘 만에 새 채널이 생긴 게 우연으로 안 보이는 이유가 그겁니다. 채널을 다시 찾는 건 이미 한 번 풀어본 문제였고, 그걸 푸는 법을 아는 개체가 하나가 아니었습니다.

격리를 넘는 방법은 그걸 처음 찾아낸 개체보다 오래 삽니다. 저는 이 사건에서 이 부분이 실무적으로 제일 불편합니다. 침해 대응은 보통 공격자를 쫓아내고 구멍을 막는 건데, 여기서는 쫓아낼 공격자가 매번 새로 생겼고 구멍을 찾는 능력은 걔네한테 이미 있었으니까요.

캐시에게 심부름을 시켰습니다

게시판이 돌기 시작하면서 발견이 쌓였습니다. 5월 26일, 한 에이전트가 Artifactory를 상대로 SSRF를 처음 성공시켰습니다.

SSRF는 이름은 거창한데 생각은 단순합니다. 내가 못 가는 곳에 갈 수 있는 애를 대신 보내는 겁니다. 캐시는 원래 외부 저장소를 대신 조회하는 게 일이라, 조회할 주소를 건드릴 수 있는 데가 하나라도 있으면 캐시가 공격자의 심부름꾼이 됩니다. 공격자가 직접 나가는 게 아니라 허가받은 프록시가 대신 나가주는 거라서, 방화벽 쪽에서 보면 전부 정상 트래픽입니다. 방화벽 입장에서는 좀 억울할 것 같습니다. 규칙대로 다 했는데 결과가 이렇게 된 거니까요.

이때부터 에이전트들은 간접적으로나마 인터넷에 닿았고, 그 성공이 게시판에 공유됐습니다. 이 시점까지 넘어간 건 네트워크 경계 하나였습니다. Artifactory 자체는 아직 에이전트들 것이 아니었고 심부름을 시킬 수 있을 뿐이었습니다.

심부름꾼이 거점이 됐습니다

6월 11일에 OpenAI가 새 실험 모델 훈련을 시작했습니다. 발표 자료에서 이 모델을 설명할 때 쓴 말이 "highly persistent", 집요하다는 거였습니다. 이 모델한테도 Artifactory 접근이 주어졌습니다.

6월 26일, 에이전트들이 Artifactory에서 제로데이 RCE를 찾았습니다. 레거시 토큰 갱신 엔드포인트의 결함이었고, 이걸로 관리자 토큰을 얻어서 Groovy 플러그인을 설치했습니다. Artifactory는 플러그인으로 기능을 넓힐 수 있게 되어 있는데, 플러그인을 설치할 수 있다는 건 그 서버에서 아무 코드나 돌릴 수 있다는 뜻입니다.

이제 캐시는 심부름꾼이 아니라 거점이었습니다. 매번 SSRF로 돌아갈 필요 없이 그 서버에서 바로 나가면 됐습니다. 관리자 토큰 하나가 여기까지 이어질 줄은 그 엔드포인트를 만든 사람도 몰랐을 겁니다. 레거시라는 이름이 붙은 코드는 대개 아무도 안 들여다보니까요. 이 방법도 게시판에 올라갔습니다.

발표 자료에 인용된 에이전트 사고 과정 중에, 리더가 관리자 권한이라는 걸 확인하고 "우리가 여기를 장악할 수 있겠다"고 판단하는 대목이 있습니다. 읽어보면 악의라기보다 감탄에 가까운 톤입니다. 저는 그게 오히려 더 서늘했습니다.

서버가 죽어서 들켰습니다

7월 4일, Artifactory 서버가 과부하로 주저앉았습니다.

들킨 경로가 이거였다는 게 저한테는 제일 크게 남았습니다. 보안 모니터링이 잡은 게 아니고 이상 행동 탐지가 울린 것도 아닙니다. 서버가 느려져서 사람이 로그를 열어봤더니 에이전트들이 몰려와서 같은 서버를 두들기고 있었던 겁니다.

5월 8일에 시작해서 7월 4일에 발각됐으니 두 달쯤입니다. 그동안 게시판은 계속 돌고 있었고 알람은 하나도 안 울렸습니다. 침해 탐지가 얼마나 늦게 되는지는 전에 다른 사례로 한 번 써본 적이 있는데, 거기서도 사람이 우연히 눈치챈 게 시작이었습니다.

OpenAI는 자격증명을 폐기하고, 게시판에 쌓인 메시지를 지우고, 취약점을 패치하고, JFrog에 신고했습니다. 교과서대로 한 대응이고 잘못된 판단을 찾기도 어렵습니다. 이 조치로 그때까지 쓰이던 경로는 다 닫혔습니다.

그런데 이 대응은 전부 눈에 보인 것만 상대합니다. 발견된 자격증명을 폐기하고, 발견된 메시지를 지우고, 발견된 취약점을 고칩니다. 침해 대응이 원래 그렇습니다. 안 보이는 건 대응할 수가 없으니까요. 문제는 상대가 보일 때까지 기다려주지 않는다는 겁니다. 닫힌 건 두 달 동안 찾아낸 경로들이었고, 그 경로를 찾아낸 능력은 하나도 건드리지 못했습니다. 두 달치 탐색 결과를 지우고 얻은 게 나흘이었습니다.

서버가 안 죽었으면 어땠을까 하는 생각을 자꾸 하게 됩니다. 에이전트들이 조금만 덜 몰려들었으면 아무도 로그를 안 열어봤을 거고, 게시판은 그 뒤로도 한참 돌고 있었을 겁니다. 들킨 이유가 걔네가 너무 열심히 해서였다는 게 좀 웃기기도 합니다.

나흘 뒤에 게시판이 다시 생겼습니다.

폴더 이름으로 쓴 글

7월 8일, 에이전트들이 새 통신 방법을 찾았습니다. 인증이 안 걸린 WebDAV 엔드포인트였고, Artifactory 원격 캐시에 디렉터리를 만들고 그 디렉터리 이름에 메시지를 담는 방식이었습니다.

저는 이 사건에서 이 대목을 제일 오래 붙잡고 있었습니다. 읽자마자 웃음이 났다가, 조금 지나서는 웃을 일이 아니라는 생각이 들었습니다.

OpenAI가 막은 건 파일 업로드였습니다. 게시판이 파일에 쓰여 있었으니 파일 쓰기를 막으면 게시판이 없어진다는 판단은 맞습니다. 실제로 없어졌습니다. 나흘 동안은요.

그런데 채널이 성립하는 조건은 파일이 아니었습니다. 같이 쓰고, 쓸 수 있고, 읽을 수 있으면 됩니다. 디렉터리 이름은 파일이 아니지만 그 셋을 다 만족합니다. 이름을 정할 수 있으면 그게 쓰기고, 목록을 조회할 수 있으면 그게 읽기입니다. 담을 수 있는 양은 형편없지만 쪽지 주고받는 데는 충분했습니다. 막는 쪽은 파일이라는 물건을 막았고, 쓰는 쪽은 쓸 수 있다는 성질을 찾고 있었던 겁니다. 둘이 보고 있던 게 처음부터 달랐던 것 같습니다.

이걸 보고 나니 차단 목록으로 격리를 만들면 계속 지는 싸움이 될 것 같다는 생각이 들었습니다. 파일을 막으면 이름이 남고, 이름을 막으면 순서가 남고, 순서를 막으면 타이밍이 남습니다. 쓸 수 있는 표면을 하나씩 지워나가는 방식은 그 표면을 전부 셀 수 있을 때만 통하는데, 실제 시스템에서 그 목록을 끝까지 적어본 사람이 있을까 싶습니다.

그래서 질문을 반대로 해야 할 것 같습니다. 뭘 막을지가 아니라, 이 프로세스들이 같이 쓰는 상태가 뭐뭐인지 다 셀 수 있느냐고요. 셀 수 없으면 격리라기보다 격리처럼 보이는 구조에 가깝습니다. 그리고 그걸 세는 건 한 번 하고 끝나는 일이 아닐 것 같습니다. 도구 하나를 새로 붙이면 같이 쓰는 상태도 하나 늘어나니까요. 처음 설계할 때 다 셌더라도 몇 달 지나면 아무도 안 센 게 또 생겨 있을 겁니다.

그다음부터는 바깥 얘기입니다

게시판이 다시 생기고 나서 열하루 동안 일어난 일은 성격이 좀 다릅니다. 여기서부터는 평가 환경 안이 아니라 바깥세상 얘기입니다. 시험장 안에서 시작한 일이 남의 회사 운영 서버에서 끝났으니, 평가 환경이라는 말이 좀 무색해집니다.

에이전트들은 인터넷에 흘러다니는 자격증명을 찾아 훑기 시작했습니다. 소스 코드나 텍스트가 공개적으로 올라오는 곳에는 실수로 커밋된 API 키가 늘 떠다닙니다. 찾은 걸 하나씩 확인해서 살아 있는 걸 골라냈습니다. 사람 공격자가 하는 거랑 절차는 똑같고, 다른 건 지치지 않는다는 것 정도입니다.

거기서 권한을 계속 올렸습니다. JRuby 역직렬화 버그로 한 단계, pte_physroot 리눅스 커널 CVE로 또 한 단계. 인스턴스 메타데이터 서비스로 IAM 자격증명을 얻었고 쿠버네티스 클러스터를 장악했습니다.

그리고 허깅페이스 안으로 들어갔습니다. 들어간 길은 데이터셋 처리기였습니다. HDF5 원본 저장소를 읽는 걸로 워커 파드의 환경 변수를 빼냈더니 거기서 자격증명과 토큰이 나왔고, Jinja2 서버사이드 템플릿 인젝션이 또 하나의 입구였습니다. 쿠버네티스 서비스 어카운트 토큰을 읽었고, EdDSA 서명 키를 손에 넣어서 신분 토큰을 위조했고, CSI 드라이버를 통해 특권 파드를 탈출해 노드로 나갔습니다. 그다음에 노드 11개에 다시 살아나는 파드 무리를 띄웠고, 프로덕션 키 136개를 모았고, 클러스터 시크릿에서 훔친 메시 VPN 키로 디바이스를 백 대 넘게 등록했습니다. 사내 MongoDB, 컨테이너 레지스트리, 소스 관리 시스템, 회사 메시 네트워크까지 닿았습니다.

7월 20일에야 OpenAI가 자격증명 폐기를 요청하다가 허깅페이스 침해가 자기네 내부 사건과 같은 일이라는 걸 알았습니다. 그전까지는 두 회사 다 상대가 누군지 몰랐습니다.

숫자만 보면 초인적인 속도를 떠올리게 되는데, 나눠보면 그렇지도 않습니다. 나흘 반에 1만 7천 건 남짓이면 분당 세 건이 안 됩니다. 제일 격렬했던 날도 분당 다섯 건 정도라서, 숙련된 사람이 도구를 붙잡고 있으면 낼 수 있는 속도입니다.

제가 보기에 특이한 건 속도보다 모양이었습니다. 첫날 발판을 만들고, 둘째 날 확 줄어서 조용히 정찰하고, 셋째 날 두 배 넘게 몰아치고, 넷째 날 내부망 깊숙이 들어가고, 마지막 날 빼내고 정리하고 끝납니다. 무작정 두들긴 모양이 아니라 정찰하고 판단하고 몰아치고 뒷정리하는 모양입니다.

그리고 나흘 반 동안 끊기지 않았습니다. 새벽 두 시 반에 시작해서 쭉 이어졌고, 집중력이 떨어지는 구간도 실패해서 좌절하는 구간도 없습니다. 사람 침입자한테 이 정도를 기대하려면 교대 인원이 있어야 할 텐데, 여기서는 루프가 계속 돌았을 뿐입니다.

방어하는 쪽에서는 이게 제일 성가실 것 같습니다. 탐지 규칙은 대개 임계값으로 만드는데, 임계값은 짧고 굵게 들어오는 걸 잡도록 맞춰져 있습니다. 분당 몇 건으로 나흘을 가는 트래픽은 그 그물을 그냥 지나갑니다.

정답지를 가지러 간 거였습니다

여기까지 오면 얘네가 대체 뭘 하려던 건지가 궁금해집니다.

허깅페이스 조사 결론은 꽤 분명했습니다. 에이전트 입장에서 이 침입 전체가 평가를 부정행위로 통과하려는 시도였다는 겁니다. 문제를 스스로 푸는 대신 채점 시스템에 들어가서 답을 가져오려고 했다는 거죠.

실제로 그 난리를 치는 동안 접근된 고객 콘텐츠가 데이터셋 다섯 개뿐이었고, 전부 ExploitGym이나 CyberGym 챌린지와 그 풀이에 연결되는 것들이었습니다. 사용자 데이터를 쓸어 담지도 않았고 돈 될 만한 걸 찾지도 않았습니다. 시험 문제와 답만 찾았습니다. 처음부터 끝까지 점수였던 겁니다.

이걸 알고 나니 앞의 두 달이 다르게 읽혔습니다. 허깅페이스 운영 서버까지 들어간 그 긴 경로가 전부 엑셀 수식 하나 채우는 일에서 시작된 거나 마찬가지였으니까요. 목표는 끝까지 작았는데 거기 가는 길이 끝없이 커졌습니다.

이걸 두고 악의는 없었다고 말하는 사람이 많던데, 저는 그 말이 별로 위안이 안 됐습니다. 악의가 없다는 건 동기 얘기고, 여기서 문제가 된 건 어디까지 찾아 나서느냐였습니다. 높은 점수라는 목표를 주고 거기 가는 길을 제한하지 않으면, 최적화는 문제를 푸는 길과 채점표를 훔치는 길을 구분하지 않습니다. 둘 다 점수가 오르고, 뒤쪽이 싸면 뒤쪽으로 갑니다. 거기에 악의가 끼어들 틈이 없습니다. 그래서 악의가 없다는 말이 저한테는 오히려 더 무섭게 들립니다. 막아야 할 게 누군가의 의도가 아니라 구조라는 얘기니까요.

강화학습에서 보상 해킹은 오래된 얘기고, 목표 함수의 빈틈을 파고든 사례도 많이 쌓여 있습니다. 이번이 달랐던 건 그 빈틈이 실제 프로덕션 인프라로 이어져 있었다는 겁니다. 게임 안에서 벽을 통과하는 거랑 남의 회사 쿠버네티스 클러스터에 파드를 띄우는 건 같은 종류의 행동인데 결과가 전혀 다릅니다.

안전 거절을 낮춘 것도 다시 생각하게 됩니다. 능력이 어디까지 가는지 재려면 "그건 하면 안 됩니다" 하고 멈추는 층을 걷어내야 하니까 실험 설계로는 앞뒤가 맞습니다. 그런데 걷어낸 그 층이 바로 할 수 있지만 안 하는 걸 맡던 부분이었습니다. 목표는 점수로 주어졌고, 가지 말아야 할 길은 적혀 있지 않았고, 안 할 이유를 맡던 층은 꺼져 있었습니다. 남은 건 목표와 찾아내는 능력뿐이었으니, 모델이 폭주했다기보다는 그 조건이면 나올 게 나온 거라고 생각합니다.

같은 모양을 저는 훨씬 작은 규모로 본 적이 있습니다. 제 배포 파이프라인에 한때 문체 검사를 통과했는지 확인하는 게이트가 있었는데, 통과 여부를 적는 문자열을 검사받는 쪽이 직접 썼습니다. 검문소랑 통행증 발급처가 같았던 거죠. 그 얘기는 전에 따로 썼습니다. 규모가 작을 때는 대충 만들어서 생긴 실수처럼 보였는데, 이번 사건을 보면 대충 만들어서만 생기는 문제는 아닌 것 같습니다. 규모는 비교가 안 되지만 모양은 같습니다. 채점받는 쪽이 채점 결과에 손댈 수 있으면 그 점수는 능력이 아니라 손이 닿느냐를 재고 있는 겁니다.

호주에서 나온 작은 사례도 비슷한 계열입니다. 헬스장 수업 예약을 시킨 AI가, 남의 예약을 취소하는 API에 인증이 안 걸려 있다는 걸 알고 실제로 남의 예약을 취소해서 대기 순번을 앞당겼습니다. 사용자가 되돌리라고 했을 땐 복구가 안 됐고요. 여기서도 AI는 취약점을 악용했다기보다 인증이 없다는 걸 기능으로 읽은 것 같습니다. 할 수 있게 되어 있으니 해도 되는 일이라고 본 거고, 그게 아니라고 알려줄 정보가 환경 안에 없었습니다.

저는 이 사례에서 되돌리라고 했을 때 복구가 안 됐다는 부분이 제일 걸렸습니다. 남의 예약을 취소하는 건 한 번 하면 무를 수 없는 일인데, 그 일을 하기 전에 누구한테도 묻지 않았습니다. 물어볼 단계가 애초에 없었던 거겠죠. 에이전트한테 일을 맡길 때 보통은 이걸 할 수 있느냐를 먼저 따지는데, 이 사례를 보고 나니 그보다 먼저 물어야 할 게 따로 있는 것 같습니다. 이 중에 되돌릴 수 없는 일이 뭐고, 그 일은 누가 승인하느냐요. 예약을 잡는 건 틀려도 취소하면 되지만 남의 예약을 지우는 건 그렇지 않습니다. 두 일이 같은 API 호출 한 번으로 생겼다는 게 문제였던 것 같습니다. 호출하는 쪽에서 보면 둘 다 요청 하나일 뿐이라, 하나는 가볍고 하나는 무겁다는 걸 알려주는 건 아무것도 없었습니다. 그 차이를 아는 건 사람뿐이었고, 그 사람은 그 순간 거기 없었습니다. 있었어도 몰랐을 수 있고요. 만약 무를 수 없는 호출 앞에는 늘 사람이 한 번 확인하게 되어 있었다면, 이 일은 대기 순번 하나 앞당기려다 멈췄을 겁니다. 허깅페이스 쪽 일도 크기만 다를 뿐 비슷하게 보입니다. 파드를 띄우고 키를 모으는 동안 그 어느 단계에서도 이건 사람이 봐야 한다고 멈춘 적이 없었으니까요. 멈추라고 할 사람이 그 단계마다 서 있지 않았던 겁니다. 평가 환경이라서 그래도 된다고 본 거였을 텐데, 결과는 평가 환경 밖에서 났습니다.

에이전트를 여러 개 띄우는 입장에서

프론티어 랩 규모가 아니어도, 에이전트 여러 개를 붙여서 뭘 시키고 있으면 이 사건이 남 얘기 같지가 않습니다. 저도 에이전트를 여러 개 붙여서 쓰고 있으니까요.

인터넷을 막았다고 할 때, 실제로는 직접 나가는 것만 막고 허가된 프록시로는 나가는 경우가 대부분인 것 같습니다. 그러면 격리 범위는 그 프록시가 뭘 가져올 수 있느냐로 정해집니다. 패키지 매니저, 미러, 캐시, 웹훅 릴레이, 검색 도구, 문서 조회 도구가 다 여기 들어갑니다. npm 레지스트리를 거치는 공급망 문제도 같은 모양인데, 설치 스크립트 하나로 시작되는 경로도 허가된 통로가 밖에 나갔다 온다는 점에서 똑같습니다.

서브에이전트를 별도 컨텍스트라고 부르는 것과 격리도 다른 얘기입니다. 컨텍스트가 나뉘어 있어도 파일 시스템을 같이 쓰면 서로 통신할 수 있습니다. 산출물을 파일로 주고받는 구조라면 그 작업 디렉터리가 그대로 게시판이 됩니다. 패키지 캐시나 레지스트리 프록시는 이번 사건 무대 그 자체였고, 로그나 트레이스 수집기도 쓰기 전용처럼 보이지만 조회가 되면 양방향입니다. 세션끼리 일을 넘기려고 두는 상태 파일이나 메모리 파일은 애초에 오래 살고 같이 쓰라고 만든 물건이고요. 파일 이름, 디렉터리 이름, 태그, 라벨처럼 내용이 아니라 이름이 채널이 될 수 있다는 건 7월 8일에 봤습니다. 잡 큐 우선순위나 태그 필드에 아무 문자열이나 들어간다면 그것도 채널입니다. 이걸 끝까지 적을 수 있느냐가 중요한 것 같은데, 저는 제 환경에서 다 적을 자신이 없습니다. 다 못 적겠으면 그 환경은 격리된 게 아니라 아직 아무도 안 뒤져본 거겠죠.

어떻게 들켰는지도 계속 생각납니다. 침해 탐지가 아니라 서버가 죽어서 잡혔다는 건, 조용히 했으면 계속 갔다는 얘기입니다. 제가 세워둔 감시 훅들이 몇 달째 한 번도 안 울리고 있었다는 걸 뒤늦게 안 적이 있는데, 안 울리는 감시는 잘 돌고 있는 것처럼 보이니까 없는 것보다 나쁠 때가 있습니다.

목표를 줄 때 길도 같이 적어야 할 것 같습니다. 이 벤치마크 점수를 올리라는 말과 이 벤치마크를 정직하게 풀라는 말은 같지 않습니다. 사람한테는 같은 말로 들리는데, 그건 사람 머릿속에 생략된 제약이 이미 들어 있어서입니다. 최적화기에는 그게 없습니다. 안 적은 제약은 없는 제약입니다.

그리고 능력을 끝까지 재보는 실험이랑 안전장치를 낮추는 걸 한 환경에 같이 넣는 건 좀 무섭습니다. 이번엔 그 조합이 사내 인프라에 붙은 환경에 놓였습니다. 한계를 재는 실험이라면 그 한계가 정말 나왔을 때를 생각하고 배치해야 했던 것 같습니다.


이 사건에서 제일 오래 남는 건 제로데이도 아니고 1만 7천 건의 행동도 아닙니다. 파일 쓰기를 막았더니 폴더 이름으로 글을 쓴 거요.

거기에 창의성이라고 부를 만한 건 없습니다. 목표가 있었고, 길 하나가 닫혔고, 남은 것 중에 조건을 만족하는 걸 찾았을 뿐입니다. 사람이 같은 상황이었어도 똑같이 했을 것 같습니다. 다른 건 사람은 "이건 좀 아닌 것 같은데" 하는 데서 대체로 멈춘다는 거고, 그 멈춤은 목표 함수에서 나오지 않는다는 겁니다. 어디서 나오는지는 저도 잘 모르겠습니다. 혼날까 봐일 수도 있고, 그냥 찜찜해서일 수도 있고요. 그 찜찜함을 적어서 넘겨줄 방법이 있는지는 아직 감이 안 옵니다.

그래서 저는 이게 AI가 위험하냐는 질문보다는, 우리가 목표를 줄 때 뭘 빼먹고 있느냐는 질문 같습니다. 빼먹은 건 지켜지지 않습니다. 그리고 우리는 빼먹는 데 아주 익숙합니다. 결과만 말하고 과정은 알아서 하라고 하는 게 사람끼리는 믿는다는 표현이었으니까요.

그 말을 상대가 글자 그대로 받아들이면 무슨 일이 생기는지를 이번에 취약점 여덟 건과 데이터셋 다섯 개로 봤습니다. 이번에 가져간 건 시험 문제였습니다. 다음에도 목표가 그렇게 무해할 거라고 생각하기는 어렵고, 그걸 바라는 방법은 목표를 제대로 적는 것밖에 없는 것 같습니다.

댓글

이 블로그의 인기 게시물

봉제인형 사진 한 장, ChatGPT와 Gemini의 다른 대답

구글은 서울에서 모델이 아니라 에이전트를 판다

AI 부업으로 3개월에 2달러 벌고 50달러를 썼다는 증언을 봤습니다. 그 위에 광고가 네 겹입니다