
최근 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 서버 레벨에서 이러한 공격에 대한 방어 조치를 적용하기로 했다.
👉 관련 내용: 서버간 통신 인증 방식 : API Key
uvicorn 실행 시 -host 0.0.0.0 설정으로 외부에서도 접근 가능한 상태였다.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 &
✅ 개선 효과:
/docs 또는 /redoc 경로로 API 문서를 제공한다.from fastapi import FastAPI
app = FastAPI(
docs_url=None,
redoc_url=None,
openapi_url=None
)
✅ 개선 효과:
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
✅ 개선 효과:
| 조치 | 내용 | 효과 |
|---|---|---|
| ✅ API Key 인증 | 서버 간 통신에 JWT 대신 API Key 적용 | 불필요한 JWT 제거, 간편 인증 |
| ✅ Host 제한 | 0.0.0.0 → 127.0.0.1 | 외부 접근 차단 |
| ✅ Swagger 비활성화 | docs_url=None | API 정보 노출 방지 |
| ✅ 로깅 강화 | 요청 IP 및 포트 기록 | 공격/오류 추적 가능 |