Как обойти ограничение окружения в 4 КБ на Vercel

В Layer3, мы запускаем всю нашу платформу в прекрасно организованном full-stack окружении на Vercel. Весь серверный и клиентский код написан на TypeScript и использует многие модули и типы.

Все шло хорошо, пока однажды…

post image

У Vercel есть ограничение на переменную окружения в 4 КБ. Это вызвано базовой инфраструктурой AWS Lambda, но, хотя у AWS есть некоторые решения для правильного управления секретами, Vercel в основном говорит, что вам нужно создать собственное управление секретами.

В это время, у платформы Vercel, есть так много плюсов, и мы сохраняем так много времени, поскольку нам не нужно настраивать сложную облачную инфраструктуру на AWS.

Поэтому мы решили исправить это.

Наше решение сфокусировано на двух вещах:

  • Безопасное управление и развертывание секретов при просмотре и производстве

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

Посмотрите пример репозитория здесь: larskarbo/next-env-encrypt-decrypt

Познакомьтесь с Doppler - менеджером по окружению

Doppler - это сервис, который специализируется на управлении переменными окружения. Это звучало идеально для нашего варианта использования, у них даже есть интеграция с Vercel!

post image

Однако мы быстро осознали, что хотя у Doppler есть интеграция с Vercel, это вовсе не решает проблему 4 КБ. На самом деле, он просто - вроде как способствует этому... Добавляя больше переменных DOPPLER_.

Однако, интерфейс Doppler и API удивительные, и мы подумали, что могли бы создать работающее решение с некоторыми взломами.

Извлечение секретов из Doppler вместо Vercel

Как только вы добавите все свои переменные окружения в Doppler вместо Vercel, вы сможете довольно легко обойти ограничение в 4 КБ, извлекая секреты из Doppler вместо Vercel.

post image

Единственная переменная окружения, которая вам нужна в Vercel, - это токен Doppler. Самый простой способ добавить токены для developmentи preview - production это установить Vercel CLI и Doppler CLI и сгенерировать три разных ключа из терминала:

echo -n "$(doppler configs tokens create vercel-gitops --config dev --plain)" | vercel env add DOPPLER_TOKEN development
echo -n "$(doppler configs tokens create vercel-gitops --config stg --plain)" | vercel env add DOPPLER_TOKEN preview
echo -n "$(doppler configs tokens create vercel-gitops --config prd --plain)" | vercel env add DOPPLER_TOKEN production

Затем мы создадим скрипт fetchSecrets.ts, который извлекает эти переменные во время создания и записывает их в .env.

import fs from "fs/promises";
import secrets from "@larskarbo/gitops-secrets";

async function main() {
  const payload = await secrets.providers.doppler.fetch();

  let envFile = "";

  Object.entries({
    ...payload,
  }).forEach(([key, value]) => {
    envFile += `${key}=${value}\n`;
  });

  envFile += `DOPPLER_TOKEN=${process.env.DOPPLER_TOKEN}\n`;

  await fs.writeFile(".env", envFile);
}

void main();

Изменения в package.json:

"scripts": {
    ...
    "build": "npm run fetch-secrets && nextjs build",
    "fetch-secrets": "ts-node fetchSecrets.ts"
}

Да, это все, что вам нужно.

В разработке, вы просто запустите npm run fetch-env. Этот процесс не добавляет много движущихся частей и ощущается очень схоже на рабочий процесс vercel env pull.

Делаем шаг вперед с зашифрованными секретами

Теперь, когда мы создаем собственное управление секретами, почему бы не сделать шаг вперед и улучшить безопасность?

Текущая настройка переменной окружения может представлять угрозу безопасности. Мошеннический npm пакет мог бы сбросить все свободно доступные переменные process.env и отправить их на отдаленный сервер. И помните, это также могло бы быть зависимостью одной из ваших зависимостей. У большинства npm приложений есть куча зависимостей, когда вы смотрите на дерево зависимостей, поэтому область поверхностного риска может быть больше, чем вы думаете.

Нашей целью будет создание системы, где:

  • Секреты всегда защифрованы, как при передаче, так и при хранении.

  • Секретам сложно непреднамеренно утечь, когда они потребляются конечным приложением.

У многих платформ для этого есть сложные решения такие, как AWS KMS и Docker Secrets. Идея в том, что эти инструменты хранят секрет в зашифрованной форме и предоставляют его приложению во время выполнения.

Мы решим это простым и индивидуальным способом с некоторыми уникальными соображениями:

  • Нам нужно, чтобы переменные NEXT_PUBLIC_ были доступны в окружении.

  • Мы хотим быть в состоянии переопределять секреты с .env.local для наших локальных окружений разработки.

post image

Основываясь на настройке Doppler, мы добавим в Vercel другую переменную окружения, SECRETS_KEY.

 gen_key () { openssl rand -base64 32 }

gen_key | vercel env add SECRETS_KEY development
gen_key | vercel env add SECRETS_KEY preview
gen_key | vercel env add SECRETS_KEY production

Теперь мы сделаем некоторые изменения в наш скрипт fetch-secrets.ts.

Это нужно, чтобы:

  1. Извлечь секреты из Doppler

  2. Записать все переменные NEXT_PUBLIC_ в .env

  3. Записать все остальные секреты в специальный файл .encrypted-secrets

Зафиксируйте этот файл в git вот так, а затем добавьте его в .gitignore. Это позволяет нам запускать приложение независимо от сгенерированного файла.

Наши супер-заряженные fetch-secrets.ts выглядят так:

import Cryptr from "cryptr";
import fs from "fs/promises";

import gitopsSecrets from "@larskarbo/gitops-secrets";
import { ENCRYPTED_SECRETS_FILE } from "../src/utils";

async function main() {
  const payload = await gitopsSecrets.providers.doppler.fetch();

  if (!process.env.SECRETS_KEY) {
    throw new Error("SECRETS_KEY is not set");
  }

  const cryptr = new Cryptr(process.env.SECRETS_KEY);

  const encryptedText = cryptr.encrypt(JSON.stringify(payload));

  await fs.writeFile(ENCRYPTED_SECRETS_FILE, encryptedText);

  let envFile = "";

  Object.entries({
    ...payload,
  })
    .filter(([key]) => key.startsWith("NEXT_PUBLIC_"))
    .forEach(([key, value]) => {
      envFile += `${key}=${value}\n`;
    });

  envFile += `DOPPLER_TOKEN=${process.env.DOPPLER_TOKEN}\n`;
  envFile += `SECRETS_KEY=${process.env.SECRETS_KEY}\n`;

  await fs.writeFile(".env", envFile);
}

void main();

Затем нам нужно расшифровать секреты в коде времени выполнения. Мы создадим вспомогательную функцию для этого.

let decryptedSecrets: null | {
  [key: string]: string;
} = null;

import { readFileSync } from "fs";

import Cryptr from "cryptr";
import path from "path";

export const ENCRYPTED_SECRETS_FILE = ".encrypted-secrets";

export const getSecret = (key: string) => {
  // in case you have some overrides in `.env.local`
  if (process.env.NODE_ENV === "development" && process.env[key]) {
    return process.env[key];
  }

  // only decrypt secrets the first time
  if (!decryptedSecrets) {
    if (!process.env.SECRETS_KEY) {
      return undefined;
    }

    const encryptedSecrets = readFileSync(
      path.join(process.cwd(), ENCRYPTED_SECRETS_FILE),
      "utf8"
    );
    const cryptr = new Cryptr(process.env.SECRETS_KEY);
    decryptedSecrets = JSON.parse(cryptr.decrypt(encryptedSecrets));
  }

  return decryptedSecrets?.[key];
};

Вуаля! Теперь вы можете использовать секреты везде в вашем приложении следующим образом:

// back-end
const apiKey = getSecret("API_KEY")

// front-end
const somePublicKey = process.env.NEXT_PUBLIC_KEY

Проверьте рабочую демонстрацию здесь: (ссылка, репозиторий в github).

Заключение

У Vercel может быть ограничение окружения в 4 КБ, но с некоторой творческой инженерией, вы можете столкнуться с системой, которая будет более удобной для разработчиков и безопасной, чем раньше.

Такой подход может быть правильным, если вы являетесь стартапом на ранней стадии. Когда вы станете больше и у вас будут строже требования к управлению конфиденциальными данными, вы, вероятно, столкнетесь с более сложной облачной инфраструктурой.

В Layer3, мы используем Vercel и Doppler для быстрого перемещения. Если вам понравился этот пост и вам понравилась идея создания новых типов приложений, которые используют преимущества децентрализованной сети, вам следует  присоединиться к нашей команде!