kyzegs/mtls-bundle

A reusable Symfony bundle for mTLS (mutual TLS) client-certificate authentication.

Maintainers

Package info

github.com/Kyzegs/mtls-bundle

Type:symfony-bundle

pkg:composer/kyzegs/mtls-bundle

Statistics

Installs: 0

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

dev-main 2026-04-13 14:31 UTC

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