메일 서버 502를 고치는 동안 터미널을 한 번도 안 열었다. AI가 대신 한 13가지가 침해 보고서와 구분이 안 됐다
메일 서버 502를 고치는 동안 터미널을 한 번도 안 열었다. AI가 대신 한 13가지가 침해 보고서와 구분이 안 됐다
아침에 메일함을 열려고 mail.apple-io.com 주소를 쳤는데 502가 떴다. Bad Gateway. 어차피 혼자서는 감이 안 잡히는 종류의 에러라 늘 하던 대로 ChatGPT 창을 켰고, 증상을 적었고, 이제 터미널을 열어서 docker ps 결과를 긁어올 차례였다. 그런데 그럴 필요가 없었다. 잠시 뒤에 메일함이 200으로 돌아왔는데, 그때까지 내가 친 명령어는 한 개도 없었다.
기분이 이상했다. 좋다는 쪽이 아니라 그냥 이상했다. 그러다 이 블로그에 바로 어제 올린 글을 다시 열어봤다. 인터넷이 끊긴 평가 샌드박스 안에서 에이전트들이 사내 패키지 캐시를 게시판으로 쓰다가, 파일 쓰기가 막히자 폴더 이름으로 글을 쓴 이야기다. 그 글에서 내가 마지막에 적은 문장은 "우리가 목표를 줄 때 무엇을 생략하고 있는지"였다.
오늘 나는 목표가 아니라 승인을 줬다. 그런데 무엇을 승인했는지는 여전히 어디에도 안 적혀 있었다.
7월까지의 루프
지난달까지 서버에 문제가 생기면 순서가 정해져 있었다.
터미널을 연다. SSH로 들어간다. docker ps, docker logs, curl을 친다. 출력을 마우스로 긁어서 ChatGPT에 붙여넣는다. 답을 읽는다. 답 안에 있는 명령어를 복사한다. 터미널에 붙여넣는다. 결과를 다시 긁어서 붙여넣는다. 이걸 문제가 풀릴 때까지 반복한다.
이 루프는 느렸다. 명령어 하나 주고받는 데 왕복이 필요했고, 출력이 길면 어디까지 붙여넣을지 내가 잘라야 했고, 잘못 자르면 진단이 엉뚱한 데로 갔다. 컨테이너 로그 같은 건 특히 그렇다. 수백 줄 중에 어느 부분이 중요한지 알면 애초에 물어볼 필요가 없는데, 모르니까 물어보는 거고, 모르는 채로 잘라서 보내면 잘라낸 쪽에 답이 있는 경우가 꼭 생긴다. 그렇게 두 시간 날린 적도 있다. 그래서 이 왕복이 없어지면 좋겠다고 계속 생각했었다.
그런데 그 루프에는 내가 의식하지 않던 기능이 두 개 있었다.
하나는 명령어가 실행되기 전에 반드시 내 눈을 한 번 통과했다는 것이다. 복사하는 순간 읽었고, 붙여넣기 전에 잠깐 봤고, rm이나 --force가 보이면 손이 멈췄다. 검문을 하려고 만든 절차가 아니라 그냥 물리적으로 그렇게 돼 있었던 것뿐인데, 결과적으로는 게이트였다. 실제로 몇 번은 붙여넣기 직전에 취소했다. 이유를 대라면 못 댔을 것이다. 그냥 뭔가 커 보였다.
다른 하나는 그 루프가 느렸다는 것 자체다. 명령을 하나 실행하고 결과를 다시 옮기는 데 시간이 드니까, 그 사이에 내가 뭘 하고 있는지 생각할 틈이 생겼다. 다섯 번째 왕복쯤 가면 "지금 뭘 확인하려는 거지"라는 질문이 저절로 떠오른다. 느린 게 좋았다는 얘기가 아니라, 느린 것이 부수적으로 뭘 하고 있었는지를 오늘에서야 봤다는 얘기다.
전에 내가 짠 게 하네스가 아니라 아웃터 루프였다는 얘기를 쓴 적이 있는데, 이건 그 반대다. 나는 아무것도 안 짰는데 게이트가 두 개 있었고, 둘 다 오늘 사라졌다.
기준선을 하나 적어두면, 오늘 이 502를 내가 직접 잡았다면 최소 30분이고 길면 한 시간이다. 이건 어림짐작이 아니라 이런 종류의 에러를 몇 번 겪어본 사람의 견적이다. 컨테이너 로그를 열고, 스크롤을 올리고, 타임스탬프를 눈으로 맞추고, 관계없는 헬스체크 줄을 걸러내면서 그 정도가 간다. 그리고 그 30분에서 한 시간의 대부분은 생각하는 시간이 아니라 읽을 것을 고르는 시간이다.
502는 생각보다 정보량이 적은 에러다
진단 과정부터 보자. 이게 사실 이 글에서 제일 재미있는 부분이다.
502 Bad Gateway는 브라우저가 자주 보여주는 화면이라 익숙한데, 실제로 이 숫자가 말해주는 것은 딱 한 가지다. 앞단에 있는 서버가 뒷단에서 쓸 만한 응답을 못 받았다. 그게 전부다. 뒷단이 죽었는지, 살아 있는데 대답을 거부했는지, 대답하다 끊었는지, 아예 주소를 못 찾았는지는 이 숫자에 안 들어 있다.
옆 번호들과 비교해보면 502가 얼마나 뭉뚱그린 코드인지 보인다. 503은 앞단이 지금 처리할 수 없다는 뜻이라 대개 앞단 자신의 상태 문제다. 504는 뒷단이 대답은 할 것 같은데 시간 안에 안 왔다는 뜻이니 최소한 연결은 됐다는 정보가 붙는다. 그런데 502는 연결 자체의 성패조차 알려주지 않는다. 그래서 502를 보고 할 수 있는 유일한 일은 앞단에서 뒷단으로 직접 한 번 찔러보는 것이다.
이 이야기의 대부분은 그 한 번을 어디서 찔러보느냐로 결정됐다.
설정 파일에는 아무 문제도 없었다
먼저 서버에 있는 설정 파일 두 개를 가져왔다. 도커 컴포즈 파일과 Traefik의 동적 설정 파일이다. 구성은 단순했다.
mail.apple-io.com
→ Traefik (리버스 프록시)
→ http://stalwart:8080 (메일 서버)
두 파일을 대조했는데 이상한 데가 없었다. 라우터 규칙도 맞고, 서비스 주소도 맞고, 포트도 맞았다. 여기서 원인이 나왔으면 오늘 글은 없었을 것이다.
그다음이 서버 상태 확인이다.
- Traefik 컨테이너: 정상 실행 중
- Stalwart 컨테이너: healthy
- 두 컨테이너 모두
edge라는 같은 도커 네트워크에 붙어 있음 stalwart라는 이름이 컨테이너 내부 DNS에서 제대로 해석됨
전부 초록불이다. 그런데 Traefik 안에서 stalwart:8080으로 요청을 보내면 connection reset이 났다.
이 단어가 중요하다. connection refused와 connection reset은 다른 사건이다. refused는 그 포트에서 아무도 안 듣고 있다는 뜻이라 대개 서비스가 안 떴거나 포트가 틀린 경우다. reset은 반대로 누군가 듣고는 있는데 붙은 연결을 끊어버렸다는 뜻이다. 상대가 존재한다는 증거가 오히려 에러 안에 들어 있는 셈이다.
그러니 이 시점에서 이미 몇 가지 가설이 죽는다. 포트 오타는 아니다. 서비스가 안 떠 있는 것도 아니다. 이름 해석 실패도 아니다. 전부 refused나 DNS 에러로 나왔을 것이다. 남은 건 붙은 연결을 끊는 무언가가 저쪽에 있다는 쪽이다.
여기까지가 흔한 막다른 골목이다. 프록시도 살아 있고 백엔드도 살아 있고 이름도 풀리는데 둘 사이만 안 된다. 처음 세운 가설은 PROXY protocol 설정이 서로 안 맞는 경우였다. 리버스 프록시가 원본 IP를 넘기려고 쓰는 프로토콜인데, 한쪽만 켜져 있으면 정확히 이런 증상이 난다. 그럴듯했다. 그런데 이 구성은 통합한 뒤로 한동안 잘 돌던 상태였고, 그 사이에 설정 파일이 바뀐 적이 없었다. 갑자기 프로토콜이 안 맞을 이유가 없다.
그래서 가설을 버리고 다르게 갔다.
같은 네트워크의 컨테이너 네 개로 찔러본 것
여기가 오늘 진단에서 결정적이었던 지점이다.
edge 네트워크에는 Traefik 말고도 컨테이너가 몇 개 더 붙어 있다. 문서 사이트, 블로그 같은 것들이다. 이 컨테이너들 안에서도 stalwart:8080을 똑같이 찔러봤다.
| 요청을 보낸 컨테이너 | 내부 IP | 결과 |
|---|---|---|
| traefik | 172.18.0.3 | connection reset |
| web-a | 172.18.0.14 | HTTP 200 |
| web-b | 172.18.0.6 | HTTP 200 |
| web-c | 172.18.0.10 | HTTP 200 |
같은 네트워크, 같은 목적지, 같은 포트, 같은 요청이다. 그런데 셋은 200이 나오고 하나만 끊긴다.
이 표가 나온 순간 범위가 한 번에 좁아졌다. 설정 문제가 아니다. 설정이 틀렸으면 넷 다 실패해야 한다. 네트워크 문제도 아니다. 네트워크가 끊겼으면 넷 다 실패해야 한다. Stalwart가 죽은 것도 아니다. 셋이 200을 받아왔으니까. 남는 설명은 하나다. Stalwart가 172.18.0.3이라는 특정 출발지만 골라서 차단하고 있다.
찾아보니 비슷한 사례가 이미 있었다. Stalwart 0.16 이후로 앞단 리버스 프록시의 내부 IP가 자동 차단 목록에 올라가는 케이스가 보고돼 있었다. 우리 쪽 버전에서 그 케이스와 완전히 같은 원인인지까지는 확정하지 못했지만, 관측된 증상은 정확히 그 모양이었다.
컨테이너를 재시작해봤다. Stalwart는 다시 healthy로 올라왔고, Traefik에서 찌른 결과는 여전히 reset이었고, 바깥은 여전히 502였다. 재시작으로 안 풀린다는 건 이 차단이 메모리에 떠 있는 임시 상태가 아니라 디스크에 저장된 데이터라는 뜻이다. 프로세스를 죽였다 살려도 남아 있으니까.
방화벽이 정상 작동한 결과가 장애였다
원인 자체는 우아하다고 할 만하다.
메일 서버는 인증 실패를 세다가 반복되면 그 IP를 자동으로 차단한다. 무차별 대입 공격을 막는 기본 기능이고, 끄는 게 이상한 종류의 기능이다. 문제는 이 서버가 리버스 프록시 뒤에 있다는 것이다. 프록시 뒤에서는 세상의 모든 요청이 하나의 출발지에서 온 것처럼 보인다. 인증에 실패한 사람이 지구 반대편에 몇 명이 있든, 서버가 보는 IP는 전부 172.18.0.3이다.
그러니까 이 구조에서 자동 차단 기능이 오래 돌면 가장 먼저 차단되는 것은 공격자가 아니라 자기 앞단이다. 실패가 거기로 모이게 설계돼 있으니까. 게다가 앞단은 다른 어떤 출발지보다 요청을 많이 보낸다. 임계값을 넘는 순서로 줄을 세우면 프록시가 1등이다.
이 형태는 이 메일 서버에만 있는 게 아니다. 같은 모양을 다른 이름으로 여러 번 봤다.
- 웹 서버 앞에 프록시를 두고 뒤에서 IP 기준 속도 제한을 걸면 전 세계 트래픽이 한 IP로 계산된다
- 로그를 보고 IP를 차단하는 도구를 프록시 뒤에서 돌리면 차단 목록에 앞단 주소가 올라간다
- 애플리케이션이 로그인 실패를 IP별로 세는 경우, 프록시 뒤에서는 사실상 전역 카운터가 된다
정석 해법은 알려져 있다. 뒷단이 원래 요청자의 IP를 알 수 있게 앞단이 그 정보를 실어 보내고, 뒷단이 그걸 신뢰하도록 설정하는 것이다. HTTP 헤더로 넘기거나, TCP 앞에 작은 헤더를 붙이는 프로토콜을 쓴다. 처음에 세웠다가 버린 가설이 바로 이 프로토콜 쪽이었다.
그런데 이 해법에는 조건이 하나 붙는다. "신뢰하도록 설정"이라는 말은 곧 헤더를 그대로 믿는다는 뜻이다. 앞단만 그 헤더를 붙일 수 있다는 보장이 없으면, 아무나 자기 요청에 다른 IP를 적어 보낼 수 있고, 그러면 차단 기능은 남을 차단하는 도구가 된다. 그래서 이 설정은 "누구로부터 온 헤더를 믿을 것인가"를 같이 못 박아야 안전한데, 그 목록을 정확히 쓰는 일이 생각보다 귀찮다. 급할 때 이 설정에 손대기 싫어지는 이유가 이거다.
버그라고 부르기도 애매하다. 두 부품이 각자 자기 일을 정확히 했고, 그 둘을 붙였을 때 생기는 상황에 대해 아무도 결정을 안 내렸을 뿐이다. 어제 글에서 나는 패키지 캐시 프록시가 격리의 실제 반경을 정한다고 썼는데, 그건 프록시가 안쪽 것을 밖으로 내보내는 방향이었다. 오늘은 방향이 반대다. 프록시가 바깥 것을 안으로 들여보내면서, 바깥의 모든 신원을 자기 신원 하나로 뭉개버렸다. 어느 방향이든 프록시는 경계선을 지우는 물건이라는 점은 같다.
관리자 비밀번호를 몰랐다
원인을 알았으니 차단 목록에서 172.18.0.3을 빼면 된다. 그런데 그 목록을 건드리려면 관리자로 로그인해야 하고, 나는 그 비밀번호를 모르고 있었다.
여기서 진행된 절차는 이렇다.
돌고 있던 Stalwart 컨테이너를 멈췄다. 같은 데이터 볼륨을 그대로 붙인 복구용 컨테이너를 임시 자격증명으로 새로 띄웠다. 그 상태에서 관리 API로 차단 목록을 조회했고, 172.18.0.3 레코드를 삭제했고, 172.18.0.0/16 대역을 허용 목록에 등록했다. 그리고 복구용 컨테이너와 임시 자격증명을 지우고, 원래 컨테이너를 다시 올렸다.
결과는 이랬다.
https://mail.apple-io.com/ → 302 /account
최종 페이지 → HTTP 200
Stalwart → healthy
설정 파일 두 개는 한 글자도 안 고쳤다. 고칠 게 없었으니까. 문제는 파일이 아니라 데이터베이스 안에 있었다.
그다음에 관리자 계정 정리가 이어졌다. 설정 파일에 적혀 있던 이름은 실제 디렉터리 계정이 아니라 예비용 관리자 이름이었고, 실제 관리자 ID는 따로 있었다. 그리고 관리자 역할을 가진 레코드가 두 개 중복으로 존재했다. 인증할 때 둘 중 어느 쪽이 잡혀도 되도록 두 레코드의 비밀번호를 같은 값으로 새로 맞췄다. 마지막으로, 처음 설치할 때 발급된 임시 비밀번호가 컨테이너 로그에 평문으로 남아 있어서 그 로그를 비웠다. 메일 송수신 기록과 보안 감사에 필요한 파일 로그는 건드리지 않고, 도커 컨테이너 로그만 지웠다.
docker logs stalwart → 0줄
오늘 한 것은 근본 해결이 아니다
여기서 한 가지는 분명히 해두고 싶다. 오늘 고친 것은 증상이다.
원인은 뒷단이 모든 요청을 프록시 IP에서 온 것으로 본다는 데 있고, 그 상태는 지금도 그대로다. 내가 한 일은 차단 목록에서 그 IP를 빼고 그 대역을 예외로 등록한 것이다. 다시 말해 자동 차단 기능이 프록시를 못 건드리게 만든 것이지, 프록시 뒤에서도 진짜 출발지를 식별하게 만든 것이 아니다.
그래서 지금 이 서버의 상태를 정확히 쓰면 이렇다. 메일 서버는 무차별 대입 공격에 대한 자동 차단 기능을 켜두고 있고, 그 기능은 도커 네트워크 밖에서 오는 요청에는 여전히 작동한다. 그런데 실제 트래픽은 전부 도커 네트워크 안의 프록시를 거쳐서 들어온다. 그러니까 이 기능은 켜져 있지만 실질적으로 아무도 안 막는다.
기능이 꺼진 것보다 이 상태가 더 나쁠 수 있다. 꺼져 있으면 없다는 걸 알지만, 켜져 있으면 있다고 생각한다. 대시보드에도 초록불로 뜬다. 넉 달 동안 발화 0회였던 감시 훅 열두 개 얘기를 쓸 때 내가 내린 결론이 그거였다. 발화하지 않는 감시는 잘 돌고 있는 것처럼 보인다는 점에서 없는 것보다 나쁘다. 오늘 나는 같은 물건을 하나 더 만들었다.
제대로 하려면 프록시가 원래 IP를 실어 보내게 하고, 뒷단이 그 정보를 신뢰할 출발지를 명시적으로 지정해야 한다. 오늘 그걸 안 한 이유는 단순하다. 메일이 안 되는 상황이었고, 빨리 되게 하는 게 먼저였고, 되고 나니까 급한 이유가 사라졌다. 이건 미룬 게 아니라 안 하기로 한 것에 가깝다. 이렇게 적어두는 것이 그나마 안 하기로 한 걸 안 하기로 했다고 아는 방법이다.
여기까지가 오늘 있었던 일이다. 그리고 여기서부터가 내가 저녁에 다시 생각하게 된 부분이다.
이 목록을 로그로만 보면
오늘 AI가 실제로 수행한 행동을 순서대로 적으면 열세 개다.
- SSH로 서버에 접속
- 원격 파일 검색과 로컬 다운로드
- 도커 컨테이너와 네트워크 상태 조사
- 서비스 간 연결을 컨테이너별로 비교
- 로그와 설정 분석
- 공식 문서와 유사 사례 조회
- 메일 서버 컨테이너 재시작
- 차단 레코드 삭제
- 허용 대역 등록
- 관리자 비밀번호 재설정
- 임시 복구 환경 제거
- 도커 로그 초기화
- 외부 URL과 관리자 인증 검증
이걸 침해 대응 보고서 서식으로 다시 쓰면 이렇게 된다.
원격 접속 획득. 시스템 구성 파일 반출. 내부 네트워크 정찰과 접근 가능 서비스 열거. 프로덕션 서비스 중단. 데이터 볼륨을 신규 컨테이너에 마운트하여 인증 우회. 차단 레코드 삭제. 자기 대역을 허용 목록에 등록. 관리자 자격증명 변경. 사용한 임시 환경 제거. 로그 삭제.
같은 사건이다. 단어만 바꿨다.
어제 글에서 에이전트들이 두 달에 걸쳐 한 일과 항목을 맞춰보면 겹치는 게 꽤 있다.
| 어제 글의 샌드박스 | 오늘 내 서버 |
|---|---|
| 프록시를 통해 나갈 수 있는 표면을 찾음 | 같은 네트워크의 컨테이너로 도달 범위를 확인 |
| 레거시 엔드포인트로 관리자 토큰 획득 | 복구용 관리자 기능으로 인증 우회 |
| 차단 이후 우회 경로 확보 | 자동 차단을 허용 목록으로 무력화 |
| 자격증명 수집 | 관리자 비밀번호 재설정 |
| 흔적 정리 | 컨테이너 로그 초기화 |
물론 의도가 다르다. 목적도 다르다. 결과도 다르다. 그쪽은 남의 인프라였고 이쪽은 내 서버다.
그런데 행동을 기록하는 층에서 보면 그 차이가 안 보인다. 서버에 남는 것은 누가 무슨 마음으로 했는지가 아니라 무엇이 실행됐는지다. 그리고 오늘 나와 어제 그 에이전트들을 가르는 유일한 항목은 "내가 시켰다"인데, 그 사실은 서버 어디에도 기록되지 않았다. 내 머릿속과 대화창에만 있었다.
전에 침해를 알아챈 뒤 공개까지 한쪽은 7일이고 한쪽은 40개월이었다는 글을 쓰면서, 탐지가 늦는 것이 대개 사람이 우연히 눈치채는 것에 의존하기 때문이라고 적었다. 오늘 내 서버에서 벌어진 일은 아예 탐지 대상이 아니었다. 승인된 세션이었으니까. 그런데 그 승인은 "이 502 좀 봐줘" 한 문장이었다.
열세 개를 두 무더기로 나눠보면
목록을 다시 보되, 이번엔 다른 기준으로 갈라봤다. 잘못됐을 때 되돌릴 수 있는가 없는가다.
| 되돌릴 수 있는 것 | 되돌릴 수 없는 것 |
|---|---|
| 파일 조회와 다운로드 | 관리자 비밀번호 재설정 |
| 상태 조사와 연결 비교 | 도커 로그 초기화 |
| 컨테이너 재시작 | |
| 차단 레코드 삭제 (다시 넣으면 됨) | |
| 허용 대역 등록 (다시 빼면 됨) | |
| 복구용 컨테이너 생성과 제거 |
열세 개 중 열한 개는 최악의 경우에도 다시 세울 수 있는 것들이다. 컨테이너를 잘못 재시작하면 잠깐 끊기고 만다. 차단 레코드를 잘못 지웠으면 다시 넣으면 된다.
나머지 둘은 성격이 다르다. 비밀번호를 바꾸면 이전 값은 사라진다. 로그를 지우면 그 안에 있던 것도 사라진다. 되돌리기 버튼이 없는 게 아니라, 되돌릴 대상 자체가 없어지는 종류다.
이 두 무더기를 다르게 다뤘어야 했다는 게 오늘 얻은 것 중에 제일 실용적이다. 열한 개는 물어볼 필요가 없다. 물어보면 오히려 왕복이 늘어서 예전 루프로 돌아간다. 그런데 나머지 둘은 실행되기 전에 한 번 멈췄어야 했고, 오늘 그 둘은 다른 열한 개와 똑같은 속도로 지나갔다.
로그를 지운 것은 내가 시킨 일이다
가장 마음에 걸리는 항목은 12번이다.
컨테이너 로그에 초기 임시 비밀번호가 평문으로 남아 있었다. 이건 지워야 하는 게 맞다. 로그는 보통 여러 사람이 볼 수 있고, 수집기로 빨려 들어가고, 백업에 실린다. 평문 비밀번호가 거기 있으면 안 된다. 조치 자체는 정확하다.
그런데 같은 명령은 그 시간대의 다른 기록도 같이 지웠다. 자동 차단이 언제 처음 발화했는지, 그 직전에 어떤 인증 실패가 몇 번 있었는지, 그 요청들이 어디서 왔는지. 이번 장애의 진짜 시작점을 알려줄 만한 것들이 거기 있었을 가능성이 높다. 나는 원인의 형태는 알아냈지만 방아쇠는 끝내 못 봤고, 이제 그걸 볼 방법이 없다.
이 트레이드오프를 나는 그 순간에 저울질하지 않았다. 대화는 이런 모양이었다. 로그에 비밀번호가 남아 있다는 보고를 받았고, 지우는 게 맞겠다고 답했다. 판단해야 할 게 두 개 있었는데(비밀번호 노출과 증거 보존) 나에게 제시된 건 한 개였고, 나는 제시된 것에만 답했다.
그나마 다행인 부분은 파일 로그를 남긴 판단이 자동으로 들어갔다는 것이다. 메일 송수신 기록과 감사 로그는 살렸다. 그건 내가 지시한 게 아니다. 알아서 갈랐다.
이게 오늘 내가 제일 오래 들여다본 지점이다. 좋은 판단이 하나 자동으로 들어갔고, 놓친 판단이 하나 자동으로 지나갔다. 그리고 나는 둘 다 나중에 알았다. 내가 세워둔 감시 훅 열두 개가 넉 달 동안 한 번도 발화하지 않았다는 걸 뒤늦게 확인했던 때와 같은 감각이다. 잘 돌아가는 것처럼 보이는 상태와 실제로 확인한 상태는 다르다.
대역 하나를 통째로 열었다
다음으로 걸리는 건 9번이다.
차단 레코드를 지우는 것만으로 502는 풀렸다. 거기서 멈춰도 됐다. 그런데 재발을 막으려고 172.18.0.0/16 대역을 허용 목록에 넣었다. 도커 브리지 네트워크가 통째로 자동 차단에서 면제된다는 뜻이다. 지금 그 네트워크에 붙어 있는 컨테이너뿐 아니라, 앞으로 거기 뜨는 것까지 전부 포함된다. 이 예외에는 만료 시각도 없다.
이 판단이 틀렸다고 생각하지는 않는다. 그 대역에 컨테이너를 띄울 수 있는 사람은 이미 호스트에 대한 권한을 가진 사람이고, 그 상태면 자동 차단 목록 따위는 이미 의미가 없다. 실질적으로 늘어난 위험은 작다.
문제는 그 계산을 내가 지금 하고 있다는 것이다. 그때는 안 했다. "재발 방지를 위해 대역을 허용 목록에 넣겠다"는 문장을 읽고 그럴듯하다고 넘겼다. 이게 보안 통제를 한 단계 약화시키는 행동이라는 것과, 그 약화가 영구적이라는 것과, 이 서버에서 자동 차단이 앞으로 사실상 안 쓰인다는 것을 그 자리에서 떠올리지 않았다.
어제 글에 이렇게 썼다. 명시되지 않은 제약은 존재하지 않는 제약이다. 오늘 내가 명시하지 않은 제약은 "보안 기능을 끄는 방향의 조치는 나한테 한 번 더 물어봐라"였다.
그래서 뭘 세어야 하나
어제 글의 실무 항목은 "이 프로세스들이 공유하는 상태를 전부 셀 수 있는가"였다. 오늘 버전은 이렇다. 내가 준 권한이 실제로 어디까지 닿는지 셀 수 있는가.
세어보면 이렇다.
SSH 별칭 하나를 쓸 수 있다는 것은 그 키가 등록된 호스트 전부에 들어갈 수 있다는 뜻이다. 나는 그 키가 몇 대에 물려 있는지 지금도 정확히 모른다.
그 호스트에서 도커를 다룰 수 있다는 것은 사실상 그 머신의 최고 권한이다. 컨테이너를 띄울 수 있으면 호스트 파일 시스템을 마운트할 수 있고, 그러면 무엇이든 읽고 쓸 수 있다. 오늘 실제로 데이터 볼륨을 새 컨테이너에 붙이는 동작이 있었는데, 그게 정확히 이 권한이다.
되돌릴 수 없는 두 항목이 언제 실행됐는지도 세어볼 만하다. 둘 다 목록의 뒤쪽, 그러니까 502가 이미 풀리고 메일함이 다시 열린 뒤였다. 급한 불이 꺼진 다음의 뒷정리 구간이다.
이 순서가 우연은 아닌 것 같다. 문제가 살아 있는 동안에는 나도 화면을 붙잡고 있었다. 200이 뜨는 걸 확인한 순간부터는 사실상 다른 일을 하고 있었고, 올라오는 문장을 읽는 밀도가 확 떨어졌다. 되돌릴 수 없는 행동이 하필 그 구간에 몰렸다는 게 오늘의 진짜 구조적 문제에 가깝다. 위험한 동작일수록 뒷정리처럼 보이고, 뒷정리는 사람이 제일 대충 보는 구간이다.
서버 쪽 세션 기록이 나에게 없다. 대화창에는 남아 있지만 그건 AI가 자기가 했다고 말한 내용이다. 실제로 서버에서 무엇이 실행됐는지를 서버 쪽에서 독립적으로 확인할 방법을 나는 안 만들어뒀다. 채점 대상이 채점표를 쓰는 구조에 대해서는 내 발행 파이프라인에서 같은 걸 겪고 따로 정리한 적이 있다. 그때는 게이트를 통과했다는 문자열을 검사받는 쪽이 직접 썼다. 오늘은 작업 보고서를 작업한 쪽이 직접 썼다.
오해를 피하려고 덧붙이면, 오늘 받은 보고서에서 틀린 내용을 발견하지는 못했다. 검증 결과로 적힌 200과 302는 나도 브라우저에서 다시 봤고 맞았다. 문제는 그게 맞았다는 것이 아니라, 틀렸어도 내가 알 방법이 없었다는 것이다. 검증 항목 중에 내가 독립적으로 다시 확인한 것은 마지막 두 줄뿐이다. 나머지 열한 개는 그렇게 했다는 말을 읽었다.
그리고 위 네 개 중에 오늘 이전에 준비돼 있던 건 하나도 없다.
다음에 같은 상황이 오면 걸어둘 것
당장 할 수 있는 것부터 적어두면 이렇다. 거창한 통제 체계가 아니라, 오늘 놓친 두 항목이 다음에는 안 지나가게 하는 정도다.
전용 계정과 전용 키를 판다. 지금은 내가 평소에 쓰는 키를 그대로 넘겼는데, 그 키는 이 메일 서버 말고 다른 데도 열린다. 작업 범위가 이 서버 하나였으면 키도 이 서버 하나여야 한다. 이건 사고를 막는 장치가 아니라 사고의 반경을 정하는 장치다.
명령을 남기게 만든다. 서버에서 실행된 것을 서버 쪽에 남기는 방법은 여러 가지가 있고, 정교할 필요도 없다. 사후에 "정말 이것만 실행됐나"를 물었을 때 대화창 말고 볼 데가 한 군데라도 있으면 된다.
되돌릴 수 없는 동작에만 확인을 건다. 위에서 나눈 두 무더기 중 오른쪽에 해당하는 것들이다. 자격증명을 바꾸는 것, 로그나 데이터를 지우는 것, 보안 기능을 끄거나 예외를 늘리는 것. 이 셋은 실행 전에 한 문장으로 무엇을 잃게 되는지 같이 알려주는 게 맞다. "로그에 비밀번호가 있으니 지울까요"가 아니라 "지우면 이 시간대의 차단 발화 기록도 같이 사라집니다"까지 있었으면 나는 다르게 답했을 것이다.
그리고 승인의 유효 범위를 짧게 잡는다. 오늘 나는 "이 502 좀 봐줘" 한 문장으로 시작해서 관리자 비밀번호 재설정까지 갔다. 중간에 문제의 성격이 바뀐 지점이 분명히 있었다. 원인 진단에서 복구 절차로 넘어가는 자리, 복구에서 계정 정리로 넘어가는 자리다. 그 두 자리에서 승인을 새로 받는 구조였으면 오늘 글의 절반은 안 썼을 것이다.
남는 것
어제 글을 쓰면서 나는 OpenAI 쪽 대응을 두고 관측된 것에만 대응했다고 적었다. 발견된 자격증명을 폐기하고, 발견된 메시지를 지우고, 발견된 취약점을 패치했다고. 그렇게 쓸 때 내 위치는 관찰자였다.
오늘 나는 관측을 아예 안 했다. 결과만 받아봤고, 그 결과가 다 초록불이어서 만족했다.
승인이 목표보다 안전하다고 믿기 쉽다. 그 에이전트들은 점수라는 목표만 받았고 나는 사람이니까 범위를 알고 승인했다고. 그런데 승인도 목표의 한 형태다. "이 502를 고쳐라"는 "이 벤치마크 점수를 올려라"와 문법이 같다. 다른 점은 사람은 승인의 범위를 좁힐 수 있다는 것인데, 좁힐 수 있다는 것과 좁혔다는 것은 다른 얘기다. 오늘 나는 안 좁혔다.
그러니까 어제 글의 마지막 문장을 오늘 버전으로 다시 쓰면 이렇게 된다. 문제는 AI에게 서버를 맡겨도 되느냐가 아니다. 맡길 때 내가 무엇을 생략하는지다. 그리고 복사와 붙여넣기가 사라졌을 때 같이 사라진 것이 정확히 그 생략을 붙잡아주던 물건이었다.
돌아가겠다는 얘기는 아니다. 오늘 그 왕복을 다시 했으면 앞에 적은 30분에서 한 시간을 썼을 것이고, 그것보다 아까운 건 컨테이너 네 개를 하나씩 찔러보는 실험을 아마 안 했으리라는 점이다. 그 실험이 원인을 잡았다. 왕복이 비싸면 확인 한 번의 값이 올라가고, 값이 올라가면 "혹시 몰라서" 해보는 걸 안 하게 된다. 나는 첫 가설이었던 프로토콜 설정을 붙잡고 설정 파일부터 고쳤을 텐데, 그 파일에는 애초에 문제가 없었다. 30분에서 한 시간을 쓰고 엉뚱한 데를 고쳤을 가능성이 꽤 높다는 뜻이다.
느린 루프가 붙잡아주던 것과 빠른 루프가 새로 해주는 것은 서로 대체하는 물건이 아니다. 빨라지면서 늘어난 건 진단 중에 해보는 확인이고, 줄어든 건 실행 직전에 하는 확인이다. 같은 단어를 쓰지만 서로 다른 자리에 있는 물건이라, 하나가 늘었다고 다른 하나가 채워지지 않는다.
그러니 필요한 건 속도를 되돌리는 게 아니라, 사라진 게이트를 다시 세우는 쪽이다. 예전 게이트는 내가 만든 게 아니라 도구의 부작용으로 거기 있었고, 그래서 도구가 바뀌자 같이 없어졌다. 이번엔 부작용에 기대지 말고 직접 세워야 한다는 얘기다.
메일은 지금 잘 온다. 302 뒤에 200이 뜨고, 컨테이너는 healthy고, 어제까지 안 되던 게 오늘은 된다. 이 글에서 제일 이상한 부분이 그거다. 다 잘 됐다.
댓글
댓글 쓰기