지난 시간에는 교내 서버에서 동작중인 stage 서버로부터 Grafana와 Loki 컨테이너가 동작하고 있는 서버로 로그를 수집하는 과정을 기록하였다. -> 이전 포스트
해당 이슈를 진행하면서, 다른 팀원들이 AWS에서 운영되는 production 서버의 Tracing, Metric정보를 교내 서버로 수집하는 과정을 시작하게 되었고, 나는 이에 따라, 교내 서버의 보안과 관련된 부분을 이어서 맡게 되었다.
이 과정을 글로 정리한다.
교내 서버는 학교라는 내부망에 존재한다.
위 그림은 현재 codedang이 사용하고 있는 네 대의 서버가 위치한 네트워크를 간략히 표시한 것이다.
학교 내부망은 80번(HTTP), 443번(HTTPS) 포트를 제외하고는 학교 정책상 방화벽으로 막혀있다. 따라서 SSH 접속을 위해서는 방화벽을 우회하기 위해 교내 서버나 VPN을 이용해야 한다. 현재 SSLVPN을 사용하여 허가받은 계정으로 동아리 서버에 접속중이다.
하지만 학교 내부망에 접속 하더라도, 내부에 위치하고 있는 서버에 무조건 접속할 수 있는 것은 아니다. 각 서버마다 ufw 방화벽이 존재하고 있다. 따라서 ufw에서 허가된 포트번호로만 내부 서버에 connection할 수 있다.
위는 3번서버의 ufw 상태를 나타낸다.
3번서버는, 이번 포스트의 주인공 서버이다. ssh접속은 22가 아닌, 21541 포트로 접속할 수 있게 설정되어있다. 이외의 80,443은 기본으로 열려있고, grafana 콘솔에서 loki를 확인할 수 있게, loki연결 포트인 3100,3200,3101,3199포트가 열려있는 모습이다. AWS에서 교내 서버의 Loki에 보내기 위한 포트로 사용하고 있는 port는 3100포트이다.
여기서 의문점이 생길 수 있다. grafana console을 위한 port는 열려있지 않다. 어떻게 된 일일까?
이는 Caddyfile을 보면 알 수 있다.
grafana.codedang.com {
handle /lokiaws/* {
uri strip_prefix /lokiaws
reverse_proxy 127.0.0.1:3100
}
handle /lokitest/* {
uri strip_prefix /lokitest
reverse_proxy 127.0.0.1:3199
}
handle {
reverse_proxy 127.0.0.1:3000
}
}
Caddyfile은 여타 다른 reverse proxy를 담당하고 있는 Nginx와 달리, 80,443포트만 서버 OS단에 개방만 되어있으면, 알아서 80,443 의 요청을 프록시하는 역할이 default로 설정되어 있다.
위 Caddyfile은 3가지의 요청을 프록시 한다. 그중 test용 프록시는 생략하고 2가지 프록시만 살펴보겠다.
https://grafana.codedang.com/lokiaws/loki/api/v1/push
https://grafana.codedang.com/loki/api/v1/push
로 요청 url이 변경된다. 이는 loki가 log를 받는 기본 uri설정 값이다.docker ps 실행시
위와 같이 "loki-aws" 컨테이너가 3100포트로 listening 하는 것을 볼 수 있다.
Caddyfile이 저렇게 구성되어있으면, 그 누구든 grafana.codedang.com/lokiaws/~~ 도메인으로 요청을 보내게되면 Caddyfile은 좋다구나 하고 요청을 삼켜서 3100포트로 proxy하게 될 것임이 분명하다.
우리는 이를 막기 위해 보안을 설정해야 한다고 생각했다.
팀원이 제안했던 부분이 username과 password로 loki로 로그를 보낼 때, 요청을 제한할 수 있다는 문서를 보았다고 해서 그 부분부터 살펴보았다.
먼저 테스트로 교내 서버에 있는 2번서버와 3번서버간의 보안설정을 해보았다.
(추후에 말하겠지만, 이는 AWS를 사용하고있는 환경과 아예 달라서 테스트가 무의미하긴 했다..)
2번서버의 stage서버는 Promtail로 로그를 수집한다. 궁금하다면 이전 포스트를 참고하자.
Caddy에는 basic auth설정으로 특정 url에 접속시 username 과 password를 띄우게 할 수 있다.
이 설정은 caddy hash-password
설정으로 가능하다.
해당 설정으로 내가 설정하고 싶은 비밀번호를 치면, 이를 해싱한 값으로 우리에게 보여준다.
이를 Caddyfile에 적용시킨다.
grafana.codedang.com {
handle /lokiaws/* {
uri strip_prefix /lokiaws
reverse_proxy 127.0.0.1:3100
}
handle /lokitest/* {
uri strip_prefix /lokitest
basicauth /loki/* {
admin $2a$14$Y9TB4NfcOKg5474oJXJfNOYtB3BjrtRr6EOnKROT9eTKea/b8/W9G
}
reverse_proxy 127.0.0.1:3199
}
handle {
reverse_proxy 127.0.0.1:3000
}
}
위의 admin은 내가 설정한 username값이고(원하는 id를 내맘대로 설정하면 된다.) 뒤에 해싱된 값은 이전 커맨드의 실행값을 복사한 것이다.
이러면, 프록시될 때, username-password가 일치하지 않으면 401 에러를 뱉는다.
그러면 2번서버(stage-서버) 에서 caddy request auth를 허가받기 위한 설정을 알아보자.
promtail-config 파일 내부에는 basic_auth설정이 존재한다.
공식문서 -> https://grafana.com/docs/loki/latest/send-data/promtail/configuration/#clients
server:
http_listen_port: 9080
grpc_listen_port: 0
positions:
filename: /var/log/positions.yaml
clients:
- url: https://grafana.codedang.com/lokitest/loki/api/v1/push
basic_auth:
username: admin # caddy basic_auth에서 설정한 username
password: a1234 # caddy에서 해싱되기 이전의 진짜 비밀번호를 넣어주어야 함
scrape_configs:
- job_name: codedang-dev
docker_sd_configs:
- host: unix:///var/run/docker.sock
refresh_interval: 5s
relabel_configs:
- source_labels: ['__meta_docker_container_name']
regex: '/(.*)'
target_label: 'container'
다음처럼 설정한 후, container를 다시 동작시키고 grafana-console에서 확인해보면 stage서버로부터 log가 온 것을 확인할 수 있었다.
이 때, basic_auth에는 caddy 에서 hash된 password가 아닌, hash되기 이전의 password, 진짜 비밀번호를 넣어야 한다는 점을 주의하자.
AWS환경(운영 환경) 의 로그를 loki로 보낼 때도 같은 설정이 있다는 것을 (눈대중으로) 확인해서 이를 바로 실행에 옮겼다.
현재 ECS task definition을 통해 Fluent Bit client를 사용하여 로그를 수집하고 있다. 이 때 주의해야할 점은, 우리는 Grafana Loki와 연계해서 사용하는 Fluent bit 오픈 소스를 사용중이라는 점이다. 이는 공식 fluent bit 오픈 소스와는 다른점이 많았다.
https://docs.fluentbit.io/manual/pipeline/outputs/loki
fluentbit 공식 사이트에서 언급한 내용은 아래와 같다.
Be aware there is a separate Golang output plugin provided by Grafana with different configuration options.
즉, Grafana plugin fluent bit과 일반적인 fluent bit config 설정값들이 다르다는 점이다.
따라서 https://grafana.com/docs/loki/latest/send-data/fluentbit/
위 사이트를 참고해야만 했다.
여기에서 ECS를 활용해서 사용하는 Loki 공식문서가 있었는데, https://grafana.com/docs/loki/latest/send-data/promtail/cloud/ecs/
위 공식 문서였다.
Creating Your Task Definition의 예시를 보고, Http_User과 Http_Passwd 설정으로 authentication 설정을 하면 되겠다고 생각을 했다. 하지만 이는 GrafanaCloud를 사용중인 사람들에게만 허용되는 Authentication설정이었다. 우리는 자체적으로 Grafana OSS를 사용중이었기 때문에, 이 설정을 해도 보안처리는 당연히 되지 않았다.
결국 원점으로 돌아왔고, ECS에서 fluent-bit 컨테이너를 띄울 때, task definition의 설정값으로는 basic_auth설정을 할 수 없다고 결론지었다. 그러면 어떠한 방법이 있을지 생각을 해보았다.
이 때 들었던 생각은, 굳이 basic_auth설정 말고, AWS 내부에서 외부로 요청을 보낼 때 사용하고 있는 NAT IP 를 사용해서 해당 IP만 proxy해주면 되지 않을까 생각했다.
다행히도 이러한 설정이 Caddy를 활용해 할 수 있었고 이를 적용시켜 보안을 해결하였다.
아래는 issue 내용이다.
https://github.com/skkuding/codedang-monitor/issues/5
간단히 Caddyfile 설정값은 다음과 같다.
grafana.codedang.com {
handle /lokiaws/* {
@blocked not remote_ip {$AWS_REQ_IP}
respond @blocked "Forbidden" 403
uri strip_prefix /lokiaws
reverse_proxy 127.0.0.1:3100
}
handle /lokitest/* {
uri strip_prefix /lokitest
reverse_proxy 127.0.0.1:3199
}
handle {
reverse_proxy 127.0.0.1:3000
}
}
위처럼 Caddyfile을 설정한 후에 github action을 활용한 CI/CD과정에서 secret값을 주입시킨다.
- name: Set Caddyfile Environment Variables
run: |
sed -i 's/{$AWS_REQ_IP}/${{secrets.AWS_REQ_IP}}/' ./Caddyfile
여기서 왜 굳이 저렇게 sed라는 stream editor를 사용해야하나? 라는 의문점이 드는 것이 매우매우 정상이다. 그냥 docker compose과정에서 env 주입을 하면 간단하다.
하지만 Caddy의 특성상, CI/CD과정에서 Caddyfile을 새로 반영하기 위해 docker compose과정에서 environment 설정값을 반영하기 위해 매번 실행하게 되면, 매번 TLS인증서를 자동으로 받아오기 때문에, 일정수치 이상이 되면 오류가 발생할 수 있다고 한다. 따라서, 다음과 같은 github action flow를 설정하였다.
name: Local Deployment
on:
push:
branches: main
jobs:
deploy:
runs-on: self-hosted
steps:
- name: Checkout Repository
uses: actions/checkout@v4
- name: Load dotenv from secret for Grafana Configuration
run: |
cat <<EOF > .env
# Sensitive configurations from GitHub Secrets
GF_SMTP_HOST = ${{ secrets.SMTP_HOST }}
GF_SMTP_USER = ${{ secrets.SMTP_USER }}
GF_SMTP_PASSWORD = ${{ secrets.SMTP_PASSWORD }}
GF_SMTP_FROM_ADDRESS = ${{ secrets.FROM_ADDRESS }}
EOF
- name: Check if Caddy containers are running
id: check-caddy-container
run: |
{
echo 'stdout<<EOF'
docker compose --profile caddy ps -q
echo EOF
} >> "$GITHUB_OUTPUT"
- name: when caddy container down, caddy up
if: steps.check-caddy-container.outputs.stdout == ''
run: |
docker compose --profile caddy up -d --no-recreate
- name: Set Caddyfile Environment Variables
run: |
sed -i 's/{$AWS_REQ_IP}/${{secrets.AWS_REQ_IP}}/' ./Caddyfile
- name: Copy Caddyfile into Caddy Container
run: |
docker cp ./Caddyfile caddy:/etc/caddy/Caddyfile
- name: Gracefully reload Caddy
run: |
docker exec -w /etc/caddy caddy caddy reload
- name: Run Docker Compose Of Log
run: |
docker compose --profile log up -d
Caddy container가 띄워져 있지 않으면, --no-recreate
설정으로 재생성을 막는 옵션을 추가한 컨테이너를 띄우고, 매번 달라질 수 있는 Caddyfile을 반영하기 위해, 컨테이너를 내리고 다시 올리는 것이 아닌, Caddyfile에 sed command를 사용하여 키값을 대치한다. 이 후, 이를 실행중인 컨테이너에 복사하고 caddy reload하는 과정으로 해결하였다.