Enforcing Certificate-User Binding in OpenVPN: Preventing Shared Credentials with LDAP Authentication


2 views

When using OpenVPN with certificate-based authentication combined with LDAP, we often encounter a security gap: valid LDAP users can share certificates among themselves. While the server validates both the certificate and LDAP credentials separately, there's no inherent binding between a specific certificate and its designated LDAP user.

Here are three effective approaches to enforce certificate-user binding:

1. Using tls-verify Scripts

Create a custom script that checks if the certificate's Common Name (CN) matches the authenticated LDAP username:

#!/bin/bash
# /etc/openvpn/verify-user.sh

LDAP_USER="$common_name"
CERT_CN=$(openssl x509 -in "$1" -noout -subject | sed 's/.*CN=//')

if [ "$LDAP_USER" != "$CERT_CN" ]; then
    exit 1
fi
exit 0

Add this to your OpenVPN server config:

tls-verify "/etc/openvpn/verify-user.sh"

2. Leveraging TLS Crypt V2

OpenVPN 2.5+ supports TLS Crypt V2 which includes client-specific keys. Combine this with certificate authentication:

tls-crypt-v2 /etc/openvpn/server/tls-crypt-v2.key

Generate per-client keys and distribute them securely.

3. Custom Certificate Fields

Extend certificates with custom fields containing the LDAP username:

openssl req -new -key bob.key -out bob.csr -subj "/CN=bob/OU=VPN/O=Company/emailAddress=bob@company.com/UID=bob"

Then modify your verification script to check this field.

For better security with LDAP:

plugin /usr/lib/openvpn/openvpn-auth-ldap.so "/etc/openvpn/auth/ldap.conf"

Sample ldap.conf snippet:

<LDAP>
    URL ldap://ldap.example.com
    BindDN cn=admin,dc=example,dc=com
    Password secret
    Timeout 15
    TLSEnable no
</LDAP>

<Authorization>
    BaseDN "ou=users,dc=example,dc=com"
    SearchFilter "(&(uid=%u)(objectClass=posixAccount))"
    RequireGroup false
</Authorization>
  • Set appropriate key usage and extended key usage in certificates
  • Implement certificate revocation (CRL or OCSP)
  • Use short certificate lifetimes (30-90 days)
  • Combine with multi-factor authentication where possible

When implementing OpenVPN with certificate-based authentication alongside LDAP, we face a critical security gap: certificates aren't inherently bound to specific LDAP users. This creates a vulnerability where any valid LDAP user can authenticate using any valid certificate.

The standard OpenVPN configuration with auth-user-pass-verify only verifies that:

  1. The certificate is valid
  2. The LDAP credentials are valid

There's no built-in mechanism to verify that the certificate belongs to the LDAP user attempting to authenticate.

We'll solve this by modifying the OpenVPN server configuration to enforce certificate-to-user binding:

# In server.conf
script-security 2
auth-user-pass-verify "/etc/openvpn/scripts/verify_user_cert_binding.py" via-file

Here's the Python verification script:

#!/usr/bin/env python3
import os
import sys
from OpenSSL import crypto

# Path to user certificate mapping file
USER_CERT_MAP = "/etc/openvpn/user_cert_map.csv"

def load_credentials():
    with open(sys.argv[1], 'r') as f:
        return f.read().splitlines()

def extract_cn(cert_path):
    with open(cert_path, 'r') as f:
        cert = crypto.load_certificate(crypto.FILETYPE_PEM, f.read())
    subject = cert.get_subject()
    return subject.CN

def verify_binding(username, cert_cn):
    with open(USER_CERT_MAP, 'r') as f:
        for line in f:
            stored_user, stored_cn = line.strip().split(',')
            if stored_user == username and stored_cn == cert_cn:
                return True
    return False

if __name__ == "__main__":
    username, password = load_credentials()
    cert_cn = extract_cn(os.environ['tls_serial_0'])
    
    if verify_binding(username, cert_cn):
        sys.exit(0)
    else:
        print(f"Certificate {cert_cn} not authorized for user {username}")
        sys.exit(1)

For this system to work, you need to implement a strict certificate issuance process:

1. When creating a certificate for user 'bob':
openssl req -new -newkey rsa:2048 -nodes -keyout bob.key -out bob.csr -subj "/CN=bob_cert_serial_12345"

2. Add to user_cert_map.csv:
bob,bob_cert_serial_12345

For additional security, consider implementing:

# In server.conf
# Prevent certificate reuse
explicit-exit-notify 1
auth-gen-token
tls-crypt v2.key

This combination provides certificate binding while maintaining the flexibility of LDAP authentication, creating a robust two-factor authentication system where both factors must match.