이미지 로딩 중...

Nginx Rate Limiting과 보안 설정 완벽 가이드 - 슬라이드 1/11
A

AI Generated

2025. 11. 14. · 2 Views

Nginx Rate Limiting과 보안 설정 완벽 가이드

실무에서 자주 겪는 API 서버 과부하와 악의적인 요청을 효과적으로 방어하는 방법을 배웁니다. Nginx의 Rate Limiting부터 보안 헤더, DDoS 방어까지 실전 설정을 다룹니다.


목차

  1. Rate Limiting 기본 설정
  2. 연결 수 제한 (Connection Limiting)
  3. 보안 헤더 설정
  4. IP 화이트리스트와 블랙리스트
  5. SSL/TLS 보안 강화
  6. 요청 본문 크기 제한
  7. 요청 타임아웃 설정
  8. 버퍼 오버플로우 방지
  9. 특정 HTTP 메서드 제한
  10. 로그 및 모니터링 강화

1. Rate Limiting 기본 설정

시작하며

여러분이 API 서버를 운영하다가 갑자기 트래픽이 폭증하면서 서버가 다운되는 상황을 겪어본 적 있나요? 혹은 특정 사용자가 무한정 요청을 보내서 다른 사용자들의 서비스 이용에 지장을 준 경험이 있으신가요?

이런 문제는 실제 개발 현장에서 매우 자주 발생합니다. 악의적인 봇이 초당 수천 건의 요청을 보내거나, 잘못 작성된 클라이언트 코드가 무한 루프로 API를 호출하는 경우가 대표적입니다.

이로 인해 서버 비용이 급증하고, 정상 사용자는 서비스를 이용할 수 없게 되며, 최악의 경우 전체 시스템이 마비될 수 있습니다. 바로 이럴 때 필요한 것이 Rate Limiting입니다.

Nginx의 Rate Limiting은 클라이언트별로 단위 시간당 요청 수를 제한하여, 서버 자원을 보호하고 공정한 서비스 이용을 보장합니다.

개요

간단히 말해서, Rate Limiting은 특정 클라이언트(IP 주소 기준)가 일정 시간 동안 보낼 수 있는 요청 수를 제한하는 기술입니다. 이 기능이 필요한 이유는 서버 자원은 한정되어 있기 때문입니다.

무제한 요청을 허용하면 서버 CPU, 메모리, 네트워크 대역폭이 고갈되어 정상적인 서비스 제공이 불가능해집니다. 예를 들어, 전자상거래 사이트에서 특가 상품 판매 시 수만 명이 동시에 접속하는 경우, Rate Limiting 없이는 서버가 감당할 수 없습니다.

기존에는 애플리케이션 레벨에서 복잡한 코드로 요청 수를 추적했다면, 이제는 Nginx 설정 몇 줄로 간단하게 구현할 수 있습니다. Rate Limiting의 핵심 특징은 크게 세 가지입니다.

첫째, 클라이언트별 독립적인 제한(각 IP마다 별도 카운트), 둘째, 유연한 시간 단위 설정(초, 분, 시간 단위), 셋째, 버스트 트래픽 처리 기능(일시적인 트래픽 증가 허용)입니다. 이러한 특징들이 실시간 트래픽 제어와 서버 안정성 확보에 매우 중요합니다.

코드 예제

# Rate Limiting 존(zone) 정의 - http 블록 안에 작성
limit_req_zone $binary_remote_addr zone=api_limit:10m rate=10r/s;

server {
    listen 80;
    server_name api.example.com;

    location /api/ {
        # 초당 10개 요청 제한, 최대 20개까지 버스트 허용
        limit_req zone=api_limit burst=20 nodelay;

        # 제한 초과 시 429 상태 코드 반환
        limit_req_status 429;

        proxy_pass http://backend;
    }
}

설명

이것이 하는 일: 이 설정은 각 클라이언트 IP가 초당 최대 10개의 요청만 보낼 수 있도록 제한하되, 일시적으로 20개까지의 버스트 트래픽은 허용합니다. 첫 번째로, limit_req_zone 지시자가 공유 메모리 존을 생성합니다.

$binary_remote_addr는 클라이언트 IP를 이진 형식으로 저장하여 메모리를 절약하고(일반 IP보다 4배 적은 공간 사용), zone=api_limit:10m은 10MB 크기의 메모리 존을 할당합니다(약 160,000개 IP 추적 가능). rate=10r/s는 초당 10개 요청(requests per second)으로 제한을 설정합니다.

그 다음으로, location 블록에서 실제 제한을 적용합니다. limit_req zone=api_limit는 앞서 정의한 존을 사용하고, burst=20은 순간적으로 20개까지 요청을 큐에 저장합니다.

nodelay 옵션이 없으면 버스트 요청들이 지연되어 처리되지만, 이 옵션을 사용하면 즉시 처리됩니다. 마지막으로, 제한을 초과한 요청은 limit_req_status 429에 의해 HTTP 429 Too Many Requests 상태 코드를 반환하며 거부됩니다.

클라이언트는 이 응답을 받아 요청 속도를 조절할 수 있습니다. 여러분이 이 설정을 사용하면 서버 자원을 효과적으로 보호하고, DDoS 공격이나 봇의 과도한 요청으로부터 안전하게 방어할 수 있습니다.

또한 정상 사용자에게는 burst 옵션으로 인해 자연스러운 사용 패턴(페이지 로드 시 여러 리소스 동시 요청)을 방해하지 않으면서도, 악의적인 대량 요청은 확실히 차단할 수 있습니다.

실전 팁

💡 존(zone) 크기는 1MB당 약 16,000개 IP를 추적할 수 있습니다. 예상 동시 접속자 수에 따라 적절히 조정하세요. 대규모 서비스라면 50m~100m 정도가 적당합니다.

💡 burst 값이 너무 크면 Rate Limiting의 의미가 없고, 너무 작으면 정상 사용자도 차단됩니다. 일반적으로 rate의 2배 정도가 적절합니다(rate=10r/s면 burst=20).

💡 nodelay 옵션을 빼면 요청이 큐에서 대기하게 되어 응답이 느려집니다. API 서버에서는 빠른 실패(fail-fast)가 중요하므로 nodelay를 사용하는 것이 좋습니다.

💡 $binary_remote_addr 대신 $http_x_forwarded_for를 사용하면 프록시 뒤의 실제 클라이언트 IP를 추적할 수 있지만, 이 값은 위조될 수 있으니 신뢰할 수 있는 프록시만 허용하세요.

💡 로그에 제한된 요청을 기록하려면 limit_req_log_level warn;을 추가하면 error.log에 경고가 남아 공격 패턴을 분석할 수 있습니다.


2. 연결 수 제한 (Connection Limiting)

시작하며

여러분의 서버에 누군가 수백 개의 연결을 동시에 맺고 유지하면서 다른 사용자들의 접속을 방해하는 상황을 상상해보세요. 이것은 Slowloris 공격이라 불리는 대표적인 DoS 공격 기법입니다.

이런 공격은 요청 수 제한만으로는 막을 수 없습니다. 공격자는 많은 요청을 보내는 대신, 연결만 열어두고 아주 천천히 데이터를 보내면서 서버의 연결 슬롯을 모두 소진시킵니다.

결과적으로 정상 사용자는 새로운 연결을 맺을 수 없게 됩니다. 바로 이럴 때 필요한 것이 Connection Limiting입니다.

IP당 동시 연결 수를 제한하여 이러한 공격을 효과적으로 차단할 수 있습니다.

개요

간단히 말해서, Connection Limiting은 하나의 클라이언트 IP가 동시에 맺을 수 있는 연결 수를 제한하는 기능입니다. 이 기능이 필요한 이유는 Rate Limiting과는 다른 공격 벡터를 방어하기 위해서입니다.

Rate Limiting은 요청의 빈도를 제한하지만, Connection Limiting은 동시 연결 수를 제한합니다. 예를 들어, 파일 다운로드 서비스에서 한 사용자가 동시에 100개 파일을 다운로드하려는 것을 방지하거나, 웹소켓 연결을 무한정 열어두는 것을 막을 수 있습니다.

기존에는 방화벽 레벨에서 연결 수를 추적했다면, Nginx에서는 애플리케이션 계층에서 더 정밀한 제어가 가능합니다. 핵심 특징은 실시간 연결 추적(현재 활성 연결 수 모니터링), 경로별 독립 제한(정적 파일과 API를 다르게 설정 가능), 그리고 세션 기반 제한(로그인 사용자별 제한도 가능)입니다.

이러한 특징들이 서버의 연결 풀을 효율적으로 관리하고 공정한 자원 분배를 가능하게 합니다.

코드 예제

# 연결 수 제한 존 정의 - http 블록 안에 작성
limit_conn_zone $binary_remote_addr zone=conn_limit:10m;

server {
    listen 80;
    server_name download.example.com;

    # 서버 전체에 대한 연결 제한
    limit_conn conn_limit 10;

    location /download/ {
        # 다운로드 경로는 더 엄격하게 제한
        limit_conn conn_limit 5;

        # 제한 초과 시 503 상태 코드 반환
        limit_conn_status 503;

        # 대역폭 제한도 함께 적용 (연결당 1MB/s)
        limit_rate 1m;

        root /var/www/files;
    }
}

설명

이것이 하는 일: 이 설정은 각 클라이언트 IP가 서버 전체에 대해 최대 10개, 다운로드 경로에 대해서는 5개의 동시 연결만 맺을 수 있도록 제한합니다. 첫 번째로, limit_conn_zone이 연결 추적을 위한 메모리 존을 생성합니다.

$binary_remote_addr를 키로 사용하여 각 IP의 현재 활성 연결 수를 실시간으로 카운트합니다. 10MB 메모리는 약 160,000개 IP의 연결 상태를 추적할 수 있습니다.

그 다음으로, 서버 레벨과 location 레벨에서 각각 다른 제한을 적용합니다. 서버 레벨의 limit_conn conn_limit 10은 모든 경로를 합쳐서 IP당 10개 연결을 허용하고, /download/ 경로는 더 엄격하게 5개로 제한합니다.

이렇게 계층적으로 적용하면 중요한 리소스를 더 강력하게 보호할 수 있습니다. 세 번째로, limit_rate 1m은 각 연결의 대역폭을 초당 1MB로 제한합니다.

이것은 연결 수 제한과 함께 사용하면 더욱 효과적입니다. 예를 들어, 5개 연결에 각 1MB/s면 한 IP가 최대 5MB/s까지 사용할 수 있게 됩니다.

마지막으로, 제한을 초과한 연결 시도는 HTTP 503 Service Unavailable로 즉시 거부됩니다. 공격자는 더 이상 연결을 맺을 수 없고, 서버는 정상 사용자를 위한 연결 슬롯을 확보할 수 있습니다.

여러분이 이 설정을 사용하면 Slowloris, R.U.D.Y 같은 저속 공격을 효과적으로 방어하고, 다운로드 서버의 대역폭을 공정하게 분배할 수 있습니다. 또한 하나의 사용자나 봇이 서버 자원을 독점하는 것을 방지하여 전체 사용자에게 안정적인 서비스를 제공할 수 있습니다.

실전 팁

💡 서버 레벨과 location 레벨의 limit_conn은 독립적이지 않습니다. location의 제한이 더 엄격하면 그것이 우선 적용되므로, 계층 구조를 잘 설계하세요.

💡 limit_rate는 처음부터 적용되지 않고 limit_rate_after를 사용하면 일정 크기까지는 전속으로 전송하고 그 이후부터 제한할 수 있습니다. 예: limit_rate_after 10m;은 10MB까지는 빠르게, 그 이후는 제한.

💡 웹소켓이나 Server-Sent Events는 오래 유지되는 연결이 정상이므로, 이러한 경로는 별도의 존을 사용하거나 제한을 완화해야 합니다.

💡 로드밸런서 뒤에 있다면 $binary_remote_addr 대신 실제 클라이언트 IP를 담은 헤더를 사용해야 합니다. 그렇지 않으면 모든 요청이 로드밸런서 IP로 집계됩니다.

💡 연결 수 제한과 요청 수 제한은 함께 사용하는 것이 가장 효과적입니다. 두 가지 다른 공격 패턴을 동시에 방어할 수 있습니다.


3. 보안 헤더 설정

시작하며

여러분의 웹 애플리케이션이 XSS 공격이나 클릭재킹 공격에 노출되어 사용자 정보가 유출되는 상황을 겪어본 적 있나요? 혹은 보안 감사에서 "보안 헤더가 설정되어 있지 않습니다"라는 지적을 받은 경험이 있으신가요?

이런 문제는 애플리케이션 코드의 취약점도 있지만, 브라우저가 제공하는 보안 기능을 제대로 활용하지 못해서 발생하는 경우가 많습니다. 현대 브라우저들은 다양한 보안 메커니즘을 내장하고 있지만, 서버가 적절한 헤더를 보내지 않으면 이러한 보호 기능이 작동하지 않습니다.

바로 이럴 때 필요한 것이 보안 헤더 설정입니다. Nginx에서 몇 가지 헤더만 추가하면 XSS, 클릭재킹, MIME 스니핑, 중간자 공격 등 다양한 위협으로부터 사용자를 보호할 수 있습니다.

개요

간단히 말해서, 보안 헤더는 브라우저에게 보안 정책을 지시하는 HTTP 응답 헤더들입니다. 이 헤더들이 필요한 이유는 브라우저의 기본 동작이 항상 안전하지는 않기 때문입니다.

예를 들어, 브라우저는 기본적으로 외부 스크립트 실행을 허용하고, iframe 삽입을 허용하며, HTTP에서 HTTPS로의 자동 업그레이드를 하지 않습니다. 실무에서 온라인 뱅킹 서비스, 전자상거래 사이트, 관리자 페이지 같은 민감한 서비스는 반드시 이러한 헤더를 설정해야 합니다.

기존에는 각 페이지의 HTML에 메타 태그를 삽입하거나 애플리케이션 코드에서 헤더를 추가했다면, Nginx에서 중앙 집중식으로 한 번만 설정하면 모든 응답에 자동으로 적용됩니다. 핵심 보안 헤더는 크게 다섯 가지입니다.

X-Frame-Options(클릭재킹 방지), X-Content-Type-Options(MIME 스니핑 방지), X-XSS-Protection(XSS 필터 활성화), Content-Security-Policy(리소스 로딩 제한), Strict-Transport-Security(HTTPS 강제)입니다. 이러한 헤더들이 다층 방어 체계를 구축하여 공격 표면을 크게 줄여줍니다.

코드 예제

server {
    listen 443 ssl http2;
    server_name secure.example.com;

    # HSTS - HTTPS 강제 (1년간 유지, 서브도메인 포함)
    add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;

    # 클릭재킹 방지 - iframe 삽입 차단
    add_header X-Frame-Options "SAMEORIGIN" always;

    # MIME 스니핑 방지
    add_header X-Content-Type-Options "nosniff" always;

    # XSS 필터 활성화
    add_header X-XSS-Protection "1; mode=block" always;

    # CSP - 외부 스크립트 차단, 같은 출처만 허용
    add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline';" always;

    # 레퍼러 정보 제한
    add_header Referrer-Policy "strict-origin-when-cross-origin" always;

    location / {
        proxy_pass http://backend;
    }
}

설명

이것이 하는 일: 이 설정은 6가지 핵심 보안 헤더를 모든 응답에 추가하여 브라우저의 보안 기능을 최대한 활용합니다. 첫 번째로, Strict-Transport-Security(HSTS) 헤더가 브라우저에게 "앞으로 1년간 이 사이트는 HTTPS로만 접속하라"고 지시합니다.

includeSubDomains는 모든 서브도메인도 포함하고, preload는 브라우저 제조사의 HSTS 프리로드 리스트에 등록될 수 있게 합니다. 한 번 HTTPS로 접속한 사용자는 이후 HTTP 링크를 클릭해도 자동으로 HTTPS로 업그레이드되어 중간자 공격을 원천 차단합니다.

두 번째로, X-Frame-Options와 X-Content-Type-Options가 전통적인 공격을 막습니다. X-Frame-Options: SAMEORIGIN은 같은 도메인에서만 iframe 삽입을 허용하여 클릭재킹 공격을 방지하고, X-Content-Type-Options: nosniff는 브라우저가 Content-Type을 무시하고 파일 내용을 분석하는 것을 차단하여 MIME 혼동 공격을 막습니다.

세 번째로, Content-Security-Policy(CSP)가 가장 강력한 방어막을 구축합니다. default-src 'self'는 기본적으로 같은 출처의 리소스만 허용하고, script-src 'self' 'unsafe-inline'은 인라인 스크립트는 허용하되 외부 스크립트는 차단합니다.

공격자가 외부에서 악성 스크립트를 삽입하려 해도 브라우저가 실행을 거부합니다. 마지막으로, always 플래그가 매우 중요합니다.

이것이 없으면 200 OK 응답에만 헤더가 추가되고 404나 500 같은 에러 응답에는 추가되지 않습니다. 공격자는 종종 에러 페이지를 노리므로 모든 응답에 헤더를 추가해야 합니다.

여러분이 이 설정을 사용하면 OWASP Top 10의 여러 위협(XSS, 클릭재킹, 중간자 공격)을 효과적으로 방어할 수 있습니다. 보안 감사 도구(SecurityHeaders.com, Mozilla Observatory)에서도 높은 점수를 받을 수 있으며, 사용자 신뢰도를 높이고 법적 책임을 줄일 수 있습니다.

실전 팁

💡 CSP는 처음에는 report-only 모드로 시작하세요. Content-Security-Policy-Report-Only 헤더를 사용하면 위반 사항을 차단하지 않고 리포트만 받아 기존 기능이 깨지지 않는지 확인할 수 있습니다.

💡 CDN을 사용한다면 CSP의 script-src에 해당 도메인을 추가해야 합니다. 예: script-src 'self' https://cdn.jsdelivr.net;

💡 HSTS preload는 신중하게 결정하세요. 한 번 등록되면 제거가 매우 어렵고, HTTPS 인증서 문제가 생기면 사용자가 사이트에 아예 접속할 수 없게 됩니다.

💡 X-XSS-Protection은 구형 브라우저용입니다. 최신 브라우저는 CSP를 사용하므로, CSP가 우선이고 이 헤더는 하위 호환성을 위해 추가하는 것입니다.

💡 보안 헤더 설정 후 https://securityheaders.com 에서 테스트하면 어떤 헤더가 누락되었는지, 설정이 약한지 즉시 확인할 수 있습니다.


4. IP 화이트리스트와 블랙리스트

시작하며

여러분의 관리자 페이지에 전 세계 어디서나 접속할 수 있다면 얼마나 위험할까요? 혹은 특정 국가에서 계속 공격이 들어오는데 그 국가의 모든 IP를 차단해야 하는 상황을 겪어본 적 있나요?

이런 문제는 아무리 강력한 인증 시스템을 구축해도 공격 시도 자체를 막을 수 없다는 한계가 있습니다. 무차별 대입 공격(brute force)은 인증 전에 발생하므로 서버 자원을 소모하고 로그를 가득 채웁니다.

특히 관리자 페이지, 데이터베이스 관리 도구, 모니터링 대시보드 같은 민감한 경로는 접근 자체를 제한해야 합니다. 바로 이럴 때 필요한 것이 IP 기반 접근 제어입니다.

Nginx의 allow/deny 지시자를 사용하면 네트워크 레벨에서 접근을 제어하여, 인증 로직이 실행되기 전에 불법 접속을 차단할 수 있습니다.

개요

간단히 말해서, IP 접근 제어는 특정 IP 주소나 IP 대역만 허용하거나 차단하는 네트워크 레벨의 방화벽 기능입니다. 이 기능이 필요한 이유는 인증보다 앞선 방어선이 필요하기 때문입니다.

인증은 "누가 맞는 비밀번호를 입력했는가"를 검증하지만, IP 접근 제어는 "누가 시도조차 할 수 있는가"를 결정합니다. 예를 들어, 회사 내부망에서만 접근해야 하는 관리자 페이지, 특정 파트너사만 호출할 수 있는 API, 또는 개발/스테이징 환경처럼 외부 노출이 절대 안 되는 서비스에 적용합니다.

기존에는 iptables나 방화벽 장비에서 설정했다면, Nginx에서는 경로별로 세밀하게 제어할 수 있습니다. 같은 서버에서 일부 경로는 공개하고 일부는 제한할 수 있습니다.

핵심 특징은 계층적 평가(순서대로 매칭되고 첫 번째 매치가 적용), CIDR 표기법 지원(IP 대역 단위 설정), 그리고 deny all 패턴(화이트리스트 방식의 기본 차단)입니다. 이러한 특징들이 Zero Trust 보안 모델의 기초가 됩니다.

코드 예제

# GeoIP 모듈로 국가별 차단도 가능 (별도 설치 필요)
# 여기서는 기본적인 IP 제어만 다룹니다

server {
    listen 80;
    server_name admin.example.com;

    # 관리자 페이지 - 회사 IP만 허용
    location /admin/ {
        # 회사 IP 대역 허용 (CIDR 표기법)
        allow 203.0.113.0/24;
        # 집에서 접속하는 관리자 IP 허용
        allow 198.51.100.50;
        # VPN 게이트웨이 허용
        allow 192.0.2.0/28;
        # 나머지 모든 IP 차단
        deny all;

        proxy_pass http://admin_backend;
    }

    # 특정 IP 차단 (블랙리스트)
    location /api/ {
        # 공격이 확인된 IP 차단
        deny 192.0.2.100;
        deny 198.51.100.0/24;
        # 나머지 모든 IP 허용
        allow all;

        proxy_pass http://api_backend;
    }
}

설명

이것이 하는 일: 이 설정은 /admin/ 경로는 화이트리스트 방식으로 특정 IP만 허용하고, /api/ 경로는 블랙리스트 방식으로 문제가 있는 IP만 차단합니다. 첫 번째로, /admin/ location에서 화이트리스트 방식을 사용합니다.

allow 지시자로 허용할 IP들을 나열하고, 마지막에 deny all로 나머지를 모두 차단합니다. CIDR 표기법(203.0.113.0/24)을 사용하면 203.0.113.0부터 203.0.113.255까지 256개 IP를 한 번에 허용할 수 있어 편리합니다.

순서가 중요한데, 위에서 아래로 평가되어 첫 번째로 매칭되는 규칙이 적용됩니다. 두 번째로, /api/ location에서는 블랙리스트 방식을 사용합니다.

먼저 deny로 차단할 IP를 명시하고, 마지막에 allow all로 나머지를 허용합니다. 공격이 확인된 IP나 스팸 IP 대역을 실시간으로 추가할 수 있습니다.

이 방식은 공개 API처럼 대부분의 접속을 허용하되 악의적인 IP만 골라내는 경우에 적합합니다. 세 번째로, Nginx는 매칭되는 즉시 평가를 중단하므로 효율적입니다.

예를 들어, allow 203.0.113.50이 매칭되면 그 다음의 deny all은 평가하지 않습니다. 따라서 자주 접속하는 IP를 위쪽에 배치하면 성능이 약간 향상됩니다.

마지막으로, 차단된 IP의 접속 시도는 HTTP 403 Forbidden을 받습니다. 이것은 인증 실패(401 Unauthorized)와는 다르게, "당신은 접근 권한 자체가 없습니다"라는 의미입니다.

공격자는 인증 페이지조차 볼 수 없습니다. 여러분이 이 설정을 사용하면 관리자 페이지 침투 시도를 원천 차단하고, 서버 로그를 깨끗하게 유지하며, 무차별 대입 공격으로 인한 서버 부하를 제거할 수 있습니다.

또한 내부 도구를 안전하게 운영하고, 규정 준수(특정 국가 IP 차단 등) 요구사항을 충족할 수 있습니다.

실전 팁

💡 로드밸런서나 CDN 뒤에 있다면 $binary_remote_addr이 아닌 X-Forwarded-For 헤더의 실제 클라이언트 IP로 판단해야 합니다. set_real_ip_fromreal_ip_header 지시자를 사용하세요.

💡 IP 화이트리스트는 별도 파일로 분리하면 관리가 편합니다. include /etc/nginx/whitelist.conf; 형태로 사용하고, IP 변경 시 해당 파일만 수정 후 nginx -s reload하면 됩니다.

💡 VPN이나 동적 IP 사용자를 위해 Basic Auth와 병행하는 것도 좋은 전략입니다. IP는 1차 방어선, 인증은 2차 방어선으로 다층 방어를 구현합니다.

💡 국가 단위 차단이 필요하면 GeoIP2 모듈을 설치하세요. MaxMind DB를 사용하여 if ($geoip2_country_code = CN) { return 403; } 같은 설정이 가능합니다.

💡 차단된 접속 시도를 로그로 남기려면 access_log /var/log/nginx/blocked.log;를 해당 location에 추가하면 공격 패턴 분석에 유용합니다.


5. SSL/TLS 보안 강화

시작하며

여러분의 HTTPS 사이트가 구형 브라우저 지원을 위해 약한 암호화 방식을 허용하고 있다면, 중간자 공격에 취약할 수 있다는 것을 알고 계신가요? SSL Labs 테스트에서 A+ 등급을 받는 것과 C 등급을 받는 것의 차이를 체감해보신 적이 있나요?

이런 문제는 HTTPS를 적용했다고 해서 끝이 아닙니다. 어떤 TLS 버전을 사용하는지, 어떤 암호화 스위트(cipher suite)를 허용하는지, Perfect Forward Secrecy를 지원하는지에 따라 실제 보안 수준이 크게 달라집니다.

약한 설정은 POODLE, BEAST, CRIME 같은 공격에 노출될 수 있습니다. 바로 이럴 때 필요한 것이 SSL/TLS 보안 강화 설정입니다.

Nginx에서 최신 보안 표준을 적용하면 중간자 공격을 방어하고, 규정 준수 요구사항을 충족하며, 사용자 신뢰를 높일 수 있습니다.

개요

간단히 말해서, SSL/TLS 보안 강화는 구형 프로토콜과 약한 암호화 방식을 비활성화하고, 강력하고 현대적인 암호화만 사용하도록 설정하는 것입니다. 이 설정이 필요한 이유는 하위 호환성과 보안이 항상 트레이드오프 관계이기 때문입니다.

Nginx의 기본 SSL 설정은 광범위한 클라이언트 지원을 위해 보수적으로 구성되어 있어, 의도적으로 강화하지 않으면 10년 전 브라우저까지 지원하느라 보안이 약해집니다. 예를 들어, 금융 서비스, 의료 정보 시스템, 전자상거래 같은 민감한 데이터를 다루는 서비스는 최신 보안 표준을 반드시 적용해야 합니다.

기존에는 인증서만 설치하면 HTTPS가 "된다"고 생각했다면, 이제는 어떤 암호화를 사용하는지까지 세밀하게 관리해야 안전합니다. 핵심 요소는 크게 네 가지입니다.

TLS 1.2+ 강제(구형 프로토콜 비활성화), 강력한 cipher suite 선택(AES-GCM, ChaCha20 우선), Perfect Forward Secrecy 활성화(세션 키 유출 시에도 과거 통신 보호), OCSP Stapling(인증서 검증 성능 향상)입니다. 이러한 설정들이 현대적인 HTTPS 보안의 기준이 됩니다.

코드 예제

server {
    listen 443 ssl http2;
    server_name secure.example.com;

    # 인증서 경로
    ssl_certificate /etc/nginx/ssl/cert.pem;
    ssl_certificate_key /etc/nginx/ssl/key.pem;

    # TLS 1.21.3만 허용 (1.0, 1.1 비활성화)
    ssl_protocols TLSv1.2 TLSv1.3;

    # 강력한 암호화 스위트만 사용 (Perfect Forward Secrecy 지원)
    ssl_ciphers 'ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305';
    ssl_prefer_server_ciphers off;  # TLS 1.3에서는 클라이언트 선택 허용

    # DH 파라미터 (2048비트 이상)
    ssl_dhparam /etc/nginx/ssl/dhparam.pem;

    # SSL 세션 캐시 (성능 향상)
    ssl_session_cache shared:SSL:10m;
    ssl_session_timeout 10m;
    ssl_session_tickets off;  # 보안을 위해 비활성화

    # OCSP Stapling (인증서 검증 성능 향상)
    ssl_stapling on;
    ssl_stapling_verify on;
    ssl_trusted_certificate /etc/nginx/ssl/chain.pem;

    location / {
        proxy_pass http://backend;
    }
}

설명

이것이 하는 일: 이 설정은 구형 TLS 프로토콜을 차단하고 최신 암호화만 사용하도록 강제하며, 성능 최적화와 보안 강화를 동시에 달성합니다. 첫 번째로, ssl_protocols TLSv1.2 TLSv1.3이 TLS 1.0과 1.1을 비활성화합니다.

TLS 1.0/1.1은 BEAST, POODLE 같은 알려진 취약점이 있어 PCI DSS 같은 보안 표준에서도 금지하고 있습니다. TLS 1.3는 가장 최신 버전으로 핸드셰이크가 더 빠르고 안전합니다.

2020년 이후 브라우저는 모두 TLS 1.2+를 지원하므로 호환성 문제도 거의 없습니다. 두 번째로, ssl_ciphers 설정이 암호화 알고리즘을 엄격하게 제한합니다.

ECDHE(타원곡선 디피-헬만)는 Perfect Forward Secrecy를 제공하여, 서버 개인키가 미래에 유출되어도 과거 통신 내용을 복호화할 수 없게 합니다. AES-GCM과 ChaCha20-Poly1305는 현대적인 AEAD(인증 암호화) 알고리즘으로 빠르고 안전합니다.

ssl_prefer_server_ciphers off는 TLS 1.3에서 클라이언트가 최적의 알고리즘을 선택하도록 하여 성능을 향상시킵니다. 세 번째로, DH 파라미터와 세션 관리 설정이 보안을 강화합니다.

ssl_dhparam은 2048비트 이상의 디피-헬만 파라미터를 사용하여 키 교환을 안전하게 하고(약한 파라미터는 Logjam 공격에 취약), ssl_session_tickets off는 세션 티켓 기능을 비활성화하여 Perfect Forward Secrecy를 완전하게 보장합니다. 마지막으로, OCSP Stapling이 인증서 검증을 최적화합니다.

일반적으로 브라우저가 인증서 유효성을 확인하려면 CA 서버에 별도로 요청해야 하는데, Stapling을 활성화하면 Nginx가 미리 검증 결과를 가져와서 TLS 핸드셰이크에 포함시킵니다. 이로써 연결 속도가 빨라지고 사용자 프라이버시도 향상됩니다.

여러분이 이 설정을 사용하면 SSL Labs에서 A+ 등급을 받을 수 있고, PCI DSS, HIPAA 같은 규정 준수 요구사항을 충족하며, 중간자 공격으로부터 사용자를 확실하게 보호할 수 있습니다. 또한 HTTP/2와 함께 사용하면 성능도 향상되어 보안과 속도를 모두 잡을 수 있습니다.

실전 팁

💡 DH 파라미터는 openssl dhparam -out /etc/nginx/ssl/dhparam.pem 2048 명령으로 생성합니다. 4096비트는 더 안전하지만 생성에 시간이 오래 걸리고 성능 영향이 있어 2048비트가 적당합니다.

💡 설정 변경 후 반드시 https://www.ssllabs.com/ssltest/ 에서 테스트하세요. 어떤 브라우저가 접속 가능한지, 어떤 취약점이 있는지 상세히 알려줍니다.

💡 ssl_session_tickets를 활성화하면 성능은 향상되지만 Perfect Forward Secrecy가 약화됩니다. 보안이 우선이면 off, 성능이 중요하면 on으로 설정하되 티켓 키를 주기적으로 로테이션하세요.

💡 Let's Encrypt를 사용한다면 Certbot이 자동으로 chain.pem을 생성하므로 OCSP Stapling을 쉽게 적용할 수 있습니다. ssl_trusted_certificate에 chain.pem 경로만 지정하면 됩니다.

💡 TLS 1.3 전용으로 설정하면 가장 안전하지만 일부 구형 Android나 기업 방화벽에서 접속이 안 될 수 있습니다. 서비스 대상 사용자를 고려하여 TLS 1.2는 유지하는 것이 현실적입니다.


6. 요청 본문 크기 제한

시작하며

여러분의 서버에 누군가 수백 MB 크기의 파일을 업로드하려고 시도하면서 서버 메모리를 고갈시키거나 디스크를 가득 채우는 상황을 겪어본 적 있나요? 혹은 파일 업로드 기능이 없는 API 엔드포인트에 거대한 JSON을 보내서 서버를 마비시키는 공격을 본 적이 있나요?

이런 문제는 애플리케이션이 요청을 처리하기 전에, 심지어 파싱하기 전에 발생합니다. 공격자는 서버가 거대한 데이터를 메모리에 로드하고 처리하는 동안 자원을 소진시킵니다.

특히 비동기 처리 방식의 Node.js나 Python 서버는 이런 공격에 더 취약할 수 있습니다. 바로 이럴 때 필요한 것이 요청 본문 크기 제한입니다.

Nginx에서 미리 차단하면 애플리케이션 서버에 도달하기 전에 과도한 요청을 거부할 수 있습니다.

개요

간단히 말해서, 요청 본문 크기 제한은 클라이언트가 보낼 수 있는 POST/PUT 요청의 최대 크기를 제한하는 기능입니다. 이 설정이 필요한 이유는 서버 자원을 보호하고 비정상적인 요청을 조기에 차단하기 위해서입니다.

무제한 업로드를 허용하면 디스크가 가득 차거나, 메모리가 부족하거나, 네트워크 대역폭이 고갈될 수 있습니다. 예를 들어, 일반 API는 수 KB 정도의 JSON만 받으면 되는데 수십 MB를 허용할 이유가 없고, 이미지 업로드는 10MB면 충분한데 1GB를 허용할 필요가 없습니다.

기존에는 애플리케이션 코드에서 파일 크기를 검사했다면, Nginx에서 먼저 차단하면 애플리케이션 서버는 정상적인 크기의 요청만 처리하게 되어 부하가 줄어듭니다. 핵심 특징은 경로별 독립 설정(API와 파일 업로드를 다르게 제한), 빠른 차단(헤더의 Content-Length만 보고 즉시 판단), 그리고 명확한 에러 메시지(413 Request Entity Too Large)입니다.

이러한 특징들이 효율적인 자원 관리와 DoS 방어를 가능하게 합니다.

코드 예제

http {
    # 기본값: 모든 경로에 대해 1MB 제한
    client_max_body_size 1m;

    server {
        listen 80;
        server_name app.example.com;

        # API 엔드포인트 - 작은 JSON만 허용
        location /api/ {
            client_max_body_size 100k;  # 100KB
            client_body_buffer_size 128k;  # 버퍼 크기

            proxy_pass http://api_backend;
        }

        # 파일 업로드 - 더 큰 크기 허용
        location /upload/ {
            client_max_body_size 50m;  # 50MB

            # 업로드 타임아웃 연장
            client_body_timeout 300s;

            # 임시 파일 경로 지정
            client_body_temp_path /var/nginx/temp;

            proxy_pass http://upload_backend;
        }

        # 프로필 이미지 - 중간 크기
        location /profile/image {
            client_max_body_size 5m;  # 5MB

            proxy_pass http://image_backend;
        }
    }
}

설명

이것이 하는 일: 이 설정은 각 경로의 특성에 맞게 요청 본문 크기를 제한하여, 불필요하게 큰 요청이 서버 자원을 소모하는 것을 방지합니다. 첫 번째로, http 블록에서 client_max_body_size 1m으로 전역 기본값을 설정합니다.

이것은 "특별히 명시하지 않은 모든 경로는 1MB까지만 허용"한다는 의미입니다. 이렇게 안전한 기본값을 설정하고, 필요한 경로만 확장하는 것이 보안의 기본 원칙입니다.

두 번째로, /api/ 경로는 100KB로 더 엄격하게 제한합니다. 일반적인 REST API는 JSON 페이로드가 수십 KB를 넘지 않으므로, 100KB면 충분합니다.

client_body_buffer_size 128k는 요청 본문을 메모리에 버퍼링할 크기를 지정하는데, 이것보다 크면 임시 파일에 쓰게 됩니다. 100KB 요청은 모두 메모리에서 처리되어 성능이 향상됩니다.

세 번째로, /upload/ 경로는 50MB로 확장하여 실제 파일 업로드를 허용합니다. client_body_timeout 300s는 대용량 파일 전송 시 타임아웃을 5분으로 늘려서, 느린 네트워크 환경의 사용자도 업로드를 완료할 수 있게 합니다.

client_body_temp_path로 임시 파일 경로를 지정하면 충분한 디스크 공간이 있는 파티션을 사용할 수 있습니다. 마지막으로, 제한을 초과한 요청은 즉시 HTTP 413 Request Entity Too Large 응답을 받고 거부됩니다.

Nginx는 요청 헤더의 Content-Length를 확인하여 본문을 받기 전에 판단하므로, 네트워크 대역폭과 시간을 낭비하지 않습니다. 여러분이 이 설정을 사용하면 디스크 공간 고갈 공격을 방지하고, 메모리 기반 DoS 공격을 차단하며, 실수로 잘못된 파일을 업로드하는 것도 막을 수 있습니다.

또한 경로별로 정확히 필요한 만큼만 허용하여 최소 권한 원칙을 실천할 수 있습니다.

실전 팁

💡 client_max_body_size를 0으로 설정하면 제한이 완전히 해제됩니다. 하지만 절대 사용하지 마세요. 대신 필요한 최대 크기를 명시적으로 지정하는 것이 안전합니다.

💡 클라이언트 측에서도 파일 크기를 미리 검증하면 사용자 경험이 향상됩니다. JavaScript로 업로드 전에 체크하여 "파일이 너무 큽니다"라고 친절하게 알려주세요.

💡 client_body_buffer_size는 메모리 사용량에 영향을 줍니다. 동시 연결이 1000개이고 버퍼가 128k면 128MB 메모리가 필요합니다. 서버 메모리를 고려하여 설정하세요.

💡 비디오 업로드처럼 매우 큰 파일을 다룬다면 chunk 업로드(여러 조각으로 나누어 전송)를 고려하세요. Nginx에서 각 청크는 작게 유지하면서 전체적으로는 큰 파일을 받을 수 있습니다.

💡 에러 페이지를 커스터마이징하려면 error_page 413 /413.html;을 추가하여 사용자에게 "파일이 너무 큽니다. 최대 50MB까지 업로드 가능합니다" 같은 친절한 메시지를 보여주세요.


7. 요청 타임아웃 설정

시작하며

여러분의 서버에 연결만 맺고 데이터를 아주 천천히 보내면서 연결을 계속 유지하는 Slowloris 공격을 받아본 적 있나요? 혹은 백엔드 서버가 응답하지 않는데 Nginx가 무한정 기다리면서 사용자에게 아무 응답도 주지 못하는 상황을 겪어본 적 있나요?

이런 문제는 타임아웃 설정이 너무 관대하거나 없을 때 발생합니다. 공격자는 연결을 느리게 유지하면서 서버의 연결 슬롯을 모두 소진시키고, 정상 사용자는 새로운 연결을 맺을 수 없게 됩니다.

또한 백엔드 장애 시 사용자는 몇 분씩 기다리다가 타임아웃 에러를 보게 됩니다. 바로 이럴 때 필요한 것이 적절한 타임아웃 설정입니다.

각 단계별로 합리적인 시간 제한을 두면 느린 공격을 방어하고, 장애 시 빠르게 실패(fail-fast)하여 사용자 경험을 개선할 수 있습니다.

개요

간단히 말해서, 타임아웃 설정은 연결, 요청, 응답의 각 단계에서 최대 대기 시간을 지정하여 무한 대기를 방지하는 기능입니다. 이 설정이 필요한 이유는 네트워크 통신은 언제나 실패할 수 있고, 무한정 기다릴 수는 없기 때문입니다.

클라이언트가 느린 네트워크를 사용할 수도 있고, 백엔드 서버가 과부하일 수도 있으며, 악의적인 공격일 수도 있습니다. 예를 들어, 일반 웹 페이지 로딩은 5초 안에 완료되어야 하고, API 호출은 30초가 넘으면 문제가 있는 것이며, 파일 다운로드는 몇 분이 걸릴 수 있습니다.

기존에는 Nginx의 기본 타임아웃(보통 60초)을 그대로 사용했다면, 각 서비스의 특성에 맞게 조정하면 보안과 성능을 모두 향상시킬 수 있습니다. 핵심 타임아웃은 크게 네 가지입니다.

client_header_timeout(요청 헤더 수신 제한), client_body_timeout(요청 본문 수신 제한), send_timeout(응답 전송 제한), proxy_read_timeout(백엔드 응답 대기 제한)입니다. 이러한 설정들이 연결의 전체 생명주기를 제어하여 자원을 효율적으로 관리합니다.

코드 예제

http {
    # 클라이언트 요청 헤더 읽기 타임아웃
    client_header_timeout 10s;

    # 클라이언트 요청 본문 읽기 타임아웃
    client_body_timeout 30s;

    # 클라이언트로 응답 전송 타임아웃
    send_timeout 30s;

    # 연결 유지 타임아웃
    keepalive_timeout 65s;

    server {
        listen 80;
        server_name api.example.com;

        location /api/ {
            # 백엔드 연결 타임아웃
            proxy_connect_timeout 5s;

            # 백엔드 응답 읽기 타임아웃
            proxy_read_timeout 30s;

            # 백엔드로 요청 전송 타임아웃
            proxy_send_timeout 30s;

            proxy_pass http://backend;
        }

        location /long-running/ {
            # 오래 걸리는 작업은 타임아웃 연장
            proxy_read_timeout 300s;  # 5분

            proxy_pass http://batch_backend;
        }
    }
}

설명

이것이 하는 일: 이 설정은 클라이언트와의 통신, 백엔드와의 통신 각 단계에서 최대 대기 시간을 제한하여 무한 대기와 자원 낭비를 방지합니다. 첫 번째로, 클라이언트 측 타임아웃이 Slowloris 같은 공격을 방어합니다.

client_header_timeout 10s는 연결 후 10초 안에 HTTP 헤더를 완전히 받지 못하면 연결을 끊습니다. 정상 클라이언트는 헤더를 즉시 보내므로 영향이 없지만, 공격자가 한 바이트씩 천천히 보내면 10초 후 차단됩니다.

client_body_timeout 30s는 POST 본문 전송에 30초를 허용하는데, 대용량 업로드가 있다면 더 늘려야 합니다. 두 번째로, keepalive_timeout 65s는 HTTP Keep-Alive 연결을 65초간 유지합니다.

같은 클라이언트가 여러 요청을 보낼 때 연결을 재사용하여 성능이 향상되지만, 너무 길면 유휴 연결이 쌓여 자원을 낭비합니다. 65초는 브라우저의 기본값과 비슷하여 적절합니다.

세 번째로, 백엔드 관련 타임아웃이 장애 전파를 방지합니다. proxy_connect_timeout 5s는 백엔드 서버 연결에 5초만 기다립니다.

백엔드가 응답하지 않으면 즉시 다음 서버로 fallback하거나 에러를 반환하여 사용자가 무한정 기다리지 않게 합니다. proxy_read_timeout 30s는 백엔드 응답 수신에 30초를 허용하는데, 일반 API는 충분하지만 무거운 쿼리나 배치 작업은 더 필요합니다.

마지막으로, /long-running/ 경로처럼 특정 작업은 타임아웃을 연장합니다. 리포트 생성, 대용량 데이터 처리, 비디오 인코딩 같은 작업은 몇 분이 걸릴 수 있으므로 경로별로 다르게 설정합니다.

하지만 너무 길게 설정하면 진짜 문제 상황을 감지하지 못하므로 합리적인 최대값을 정해야 합니다. 여러분이 이 설정을 사용하면 느린 속도 공격을 효과적으로 차단하고, 백엔드 장애 시 빠르게 에러를 반환하여 사용자 대기 시간을 줄이며, 유휴 연결을 정리하여 서버 자원을 효율적으로 사용할 수 있습니다.

또한 타임아웃 로그를 분석하여 성능 병목 지점을 찾아낼 수 있습니다.

실전 팁

💡 타임아웃은 너무 짧으면 정상 사용자가 에러를 보고, 너무 길면 공격을 막지 못합니다. 실제 사용 패턴을 로그로 분석하여 95 percentile 값을 기준으로 설정하세요.

💡 proxy_next_upstream_timeout을 함께 설정하면 여러 백엔드를 시도할 때 전체 시간을 제한할 수 있습니다. 예: proxy_next_upstream_timeout 10s;는 모든 재시도를 포함해 10초 안에 결론을 내립니다.

💡 타임아웃 에러는 기본적으로 504 Gateway Timeout이지만, error_page 504 /504.html;로 사용자에게 친절한 메시지를 보여줄 수 있습니다. "서버가 혼잡합니다. 잠시 후 다시 시도해주세요."

💡 WebSocket이나 Server-Sent Events는 연결이 오래 유지되는 것이 정상이므로 별도 location으로 분리하고 타임아웃을 크게 늘리세요. 예: proxy_read_timeout 1h;

💡 로그에서 타임아웃을 추적하려면 log_format$request_time$upstream_response_time을 포함하세요. 어느 단계에서 시간이 오래 걸리는지 분석할 수 있습니다.


8. 버퍼 오버플로우 방지

시작하며

여러분의 서버가 거대한 URL이나 쿠키를 받아서 메모리가 갑자기 폭증하거나, 비정상적으로 큰 헤더로 인해 버퍼 오버플로우가 발생하는 상황을 겪어본 적 있나요? 혹은 공격자가 의도적으로 수 MB 크기의 쿠키를 보내서 서버 자원을 소진시키는 것을 본 적이 있나요?

이런 문제는 HTTP 헤더의 크기를 제한하지 않으면 발생합니다. URL, 쿠키, User-Agent 같은 헤더는 클라이언트가 임의로 크기를 조작할 수 있으므로, 서버는 반드시 최대 크기를 제한해야 합니다.

무제한 버퍼는 메모리 고갈, 버퍼 오버플로우, 그리고 DoS 공격의 원인이 됩니다. 바로 이럴 때 필요한 것이 버퍼 크기 제한 설정입니다.

Nginx에서 각종 버퍼의 최대 크기를 제한하면 메모리를 보호하고 비정상적인 요청을 조기에 차단할 수 있습니다.

개요

간단히 말해서, 버퍼 크기 제한은 요청 라인, 헤더, 쿠키 등 각종 HTTP 요소의 최대 크기를 제한하여 메모리 오버플로우를 방지하는 기능입니다. 이 설정이 필요한 이유는 클라이언트가 보내는 모든 데이터는 일단 메모리에 로드되기 때문입니다.

공격자는 수 MB 크기의 URL이나 수천 개의 쿠키를 보내서 서버 메모리를 소진시킬 수 있습니다. 예를 들어, 동시 접속 1000명이 각각 10MB 헤더를 보내면 10GB 메모리가 순식간에 사라집니다.

실무에서 정상적인 HTTP 요청은 헤더가 수 KB를 넘지 않으므로 엄격한 제한이 가능합니다. 기존에는 Nginx의 기본 버퍼 크기(보통 4KB~8KB)를 사용했다면, 보안을 위해 더 줄이거나 명시적으로 제한하는 것이 안전합니다.

핵심 버퍼 설정은 네 가지입니다. client_header_buffer_size(기본 헤더 버퍼), large_client_header_buffers(큰 헤더용 버퍼), client_body_buffer_size(요청 본문 버퍼), output_buffers(응답 버퍼)입니다.

이러한 설정들이 메모리 사용량을 예측 가능하게 만들고 폭증을 방지합니다.

코드 예제

http {
    # 일반적인 요청 라인(URL) 및 헤더 버퍼 크기
    client_header_buffer_size 1k;

    # 큰 헤더를 위한 버퍼 (개수 x 크기)
    # 최대 4개까지, 각 8KB로 제한
    large_client_header_buffers 4 8k;

    # 요청 본문 버퍼 크기
    client_body_buffer_size 128k;

    # 파일 디스크립터와 에러 페이지 버퍼
    output_buffers 2 32k;

    server {
        listen 80;
        server_name secure.example.com;

        location /api/ {
            # API는 헤더가 작으므로 더 엄격하게 제한
            client_header_buffer_size 512;  # 512 bytes
            large_client_header_buffers 2 4k;

            # 큰 헤더는 즉시 거부
            if ($request_uri ~ ".{2048,}") {
                return 414;  # Request-URI Too Large
            }

            proxy_pass http://api_backend;
        }
    }
}

설명

이것이 하는 일: 이 설정은 HTTP 요청의 각 부분에 대해 메모리 버퍼 크기를 제한하여, 악의적인 대용량 요청이 서버 메모리를 소진하는 것을 방지합니다. 첫 번째로, client_header_buffer_size 1k가 일반적인 요청을 위한 초기 버퍼를 1KB로 설정합니다.

대부분의 HTTP 요청은 "GET /api/users HTTP/1.1" + 몇 개 헤더로 1KB 안에 들어갑니다. Nginx는 이 버퍼로 먼저 시도하고, 부족하면 large_client_header_buffers로 확장합니다.

1KB는 대부분의 요청을 처리하기에 충분하면서도 메모리 사용을 최소화합니다. 두 번째로, large_client_header_buffers 4 8k는 큰 헤더를 위한 예비 버퍼를 설정합니다.

"4개 × 8KB = 32KB"가 최대 헤더 크기입니다. 쿠키가 많거나 긴 Referer URL이 있는 경우를 위한 것인데, 32KB를 넘는 헤더는 비정상이므로 거부됩니다.

이것이 없으면 Nginx는 헤더를 받을 수 없어 400 Bad Request를 반환하지만, 너무 크게 설정하면 공격에 취약합니다. 세 번째로, client_body_buffer_size 128k는 요청 본문을 메모리에 버퍼링할 크기입니다.

이것보다 큰 본문은 디스크의 임시 파일에 쓰여집니다. 128KB면 대부분의 API 요청(JSON)은 메모리에서 처리되어 빠르고, 파일 업로드는 디스크로 가서 메모리를 보호합니다.

메모리 버퍼는 빠르지만 비싸므로, 적절한 균형이 중요합니다. 마지막으로, /api/ location에서는 더 엄격한 제한을 적용합니다.

API는 쿠키나 긴 URL이 필요 없으므로 512 bytes와 4KB로 줄입니다. 정규식으로 URL 길이를 직접 체크하여 2KB 이상은 414 에러로 즉시 거부합니다.

이렇게 경로별로 다르게 설정하면 보안이 더 강화됩니다. 여러분이 이 설정을 사용하면 메모리 기반 DoS 공격을 효과적으로 방어하고, 서버 메모리 사용량을 예측 가능하게 만들며, 비정상적인 요청을 조기에 감지하여 차단할 수 있습니다.

또한 동시 접속자가 늘어나도 메모리 사용량이 선형적으로 증가하여 안정적인 운영이 가능합니다.

실전 팁

💡 메모리 사용량 계산: 동시 접속 1000명 × 32KB(최대 헤더) = 32MB입니다. 서버 메모리를 고려하여 버퍼 크기와 worker_connections를 함께 설계하세요.

💡 large_client_header_buffers는 헤더 하나의 크기가 아니라 요청 라인 + 모든 헤더의 합입니다. URL이 2KB이고 헤더가 6KB면 8KB 버퍼 하나로 충분합니다.

💡 로그에서 큰 헤더를 추적하려면 $request_length를 기록하세요. 이상하게 큰 요청을 찾아 공격 패턴을 분석할 수 있습니다.

💡 CDN이나 로드밸런서를 사용하면 X-Forwarded-For 같은 헤더가 추가되어 헤더 크기가 커집니다. 프록시 체인이 길면 버퍼를 약간 늘려야 할 수 있습니다.

💡 414 Request-URI Too Large나 413 Request Entity Too Large 에러가 정상 사용자에게 발생한다면 버퍼가 너무 작은 것입니다. 로그를 확인하여 실제 필요한 크기를 파악하고 조정하세요.


9. 특정 HTTP 메서드 제한

시작하며

여러분의 API가 GET과 POST만 사용하는데 누군가 PUT, DELETE, TRACE, OPTIONS 같은 메서드로 무분별하게 요청을 보내는 상황을 겪어본 적 있나요? 혹은 TRACE 메서드를 이용한 XST(Cross-Site Tracing) 공격으로 쿠키가 노출되는 보안 취약점을 들어본 적이 있나요?

이런 문제는 HTTP 프로토콜이 기본적으로 많은 메서드를 지원하지만, 대부분의 애플리케이션은 소수의 메서드만 필요하기 때문에 발생합니다. 불필요한 메서드를 열어두면 공격 표면이 넓어지고, 의도하지 않은 동작이 실행될 수 있습니다.

특히 TRACE, CONNECT 같은 메서드는 디버깅 목적이지만 보안 위험이 있습니다. 바로 이럴 때 필요한 것이 HTTP 메서드 제한입니다.

필요한 메서드만 허용하고 나머지는 차단하여 공격 표면을 최소화할 수 있습니다.

개요

간단히 말해서, HTTP 메서드 제한은 서버가 처리할 수 있는 메서드를 명시적으로 지정하고, 나머지는 거부하는 화이트리스트 방식의 보안 기법입니다. 이 설정이 필요한 이유는 최소 권한 원칙을 적용하기 위해서입니다.

일반 웹사이트는 GET, POST만 있으면 되고, REST API는 GET, POST, PUT, DELETE 정도만 필요합니다. OPTIONS는 CORS preflight에 필요하지만, TRACE는 거의 사용되지 않으며 보안 위험만 있습니다.

예를 들어, 정적 파일 서버는 GET과 HEAD만 허용하고, 관리자 API는 POST만 허용하는 식으로 경로별로 다르게 설정할 수 있습니다. 기존에는 애플리케이션에서 메서드를 검증했다면, Nginx에서 미리 차단하면 불필요한 요청이 백엔드에 도달하지 않아 성능과 보안이 향상됩니다.

핵심 원칙은 화이트리스트 방식(필요한 것만 허용), 경로별 독립 설정(정적 파일과 API를 다르게), 그리고 명확한 에러 응답(405 Method Not Allowed)입니다. 이러한 원칙들이 공격 표면을 줄이고 의도하지 않은 동작을 방지합니다.

코드 예제

server {
    listen 80;
    server_name api.example.com;

    # 정적 파일 - GETHEAD만 허용
    location /static/ {
        limit_except GET HEAD {
            deny all;
        }

        root /var/www;
    }

    # 읽기 전용 API - GET만 허용
    location /api/public/ {
        limit_except GET {
            deny all;
        }

        proxy_pass http://readonly_backend;
    }

    # 일반 REST API - 표준 메서드만 허용
    location /api/ {
        limit_except GET POST PUT DELETE {
            deny all;
        }

        # OPTIONSCORS를 위해 별도 처리
        if ($request_method = 'OPTIONS') {
            add_header Access-Control-Allow-Origin '*';
            add_header Access-Control-Allow-Methods 'GET, POST, PUT, DELETE';
            add_header Access-Control-Max-Age 3600;
            return 204;
        }

        proxy_pass http://api_backend;
    }

    # TRACECONNECT 전역 차단 (보안 위험)
    if ($request_method !~ ^(GET|POST|PUT|DELETE|HEAD|OPTIONS)$ ) {
        return 405;
    }
}

설명

이것이 하는 일: 이 설정은 각 경로의 특성에 맞게 허용되는 HTTP 메서드를 제한하여, 불필요한 메서드를 통한 공격과 오용을 방지합니다. 첫 번째로, /static/ 경로는 정적 파일이므로 GET과 HEAD만 허용합니다.

limit_except GET HEAD { deny all; }는 "GET과 HEAD를 제외한 모든 메서드는 거부"라는 의미입니다. 누군가 POST /static/logo.png를 시도하면 405 Method Not Allowed를 받습니다.

HEAD는 파일 존재 여부나 크기를 확인하는 데 쓰이므로 허용하는 것이 좋습니다. 두 번째로, /api/public/ 경로는 공개 조회 API이므로 GET만 허용합니다.

읽기 전용 데이터를 제공하는 엔드포인트는 수정 메서드(POST, PUT, DELETE)를 원천 차단하여 실수나 공격으로 인한 데이터 변경을 방지합니다. 이것은 CQRS(Command Query Responsibility Segregation) 패턴의 구현이기도 합니다.

세 번째로, /api/ 경로는 일반 REST API이므로 GET, POST, PUT, DELETE를 허용합니다. OPTIONS는 CORS preflight 요청을 위해 별도로 처리하는데, if ($request_method = 'OPTIONS')로 감지하여 적절한 CORS 헤더를 보내고 204 No Content로 즉시 응답합니다.

이렇게 하면 브라우저의 preflight 요청을 Nginx에서 처리하여 백엔드 부하를 줄일 수 있습니다. 마지막으로, 서버 레벨에서 정규식으로 전역 필터를 추가합니다.

$request_method !~ ^(GET|POST|PUT|DELETE|HEAD|OPTIONS)$는 "이 목록에 없는 메서드"를 의미하며, TRACE, CONNECT, PATCH 같은 메서드는 모두 405로 거부됩니다. TRACE는 XST 공격에 악용될 수 있으므로 특히 차단이 중요합니다.

여러분이 이 설정을 사용하면 불필요한 HTTP 메서드를 통한 정보 유출과 공격을 방지하고, REST API의 의도된 동작만 허용하며, 보안 감사에서 지적받을 요소를 제거할 수 있습니다. 또한 경로별로 정확히 필요한 메서드만 열어서 최소 권한 원칙을 실천할 수 있습니다.

실전 팁

💡 limit_except는 화이트리스트 방식입니다. "GET을 제외하고 차단"이 아니라 "GET만 허용"입니다. 헷갈리지 않도록 주의하세요.

💡 CORS를 사용한다면 OPTIONS를 차단하면 브라우저 preflight가 실패합니다. OPTIONS는 허용하되 Nginx에서 직접 응답하여 백엔드 부하를 줄이세요.

💡 PATCH 메서드는 REST API에서 부분 수정에 사용되므로, 필요하다면 허용 목록에 추가하세요. 하지만 사용하지 않는다면 굳이 열어두지 마세요.

💡 로그에서 차단된 메서드를 추적하려면 $request_method를 로그 포맷에 포함하세요. 누군가 TRACE를 시도했다면 공격 징후일 수 있습니다.

💡 RESTful하지 않은 레거시 API는 POST만 사용하기도 합니다. 현재 시스템을 먼저 파악하고, 실제 사용되는 메서드만 허용하도록 설정하세요.


10. 로그 및 모니터링 강화

시작하며

여러분의 서버가 공격을 받고 있는데 로그가 너무 방대하거나 필요한 정보가 누락되어 분석이 불가능한 상황을 겪어본 적 있나요? 혹은 보안 사고 후 "누가, 언제, 어떤 요청을 보냈는지" 추적할 데이터가 없어서 곤란했던 경험이 있나요?

이런 문제는 기본 로그 설정으로는 보안 분석에 필요한 정보가 부족하기 때문입니다. 공격자의 IP, User-Agent, 응답 시간, 백엔드 상태 등의 정보가 있어야 공격 패턴을 분석하고 대응할 수 있습니다.

또한 로그가 너무 많으면 디스크가 가득 차고, 너무 적으면 증거가 부족합니다. 바로 이럴 때 필요한 것이 로그 및 모니터링 강화 설정입니다.

Nginx에서 적절한 로그 포맷과 레벨을 설정하면 보안 인시던트를 빠르게 탐지하고 대응할 수 있습니다.

개요

간단히 말해서, 로그 강화는 보안 분석에 필요한 정보를 포함하도록 로그 포맷을 커스터마이징하고, 적절한 로그 레벨과 로테이션을 설정하는 것입니다. 이 설정이 필요한 이유는 "측정할 수 없으면 개선할 수 없다"는 원칙 때문입니다.

공격 시도, 이상 트래픽, 성능 병목을 발견하려면 상세한 로그가 필수입니다. 예를 들어, 누군가 관리자 페이지에 무차별 대입 공격을 시도한다면 IP, User-Agent, 시간대 패턴을 분석하여 차단 규칙을 만들 수 있습니다.

GDPR이나 PCI DSS 같은 규정도 접근 로그 보관을 요구합니다. 기존에는 Nginx의 combined 포맷을 사용했다면, 보안에 특화된 커스텀 포맷을 만들면 훨씬 유용한 정보를 얻을 수 있습니다.

핵심 요소는 네 가지입니다. 상세한 로그 포맷(실제 클라이언트 IP, 응답 시간, 백엔드 상태 포함), 경로별 로그 분리(보안 이벤트와 일반 접속 분리), 에러 로그 레벨 조정(공격 시도 기록), 그리고 로그 로테이션(디스크 공간 관리)입니다.

이러한 설정들이 효과적인 보안 모니터링과 사고 대응을 가능하게 합니다.

코드 예제

http {
    # 보안 분석용 상세 로그 포맷
    log_format security '$remote_addr - $remote_user [$time_local] '
                        '"$request" $status $body_bytes_sent '
                        '"$http_referer" "$http_user_agent" '
                        '"$http_x_forwarded_for" '
                        'rt=$request_time uct=$upstream_connect_time '
                        'uht=$upstream_header_time urt=$upstream_response_time '
                        'country=$geoip2_country_code';

    # Rate Limiting 위반 로그 레벨
    limit_req_log_level warn;

    server {
        listen 80;
        server_name secure.example.com;

        # 일반 접속 로그
        access_log /var/log/nginx/access.log security;

        # 에러 로그 (warn 레벨 이상)
        error_log /var/log/nginx/error.log warn;

        # 관리자 페이지 - 별도 로그
        location /admin/ {
            access_log /var/log/nginx/admin_access.log security;
            error_log /var/log/nginx/admin_error.log notice;

            proxy_pass http://admin_backend;
        }

        # 차단된 요청 - 별도 로그
        location /blocked/ {
            access_log /var/log/nginx/blocked.log security;
            deny all;
        }

        # 정적 파일 - 로그 줄이기
        location ~* \.(jpg|jpeg|png|gif|ico|css|js)$ {
            access_log off;  # 또는 별도 파일
            expires 30d;
        }
    }
}

설명

이것이 하는 일: 이 설정은 보안 분석에 필요한 정보를 포함하는 커스텀 로그 포맷을 정의하고, 중요도에 따라 로그를 분리하여 효율적인 모니터링을 가능하게 합니다. 첫 번째로, log_format security가 상세한 정보를 포함하는 커스텀 포맷을 정의합니다.

$remote_addr는 직접 연결한 IP이고, $http_x_forwarded_for는 프록시 뒤의 실제 클라이언트 IP입니다. 로드밸런서를 사용하면 remote_addr은 LB의 IP이므로, X-Forwarded-For를 함께 기록해야 진짜 공격자를 추적할 수 있습니다.

$request_time은 전체 요청 처리 시간, $upstream_response_time은 백엔드 응답 시간으로, 이 둘의 차이로 Nginx에서의 지연을 파악할 수 있습니다. 두 번째로, GeoIP 정보를 포함하여 공격 출처를 분석합니다.

$geoip2_country_code는 MaxMind GeoIP2 모듈을 사용하여 국가 코드를 기록합니다(별도 설치 필요). 만약 중국에서 관리자 페이지 접속이 계속 들어온다면, 국가 단위 차단을 고려할 수 있습니다.

User-Agent도 함께 분석하면 봇인지 사람인지 구분할 수 있습니다. 세 번째로, 경로별로 로그를 분리하여 중요한 이벤트를 집중 관리합니다.

/admin/ 경로는 별도 파일(admin_access.log)에 기록하여, 관리자 페이지 접속 시도만 따로 모니터링할 수 있습니다. 이 로그에 실패한 로그인 시도가 많다면 무차별 대입 공격일 가능성이 높습니다.

반대로 정적 파일은 access_log off로 로그를 끄거나 별도 파일에 기록하여 노이즈를 줄입니다. 마지막으로, 에러 로그 레벨을 조정하여 보안 이벤트를 놓치지 않습니다.

error_log /var/log/nginx/error.log warn;은 경고 레벨 이상만 기록하는데, Rate Limiting 위반이나 SSL 에러는 여기에 포함됩니다. limit_req_log_level warn;은 Rate Limiting이 작동할 때 경고를 남겨서, 누가 제한에 걸렸는지 추적할 수 있게 합니다.

여러분이 이 설정을 사용하면 보안 인시던트를 빠르게 탐지하고, 공격 패턴을 분석하여 대응 규칙을 만들며, 규정 준수를 위한 감사 로그를 확보할 수 있습니다. 또한 성능 병목 지점을 찾아 최적화하고, 실제 사용자 경험을 데이터로 파악할 수 있습니다.

실전 팁

💡 로그 파일이 빠르게 커지므로 logrotate를 설정하세요. /etc/logrotate.d/nginx에서 일별 로테이션, 30일 보관, 압축 등을 설정할 수 있습니다.

💡 실시간 모니터링은 tail -f /var/log/nginx/access.log | grep -E "(40[0-9]|50[0-9])"로 에러 응답만 필터링하면 유용합니다. 또는 GoAccess 같은 실시간 분석 도구를 사용하세요.

💡 중요한 로그는 중앙 로그 서버로 전송하세요. rsyslog, Fluentd, Filebeat를 사용하여 Elasticsearch나 Splunk로 보내면 대규모 분석이 가능합니다.

💡 민감한 정보(비밀번호, 토큰)가 URL에 포함될 수 있으므로, 로그 필터링을 고려하세요. 예: if ($request_uri ~ "password=") { access_log off; }로 민감한 요청은 로그를 남기지 않습니다.

💡 로그를 JSON 형태로 출력하면 파싱이 쉽습니다. log_format json '{"time":"$time_iso8601","ip":"$remote_addr","method":"$request_method",...}'; 형식으로 설정하면 Logstash나 Filebeat가 쉽게 처리합니다.


#Nginx#RateLimiting#Security#APIGateway#DDoSProtection#Nginx,보안

댓글 (0)

댓글을 작성하려면 로그인이 필요합니다.