Русский
Секреты для код-ролей
Код-роли, которая ходит во внешний API, нужен токен. Дать его можно двумя способами, и ни один не костыль для другого.
Обычный способ — окружение запуска: значение держит оператор сервиса codellm, а код читает его как любую переменную окружения. Если у вас уже подняты Docker secrets, секреты Kubernetes или Vault agent, ваши креденшелы уже лежат там, и переносить их никто не просит.
Второй способ — тот же, что у Rails с credentials.yml.enc: зашифрованное значение коммитится, ключ остаётся снаружи. Ролевая заметка несёт шифротекст, ключ держит codellm, и значение расшифровывается для одной этой роли в момент запуска. За ним тянутся, когда секрет принадлежит одной роли или когда добавить его надо без правки compose и перезапуска.
В статье:
- Из окружения запуска
- Запечатать значение
- Объявить его в роли
- Прочитать в коде
- От чего это защищает
- Ротация ключей
Из окружения запуска
Что увидит код, решает оператор codellm — списком имён переменных:
CODELLM_EXPOSE_ENV=KRISP_TOKEN,KRISP_BASE_URL
CODELLM_EXPOSE_ENV_PREFIX=KRISP_
По умолчанию не открыто ничего. Переменная, которой нет ни в одном списке, до кода не доедет, что бы роль ни просила.
Роль читает её так:
import os
token = os.environ["KRISP_TOKEN"]
По умолчанию каждая код-роль на этом codellm видит все разрешённые переменные. Роль может сузить свою долю:
env_passthrough: [KRISP_TOKEN]
env_prefix: [KRISP_]
Это только сужает. Границей остаётся список оператора: объявить переменную, которую он не разрешил, — значит не получить ничего, а роль, не объявившая ни одного поля, по-прежнему видит весь список. Сужать стоит, когда на одном codellm живёт несколько ролей и у каждой свой креденшел.
Цена этого пути — добавление секрета остаётся действием оператора: новая переменная означает правку деплоя и перезапуск. Именно это убирает второй путь.
Запечатать значение
Ключ — строка ровно в 32 байта в окружении codellm, по умолчанию SEAL_KEY. Сгенерируйте один раз:
openssl rand -hex 16 # 32 символа
Положите его в окружение codellm так же, как любой другой серверный секрет, и запечатайте значение:
printf %s "$KRISP_TOKEN" | codellm seal
sealed:v1:9nR2v0QkX8tWc1pE...
Секрет уходит в stdin, а не аргументом: флаг был бы виден в таблице процессов и осел бы в истории шелла.
Из браузера
Терминал на хосте codellm не обязателен. Та же операция есть формой на /_system/codellm/seal, за тем же админским логином, что и остальной codellm: укажите переменную с ключом (SEAL_KEY, если не используете другую), вставьте значение и скопируйте результат sealed:v1:... прямо в заметку.
Значение уходит в теле формы и никогда не попадает в URL — секрет в URL оседает в логах прокси, истории браузера и заголовке Referer. Страница с результатом показывает только запечатанный блоб, но не то, что вы ввели.
Путь настраивается (CODELLM_SEAL_PATH), если значение по умолчанию с чем-то конфликтует.
Объявить его в роли
Вставьте результат во frontmatter ролевой заметки и перечислите поля, которые надо открывать:
---
fleet_id: codellm
mode: cron
cron_schedule: "*/30 * * * *"
write_patterns: ["transcripts/**"]
unseal: [krisp_token]
krisp_token: sealed:v1:9nR2v0QkX8tWc1pE...
krisp_base_url: https://api.krisp.ai
---
unseal — список имён полей, а не переключатель. Когда поля названы явно, испорченный блоб падает с понятной ошибкой сразу, а не едет дальше обычной строкой, чтобы через двадцать минут вернуться из API как 401.
Остальной frontmatter остаётся собой. krisp_base_url не секрет, и церемоний ему не нужно.
Прочитать в коде
Два отдельных доступа, и разделены они намеренно:
import fleetkit
cfg = fleetkit.frontmatter() # собственные настройки роли
sec = fleetkit.secrets() # то, что открыто для этого запуска
base_url = cfg.krisp_base_url.rstrip("/")
token = sec.krisp_token
Секреты не подмешиваются во frontmatter по практической причине: bag доставки — это то, что распечатывают при отладке роли. print(cfg) с токеном внутри опубликовал бы его в заметку, то есть ровно туда, откуда весь механизм его и убирает. Разделение оставляет секрет вне объекта, который чаще всего дампят целиком.
Шифротекст при этом виден в cfg.krisp_token, так что роль может отличить запечатанное поле.
Node-роли читают те же два:
const cfg = fleetkit.frontmatter();
const sec = fleetkit.secrets();
От чего это защищает
Граница уже, чем обещает слово «зашифровано», поэтому её стоит назвать точно.
Секрет не лежит открытым текстом нигде, куда попадает волт: ни в синхронизации Obsidian, ни в истории git, ни в бэкапе заметок, ни в дампе базы. Тот, кто прочитает ваши заметки, не узнает из них ничего. Каждый блоб открывается только для той роли, которая его несёт, поэтому неаккуратная роль не выльет чужой креденшел в заметку.
Тот, кто запускает codellm, читает любой открытый секрет. Исполняемому коду нужен плейнтекст, значит плейнтекст есть в этом процессе. Это не способ спрятать креденшел от собственного оператора сервера, и никакая схема хранения им быть не может: машина, которая пользуется секретом, его видит.
Роль, которая решит записать свой токен в заметку, тоже это сделает. Проверка границ смотрит, в какие пути роль может писать, и никогда — что именно она туда пишет. Останавливает это не шифрование, а то, что роль написали вы.
Сам ключ в песочницу не попадает. Код ничего не расшифровывает — он получает только значения, объявленные его же заметкой. А если ключ настроить так, чтобы он передавался в исполняемый код, codellm откажется стартовать: одна такая строка отменила бы всю схему, и снаружи ничего бы не сломалось.
Ротация ключей
Роль может назвать свой ключ:
unseal: [krisp_token]
unseal_env_key: SEAL_KEY_V2
Так ротация становится постепенной вместо общего дня переезда. Добавьте SEAL_KEY_V2 рядом со старым ключом, перепечатывайте и переводите роли по одной, а старый ключ уберите, когда на него больше никто не ссылается.
Перепечатать значит поправить заметку: замена скомпрометированного токена — это правка и синхронизация, а не обновление строки в базе. И старый шифротекст остаётся в истории git волта: без ключа он нечитаем, но ротация его оттуда не стирает, так что утечку SEAL_KEY считайте утечкой всего, что им запечатано.