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é.
// 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 initContainers3. 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.
Sidecar
Kubernetes, multi-langage, isolation forte par service.
API Gateway
Les services backend ne doivent PAS accéder aux données PII, ou pour une migration sans modifier les services.