FastAPI 서버 보안 강화: 무작위 스캔 공격 대응과 개선 기록

LTT·2025년 7월 7일

FastAPI 서버 보안 강화 기록 🛡️


1. 문제 상황


최근 FastAPI 서버 로그에서 아래와 같은 이상 요청들이 다수 발견되었다.

2025-06-24 01:37:46,904 - app_logger - INFO - ENDPOINT: GET /
2025-06-24 01:37:46,906 - app_logger - INFO - RESPONSE: {"detail":"Not Found"}
2025-06-24 03:48:44,430 - app_logger - INFO - ENDPOINT: GET /login
2025-06-24 03:48:44,431 - app_logger - INFO - RESPONSE: {"detail":"Not Found"}
2025-06-24 03:48:45,208 - app_logger - INFO - ENDPOINT: GET /ab2g
2025-06-24 03:48:45,210 - app_logger - INFO - RESPONSE: {"detail":"Not Found"}

이러한 요청은 무작위 스캔 공격 (Random Scan Attack) 의 일종으로, 공격자가 서버에 존재할 법한 다양한 엔드포인트로 무작위 요청을 보내 취약점을 탐색하는 시도다.

개발서버 자체의 설정을 수정하는 건 부담스러웠고, 우선은 FastAPI 서버 레벨에서 이러한 공격에 대한 방어 조치를 적용하기로 했다.


2. 조치사항 리스트업 ✅


2-1. 서버 간 통신: JWT → API Key 인증 방식 전환

  • 기존에는 서버 간 통신도 무조건 JWT를 사용했지만, 현재 시스템에서는 사용자 인증이 필요 없는 서버 간 단순 인증만으로 충분하다.
  • API Key + AES 암호화 방식으로 인증 방식을 개선함.

👉 관련 내용: 서버간 통신 인증 방식 : API Key


2-2. 배포 쉘스크립트 개선

  • 기존에는 uvicorn 실행 시 -host 0.0.0.0 설정으로 외부에서도 접근 가능한 상태였다.
  • 실제로는 같은 서버 인스턴스 내의 Spring 서버만 요청을 보내기 때문에 127.0.0.1 로 수정하여 로컬 접속만 허용하도록 변경했다.
#!/bin/bash

PYTHON=<파이썬 경로>

echo "> FastAPI 서버 프로세스 확인"
CURRENT_PID=$(lsof -i | grep python3 | grep -v grep | awk 'NR==2 {print $2}')

echo "> 현재 PID: $CURRENT_PID"

if [ -z "$CURRENT_PID" ]; then
    echo "> 프로세스 없음"
else
    echo "> 프로세스 종료 시도: $CURRENT_PID"
    sudo kill "$CURRENT_PID"
    sleep 4
    if ps -p "$CURRENT_PID" > /dev/null; then
        echo "> 프로세스가 종료되지 않아 강제 종료"
        sudo kill -9 "$CURRENT_PID"
        sleep 2
    fi
fi

echo "> 패키지 설치"
$PYTHON -m pip install -r ./<Directory Path>/requirements.txt

echo "> 서버 재시작"
cd <Directory Path>/
nohup $PYTHON -m uvicorn app.main:app \
  --ssl-keyfile=./<ssl-keyfile.pem> \
  --ssl-certfile=./<ssl-certfile.pem> \
  --host 127.0.0.1 \
  --port 8000 > ../nohup.out 2>&1 &

✅ 개선 효과:

  • 외부 공격자가 IP와 포트만 알아내도 접근 가능한 상태 → 내부 접근만 가능하도록 변경

2-3. Swagger 문서 비활성화

  • 기본적으로 FastAPI는 /docs 또는 /redoc 경로로 API 문서를 제공한다.
  • 공격자는 해당 문서만으로도 API 엔드포인트를 쉽게 유추할 수 있다.
  • 배포 환경에서는 Swagger 문서를 완전히 비활성화함.
from fastapi import FastAPI

app = FastAPI(
    docs_url=None,
    redoc_url=None,
    openapi_url=None
)

✅ 개선 효과:

  • 외부인이 Swagger 문서를 통해 API 정보를 탐색하는 위험 차단

2-4. 로깅 미들웨어 개선 (IP 주소 포함)

  • 기존 로깅에는 요청자의 IP 정보가 빠져 있어, 누가 요청했는지 알 수 없었다.
  • 요청자의 IP, 포트 정보까지 함께 로깅하도록 개선했다.
from fastapi import Request, Response
from starlette.middleware.base import BaseHTTPMiddleware
from starlette.responses import JSONResponse
from starlette.background import BackgroundTask
from starlette.types import Message
from app.utils.log_util import logger
import traceback

class LogMiddleware(BaseHTTPMiddleware):
    def entry_log_info(self, endpoint: str):
        logger.info(f"REQUEST: {endpoint}")

    def end_log_info(self, res_body: bytes):
        logger.info(f"RESPONSE: {res_body.decode(errors='ignore')}")

    async def set_body(self, request: Request, body: bytes):
        async def receive() -> Message:
            return {"type": "http.request", "body": body}
        request._receive = receive

    async def dispatch(self, request: Request, call_next):
        req_body = await request.body()
        await self.set_body(request, req_body)

        # IP + 포트 + 요청 경로
        endpoint = f"[{request.client.host}:{request.client.port}] {request.method} {request.url.path}"
        self.entry_log_info(endpoint)

        try:
            response = await call_next(request)
            res_body = b""
            async for chunk in response.body_iterator:
                res_body += chunk

            task = BackgroundTask(self.end_log_info, res_body)

            return Response(
                content=res_body,
                status_code=response.status_code,
                headers=dict(response.headers),
                media_type=response.media_type,
                background=task,
            )
        except Exception as e:
            logger.error(f"EXCEPTION OCCURRED: {type(e).__name__}: {str(e)}")
            logger.debug(traceback.format_exc())

            error_response = {"detail": "Internal Server Error", "exception": str(e)}
            res_body = JSONResponse(status_code=500, content=error_response)
            task = BackgroundTask(self.end_log_info, res_body.body)
            res_body.background = task
            return res_body

✅ 개선 효과:

  • 요청자 IP 기록 → 문제 발생 시 빠른 추적 가능
  • 예외 상황까지 상세 로깅

3. 최종 정리 📝


조치내용효과
✅ API Key 인증서버 간 통신에 JWT 대신 API Key 적용불필요한 JWT 제거, 간편 인증
✅ Host 제한0.0.0.0 → 127.0.0.1외부 접근 차단
✅ Swagger 비활성화docs_url=NoneAPI 정보 노출 방지
✅ 로깅 강화요청 IP 및 포트 기록공격/오류 추적 가능

4. 앞으로의 보완 아이디어 💡


  • Fail2Ban 적용: 비정상 IP 차단 자동화
  • API Rate Limiting: 요청 횟수 제한 적용
  • SSL 인증서 갱신 자동화: 장기적 서비스 안정화
  • 서버간 Mutual TLS: 강력한 상호 인증 고려
profile
개발자에서 엔지니어로, 엔지니어에서 리더로

0개의 댓글