Перейти к содержанию

Модель безопасности

vaultly — менеджер секретов, поэтому что именно он защищает (и не защищает) — самая важная вещь, которую нужно понимать.

Что vaultly маскирует

Операция Маскируется?
repr(model)
str(model)
model.model_dump()
model.model_dump_json()
model.model_dump(mode="json")
Pydantic rich repr / IPython ✅ (через __repr_args__)

Каждое секретное поле во всём перечисленном выводится как "***".

Что vaultly НЕ маскирует

Прямой доступ к атрибуту

print(config.db_password)   # печатает реальное значение
log.info("pw=%s", config.db_password)   # пишет реальное значение в лог

Секретное поле — это обычный str (или int, dict, …), не SecretStr-прокси. Это сделано намеренно, чтобы драйверы БД, HTTP-клиенты работали без обёрток. Цена — вы отвечаете за то, чтобы секреты не попадали в логи, форматные строки и сообщения исключений.

Pickling и copy

Эти операции поднимают NotImplementedError:

  • pickle.dumps(model)
  • copy.copy(model)
  • copy.deepcopy(model)
  • model.model_copy()

Причина: in-memory кэш содержит секреты в открытом тексте. Pickle отправил бы их на диск или в сеть. copy.copy тихо разделил бы кэш между двумя инстансами. model_copy склонировал бы его и поломал связи _root во вложенных деревьях.

Если нужно пересоздать модель в другом процессе или после reload'а, передавайте аргументы конструктора (значение stage="prod" и конфиг бэкенда) и пересобирайте модель на той стороне.

Память процесса

Секретные строки лежат в обычной памяти Python, без шифрования. Они не зануляются при удалении из кэша — обычная Python GC рано или поздно освободит память, но не сразу. Hardening через mlock потребовал бы C-extension, который мы не пишем.

Это значит, что дамп памяти процесса (core file, gcore, debugger attach) покажет закэшированные секреты. Меры защиты — на уровне ОС / оркестратора (запретить core dumps в проде, ограничить ptrace, запретить debugger attach), это вне компетенции vaultly.

Tracebacks

Traceback от vaultly может содержать резолвленный путь, но никогда не содержит значение. Сообщения ошибок написаны так, чтобы это было верно. Однако callable transform=, который сам кинет ValueError("bad value: <actual value>"), естественно утечёт значение. Аудитьте свои transform отдельно.

Логи

vaultly использует логгер с именем vaultly, к которому подключён NullHandler. Без явной настройки логгирования vaultly не пишет вообще ничего в stderr.

Когда вы подключите свой handler, vaultly будет писать WARNING на ретраи и stale-on-error fallback'и. В записях есть резолвленный путь (например /prod/db/password), но не значение. Резолвленные пути могут содержать {var}-подстановки (tenant id, регион), что в некоторых compliance-режимах считается PII.

Чтобы фильтровать / скрабить логи перед отправкой:

import logging

class ScrubVaultlyPaths(logging.Filter):
    def filter(self, record):
        # модифицируйте record.msg / record.args здесь
        return True

logging.getLogger("vaultly").addFilter(ScrubVaultlyPaths())

Что vaultly НЕ обещает

  • Это не криптографическая библиотека. Encryption-at-rest, attestation, constant-time comparison — не наше.
  • Не запрещает вам логировать секретные значения. Если print(cfg.api_key) оказался в stdout — это на вас.
  • Не делает ротацию. Ротацию делает SSM / Vault / другой ваш store; vaultly просто потребляет результат.

Defense-in-depth: рекомендации

  • Минимизируйте IAM / Vault-политики до того, что реально нужно приложению. vaultly показывает резолвленный путь в ошибках — пусть путь, который вам разрешено читать, совпадает с тем, который вы реально читаете.
  • Используйте SSM SecureString (KMS-encrypted) или Vault для всего, что важно. Не кладите credentials в обычный String-параметр.
  • Не пиклите и не сохраняйте на диск объекты, держащие SecretModel.
  • Ставьте stale_on_error=True только осознанно. Иногда упасть безопаснее, чем продолжить с устаревшими credentials.
  • Запретите core dumps в проде (ulimit -c 0, в k8s securityContext.allowPrivilegeEscalation: false и т. п.).
  • Ограничьте доступ к логам так же, как к любому источнику PII.