2 / 8
Шифрование: age и sops
Забудем на время про агентов, MCP и всё, что построено сверху. В самом основании Athanor лежит простой вопрос: как хранить секрет на диске так, чтобы файл с ним можно было спокойно скопировать, закоммитить или потерять на флешке — и чтобы это ничего не значило без одной конкретной вещи.
age: пара ключей вместо пароля
age шифрует данные на пару ключей, а не на пароль:
- публичный ключ (
age1...) умеет только шифровать. Раздавать его не страшно; - приватный ключ (
AGE-SECRET-KEY-1...) умеет только расшифровывать. Это единственное, что нужно беречь.
Хорошая аналогия — почтовый ящик: публичный ключ — прорезь, куда любой может бросить письмо, приватный — ключ от дверцы, только им письма достаются обратно.
В Athanor это устроено так: athanor init генерирует такую пару один раз
(internal/crypto/identity.go, age.GenerateX25519Identity()) и кладёт
приватную часть в ~/.config/athanor/identity.txt — по умолчанию одну на
всего разработчика, а не на проект, чтобы единственный бэкап ключа
закрывал сразу все проекты. Публичная часть (recipient) уходит в
athanor.yaml — её видно, и коммитить не страшно.
sops: шифрование по значениям, а не по файлу целиком
Сам по себе age шифрует данные в одну сплошную кашу — непонятно, что
внутри, и не подифаешь по git. sops решает именно это: шифрует
значения в YAML/JSON, оставляя имена ключей читаемыми.
DB_PASSWORD: ENC[AES256_GCM,data:xqmOcd0=,iv:...,tag:...,type:str]
STRIPE_TEST_KEY: ENC[AES256_GCM,data:...,type:str]
sops:
age:
- recipient: age1...
enc: |
-----BEGIN AGE ENCRYPTED FILE-----
...
Видно, какие секреты вообще есть — но не их значения. Ровно это и делает
athanor list: список имён без единого значения.
Как Athanor на самом деле вызывает sops: без plaintext на диске
Главное инженерное решение здесь — не что используется (age и sops), а
как именно они вызываются. Обычный интерактивный режим sops file.yaml
создаёт временный расшифрованный файл на диске, открывает его в редакторе и
только потом зашифровывает обратно. Именно этого Athanor избегает: любой
временный файл с plaintext — потенциальная дыра.
Вместо этого internal/crypto/sops.go проводит всё через stdin/stdout:
ЧТЕНИЕ (Decrypt):
sops --decrypt --input-type yaml --output-type json secrets.enc.yaml
(SOPS_AGE_KEY_FILE=identity.txt в окружении процесса sops)
│
▼
JSON прилетает в stdout процесса sops
│
▼
Athanor парсит его прямо в память (map[string]string)
— на диск ничего не пишется
ЗАПИСЬ (Encrypt):
Athanor сериализует map в JSON в памяти
│
▼ (передаётся через stdin, не через файл)
sops --encrypt --input-type json --output-type yaml
--age <recipient> /dev/stdin
│
▼
зашифрованный YAML прилетает в stdout процесса sops
│
▼
Athanor одним os.WriteFile кладёт его в secrets.enc.yaml (chmod 600)
Если файла secrets.enc.yaml ещё нет — Decrypt просто возвращает пустую
карту. Это штатный случай "новое хранилище", а не ошибка.
Кто за что отвечает
- age отвечает только за то, чтобы пара ключей существовала и
sopsумел ими пользоваться — сам Athanor не вызываетageнапрямую для шифрования файлов, только берёт Go-библиотекуfilippo.io/ageдля генерации identity приathanor init. - sops — внешний бинарник, вызывается через
os/exec, и делает всю настоящую крипто-работу с файлом. Athanor не хранит и не переизобретает ничего криптографического сам — тот самый принцип "не пиши свою криптографию", который стоит соблюдать всегда и везде. - Athanor решает, когда звать sops, что ему передать на вход и кому отдать результат — а это уже вопрос модели доступа, разбор в следующей главе.
Контрольный вопрос. Почему интерактивный режим sops file.yaml (с
открытием редактора) не подходит Athanor, и что используется вместо него?
Ответ: интерактивный режим сам создаёт временный расшифрованный файл на
диске для редактора, а Athanor гарантирует, что plaintext никогда не
коснётся диска. Вместо него используются неинтерактивные
sops --decrypt/--encrypt, где данные идут через stdin/stdout и живут
только в памяти процесса Athanor.