When implementing client certificate authentication in Nginx for specific endpoints while keeping other routes open, many developers encounter unexpected browser behavior. The core issue lies in how browsers handle SSL/TLS negotiation when client certificates are involved.
Most browsers will prompt for client certificates when:
- The server requests client authentication (even with ssl_verify_client set to optional)
- The CA list is presented during SSL handshake
- The browser has matching certificates in its store
Here's a more elegant approach using server blocks and smart SSL configuration:
# Main server block for non-client-cert routes
server {
listen 443 ssl;
server_name example.com;
ssl_certificate /etc/nginx/server.crt;
ssl_certificate_key /etc/nginx/server.key;
location / {
proxy_pass http://backend;
}
}
# Dedicated server block for client-cert protected endpoint
server {
listen 443 ssl;
server_name example.com;
ssl_certificate /etc/nginx/server.crt;
ssl_certificate_key /etc/nginx/server.key;
ssl_client_certificate /etc/nginx/client-ca.crt;
ssl_verify_client on; # Changed from optional to required
location /jsonrpc {
if ($ssl_client_verify != SUCCESS) {
return 403;
}
proxy_pass http://localhost:8282/jsonrpc-api;
proxy_set_header X-SSL-Client-Verify $ssl_client_verify;
proxy_set_header X-SSL-Client-DN $ssl_client_s_dn;
}
}
For cases where separate server blocks aren't feasible:
map $uri $needs_client_cert {
~^/jsonrpc 1;
default 0;
}
server {
listen 443 ssl;
ssl_certificate /etc/nginx/server.crt;
ssl_certificate_key /etc/nginx/server.key;
ssl_client_certificate /etc/nginx/client-ca.crt;
ssl_verify_client optional_no_ca;
location / {
if ($needs_client_cert) {
set $ssl_verify_required on;
}
if ($ssl_verify_required = on) {
set $auth_pass $ssl_client_verify;
}
if ($auth_pass != SUCCESS) {
return 403;
}
proxy_pass http://backend;
}
}
Different browsers handle client certificates differently:
- Chrome/Chromium: Will only prompt if site has previously requested cert
- Safari: More aggressive in prompting when CA list is present
- Firefox: Respects certificate store organization better
The two-server-block approach:
- Adds minimal overhead (additional SSL context)
- Eliminates unnecessary SSL negotiations for regular traffic
- Provides cleaner separation of security requirements
When implementing client certificate authentication in Nginx, a common requirement is to enforce it only for specific endpoints while keeping other routes accessible without certificates. The standard approach using ssl_verify_client optional
with location-based checks works technically, but causes unwanted browser behavior.
The root issue lies in how browsers handle the SSL handshake. When they detect that a server might request a client certificate (due to ssl_verify_client optional
), some browsers like Safari and Chrome-on-Android will proactively prompt the user regardless of the actual requested path.
Here's an improved setup that avoids unwanted browser prompts while maintaining security:
server {
listen 443 ssl;
# Base SSL configuration
ssl_certificate /etc/nginx/server.crt;
ssl_certificate_key /etc/nginx/server.key;
# Default - no client cert verification
ssl_verify_client off;
location /jsonrpc {
# Enable client cert verification just for this location
ssl_client_certificate /etc/nginx/client-ca.crt;
ssl_verify_client on;
if ($ssl_client_verify != SUCCESS) {
return 403;
}
proxy_pass http://localhost:8282/jsonrpc-api;
proxy_read_timeout 90;
}
# Other locations inherit the default 'off' setting
location / {
proxy_pass http://localhost:8080;
}
}
- Set
ssl_verify_client off
at the server level as default - Override it to
on
only in the specific location block - Include
ssl_client_certificate
directive inside the location block - The CA certificate is only loaded when needed
Verify the behavior with these curl commands:
# Should work without client cert
curl https://yoursite.com/
# Should require client cert
curl https://yoursite.com/jsonrpc --cert client.crt --key client.key
For more complex scenarios, consider separating the endpoints into different server blocks:
# Non-cert protected server
server {
listen 443 ssl;
server_name yoursite.com;
ssl_certificate /etc/nginx/server.crt;
ssl_certificate_key /etc/nginx/server.key;
location / {
proxy_pass http://localhost:8080;
}
}
# Cert-protected server
server {
listen 443 ssl;
server_name secure.yoursite.com;
ssl_certificate /etc/nginx/server.crt;
ssl_certificate_key /etc/nginx/server.key;
ssl_client_certificate /etc/nginx/client-ca.crt;
ssl_verify_client on;
location / {
proxy_pass http://localhost:8282/jsonrpc-api;
}
}
This completely separates the certificate requirements at the DNS level.
While this solution works for most modern browsers, be aware that:
- Some corporate environments may have custom CA configurations
- Mobile browsers might still cache certificate decisions
- Always test with your target user base's common browsers