-
-
Save wkrea/98063a6f2becb3de8990f9a5c9481631 to your computer and use it in GitHub Desktop.
Deploying NGINX Plus as an API Gateway, Part 2: Protecting Backend Services
This file contains hidden or 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
log_format api_main '$remote_addr - $remote_user [$time_local] "$request"' | |
'$status $body_bytes_sent "$http_referer" "$http_user_agent"' | |
'"$http_x_forwarded_for" "$api_name"'; | |
include api_backends.conf; | |
include api_keys.conf; | |
limit_req_zone $binary_remote_addr zone=client_ip_10rs:1m rate=10r/s; | |
limit_req_zone $http_apikey zone=apikey_200rs:1m rate=200r/s; | |
server { | |
set $api_name -; # Start with an undefined API name, each API will update this value | |
access_log /var/log/nginx/api_access.log api_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.1 TLSv1.2; | |
# 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 | |
# Dummy location used to populate $request_body for JSON validation | |
location = /_NULL { | |
internal; | |
return 204; | |
} | |
} | |
js_include json_validator.js; | |
js_set $validated json_validator; | |
server { | |
listen 127.0.0.1:10415; # This is the error response of json_validator() | |
return 415; # Unsupported media type | |
include api_json_errors.conf; | |
} | |
# Coalesce request method to READ|WRITE | |
map $request_method $request_type { | |
"GET" "READ"; | |
"HEAD" "READ"; | |
"OPTIONS" "READ"; | |
default "WRITE"; | |
} | |
# vim: syntax=nginx |
This file contains hidden or 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 hidden or 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 hidden or 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
function json_validator(req) { | |
try { | |
if ( req.variables.request_body.length > 0 ) { | |
JSON.parse(req.variables.request_body); | |
} | |
return req.variables.upstream; | |
} catch (e) { | |
req.log('JSON.parse exception'); | |
return '127.0.0.1:10415'; // Address for error response | |
} | |
} |
This file contains hidden or 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 hidden or 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 { | |
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 hidden or 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 { | |
set $upstream pricing_service; | |
rewrite ^ /_warehouse last; | |
} | |
location /api/warehouse/inventory { | |
set $upstream inventory_service; | |
rewrite ^ /_warehouse last; | |
} | |
# Policy section | |
# | |
location = /_warehouse { | |
internal; | |
set $api_name "Warehouse"; | |
client_max_body_size 16k; | |
proxy_pass http://$upstream$request_uri; | |
} | |
# vim: syntax=nginx |
This file contains hidden or 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 { | |
set $upstream pricing_service; | |
rewrite ^ /_warehouse last; | |
} | |
location /api/warehouse/inventory { | |
set $upstream inventory_service; | |
rewrite ^ /_warehouse last; | |
} | |
# Policy section | |
# | |
location = /_warehouse { | |
internal; | |
set $api_name "Warehouse"; | |
mirror /_NULL; # Create a copy of the request to capture request body | |
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://$validated$request_uri; | |
} | |
# vim: syntax=nginx |
This file contains hidden or 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 DELETE { | |
deny all; | |
} | |
error_page 403 = @405; # Convert response from '403 (Forbidden)' to '405 (Method Not Allowed)' | |
set $upstream pricing_service; | |
rewrite ^ /_warehouse_$request_type last; | |
} | |
location /api/warehouse/inventory { | |
set $upstream inventory_service; | |
rewrite ^ /_warehouse_$request_type last; | |
} | |
# Policy section | |
# | |
location = /_warehouse_READ { | |
internal; | |
set $api_name "Warehouse"; | |
auth_jwt $api_name; | |
auth_jwt_key_file /etc/nginx/jwk.json; | |
proxy_pass http://$upstream$request_uri; | |
} | |
location = /_warehouse_WRITE { | |
internal; | |
set $api_name "Warehouse"; | |
auth_jwt $api_name; | |
auth_jwt_key_file /etc/nginx/jwk.json; | |
if ($jwt_claim_admin != "true") { # Write operations must have "admin":true | |
return 403; # Forbidden | |
} | |
proxy_pass http://$upstream$request_uri; | |
} | |
# vim: syntax=nginx |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment