Guide pratique

Architecture micro-services

Trois patterns d'intégration pour les architectures distribuées : service crypto centralisé, sidecar container, et API gateway. Plus la gestion du chiffrement dans les événements Kafka/RabbitMQ.

1. Service crypto centralisé

Un service dédié encapsule toute la logique de chiffrement et expose une API REST ou gRPC interne. Les autres services appellent ce service plutôt que l'API MAFATE directement. Avantages : une seule clé API MAFATE, logs centralisés, circuit breaker centralisé.

[User Service]     ──▶ [Crypto Service] ──▶ [MAFATE API]
[Payment Service] ──▶ [Crypto Service] ──▶ [MAFATE API]
[Analytics]      ──▶ [Crypto Service] ──▶ [MAFATE API]
// crypto-service/src/server.ts : Service crypto centralisé (REST)
import express from 'express'
import { Mafate } from '@mafate/sdk'

const app = express()
const mafate = new Mafate({ apiKey: process.env.MAFATE_API_KEY! })

app.use(express.json())

// Middleware d'authentification inter-services
app.use((req, res, next) => {
  const token = req.headers['x-service-token']
  if (token !== process.env.INTER_SERVICE_SECRET) {
    return res.status(401).json({ error: 'Unauthorized' })
  }
  next()
})

// POST /encrypt
app.post('/encrypt', async (req, res) => {
  const { plaintext, keyId } = req.body
  if (!plaintext || !keyId) {
    return res.status(400).json({ error: 'plaintext et keyId requis' })
  }
  const enc = await mafate.encryptLocal(plaintext, keyId)
  res.json(enc)
})

// POST /decrypt
// ⛔ L'ENVELOPPE EST RELAYEE ENTIERE, ET SOUS SES NOMS EXACTS.
// `keyId` / `keyVersion` n'existent pas : les champs sont en snake_case,
// et il en faut SIX. En omettre un rend la donnee indechiffrable, sans erreur
// visible au moment de l'ecriture.
app.post('/decrypt', async (req, res) => {
  const { ciphertext, wrapped_key, iv, key_id, key_version, envelope_version } = req.body
  const dec = await mafate.decryptLocalToString({
    ciphertext, wrapped_key, iv, key_id, key_version, envelope_version,
  })
  res.json({ plaintext: dec })
})

// POST /encrypt-batch : chiffrer plusieurs champs en un appel
app.post('/encrypt-batch', async (req, res) => {
  const { items } = req.body // [{ plaintext, keyId }]
  const results = await Promise.all(
    items.map(({ plaintext, keyId }) => mafate.encryptLocal(plaintext, keyId))
  )
  res.json({ results })
})

app.listen(9090, () => console.log('Crypto service démarré sur :9090'))

// user-service/src/crypto-client.ts : Client vers le service crypto
import axios from 'axios'

const cryptoClient = axios.create({
  baseURL: process.env.CRYPTO_SERVICE_URL, // http://crypto-svc:9090
  headers: { 'X-Service-Token': process.env.INTER_SERVICE_SECRET },
  timeout: 5000,
})

export async function encrypt(plaintext: string, keyId: string) {
  const { data } = await cryptoClient.post('/encrypt', { plaintext, keyId })
  return data
}

export async function decrypt(enc: object) {
  const { data } = await cryptoClient.post('/decrypt', enc)
  return data.plaintext
}

2. Pattern Sidecar

Un container sidecar tourne aux côtés de chaque service applicatif et expose le chiffrement sur localhost. L'application appelle http://localhost:9090, sans dépendance directe vers l'API MAFATE. Idéal pour Kubernetes.

# docker-compose.yml : Architecture sidecar
version: '3.9'

services:
  user-service:
    image: my-app/user-service:latest
    environment:
      # L'application ne parle PAS directement à l'API MAFATE
      # Elle appelle le sidecar sur localhost:9090
      - CRYPTO_SIDECAR_URL=http://localhost:9090
      - DATABASE_URL=postgres://db:5432/users
    depends_on:
      - db

  crypto-sidecar:
    image: mafate/sidecar:latest
    environment:
      - MAFATE_API_KEY=eaas_prod_xxxxxxxx
      - ALLOWED_KEY_IDS=key-users-pii,key-users-auth
      - LOG_LEVEL=info
    # Partager le réseau avec user-service → localhost:9090
    network_mode: "service:user-service"

  payment-service:
    image: my-app/payment-service:latest
    environment:
      - CRYPTO_SIDECAR_URL=http://localhost:9090
    depends_on: [db]

  payment-crypto-sidecar:
    image: mafate/sidecar:latest
    environment:
      - MAFATE_API_KEY=eaas_prod_yyyyyyyy      # clé API différente !
      - ALLOWED_KEY_IDS=key-financial           # clés différentes !
    network_mode: "service:payment-service"

  db:
    image: postgres:16
    environment:
      - POSTGRES_DB=myapp
      - POSTGRES_PASSWORD=secret

# Kubernetes : voir la doc pour le déploiement sidecar avec initContainers

3. Pattern API Gateway

Chiffrer et déchiffrer au niveau de la gateway : les services backend ne voient jamais les données en clair et n'ont pas besoin de connaître MAFATE. Compatible Kong, Nginx+Lua, AWS API Gateway.

# Kong API Gateway : plugin de chiffrement MAFATE
# kong.yml

services:
  - name: user-service
    url: http://user-service:3000
    routes:
      - name: user-routes
        paths: ['/api/users']
    plugins:
      - name: mafate-encrypt
        config:
          # Chiffrer ces champs dans le body des requêtes POST/PUT
          request_fields:
            - path: $.email
              key_id: key-users-pii
            - path: $.phone
              key_id: key-users-pii
            - path: $.firstName
              key_id: key-users-pii
          # Déchiffrer ces champs dans les réponses
          response_fields:
            - path: $.email
            - path: $.phone
            - path: $.firstName
          mafate_api_key:
            value_from: env
            env_var: MAFATE_API_KEY

# Nginx + Lua : chiffrement au niveau de la gateway
# nginx.conf
server {
    listen 80;

    location /api/users {
        # Appel au module Lua qui chiffre le body avant de proxifier
        access_by_lua_block {
            local mafate = require('mafate')
            local body = ngx.req.get_body_data()
            local encrypted = mafate.encrypt_json_fields(body, {
                email = 'key-users-pii',
                phone = 'key-users-pii',
            })
            ngx.req.set_body_data(encrypted)
        }
        proxy_pass http://user-service:3000;
    }
}

4. Architecture événementielle (Kafka / RabbitMQ)

Les événements publiés dans Kafka ou RabbitMQ peuvent contenir des données PII. Chiffrez-les avant publication : un consommateur compromis ne verra que des ciphertexts illisibles sans accès à MAFATE.

import { Kafka } from 'kafkajs'
import { Mafate } from '@mafate/sdk'

const mafate = new Mafate({ apiKey: process.env.MAFATE_API_KEY! })
const kafka = new Kafka({ brokers: ['kafka:9092'] })

// PRODUCTEUR : chiffrer avant de publier dans Kafka
const producer = kafka.producer()
await producer.connect()

async function publishUserCreated(user: {
  id: string; email: string; name: string; phone: string
}) {
  // Chiffrer tous les champs PII avant de publier
  const [encEmail, encName, encPhone] = await Promise.all([
    mafate.encryptLocal(user.email, 'key-events-pii'),
    mafate.encryptLocal(user.name,  'key-events-pii'),
    mafate.encryptLocal(user.phone, 'key-events-pii'),
  ])

  const event = {
    type: 'user.created',
    version: '1',
    timestamp: new Date().toISOString(),
    data: {
      id:    user.id,         // non chiffré, identifiant technique
      email: encEmail,        // chiffré
      name:  encName,         // chiffré
      phone: encPhone,        // chiffré
    },
  }

  await producer.send({
    topic: 'user-events',
    messages: [{ key: user.id, value: JSON.stringify(event) }],
  })
  console.log(`Événement user.created publié pour ${user.id}`)
}

// CONSOMMATEUR : déchiffrer après avoir consommé
const consumer = kafka.consumer({ groupId: 'analytics-service' })
await consumer.connect()
await consumer.subscribe({ topic: 'user-events' })

await consumer.run({
  eachMessage: async ({ message }) => {
    const event = JSON.parse(message.value!.toString())
    if (event.type !== 'user.created') return

    // Déchiffrer les champs PII nécessaires au traitement
    const [email, name, phone] = await Promise.all([
      mafate.decryptLocalToString(event.data.email),
      mafate.decryptLocalToString(event.data.name),
      mafate.decryptLocalToString(event.data.phone),
    ])

    // Traitement avec les données en clair
    await analyticsService.trackNewUser({
      id: event.data.id,
      email, name, phone,
    })
  },
})

Quel pattern choisir ?

Service centralisé

Équipe mono-langage, besoin de centraliser les logs et métriques, budget API key unique.

+ Un seul point de configuration- Point de défaillance unique, nécessite haute disponibilité

Sidecar

Kubernetes, multi-langage, isolation forte par service.

+ Latence minimale (localhost), isolation totale- Une clé API MAFATE par service

API Gateway

Les services backend ne doivent PAS accéder aux données PII, ou pour une migration sans modifier les services.

+ Zéro modification des services backend- Gateway devient critique, complexité de configuration