Configure NGINX microgateway for MTLS termination and client certificate hash verification

Problem this snippet solves:

This configuration is used to support advanced Open Banking scenarios where NGINX performs MTLS termination and checks the validity of the JWT by comparing the computed client certificate hash with the hash found inside JWT.

How to use this snippet:

The code snipped shows the contents of nginx.conf and x5t.js, the njs function responsible for computing the client certificate hash.

Code :

##########
#nginx.conf:
##########

user nginx;
worker_processes auto;
load_module modules/ngx_http_js_module.so;
load_module modules/ngx_stream_js_module.so;
error_log /var/log/nginx/error.log debug;


events {
    worker_connections 10240;
}
http {
    include /etc/nginx/mime.types;
    default_type application/octet-stream;
    error_log /var/log/nginx/error.log;
    log_format jwt_log_format '$remote_addr - "$jwt_claim_sub" [$time_local] "$request" $status $body_bytes_sent "$http_referer" "$http_user_agent" "$http_x_forwarded_for" "$host" sn="$server_name" rt=$request_time ua="$upstream_addr" us="$upstream_status" ut="$upstream_response_time" ul="$upstream_response_length" cs="$upstream_cache_status" http_authorization="$http_authorization" ssl_client_verify="$ssl_client_verify" jwt_cnf_fingerprint="$jwt_cnf_fingerprint" client_cert=$ssl_client_raw_cert thumbprint_match=$thumbprint_match';
 
    sendfile on;
    keyval_zone zone=one:1m type=ip state=one.keyval timeout=1h;
    keyval $remote_addr $target zone=one;
    keepalive_timeout 65;
    server_tokens off;
    map $request_uri $request_uri_path {
        '~^(?P[^?]*)(\?.*)?$' $path;
    }
    limit_req_zone $binary_remote_addr zone=policy_1_99c0d81f-d277-4cd3-a97b-e94dde53453f_binary_remote_addr:10m rate=5000r/m;
    proxy_cache_path /etc/nginx/jwk_bank_idp levels=1 keys_zone=jwk_bank_idp:1m max_size=10m;
    map $jwt_claim_scope $passes_conditional_policy_policy_1_761 {
        default 0;
        ~.*payments.* 1;
    }
    auth_jwt_claim_set $jwt_cnf_fingerprint 'cnf' 'x5t#S256';
    js_import /etc/nginx/x5t.js;
    js_set $thumbprint_match x5t.validate;
    
    server {
        server_name api.bank.f5lab;
        listen 443 ssl;
        ssl_certificate /etc/nginx/f5lab.crt;
        ssl_certificate_key /etc/nginx/f5lab.key;
        ssl_session_cache off;
        ssl_prefer_server_ciphers off;
        ssl_client_certificate /etc/nginx/updated_ca.crt;
        ssl_verify_client on;
        ssl_verify_depth 10;
        
        access_log /var/log/nginx/access.log jwt_log_format;
        
        location = /_jwks_uri_bank_idp {
            internal;
            subrequest_output_buffer_size 64k;
            proxy_cache jwk_bank_idp;
            proxy_cache_valid 200 1h;
            proxy_cache_use_stale error timeout updating;
            proxy_ignore_headers Cache-Control Expires Set-Cookie;
            proxy_method GET;
            proxy_pass https://idp.bank.f5lab/ext/open_banking;
        }
        location = /open-banking/v3.1/pisp/domestic-payments {
            if ($request_method !~ '^GET|POST$') {
                return 405;
            }
            error_log /dev/null;
            proxy_set_header Host '';
            proxy_set_header Host bank.f5lab;
            proxy_set_header X-Forwarded-For $remote_addr;
            proxy_set_header Connection '';
            proxy_http_version 1.1;
            limit_req zone=policy_1_99c0d81f-d277-4cd3-a97b-e94dde53453f_binary_remote_addr nodelay;
            limit_req_status 429;
            auth_jwt bank_idp;
            auth_jwt_key_request /_jwks_uri_bank_idp;
            if ($passes_conditional_policy_policy_1_761 = 0) {
                return 403 'Access denied because payments scope is missing';
            }
            if ($thumbprint_match != 1) {
                return 403 'Access denied because client SSL certificate thumbprint does not match jwt_cnf_fingerprint';
            }
            if ($ssl_client_verify != "SUCCESS") { 
                return 403 'Client SSL cert verification failed'; 
            } 
            proxy_pass http://wl_openbanking;
        }
        location ~* /open-banking/v3.1/pisp/domestic-payments/* {
            if ($request_method !~ '^GET|POST$') {
                return 405;
            }
            error_log /dev/null;
            proxy_set_header Host '';
            proxy_set_header Host bank.f5lab;
            proxy_set_header X-Forwarded-For $remote_addr;
            proxy_set_header Connection '';
            proxy_http_version 1.1;
            limit_req zone=policy_1_99c0d81f-d277-4cd3-a97b-e94dde53453f_binary_remote_addr nodelay;
            limit_req_status 429;
            auth_jwt bank_idp;
            auth_jwt_key_request /_jwks_uri_bank_idp;
            if ($passes_conditional_policy_policy_1_761 = 0) {
                return 403 'Access denied because payments scope is missing';
            }
            if ($thumbprint_match != 1) {
                return 403 'Access denied because client SSL certificate thumbprint does not match jwt_cnf_fingerprint';
            }
            if ($ssl_client_verify != "SUCCESS") { 
                return 403 'Client SSL cert verification failed'; 
            } 
            proxy_pass http://wl_openbanking;
        }
        location = /open-banking/v3.1/pisp/domestic-payment-consents {
            if ($request_method !~ '^GET|POST$') {
                return 405;
            }
            error_log /dev/null;
            proxy_set_header Host '';
            proxy_set_header Host bank.f5lab;
            proxy_set_header X-Forwarded-For $remote_addr;
            proxy_set_header Connection '';
            proxy_http_version 1.1;
            limit_req zone=policy_1_99c0d81f-d277-4cd3-a97b-e94dde53453f_binary_remote_addr nodelay;
            limit_req_status 429;
            auth_jwt bank_idp;
            auth_jwt_key_request /_jwks_uri_bank_idp;
            if ($passes_conditional_policy_policy_1_761 = 0) {
                return 403 'Access denied because payments scope is missing';
            }
            if ($thumbprint_match != 1) {
                return 403 'Access denied because client SSL certificate thumbprint does not match jwt_cnf_fingerprint';
            }
            if ($ssl_client_verify != "SUCCESS") { 
                return 403 'Client SSL cert verification failed'; 
            } 
            proxy_pass http://wl_openbanking;
        }
        location ~* /open-banking/v3.1/pisp/domestic-payment-consents/* {
            if ($request_method !~ '^GET|POST$') {
                return 405;
            }
            error_log /dev/null;
            proxy_set_header Host '';
            proxy_set_header Host bank.f5lab;
            proxy_set_header X-Forwarded-For $remote_addr;
            proxy_set_header Connection '';
            proxy_http_version 1.1;
            limit_req zone=policy_1_99c0d81f-d277-4cd3-a97b-e94dde53453f_binary_remote_addr nodelay;
            limit_req_status 429;
            auth_jwt bank_idp;
            auth_jwt_key_request /_jwks_uri_bank_idp;
            if ($passes_conditional_policy_policy_1_761 = 0) {
                return 403 'Access denied because payments scope is missing';
            }
            if ($thumbprint_match != 1) {
                return 403 'Access denied because client SSL certificate thumbprint does not match jwt_cnf_fingerprint';
            }
            if ($ssl_client_verify != "SUCCESS") { 
                return 403 'Client SSL cert verification failed'; 
            } 
            proxy_pass http://wl_openbanking;
        }
        location ~* /open-banking/v3.1/pisp/domestic-payments/*/payment-details {
            if ($request_method !~ '^GET|POST$') {
                return 405;
            }
            error_log /dev/null;
            proxy_set_header Host '';
            proxy_set_header Host bank.f5lab;
            proxy_set_header X-Forwarded-For $remote_addr;
            proxy_set_header Connection '';
            proxy_http_version 1.1;
            limit_req zone=policy_1_99c0d81f-d277-4cd3-a97b-e94dde53453f_binary_remote_addr nodelay;
            limit_req_status 429;
            auth_jwt bank_idp;
            auth_jwt_key_request /_jwks_uri_bank_idp;
            if ($passes_conditional_policy_policy_1_761 = 0) {
                return 403 'Access denied because payments scope is missing';
            }
            if ($thumbprint_match != 1) {
                return 403 'Access denied because client SSL certificate thumbprint does not match jwt_cnf_fingerprint';
            }
            if ($ssl_client_verify != "SUCCESS") { 
                return 403 'Client SSL cert verification failed'; 
            } 
            proxy_pass http://wl_openbanking;
        }
        location ~* /open-banking/v3.1/pisp/domestic-payment-consents/*/funds-confirmation {
            if ($request_method !~ '^GET|POST$') {
                return 405;
            }
            error_log /dev/null;
            proxy_set_header Host '';
            proxy_set_header Host bank.f5lab;
            proxy_set_header X-Forwarded-For $remote_addr;
            proxy_set_header Connection '';
            proxy_http_version 1.1;
            limit_req zone=policy_1_99c0d81f-d277-4cd3-a97b-e94dde53453f_binary_remote_addr nodelay;
            limit_req_status 429;
            auth_jwt bank_idp;
            auth_jwt_key_request /_jwks_uri_bank_idp;
            if ($passes_conditional_policy_policy_1_761 = 0) {
                return 403 'Access denied because payments scope is missing';
            }
            if ($thumbprint_match != 1) {
                return 403 'Access denied because client SSL certificate thumbprint does not match jwt_cnf_fingerprint';
            }
            if ($ssl_client_verify != "SUCCESS") { 
                return 403 'Client SSL cert verification failed'; 
            } 
            proxy_pass http://wl_openbanking;
        }
        if ($target) {
            return 403;
        }
    }
    upstream wl_openbanking {
        zone wl_openbanking 512k;
        server 10.0.0.1:30511;
        keepalive 64;
        keepalive_requests 100;
        keepalive_timeout 60s;
    }
}

###################################################################################

##########
#x5t.js
##########

function validate(r) {
    var clientThumbprint = require("crypto")
        .createHash("sha256")
        .update(Buffer.from(r.variables.ssl_client_raw_cert.replace(/(\n|----|-BEGIN|-END| CERTIFICATE-)/gm, ''), 'base64'))
        .digest("base64url");
    return clientThumbprint === r.variables.jwt_cnf_fingerprint ? '1' : '0';
}

export default { validate }

Tested this on version:

16.0
Published Oct 18, 2021
Version 1.0

Was this article helpful?

No CommentsBe the first to comment