cron 이 새벽 세 시에 curl 을 했다 · 남는 자리와 남지 않는 자리 · 이론
누가 무엇을 했는지는 어디에 남는가
한 줄 요약
탐지는 규칙에서 시작하지 않는다. 그 사건이 애초에 어딘가에 기록되는가에서 시작한다. 리눅스 호스트에서 그 자리는 세 곳이고, 세 곳이 보는 것은 겹치지 않는다 — 커널 감사(auditd)는 시스콜을, journald 는 프로그램이 스스로 말한 것을, 쿠버네티스 감사 로그는 API 서버에 들어온 요청을 본다.
왜 이게 필요했나
새벽 세 시에 예약 작업이 바깥으로 요청을 보냈다고 하자. 아침에 출근해서 이 문장을 증명하려면 네 가지를 알아야 한다. 무엇이 실행됐는가, 누가 그것을 예약했는가, 언제 그 예약이 생겼는가, 그리고 그 요청이 어디로 갔는가.
여기서 사람들이 가장 자주 부딪히는 벽은 "로그를 다 모으고 있는데 답이 없다" 이다. 로그를 모으는 일과 질문에 답할 수 있는 기록을 남기는 일은 다르기 때문이다. /var/log/syslog 에는 cron 이 무엇을 실행했는지가 남지만, 그 cron 파일을 누가 언제 만들었는지는 없다. 반대로 커널 감사는 파일이 만들어진 순간을 정확히 남기지만, 감시 규칙을 걸어 두지 않은 경로는 아예 보지 않는다.
그래서 탐지 엔지니어링의 첫 일은 규칙 작성이 아니라 원천 조사다. 질문을 먼저 적고, 그 질문에 답할 수 있는 기록이 지금 남고 있는지를 하나씩 확인한다. 남지 않는다면 그것을 남기게 만드는 것이 첫 번째 작업이고, 그 뒤에야 규칙을 쓸 자격이 생긴다.
어떻게 동작하나
커널 감사 서브시스템은 규칙 두 종류를 받는다. 하나는 파일 감시이고 하나는 시스콜 필터다.
-w /etc/cron.d -p wa -k cron_change └ 경로 └ 권한 └ 검색용 키 이 디렉터리에 쓰기(w)나 속성 변경(a)이 일어나면 남긴다-a always,exit -F arch=b64 -S execve -F exe=/usr/bin/curl -k outbound_exec └ 언제 └ 아키텍처 └ 시스콜 └ 추가 필터 └ 키 64비트 execve 중 실행 파일이 curl 인 것만 남긴다-w 는 사실 시스콜 규칙의 줄임 표현이고, -k 로 붙인 키가 나중에 ausearch -k 의 검색 열쇠가 된다. 규칙 문법과 필터 필드는 [auditctl(8)](https://man7.org/linux/man-pages/man8/auditctl.8.html) 에, 규칙 파일에 적는 형식은 [audit.rules(7)](https://man7.org/linux/man-pages/man7/audit.rules.7.html) 에 있다. 손으로 auditctl 을 쳐서 넣은 규칙은 재부팅하면 사라지므로, 살아남게 하려면 /etc/audit/rules.d/ 에 적고 [augenrules(8)](https://man7.org/linux/man-pages/man8/augenrules.8.html) 이 그것을 합쳐서 올리게 한다. 읽을 때는 [ausearch(1)](https://man7.org/linux/man-pages/man8/ausearch.8.html) 이 키·사용자·시간으로 잘라 주고, [aureport(8)](https://man7.org/linux/man-pages/man8/aureport.8.html) 이 집계를 낸다.
journald 는 성격이 완전히 다르다. 커널이 관찰하는 것이 아니라 프로그램이 스스로 말한 것을 받아 적는다. 그래서 cron 이 남기는 (root) CMD (…) 한 줄은 cron 이 친절해서 있는 것이지 커널이 보장하는 것이 아니다. 대신 journald 는 구조화된 필드를 함께 갖는다 — _PID, _UID, _SYSTEMD_UNIT, SYSLOG_IDENTIFIER 같은 것들이고, 목록은 [systemd.journal-fields(7)](https://www.freedesktop.org/software/systemd/man/latest/systemd.journal-fields.html) 에 있다. journalctl -o json 으로 뽑으면 이 필드가 그대로 나와서 기계가 읽을 수 있다([journalctl(1)](https://www.freedesktop.org/software/systemd/man/latest/journalctl.html)).
쿠버네티스는 또 다른 층이다. API 서버 앞에서 벌어진 일은 호스트 로그에 남지 않는다. 감사 로그는 [기본으로 꺼져 있고](https://kubernetes.io/docs/tasks/debug/debug-cluster/audit/), --audit-policy-file 로 정책을 줘야 켜진다. 정책은 규칙마다 수준을 고르는데 None 은 남기지 않고, Metadata 는 요청자·시각·리소스·동사만, Request 는 요청 본문까지, RequestResponse 는 응답 본문까지 남긴다. 단계도 넷이다 — RequestReceived, ResponseStarted, ResponseComplete, Panic. 이 선택이 그대로 비용이자 증거의 해상도다.
현장에서 만나는 모습
첫 번째 모습은 "규칙은 있는데 자리가 틀린" 경우다. /etc/cron.d 는 감시하는데 /usr/local/bin 은 감시하지 않는 호스트가 흔하다. 공격자는 예약 작업을 새로 만들 필요가 없다 — 이미 예약돼 있는 스크립트의 내용을 한 줄 고치면 된다. 그 순간 감사 로그는 조용하다. 조용한 것이 안전하다는 뜻이 아니라, 그 자리를 보고 있지 않다는 뜻이다.
두 번째는 "기록은 있는데 못 읽는" 경우다. 커널 감사는 사람이 읽기 어려운 숫자로 남는다. uid 는 숫자이고 시스콜도 숫자다. ausearch -i 가 그것을 이름으로 바꿔 주는데, 이 옵션을 모르고 원본만 들여다보다 "우리 감사 로그는 쓸모없다" 는 결론에 도달한 팀을 여러 번 봤다.
세 번째는 쿠버네티스 쪽이다. 노드가 뚫리면 그 노드의 kubelet 인증서가 함께 나간다. 호스트 로그는 프로세스가 파일을 읽은 것까지만 보여 주고, 그 인증서로 클러스터에서 무엇을 했는지는 API 서버 감사 로그에만 있다. 두 기록을 이어 붙이지 못하면 "파일 하나 읽혔다" 에서 조사가 끝난다.
다음 실습에서 할 것
실습에서는 VM 한 대에 직접 들어가 감사 규칙을 걸고, 사건을 일부러 일으키고, 그 기록을 ausearch 로 꺼내 본다. 그다음 같은 사건을 journald 쪽에서 다시 찾아 두 기록이 무엇을 각각 갖고 있는지 비교한다. 마지막에는 감시하지 않는 자리를 하나 골라 기록이 남지 않는 것을 직접 확인하고, 그 자리를 덮는 규칙을 새 키로 추가한다. 파드에는 커널 권한이 하나도 없어서 이 실습은 VM 에서 돈다.