Хранение ключей в Node.js: файловая система, переменные окружения, секрет-менеджеры

Классы секретов в приложениях с криптографией

В приложениях, использующих TweetNaCl.js / nacl.js, ключи делятся на несколько категорий:

  • Приватные ключи (secret key) — никогда не покидают доверенную среду
  • Публичные ключи (public key) — могут распространяться свободно
  • Временные ключи сессий — используются для краткоживущих операций
  • Корневые ключи (master keys) — применяются для шифрования других ключей

Основная задача хранения — минимизировать время существования секретов в открытом виде и исключить их попадание в исходный код.


Генерация ключей в TweetNaCl.js

Базовый пример генерации пары ключей:

const nacl = require('tweetnacl');
nacl.util = require('tweetnacl-util');

const keyPair = nacl.box.keyPair();

const publicKey = nacl.util.encodeBase64(keyPair.publicKey);
const privateKey = nacl.util.encodeBase64(keyPair.secretKey);

Полученные значения требуют немедленного выбора стратегии хранения.


Файловая система как способ хранения ключей

Общая модель

Файловая система применяется в случаях, когда:

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

Ключи обычно сохраняются в зашифрованном виде.


Простой вариант хранения (не рекомендуется без защиты)

const fs = require('fs');

fs.writeFileSync('./keys/private.key', privateKey);
fs.writeFileSync('./keys/public.key', publicKey);

Проблема такого подхода — отсутствие защиты от чтения на уровне ОС и отсутствие шифрования.


Безопасный вариант с шифрованием

Используется симметричное шифрование (например, через AES) перед записью:

const crypto = require('crypto');
const fs = require('fs');

function encrypt(text, password) {
  const iv = crypto.randomBytes(16);
  const key = crypto.scryptSync(password, 'salt', 32);

  const cipher = crypto.createCipheriv('aes-256-gcm', key, iv);

  let encrypted = cipher.update(text, 'utf8', 'base64');
  encrypted += cipher.final('base64');

  const tag = cipher.getAuthTag();

  return {
    iv: iv.toString('base64'),
    content: encrypted,
    tag: tag.toString('base64')
  };
}

Хранение результата:

fs.writeFileSync('./keys/private.enc.json', JSON.stringify(encryptedKey));

Права доступа к файлам

Критически важно ограничивать доступ:

chmod 600 private.key

или через Node.js:

fs.writeFileSync('./keys/private.key', privateKey, { mode: 0o600 });

Переменные окружения

Базовая модель

Переменные окружения используются для передачи секретов в рантайм без записи на диск.

Пример:

PRIVATE_KEY=base64encodedkey
PUBLIC_KEY=base64encodedkey

Доступ в Node.js:

const privateKey = process.env.PRIVATE_KEY;
const publicKey = process.env.PUBLIC_KEY;

Использование dotenv

Файл .env:

PRIVATE_KEY=...
PUBLIC_KEY=...

Загрузка:

require('dotenv').config();

Ограничения подхода

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

Практика безопасного использования

  • не выводить process.env в лог
  • не передавать окружение в сторонние процессы
  • не использовать .env в production без защиты контейнеров

Секрет-менеджеры

Общая архитектура

Секрет-менеджеры обеспечивают:

  • централизованное хранение ключей
  • аудит доступа
  • автоматическую ротацию
  • шифрование at-rest

AWS Secrets Manager

Получение секрета:

const { SecretsManagerClient, GetSecretValueCommand } = require("@aws-sdk/client-secrets-manager");

const client = new SecretsManagerClient({ region: "us-east-1" });

async function getSecret() {
  const command = new GetSecretValueCommand({
    SecretId: "my-private-key"
  });

  const response = await client.send(command);
  return JSON.parse(response.SecretString);
}

HashiCorp Vault

Типичная схема работы:

  • приложение аутентифицируется (token / AppRole)
  • запрашивает секрет
  • получает временный доступ

Пример:

const vault = require('node-vault')({
  endpoint: 'https://vault.example.com',
  token: process.env.VAULT_TOKEN
});

async function getKey() {
  const result = await vault.read('secret/data/app');
  return result.data.data.privateKey;
}

Google Secret Manager

const { SecretManagerServiceClient } = require('@google-cloud/secret-manager');

const client = new SecretManagerServiceClient();

async function accessSecret() {
  const [version] = await client.accessSecretVersion({
    name: 'projects/my-project/secrets/private-key/versions/latest'
  });

  return version.payload.data.toString();
}

Azure Key Vault

const { DefaultAzureCredential } = require('@azure/identity');
const { SecretClient } = require('@azure/keyvault-secrets');

const credential = new DefaultAzureCredential();
const client = new SecretClient("https://myvault.vault.azure.net", credential);

async function getSecret() {
  const secret = await client.getSecret("private-key");
  return secret.value;
}

Интеграция с TweetNaCl.js / nacl.js

Типовой поток использования ключей

  1. Получение ключей из источника (env / vault / file)
  2. Декодирование base64
  3. Преобразование в Uint8Array
  4. Использование в cryptographic operations

Пример восстановления ключа

const nacl = require('tweetnacl');
nacl.util = require('tweetnacl-util');

function loadKey(base64Key) {
  return nacl.util.decodeBase64(base64Key);
}

const secretKey = loadKey(process.env.PRIVATE_KEY);
const publicKey = loadKey(process.env.PUBLIC_KEY);

Использование ключей в шифровании

const message = nacl.util.decodeUTF8("data");

const encrypted = nacl.box(
  message,
  nonce,
  recipientPublicKey,
  senderSecretKey
);

Ротация ключей

Секреты не должны иметь бесконечный срок жизни.

Стандартная схема:

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

Пример логики ротации

const keys = {
  current: process.env.PRIVATE_KEY_V2,
  previous: process.env.PRIVATE_KEY_V1
};

Типичные ошибки хранения ключей

  • хранение приватных ключей в репозитории
  • использование одинаковых ключей в dev и prod
  • отсутствие контроля доступа к .env
  • логирование ключей в debug-режиме
  • хранение ключей в localStorage (в случае frontend-интеграций)
  • отсутствие ротации

Сравнение подходов хранения

Метод Безопасность Удобство Масштабируемость
Файловая система (plaintext) низкая высокая низкая
Файловая система (encrypted) средняя средняя средняя
переменные окружения средняя высокая средняя
secret manager высокая средняя высокая

Выбор стратегии хранения

Архитектура хранения ключей зависит от:

  • масштаба системы
  • модели угроз
  • среды выполнения (VM, контейнеры, serverless)
  • частоты ротации

В распределённых системах с криптографией на базе TweetNaCl.js предпочтение отдается внешним секрет-менеджерам, тогда как локальные сервисы часто используют защищённые файлы или переменные окружения с ограничениями доступа.