본문 바로가기
서버구축 (WEB,DB)

Wazuh Indexer(OpenSearch) 모니터링 표준 메트릭, 대시보드, 경보 관제

by 날으는물고기 2025. 11. 15.

Wazuh Indexer(OpenSearch) 모니터링 표준 메트릭, 대시보드, 경보 관제

728x90

아래 구성은 3개 노드 Wazuh Indexer(OpenSearch) 클러스터를 대상으로,

  1. Prometheus Exporter로 노드 성능 지표 수집 → Prometheus/Grafana 시각화,
  2. ElastAlert2로 OpenSearch 쿼리 기반 탐지 → Slack 알림,
  3. 오류 해결(매핑/호환모드/TLS),
  4. Dev Tools(콘솔)에서 바로 실행할 스니펫

까지 한 번에 정리한 운영 표준 레시피입니다.

인프라 구성 개요(아키텍처)

  • OpenSearch(Wazuh Indexer) ×3
    • 각 노드에 opensearch-prometheus-exporter 플러그인 설치
    • /_prometheus/metrics에서 노드 지표 노출
  • Prometheus
    • 각 노드 /_prometheus/metrics를 스크레이프
    • (선택) Alertmanager 연동
  • Grafana
    • Prometheus를 데이터소스로 등록
    • 대시보드/Rule 알림 구성
  • ElastAlert2
    • OpenSearch에 쿼리 → 조건 매칭 시 Slack 알림
    • 내부 writeback 인덱스(elastalert_status 등) 사용
  • 보안
    • TLS + 전용 모니터/읽기 계정 + 내부망 스크레이프 권장

Indexer 이미지 커스터마이징(Exporter 포함)

1) Dockerfile (예시)

# Dockerfile.wazuh-indexer-exporter
FROM wazuh/wazuh-indexer:4.14.1
ENV OPENSEARCH_HOME=/usr/share/opensearch
RUN ${OPENSEARCH_HOME}/bin/opensearch-plugin install --batch \
  https://github.com/opensearch-project/opensearch-prometheus-exporter/releases/download/3.3.2.0/prometheus-exporter-3.3.2.0.zip
# ↑ 실제 Indexer 내장 OpenSearch 버전에 호환되는 Exporter 릴리스를 선택하세요.

※ 버전 호환이 가장 중요합니다. 설치 실패 시 Exporter 버전을 상/하 조정하세요.

2) opensearch.yml(Prometheus 노출 옵션)

prometheus.metric_name.prefix: "opensearch_"
prometheus.nodes.filter: "_all"   # 기본은 _local
# (권장) ES7 호환 응답 끄기 — ElastAlert2 매핑 오류 예방
compatibility:
  override_main_response_version: false

docker-compose.yml에서 해당 파일을 각 노드에 마운트하세요.

docker-compose

version: "3.8"
services:
  wazuh1.indexer:
    build: { context: ., dockerfile: Dockerfile.wazuh-indexer-exporter }
    image: local/wazuh-indexer-exporter:4.14.1
    hostname: wazuh1.indexer
    environment:
      - cluster.name=wazuh
      - node.name=wazuh1.indexer
      - plugins.security.disabled=true   # 테스트용. 운영은 TLS/보안 활성화 권장
    ports:
      - "9200:9200"
    volumes:
      - ./indexer-config/opensearch.yml:/usr/share/opensearch/config/opensearch.yml:ro
    ulimits:
      memlock: { soft: -1, hard: -1 }
      nofile: { soft: 65536, hard: 65536 }

  wazuh2.indexer:
    image: local/wazuh-indexer-exporter:4.14.1
    hostname: wazuh2.indexer
    environment:
      - cluster.name=wazuh
      - node.name=wazuh2.indexer
      - plugins.security.disabled=true
    ports: [ "9201:9200" ]
    volumes:
      - ./indexer-config/opensearch.yml:/usr/share/opensearch/config/opensearch.yml:ro
    ulimits:
      memlock: { soft: -1, hard: -1 }
      nofile: { soft: 65536, hard: 65536 }

  wazuh3.indexer:
    image: local/wazuh-indexer-exporter:4.14.1
    hostname: wazuh3.indexer
    environment:
      - cluster.name=wazuh
      - node.name=wazuh3.indexer
      - plugins.security.disabled=true
    ports: [ "9202:9200" ]
    volumes:
      - ./indexer-config/opensearch.yml:/usr/share/opensearch/config/opensearch.yml:ro
    ulimits:
      memlock: { soft: -1, hard: -1 }
      nofile: { soft: 65536, hard: 65536 }

  prometheus:
    image: prom/prometheus:v2.55.0
    container_name: prometheus
    volumes:
      - ./prometheus/prometheus.yml:/etc/prometheus/prometheus.yml:ro
      - prometheus-data:/prometheus
    command:
      - --config.file=/etc/prometheus/prometheus.yml
      - --storage.tsdb.retention.time=15d
    ports: [ "9090:9090" ]
    depends_on: [ wazuh1.indexer, wazuh2.indexer, wazuh3.indexer ]

  grafana:
    image: grafana/grafana:11.2.0
    container_name: grafana
    environment:
      - GF_SECURITY_ADMIN_USER=admin
      - GF_SECURITY_ADMIN_PASSWORD=admin123!
    ports: [ "3000:3000" ]
    volumes:
      - grafana-data:/var/lib/grafana
    depends_on: [ prometheus ]

volumes:
  prometheus-data:
  grafana-data:

Prometheus 스크레이프 설정

./prometheus/prometheus.yml

global:
  scrape_interval: 15s
  evaluation_interval: 15s

scrape_configs:
  - job_name: opensearch-indexer
    metrics_path: /_prometheus/metrics
    scheme: http    # 운영은 https 권장
    static_configs:
      - targets:
          - wazuh1.indexer:9200
          - wazuh2.indexer:9200
          - wazuh3.indexer:9200
    # 보안 활성 시:
    # basic_auth:
    #   username: monitor
    #   password: ${OPENSEARCH_MONITOR_PASSWORD}
    # tls_config:
    #   ca_file: /etc/prometheus/certs/ca.crt

확인

docker compose up -d
curl http://localhost:9200/_prometheus/metrics | head
# Prometheus UI: http://localhost:9090 → Status > Targets → UP 확인

Grafana 대시보드(빠른 시작)

  • 데이터소스 등록: URL http://prometheus:9090
  • 샘플 쿼리
    • CPU: avg by (instance) (opensearch_os_cpu_percent)
    • Heap%: avg by (instance) (opensearch_jvm_mem_heap_used_percent)
    • Rejected(write): rate(opensearch_thread_pool_write_rejected_total[5m])
  • 알림 예시(Grafana Rule)
    • Heap% > 75% 5분 지속 → Slack/Webhook

ElastAlert2 — 설치/전역 설정/Slack/룰

1) Docker Compose

version: "3.8"
services:
  elastalert:
    image: jertel/elastalert2:latest
    container_name: elastalert2
    volumes:
      - ./elastalert/config.yaml:/opt/elastalert/config.yaml:ro
      - ./elastalert/rules:/opt/elastalert/rules:ro
      - ./certs/ca.crt:/opt/elastalert/certs/ca.crt:ro
    environment:
      - TZ=Asia/Seoul
    depends_on:
      - wazuh1.indexer
    restart: unless-stopped

2) config.yaml(전역)

es_url: "https://wazuh1.indexer:9200"
use_opensearch: true
use_ssl: true
verify_certs: true
ca_certs: /opt/elastalert/certs/ca.crt
es_username: "monitor"
es_password: "********"

writeback_index: "elastalert_status"

run_every:   { minutes: 1 }
buffer_time: { minutes: 15 }

logging: { level: INFO }

중요: OpenSearch의 opensearch.yml에서 compatibility.override_main_response_version: false 권장.
(ES7 호환 응답이 켜져 있으면 include_type_name 에러 유발 가능)

3) Slack Webhook

  • Slack 앱 → Incoming Webhooks 활성화 → URL 발급
  • 룰에 slack_webhook_url 기입(보안상 Secret/환경변수 권장)

4) 룰 예시(3종)

Wazuh 고심각 경보 버스트(5분 10건)
./elastalert/rules/wazuh_severity_high.yaml

name: Wazuh High-Severity Burst
type: frequency
index: wazuh-alerts-4.*
num_events: 10
timeframe: { minutes: 5 }
filter:
  - query:
      query_string:
        query: 'rule.level:>=10'
query_key: agent.name
realert: { minutes: 10 }
alert: slack
slack_webhook_url: "https://hooks.slack.com/services/XXX/YYY/ZZZ"
slack_emoji_override: ":rotating_light:"
slack_msg_color: "danger"
alert_text_type: alert_text_only
alert_text: |
  🚨 *고심각 경보 버스트*
  • 에이전트: {0}
  • 5분 내 건수: {1}
  • 예: {2}
alert_text_args: [agent.name, num_matches, rule.description]

Nginx 5xx 스파이크(10분 3배 증가)
./elastalert/rules/nginx_5xx_spike.yaml

name: Nginx 5xx Spike
type: spike
index: logs-nginx-*
timeframe: { minutes: 10 }
spike_height: 3
spike_type: up
filter: [ { term: { response_code: "500" } } ]
query_key: host.name
realert: { minutes: 15 }
alert: slack
slack_webhook_url: "https://hooks.slack.com/services/XXX/YYY/ZZZ"
slack_msg_color: "warning"
alert_text_type: alert_text_only
alert_text: |
  📈 *5xx 급증 감지*
  • 호스트: {0}
  • 10분 기준 과거 대비 3배 ↑
alert_text_args: [host.name]

에이전트 침묵(flatline, 10분 무이벤트)
./elastalert/rules/agent_flatline.yaml

name: Agent Flatline (No Events)
type: flatline
index: wazuh-alerts-4.*
threshold: 1
timeframe: { minutes: 10 }
filter: [ { term: { agent.name: "web-01" } } ]
alert: slack
slack_webhook_url: "https://hooks.slack.com/services/XXX/YYY/ZZZ"
slack_msg_color: "danger"
alert_text_type: alert_text_only
alert_text: |
  💤 *에이전트 이벤트 끊김*
  • 대상: web-01
  • 지난 10분 동안 이벤트 없음

5) writeback 인덱스 생성(Dev Tools 권장)

include_type_name 오류를 피하려면 Dev Tools에서 수동 생성이 가장 확실합니다.
(템플릿 1개 + 본체 인덱스 or 5개 전체 인덱스 수동 생성)

방법 A) 템플릿 + 본체(권장)

# (기존 잘못된 인덱스가 있으면 정리)
DELETE elastalert_status_status
DELETE elastalert_status_error
DELETE elastalert_status_past
DELETE elastalert_status_silence
# 필요 시: DELETE elastalert_status

# 템플릿
PUT _index_template/elastalert_status_template
{
  "index_patterns": ["elastalert_status*"],
  "template": {
    "settings": { "number_of_shards": 1, "number_of_replicas": 1 },
    "mappings": {
      "dynamic": true,
      "properties": {
        "alert_time":   { "type": "date" },
        "alert_sent":   { "type": "boolean" },
        "aggregate_id": { "type": "keyword" },
        "rule_name":    { "type": "keyword" },
        "match_body":   { "type": "object", "enabled": false },
        "alert":        { "type": "object", "enabled": false }
      }
    }
  }
}

# 본체 인덱스 생성
PUT elastalert_status

방법 B) 5개 인덱스 모두 수동 생성

(필수 핵심 필드 타입만 정확히 지정)

PUT elastalert_status
{
  "settings": { "number_of_shards": 1, "number_of_replicas": 1 },
  "mappings": {
    "dynamic": true,
    "properties": {
      "alert_time":   { "type": "date" },
      "alert_sent":   { "type": "boolean" },
      "aggregate_id": { "type": "keyword" },
      "rule_name":    { "type": "keyword" },
      "match_body":   { "type": "object", "enabled": false },
      "alert":        { "type": "object", "enabled": false }
    }
  }
}

PUT elastalert_status_status
{
  "settings": { "number_of_shards": 1, "number_of_replicas": 1 },
  "mappings": {
    "dynamic": true,
    "properties": {
      "timestamp": { "type": "date" },
      "rule_name": { "type": "keyword" },
      "status":    { "type": "keyword" },
      "error":     { "type": "text" }
    }
  }
}

PUT elastalert_status_error
{
  "settings": { "number_of_shards": 1, "number_of_replicas": 1 },
  "mappings": {
    "dynamic": true,
    "properties": {
      "@timestamp": { "type": "date" },
      "rule_name":  { "type": "keyword" },
      "alert_time": { "type": "date" },
      "message":    { "type": "text" }
    }
  }
}

PUT elastalert_status_past
{
  "settings": { "number_of_shards": 1, "number_of_replicas": 1 },
  "mappings": {
    "dynamic": true,
    "properties": {
      "alert_time":   { "type": "date" },
      "rule_name":    { "type": "keyword" },
      "aggregate_id": { "type": "keyword" },
      "match_body":   { "type": "object", "enabled": false }
    }
  }
}

PUT elastalert_status_silence
{
  "settings": { "number_of_shards": 1, "number_of_replicas": 1 },
  "mappings": {
    "dynamic": true,
    "properties": {
      "rule_name": { "type": "keyword" },
      "until":     { "type": "date" }
    }
  }
}

6) 인덱스/매핑 점검(Dev Tools)

GET elastalert_status/_mapping

# 핵심: alert_time 정렬 가능해야 함
GET elastalert_status/_search?size=1&sort=alert_time:asc
{
  "query": { "match_all": {} }
}

7) 룰 테스트/실행

# 테스트
docker run --rm \
  -v $(pwd)/elastalert/config.yaml:/opt/elastalert/config.yaml:ro \
  -v $(pwd)/elastalert/rules:/opt/elastalert/rules:ro \
  jertel/elastalert2:latest \
  elastalert-test-rule --config /opt/elastalert/config.yaml /opt/elastalert/rules/wazuh_severity_high.yaml

# 본 실행
docker compose -f docker-compose.elastalert.yml up -d
docker logs -f elastalert2

운영 가이드

  1. TLS 필수 + CA 검증
    • ElastAlert2 verify_certs: true, ca_certs 마운트
    • Prometheus는 내부망에서만 스크레이프(필요시 mTLS/BasicAuth)
  2. 전용 계정(최소 권한)
    • OpenSearch: 모니터/읽기 전용 Role
    • Slack Webhook: Secret/환경변수 관리
  3. ES7 호환 응답 끄기(중요)
    • compatibility.override_main_response_version: false
    • include_type_name 매핑 오류 방지
  4. 소음 억제
    • ElastAlert 룰: realert, query_key 사용
    • Grafana/Prometheus 알림: 지연/지속시간 조건 설정
  5. 보관/성능
    • elastalert_status*에 ISM/ILM 적용(롤오버+삭제)
    • Prometheus 보관기간/스크레이프 주기 조정(예: 15s/15d)

트러블슈팅(핵심 에러별)

  • InsecureRequestWarning
    → TLS 검증 미설정. verify_certs: true, ca_certs 설정/마운트.
  • include_type_name 400
    → OpenSearch 호환 응답 ON 또는 ElastAlert2 구버전.
    1. override_main_response_version: false
    2. jertel/elastalert2:latest 갱신
    3. Dev Tools로 수동 인덱스 생성
  • No mapping found for [alert_time]
    elastalert_status 매핑에 alert_time: date 없음.
    → Dev Tools로 템플릿/인덱스를 위 스니펫대로 재생성.
  • Prometheus Target DOWN
    → 네트워크/포트/DNS 점검. 컨테이너 내에서 curl http://wazuh1.indexer:9200/_prometheus/metrics.
  • Slack 미수신
    → 잘못된 Webhook, 채널 권한, 방화벽 확인. ElastAlert 로그에서 alert_sent 여부 확인.

설치·검증 체크리스트

  • 각 Indexer에 Exporter 플러그인 정상 설치
  • /_prometheus/metrics로 메트릭 수집 확인
  • Prometheus Target UP
  • Grafana에서 지표 시각화/알림 구성
  • OpenSearch compatibility.override_main_response_version: false
  • Dev Tools로 writeback 인덱스(템플릿/매핑) 정상화
  • ElastAlert2 룰 테스트 통과 → Slack 수신 확인
  • TLS/계정/ISM 정책 등 보안·보관 기준 충족

Dev Tools 빠른 진단 스니펫 모음

# 메트릭 확인(Exporter)
GET _prometheus/metrics

# 인덱스/매핑 확인
GET elastalert_status/_mapping

# alert_time 정렬 테스트
GET elastalert_status/_search?size=1&sort=alert_time:asc
{
  "query": { "match_all": {} }
}

# 템플릿 조회
GET _index_template/elastalert_status_template

# (옵션) ISM 정책 확인
GET _plugins/_ism/policies/elastalert-status-retention
728x90
그리드형(광고전용)

댓글