Self-hosted 고스트 매직링크를 이용한 메일 스푸핑 방지
Self-Hosted 고스트
예전에 티스토리에서 블로그 이사를 할 때 워드프레스와 고스트 사이에서 고민을 하다가 결국 고스트를 선택했습니다. 전통의 워드프레스도 장점이 많지만 아무리 봐도 정이 안가는 PHP 비중이 높아서 javascript로 만들어진 고스트를 선택했는데 UI도 깔끔하면서 있을 거 다 있어서 현재까지 별 문제 없이 사용하고 있었습니다.
이 사건이 있기 전까지는...
배경지식
이 글은 Self-Hosted 고스트를 운영하면서 겪은 메일 스푸핑 문제와 해결 경험을 공유하기 위함입니다. 아마도 Self-Hosted로 고스트를 설치해서 사용하시는 분들 중에 컴덕이나 개발자분들이 많아서 알아서 잘 하시리라 생각되지만 그래도 혹시 필요한 분들이 계실까봐 가급적 자세히 적어봅니다. 다행히 금방 처리한 편이라 메일 평판에 큰 피해없이 해결되었습니다.
매직링크 로그인
고스트 블로그를 구독하거나 댓글을 작성하기 위해서는 로그인을 해야 합니다. 로그인 방식 중에 메일인증을 이용한 방식이 한 때 유행했는데 고스트에서는 이러한 메일 인증 방식을 사용합니다. 인증을 요청하면 등록된 메일로 매직링크가 전송되고 이 매직링크를 클릭하면 다시 고스트 블로그로 이동하면서 로그인이 됩니다.
매일 스푸핑
메일 스푸핑은 공격자가 이메일의 발신자 주소를 위조하여 수신자가 이메일을 신뢰하도록 속이는 행위입니다. 최근 고스트의 매직 링크를 악용해 중계 서버를 거쳐 제3자에게 메일을 보내는 악용 행위가 있었습니다. 이렇게 내 메일 주소로 해커가 스팸 메일을 보내게 되면 메일 평판에 악영향이 갑니다.
고스트 설치 환경
서버 : 오라클 클라우드 A1
OS : ubuntu 22.04 (arm64)
고스트 설치 : Docker compose
SMTP 메일 : Zoho 도메인 메일
문제발견과 해결 과정
이상한 이메일 보고서
언젠가부터 Zoho로부터 이상한 메일 보고서와 왔습니다. 전송 실패는 알리는 메일이었습니다.
전화번호가 메일 주소가 되는 캐나다의 이메일 주소로 수많은 메일을 보내고 있었고, 메일 전송에 실패해서 보고서를 보낸 것입니다.
Zoho 메일 관리자 계정으로 로그 확인
Zoho 메일 관리자로 로그인해서 로그를 살펴봤더니 의외로 전송에 성공한 메일이 많았습니다. 빨리 처리하지 않으면 안되겠다 싶었습니다.
기존 보안 확인
참고로 제 도메인 메일은 SPF, DKIM, DMARC 전부 보안 설정이 되어 있었습니다. Zoho메일의 "피싱 및 맬웨어 - 표시 이름 스푸핑 방지"는 안되어 있어서 이번에 하긴 했는데 로그를 보니 메일을 보낸 주소가 제 주소라서 별 효과가 없을 것 같습니다.
고스트 매직링크를 이용한거라 SMTP 계정 자체가 털린 건 아니었습니다. 중계 서버도 아주 다양하게 사용하고 있어서 Zoho의 기능을 이용하는 것 말고 발신 요청 자체를 막아야겠다 싶었습니다.
메일 내용 확인
보낸 메일이 모든 보안 필터를 통과했기 때문에 보낸 내용도 확인할 수 있는데 정상적인 로그인 요청 내용말고는 별 내용이 없었습니다. 그냥 단지 요청한 적이 없는 매직링크를 제3자가 요청한 것과 같은 현상입니다. 아마도 해커가 스푸핑메일 전송을 위해 중계서버 테스트 하는 과정에서 내용을 변경하는 걸 구현 못한 거 같습니다.
해결 과정
아래는 요청 자체를 막기 위한 해결 과정입니다.
(우선)고스트 업데이트
도커로 설치한 고스트를 최신 버전으로 업데이트 했습니다. 당시 별 효과는 없었습니다. 릴리즈 노트를 보니 이메일 스팸 차단에 관한 내용이 있긴 합니다.
5.106.2, 5.107.1, 5.108.1에 " Blocked spammy email domains in member signups"라는 내용이 있습니다. 해결이 한 번에는 안됐나 봅니다.
아마도 최신 버전으로 업데이트만 해도 대부분 문제는 해결됐을 거 같습니다. 저는 아직 5.107사용중입니다. 이 문제를 해결했던 당시의 버전은 더 낮았을 겁니다.
고스트 로그 확인
도커에 접속해서 로그를 살펴봤습니다. 도커에 접속
docker compose exec ghost bash
로그 중 매직링크 요청만 확인
cat /var/lib/ghost/content/logs/https___bonik_me_production.log | grep "members/api/send-magic-link"
로그 중 의심가는 요청
{"name":"Log","hostname":"f5________","pid":1,"level":30,"version":"5.89.1","req":{"meta":{"requestId":"00000000-f000-0000-0000-00000000000","userId":null},"url":"/api/send-magic-link","method":"POST","originalUrl":"//members/api/send-magic-link","params":{},"headers":{"host":"bonik.me","x-forwarded-scheme":"https","x-forwarded-proto":"https","x-forwarded-for":"123.123.123.123","x-real-ip":"123.123.123.123","content-length":"254","content-type":"application/json","accept":"*/*","accept-encoding":"gzip, deflate","user-agent":"Python/3.12 aiohttp/3.9.5"},"query":{}},"res":{"_headers":{"x-powered-by":"Express","cache-control":"no-cache, private, no-store, must-revalidate, max-stale=0, post-check=0, pre-check=0","access-control-allow-origin":"*"},"statusCode":201,"responseTime":"1706ms"},"msg":"","time":"2024-08-16T21:25:25.122Z","v":0}
위 내용을 보고 어떤 식으로 요청했는지 단서를 찾아봤더니 User-Agent가 Python/3.12 aiohttp/3.9.5
인 점을 발견했습니다.
에이전트 차단
클라우드 플레어를 이용해서 차단하려고 했는데 클라우드 플레어의 proxy를 사용하지 않아서 다른 방법을 찾았습니다. robots.txt로 차단하려고 했는데 정규식을 지원하지 않아서 접었습니다. 가장 빠른 차단은 robots.txt로 처리하는 것도 괜찮을 거 같습니다.
그 밖에 방법을 고민하다가 Nginx Proxy Manager에서 하는 방법이 가장 손쉽겠다 생각하고 NPM에서 이를 차단했습니다. 클라우드 플레어에서 차단하는 방법도 괜찮을 것 같습니다.
/members/api/send-magic-link/
라는 경로로 "Python 숫자 + aiohttp 숫자"라는 요청이 있을 경우 이를 차단하도록 했습니다.
location /members/api/send-magic-link/ {
if ($http_user_agent ~* "Python/.+aiohttp/.+") {
return 403;
}
proxy_pass http://172.17.0.1:2368; # Ghost 서버
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
차단 확인
얼마 뒤 메일 로그를 살펴보니 요청이 차단되었다는 걸 확인했습니다. 해커가 유저에이전트를 변경해서 시도하면 다른 방법으로 막아야겠지만 그 전에 고스트 코어가 개선될 거 같습니다. 아직 설치해보지 않았지만 이미 처리됐을 거 같기도 합니다.