Un backend Fastify reçoit des positions GPS depuis Traccar via le forwarder JSON intégré. Le backend est exposé en HTTPS public (Coolify/Traefik) — donc l’endpoint /webhook/position aussi. Sans auth, n’importe qui peut POSTer de fausses positions et déclencher des alertes côté métier.
Contrainte : la conf Traccar (forward.header) ne sait poser qu’un header statique — pas de signature HMAC dynamique sur le corps, pas de header calculé à partir du payload. Donc le pattern « X-Signature: sha256=hex(hmac(secret, body)) » n’est pas réalisable côté émetteur.
Pattern retenu : secret partagé dans l’URL
Pas un header, un segment de chemin :
POST /webhook/position/<TOKEN_32_OCTETS_B64URL>
Le token est un secret aléatoire généré une fois (crypto.randomBytes(32).toString('base64url')), connu de Traccar et du backend. Le routeur Fastify le capture comme paramètre de route et le compare en timing-safe au secret côté serveur.
import crypto from 'node:crypto';
function tokenMatches(provided) {
const expected = process.env.WEBHOOK_TOKEN;
if (!expected || !provided) return false;
const a = Buffer.from(provided);
const b = Buffer.from(expected);
return a.length === b.length && crypto.timingSafeEqual(a, b);
}
fastify.post('/webhook/position/:token', async (req, reply) => {
if (!tokenMatches(req.params.token)) {
return reply.code(401).send({ error: 'Invalid webhook token' });
}
// … traitement
});
Trade-offs honnêtes
✅ Zéro modif côté émetteur : Traccar ne fait que dire forward.url=https://…/webhook/position/<TOKEN>. Le secret voyage chiffré dans TLS, illisible sur le réseau.
✅ timingSafeEqual empêche les attaques par chronométrage : sans ça, comparer deux strings avec === permet à un attaquant de deviner le token caractère par caractère via la durée de réponse.
⚠ Le token apparaît dans les logs d’accès si l’URL complète est logguée. Mitigation : règle de masquage dans les logs Fastify et Traefik (/webhook/position/<redacted>).
⚠ Pas de protection contre le replay : si un attaquant capture une requête (improbable avec TLS), il peut la rejouer. Tolérable ici parce que les positions GPS sont idempotentes côté logique métier (cooldown anti-flood sur les alertes). Si tu manipules des virements bancaires, ce pattern ne suffit pas.
Lessons learned
forward.headerstatique = pattern signature HMAC impossible côté client ; tomber sur le secret-dans-URL est souvent la voie la plus simplecrypto.timingSafeEqualest gratuit dans la stdlib Node — pas d’excuse pour un===sur un secret- Filtrer le token des access logs dès le départ, sinon il fuite dans le premier dump de logs partagé
- Anti-spoof ne protège pas du replay ; si le replay est dangereux pour ton métier, ajoute un nonce ou un timestamp signé côté émetteur — mais alors le pattern « header statique » ne suffit plus
- Le segment d’URL est protégé par TLS au même titre que les headers : le path n’est PAS en clair sur le réseau (contrairement à une idée reçue)