최근 프로젝트 인프라 구축을 하면서 리버스 프록시를 서비스에 붙히게 되었는데...
접속 로그를 확인 해보니 이상한 로그들이 보이기 시작했다
{"ClientAddr":"185.224.128.84:53964","ClientHost":"185.224.128.84","ClientPort":"53964","ClientUsername":...}
아이피 주소가 조금 이상한거 같아서 어디 아이피인지 확인을 해보니...
네? 네델란드요?
네델란드에서 우리 서비스를 접속할 이유가 없을뿐더러 엔드포인트가 엄청 수상 했는데...
"RequestMethod":"GET","RequestPath":"/application/.env"
아.. 이거 해킹 시도구나.. 를 바로 알게 되었다.
참고로 위 말고도 여러 방면에서 secret이나 env를 털려고 하는 path로 공격이 많이 들어왔다.
다행이 우리 서비스에 저런 path가 존재 하지 않기에 404로 처리 되었지만 만약에 하나라도 얻어걸리는 방식의 공격으로 털릴수 있다는 걸 알아냈다.
우리 서비스는 어디 일보 처럼 쉽게 털리지 않는다
그래서 이걸 방지 하는 방법을 찾아보니 대충 내가 할수 있는건 두가지가 있었다.
일단 필자는 백엔드가 아니라 인프라 구축과 모니터링을 담당하는 데브옵스를 맡고 있기에 두번째 방법을 한번 구현 해보려고 했다. 물론 첫번째 방법은 우리 백엔드 멤버들이 알아서 해주기를 기대한다
하지만 그정도 가지고는 재미가 좀 없어서 고민을 하던 찰나...
만약에 ELK (ElasticSearch, Logstash, Kibana)를 사용할 경우 저런식으로 아이피 주소를 가지고 Kibana 에서 어디서 리퀘스트가 왔는지 시각화를 할수있다고 나와있었다!
하지만... 필자는 ELK스택을 안쓰고 사실 하다가 오류 크리와 빡침으로 때려쳤다 Grafana에서 만든 Loki와 Promtail 을 자주 사용함으로 이번 프로젝트에서도 위 기술들로 구현을 해놨다.
그래서 아쉬움에 그냥 아이피 주소 화이트리스팅 작업만 진행 하려는 찰나.. 시무룩
Stack Overflow, Grafana Community, Reddit등을 찾아보며 그라파나에서도 가능한거를 알아냈다!
심지어 2023년 2월 부터 생각보다 간단하게 구현이 가능하도록 바뀌었다는걸 알아냈다.
그나저나 국내에서는 loki promtail을 거의 안쓰는지 자료가 거의 없었다
해커들의 정보 라고 해봤자 그냥 아이피 위치.. 를 털수 있다는 마음에 너무 행복해서 자료를 찾아 여기에 정리를 해보려 글을 쓰는 계기가 되었다.
아마 국내에서 loki, promtail, grafana를 사용한 최초 GeoIp 시각화 아닐까... 아님 말구
사실 준비물이라고 해봤자 뭐가 많이 있지는 않다
준비가 다 끝났으면 이제 한번 털러 가봅시다.
필자는 리버스 프록시로 Traefik이라는걸 사용했다. 트레픽에서는 아래와 같이 도커 컴포즈에 설정을 해주었다
traefik:
image: traefik:v2.9
command:
- "--api.insecure=false"
- "--providers.docker=true"
- "--providers.docker.swarmMode=true"
- "--providers.docker.exposedbydefault=false"
- "--providers.docker.network=<네트워크 이름>"
- "--entrypoints.websecure.address=:443"
- "--entrypoints.web.http.redirections.entrypoint.to=websecure"
- "--entryPoints.web.http.redirections.entrypoint.scheme=https"
- "--certificatesresolvers.letsencrypt.acme.tlschallenge=true"
- "--certificatesresolvers.letsencrypt.acme.email=<이메일>@gmail.com"
- "--certificatesresolvers.letsencrypt.acme.storage=/letsencrypt/acme.json"
- "--log.level=INFO"
- "--accesslog=true"
- "--accesslog.filepath=/traefik/logs/access.log"
- "--accesslog.bufferingsize=100"
- "--accesslog.format=json"
- "--accesslog.fields.defaultmode=keep"
- "--accesslog.fields.headers.defaultmode=keep"
- "--accesslog.fields.headers.names.X-Forwarded-For=keep"
- "--api.dashboard=true"
ports:
- target: 443
published: 443
protocol: tcp
mode: host
volumes:
- "/var/run/docker.sock:/var/run/docker.sock"
- "traefik-certificates:/letsencrypt"
- "traefik_logs:/traefik/logs/"
networks:
- main-network
deploy:
replicas: 1
placement:
constraints:
- node.labels.instance == main
참고로 위에 파일은 도커 컴포즈 지만 베포를 도커 스웜으로 진행할때 사용한 설정이다.
컴포즈에 똑같이 사용하고 싶으면 deploy
부분을 생략하면 된다.
위에서 중요한 점은 로그들이 /traefik/logs/access.log
라는 곳에 저장이 되어있고 그 부분은 traefik_logs
라는 볼륨에 맵핑이 되있는 점이다.
나머지 부분은 라우트 설정 및 https설정 등등인데 이런건 나중에 따로 찾아보는걸 추천한다. 심심하면 트래픽 관련 글도 올려보긴 할꺼다
로키는 수집한 데이터를 저장하면서 인덱싱을 동시에 진행 한다. 인덱싱은 나중에 그라파나에서 데이터를 볼때 도움이 많이 된다
ELK에서 Logstash, Elasticsearch역활을 조금씩 해준다고 생각하면 편하다. 물론 일라스틱 서치처럼 서치엔진 수준을 기대하면 안된다. 그러면 진짜 개사기일듯
참고로 비교적으로 Elasticsearch, Logstash대비 조금 가볍다!
로키 설정은 도커 컴포즈에서 다음과 같이 했다
loki:
image: grafana/loki:latest
command: -config.file=/etc/loki/local-config.yaml
networks:
- main-network
deploy:
replicas: 1
placement:
constraints:
- node.labels.instance == monitoring
사실 별거 없다
그냥 기본 설정 파일 쓰게 해도 문제 없다. 따로 볼륨 마운팅도 안해도 된다.
프롬테일은 로그를 수집해오고 로키로 쏴?주는 역활을 한다.
ELK에서는 Beats가 하는 일을 얘가 한다고 생각하면 된다.
도커 컴포즈 설정은 아래와 같다.
promtail:
image: grafana/promtail:latest
volumes:
- ./promtail:/etc/promtail/
- /var/log:/var/log:ro
- /var/lib/docker/containers:/var/lib/docker/containers:ro
- /var/run/docker.sock:/var/run/docker.sock
- traefik_logs:/traefik/logs/
command:
- -config.file=/etc/promtail/config.yaml
- -config.expand-env=true
depends_on:
- loki
networks:
- main-network
deploy:
replicas: 1
placement:
constraints:
- node.labels.instance == main
참고로 promtail
이라는 디렉토리에 config.yaml
과 GeoLite2-City.mmdb
가 존재한다.
그리고 당연히 트래픽 로그를 보기 위해 트래픽 로그가 존재하는 볼륨을 마운팅 했다 (traefik_logs:/traefik/logs/
)
사실 이 파일이 많이 중요하다. 이 파일에서 모든 일이 일어난다고 생각해도 될정도다.
우리가 Traefik(트래픽) 에서 받아오는 로그들은 json형태고 아래와 같이 온다.
{"ClientAddr":"78.153.140.177:40130","ClientHost":"78.153.140.177","ClientPort":"40130","ClientUsername":"-","DownstreamContentSize":19,"DownstreamStatus":404,"Duration":164150,"Overhead":164150,"RequestAddr":"xxxx:443","RequestContentSize":0,"RequestCount":1296,"RequestHost":"xxxx","RequestMethod":"GET","RequestPath":"/public/.env","RequestPort":"443","RequestProtocol":"HTTP/1.1","RequestScheme":"http","RetryAttempts":0,"StartLocal":"2024-08-29T20:54:00.775987393Z","StartUTC":"2024-08-29T20:54:00.775987393Z","downstream_Content-Type":"text/plain; charset=utf-8","downstream_X-Content-Type-Options":"nosniff","level":"info","msg":"","request_Accept":"*/*","request_User-Agent":"Mozilla/5.0 (Windows NT 6.2; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/45.0.2454.85 Safari/537.36","request_X-Forwarded-Host":"xxxx:443","request_X-Forwarded-Port":"443","request_X-Forwarded-Proto":"http","request_X-Forwarded-Server":"997382be9587","request_X-Real-Ip":"78.153.140.177","time":"2024-08-29T20:54:00Z"}
겁나 기네. 그와중에 해킹 시도 로그
하지만 여기서 우리가 필요한 부분은 바로 "request_X-Real-Ip":"78.153.140.177"
이 부분이다.
보다싶이 접속한 사람의 아이피가 변형되지 않고 날아온다.
그럼 이 정보를 가지고 아래와 같은 파이프라인을 추가해준다고 생각하면 된다.
필자와 같이 귀차니즘이 심하면 조금 힘들수 있다. 하지만 걱정마라. 해커들을 생각하면 귀찮아도 하게 된다. 경험담이다!
그래서 위 파이프라인을 promtail.yml에 담으면...
server:
http_listen_port: 9080
grpc_listen_port: 0
log_level: debug
positions:
filename: /tmp/positions.yaml
clients:
- url: http://loki:3100/loki/api/v1/push
scrape_configs:
- job_name: traefik_file
static_configs:
- targets:
- localhost
labels:
job: traefik_file
__path__: /traefik/logs/access.log
pipeline_stages:
- json:
expressions:
client_ip: "\"request_X-Real-Ip\""
- geoip:
db: /etc/promtail/GeoLite2-City.mmdb
db_type: city
source: client_ip
- labels:
client_ip: client_ip
city_name: geoip_city_name
country_name: geoip_country_name
continent_name: geoip_continent_name
요런 yaml파일을 하나 만들게 된다!
설명을 하자면..
- job_name: traefik_file
static_configs:
- targets:
- localhost
labels:
job: traefik_file
__path__: /traefik/logs/access.log
위 코드가 데이터를 access.log파일에서 찾아오는 작업을 한다
pipeline_stages:
- json:
expressions:
client_ip: "\"request_X-Real-Ip\""
위 코드는 파이프라인을 사용할꺼라는 선언을 하고 request_X-Real-Ip
를 로그에서 찾아 client_ip
라는 키에 값으로 넣고싶다는 코드다
- geoip:
db: /etc/promtail/GeoLite2-City.mmdb
db_type: city
source: client_ip
위 코드는 미리 다운 받은 위치 기반 도시 디비를 사용해 (말이 도시지 사실 다 나온다) client_ip
에 저장해둔 ip값을 가지고 데이터를 뽑아오는 파이프라인이다
- labels:
client_ip: client_ip
city_name: geoip_city_name
country_name: geoip_country_name
continent_name: geoip_continent_name
마지막으로 위 코드는 방금 geoip
파이프라인에서 나오는 3가지 데이터들과 클라이언트 아이피에 라벨링 이름을 바꿔 로키에 넘겨주는 파이프라인이다. 위 라벨들은 로키에 인덱싱이 될꺼다.
이제 위 파일을 promtail
디렉토리에 넣어주면 된다.
만약에 잘 되면 그라파나에서 봤을때 아래와 같은 라벨들이 그라파나에 생긴다.
그라파나는 솔직히 딱히 할꺼가 많이 없다.
그냥 그라파나 올리면 된다.
걍 대충 해라
grafana:
image: grafana/grafana:latest
ports:
- "3000:3000"
deploy:
replicas: 1
placement:
constraints:
- node.labels.instance == monitoring
volumes:
- grafana_data:/var/lib/grafana
networks:
- main-network
특별한거 없다.
이제 데이터가 준비가 되었으니 시각화를 해보자.
그라파나 Data Source에 Loki를 추가해주면 된다. 프로메테우스랑 하는 방법은 거의 같다. 뵤여주기 귀차느니 알아서 해라
최신 그라파나에는 Geomap이라는 시각화 옵션이 있다. 이걸 골라주면 된다.
그러면 아래와 같은 지도가 나오는데..
우리는 데이터를 위도와 경도를 사용할꺼다.
위에 Table View토글을 눌러주고 traefik_file데이터를 한번 살펴보자.
엄청나게 뭐가 많이 나온다...
잘 살펴보니 {} labels
라고 되어있는 부분에 위치 정보들이 다 담겨 있다. 그럼 이걸 이제 위도와 경도만 뽑기 위해 데이터 가공을 해야된다.
본격적인 데브옵스가 아닌 데이터 엔지니어링
Transform data에 들어가 라벨들만 빼주기로 하자.
Extract Field를 사용하면 편하게 가능하다.
아래와 같이 설정을 해주면
이렇데 데이터가 바뀌었다!
데이터가 잘 가공된거 같으니 geomap 설정을 다시 살펴보자.
여기다가 설정을 해주면 데이터 레이어를 입힐수 있는거 같다.
그래서 진행을 하는데...
분명히 위도 경도가 데이터에 보이는데 geomap에서 찾지를 못한다?
오류문을 잘 살펴보니 No numeric fields found라고 되어있다.
그래서 차근 차근 생각해보니... 라벨에서는 데이터가 다 스트링으로 되어있다는거였다. 즉 스트링 값을 숫자로 한번 바꿔줘야지 되는거다.
진짜 귀찮네
그래서 Data Transform을 한번 더 해본다. (진짜 이정도면 데이터 엔지니어로 취업 가능할꺼같은데)
Convert Field Type을 사용해서
위도와 경도를 숫자로 바꿔준다.
그랬더니..
성공!
위도와 경도를 잘 넣어주고
Table View 토글을 꺼보니
완성!
이제 해커들이 어디서 접속하는지 시각화를 진행 했다.
어차피 대부분의 요청들은 301 아니면 404로 막히기 때문에 문제 없지만 최소 그 요청들이 어디서 오는지 잘 알게됐다!
요즘 로그를 보니 전세계에서 요청이 날아오는중이다. 이정도면 우리 팀 서비스가 위알더 월드 아닌가.
읽어주셔서 감사하고 다들 해킹 공격 조심 하시길 바란다!