시스템 아키텍처 및 구성
🚀 1단계: Filebeat 설치
ModSecurity와 Suricata가 설치된 서버에 Filebeat를 설치합니다. (운영체제는 Debian/Ubuntu 계열을 가정합니다.)
# 1. GPG 키 추가
wget -qO - https://artifacts.elastic.co/GPG-KEY-elasticsearch | sudo gpg --dearmor -o /usr/share/keyrings/elasticsearch-keyring.gpg
# 2. apt 저장소 등록
echo "deb [signed-by=/usr/share/keyrings/elasticsearch-keyring.gpg] https://artifacts.elastic.co/packages/8.x/apt stable main" | sudo tee /etc/apt/sources.list.d/elastic-8.x.list
# 3. Filebeat 설치
sudo apt update && sudo apt install filebeat
⚙️ 2단계: ModSecurity 및 Suricata 모듈 설정
Filebeat에는 주요 서비스의 로그를 자동으로 인식하고 파싱해주는 모듈 기능이 있습니다. 우리는 ModSecurity와 Suricata 모듈을 활성화하고 설정할 것입니다.
A. Suricata 모듈 설정
Suricata는 보통 **eve.json**이라는 JSON 형식의 알림 로그를 생성하며, Filebeat Suricata 모듈이 이 파일을 완벽하게 처리합니다.
- Suricata 모듈 활성화
sudo filebeat modules enable suricata - Suricata 설정 파일 편집다음과 같이 paths 항목을 Suricata의 eve.json 파일 경로에 맞게 수정합니다.
sudo nano /etc/filebeat/modules.d/suricata.yml # 아래 내용 추가하기 - module: suricata # ⚠️ enabled를 true로 설정했는지 확인 enabled: true log: enabled: true # ⚠️ Suricata의 실제 eve.json 경로로 수정 paths: - /var/log/suricata/eve.json
B. ModSecurity 로그 수집 방법
Filebeat의 일반 로그 수집 기능(input: log)을 사용하여 로그 파일을 읽고, Logstash에서 직접 파싱
1. ⚙️ Filebeat 설정 (로그 수집)
modsecurity 모듈을 사용하는 대신, Filebeat의 기본 log 입력 타입을 사용하여 ModSecurity 감사 로그 파일의 경로를 지정합니다.
/etc/filebeat/filebeat.yml 파일을 열어서 내용을 추가합니다.
# ---------------------------- Custom ModSecurity Input ----------------------------
filebeat.inputs:
- type: log
enabled: true
# ⚠️ 로그 파일 경로를 ModSecurity Audit Log 경로로 정확히 지정
paths:
- /var/log/apache2/modsec_audit.log
# 또는 Nginx 경로: /var/log/nginx/modsec_audit.log
# 이 로그에 대한 고유한 타입 정의 (Logstash에서 이 값을 사용하여 파싱)
tags: ["modsecurity"]
# Audit Log가 일반적으로 한 요청에 대해 여러 줄로 구성되므로 멀티라인 설정 필수
multiline.pattern: '^--[a-zA-Z0-9]{8}-A--$'
multiline.negate: true
multiline.match: after
- type: log: Filebeat에게 일반 텍스트 파일을 읽도록 지시합니다.
- tags: ["modsecurity"]: Logstash에서 이 로그를 쉽게 식별할 수 있도록 태그를 추가합니다.
- multiline.pattern: ModSecurity Audit Log는 ---A--, ---B-- 등으로 구분되는 여러 섹션으로 구성된 단일 이벤트입니다. 이 설정은 --[8자리 해시]-A--로 시작하는 패턴을 기준으로 로그 이벤트 하나를 묶어주어 Logstash가 통째로 처리할 수 있게 합니다.
2. 🔌 Logstash 설정 (로그 파싱)
Filebeat가 전송한 ModSecurity 로그를 Logstash에서 받아 JSON 구조로 파싱합니다.
/etc/logstash/conf.d/30-modsecurity.conf 파일을 생성하고 다음 내용을 추가합니다.
# Logstash Input (Filebeat로부터 수신)
input {
beats {
port => 5044
}
}
# Logstash Filter (ModSecurity 로그 파싱)
filter {
# Filebeat 설정에서 정의한 tags를 사용하여 ModSecurity 로그만 필터링
if "modsecurity" in [tags] {
# Audit Log는 섹션 A (요청 헤더)부터 H (감사 엔진)까지 구조화되어 있음.
# Grok 패턴을 사용해 각 섹션을 분리하고 key-value 쌍으로 파싱합니다.
grok {
# 이 패턴은 Audit Log의 A, B, C, F, H 섹션을 분리합니다.
# ModSecurity 로그 구조가 복잡하므로 완벽한 파싱을 위해서는 더 복잡한 패턴이 필요할 수 있습니다.
match => { "message" => "(?m)^--(?<unique_id>[a-zA-Z0-9]{8})-[A]-{2}\n%{TIMESTAMP_ISO8601:timestamp} %{HOSTPORT:client_ip_port} %{HOSTPORT:server_ip_port}\n%{DATA:request_headers}\n--%{unique_id}-[B]-{2}\n%{DATA:request_body}\n--%{unique_id}-[C]-{2}\n%{DATA:response_headers}\n--%{unique_id}-[F]-{2}\n%{DATA:audit_log_message}\n--%{unique_id}-[H]-{2}\n%{DATA:audit_engine_result}" }
remove_tag => ["_grokparsefailure"]
}
# timestamp 필드를 ISO8601 형식으로 변환
date {
match => ["timestamp", "ISO8601"]
target => "@timestamp"
}
# ID와 메시지를 추출하여 필드에 저장 (F 섹션에서 상세 정보를 추출하는 예시)
grok {
match => { "audit_log_message" => "\[id \"%{NUMBER:modsec_id}\"\].*\[msg \"%{DATA:modsec_msg}\"\]" }
tag_on_failure => [] # 파싱 실패해도 태그를 추가하지 않음
}
}
}
# Logstash Output (Elasticsearch로 전송)
output {
elasticsearch {
hosts => ["http://<Elasticsearch-IP>:9200"]
index => "modsecurity-%{+YYYY.MM.dd}"
# 사용자 인증 정보 (필요시)
# user => "elastic"
# password => "<your-password>"
}
}
3. 🏁 서비스 재시작
설정 변경 후 Filebeat와 Logstash 서비스를 재시작합니다.
# Logstash 설정 파일을 확인합니다.
sudo /usr/share/logstash/bin/logstash --config.test_and_exit -f /etc/logstash/conf.d/
# Logstash 서비스 재시작
sudo systemctl restart logstash
# Filebeat 서비스 재시작
sudo systemctl restart filebeat
이제 Filebeat가 ModSecurity 로그를 Logstash로 보내고, Logstash가 이를 파싱하여 Elasticsearch에 색인(Index)하게 됩니다. Kibana의 Discover 메뉴에서 modsecurity-* 인덱스를 검색하여 로그가 JSON 구조로 변환되어 들어오는지 확인하실 수 있습니다.
🔌 3단계: Filebeat 출력 설정
Filebeat가 로그를 보낼 ELK 스택의 주소(Elasticsearch 또는 Logstash)를 설정합니다.
A. Filebeat 메인 설정 파일 편집
sudo nano /etc/filebeat/filebeat.yml
B. Output 설정 (Logstash 또는 Elasticsearch)
Logstash에서 파싱(parsing) 작업을 수행하도록 설정하는 것이 일반적이지만, 간단한 구성을 위해서는 Elasticsearch로 직접 보내도 무방합니다. 이번 프로젝트에서는 logstash로 전송합니다.
1. Elasticsearch로 직접 전송 (간단)
Logstash 섹션을 주석 처리하고 Elasticsearch 섹션을 활성화합니다.
# ---------------------------- Elasticsearch Output ----------------------------
output.elasticsearch:
# ⚠️ ELK 스택의 Elasticsearch 주소 및 포트로 변경
hosts: ["http://<Elasticsearch-IP>:9200"]
# 만약 ELK 스택에 보안(Username/Password)이 설정되어 있다면 추가
# username: "elastic"
# password: "<your-password>"
2. Logstash를 거쳐 전송
Elasticsearch 섹션을 주석 처리하고 Logstash 섹션을 활성화합니다. Logstash에서 로그를 파싱하고 보강하는 작업을 수행할 때 이 방법이 유용합니다. (Suricata와 Mod_security 둘 다 활성화)
# ------------------------------ Logstash Output -------------------------------
output.logstash:
# ⚠️ Logstash 서버의 주소 및 Beats Input Plugin 포트로 변경
hosts: ["<Logstash-IP>:5044"]

ELK 머신은 서버 역할을 합니다. 따라서 외부 클라이언트(Filebeat)의 접속을 받아들이기 위해 5044/TCP 포트의 인바운드(Inbound) 접속을 허용해야 합니다.
sudo ufw allow 5044/tcp
클라이언트가 외부 서버로 접속할 때는 일반적으로 특정 포트를 열어줄 필요가 없습니다. 클라이언트의 출발 포트(Source Port)는 임의의 높은 포트가 사용되며, 방화벽은 아웃바운드(Outbound) 트래픽을 기본적으로 허용합니다.
Logstash를 사용한다면, Logstash 서버에서 Filebeat로부터 로그를 수신할 수 있도록 Beats Input Plugin (포트 5044) 설정이 완료되어 있어야 합니다.
4단계: Kibana에서 모니터링
Kibana에 접속하여 다음을 확인합니다.
- Discover (탐색): filebeat-* 인덱스 패턴을 선택하고 데이터가 정상적으로 수신되는지 확인합니다.
- Dashboard (대시보드): Suricata Overview 및 Modsecurity Overview 등 Filebeat가 로드한 기본 대시보드를 찾아 모의해킹 공격에 대한 탐지 결과가 시각화되는지 확인합니다.
이제 취약한 서버에 모의 해킹을 진행하여, ModSecurity와 Suricata가 어떻게 탐지하고, 그 결과가 ELK 통합 솔루션에서 어떻게 시각화되는지 확인하실 수 있습니다.
오류 1.
ERROR 1.
Failed to start filebeat.service - Filebeat sends log files to Logstash or directly to Elasticsearch..
Exiting: error unpacking config data: more than one namespace configured accessing 'output' (source:'/etc/filebeat/filebeat.yml')

Logstash로 로그를 전송하려면 output.logstash 섹션만 활성화하고, 다른 출력 섹션(주로 output.elasticsearch)은 반드시 주석 처리해야 합니다.
sudo nano /etc/filebeat/filebeat.yml
명령어로 설정 파일을 열고, 'logstash', 'elasticsearch' 두 섹션을 확인하여 충돌을 해결해야 합니다.


오류 2.
Pipeline error {:pipeline_id=>"main", :exception=>"Grok::PatternError", :error=>"pattern %{unique_id} not defined"
Pipeline error {:pipeline_id=>"main", :exception=>"Grok::PatternError", :error=>"pattern %{modsec_id:modsec_id_b} not defined"

Logstash의 한 Grok 필터에서 긴 패턴을 작성하고 내부적으로 재사용하는 대신, ModSecurity의 고유 ID를 정의하는 부분을 별도의 커스텀 Grok 패턴 파일로 분리하거나, 패턴 재사용을 피하도록 수정해야 합니다.
가장 간단한 방법은 Grok의 내장 패턴만 사용하고, 패턴 재사용은 mutate 필터 등을 이용해 후처리하는 것입니다.
# Logstash Filter (ModSecurity 로그 파싱)
filter {
if "modsecurity" in [tags] {
# 1. ModSecurity Audit Log의 기본 구조와 섹션 A, F 부분만 추출하는 Grok
grok {
# DATA는 모든 문자를 허용하는 패턴이므로, 각 섹션 시작 부분만 정확히 잡습니다.
match => { "message" => "(?m)^--(?<modsec_unique_id>[a-zA-Z0-9]{8})-[A]-{2}\n%{TIMESTAMP_ISO8601:timestamp} %{DATA:client_info} %{DATA:server_info}\n%{DATA:request_headers}.*^--%{DATA:modsec_id_f}-[F]-{2}\n%{DATA:audit_log_message}.*" }
remove_tag => ["_grokparsefailure"]
}
# 2. timestamp 필드 변환
date {
match => ["timestamp", "ISO8601"]
target => "@timestamp"
remove_field => ["timestamp"]
}
# 3. F 섹션에서 상세 정보 추출
grok {
match => { "audit_log_message" => "\[id \"%{NUMBER:modsec_id}\"\].*\[msg \"%{DATA:modsec_msg}\"\]" }
tag_on_failure => []
}
}
}
오류 3.
#오류 메시지:
Failed to perform request {:message=>"localhost:9200 failed to respond", :exception=>Manticore::ClientProtocolException
#추가 메시지:
Attempted to resurrect connection to dead ES instance, but got an error {:url=>"http://elastic:xxxxxx@localhost:9200/"

이는 Logstash가 데이터를 저장하기 위해 **localhost:9200**으로 Elasticsearch에 접속하려 하지만, Elasticsearch가 응답하지 않는다는 명확한 증거입니다.
sudo systemctl status elasticsearch
# Elasticsearch의 9200 포트가 응답하는지 확인
curl http://localhost:9200

Elasticsearch 서비스가 9200 포트에서 실행되고 있지만, 요청에 응답할 준비가 안 되었거나 요청을 받고 즉시 연결을 끊는다는 의미입니다.
이는 Elasticsearch가 현재 다운된 것이 아니라, 실행은 되고 있지만 제대로 초기화되지 않았거나 혹은 내부적인 설정 문제로 인해 HTTP 응답을 보내지 못하는 상황임을 나타냅니다.
sudo journalctl -u elasticsearch -n 50
→ journalctl 실행 결과 아무런 오류가 없음.
# Elasticsearch의 주 로그 경로 확인 (버전 및 설정에 따라 다를 수 있음)
sudo tail -n 50 /var/log/elasticsearch/elasticsearch.log

#로그에 반복되는 경고메세지
received plaintext http traffic on an https channel, closing connection
이 메시지는 Elasticsearch가 현재 **HTTPS (보안 연결)**만 허용하도록 설정되어 있는데, Logstash와 curl 명령이 **HTTP (암호화되지 않은 연결)**로 접속을 시도하고 있기 때문에 연결을 강제로 끊고 있다는 뜻입니다.
- Elasticsearch (Server): 9200 포트를 HTTPS로 리스닝 중.
- Logstash & curl (Client): http://localhost:9200 (HTTP)로 접속 시도.
최신 버전의 Elastic Stack은 보안이 기본으로 활성화되어 있어 이 문제가 매우 흔하게 발생합니다.
→ Logstash 설정을 HTTPS로 변경
Logstash 설정 파일에서 Elasticsearch 연결 주소를 http에서 https로 변경

오류 4.
에러가 나는 Elasticsearch 로그를 보면, Logstash가 ssl_verification_mode => "none" 설정을 적용했음에도 불구하고 여전히 SSL Handshake 오류가 발생하고 있습니다.
이는 Logstash의 Elasticsearch Output Plugin 버전이나 Logstash가 사용하는 JRuby 환경이 ssl_verification_mode => "none" 설정을 올바르게 처리하지 못하고 있거나, Logstash가 인증서 검증을 무시하는 것 외에 특정 인증서를 신뢰하도록 요구하고 있음을 시사합니다.
curl 테스트에서는 -k 옵션으로 인증서 검증을 성공적으로 무시했으므로, 문제는 Logstash 자체의 SSL/TLS 처리 방식에 있습니다.
🛠️ 최종 해결책: Logstash에 CA 인증서 직접 제공
ssl_verification_mode => "none"이 효과가 없을 경우, 가장 확실한 방법은 Elasticsearch가 사용하는 CA(인증 기관) 인증서를 Logstash에 직접 제공하여 신뢰하도록 명시하는 것입니다.
# 이 경로에 CA 인증서 파일이 있어야 합니다.
sudo ls /etc/elasticsearch/certs/http_ca.crt
output {
elasticsearch {
hosts => ["https://localhost:9200"] # https:// 유지
index => "modsecurity-%{+YYYY.MM.dd}"
user => "elastic"
password => "<설치 시 설정한 비밀번호>"
# 🌟 문제 해결 핵심: CA 인증서 경로 명시
ssl => true
ssl_certificate_verification => true # 검증을 다시 켜고,
cacert => '/etc/elasticsearch/certs/http_ca.crt' # 신뢰할 인증서를 제공
# ssl_verification_mode => "none" 은 제거하거나 주석 처리
}
}
[2025-12-01T11:44:55,729][INFO ][o.e.c.r.a.AllocationService] [cmd-VirtualBox] current.health="GREEN" message="Cluster health status changed from [RED] to [GREEN] (reason: [shards started [[.ds-ilm-history-7-2025.11.26-000001][0], [.apm-source-map][0]]])." previous.health="RED" reason="shards started [[.ds-ilm-history-7-2025.11.26-000001][0], [.apm-source-map][0]]"
# [cmd-VirtualBox] current.health="GREEN"로 바뀐것을 확인
오류 5.
[2025-12-01T11:51:50,594][ERROR][logstash.outputs.elasticsearch] Invalid setting for elasticsearch output plugin:
# File does not exist or cannot be opened /etc/elasticsearch/certs/http_ca.crt
cacert => "/etc/elasticsearch/certs/http_ca.crt"

Logstash는 cacert => "/etc/elasticsearch/certs/http_ca.crt" 설정을 읽었지만, 해당 경로의 파일에 접근할 수 없거나 (권한 문제), 파일이 실제 존재하지 않기 때문에 파이프라인을 로드하는 데 실패하고 있습니다.
Logstash 서비스는 일반적으로 logstash 사용자 계정으로 실행되지만, Elasticsearch의 인증서 파일은 root 또는 elasticsearch 사용자에게만 접근 권한이 있을 수 있습니다.
Logstash 사용자에게 파일 읽기 권한 부여 (가장 유력한 해결책)
파일이 존재한다면, logstash 사용자가 이 파일을 읽을 수 있도록 권한을 변경해야 합니다.
- 현재 파일 권한 확인:
ls -l /etc/elasticsearch/certs/ - Logstash 사용자에게 읽기 권한 부여: http_ca.crt 파일은 민감하지 않으므로, logstash 그룹 또는 모든 사용자에게 읽기 권한을 부여하는 것이 일반적입니다.
# 1. logstash 그룹이 읽을 수 있도록 권한 변경 (가장 권장) sudo chown :logstash /etc/elasticsearch/certs/http_ca.crt sudo chmod g+r /etc/elasticsearch/certs/http_ca.crt # 2. 또는 모든 사용자가 읽을 수 있도록 권한 변경 (쉬운 방법) # sudo chmod a+r /etc/elasticsearch/certs/http_ca.crt # 3. logstash 재시작 sudo systemctl restart logstash디렉토리 실행 권한 부여
http_ca.crt 파일을 직접 수정하는 대신, logstash 사용자가 파일이 위치한 디렉토리를 통과할 수 있도록 권한을 설정하겠습니다.
1. 🔑 Logstash를 elasticsearch 그룹에 추가
가장 깔끔하고 보안적인 방법은 logstash 사용자를 파일의 소유 그룹인 elasticsearch 그룹에 추가하여 그룹 권한(rw-)을 부여하는 것입니다.
# Logstash 사용자를 elasticsearch 그룹에 추가 sudo usermod -a -G elasticsearch logstash2. 디렉토리에 접근 권한 부여
혹시 모를 상황에 대비하여 상위 디렉토리의 권한도 확인하고, elasticsearch 그룹에 실행 권한이 있는지 확인합니다.
# certs 디렉토리의 권한 확인 ls -ld /etc/elasticsearch/certs/# elasticsearch 그룹에 디렉토리 접근(실행) 권한 부여 sudo chmod g+x /etc/elasticsearch/certs/

#Elasticsearch 연결 성공
[2025-12-01T12:08:20,145][WARN ][logstash.outputs.elasticsearch][main] Restored connection to ES instance {:url=>"https://elastic:xxxxxx@localhost:9200/"}
[2025-12-01T12:08:20,146][INFO ][logstash.outputs.elasticsearch][main] Elasticsearch version determined (8.19.7)
#파이프라인 시작 성공
[2025-12-01T12:08:20,965][INFO ][logstash.javapipeline ][main] Pipeline started {"pipeline.id"=>"main"}
[2025-12-01T12:08:21,037][INFO ][org.logstash.beats.Server][main][...] Starting server on port: 5044





