Защищённое межсервисное взаимодействие

В распределённых системах каждый сервис становится потенциальной точкой входа для атаки. Даже внутри доверенной инфраструктуры нельзя полагаться на «внутреннюю сеть» как на безопасную среду. Межсервисное взаимодействие должно рассматриваться как недоверенное по умолчанию: каждый запрос — потенциально перехваченный, модифицированный или подделанный.

Криптографическая библиотека TweetNaCl.js (и её интерфейсные обёртки вроде nacl.js) предоставляет минималистичный набор примитивов, достаточный для построения защищённого канала связи между сервисами без необходимости внедрения тяжёлых криптографических стеков.


Базовые криптографические примитивы TweetNaCl.js

TweetNaCl.js реализует проверенный набор алгоритмов из семейства NaCl (Networking and Cryptography library). Архитектура ориентирована на простоту и безопасность без конфигурируемых «опасных параметров».

Основные примитивы:

  • Public-key cryptography (box) — шифрование с использованием пары ключей
  • Secret-key cryptography (secretbox) — симметричное шифрование
  • Digital signatures (sign) — цифровая подпись сообщений
  • Hash functions (hash) — криптографическое хеширование
  • Random generation — криптографически стойкие случайные значения

Модель межсервисного взаимодействия

В микросервисной архитектуре каждый сервис рассматривается как автономный участник сети, обладающий собственным ключом:

  • каждый сервис имеет public/private keypair
  • публичный ключ распространяется через доверенный реестр (service registry / config / vault)
  • приватный ключ хранится изолированно (env, HSM, secret manager)
  • каждое сообщение подписывается и/или шифруется

Генерация ключевой пары

TweetNaCl.js предоставляет генерацию ключей для асимметричного шифрования:

import nacl from "tweetnacl";
import util from "tweetnacl-util";

const keyPair = nacl.box.keyPair();

const publicKey = keyPair.publicKey;
const secretKey = keyPair.secretKey;

В контексте сервиса:

  • publicKey публикуется
  • secretKey никогда не покидает сервис

Шифрование межсервисных сообщений (box)

Механизм nacl.box реализует публично-ключевое шифрование между двумя сторонами: отправителем и получателем.

Каждый участник имеет пару ключей. Для шифрования требуется:

  • публичный ключ получателя
  • приватный ключ отправителя
  • nonce (одноразовый вектор)

Формирование защищённого сообщения

const message = "transfer:1000:account42";

const nonce = nacl.randomBytes(nacl.box.nonceLength);

const encrypted = nacl.box(
  util.decodeUTF8(message),
  nonce,
  receiverPublicKey,
  senderSecretKey
);

Дешифрование на стороне сервиса-получателя

const decrypted = nacl.box.open(
  encrypted,
  nonce,
  senderPublicKey,
  receiverSecretKey
);

const originalMessage = util.encodeUTF8(decrypted);

Критическая роль nonce

Nonce (number used once) — обязательный элемент защиты. Повторное использование nonce с тем же ключом приводит к компрометации шифрования.

В межсервисной архитектуре:

  • nonce всегда уникален на пару ключей
  • часто генерируется через randomBytes
  • может включать timestamp + random suffix

Симметричное шифрование для высоконагруженных каналов (secretbox)

Когда оба сервиса уже обменялись ключом сессии, эффективнее использовать симметричное шифрование.

const key = nacl.randomBytes(nacl.secretbox.keyLength);
const nonce = nacl.randomBytes(nacl.secretbox.nonceLength);

const message = util.decodeUTF8("update-cache:user:42");

const box = nacl.secretbox(message, nonce, key);

Расшифровка:

const opened = nacl.secretbox.open(box, nonce, key);
const result = util.encodeUTF8(opened);

Подпись сообщений (sign)

Шифрование не гарантирует целостность источника без отдельной проверки подписи. Для этого используется nacl.sign.

Подписывающая сторона:

const keyPair = nacl.sign.keyPair();

const message = util.decodeUTF8("event:order_created:123");

const signed = nacl.sign(message, keyPair.secretKey);

Проверка подписи:

const verified = nacl.sign.open(signed, keyPair.publicKey);

const payload = util.encodeUTF8(verified);

Разделение ответственности: шифрование vs подпись

В межсервисной архитектуре важно разделять:

  • box/secretbox — конфиденциальность
  • sign — аутентификация и целостность

Типовая схема:

  1. Сервис A формирует сообщение
  2. Подписывает его своим ключом
  3. Шифрует для сервиса B
  4. Сервис B расшифровывает
  5. Проверяет подпись сервиса A

Протокол взаимодействия между сервисами

Типовой поток выглядит следующим образом:

  1. Получение публичных ключей из реестра
  2. Генерация nonce
  3. Формирование payload
  4. Подпись сообщения
  5. Шифрование сообщения
  6. Передача по HTTP/gRPC/message broker
  7. Дешифрование на стороне получателя
  8. Проверка подписи отправителя

Пример комплексного запроса

Отправка защищённого события:

const payload = {
  type: "payment",
  amount: 250,
  currency: "KZT"
};

const serialized = util.decodeUTF8(JSON.stringify(payload));

const signature = nacl.sign(serialized, senderSecretKey);

const nonce = nacl.randomBytes(nacl.box.nonceLength);

const encrypted = nacl.box(
  signature,
  nonce,
  receiverPublicKey,
  senderSecretKey
);

Проверка и восстановление на стороне получателя

const decrypted = nacl.box.open(
  encrypted,
  nonce,
  senderPublicKey,
  receiverSecretKey
);

const verified = nacl.sign.open(
  decrypted,
  senderPublicKey
);

const message = JSON.parse(util.encodeUTF8(verified));

Управление ключами в распределённой системе

Ключевая проблема межсервисной безопасности — не алгоритмы, а управление ключами.

Практические подходы:

  • централизованный vault (HashiCorp Vault, KMS)
  • периодическая ротация ключей
  • привязка ключей к идентичности сервиса
  • хранение публичных ключей в service discovery
  • автоматическое обновление ключей без остановки сервиса

Защита от replay-атак

Replay-атаки особенно опасны в микросервисах: злоумышленник повторно отправляет валидное сообщение.

Механизмы защиты:

  • nonce + cache проверенных nonce
  • timestamp в payload
  • ограничение TTL сообщений

Пример проверки:

if (Date.now() - payload.timestamp > 30000) {
  throw new Error("Message expired");
}

if (nonceCache.has(payload.nonce)) {
  throw new Error("Replay detected");
}

Ограничения TweetNaCl.js в сервисной архитектуре

Несмотря на надёжность примитивов, библиотека не решает архитектурные задачи:

  • нет встроенного протокола обмена ключами
  • нет политики ротации ключей
  • нет стандарта транспортного уровня
  • нет формата сообщений

Поэтому TweetNaCl.js используется как криптографический слой, а не как протокол.


Комбинирование с транспортным уровнем

На практике криптографический слой накладывается на:

  • HTTP REST
  • gRPC
  • Kafka / RabbitMQ
  • WebSocket

Типичная интеграция:

Service A
  → encrypt + sign (TweetNaCl.js)
  → HTTP request
API Gateway
  → forward payload
Service B
  → verify + decrypt

Производительность и оптимизация

TweetNaCl.js оптимизирован для:

  • минимального размера
  • предсказуемого времени выполнения
  • отсутствия динамической аллокации сложных структур

Для высоконагруженных систем:

  • предпочтение secretbox вместо box после установления сессии
  • кэширование keypair
  • минимизация сериализации
  • использование бинарных буферов вместо JSON там, где возможно

Безопасные паттерны межсервисного взаимодействия

  • каждый сервис владеет собственной криптографической идентичностью
  • доверие строится через публичные ключи, а не сетевую сегментацию
  • каждое сообщение имеет проверяемую подпись
  • каждое сообщение шифруется на уровне приложения
  • отсутствие «чистого текста» в межсервисном трафике
  • обязательная защита от повторной отправки

Типовые ошибки реализации

  • использование одного nonce повторно
  • хранение приватных ключей в коде
  • отсутствие проверки подписи
  • доверие только TLS без application-layer encryption
  • отсутствие ротации ключей
  • отсутствие контроля времени жизни сообщений