This guide covers setting up PKI (Public Key Infrastructure) certificate-based authentication with OpenTranscribe.
PKI authentication allows users to authenticate using X.509 client certificates instead of passwords. This is commonly used in:
- Government and military environments (CAC/PIV cards)
- Enterprise environments with certificate-based security
- High-security deployments requiring mutual TLS
v0.4.0 Change: PKI configuration is now managed via the Super Admin UI (Settings → Authentication → PKI/X.509). Settings are stored encrypted (AES-256-GCM) in the database. Environment variables continue to work as a fallback seed.
OCSP/CRL: v0.4.0 adds OCSP (Online Certificate Status Protocol) and CRL (Certificate Revocation List) checking. Configure via Admin UI for real-time or periodic revocation checking.
Super admin password fallback: Even when PKI is the only enabled auth method, the super admin account can always authenticate with a password. This ensures emergency access and the ability to reconfigure authentication settings if PKI infrastructure fails.
┌─────────────────┐ ┌──────────────────┐ ┌─────────────────┐
│ │ │ │ │ │
│ Client │────▶│ Nginx │────▶│ OpenTranscribe │
│ (with cert) │ │ (mTLS termination) │ Backend │
│ │ │ │ │ │
└─────────────────┘ └──────────────────┘ └─────────────────┘
│ │ │
│ │ │
Client Cert Extract DN/Cert Validate & Auth
Presented Pass via Headers Create User
- Client presents X.509 certificate during TLS handshake
- Nginx validates certificate against CA
- Nginx extracts certificate info and passes via headers
- Backend authenticates user based on certificate DN
- User is created/updated in database
| Method | Complexity | Use Case |
|---|---|---|
| API Testing | Easy | Verify PKI logic works without browser |
| Browser Testing | Medium | Full end-to-end with certificate selection |
| Production | Advanced | Enterprise deployment with real CA |
For quick API testing without setting up HTTPS/mTLS:
# Generate test CA and client certificates
./scripts/pki/setup-test-pki.shThis creates:
- Root CA at
scripts/pki/test-certs/ca/ca.crt - Client certificates for: testuser, admin, john.doe, jane.smith
- PKCS12 files for browser import (password:
changeit)
# .env settings
PKI_ENABLED=true
PKI_CA_CERT_PATH=/mnt/nvm/repos/transcribe-app/scripts/pki/test-certs/ca/ca.crt
PKI_VERIFY_REVOCATION=false
PKI_CERT_HEADER=X-Client-Cert
PKI_CERT_DN_HEADER=X-Client-Cert-DN
PKI_ADMIN_DNS=emailAddress=admin@example.com,CN=Admin User,OU=Users,O=OpenTranscribe Admins,L=Arlington,ST=Virginia,C=US# Using the test script
./scripts/pki/test-pki-auth.sh admin # Gets admin role
./scripts/pki/test-pki-auth.sh testuser # Gets user role
# Or manually with curl (simulates what Nginx does)
# Get the DN from the certificate
ADMIN_DN=$(openssl x509 -in scripts/pki/test-certs/clients/admin.crt -noout -subject | sed 's/subject=//')
# Authenticate
curl -X POST http://localhost:5174/api/auth/pki/authenticate \
-H "Content-Type: application/json" \
-H "X-Client-Cert-DN: $ADMIN_DN"This simulates what Nginx would do in production by passing the X-Client-Cert-DN header.
Note: API testing only verifies the backend PKI logic. For full browser-based testing with certificate selection prompts, see "Browser-Based PKI Testing" below.
For browser-based PKI login (clicking "Sign in with Certificate"), you need HTTPS with mutual TLS.
This is the easiest way to test PKI authentication with a real browser.
./scripts/pki/setup-test-pki.shAdmin UI (recommended — v0.4.0+):
- Log in as super_admin
- Navigate to Settings → Authentication → PKI/X.509
- Enable PKI, set CA Certificate Path, and optionally configure OCSP/CRL
Environment variables (fallback / initial seed):
PKI_ENABLED=true
PKI_CA_CERT_PATH=/app/scripts/pki/test-certs/ca/ca.crt
PKI_VERIFY_REVOCATION=false
PKI_CERT_HEADER=X-Client-Cert
PKI_CERT_DN_HEADER=X-Client-Cert-DN
PKI_ADMIN_DNS=emailAddress=admin@example.com,CN=Admin User,OU=Users,O=OpenTranscribe Admins,L=Arlington,ST=Virginia,C=USIMPORTANT: PKI authentication requires production mode because it needs nginx with mTLS (mutual TLS) to verify client certificates. Dev mode uses Vite dev server which cannot handle client certificate verification.
Recommended Method (using opentr.sh):
# Production mode with PKI (test before push)
./opentr.sh start prod --build --with-pki
# Production mode with PKI (Docker Hub images)
./opentr.sh start prod --with-pkiAdvanced Method (manual docker compose):
# Production mode with PKI (local images)
docker compose -f docker-compose.yml -f docker-compose.prod.yml -f docker-compose.local.yml -f docker-compose.pki.yml up -d --build
# Production mode with PKI (local images)
docker compose -f docker-compose.yml -f docker-compose.prod.yml -f docker-compose.local.yml -f docker-compose.pki.yml up -d --buildImport one of the .p12 files from scripts/pki/test-certs/clients/:
macOS:
# Import to keychain (password: changeit)
security import scripts/pki/test-certs/clients/admin.p12 -k ~/Library/Keychains/login.keychain-db -P changeit -AThen in Keychain Access:
- Find the private key under "Keys"
- Right-click → Get Info → Access Control
- Select "Allow all applications to access this item"
- Save Changes
Windows/Linux Chrome:
- Settings → Privacy and security → Security → Manage certificates
- Import → Select
admin.p12 - Password:
changeit
Firefox:
- Settings → Privacy & Security → Certificates → View Certificates
- Your Certificates → Import
- Select
admin.p12, password:changeit
Open: https://localhost:5182
- Accept the self-signed certificate warning
- Click "Sign in with Certificate"
- Browser will prompt you to select a certificate
- Select the imported certificate
- You'll be authenticated!
| Certificate | Role | |
|---|---|---|
| admin.p12 | admin@example.com | Admin |
| testuser.p12 | testuser@example.com | User |
| john.doe.p12 | john.doe@example.com | User |
| jane.smith.p12 | jane.smith@example.com | User |
Password for all .p12 files: changeit
# Start Step CA for PKI testing
docker compose -f docker-compose.yml -f docker-compose.keycloak.yml --profile pki up -d step-ca
# View CA initialization logs
docker compose -f docker-compose.yml -f docker-compose.keycloak.yml logs step-ca# Get the CA fingerprint
docker exec step-ca step ca health
docker exec step-ca step ca bootstrap --ca-url https://localhost:9000 --fingerprint <fingerprint># Generate a client certificate
docker exec step-ca step ca certificate "user@example.com" user.crt user.key --not-after=8760h
# Copy certificates from container
docker cp step-ca:/home/step/user.crt ./certs/
docker cp step-ca:/home/step/user.key ./certs/
# Export as PKCS#12 for browser import
openssl pkcs12 -export -out user.p12 -inkey certs/user.key -in certs/user.crtCreate or update Nginx configuration for mutual TLS:
# /etc/nginx/conf.d/pki.conf
server {
listen 443 ssl;
server_name localhost;
# Server certificate
ssl_certificate /etc/nginx/certs/server.crt;
ssl_certificate_key /etc/nginx/certs/server.key;
# Client certificate verification
ssl_client_certificate /etc/nginx/certs/ca.crt;
ssl_verify_client optional; # 'required' for mandatory PKI
ssl_verify_depth 2;
# PKI authentication endpoint
location /api/auth/pki {
# Pass certificate info to backend
proxy_set_header X-Client-Cert $ssl_client_escaped_cert;
proxy_set_header X-Client-Cert-Verify $ssl_client_verify;
proxy_set_header X-Client-Cert-DN $ssl_client_s_dn;
proxy_pass http://backend:8080;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
# Other API routes (no PKI required)
location /api/ {
proxy_pass http://backend:8080;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
# Frontend
location / {
proxy_pass http://frontend:5173;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}Add to your .env file:
# PKI/X.509 Configuration
PKI_ENABLED=true
PKI_CA_CERT_PATH=/etc/ssl/certs/ca.crt
PKI_VERIFY_REVOCATION=false
PKI_CERT_HEADER=X-Client-Cert
PKI_CERT_DN_HEADER=X-Client-Cert-DN
PKI_ADMIN_DNS=CN=Admin User,O=OpenTranscribe,C=USChrome/Edge (Windows/Linux):
- Settings → Privacy and security → Security → Manage certificates
- Import → Select user.p12 file
- Enter password:
changeit
Firefox:
- Settings → Privacy & Security → Certificates → View Certificates
- Your Certificates → Import
- Select user.p12 file, password:
changeit
macOS (Safari/Chrome):
- Import via terminal (recommended):
security import admin.p12 -k ~/Library/Keychains/login.keychain-db -P changeit -A - Or double-click the .p12 file to open Keychain Access
- Important: After import, open Keychain Access:
- Find the private key under "Keys"
- Right-click → Get Info → Access Control
- Select "Allow all applications to access this item"
- Click Save Changes
- This prevents the browser from freezing on repeated keychain prompts
- Open OpenTranscribe in your browser
- Click "Sign in with Certificate"
- Browser will prompt to select certificate
- Select your imported certificate
- You'll be authenticated and redirected
For production, use your organization's Certificate Authority. Configure via the Admin UI (Settings → Authentication → PKI/X.509) for encrypted storage:
# .env for production (used only if no database config exists)
PKI_ENABLED=true
PKI_CA_CERT_PATH=/etc/ssl/certs/enterprise-ca.crt
PKI_VERIFY_REVOCATION=true
PKI_CERT_HEADER=X-Client-Cert
PKI_CERT_DN_HEADER=X-Client-Cert-DN
PKI_ADMIN_DNS=CN=Admin1,OU=IT,O=Company,C=US|CN=Admin2,OU=IT,O=Company,C=USOCSP provides real-time certificate revocation status. When a certificate is added to the OCSP responder's revocation list, access is denied on the next login attempt:
| Setting | Description | Example |
|---|---|---|
| Enable OCSP | Turn on OCSP checking | true |
| OCSP Responder URL | Your CA's OCSP endpoint | http://ocsp.pki.company.com |
Configure via Admin UI: Settings → Authentication → PKI/X.509 → Enable OCSP.
CRL downloads and caches a list of revoked certificates, refreshed periodically:
| Setting | Description | Default |
|---|---|---|
| Enable CRL | Turn on CRL checking | false |
| CRL Endpoint URL | CRL distribution point | http://pki.company.com/crl |
| CRL Refresh Hours | How often to refresh the CRL | 24 |
CRL checking is suitable when an OCSP responder is not available. The system caches the CRL locally and refreshes on the configured interval.
OCSP vs CRL:
- OCSP: Real-time check on every login — revocation takes effect immediately
- CRL: Periodic check — revocation takes effect within the refresh interval (up to 24 hours)
- Both can be enabled simultaneously for defence-in-depth
server {
listen 443 ssl;
server_name yourdomain.com;
# Strong TLS configuration
ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256;
ssl_prefer_server_ciphers off;
# Server certificate
ssl_certificate /etc/nginx/certs/server.crt;
ssl_certificate_key /etc/nginx/certs/server.key;
# Client certificate verification
ssl_client_certificate /etc/nginx/certs/ca-bundle.crt;
ssl_verify_client optional_no_ca; # Verify if provided
ssl_verify_depth 4;
# CRL checking (if enabled)
# ssl_crl /etc/nginx/certs/ca.crl;
location /api/auth/pki {
# Only allow if client cert was verified
if ($ssl_client_verify != SUCCESS) {
return 403;
}
proxy_set_header X-Client-Cert $ssl_client_escaped_cert;
proxy_set_header X-Client-Cert-Verify $ssl_client_verify;
proxy_set_header X-Client-Cert-DN $ssl_client_s_dn;
proxy_pass http://backend:8080;
}
}Client certificates should include:
- Common Name (CN): User's full name
- Email Address: User's email (in subject or SAN)
- Key Usage: Digital Signature, Key Encipherment
- Extended Key Usage: TLS Web Client Authentication
Example certificate subject:
CN=John Doe,emailAddress=john.doe@company.com,OU=Engineering,O=Company Inc,C=US
Admins are designated by their certificate Distinguished Name (DN):
# Single admin
PKI_ADMIN_DNS=CN=John Doe,O=Company,C=US
# Multiple admins (comma-separated)
PKI_ADMIN_DNS=CN=John Doe,O=Company,C=US,CN=Jane Smith,O=Company,C=USNote: DN must match exactly as it appears in the certificate.
When a user authenticates via PKI for the first time:
- User is automatically created in the database
- Email is extracted from certificate (or generated from CN if not present)
- Full name is extracted from CN
- Role is set based on PKI_ADMIN_DNS match
For DoD CAC or PIV card authentication:
- Ensure card reader drivers are installed
- Browser must have PKCS#11 module configured
- Insert smart card before navigating to login page
- Select certificate when prompted
# Install OpenSC
sudo apt install opensc opensc-pkcs11
# Configure Chrome to use PKCS#11
# Settings → Privacy → Security → Manage certificates → Security Devices
# Add: /usr/lib/x86_64-linux-gnu/opensc-pkcs11.so# Settings → Privacy & Security → Certificates → Security Devices
# Load module: /usr/lib/x86_64-linux-gnu/opensc-pkcs11.so"PKI authentication is not enabled"
- If using Admin UI: verify PKI is enabled in Settings → Authentication → PKI/X.509
- If using .env: ensure
PKI_ENABLED=trueand restart the backend - Database config takes precedence — an explicit
enabled=falsein the database overrides .env
"Invalid or missing client certificate"
- Verify certificate is imported in browser
- Check Nginx is passing headers correctly
- Verify certificate is not expired
"Certificate not accepted"
- Verify CA certificate matches issuer
- Check certificate validity dates
- Ensure certificate has correct key usage
Check Nginx logs for certificate verification:
# Nginx error log
tail -f /var/log/nginx/error.log
# Look for ssl_client_verify statusCheck backend logs:
./opentr.sh logs backend# Verify certificate against CA
openssl verify -CAfile ca.crt user.crt
# View certificate details
openssl x509 -in user.crt -text -noout
# Check certificate DN format
openssl x509 -in user.crt -subject -noout- CA Security: Protect your CA private key — compromise of the CA allows issuing fraudulent certificates
- Certificate Revocation: Enable OCSP or CRL checking in production to enforce immediate certificate revocation
- Certificate Lifecycle: Plan for certificate renewal before expiry; expired certificates will fail authentication
- Key Storage: Use hardware tokens (CAC, PIV, YubiKey) for high-security environments
- DN Validation: DN matching is case-sensitive and exact — copy DN strings directly from certificate details
- Super Admin Fallback: Ensure the super admin password is documented in a secure location; it is the only non-PKI access path when PKI-only mode is active
- mTLS Requirement: PKI authentication requires NGINX with mTLS (
--with-pkiflag); dev mode cannot use PKI
Enable via Admin UI (Settings → Authentication → PKI/X.509 → Enable OCSP) or environment variable:
PKI_OCSP_ENABLED=true
PKI_OCSP_RESPONDER_URL=http://ocsp.your-ca.comEnable via Admin UI or environment variable:
PKI_CRL_ENABLED=true
PKI_CRL_URL=http://pki.your-ca.com/crl
PKI_CRL_REFRESH_HOURS=24Nginx CRL configuration (optional — additional layer at the TLS termination point):
ssl_crl /etc/nginx/certs/ca.crl;Generate CRL for testing:
# Using Step CA
step ca crl > ca.crl
# Using OpenSSL
openssl ca -gencrl -out ca.crl -config openssl.cnf