제목은 Grafana 로 서비스 접속 기록 분석 해보기로 하겠습니다. 근데 이제 해커를 곁들인

peterTheAnteater·2024년 8월 29일
16

데브옵스

목록 보기
1/11
post-thumbnail

글을 쓰게 된 동기

최근 프로젝트 인프라 구축을 하면서 리버스 프록시를 서비스에 붙히게 되었는데...
접속 로그를 확인 해보니 이상한 로그들이 보이기 시작했다

{"ClientAddr":"185.224.128.84:53964","ClientHost":"185.224.128.84","ClientPort":"53964","ClientUsername":...}

아이피 주소가 조금 이상한거 같아서 어디 아이피인지 확인을 해보니...

네? 네델란드요?
네델란드에서 우리 서비스를 접속할 이유가 없을뿐더러 엔드포인트가 엄청 수상 했는데...

"RequestMethod":"GET","RequestPath":"/application/.env"

아.. 이거 해킹 시도구나.. 를 바로 알게 되었다.
참고로 위 말고도 여러 방면에서 secret이나 env를 털려고 하는 path로 공격이 많이 들어왔다.

다행이 우리 서비스에 저런 path가 존재 하지 않기에 404로 처리 되었지만 만약에 하나라도 얻어걸리는 방식의 공격으로 털릴수 있다는 걸 알아냈다.


우리 서비스는 어디 일보 처럼 쉽게 털리지 않는다

그래서 이걸 방지 하는 방법을 찾아보니 대충 내가 할수 있는건 두가지가 있었다.

1. 백엔드인 경우 CORS설정을 통해 origin을 프런트 도메인으로 설정을 해두면 프런트 도메인에서 오는 리퀘스트 말고는 처리가 불가능하다.

2. 리버스 프록시에서 특정 아이피를 막거나 허용해주게 설정을 해주면 된다!

일단 필자는 백엔드가 아니라 인프라 구축과 모니터링을 담당하는 데브옵스를 맡고 있기에 두번째 방법을 한번 구현 해보려고 했다. 물론 첫번째 방법은 우리 백엔드 멤버들이 알아서 해주기를 기대한다

하지만 그정도 가지고는 재미가 좀 없어서 고민을 하던 찰나...

만약에 ELK (ElasticSearch, Logstash, Kibana)를 사용할 경우 저런식으로 아이피 주소를 가지고 Kibana 에서 어디서 리퀘스트가 왔는지 시각화를 할수있다고 나와있었다!

하지만... 필자는 ELK스택을 안쓰고 사실 하다가 오류 크리와 빡침으로 때려쳤다 Grafana에서 만든 Loki와 Promtail 을 자주 사용함으로 이번 프로젝트에서도 위 기술들로 구현을 해놨다.

그래서 아쉬움에 그냥 아이피 주소 화이트리스팅 작업만 진행 하려는 찰나.. 시무룩

Stack Overflow, Grafana Community, Reddit등을 찾아보며 그라파나에서도 가능한거를 알아냈다!

심지어 2023년 2월 부터 생각보다 간단하게 구현이 가능하도록 바뀌었다는걸 알아냈다.

그나저나 국내에서는 loki promtail을 거의 안쓰는지 자료가 거의 없었다

해커들의 정보 라고 해봤자 그냥 아이피 위치.. 를 털수 있다는 마음에 너무 행복해서 자료를 찾아 여기에 정리를 해보려 글을 쓰는 계기가 되었다.

아마 국내에서 loki, promtail, grafana를 사용한 최초 GeoIp 시각화 아닐까... 아님 말구

준비물

사실 준비물이라고 해봤자 뭐가 많이 있지는 않다

  • 리버스 프록시 (이건 엔진엑스가 제일 흔하다. 필자는 Traefik이라는 리버스 프록시를 썼다. 자세한거는 다음 글에 써볼예정이다)
  • Docker (그냥... 도커 없어도 되긴 하지만 필자 프로젝트 설정이 도커 기반이다)
  • Grafana (시각화)
  • Loki (로그 저장 및 인덱싱)
  • Promtail (로그 수집)
  • GeoLite DB (사실 아무 위치 기반 데이터베이스 있으면 된다. 필자는 GeoLite2-City.mmdb를 사용했다)

준비가 다 끝났으면 이제 한번 털러 가봅시다.

리버스 프록시 설정

필자는 리버스 프록시로 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설정 등등인데 이런건 나중에 따로 찾아보는걸 추천한다. 심심하면 트래픽 관련 글도 올려보긴 할꺼다

Loki

로키는 수집한 데이터를 저장하면서 인덱싱을 동시에 진행 한다. 인덱싱은 나중에 그라파나에서 데이터를 볼때 도움이 많이 된다

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

사실 별거 없다

그냥 기본 설정 파일 쓰게 해도 문제 없다. 따로 볼륨 마운팅도 안해도 된다.

Promtail

프롬테일은 로그를 수집해오고 로키로 쏴?주는 역활을 한다.

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.yamlGeoLite2-City.mmdb가 존재한다.

그리고 당연히 트래픽 로그를 보기 위해 트래픽 로그가 존재하는 볼륨을 마운팅 했다 (traefik_logs:/traefik/logs/)

config.yaml

사실 이 파일이 많이 중요하다. 이 파일에서 모든 일이 일어난다고 생각해도 될정도다.

우리가 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" 이 부분이다.

보다싶이 접속한 사람의 아이피가 변형되지 않고 날아온다.

그럼 이 정보를 가지고 아래와 같은 파이프라인을 추가해준다고 생각하면 된다.

필자와 같이 귀차니즘이 심하면 조금 힘들수 있다. 하지만 걱정마라. 해커들을 생각하면 귀찮아도 하게 된다. 경험담이다!

  1. 로그를 가지고 온다
  2. 로그를 json형식으로 파싱해서 ip정보를 뽑아온다
  3. 아이피를 디비에 비교해서 위치 정보를 가지고 온다
    a. 이 정보는 나라, 대륙, 도시, 위도, 경도, 등등을 가지고 올수 있다.
  4. 위 정보들을 키:값 으로 json처럼 처리를 해준다 (그라파나 label이나 field로 추가를 해주면 된다.)
  5. 그라파나에서 시각화를 해준다.

그래서 위 파이프라인을 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

그라파나는 솔직히 딱히 할꺼가 많이 없다.

그냥 그라파나 올리면 된다.

걍 대충 해라

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로 막히기 때문에 문제 없지만 최소 그 요청들이 어디서 오는지 잘 알게됐다!

요즘 로그를 보니 전세계에서 요청이 날아오는중이다. 이정도면 우리 팀 서비스가 위알더 월드 아닌가.

읽어주셔서 감사하고 다들 해킹 공격 조심 하시길 바란다!

profile
소프트웨어 개발과 밀당하는 개발자

0개의 댓글