Last active
November 10, 2022 23:53
-
-
Save nginx-gists/6a8e7c65fdc41bc955f7e67a1d475469 to your computer and use it in GitHub Desktop.
Deploying NGINX Plus as an API Gateway, Part 2: Protecting Backend Services
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | |
# API key validation | |
location = /_validate_apikey { | |
internal; | |
if ($http_apikey = "") { | |
return 401; # Unauthorized | |
} | |
if ($api_client_name = "") { | |
return 403; # Forbidden | |
} | |
return 204; # OK (no content) | |
} | |
# Dummy location used to populate $request_body for JSON validation | |
location /_get_request_body { | |
return 204; | |
} | |
} | |
js_import json_validation.js; | |
js_set $json_validated json_validation.parseRequestBody; | |
server { | |
listen 127.0.0.1:10415; | |
return 415; # Unsupported media type | |
include api_json_errors.conf; | |
} | |
# 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; | |
} | |
# vim: syntax=nginx |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
error_page 400 = @400; | |
location @400 { return 400 '{"status":400,"message":"Bad request"}\n'; } | |
error_page 401 = @401; | |
location @401 { return 401 '{"status":401,"message":"Unauthorized"}\n'; } | |
error_page 403 = @403; | |
location @403 { return 403 '{"status":403,"message":"Forbidden"}\n'; } | |
error_page 404 = @404; | |
location @404 { return 404 '{"status":404,"message":"Resource not found"}\n'; } | |
error_page 405 = @405; | |
location @405 { return 405 '{"status":405,"message":"Method not allowed"}\n'; } | |
error_page 408 = @408; | |
location @408 { return 408 '{"status":408,"message":"Request timeout"}\n'; } | |
error_page 413 = @413; | |
location @413 { return 413 '{"status":413,"message":"Payload too large"}\n'; } | |
error_page 414 = @414; | |
location @414 { return 414 '{"status":414,"message":"Request URI too large"}\n'; } | |
error_page 415 = @415; | |
location @415 { return 415 '{"status":415,"message":"Unsupported media type"}\n'; } | |
error_page 426 = @426; | |
location @426 { return 426 '{"status":426,"message":"HTTP request was sent to HTTPS port"}\n'; } | |
error_page 429 = @429; | |
location @429 { return 429 '{"status":429,"message":"API rate limit exceeded"}\n'; } | |
error_page 495 = @495; | |
location @495 { return 495 '{"status":495,"message":"Client certificate authentication error"}\n'; } | |
error_page 496 = @496; | |
location @496 { return 496 '{"status":496,"message":"Client certificate not presented"}\n'; } | |
error_page 497 = @497; | |
location @497 { return 497 '{"status":497,"message":"HTTP request was sent to mutual TLS port"}\n'; } | |
error_page 500 = @500; | |
location @500 { return 500 '{"status":500,"message":"Server error"}\n'; } | |
error_page 501 = @501; | |
location @501 { return 501 '{"status":501,"message":"Not implemented"}\n'; } | |
error_page 502 = @502; | |
location @502 { return 502 '{"status":502,"message":"Bad gateway"}\n'; } | |
# vim: syntax=nginx |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
map $http_apikey $api_client_name { | |
default ""; | |
"7B5zIqmRGXmrJTFmKa99vcit" "client_one"; | |
"QzVV6y1EmQFbbxOfRCwyJs35" "client_two"; | |
"mGcjH8Fv6U9y3BVF9H3Ypb9T" "client_six"; | |
} | |
# Infrastructure clients | |
# | |
map $api_client_name $is_infrastructure { | |
default 0; | |
"client_one" 1; | |
"client_six" 1; | |
} | |
# vim: syntax=nginx |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# API definition | |
# | |
location /api/warehouse/pricing { | |
limit_except GET PATCH { | |
deny all; | |
} | |
error_page 403 = @405; # Convert response from '403 (Forbidden)' | |
# to '405 (Method Not Allowed)' | |
set $upstream pricing_service; | |
rewrite ^ /_warehouse last; | |
} | |
location /api/warehouse/inventory { | |
limit_except GET { | |
deny all; | |
} | |
error_page 403 = @405; | |
set $upstream inventory_service; | |
rewrite ^ /_warehouse last; | |
} | |
# Policy section | |
# | |
location = /_warehouse { | |
internal; | |
set $api_name "Warehouse"; | |
limit_req zone=client_ip_10rs; | |
limit_req_status 429; | |
proxy_pass http://$upstream$request_uri; | |
} | |
# vim: syntax=nginx |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# Warehouse API | |
# | |
location /api/warehouse/pricing { | |
set $upstream pricing_service; | |
rewrite ^ /_warehouse last; | |
} | |
location /api/warehouse/inventory { | |
set $upstream inventory_service; | |
rewrite ^ /_warehouse last; | |
} | |
location = /api/warehouse/inventory/audit { | |
if ($is_infrastructure = 0) { | |
return 403; # Forbidden (not infrastructure) | |
} | |
set $upstream inventory_service; | |
rewrite ^ /_warehouse last; | |
} | |
# Policy section | |
# | |
location = /_warehouse { | |
internal; | |
set $api_name "Warehouse"; | |
if ($http_apikey = "") { | |
return 401; # Unauthorized (please authenticate) | |
} | |
if ($api_client_name = "") { | |
return 403; # Forbidden (invalid API key) | |
} | |
proxy_pass http://$upstream$request_uri; | |
} | |
# vim: syntax=nginx |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# 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; | |
# URI routing | |
# | |
location /api/warehouse/inventory { | |
proxy_pass http://warehouse_inventory; | |
} | |
location /api/warehouse/pricing { | |
proxy_pass http://warehouse_pricing; | |
} | |
return 404; # Catch-all | |
} | |
# vim: syntax=nginx |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# Warehouse API | |
# | |
location /api/warehouse/ { | |
# Policy configuration here (authentication, rate limiting, logging...) | |
# | |
access_log /var/log/nginx/warehouse_api.log main; | |
# 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; | |
} | |
return 404; # Catch-all | |
} | |
# vim: syntax=nginx |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# 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 | |
} | |
# vim: syntax=nginx |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# 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 | |
} | |
# vim: syntax=nginx |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# 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 | |
} | |
# vim: syntax=nginx |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
For a discussion of these files, see Deploying NGINX Plus as an API Gateway, Part 2: Protecting Backend Services