NGINX API Gateway – Backend 서비스 보호

NGINX 오픈소스 및 NGINX Plus를 API Gateway로 배포하는 것에 대한 시리즈의 두 번째 모범사례입니다.

참고 : 명시된 경우를 제외하고 이 모범 사례의 모든 정보는 NGINX 오픈소스와 NGINX Plus 모두에 적용됩니다. 읽기 쉽도록 나머지 부분은 “NGINX”로 표현합니다.

목차

1. 속도 제한(Rate limiting)
2. 특정 요청 메소드만 허용
3. 세분화된 액세스 제어 적용
  3-1. 특정 리소스에 대한 액세스 제어
  3-2. 특정 메서드에 대한 액세스 제어
4. 요청 사이즈 제어
5. 요청 본문 유효성 검증
  5-1. $request_body 변수에 대한 참고 사항
6. 요약

1. 속도 제한(Rate limiting)

브라우저 기반 클라이언트와 달리 개별 API 클라이언트는 다른 API 클라이언트가 효과적으로 잠겨 있는 시스템 리소스를 너무 많이 사용하는 경우에도 API에 막대한 부하를 가할 수 있습니다. 악의적인 클라이언트만이 이러한 위협을 제기하는 것이 아닙니다. 오작동하거나 버그가 있는 API 클라이언트가 Backend를 압도하는 루프에 들어갈 수 있습니다. 이를 방지하기 위해 각 클라이언트의 공정한 사용을 보장하고 Backend 서비스의 리소스를 보호하기 위해 속도 제한을 적용합니다.

NGINX는 요청의 모든 속성을 기반으로 속도 제한을 적용할 수 있습니다. 일반적으로 클라이언트 IP 주소가 사용되지만 API에 대해 인증이 활성화된 경우 인증된 클라이언트 ID가 더 안정적이고 정확한 속성입니다.

속도 제한(Rate limiting) 자체는 최상위 API Gateway 구성 파일에 정의된 다음 전역적으로, API별로 또는 URI별로 적용될 수 있습니다.

include api_backends.conf;
include api_keys.conf;

limit_req_zone $binary_remote_addr zone=client_ip_10rs:1m rate=1r/s;
limit_req_zone $http_apikey        zone=apikey_200rs:1m   rate=200r/s;

server {
    access_log /var/log/nginx/api_access.log main; # Each API may also log to a 
                                                   # separate file

    listen 443 ssl;
    server_name api.example.com;

    # TLS config
    ssl_certificate      /etc/ssl/certs/api.example.com.crt;
    ssl_certificate_key  /etc/ssl/private/api.example.com.key;
    ssl_session_cache    shared:SSL:10m;
    ssl_session_timeout  5m;
    ssl_ciphers          HIGH:!aNULL:!MD5;
    ssl_protocols        TLSv1.2 TLSv1.3;

    # API definitions, one per file
    include api_conf.d/*.conf;

    # Error responses
    error_page 404 = @400;         # Invalid paths are treated as bad requests
    proxy_intercept_errors on;     # Do not send backend errors to the client
    include api_json_errors.conf;  # API client friendly JSON error responses
    default_type application/json; # If no content-type then assume JSON
}

이 예에서 4행의 limit_req_zone 지시문은 각 클라이언트 IP 주소($binary_remote_addr)에 대해 초당 10개 요청의 비율 제한을 정의하고 5행의 제한은 인증된 각 클라이언트 ID($ http_apikey). 이것은 적용되는 위치에 관계없이 여러 비율 제한을 정의하는 방법을 보여줍니다. API는 동시에 여러 비율 제한을 적용하거나 다른 리소스에 대해 다른 비율 제한을 적용할 수 있습니다.

그런 다음 다음 구성 스니펫에서 limit_req 지시문을 사용하여 1부에 설명된 “Warehouse API”의 정책 섹션에서 첫 번째 속도 제한을 적용합니다. 기본적으로 NGINX는 속도 제한이 완료되었을 때 503(서비스 사용 불가) 응답을 보냅니다. 초과했습니다. 그러나 API 클라이언트가 자신의 동작을 수정할 수 있도록 속도 제한을 초과했음을 명시적으로 아는 것이 도움이 됩니다. 이를 위해 limit_req_status 지시문을 사용하여 대신 429(Too Many Requests) 응답을 보냅니다.

# Warehouse API
#
location /api/warehouse/ {
    # Policy configuration here (authentication, rate limiting, logging...)
    #
    access_log /var/log/nginx/warehouse_api.log main;
    limit_req zone=client_ip_10rs;
    limit_req_status 429;

    # URI routing
    #
    location /api/warehouse/inventory {
        limit_except GET {
            deny all;
        }
        error_page 403 = @405; # Convert deny response from '403 (Forbidden)'
                               # to '405 (Method Not Allowed)'
        proxy_pass http://warehouse_inventory;
    }

    location /api/warehouse/pricing {
        limit_except GET PATCH {
            deny all;
        }
        error_page 403 = @405;
        proxy_pass http://warehouse_pricing;
    }

    return 404; # Catch-all
}

limit_req 지시문에 대한 추가 매개변수를 사용하여 NGINX가 속도 제한을 적용하는 방법을 미세 조정할 수 있습니다. 예를 들어 제한을 초과할 때 요청을 완전히 거부하는 대신 대기열에 추가하여 요청 비율이 정의된 제한 아래로 떨어지는 시간을 허용할 수 있습니다.

2. 특정 요청 메소드만 허용

RESTful API에서 HTTP 메소드는 각 API 호출의 중요한 부분이며 API 정의에 매우 중요합니다. Warehouse API의 가격 책정 서비스를 예로 들어 보겠습니다.

  • GET /api/warehouse/pricing/item001        item001의 가격을 반환합니다.
  • PATCH /api/warehouse/pricing/item001   item001의 가격을 변경합니다.

Warehouse API의 URI 라우팅 정의를 업데이트하여 가격 책정 서비스에 대한 요청에서 이 두 가지 HTTP 메서드만(인벤토리 서비스에 대한 요청에서는 GET 메서드만) 수락할 수 있습니다.

  # URI routing
    #
    location /api/warehouse/inventory {
        limit_except GET {
            deny all; 
        }
        error_page 403 = @405; # Convert deny response from '403 (Forbidden)'
                               # to '405 (Method Not Allowed)'
        proxy_pass http://warehouse_inventory;
    }

    location /api/warehouse/pricing {
        limit_except GET PATCH {
            deny all;
        }
        error_page 403 = @405; # Convert deny response from '403 (Forbidden)'
                               # to '405 (Method Not Allowed)'
        proxy_pass http://warehouse_pricing;
    }

이 구성을 사용하면 22행에 나열된 방법 이외의 방법을 사용하는 가격 책정 서비스(및 13행 이외의 인벤토리 서비스에 대한 요청)가 거부되고 Backend 서비스로 전달되지 않습니다. NGINX는 405(Method Not Allowed) 응답을 전송하여 다음 콘솔 추적과 같이 API 클라이언트에 오류의 정확한 특성을 알립니다. 최소 공개 보안 정책이 필요한 경우 error_page 지시문을 사용하여 이 응답을 정보가 덜한 오류(예: 400(잘못된 요청))로 대신 변환할 수 있습니다.

$ curl https://api.example.com/api/warehouse/pricing/item001
{"sku":"item001","price":179.99}
$ curl -X DELETE https://api.example.com/api/warehouse/pricing/item001
{"status":405,"message":"Method not allowed"}

3. 세분화된 액세스 제어 적용

이 시리즈의 1부에서는 API Key 및 JWT(JSON 웹 토큰) 와 같은 인증 옵션을 활성화하여 무단 액세스로부터 API를 보호하는 방법을 설명했습니다 . 인증된 ID 또는 인증된 ID의 속성을 사용하여 세분화된 액세스 제어를 수행할 수 있습니다.

여기에서는 그러한 두 가지 예를 보여줍니다.

  • 첫 번째 예는 특정 API 리소스에 대한 액세스를 제어 하여 1부에서 설명한 구성을 확장하고 API 키 인증을 사용하여 주어진 API 클라이언트가 허용 목록에 있는지 확인합니다.
  • 클라이언트가 사용할 수 있는 HTTP 메서드 의 두 번째 예 입니다. 1부에서 언급한 JWT 인증 방법을 구현하고 사용자 지정 클레임을 사용하여 자격을 갖춘 API 클라이언트를 식별합니다. (JWT 지원은 NGINX Plus 전용입니다.)

물론 HTTP 기본 인증 및 OAuth 2.0 토큰 자체 검사 와 같은 다른 인증 방법을 이러한 샘플 사용 사례에 적용할 수 있습니다.

3-1. 특정 리소스에 대한 액세스 제어

“인프라 클라이언트”만 Warehouse API 인벤토리 서비스의 감사 리소스에 액세스할 수 있도록 허용한다고 가정해 보겠습니다. API 키 인증이 활성화되면 맵 블록을 사용하여 인프라 클라이언트 이름의 허용 목록을 생성하여 해당 API 키가 사용될 때 $is_infrastructure 변수가 1로 평가되도록 합니다.

map $api_client_name $is_infrastructure {
    default       0;

    "client_one"  1;
    "client_six"  1;
}

Warehouse API의 정의에서 인벤토리 감사 리소스에 대한 위치 블록을 추가합니다(15-20행). if 블록은 인프라 클라이언트만 리소스에 액세스할 수 있도록 합니다.

# Warehouse API
#
location /api/warehouse/ {
    # Policy configuration here (authentication, rate limiting, logging...)
    #
    access_log /var/log/nginx/warehouse_api.log main;
    auth_request /_validate_apikey;

    # URI routing
    #
    location /api/warehouse/inventory {
        proxy_pass http://warehouse_inventory;
    }

    location = /api/warehouse/inventory/audit {
        if ($is_infrastructure = 0) {
            return 403; # Forbidden (not infrastructure)
        }
        proxy_pass http://warehouse_inventory;
    }

    location /api/warehouse/pricing {
        proxy_pass http://warehouse_pricing;
    }

    return 404; # Catch-all
}

15행의 위치 지시문은 =(등호) 수정자를 사용하여 감사 리소스에서 정확히 일치하도록 합니다. 정확히 일치하는 항목은 다른 리소스에 사용되는 기본 경로 접두어 정의보다 우선합니다. 다음 추적은 이 구성을 사용하여 허용 목록에 없는 클라이언트가 인벤토리 감사 리소스에 액세스할 수 없는 방법을 보여줍니다. 표시된 API 키는 client_two에 속합니다(1부에서 정의됨).

$ curl -H "apikey: QzVV6y1EmQFbbxOfRCwyJs35" https://api.example.com/api/warehouse/inventory/audit
{"status":403,"message":"Forbidden"}

3-2. 특정 메서드에 대한 액세스 제어

위에서 정의한 바와 같이 가격 책정 서비스는 GET 및 PATCH 방법을 허용하며, 이를 통해 클라이언트는 각각 특정 항목의 가격을 획득하고 수정할 수 있습니다. (가격 데이터의 전체 수명 주기 관리를 제공하기 위해 POST 및 DELETE 방법을 허용하도록 선택할 수도 있습니다.) 이 섹션에서는 특정 사용자가 발행할 수 있는 방법을 제어하기 위해 해당 사용 사례를 확장합니다. Warehouse API에 대해 JWT 인증이 활성화되면 각 클라이언트에 대한 권한이 사용자 정의 클레임으로 인코딩됩니다. 가격 데이터를 변경할 수 있는 권한이 있는 관리자에게 발급된 JWT에는 “admin”:true 클레임이 포함됩니다. 이제 관리자만 변경할 수 있도록 액세스 제어 논리를 확장합니다.

# Access to write operations is evaluated by JWT claim 'admin'
map $request_method $admin_permitted_method {
    "GET"     true;
    "HEAD"    true;
    "OPTIONS" true;
    default   $jwt_claim_admin;
}

api_gateway.conf 하단에 추가된 이 맵 블록은 요청 메소드($request_method)를 입력으로 받아 새로운 변수 $admin_permitted_method를 생성합니다. 읽기 전용 방법은 항상 허용되지만(라인 62–64) 쓰기 작업에 대한 액세스는 JWT(라인 65)의 admin 클레임 값에 따라 다릅니다. 이제 관리자만 가격을 변경할 수 있도록 Warehouse API 구성을 확장합니다.

# Warehouse API
#
location /api/warehouse/ {
    # Policy configuration here (authentication, rate limiting, logging...)
    #
    access_log /var/log/nginx/warehouse_api.log main;
    auth_jwt "Warehouse API";
    auth_jwt_key_file /etc/nginx/idp_jwks.json;

    # URI routing
    #
    location /api/warehouse/inventory {
        limit_except GET {
            deny all; 
        }
        error_page 403 = @405; # Convert deny response from '403 (Forbidden)'
                               # to '405 (Method Not Allowed)'
        proxy_pass http://warehouse_inventory;
    }

    location /api/warehouse/pricing {
        limit_except GET PATCH {
            deny all;
        }
        if ($admin_permitted_method != "true") {
            return 403;
        }
        error_page 403 = @405; # Convert deny response from '403 (Forbidden)'
                               # to '405 (Method Not Allowed)'
        proxy_pass http://warehouse_pricing;
    }

    return 404; # Catch-all
}

Warehouse API를 사용하려면 모든 클라이언트가 유효한 JWT를 제시해야 합니다(7행). 또한 $admin_permitted_method 변수를 평가하여 쓰기 작업이 허용되는지 확인합니다(25행). JWT 인증은 NGINX Plus 전용입니다.

4. 요청 사이즈 제어

HTTP API는 일반적으로 요청 본문을 사용하여 Backend API 서비스가 처리할 지침과 데이터를 포함합니다. 이것은 XML/SOAP API와 JSON/REST API에 해당됩니다. 결과적으로 요청 본문은 Backend API 서비스에 대한 공격 벡터가 될 수 있으며, 이는 매우 큰 요청 본문을 처리할 때 버퍼 오버플로 공격에 취약할 수 있습니다.

기본적으로 NGINX는 본문이 1MB보다 큰 요청을 거부합니다. 특히 이미지 처리와 같은 대용량 페이로드를 처리하는 API의 경우 이 값을 늘릴 수 있지만 대부분의 API에 대해 더 낮은 값을 설정합니다.

# Warehouse API
#
location /api/warehouse/ {
    # Policy configuration here (authentication, rate limiting, logging...)
    #
    access_log /var/log/nginx/warehouse_api.log main;
    client_max_body_size 16k;

7행의 client_max_body_size 지시문은 요청 본문의 크기를 제한합니다. 이 구성을 사용하면 가격 책정 서비스에 대한 두 개의 서로 다른 PATCH 요청을 수신할 때 API Gateway의 동작을 비교할 수 있습니다. 첫 번째 curl 명령은 작은 JSON 데이터 조각을 보내는 반면 두 번째 명령은 큰 파일(/etc/services)의 내용을 보내려고 시도합니다.

$ curl -iX PATCH -d '{"price":199.99}' https://api.example.com/api/warehouse/pricing/item001
HTTP/1.1 204 No Content
Server: nginx/1.19.5
Connection: keep-alive

$ curl -iX PATCH -d@/etc/services https://api.example.com/api/warehouse/pricing/item001
HTTP/1.1 413 Request Entity Too Large
Server: nginx/1.19.5
Content-Type: application/json
Content-Length: 45
Connection: close

{"status":413,"message":"Payload too large"}

5. 요청 본문 유효성 검증

Editor  – 다음 사용 사례는 NGINX JavaScript 모듈에 대한 여러 가지 중 하나입니다. ]

요청 본문이 큰 버퍼 오버플로 공격에 취약할 뿐만 아니라 Backend API 서비스는 유효하지 않거나 예기치 않은 데이터가 포함된 본문에 취약할 수 있습니다. 요청 본문에 올바른 형식의 JSON이 필요한 애플리케이션의 경우 NGINX JavaScript 모듈을 사용하여 JSON 데이터가 Backend API 서비스에 프록시하기 전에 오류 없이 구문 분석되는지 확인할 수 있습니다.

JavaScript 모듈이 설치된 상태에서 js_import 지시문을 사용하여 JSON 데이터의 유효성을 검사하는 함수에 대한 JavaScript 코드가 포함된 파일을 참조합니다.


js_import json_validation.js;
js_set $json_validated json_validation.parseRequestBody;

js_set 지시문은 parseRequestBody 함수를 호출하여 평가되는 새 변수 $json_validated를 정의합니다.

export default { parseRequestBody };

function parseRequestBody(r) {
    try {
        if (r.variables.request_body) {
            JSON.parse(r.variables.request_body);
        }
        return r.variables.upstream;
    } catch (e) {
        r.error('JSON.parse exception');
        return '127.0.0.1:10415'; // Address for error response
    }
}

parseRequestBody 함수는 JSON.parse 메소드를 사용하여 요청 본문의 구문 분석을 시도합니다(6행). 구문 분석이 성공하면 이 요청에 대해 의도된 업스트림 그룹의 이름이 반환됩니다(8행). 요청 본문을 구문 분석할 수 없는 경우(예외 발생) 로컬 서버 주소가 반환됩니다(11행). return 지시문은 $json_validated 변수를 채워 요청을 보낼 위치를 결정하는 데 사용할 수 있습니다.

# URI routing
    #
    location /api/warehouse/inventory {
        proxy_pass http://warehouse_inventory;
    }

    location /api/warehouse/pricing {
        set $upstream warehouse_pricing;
        mirror /_get_request_body;        # Force early read
        client_body_in_single_buffer on;  # Minimize memory copy operations 
                                          # on request body
        client_body_buffer_size      16k; # Largest body to keep in memory 
                                          # (before writing to file)
        client_max_body_size         16k;
        proxy_pass http://$json_validated$request_uri;
    }

Warehouse API의 URI 라우팅 섹션에서 22행의 proxy_pass 지시문을 수정합니다. 이전 섹션에서 논의한 Warehouse API 구성에서와 같이 Backend API 서비스에 요청을 전달하지만 이제 $json_validated 변수를 대상 주소로 사용합니다. . 클라이언트 본문이 JSON으로 성공적으로 구문 분석된 경우 15행에 정의된 업스트림 그룹으로 프록시합니다. 그러나 예외가 있는 경우 반환된 값 127.0.0.1:10415를 사용하여 클라이언트에 오류 응답을 보냅니다.

server {
    listen 127.0.0.1:10415;
    return 415; # Unsupported media type
    include api_json_errors.conf;
}

요청이 이 가상 서버로 프록시 처리되면 NGINX는 415(지원되지 않는 미디어 유형) 응답을 클라이언트에 보냅니다.

이 완전한 구성을 사용하면 NGINX는 올바르게 형식이 지정된 JSON 본문이 있는 경우에만 Backend API 서비스에 대한 요청을 프록시합니다.

$ curl -iX POST -d '{"sku":"item002","price":85.00}' https://api.example.com/api/warehouse/pricing
HTTP/1.1 201 Created
Server: nginx/1.19.5
Location: /api/warehouse/pricing/item002

$ curl -X POST -d 'item002=85.00' https://api.example.com/api/warehouse/pricing
{"status":415,"message":"Unsupported media type"}

5-1. $request_body 변수에 대한 참고 사항

JavaScript 함수 parseRequestBody는 $request_body 변수를 사용하여 JSON 구문 분석을 수행합니다. 그러나 NGINX는 기본적으로 이 변수를 채우지 않으며 중간 복사본을 만들지 않고 요청 본문을 Backend로 스트리밍합니다. URI 라우팅 섹션(16행) 내에서 미러 지시문을 사용하여 클라이언트 요청의 복사본을 만들고 결과적으로 $request_body 변수를 채웁니다.

    location /api/warehouse/pricing {
        set $upstream warehouse_pricing;
        mirror /_get_request_body;        # Force early read
        client_body_in_single_buffer on;  # Minimize memory copy operations 
                                          # on request body
        client_body_buffer_size      16k; # Largest body to keep in memory 
                                          # (before writing to file)
        client_max_body_size         16k;
        proxy_pass http://$json_validated$request_uri;
    }

17행과 19행의 지시문은 NGINX가 내부적으로 요청 본문을 처리하는 방법을 제어합니다. 요청 본문이 디스크에 기록되지 않도록 client_body_buffer_size를 client_max_body_size와 동일한 크기로 설정합니다. 이렇게 하면 디스크 I/O 작업을 최소화하여 전체 성능이 향상되지만 추가 메모리 활용이 희생됩니다. 요청 본문이 작은 대부분의 API Gateway 사용 사례의 경우 이는 좋은 절충안입니다.

언급했듯이 미러 지시문은 클라이언트 요청의 복사본을 생성합니다. $request_body를 채우는 것 외에는 이 복사본이 필요하지 않으므로 최상위 API Gateway 구성의 서버 블록에 정의한 “막다른 골목” 위치(/_get_request_body)로 보냅니다.

    # Dummy location used to populate $request_body for JSON validation
    location /_get_request_body {
        return 204;
    }

이 위치는 204(No Content) 응답을 보내는 것 외에는 아무 것도 하지 않습니다. 이 응답은 미러링된 요청과 관련되어 있으므로 무시되므로 원래 클라이언트 요청 처리에 무시할 수 있는 오버헤드가 추가됩니다.

6. 요약

NGINX 오픈소스 및 NGINX Plus를 API Gateway로 배포하는 것에 대한 이 시리즈의 두 번째 모범 사례에서는 악의적이고 오작동하는 클라이언트로부터 프로덕션 환경의 Backend API 서비스를 보호하는 문제에 중점을 두었습니다. NGINX는 오늘날 인터넷에서 가장 바쁜 사이트에 전원을 공급하고 보호하는 데 사용되는 API 트래픽 관리에 동일한 기술을 사용합니다 .

NGINX STORE 뉴스레터 및 최신 소식 구독하기

* indicates required