블로그 ELK 스택 도입 -Fin(?)-
2025년 03월 05일
지난 편에서 언급한대로 JSON 파싱 오류가 일어났는데 이게 단순히 파싱이 잘못된 게 아니라, Kibana로 로그 전송 자체가 안되고 있다는 사실을 깨달았다. 다시 확인을 해보니 FastAPI 내에서 생성된 로그가 Logstash로 전송이 안되고 있기 때문이었는데, 이를 전송하는 방법은 여러가지 방법이 있지만 TCP 소켓을 통해 Logstash로 전송하는 핸들러를 추가해줬다.
TCP를 이용한 로그 전송
class TCPLogstashHandler(logging.Handler):
def __init__(self, host, port):
super().__init__()
self.host = host
self.port = port
self.sock = None
self._connect()
def _connect(self):
try:
self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
self.sock.connect((self.host, self.port))
except (socket.error, socket.timeout) as e:
self.sock = None
print(f"로그스태시 연결 실패: {e}")
def emit(self, record):
try:
if self.sock is None:
self._connect()
if self.sock is not None:
msg = self.format(record) + '\n'
self.sock.sendall(msg.encode('utf-8'))
except (BrokenPipeError, socket.error) as e:
self.sock = None
print(f"로그 전송 실패: {e}")
하지만 여전히 JSON 파싱 오류가 발생했고, Logstash 파이프라인의 코덱을 명시적으로 json_lines로 설정해줬다.
input {
tcp {
port => 5044
codec => json_lines {
charset => "UTF-8"
}
}
}
filter {
# 필터 로직...
}
output {
elasticsearch {
hosts => ["elasticsearch:9200"]
index => "fastapi-logs-%{+YYYY.MM.dd}"
}
stdout { codec => rubydebug }
}
이렇게 설정한 뒤, Kibana를 확인해보니 정상적으로 FastAPI의 로그는 수집이 되고 있었는데, 문제는 로그 시간이 UTC로 표기된다는 것이었다. 그 이유는 기본적으로 ELK 스택의 설정이 UTC기 때문이었는데, Kibana 설정과 호스트 설정을 KST 기준으로 바꾸는 것으로 해결했다.
Kibana 설정 경로:
Stack Management > Advanced Settings
에서Timezone for date formatting
을Asia/Seoul
로 설정
그렇게 타임존까지 설정하고 시험때문에 한 이틀 정도 후에 Kibana로 로그를 확인했는데 아주 개판이 나있었다.
실제로는 존재하지 않는 경로를 뒤져서 쓸데 없는 404 로그를 엄청 띄우고 있었다.
일단 첫 번째로 한 대응은 봇들에게 여지를 주지 않기 위해서 404가 아닌 444를 반환하는 방법이었다.
server {
listen 443 ssl default_server;
listen [::]:443 ssl default_server;
server_name _;
ssl_certificate /etc/nginx/ssl/default.crt;
ssl_certificate_key /etc/nginx/ssl/default.key;
return 444;
}
server {
listen 80 default_server;
listen [::]:80 default_server;
server_name _;
return 444;
}
server {
listen 80;
listen [::]:80;
server_name cliche.life;
if ($host = cliche.life) {
return 301 https://$host$request_uri;
}
return 444;
}
정상적으로 접속할 경우에만 접속을 허용하도록 바꿨고, IP 혹은 포트 직접 접속은 404가 아닌 444로 원천 차단하는 방식을 선택했다.
444로 설정을 하면 아예 TCP 연결을 끊어버리는 거라, 브라우저에서는 '연결 끊김'으로 나오고, 대부분의 봇들은 응답이 없다고 판단해서 다시 시도를 하지 않는다.
근데 그 뿐만이 아닌, .env부터 시작해서, .git, wp-login, admin, .bak, config/ 등등 민감한 경로란 경로는 다 헤집어놓고 있었다.
물론 나도 병신은 아니라 그런 민감한 설정들이 외부에 노출되도록 설정을 해놓지는 않았지만, 이 새끼들은 정말 성의가 없었다. wp-login이나 php 설정 같이 이 블로그와 아무런 상관없는 경로까지 뒤져서 내 로그창이 더러워지는 걸 더 이상 볼 수 없었던 나는 fail2ban을 도입하기로 했다.
Fail2ban 도입
Fail2ban이란?
Fail2ban은 침입 차단 프레임워크로서 컴퓨터 서버를 무차별 대입 공격으로부터 보호한다. 파이썬 프로그래밍 언어로 쓰여졌으며, 패킷 제어 시스템이나 로컬에 설치된 방화벽(iptables 또는 TCP 래퍼)과의 인터페이스를 갖는 POSIX 시스템에서 돌아갈 수 있다.
현재 로그 꼬라지를 보니까 현 상황에 가장 맞는 프레임워크라는 생각이 들어서, Fail2ban 도입을 결정했다.
sudo apt install fail2ban
나와 같은 Ubuntu 환경에서는 이 명령어로 fail2ban을 설치 할 수 있다.
그 뒤에 사용자 정의 필터를 생성하면 되는데, 나같은 경우에는
[Definition]
failregex = ^<HOST> .* "(?:GET|POST|HEAD) /(?:\.git/|\?XDEBUG|\?a=|\.env|wp-login|admin/|administrator/|xmlrpc\.php|config/|\.well-known/|console/|_ignition/|\.sql|\.bak|\.php\?|cgi-bin/|\.asp|myadmin|phpmyadmin|adminer|manager/|jenkins/|solr/|%00|%27|%20select%20|%20or%201=1|%20and%201=1|%20from%20|/etc/passwd).*" .*$
ignoreregex =
이런 식으로 실제 Kibana 로그를 csv로 뽑아서 분석 한 뒤 들어온 요청들을 전부 차단하는 방식으로 진행했다.
이런 식으로 로그를 볼 수 있다.
실제로 로그를 보면 요 2~3일 간 차단된 숫자만 봐도 얼마나 많은 요청이 들어오는 지 알 수 있다.
근데 이 로그를 보려면 내가 호스트에서
sudo tail -f /var/log/fail2ban.log
이런 식으로 확인을 해야했는데, 이렇게 된 거 fail2ban까지 ELK 스택에 합쳐서 한번에 모든 걸 관리해야겠다는 생각이 들었다. (진짜 솔직히 말하면 이 정도면 되지 않을까 라는 생각을 하긴 했지만)
왜냐면 애초에 ELK 스택을 도입하게 된 게 호스트에서 로그를 보는 게 관리도 힘들고 귀찮아서였는데, fail2ban 로그를 확인하려고 매번 터미널을 켜서 호스트 접속해서 저렇게 로그를 보는 건 의미가 없다는 생각이 들었다.
근데 여기서 문제는 ELK 스택은 Docker 컨테이너 상에서 돌아가고 있고, Fail2ban은 호스트 환경에서 돌아가고 있다는 점이었는데, 이를 해결한 방법은 filebeat를 이용해서 Fail2ban의 로그를 따로 Logstash로 쏴주는 거였다.
이 과정에서도 많은 우여곡절이 있었는데, Filebeat의 포트를 5044로 설정하니 기존 FastAPI의 로그를 옮기던 Logstash와의 충돌이 발생해서 로그스태시가 셧다운 되었고, 이를 방지하기 위해 Filebeat의 포트는 5045로 주니, Docker 컨테이너에서는 5045 포트가 열려있지 않아 로그스태시로 로그가 전송되지 않았다.
또한 따로 태그 설정을 해놓지 않아서 Fail2ban의 로그가 기존 FastAPI의 로그에 섞여서 들어가는 일도 있었다.
이를 각각 해결한 방법은 docker-compose.yml을 수정해서
logstash:
image: docker.elastic.co/logstash/logstash:8.12.0
volumes:
- ./elk/logstash/pipeline:/usr/share/logstash/pipeline
ports:
- "5044:5044" # FastAPI
- "5045:5045" # Fail2Ban
depends_on:
- elasticsearch
networks:
- elk
Logstash에서 Filebeat를 위한 포트를 따로 열어주었고,
output {
if "fail2ban" not in [tags] {
elasticsearch {
hosts => ["elasticsearch:9200"]
index => "fastapi-logs-%{+YYYY.MM.dd}"
}
stdout { codec => rubydebug }
}
}
기존 태그 설정을 수정해서 fail2ban이 태그에 포함되지 않을 경우에만 fastapi-logs로 보내도록 설정했다.
이로써 나는 ELK 스택을 이용해서 Fail2ban과 fastAPI의 로그를 수집하고, 시각화할 수 있게 되었으며, 이 과정에서 추가적인 보안 설정도 진행할 수 있었다. 직접 확인해본 결과 생각보다 엄청나게 많은 수의 악의적 요청이 들어오고 있었음을 알 수 있었고, 단순히 개발->배포로 끝나는 프로세스가 아닌 보안적인 부분도 신경을 써줘야 한다는 사실을 새롭게 알게 되었다.