- Add YARA guide, WAF integration docs, and webshell rules - Ignore *.lock files in .gitignore Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1126 lines
30 KiB
Markdown
1126 lines
30 KiB
Markdown
# HAProxy WAF 통합 가이드
|
|
|
|
ModSecurity와 CrowdSec을 HAProxy와 연동하여 웹 애플리케이션 방화벽(WAF)을 구성하는 방법을 설명합니다.
|
|
|
|
## 목차
|
|
|
|
1. [개요](#개요)
|
|
2. [아키텍처](#아키텍처)
|
|
3. [ModSecurity 구성](#modsecurity-구성)
|
|
4. [CrowdSec 구성](#crowdsec-구성)
|
|
5. [HAProxy 연동](#haproxy-연동)
|
|
6. [별도 서버 구성](#별도-서버-구성)
|
|
7. [테스트 방법](#테스트-방법)
|
|
8. [운영 명령어](#운영-명령어)
|
|
|
|
---
|
|
|
|
## 개요
|
|
|
|
### ModSecurity vs CrowdSec
|
|
|
|
| 구분 | ModSecurity | CrowdSec |
|
|
|------|-------------|----------|
|
|
| **역할** | WAF (웹 애플리케이션 방화벽) | 침입 탐지/차단 시스템 (IDS/IPS) |
|
|
| **분석 대상** | HTTP 요청/응답 내용 | IP 기반 행위 패턴 |
|
|
| **탐지 방식** | 시그니처 기반 (OWASP CRS) | 행위 기반 + 커뮤니티 위협 인텔리전스 |
|
|
| **차단 범위** | 개별 악성 요청 | IP 단위 (반복 공격자) |
|
|
| **주요 탐지** | SQL Injection, XSS, RCE, LFI/RFI | 브루트포스, 스캐닝, DDoS, 알려진 악성 IP |
|
|
| **성능 영향** | 높음 (모든 요청 검사) | 낮음 (IP 조회만) |
|
|
|
|
### 권장 조합
|
|
|
|
```
|
|
CrowdSec (먼저) → ModSecurity (나중)
|
|
```
|
|
|
|
- **CrowdSec**: 알려진 악성 IP를 빠르게 차단 (낮은 지연)
|
|
- **ModSecurity**: 나머지 요청에 대해 상세 분석 (깊은 검사)
|
|
|
|
---
|
|
|
|
## 아키텍처
|
|
|
|
### 동일 서버 구성 (권장)
|
|
|
|
```
|
|
┌─────────────────────────────────────────────────────────────────────────┐
|
|
│ Host Server │
|
|
│ │
|
|
│ ┌──────────────────────────────────────────────────────────────────┐ │
|
|
│ │ HAProxy (host network) │ │
|
|
│ │ TCP 80/443 + UDP 443 │ │
|
|
│ │ │ │
|
|
│ │ ┌─────────────┐ ┌─────────────┐ │ │
|
|
│ │ │ SPOE Agent │ │ SPOE Agent │ │ │
|
|
│ │ │ (CrowdSec) │ │(ModSecurity)│ │ │
|
|
│ │ └──────┬──────┘ └──────┬──────┘ │ │
|
|
│ └───────────┼───────────────────┼──────────────────────────────────┘ │
|
|
│ │ │ │
|
|
│ │ TCP :8888 │ TCP :12345 │
|
|
│ ▼ ▼ │
|
|
│ ┌───────────────────┐ ┌───────────────────┐ ┌───────────────────┐ │
|
|
│ │ CrowdSec Bouncer │ │ ModSecurity SPOA │ │ CrowdSec Engine │ │
|
|
│ │ (haproxy bouncer)│ │ (modsecurity-spoa)│ │ (crowdsec) │ │
|
|
│ │ Podman Container │ │ Podman Container │ │ Podman Container │ │
|
|
│ │ │ │ │ │ │ │
|
|
│ │ - API Key 검증 │ │ - OWASP CRS 3.x │ │ - 로그 분석 │ │
|
|
│ │ - LAPI 연동 │ │ - 요청 검사 │ │ - 시나리오 탐지 │ │
|
|
│ └─────────┬─────────┘ └───────────────────┘ │ - 알림 발송 │ │
|
|
│ │ └─────────┬─────────┘ │
|
|
│ │ TCP :8080 │ │
|
|
│ └──────────────────────────────────────────────┘ │
|
|
│ (LAPI 연동) │
|
|
└─────────────────────────────────────────────────────────────────────────┘
|
|
```
|
|
|
|
### 별도 서버 구성
|
|
|
|
```
|
|
┌──────────────────────┐ ┌──────────────────────┐
|
|
│ HAProxy Server │ │ Security Server │
|
|
│ │ │ │
|
|
│ ┌────────────────┐ │ │ ┌────────────────┐ │
|
|
│ │ HAProxy │ │ TCP │ │ CrowdSec │ │
|
|
│ │ (host network)│──┼──:8888───┼─▶│ Bouncer │ │
|
|
│ │ │ │ │ └───────┬────────┘ │
|
|
│ │ │──┼─:12345───┼─▶│ │ │
|
|
│ └────────────────┘ │ │ │ ModSecurity │ │
|
|
│ │ │ │ SPOA │ │
|
|
│ │ │ └────────────────┘ │
|
|
│ │ │ │
|
|
│ │ │ ┌────────────────┐ │
|
|
│ │ │ │ CrowdSec │ │
|
|
│ │ │ │ Engine │ │
|
|
│ │ │ └────────────────┘ │
|
|
└──────────────────────┘ └──────────────────────┘
|
|
```
|
|
|
|
---
|
|
|
|
## ModSecurity 구성
|
|
|
|
### 디렉토리 구조
|
|
|
|
```bash
|
|
mkdir -p /opt/haproxy/modsecurity/{conf,rules,logs}
|
|
```
|
|
|
|
```
|
|
/opt/haproxy/modsecurity/
|
|
├── conf/
|
|
│ ├── modsecurity.conf # ModSecurity 메인 설정
|
|
│ └── crs-setup.conf # OWASP CRS 설정
|
|
├── rules/
|
|
│ └── (OWASP CRS 룰셋)
|
|
├── logs/
|
|
│ └── modsec_audit.log
|
|
└── Dockerfile
|
|
```
|
|
|
|
### Dockerfile
|
|
|
|
```dockerfile
|
|
# /opt/haproxy/modsecurity/Dockerfile
|
|
FROM alpine:3.19
|
|
|
|
# 빌드 의존성 설치
|
|
RUN apk add --no-cache \
|
|
git \
|
|
build-base \
|
|
autoconf \
|
|
automake \
|
|
libtool \
|
|
pcre2-dev \
|
|
yajl-dev \
|
|
curl-dev \
|
|
libxml2-dev \
|
|
lmdb-dev \
|
|
lua5.4-dev \
|
|
geoip-dev \
|
|
libmaxminddb-dev \
|
|
linux-headers \
|
|
haproxy
|
|
|
|
# ModSecurity v3 빌드
|
|
WORKDIR /build
|
|
RUN git clone --depth 1 https://github.com/owasp-modsecurity/ModSecurity.git && \
|
|
cd ModSecurity && \
|
|
git submodule init && \
|
|
git submodule update && \
|
|
./build.sh && \
|
|
./configure --with-pcre2 --with-lmdb && \
|
|
make -j$(nproc) && \
|
|
make install
|
|
|
|
# SPOA 에이전트 빌드
|
|
RUN git clone --depth 1 https://github.com/haproxy/spoa-modsecurity.git && \
|
|
cd spoa-modsecurity && \
|
|
make MODSEC_INC=/usr/local/modsecurity/include \
|
|
MODSEC_LIB=/usr/local/modsecurity/lib \
|
|
APACHE2_INC=/usr/include/apache2
|
|
|
|
# OWASP CRS 다운로드
|
|
RUN git clone --depth 1 https://github.com/coreruleset/coreruleset.git /opt/owasp-crs && \
|
|
cp /opt/owasp-crs/crs-setup.conf.example /opt/owasp-crs/crs-setup.conf
|
|
|
|
# 런타임 이미지
|
|
FROM alpine:3.19
|
|
|
|
RUN apk add --no-cache \
|
|
pcre2 \
|
|
yajl \
|
|
libcurl \
|
|
libxml2 \
|
|
lmdb \
|
|
lua5.4-libs \
|
|
geoip \
|
|
libmaxminddb
|
|
|
|
# 빌드 결과 복사
|
|
COPY --from=0 /usr/local/modsecurity /usr/local/modsecurity
|
|
COPY --from=0 /build/spoa-modsecurity/modsecurity /usr/local/bin/modsecurity-spoa
|
|
COPY --from=0 /opt/owasp-crs /opt/owasp-crs
|
|
|
|
# 라이브러리 경로 설정
|
|
ENV LD_LIBRARY_PATH=/usr/local/modsecurity/lib
|
|
|
|
# 설정 디렉토리
|
|
VOLUME ["/etc/modsecurity", "/var/log/modsecurity"]
|
|
|
|
# SPOA 포트
|
|
EXPOSE 12345
|
|
|
|
# 실행
|
|
ENTRYPOINT ["/usr/local/bin/modsecurity-spoa"]
|
|
CMD ["-f", "/etc/modsecurity/modsecurity.conf", "-a", "0.0.0.0", "-p", "12345", "-n", "4"]
|
|
```
|
|
|
|
### ModSecurity 설정 파일
|
|
|
|
```apache
|
|
# /opt/haproxy/modsecurity/conf/modsecurity.conf
|
|
|
|
# 기본 설정
|
|
SecRuleEngine On
|
|
SecRequestBodyAccess On
|
|
SecResponseBodyAccess Off
|
|
|
|
# 요청 본문 설정
|
|
SecRequestBodyLimit 13107200
|
|
SecRequestBodyNoFilesLimit 131072
|
|
SecRequestBodyLimitAction Reject
|
|
|
|
# 임시 파일 경로
|
|
SecTmpDir /tmp/
|
|
SecDataDir /tmp/
|
|
|
|
# 로깅 설정
|
|
SecAuditEngine RelevantOnly
|
|
SecAuditLogRelevantStatus "^(?:5|4(?!04))"
|
|
SecAuditLogParts ABIJDEFHZ
|
|
SecAuditLogType Serial
|
|
SecAuditLog /var/log/modsecurity/modsec_audit.log
|
|
|
|
# 디버그 로그 (문제 해결시 활성화)
|
|
# SecDebugLog /var/log/modsecurity/debug.log
|
|
# SecDebugLogLevel 3
|
|
|
|
# 응답 상태
|
|
SecStatusEngine On
|
|
|
|
# 유니코드 매핑
|
|
SecUnicodeMapFile unicode.mapping 20127
|
|
|
|
# PCRE 설정
|
|
SecPcreMatchLimit 100000
|
|
SecPcreMatchLimitRecursion 100000
|
|
|
|
# OWASP CRS 포함
|
|
Include /opt/owasp-crs/crs-setup.conf
|
|
Include /opt/owasp-crs/rules/*.conf
|
|
```
|
|
|
|
### OWASP CRS 설정
|
|
|
|
```apache
|
|
# /opt/haproxy/modsecurity/conf/crs-setup.conf
|
|
|
|
# Paranoia Level (1-4, 높을수록 엄격)
|
|
SecAction \
|
|
"id:900000,\
|
|
phase:1,\
|
|
pass,\
|
|
t:none,\
|
|
nolog,\
|
|
setvar:tx.blocking_paranoia_level=1"
|
|
|
|
# 탐지 Paranoia Level
|
|
SecAction \
|
|
"id:900001,\
|
|
phase:1,\
|
|
pass,\
|
|
t:none,\
|
|
nolog,\
|
|
setvar:tx.detection_paranoia_level=1"
|
|
|
|
# 차단 임계값 설정
|
|
SecAction \
|
|
"id:900110,\
|
|
phase:1,\
|
|
pass,\
|
|
t:none,\
|
|
nolog,\
|
|
setvar:tx.inbound_anomaly_score_threshold=5,\
|
|
setvar:tx.outbound_anomaly_score_threshold=4"
|
|
|
|
# 특정 규칙 제외 (오탐 방지)
|
|
# SecRuleRemoveById 920350
|
|
# SecRuleRemoveById 942100
|
|
|
|
# 특정 경로 제외
|
|
SecRule REQUEST_URI "@beginsWith /api/upload" \
|
|
"id:1001,\
|
|
phase:1,\
|
|
pass,\
|
|
t:none,\
|
|
nolog,\
|
|
ctl:ruleRemoveById=920420"
|
|
|
|
# 허용 IP (내부망)
|
|
SecRule REMOTE_ADDR "@ipMatch 10.0.0.0/8,172.16.0.0/12,192.168.0.0/16,100.64.0.0/10" \
|
|
"id:1002,\
|
|
phase:1,\
|
|
pass,\
|
|
t:none,\
|
|
nolog,\
|
|
ctl:ruleEngine=DetectionOnly"
|
|
```
|
|
|
|
### ModSecurity Quadlet
|
|
|
|
```ini
|
|
# /etc/containers/systemd/modsecurity.container
|
|
|
|
[Unit]
|
|
Description=ModSecurity SPOA Agent
|
|
After=network-online.target
|
|
Wants=network-online.target
|
|
|
|
[Container]
|
|
Image=localhost/modsecurity-spoa:latest
|
|
ContainerName=modsecurity
|
|
AutoUpdate=local
|
|
|
|
# 네트워크 설정
|
|
PublishPort=12345:12345
|
|
|
|
# 볼륨 마운트
|
|
Volume=/opt/haproxy/modsecurity/conf:/etc/modsecurity:ro,Z
|
|
Volume=/opt/haproxy/modsecurity/logs:/var/log/modsecurity:Z
|
|
|
|
# 환경 변수
|
|
Environment=LD_LIBRARY_PATH=/usr/local/modsecurity/lib
|
|
|
|
# 리소스 제한
|
|
Memory=512M
|
|
CPUQuota=100%
|
|
|
|
[Service]
|
|
Restart=always
|
|
RestartSec=5
|
|
TimeoutStartSec=60
|
|
|
|
[Install]
|
|
WantedBy=multi-user.target
|
|
```
|
|
|
|
### ModSecurity 이미지 빌드
|
|
|
|
```bash
|
|
# 이미지 빌드
|
|
cd /opt/haproxy/modsecurity
|
|
podman build -t localhost/modsecurity-spoa:latest .
|
|
|
|
# Quadlet 활성화
|
|
systemctl daemon-reload
|
|
systemctl start modsecurity
|
|
systemctl enable modsecurity
|
|
|
|
# 상태 확인
|
|
systemctl status modsecurity
|
|
podman logs -f modsecurity
|
|
```
|
|
|
|
---
|
|
|
|
## CrowdSec 구성
|
|
|
|
### 디렉토리 구조
|
|
|
|
```bash
|
|
mkdir -p /opt/haproxy/crowdsec/{config,data,hub}
|
|
mkdir -p /opt/haproxy/crowdsec-bouncer
|
|
```
|
|
|
|
```
|
|
/opt/haproxy/crowdsec/
|
|
├── config/
|
|
│ ├── config.yaml # CrowdSec 메인 설정
|
|
│ ├── acquis.yaml # 로그 수집 설정
|
|
│ └── local_api_credentials.yaml
|
|
├── data/
|
|
│ └── (데이터베이스)
|
|
└── hub/
|
|
└── (시나리오, 파서)
|
|
|
|
/opt/haproxy/crowdsec-bouncer/
|
|
└── config.yaml # HAProxy Bouncer 설정
|
|
```
|
|
|
|
### CrowdSec Engine Quadlet
|
|
|
|
```ini
|
|
# /etc/containers/systemd/crowdsec.container
|
|
|
|
[Unit]
|
|
Description=CrowdSec Security Engine
|
|
After=network-online.target
|
|
Wants=network-online.target
|
|
|
|
[Container]
|
|
Image=docker.io/crowdsecurity/crowdsec:latest
|
|
ContainerName=crowdsec
|
|
AutoUpdate=registry
|
|
|
|
# 네트워크 설정
|
|
PublishPort=8080:8080
|
|
PublishPort=6060:6060
|
|
|
|
# 볼륨 마운트
|
|
Volume=/opt/haproxy/crowdsec/config:/etc/crowdsec:Z
|
|
Volume=/opt/haproxy/crowdsec/data:/var/lib/crowdsec/data:Z
|
|
Volume=/opt/haproxy/crowdsec/hub:/var/lib/crowdsec/hub:Z
|
|
|
|
# HAProxy 로그 접근 (호스트 로그)
|
|
Volume=/var/log:/var/log:ro
|
|
|
|
# 환경 변수
|
|
Environment=COLLECTIONS="crowdsecurity/haproxy crowdsecurity/http-cve crowdsecurity/linux"
|
|
Environment=GID=1000
|
|
|
|
[Service]
|
|
Restart=always
|
|
RestartSec=10
|
|
TimeoutStartSec=120
|
|
|
|
[Install]
|
|
WantedBy=multi-user.target
|
|
```
|
|
|
|
### CrowdSec 설정 파일
|
|
|
|
```yaml
|
|
# /opt/haproxy/crowdsec/config/config.yaml
|
|
|
|
common:
|
|
daemonize: false
|
|
log_media: stdout
|
|
log_level: info
|
|
log_dir: /var/log/
|
|
working_dir: /var/lib/crowdsec/data/
|
|
|
|
config_paths:
|
|
config_dir: /etc/crowdsec/
|
|
data_dir: /var/lib/crowdsec/data/
|
|
simulation_path: /etc/crowdsec/simulation.yaml
|
|
hub_dir: /var/lib/crowdsec/hub/
|
|
index_path: /var/lib/crowdsec/hub/.index.json
|
|
notification_dir: /etc/crowdsec/notifications/
|
|
plugin_dir: /usr/local/lib/crowdsec/plugins/
|
|
|
|
crowdsec_service:
|
|
acquisition_path: /etc/crowdsec/acquis.yaml
|
|
parser_routines: 1
|
|
|
|
cscli:
|
|
output: human
|
|
|
|
db_config:
|
|
log_level: info
|
|
type: sqlite
|
|
db_path: /var/lib/crowdsec/data/crowdsec.db
|
|
|
|
api:
|
|
client:
|
|
insecure_skip_verify: false
|
|
credentials_path: /etc/crowdsec/local_api_credentials.yaml
|
|
server:
|
|
log_level: info
|
|
listen_uri: 0.0.0.0:8080
|
|
profiles_path: /etc/crowdsec/profiles.yaml
|
|
online_client:
|
|
credentials_path: /etc/crowdsec/online_api_credentials.yaml
|
|
|
|
prometheus:
|
|
enabled: true
|
|
level: full
|
|
listen_addr: 0.0.0.0
|
|
listen_port: 6060
|
|
```
|
|
|
|
### 로그 수집 설정
|
|
|
|
```yaml
|
|
# /opt/haproxy/crowdsec/config/acquis.yaml
|
|
|
|
# HAProxy 로그 (호스트에서 rsyslog/journald로 수집)
|
|
filenames:
|
|
- /var/log/haproxy.log
|
|
labels:
|
|
type: haproxy
|
|
---
|
|
# syslog에서 HAProxy 로그 수집
|
|
filenames:
|
|
- /var/log/syslog
|
|
- /var/log/messages
|
|
labels:
|
|
type: syslog
|
|
---
|
|
# HAProxy 컨테이너 로그 (journald)
|
|
source: journalctl
|
|
journalctl_filter:
|
|
- "_SYSTEMD_UNIT=haproxy.service"
|
|
labels:
|
|
type: haproxy
|
|
```
|
|
|
|
### HAProxy Bouncer Quadlet
|
|
|
|
```ini
|
|
# /etc/containers/systemd/crowdsec-bouncer.container
|
|
|
|
[Unit]
|
|
Description=CrowdSec HAProxy Bouncer
|
|
After=crowdsec.service
|
|
Requires=crowdsec.service
|
|
|
|
[Container]
|
|
Image=docker.io/crowdsecurity/crowdsec-haproxy-bouncer:latest
|
|
ContainerName=crowdsec-bouncer
|
|
AutoUpdate=registry
|
|
|
|
# 네트워크 설정 (HAProxy가 연결할 포트)
|
|
PublishPort=8888:8888
|
|
|
|
# 설정 파일
|
|
Volume=/opt/haproxy/crowdsec-bouncer:/etc/crowdsec:Z
|
|
|
|
# 환경 변수 (LAPI 연결 정보)
|
|
Environment=CROWDSEC_LAPI_URL=http://host.containers.internal:8080
|
|
Environment=CROWDSEC_LAPI_KEY=
|
|
|
|
[Service]
|
|
Restart=always
|
|
RestartSec=5
|
|
TimeoutStartSec=30
|
|
# ExecStartPre에서 API 키 설정
|
|
ExecStartPre=/bin/sh -c 'podman exec crowdsec cscli bouncers add haproxy-bouncer -o raw > /opt/haproxy/crowdsec-bouncer/api_key.txt 2>/dev/null || true'
|
|
|
|
[Install]
|
|
WantedBy=multi-user.target
|
|
```
|
|
|
|
### Bouncer 설정 파일
|
|
|
|
```yaml
|
|
# /opt/haproxy/crowdsec-bouncer/config.yaml
|
|
|
|
# SPOE 서버 설정
|
|
listen_addr: 0.0.0.0:8888
|
|
|
|
# CrowdSec LAPI 연결
|
|
crowdsec_lapi_url: http://host.containers.internal:8080
|
|
crowdsec_lapi_key: "${API_KEY}" # 자동 생성됨
|
|
|
|
# 캐시 설정 (성능 최적화)
|
|
cache:
|
|
enabled: true
|
|
ttl: 60s
|
|
size: 10000
|
|
|
|
# 로깅
|
|
log_level: info
|
|
log_mode: stdout
|
|
|
|
# 차단 응답
|
|
ban_response_code: 403
|
|
ban_response_msg: "Access Denied by CrowdSec"
|
|
|
|
# Captcha (선택)
|
|
captcha:
|
|
enabled: false
|
|
```
|
|
|
|
### CrowdSec 초기 설정
|
|
|
|
```bash
|
|
# CrowdSec 시작
|
|
systemctl daemon-reload
|
|
systemctl start crowdsec
|
|
systemctl enable crowdsec
|
|
|
|
# 컬렉션 설치 확인
|
|
podman exec crowdsec cscli collections list
|
|
|
|
# HAProxy 컬렉션 설치 (필요시)
|
|
podman exec crowdsec cscli collections install crowdsecurity/haproxy
|
|
podman exec crowdsec cscli collections install crowdsecurity/http-cve
|
|
|
|
# Bouncer API 키 생성
|
|
podman exec crowdsec cscli bouncers add haproxy-bouncer -o raw
|
|
|
|
# 생성된 키를 bouncer 설정에 추가
|
|
# /opt/haproxy/crowdsec-bouncer/config.yaml의 crowdsec_lapi_key에 설정
|
|
|
|
# Bouncer 시작
|
|
systemctl start crowdsec-bouncer
|
|
systemctl enable crowdsec-bouncer
|
|
|
|
# 연결 확인
|
|
podman exec crowdsec cscli bouncers list
|
|
```
|
|
|
|
---
|
|
|
|
## HAProxy 연동
|
|
|
|
### SPOE 설정 파일
|
|
|
|
#### CrowdSec SPOE
|
|
|
|
```
|
|
# /opt/haproxy/conf/crowdsec-spoe.conf
|
|
|
|
[crowdsec]
|
|
spoe-agent crowdsec-agent
|
|
messages check-client-ip
|
|
option var-prefix crowdsec
|
|
timeout hello 2s
|
|
timeout idle 2m
|
|
timeout processing 10ms
|
|
use-backend crowdsec-backend
|
|
|
|
spoe-message check-client-ip
|
|
args src=src dst=dst method=method path=path query=query version=req.ver headers=req.hdrs body=req.body
|
|
event on-frontend-http-request
|
|
```
|
|
|
|
#### ModSecurity SPOE
|
|
|
|
```
|
|
# /opt/haproxy/conf/modsecurity-spoe.conf
|
|
|
|
[modsecurity]
|
|
spoe-agent modsecurity-agent
|
|
messages check-request
|
|
option var-prefix modsec
|
|
timeout hello 100ms
|
|
timeout idle 30s
|
|
timeout processing 1s
|
|
use-backend modsecurity-backend
|
|
|
|
spoe-message check-request
|
|
args unique-id=unique-id src-ip=src dst-ip=dst dst-port=dst_port method=method path=path query=query version=req.ver headers=req.hdrs body=req.body
|
|
event on-frontend-http-request
|
|
```
|
|
|
|
### HAProxy 설정 수정
|
|
|
|
아래 내용을 `/opt/haproxy/conf/haproxy.cfg`에 추가합니다.
|
|
|
|
#### 방법 1: CrowdSec만 사용
|
|
|
|
```haproxy
|
|
# haproxy.cfg에 추가 (global 섹션 아래)
|
|
|
|
# =============================================================================
|
|
# CrowdSec 연동 (IP 기반 차단)
|
|
# =============================================================================
|
|
|
|
# CrowdSec Bouncer 백엔드
|
|
backend crowdsec-backend
|
|
mode tcp
|
|
server crowdsec 127.0.0.1:8888 check
|
|
|
|
# HTTPS Frontend에 SPOE 필터 추가
|
|
frontend https_front
|
|
# ... 기존 설정 유지 ...
|
|
|
|
# CrowdSec SPOE 필터
|
|
filter spoe engine crowdsec config /usr/local/etc/haproxy/crowdsec-spoe.conf
|
|
|
|
# CrowdSec 차단 (crowdsec.action이 설정되면 차단)
|
|
http-request deny deny_status 403 if { var(sess.crowdsec.action) -m str ban }
|
|
http-request deny deny_status 403 if { var(sess.crowdsec.action) -m str captcha }
|
|
```
|
|
|
|
#### 방법 2: ModSecurity만 사용
|
|
|
|
```haproxy
|
|
# haproxy.cfg에 추가 (global 섹션 아래)
|
|
|
|
# =============================================================================
|
|
# ModSecurity 연동 (WAF)
|
|
# =============================================================================
|
|
|
|
# ModSecurity SPOA 백엔드
|
|
backend modsecurity-backend
|
|
mode tcp
|
|
server modsec 127.0.0.1:12345 check
|
|
|
|
# HTTPS Frontend에 SPOE 필터 추가
|
|
frontend https_front
|
|
# ... 기존 설정 유지 ...
|
|
|
|
# 유니크 ID 생성 (ModSecurity 트랜잭션 추적용)
|
|
unique-id-format %{+X}o\ %ci:%cp_%fi:%fp_%Ts_%rt:%pid
|
|
unique-id-header X-Unique-ID
|
|
|
|
# ModSecurity SPOE 필터
|
|
filter spoe engine modsecurity config /usr/local/etc/haproxy/modsecurity-spoe.conf
|
|
|
|
# ModSecurity 차단 (modsec.code가 0이 아니면 차단)
|
|
http-request deny deny_status 403 if { var(txn.modsec.code) -m int gt 0 }
|
|
```
|
|
|
|
#### 방법 3: CrowdSec + ModSecurity 함께 사용 (권장)
|
|
|
|
```haproxy
|
|
# /opt/haproxy/conf/haproxy.cfg
|
|
|
|
global
|
|
# ... 기존 global 설정 유지 ...
|
|
log stdout format raw local0
|
|
|
|
defaults
|
|
# ... 기존 defaults 설정 유지 ...
|
|
|
|
# =============================================================================
|
|
# WAF 백엔드 (CrowdSec + ModSecurity)
|
|
# =============================================================================
|
|
|
|
# CrowdSec Bouncer 백엔드
|
|
backend crowdsec-backend
|
|
mode tcp
|
|
server crowdsec 127.0.0.1:8888 check
|
|
|
|
# ModSecurity SPOA 백엔드
|
|
backend modsecurity-backend
|
|
mode tcp
|
|
server modsec 127.0.0.1:12345 check
|
|
|
|
# =============================================================================
|
|
# HTTPS Frontend (WAF 통합)
|
|
# =============================================================================
|
|
|
|
frontend https_front
|
|
bind *:443 ssl crt /etc/haproxy/certs/ alpn h2,http/1.1
|
|
bind quic4@:443 ssl crt /etc/haproxy/certs/ alpn h3
|
|
http-response set-header alt-svc "h3=\":443\"; ma=86400"
|
|
|
|
# 유니크 ID 생성
|
|
unique-id-format %{+X}o\ %ci:%cp_%fi:%fp_%Ts_%rt:%pid
|
|
unique-id-header X-Unique-ID
|
|
|
|
# =========================================================================
|
|
# WAF 필터 (순서 중요: CrowdSec 먼저, ModSecurity 나중)
|
|
# =========================================================================
|
|
|
|
# 1. CrowdSec SPOE (IP 기반 빠른 차단)
|
|
filter spoe engine crowdsec config /usr/local/etc/haproxy/crowdsec-spoe.conf
|
|
|
|
# 2. ModSecurity SPOE (상세 요청 분석)
|
|
filter spoe engine modsecurity config /usr/local/etc/haproxy/modsecurity-spoe.conf
|
|
|
|
# =========================================================================
|
|
# 차단 규칙 (순서 중요)
|
|
# =========================================================================
|
|
|
|
# CrowdSec 차단 (알려진 악성 IP)
|
|
http-request deny deny_status 403 content-type "text/plain" string "Access denied by CrowdSec" if { var(sess.crowdsec.action) -m str ban }
|
|
http-request deny deny_status 403 if { var(sess.crowdsec.action) -m str captcha }
|
|
|
|
# ModSecurity 차단 (WAF 규칙 위반)
|
|
http-request deny deny_status 403 content-type "text/plain" string "Access denied by WAF" if { var(txn.modsec.code) -m int gt 0 }
|
|
|
|
# =========================================================================
|
|
# 내부 IP 우회 (선택사항)
|
|
# =========================================================================
|
|
|
|
# Tailscale IP는 WAF 우회
|
|
acl is_tailscale src 100.64.0.0/10
|
|
# http-request set-var(txn.skip_waf) bool(true) if is_tailscale
|
|
|
|
# =========================================================================
|
|
# 기존 라우팅 설정 (변경 없음)
|
|
# =========================================================================
|
|
|
|
# MCP 인증
|
|
acl is_mcp hdr(host) -i mcp.inouter.com
|
|
acl valid_token req.hdr(Authorization) -m str "Bearer dcb7963ab3ef705f6b780818f78942a100efa3b55e3d2f99c4560b65da64c426"
|
|
http-request deny deny_status 401 if is_mcp !valid_token !is_tailscale
|
|
|
|
# Map 기반 라우팅
|
|
use_backend %[req.hdr(host),lower,map_dom(/usr/local/etc/haproxy/domains.map)] if { req.hdr(host),lower,map_dom(/usr/local/etc/haproxy/domains.map) -m found }
|
|
|
|
default_backend default_backend
|
|
|
|
# ... 나머지 backend 설정 유지 ...
|
|
```
|
|
|
|
### HAProxy 설정 적용
|
|
|
|
```bash
|
|
# SPOE 설정 파일 복사 (이미 conf 디렉토리에 있다면 생략)
|
|
# HAProxy 컨테이너에서 /usr/local/etc/haproxy/로 마운트됨
|
|
|
|
# 설정 검증
|
|
podman exec haproxy haproxy -c -f /usr/local/etc/haproxy/haproxy.cfg
|
|
|
|
# HAProxy 리로드 (무중단)
|
|
systemctl reload haproxy
|
|
|
|
# 또는 완전 재시작
|
|
systemctl restart haproxy
|
|
```
|
|
|
|
---
|
|
|
|
## 별도 서버 구성
|
|
|
|
대규모 트래픽이나 보안 격리가 필요한 경우, CrowdSec과 ModSecurity를 별도 서버에서 운영할 수 있습니다.
|
|
|
|
### HAProxy 서버 설정
|
|
|
|
```haproxy
|
|
# haproxy.cfg (HAProxy 서버)
|
|
|
|
# CrowdSec Bouncer (원격 서버)
|
|
backend crowdsec-backend
|
|
mode tcp
|
|
server crowdsec 10.0.0.100:8888 check
|
|
|
|
# ModSecurity SPOA (원격 서버)
|
|
backend modsecurity-backend
|
|
mode tcp
|
|
server modsec 10.0.0.100:12345 check
|
|
```
|
|
|
|
### 보안 서버 Quadlet
|
|
|
|
```ini
|
|
# /etc/containers/systemd/crowdsec-bouncer.container (보안 서버)
|
|
|
|
[Container]
|
|
# ...
|
|
# 모든 인터페이스에서 수신
|
|
PublishPort=0.0.0.0:8888:8888
|
|
```
|
|
|
|
### 방화벽 설정
|
|
|
|
```bash
|
|
# 보안 서버 방화벽
|
|
firewall-cmd --permanent --add-rich-rule='rule family="ipv4" source address="HAProxy서버IP" port port="8888" protocol="tcp" accept'
|
|
firewall-cmd --permanent --add-rich-rule='rule family="ipv4" source address="HAProxy서버IP" port port="12345" protocol="tcp" accept'
|
|
firewall-cmd --reload
|
|
```
|
|
|
|
---
|
|
|
|
## 테스트 방법
|
|
|
|
### CrowdSec 테스트
|
|
|
|
```bash
|
|
# 1. Bouncer 연결 확인
|
|
podman exec crowdsec cscli bouncers list
|
|
|
|
# 2. 현재 차단 목록 확인
|
|
podman exec crowdsec cscli decisions list
|
|
|
|
# 3. 테스트 IP 수동 차단
|
|
podman exec crowdsec cscli decisions add --ip 1.2.3.4 --reason "test ban" --duration 1h
|
|
|
|
# 4. 차단 확인 (403 응답)
|
|
curl -I https://yourdomain.com --resolve yourdomain.com:443:1.2.3.4
|
|
|
|
# 5. 테스트 차단 해제
|
|
podman exec crowdsec cscli decisions delete --ip 1.2.3.4
|
|
|
|
# 6. HAProxy SPOE 통계 확인
|
|
echo "show stat" | nc localhost 9999 | grep crowdsec
|
|
```
|
|
|
|
### ModSecurity 테스트
|
|
|
|
```bash
|
|
# 1. SQL Injection 테스트
|
|
curl -I "https://yourdomain.com/?id=1' OR '1'='1"
|
|
# 예상: 403 Forbidden
|
|
|
|
# 2. XSS 테스트
|
|
curl -I "https://yourdomain.com/?q=<script>alert(1)</script>"
|
|
# 예상: 403 Forbidden
|
|
|
|
# 3. Path Traversal 테스트
|
|
curl -I "https://yourdomain.com/../../../etc/passwd"
|
|
# 예상: 403 Forbidden
|
|
|
|
# 4. 로그 확인
|
|
tail -f /opt/haproxy/modsecurity/logs/modsec_audit.log
|
|
|
|
# 5. HAProxy SPOE 통계 확인
|
|
echo "show stat" | nc localhost 9999 | grep modsec
|
|
```
|
|
|
|
### 통합 테스트
|
|
|
|
```bash
|
|
# 1. 정상 요청 테스트
|
|
curl -I https://yourdomain.com
|
|
# 예상: 200 OK
|
|
|
|
# 2. nikto 스캔 시뮬레이션 (CrowdSec 탐지)
|
|
for i in {1..100}; do
|
|
curl -s -o /dev/null "https://yourdomain.com/admin" &
|
|
done
|
|
|
|
# 3. 차단 확인
|
|
podman exec crowdsec cscli decisions list
|
|
|
|
# 4. 메트릭 확인
|
|
curl http://localhost:6060/metrics | grep crowdsec
|
|
```
|
|
|
|
---
|
|
|
|
## 운영 명령어
|
|
|
|
### CrowdSec 관리
|
|
|
|
```bash
|
|
# =====================
|
|
# 차단 목록 관리
|
|
# =====================
|
|
|
|
# 현재 차단 목록 조회
|
|
podman exec crowdsec cscli decisions list
|
|
|
|
# 특정 IP 차단 정보 조회
|
|
podman exec crowdsec cscli decisions list --ip 1.2.3.4
|
|
|
|
# IP 수동 차단
|
|
podman exec crowdsec cscli decisions add --ip 1.2.3.4 --reason "manual ban" --duration 24h
|
|
|
|
# IP 대역 차단
|
|
podman exec crowdsec cscli decisions add --range 1.2.3.0/24 --reason "subnet ban" --duration 24h
|
|
|
|
# 차단 해제
|
|
podman exec crowdsec cscli decisions delete --ip 1.2.3.4
|
|
|
|
# 모든 차단 해제
|
|
podman exec crowdsec cscli decisions delete --all
|
|
|
|
# =====================
|
|
# 알림 및 시나리오
|
|
# =====================
|
|
|
|
# 최근 알림 조회
|
|
podman exec crowdsec cscli alerts list
|
|
|
|
# 시나리오 목록
|
|
podman exec crowdsec cscli scenarios list
|
|
|
|
# 파서 목록
|
|
podman exec crowdsec cscli parsers list
|
|
|
|
# =====================
|
|
# Hub 관리 (컬렉션/시나리오)
|
|
# =====================
|
|
|
|
# 업데이트 확인
|
|
podman exec crowdsec cscli hub update
|
|
|
|
# 모든 업그레이드
|
|
podman exec crowdsec cscli hub upgrade
|
|
|
|
# 컬렉션 설치
|
|
podman exec crowdsec cscli collections install crowdsecurity/nginx
|
|
podman exec crowdsec cscli collections install crowdsecurity/http-cve
|
|
|
|
# =====================
|
|
# Bouncer 관리
|
|
# =====================
|
|
|
|
# Bouncer 목록
|
|
podman exec crowdsec cscli bouncers list
|
|
|
|
# Bouncer 추가
|
|
podman exec crowdsec cscli bouncers add my-bouncer
|
|
|
|
# Bouncer 삭제
|
|
podman exec crowdsec cscli bouncers delete my-bouncer
|
|
|
|
# =====================
|
|
# 메트릭 및 상태
|
|
# =====================
|
|
|
|
# 엔진 상태
|
|
podman exec crowdsec cscli metrics
|
|
|
|
# Prometheus 메트릭
|
|
curl http://localhost:6060/metrics
|
|
```
|
|
|
|
### ModSecurity 관리
|
|
|
|
```bash
|
|
# =====================
|
|
# 로그 확인
|
|
# =====================
|
|
|
|
# 감사 로그 실시간 확인
|
|
tail -f /opt/haproxy/modsecurity/logs/modsec_audit.log
|
|
|
|
# 차단된 요청만 필터링
|
|
grep -A 20 "403" /opt/haproxy/modsecurity/logs/modsec_audit.log
|
|
|
|
# 특정 규칙 ID로 필터링
|
|
grep "942100" /opt/haproxy/modsecurity/logs/modsec_audit.log
|
|
|
|
# =====================
|
|
# 규칙 관리
|
|
# =====================
|
|
|
|
# OWASP CRS 업데이트
|
|
cd /opt/haproxy/modsecurity
|
|
podman exec modsecurity git -C /opt/owasp-crs pull
|
|
|
|
# 규칙 비활성화 (crs-setup.conf 또는 별도 파일에 추가)
|
|
# SecRuleRemoveById 942100
|
|
|
|
# 특정 경로 제외
|
|
# SecRule REQUEST_URI "@beginsWith /api/webhook" "id:1003,phase:1,pass,nolog,ctl:ruleEngine=Off"
|
|
|
|
# =====================
|
|
# 서비스 관리
|
|
# =====================
|
|
|
|
# 컨테이너 재시작
|
|
systemctl restart modsecurity
|
|
|
|
# 로그 확인
|
|
podman logs -f modsecurity
|
|
```
|
|
|
|
### HAProxy WAF 상태 확인
|
|
|
|
```bash
|
|
# SPOE 에이전트 상태
|
|
echo "show stat" | nc localhost 9999 | grep -E "(crowdsec|modsec)"
|
|
|
|
# 백엔드 상태
|
|
echo "show servers state" | nc localhost 9999 | grep -E "(crowdsec|modsec)"
|
|
|
|
# 필터 통계
|
|
echo "show stat" | nc localhost 9999 | column -t -s ','
|
|
```
|
|
|
|
### 화이트리스트 관리
|
|
|
|
```bash
|
|
# CrowdSec 화이트리스트 추가
|
|
podman exec crowdsec cscli decisions add --ip 10.0.0.1 --type whitelist --duration 87600h --reason "Internal server"
|
|
|
|
# ModSecurity 화이트리스트 (modsecurity.conf에 추가)
|
|
# SecRule REMOTE_ADDR "@ipMatch 10.0.0.1" "id:1,phase:1,pass,nolog,ctl:ruleEngine=Off"
|
|
```
|
|
|
|
---
|
|
|
|
## 문제 해결
|
|
|
|
### CrowdSec 연결 오류
|
|
|
|
```bash
|
|
# Bouncer 연결 확인
|
|
podman exec crowdsec cscli bouncers list
|
|
|
|
# API 키 재생성
|
|
podman exec crowdsec cscli bouncers delete haproxy-bouncer
|
|
podman exec crowdsec cscli bouncers add haproxy-bouncer -o raw
|
|
|
|
# Bouncer 재시작
|
|
systemctl restart crowdsec-bouncer
|
|
```
|
|
|
|
### ModSecurity 오탐 처리
|
|
|
|
```bash
|
|
# 1. 감사 로그에서 규칙 ID 확인
|
|
grep "id \"" /opt/haproxy/modsecurity/logs/modsec_audit.log | tail -20
|
|
|
|
# 2. 규칙 비활성화 (crs-setup.conf)
|
|
# SecRuleRemoveById 942100
|
|
|
|
# 3. 또는 특정 경로만 제외
|
|
# SecRule REQUEST_URI "@beginsWith /api/" "id:1,phase:1,pass,nolog,ctl:ruleRemoveById=942100"
|
|
|
|
# 4. 컨테이너 재시작
|
|
systemctl restart modsecurity
|
|
```
|
|
|
|
### HAProxy SPOE 타임아웃
|
|
|
|
```
|
|
# SPOE 타임아웃 조정 (haproxy.cfg)
|
|
spoe-agent crowdsec-agent
|
|
timeout processing 100ms # 기본 10ms에서 증가
|
|
```
|
|
|
|
---
|
|
|
|
## 성능 튜닝
|
|
|
|
### CrowdSec Bouncer 캐시
|
|
|
|
```yaml
|
|
# /opt/haproxy/crowdsec-bouncer/config.yaml
|
|
cache:
|
|
enabled: true
|
|
ttl: 60s # 캐시 TTL
|
|
size: 50000 # 캐시 크기 (IP 수)
|
|
```
|
|
|
|
### ModSecurity 최적화
|
|
|
|
```apache
|
|
# modsecurity.conf
|
|
SecRequestBodyLimit 1048576 # 1MB로 제한
|
|
SecRequestBodyNoFilesLimit 65536 # 64KB
|
|
SecPcreMatchLimit 50000 # PCRE 매칭 제한
|
|
SecPcreMatchLimitRecursion 50000
|
|
```
|
|
|
|
### HAProxy SPOE 워커
|
|
|
|
```
|
|
# SPOE 설정
|
|
spoe-agent modsecurity-agent
|
|
option async # 비동기 처리
|
|
option pipelining # 파이프라이닝
|
|
max-frame-size 16384 # 프레임 크기
|
|
```
|