Image de couverture : SOC 2 & Symfony : pourquoi le RBAC seul ne suffit plus pour sécuriser vos données
tech

SOC 2 & Symfony : pourquoi le RBAC seul ne suffit plus pour sécuriser vos données

07 June 2026
6 min de lecture
3 vues
Sébastien Muler

SOC 2 & Symfony : pourquoi le RBAC seul ne suffit plus pour sécuriser vos données

Vos clients grands comptes posent de plus en plus souvent la question : « Comment prouvez-vous que vos logs d'audit ne peuvent pas être falsifiés ? » ou « Que se passe-t-il si une clé de chiffrement est compromise ? ». La gestion des droits par rôles (RBAC) est un prérequis, mais elle ne répond pas à ces exigences. Le référentiel SOC 2 va bien au-delà : il impose une traçabilité immuable et un cycle de vie rigoureux pour les clés cryptographiques.

Cet article s'appuie sur les travaux de Matt Mochalkin publiés sur DEV Community pour illustrer, avec Symfony et PHP, deux patterns d'architecture avancés : l'isolation de la piste d'audit via l'Outbox Pattern et la gestion distribuée des clés maîtresses par fragmentation (Key Sharding).


Les deux lacunes du RBAC face à SOC 2

Le contrôle d'accès basé sur les rôles garantit qui peut faire quoi dans l'application. C'est nécessaire, mais SOC 2 impose deux exigences supplémentaires que le RBAC ne couvre pas :

1. L'intégrité des logs (Audit Trail) Vous devez prouver que les journaux de sécurité sont immuables, y compris si votre base de données principale est compromise. Un administrateur disposant d'un accès ROLE_ADMIN ne doit pas pouvoir effacer ses propres traces.

2. Le cycle de vie des clés de chiffrement Vous devez démontrer que vos clés sont régulièrement tournées et que la compromission d'une clé unique n'entraîne pas la compromission de l'ensemble du système. C'est le principe du blast radius limité.

Ces deux points nécessitent une réponse architecturale, pas seulement une configuration de permissions.


L'architecture trois bases de données

La première décision structurante est de sortir du monolithe de données. Plutôt qu'une base unique, on sépare physiquement les responsabilités en trois bases SQLite (ou tout autre moteur en production) :

Base Rôle Contrainte
data.db Utilisateurs, documents chiffrés, enveloppes RSA Lecture/écriture standard
audit.db Entrées de log immuables Idéalement Append-Only en production
keys.db Pool de clés maîtresses chiffrées Accès restreint, rotation planifiée

Dans Symfony, cela se traduit par la configuration de trois Entity Managers distincts dans doctrine.yaml. Chaque manager pointe vers sa propre connexion, ce qui rend impossible — par construction — qu'une requête sur data.db affecte audit.db.

# config/packages/doctrine.yaml
doctrine:
    dbal:
        connections:
            default:  { url: '%env(DATABASE_URL)%' }
            audit:    { url: '%env(AUDIT_DATABASE_URL)%' }
            keys:     { url: '%env(KEYS_DATABASE_URL)%' }
    orm:
        entity_managers:
            default: { connection: default }
            audit:   { connection: audit }
            keys:    { connection: keys }

Cette séparation physique est un argument auditable concret lors d'un passage en revue SOC 2.


L'Outbox Pattern pour une piste d'audit fiable

Écrire directement dans audit.db depuis le code métier pose un problème : si la transaction principale échoue à mi-chemin, le log peut être écrit sans que l'action associée ne soit réellement enregistrée — ou inversement. On obtient une piste d'audit incohérente.

L'Outbox Pattern résout ce problème en deux temps :

  1. Dans la même transaction que l'action métier, on insère un événement dans une table outbox de data.db.
  2. Un worker asynchrone (Symfony Messenger, par exemple) lit cet outbox et reporte l'entrée dans audit.db de façon définitive.
// Dans votre service métier
$this->entityManager->beginTransaction();
try {
    // 1. Action métier
    $document->setContent($encryptedContent);
    $this->entityManager->persist($document);

    // 2. Événement outbox dans la même transaction
    $outboxEvent = new OutboxEvent(
        type: 'document.updated',
        payload: ['document_id' => $document->getId(), 'user' => $userId]
    );
    $this->entityManager->persist($outboxEvent);
    $this->entityManager->flush();
    $this->entityManager->commit();
} catch (\Throwable $e) {
    $this->entityManager->rollback();
    throw $e;
}

Le worker qui consomme l'outbox écrit dans audit.db avec l'Entity Manager dédié. Le résultat : aucun log orphelin, aucun log manquant. La piste d'audit reflète exactement ce qui s'est passé dans le système.


Key Sharding : limiter le blast radius

L'envelope encryption (vue en Partie 1) protège chaque document avec une clé AES propre, elle-même chiffrée par une clé RSA utilisateur. Mais si la clé maîtresse RSA est compromise, tous les documents de cet utilisateur sont exposés.

Le Key Sharding va plus loin : la clé maîtresse est fragmentée en N parts (par exemple 3), et la reconstruction nécessite un seuil K de ces parts (par exemple 2 sur 3). C'est le principe de Shamir's Secret Sharing.

Concrètement dans keys.db :

  • Chaque fragment est stocké chiffré séparément.
  • Ils peuvent être distribués sur des services ou des HSM différents.
  • La compromission d'un seul fragment ne révèle rien.

Pour la rotation des clés, Symfony Messenger permet de planifier un job périodique :

// Message
class RotateMasterKeyMessage
{
    public function __construct(public readonly string $keyId) {}
}

// Handler
class RotateMasterKeyHandler implements MessageHandlerInterface
{
    public function __invoke(RotateMasterKeyMessage $message): void
    {
        $newKey = $this->kmsService->generateAndShardNewKey();
        $this->kmsService->reEncryptEnvelopes($message->keyId, $newKey);
        $this->kmsService->deactivateKey($message->keyId);
    }
}

La rotation devient un processus auditable, planifiable et réversible — exactement ce qu'un auditeur SOC 2 veut voir.


Ce que cela change concrètement pour vos clients grands comptes

Ces patterns peuvent sembler sur-ingénierés pour un projet standard. Mais dès que vous adressez des clients du secteur financier, de la santé ou des grandes entreprises, les questions suivantes deviennent bloquantes dans les appels d'offres :

  • « Vos logs peuvent-ils être modifiés par un administrateur de la base ? » → L'isolation physique de audit.db répond non.
  • « Que se passe-t-il si un développeur exfiltre votre base de données ? » → Le Key Sharding garantit que les données restent inutilisables sans les fragments de clés.
  • « Comment prouvez-vous la rotation de vos clés ? » → Le worker de rotation crée lui-même des entrées dans audit.db.

L'architecture devient un argument commercial différenciant, pas seulement une contrainte technique.


Conclusion

Le RBAC reste fondamental, mais il ne constitue que la première couche d'une posture de sécurité SOC 2. L'isolation physique des bases de données, l'Outbox Pattern pour la traçabilité et le Key Sharding pour la résilience cryptographique forment ensemble une architecture défendable face à un audit sérieux.

Symfony offre les outils natifs pour implémenter ces patterns sans sur-complexifier le code : plusieurs Entity Managers, Messenger pour l'asynchrone, et une configuration d'infrastructure claire. La vraie valeur ajoutée, c'est de penser l'architecture comme une preuve — chaque décision doit pouvoir être expliquée et vérifiée.

📖 Article source : The SOC 2 Blueprint: Beyond RBAC with AppLevel Infrastructure Isolation & Key Sharding — Part 2 par Matt Mochalkin sur DEV Community.

Partager cet article