해킹 시도를 받다
프로젝트를 완성하고 VM 하나를 빌려 내 API 서버를 배포하였습니다.
근데 어느 날인가부터 아래 사진과 같이 누가 봐도 이상한 API 요청이 들어오기 시작했습니다.
아직 데이터도 많지 않은 서버라 안일하게 특별한 보안 장치를 두지 않아 이런 일이 발생했고
일단 개발이 급했기에 그냥 놔뒀는데 어느날 vm에 ssh 연결이 안 되는 이슈가 있었습니다.
정확히는 ssh 접근 시도를 하면 아무런 에러도 뜨지 않은채 계속 연결 시도 대기 상태에 멈춰있었습니다
뭔가 느낌이 쌔해서 aws 콘솔에 가서 네트워크 모니터링 로그를 보니
CPU 100%가 되어 있네요.. ㅎㅎ
무작위 요청을 하던 그놈 짓이구나 라는 걸 바로 알 수 있었고
일단 vm은 접속해야 하니 vm을 중단 후 재시작하여 연결하였습니다.
이후 access log를 확인해 보니 역시나 엄청나게 많은 해킹 시도 요청이 쌓여있더라고요.
그래서 이젠 미룰 수가 없겠다 하고 여러 방법을 찾기 시작했습니다.
이번 포스팅은 저와 같은 이슈를 겪고 계신 분들에게 조금이나마 도움이 되고자
제가 해당 이슈를 해결한 방법을 공유합니다!
일단 제가 처음 배포했던 서버 구조를 간단히 도식화하면 아래와 같았습니다.
1. 서버 도메인으로 접근
2. 로드밸런서가 단일 인스턴스에게 라우팅
3. API 서버 앞에 Gunicorn이 멀티 프로세스로 동작
4. fastapi가 uvicorn worker로 구동되어 처리
위 같은 구조였고 문제의 요점인 해킹 시도를 방어하기 위해 아래 내용을 적용하였습니다.
1. SSH 접속 IP 제한
이건 간단하고도 기본적인 것이지만 저는 이것조차 하지 않았습니다.
VM에 접속하는 SSH 연결 IP를 제한하는 것인데 따로 수정하지 않으셨다면 22 포트로 접근을 합니다.
방화벽에 가서 22 포트에 허용된 ip가 0.0.0.0 이라면 자신이 자주 접근하는 ip로 제한해주셔야 합니다.
아래 ssh 접근 시도한 내역을 볼 수 있습니다.
sudo last -f /var/log/btmp
아래는 제 내역이었는데 비밀번호마저 쉽게 해 놓았다면 충분히 뚫릴 가능성이 있었습니다. (반성..)
2. Rate Limit 적용하기
Rate limit는 네트워크에서 특정 유형의 트래픽이나 요청을 제한하는 방법입니다.
즉 API 호출이 가능한 범위를 설정해 놓고 이 범위를 초과하는 요청이 들어오면 429 (Too Many Requests)를 반환하는 방법입니다.
Rate Limit는 Nginx와 같은 웹 서버에 적용할 수도 있고 FastAPI와 같은 API 서버에 코드상으로 적용할 수도 있는데
저는 Gunicorn 앞 단에 Nginx를 두고 Nginx에 Rate limit를 적용하는 방법을 선택하였습니다.
그 이유는 여러 가지를 고려했는데
Nginx를 도입하지 않으면 공격 요청이 API 서버에 도달하게 되는데 그것조차 원하지 않았고
Nginx가 앞단에서 이런 요청을 거른 정상적인 요청들만 API 서버가 받도록 하고 싶었습니다.
또한 Nginx에서 ip를 기반으로 제한하기가 수월하다는 장점 때문에
기존 배포 구조에서 앞단에 Nginx를 붙이기로 했습니다.
그러기 위해서 일단 기존 VPC 방화벽에서 열어놨던 API 서버의 포트를 닫아버렸고
vm에 nginx를 설치하고 띄운 후 로드밸런서가 80 포트로 요청을 보내고
nginx는 80 포트로 들어온 요청을 API 서버 포트로 전달함으로 리버스 프록시 효과를 챙겼습니다.
이 글은 해킹 시도를 방어하는 방법을 설명하고 있기 때문에 구체적인 과정을 설명하진 않겠습니다.
이후 Nginx에 Rate Limit를 적용하기 위해
/etc/nginx/nginx.conf 파일에서 아래와 같이 수정했습니다.
http {
... other settings
limit_req_zone $binary_remote_addr zone=limit_per_ip:10m rate=1r/s;
... other settings
}
limit_req_zone은 nginx에 rate limit를 적용하는 옵션이며 각 값은 아래 의미를 가집니다.
- $binary_remote_addr | 클라이언트의 ip 주소를 이진 형식으로 변환하여 메모리 사용량을 줄이고 처리 속도를 향상
- zone=lmit_per_ip:10m | zone의 이름과 용량을 정의하는 부분으로 limit_per_ip라는 이름으로 10mb까지 저장할 수 있음
- rate=1r/s | 각 IP 별로 초당 1개의 요청을 허용한다는 것을 의미
그다음 서버 conf에 가서 아래와 같이 수정합니다.
저의 경우 서버 conf를 /etc/nginx/site-available/<프로젝트명>.conf 에 저장해 두었습니다.
server {
server_name [도메인 혹은 ip];
location / {
include proxy_params;
proxy_pass http://127.0.0.1:9999;
limit_req zone=limit_per_ip nodelay; # 이부분 추가
}
}
limit_req는 서버에 rate limit를 적용하는 것을 의미하며
zone에는 위에서 적었던 zone의 이름을 적어줍니다.
nodelay는 지연시키지 않고 바로 요청을 차단하는 옵션이며 유연성을 보장하기 위해
일시적으로 허용하면서 천천히 차단하는 옵션도 있습니다.
이후 아래 명령어를 입력해 세팅이 정상적인지 확인하고
sudo nginx -t
에러가 안 난다면 nginx 서버를 재구동합니다.
sudo systemctl restart nginx
이후 부하테스트 툴 같은 걸로 테스트해 보시면 초반에 몇 개는 200이 뜨지만 너무 많은 요청이 올 경우 503이 뜨는 것을 보실 수 있습니다.
저는 python 부하테스트 툴인 wrk를 사용하여 테스트하였습니다.
여기까지 하면 너무 많은 요청으로 인해 cpu 100%를 찍고 ec2가 다운되는 일은 없었는데요.
하지만 여전히 해킹 시도로 보이는 API 요청이 들어오는 건 막을 수 없었습니다.
3. Fail2Ban 도입
그래서 도입한 것이 바로 Fail2Ban입니다.
Fail2Ban은 로그를 보고 비정상적인 요청이 반복될 경우 해당 ip를 차단시킬 수 있는 툴입니다.
nginx와 통합이 쉽다는 장점이 있어 바로 도입하게 되었습니다.
적용하는 과정은 일단 fail2ban을 설치해 주고
sudo apt update
sudo apt install fail2ban
/etc/fail2ban에 jail.local 파일을 생성한 후 아래 내용을 입력하고 저장합니다.
[nginx]
enabled = true
port = http,https
filter = nginx
logpath = /var/log/nginx/access.log
maxretry = 5
http와 https를 모두 검사할 것이고 nginx로 필터를 걸 것이며 logpath 경로에 있는 로그 파일을 보고 비정상적인 요청임을 판단합니다.
최대 5번이 비정상적인 요청이 감지되면 일정기간 동안 혹은 영원히 IP Ban을 때립니다.
그럼 비정상적인 요청을 판별하기 위한 기준이 있어야 하는데요.
위에 filter를 nginx로 걸었으니 이 필터 정의를 해야 합니다.
/etc/fail2ban/filter.d로 이동하여 nginx.conf 파일을 생성하고 아래와 같이 필터를 정의합니다.
[Definition]
failregex = ^<HOST> .* "(GET|POST|PATCH|PUT|DELETE) .*" (444|404|403) .*$
위 설정은 제 설정이고 입맛에 맞춰 수정하시면 됩니다.
저는 GET, POST, PATCH, PUT, DELETE 메서드에서 444, 404, 403 에러가 발생하면 필터에 걸리도록 작성하였습니다.
이후 fail2ban을 재시작해주면 필터가 적용됩니다.
sudo systemctl restart fail2ban
실제 밴과 언밴 목록을 보려면 아래 명령어를 입력해 주면 되는데요
sudo cat /var/log/fail2ban.log | grep -E 'Ban|Unban'
저는 적용하자마자 ip가 하나가 밴됐네요.. ㅋㅋㅋㅋ
잡았다 요놈.. 속이 아주 후련합니다
이후 모니터링 해본 결과 정말 효과가 좋습니다. 요청 로그에는 healthcheck 하는 로그 밖에 안 남았네요
실제 운영되는 서버를 배포해 보는 것은 처음이라 이런저런 시행착오를 겪고 해결도 해보았는데요.
오늘도 한 단계 성장한 것 같습니다.
해킹 시도한 놈은 밉지만 덕분에 경험도 하고 해결하면서 성장도 했네요.
'개발경험' 카테고리의 다른 글
VM(EC2)에서 CPU 100% 찍는 이슈 해결 방법 (0) | 2024.03.24 |
---|---|
[React-Native] 패키지명 & Bundle ID 바꾸는 방법 (0) | 2024.02.21 |
[React-Native] 전역적으로 사용할 수 있는 모달 만들기 (1) | 2024.02.06 |
[React-Native] 커스텀 폰트를 적용하고 전역적으로 사용하는 방법 (0) | 2024.02.05 |
현업에서 마주친 MSA 데이터 동기화 이슈 (0) | 2023.12.20 |