ATHANOR
EN GitHub ↗

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.