웹/보안 로그를 수집 → 검색 → 시각화 → 위협 탐지하는 SIEM 실습입니다. ELK/OpenSearch는 고사양(수 GB 메모리)이라 본인 PC에 설치하지 않고 아래 공용 분석 서버 한 곳에서 브라우저만으로 실습합니다. 서버가 실시간으로 샘플 로그를 흘려보내고 있어 바로 시작할 수 있습니다. (직접 설치는 맨 아래 부록(apt) 참고)
elklab 또는 student / 비밀번호 seekerslab). 설치 없이 브라우저로 접속하세요.
세 가지 구성요소가 파이프라인을 이룹니다.
| 구성요소 | ELK | OpenSearch(이 랩) | 역할 |
|---|---|---|---|
| 수집 | Logstash / Beats | Data Prepper / Fluent Bit | 로그를 모아 파싱·정규화해 전송 |
| 저장·검색 엔진 | Elasticsearch | OpenSearch | 문서를 인덱스에 저장, 역색인으로 초고속 검색·집계 |
| 시각화 | Kibana | OpenSearch Dashboards | Discover(검색)·Visualize·Dashboard·Alerting |
SIEM(Security Information & Event Management) = 이 스택으로 보안 로그를 한곳에 모아 상관분석·탐지·대응하는 것.
흐름: 로그 발생 → 수집 → 인덱스 적재 → 검색/집계 → 시각화 → 탐지 규칙/알럿.
loggen 이 2초마다 웹/보안 로그를 logs-demo-YYYY.MM.DD 인덱스로
_bulk 적재합니다. 여러분은 이 데이터를 검색·분석합니다.모든 쿼리는 아래 필드명을 사용합니다. 필드명을 정확히 써야 검색이 됩니다.
| 필드 | 타입 | 예시 | 의미 |
|---|---|---|---|
@timestamp | date | 2026-07-08T04:12:33.482Z | 이벤트 발생 시각(UTC) |
event | keyword | access · attack · auth_fail | 이벤트 분류(핵심 필드) |
client_ip | ip | 203.0.113.7 | 요청 출발 IP |
geo.country | keyword | KR · US · CN · RU | 접속 국가 코드 |
http.method | keyword | GET · POST · PUT · DELETE | HTTP 메서드 |
http.status | number | 200 · 401 · 403 · 404 · 500 | 응답 상태코드 |
http.bytes | number | 200 ~ 40000 | 응답 크기(byte) |
url | text | /login · /.env · /search?id=1'… | 요청 경로(+공격 payload) |
response_ms | number | 2 ~ 900 | 응답 시간(ms) |
user_agent | text | Chrome · curl/8 · sqlmap/1.8 | User-Agent 문자열 |
attack_type | keyword | sqli · xss · path_traversal · cmd_injection | 공격 유형(event:attack 일 때) |
severity | keyword | medium · high · critical | 심각도(공격 이벤트) |
user | keyword | admin · root · test | 로그인 시도 계정(auth_fail 일 때) |
실제 문서(예시):
{
"@timestamp": "2026-07-08T04:12:33.482Z",
"event": "auth_fail",
"client_ip": "203.0.113.7",
"geo": { "country": "RU" },
"http": { "method": "POST", "status": 401, "bytes": 512 },
"url": "/login",
"response_ms": 88,
"user_agent": "python-requests/2.31",
"user": "admin"
}
인덱스 패턴 logs-demo-* 는 이미 생성돼 기본값으로 지정돼 있습니다 — 따로 만들 필요 없이 바로 검색하세요.
elklab 또는 student / seekerslab.logs-demo-* 인지 확인(기본값).logs-demo-* → 시간 필드 @timestamp.
상단 검색창에 KQL(Query Language)을 입력합니다. 좌측 Available fields 에서 필드를 클릭하면 상위 값 분포가 뜨고, + 로 표(컬럼)에 추가할 수 있습니다. 문서 왼쪽 ▸ 로 원본 JSON 을 펼쳐 보세요.
1
2
3
4
5
6
logs-demo-*) · ② KQL 검색창 · ③ 시간 범위(Last 1 hour) · ④ Refresh · ⑤ 필드 클릭(값 분포·+로 컬럼 추가) · ⑥ ▸ 로 문서 원본(JSON) 펼치기# 이벤트 분류별
event: access
event: auth_fail
event: attack
# 특정 공격 유형
event: attack and attack_type: sqli
# 인증/권한 오류 상태코드
http.status: 401 or http.status: 403
# 서버 오류(5xx)
http.status >= 500
# 스캐너·자동화 도구 UA
user_agent: *sqlmap* or user_agent: *python-requests* or user_agent: *curl*
# 민감 경로 스캔 시도
url: *.env* or url: *wp-login* or url: "/admin"
# 대용량 응답(데이터 유출 의심)
http.status: 200 and http.bytes > 30000
# 특정 국가발 공격
geo.country: RU and event: attack
# 특정 IP 의 모든 활동
client_ip: "203.0.113.7"
# 심각도 높은 공격
severity: (high or critical)
# 로그인 실패 POST (브루트포스 후보)
url: "/login" and http.method: POST and http.status: 401
좌측 Visualize → Create visualization → 아래 갤러리에서 컴포넌트를 고르고 인덱스 logs-demo-* 를 선택합니다.
1
2
.keyword: 텍스트 필드(attack_type·geo.country·client_ip·url·user_agent)를
Terms 로 집계할 땐 필드.keyword 를 선택하세요(안 그러면 “invalid for Terms aggregation” 오류). 숫자(http.status·http.bytes)와 날짜(@timestamp)는 그대로.| 컴포넌트 | Metric / Bucket 설정 | 보는 것 |
|---|---|---|
| Metric 공격 건수 | 필터 event: attack · Metric = Count | 총 공격 수 한 눈에 |
| Pie / Donut 상태코드 | Split slices → Terms → http.status | 2xx/4xx/5xx 비율 |
| Vertical Bar 공격 유형 | 필터 event: attack · X-axis → Terms → attack_type.keyword | 가장 많은 공격 벡터 |
| Line 트래픽 추이 | X-axis → Date Histogram → @timestamp | 시간대별 스파이크 |
| Data Table 상위 IP | 필터 event: attack · Bucket → Terms → client_ip.keyword (Size 10) | 집중 공격 출발지 |
| Heat Map | X=geo.country.keyword, Y=http.status, 값=Count | 국가×상태코드 밀도 |
| Tag Cloud | Terms → user_agent.keyword | 많이 쓰인 UA(도구) 워드클라우드 |
logs-demo-*.geo.country.keyword → Size 50.geo.country 값이 KR·US·CN 같은 ISO2 국가코드라 iso2 로 조인됩니다.
1
2
3
4
geo.country.keyword) · ② Layer Options(Vector map=World Countries, Join=iso2) · ③ Update(미리보기 갱신) · ④ Save. geo.country(ISO2)를 국가 경계에 조인해 접속량을 색으로 표현.[랩] … 항목(공격 건수·상태코드 분포·공격 유형·국가별 접속·시간대별 트래픽·상위 공격자 IP)을 열어 설정을 그대로 참고하세요.
저장 시 공유 환경이므로 이름 앞에 본인 이니셜(예 kim-공격유형).kim-보안관제).event: attack).
1
2
3
4
event: attack) · ② 시간 범위 · ③ Refresh/Auto refresh · ④ Edit(패널 추가·배치·저장). 공격 건수·상태코드·공격 유형·국가별 접속·트래픽·상위 IP를 한 화면에.event: auth_failclient_ip 필드의 상위 값(또는 Data table Terms)에서 실패가 급증한 IP·계정(user) 확인.event: attack → attack_type·severity 별 분포.sqli/xss/path_traversal/cmd_injection 중 가장 많은 벡터, critical 비중.user_agent: *sqlmap* or user_agent: *nikto* or url: *.env* or url: *wp-login*client_ip 로 출발지 특정.http.status: 200 and http.bytes > 30000response_ms 도 함께 확인.geo.country: (RU or CN) and event: attackhttp.status >= 500)은 DoS/오작동 신호.좌측 Dev Tools → Console 에서 API 로 문서를 넣고 조회할 수 있습니다(스키마 이해에 좋음).
# 문서 여러 개 한 번에 적재 (_bulk)
POST logs-demo-2026.07.08/_bulk
{ "index": {} }
{ "@timestamp":"2026-07-08T04:00:00Z","event":"auth_fail","client_ip":"10.0.0.9","user":"admin","url":"/login","http":{"method":"POST","status":401} }
{ "index": {} }
{ "@timestamp":"2026-07-08T04:00:01Z","event":"attack","attack_type":"sqli","severity":"high","client_ip":"10.0.0.9","url":"/search?id=1' OR '1'='1","http":{"method":"GET","status":500} }
# 집계 조회 — 공격 유형별 개수
GET logs-demo-*/_search
{ "size":0, "query":{"term":{"event":"attack"}},
"aggs":{"by_type":{"terms":{"field":"attack_type"}}} }
# 상위 공격자 IP
GET logs-demo-*/_search
{ "size":0, "query":{"term":{"event":"attack"}},
"aggs":{"top_ip":{"terms":{"field":"client_ip","size":10}}} }
| 연산 | 예 | 의미 |
|---|---|---|
| 등호 | event: attack | 필드 = 값 |
| AND / OR / NOT | a and b · a or b · not a | 불리언 결합 |
| 범위 | http.status >= 400 · http.bytes > 30000 | 숫자 비교 |
| 와일드카드 | user_agent: *curl* | 부분 일치 |
| 존재 여부 | attack_type: * | 필드가 존재하는 문서 |
| 구문(정확히) | url: "/api/users" | 따옴표로 정확 일치 |
| 여러 값 | http.status: (401 or 403) | OR 축약 |
| 괄호 | (a or b) and c | 우선순위 |
아래는 플레인 Linux 컨테이너에서 apt 로 ELK/OpenSearch 를 직접 설치하는 과정입니다. 실습에 필수가 아니며, 권장 8GB+ RAM · Docker 가 있는 경우에만 시도하세요. (보안 비활성 설정은 학습 전용 — 운영 금지)
# (호스트) 커널 파라미터 — 없으면 엔진이 부팅 실패
sudo sysctl -w vm.max_map_count=262144
# (호스트) 플레인 Ubuntu 컨테이너 — 포트 9200(엔진)·5601(대시보드) 매핑
docker run -it --name elk-lab -p 9200:9200 -p 5601:5601 ubuntu:22.04 bash
# ── 이하 컨테이너 내부 ──
apt-get update
apt-get install -y wget curl gnupg apt-transport-https ca-certificates sudo
systemctl start 가 안 됩니다 → 아래처럼 바이너리를 직접 실행합니다.# 1) Elastic 공개키 + apt 저장소(8.x)
wget -qO - https://artifacts.elastic.co/GPG-KEY-elasticsearch \
| gpg --dearmor -o /usr/share/keyrings/elastic-keyring.gpg
echo "deb [signed-by=/usr/share/keyrings/elastic-keyring.gpg] \
https://artifacts.elastic.co/packages/8.x/apt stable main" \
| tee /etc/apt/sources.list.d/elastic-8.x.list
# 2) 설치
apt-get update && apt-get install -y elasticsearch kibana
# 3) 실습용 설정 (보안·TLS 비활성 — 학습 전용)
cat >> /etc/elasticsearch/elasticsearch.yml <<'YML'
discovery.type: single-node
network.host: 0.0.0.0
xpack.security.enabled: false
YML
printf '%s\n' '-Xms1g' '-Xmx1g' > /etc/elasticsearch/jvm.options.d/heap.options
cat >> /etc/kibana/kibana.yml <<'YML'
server.host: "0.0.0.0"
elasticsearch.hosts: ["http://localhost:9200"]
YML
# 4) 실행 (systemd 없으므로 서비스 유저로 직접)
sudo -u elasticsearch /usr/share/elasticsearch/bin/elasticsearch &
# 엔진이 뜬 뒤(~30초) Kibana
sudo -u kibana /usr/share/kibana/bin/kibana &
# 5) 확인
curl http://localhost:9200 # Elasticsearch JSON
# 브라우저: http://localhost:5601 (Kibana)
# 1) 공개키 + apt 저장소(2.x)
curl -o- https://artifacts.opensearch.org/publickeys/opensearch.pgp \
| gpg --dearmor -o /usr/share/keyrings/opensearch-keyring
echo "deb [signed-by=/usr/share/keyrings/opensearch-keyring] \
https://artifacts.opensearch.org/releases/bundle/opensearch/2.x/apt stable main" \
| tee /etc/apt/sources.list.d/opensearch-2.x.list
echo "deb [signed-by=/usr/share/keyrings/opensearch-keyring] \
https://artifacts.opensearch.org/releases/bundle/opensearch-dashboards/2.x/apt stable main" \
| tee /etc/apt/sources.list.d/opensearch-dashboards-2.x.list
# 2) 설치 (2.12+ 는 최초 admin 비밀번호를 환경변수로 요구)
apt-get update
env OPENSEARCH_INITIAL_ADMIN_PASSWORD='Seekers-Lab-2026!' \
apt-get install -y opensearch opensearch-dashboards
# 3) 실습용 설정 (보안 플러그인 비활성 — 학습 전용)
printf '%s\n' 'plugins.security.disabled: true' 'network.host: 0.0.0.0' 'discovery.type: single-node' \
>> /etc/opensearch/opensearch.yml
# 4) 실행
sudo -u opensearch /usr/share/opensearch/bin/opensearch &
sudo -u opensearch-dashboards /usr/share/opensearch-dashboards/bin/opensearch-dashboards &
# 5) 확인
curl http://localhost:9200 # OpenSearch JSON
# 브라우저: http://localhost:5601 (Dashboards)
# docker-compose.yml — 단일 노드 OpenSearch + Dashboards (학습용, 보안 비활성)
services:
opensearch:
image: opensearchproject/opensearch:2
environment:
- discovery.type=single-node
- plugins.security.disabled=true
- "OPENSEARCH_JAVA_OPTS=-Xms1g -Xmx1g"
ports: ["9200:9200"]
dashboards:
image: opensearchproject/opensearch-dashboards:2
environment:
- OPENSEARCH_HOSTS=http://opensearch:9200
- DISABLE_SECURITY_DASHBOARDS_PLUGIN=true
ports: ["5601:5601"]
docker compose up -d # http://localhost:5601
docker compose down
| 증상 | 원인 · 해결 |
|---|---|
엔진이 바로 죽음 / max virtual memory areas | 호스트에서 sysctl -w vm.max_map_count=262144 |
can not run elasticsearch as root | 서비스 유저로 실행: sudo -u elasticsearch … |
| 느리거나 OOM 종료 | heap 을 RAM 의 50% 이하로(-Xms1g -Xmx1g), 컨테이너 메모리 4GB+ |
systemctl 안 됨 | 플레인 컨테이너엔 systemd 없음 → 바이너리 직접 실행, 또는 --privileged+init 이미지 |
| Dashboards 가 엔진에 연결 못 함 | OPENSEARCH_HOSTS/elasticsearch.hosts 확인, 엔진이 먼저 떠야 함 |
_bulk 예제를 로컬 설치본에도 동일하게 사용.