kyzegs / mtls-bundle
A reusable Symfony bundle for mTLS (mutual TLS) client-certificate authentication.
Requires
- php: ^8.2
- symfony/config: ^6.4|^7.0|^8.0
- symfony/dependency-injection: ^6.4|^7.0|^8.0
- symfony/http-foundation: ^6.4|^7.0|^8.0
- symfony/http-kernel: ^6.4|^7.0|^8.0
- symfony/security-http: ^6.4|^7.0|^8.0
Requires (Dev)
- phpunit/phpunit: ^11.0
- symfony/framework-bundle: ^6.4|^7.0|^8.0
- symfony/security-bundle: ^6.4|^7.0|^8.0
This package is auto-updated.
Last update: 2026-04-13 18:46:25 UTC
README
A reusable Symfony bundle for mTLS (mutual TLS) client certificate authentication.
mTLS validation happens at the transport layer (Nginx / Caddy / Apache / Envoy). This bundle lives at the application layer: it reads the certificate the web server already validated, parses it, maps it to a Symfony security user, and integrates with Symfony's firewall, passport, and access-control system.
Requirements
- PHP 8.2+
- Symfony 6.4 or 7.x
Installation
composer require kyzegs/mtls-bundle
If you are not using Symfony Flex, register the bundle manually:
// config/bundles.php return [ Kyzegs\MTLSBundle\MTLSBundle::class => ['all' => true], ];
Web server configuration
The bundle expects your reverse proxy to forward the client certificate and verification status as server variables (or headers converted to server vars by your framework).
Nginx example
ssl_client_certificate /etc/ssl/ca.crt; ssl_verify_client on; location / { proxy_pass http://127.0.0.1:8000; proxy_set_header SSL_CLIENT_CERT $ssl_client_escaped_cert; proxy_set_header SSL_CLIENT_VERIFY $ssl_client_verify; }
Caddy example
https://api.example.com { tls /certs/server.crt /certs/server.key { client_auth { mode require_and_verify trust_pool file /certs/ca.crt } } reverse_proxy 127.0.0.1:8000 { header_up SSL_CLIENT_CERT {http.request.tls.client.certificate_pem} header_up SSL_CLIENT_VERIFY SUCCESS } }
Configuration
# config/packages/mtls.yaml mtls: enabled: true certificate_server_key: 'SSL_CLIENT_CERT' # server var carrying the PEM verify_server_key: 'SSL_CLIENT_VERIFY' # server var carrying the verify status allowed_verify_values: ['SUCCESS'] # accepted verify statuses request_attribute_certificate: '_mtls_certificate' # request attribute key for the ParsedCertificate
Security configuration
# config/packages/security.yaml security: providers: mtls_memory: memory: ~ firewalls: dev: pattern: ^/(_(profiler|wdt)|css|images|js)/ security: false api: pattern: ^/api stateless: true custom_authenticators: - Kyzegs\MTLSBundle\Security\Authenticator\MTLSAuthenticator access_control: - { path: ^/api, roles: ROLE_MTLS }
Custom user resolver
The bundle ships with DefaultCertificateUserResolver, which maps the certificate's
Common Name (CN) — or serial number as fallback — to an MTLSUser.
To plug in your own identity mapping, implement CertificateUserResolverInterface
and alias it in your services configuration:
// src/Security/MyCustomUserResolver.php namespace App\Security; use Kyzegs\MTLSBundle\Contract\CertificateUserResolverInterface; use Kyzegs\MTLSBundle\ValueObject\ParsedCertificate; use Symfony\Component\Security\Core\User\UserInterface; final class MyCustomUserResolver implements CertificateUserResolverInterface { public function resolve(ParsedCertificate $certificate): ?UserInterface { // Your domain logic here — e.g. look up participant by SAN URI $participantId = $certificate->subjectAltName('URI')[0] ?? null; // ... } }
# config/services.yaml services: Kyzegs\MTLSBundle\Contract\CertificateUserResolverInterface: alias: App\Security\MyCustomUserResolver
Multiple firewalls / multiple resolvers
When you need different certificate resolvers for different API endpoints, define multiple named authenticator services — each wired to a different resolver — and assign them to separate Symfony firewalls:
# config/services.yaml services: App\Security\PartnerCertificateUserResolver: ~ mtls.authenticator.partner: class: Kyzegs\MTLSBundle\Security\Authenticator\MTLSAuthenticator arguments: $enabled: '%mtls.enabled%' $extractor: '@Kyzegs\MTLSBundle\Service\CertificateExtractor' $parser: '@Kyzegs\MTLSBundle\Service\CertificateParser' $validator: '@Kyzegs\MTLSBundle\Service\CertificateValidator' $userResolver: '@App\Security\PartnerCertificateUserResolver' $requestAttributeCertificate: '%mtls.request_attribute_certificate%'
# config/packages/security.yaml security: firewalls: api_partner: pattern: ^/api/partner stateless: true custom_authenticators: - mtls.authenticator.partner
Accessing the certificate in a controller
After successful authentication the parsed certificate is available as a request attribute:
use Kyzegs\MTLSBundle\ValueObject\ParsedCertificate; #[Route('/api/profile')] public function profile(Request $request): Response { /** @var ParsedCertificate $cert */ $cert = $request->attributes->get('_mtls_certificate'); return new JsonResponse([ 'cn' => $cert->commonName, 'serial' => $cert->serialNumber, 'san' => $cert->subjectAltName('URI'), ]); }
Architecture
Web server (Nginx / Caddy)
└─ validates client cert via TLS (mTLS)
└─ forwards SSL_CLIENT_CERT + SSL_CLIENT_VERIFY to Symfony
MTLSAuthenticator::supports()
└─ checks bundle is enabled + SSL_CLIENT_CERT header present
MTLSAuthenticator::authenticate()
├─ CertificateExtractor — reads PEM + verify status from $request->server
├─ CertificateValidator — asserts verify status is in allowed_verify_values
├─ CertificateParser — openssl_x509_parse() → ParsedCertificate
├─ CertificateUserResolverInterface::resolve() → UserInterface
└─ Returns SelfValidatingPassport with CertificateBadge
Local development / testing
See the ChatGPT design conversation for a full Caddy + OpenSSL local mTLS setup. Quick summary:
# 1. Generate local CA openssl genrsa -out ca.key 4096 openssl req -x509 -new -nodes -key ca.key -sha256 -days 3650 -out ca.crt \ -subj "/CN=Local Dev CA" # 2. Generate client certificate openssl genrsa -out client.key 2048 openssl req -new -key client.key -out client.csr -subj "/CN=test-participant" openssl x509 -req -in client.csr -CA ca.crt -CAkey ca.key \ -CAcreateserial -out client.crt -days 825 -sha256 # 3. Call your endpoint curl --cacert ca.crt --cert client.crt --key client.key \ https://localhost:8443/api/profile
License
MIT