Key Vault: секреты и ключи
Пароль от базы не должен лежать ни в коде, ни в переменной окружения. Разбираемся, где ему место и как приложение достаёт его вообще без пароля.
Azure Key Vault — сейф для секретов, ключей шифрования и TLS-сертификатов. Приложение не хранит пароли у себя: оно предъявляет своё удостоверение и получает нужный секрет по запросу, а каждое обращение попадает в журнал.
Почему коду не место рядом с паролем
«Да я потом вынесу в конфиг» — фраза, после которой строка подключения к продовой базе живёт в репозитории годами. Проблема даже не в том, что кто-то заглянет в исходники. Проблема в том, что у секрета в коде нет ни одного из свойств, которые нужны секрету:
- Его нельзя отозвать. Git помнит всё: удалили строку в новом коммите — она осталась в истории и в форках.
- Его нельзя сменить. Ротация пароля превращается в релиз с пересборкой образа.
- Никто не знает, кто им пользовался. Аудита нет. После инцидента вы не ответите на вопрос «кто и когда читал».
- Он расползается. Один и тот же пароль оказывается в CI, в Docker-образе, в тикете, в скриншоте.
Переменные окружения лучше кода, но ненамного. Значение appsettings в App Service видно в портале любому Contributor, оно светится в логах развёртывания и в дампах процесса. Это по-прежнему пароль, лежащий в открытом виде — просто в другом месте.
Что умеет Key Vault
| Тип объекта | Что это | Пример |
| Secret | произвольная строка до 25 КБ | строка подключения, API-токен, пароль SMTP |
| Key | криптографический ключ, который не покидает хранилище | ключ шифрования дисков, подпись JWT |
| Certificate | TLS-сертификат с автообновлением | сертификат для домена в App Service |
Разница между секретом и ключом принципиальна. Секрет вы забираете — Key Vault отдаёт вам строку. Ключ вы не забираете никогда: вы отправляете данные в Key Vault и просите зашифровать или подписать их, а приватная часть ключа физически не покидает хранилище (в Premium — вообще живёт в HSM, аппаратном модуле). Поэтому пароль от базы — это secret, а ключ, которым вы подписываете токены, — это key.
# хранилище сразу в режиме RBAC — так и надо для новых проектов
az keyvault create \
--resource-group rg-shop \
--name kv-shop-prod \
--location westeurope \
--enable-rbac-authorization true
az keyvault secret set \
--vault-name kv-shop-prod \
--name db-connection \
--value 'postgresql://app:S3cure!@pg-shop.postgres.database.azure.com/shop'
az keyvault secret show --vault-name kv-shop-prod --name db-connection --query value -o tsv
Флаг --enable-rbac-authorization true важен. У Key Vault исторически две модели доступа: старые access policies (отдельный список прав внутри самого хранилища) и обычный Azure RBAC. Access policies — легаси: они не наследуются, не видны в общем аудите ролей и не поддерживают Deny. Берите RBAC.
Как приложение получает секрет без пароля
Схема ровно та, о которой шла речь в уроке про RBAC: у приложения есть managed identity, ему выдаётся роль Key Vault Secrets User на конкретное хранилище — и всё.
PRINCIPAL_ID=$(az webapp identity assign --resource-group rg-shop --name app-shop-api --query principalId -o tsv)
VAULT_ID=$(az keyvault show --name kv-shop-prod --query id -o tsv)
az role assignment create \
--assignee-object-id "$PRINCIPAL_ID" \
--assignee-principal-type ServicePrincipal \
--role "Key Vault Secrets User" \
--scope "$VAULT_ID"
Код приложения при этом не содержит ни логина, ни пароля, ни ключа: DefaultAzureCredential сам разберётся, где он выполняется. На вашем ноутбуке он возьмёт токен из az login, в App Service — сходит к эндпоинту managed identity. Один и тот же код работает и локально, и в проде.
from azure.identity import DefaultAzureCredential
from azure.keyvault.secrets import SecretClient
credential = DefaultAzureCredential()
client = SecretClient(
vault_url="https://kv-shop-prod.vault.azure.net/",
credential=credential,
)
secret = client.get_secret("db-connection")
print("Версия секрета:", secret.properties.version)
# secret.value передаём в драйвер БД и никуда не логируем
Если приложение живёт в App Service или Azure Functions, можно вообще обойтись без SDK — достаточно ссылки на Key Vault в настройках приложения. Платформа сама сходит в хранилище от имени managed identity и подставит значение в переменную окружения:
DB_CONNECTION=@Microsoft.KeyVault(SecretUri=https://kv-shop-prod.vault.azure.net/secrets/db-connection/)
Как это работает
Под капотом — уже знакомая цепочка. Приложение просит токен у локального эндпоинта удостоверения, Entra ID выдаёт JWT для audience https://vault.azure.net, приложение идёт с этим токеном на data plane Key Vault, Key Vault проверяет подпись и спрашивает RBAC, есть ли у принципала право .../secrets/getSecret/action на это хранилище. Паролей в этой цепочке нет ни на одном шаге.
Есть два свойства Key Vault, о которых надо знать заранее, потому что они ведут себя не так, как ожидают новички.
Версии секретов
Секрет не перезаписывается — каждый secret set создаёт новую версию, старые остаются доступными по полному URI с версией. Это спасает при откате, но и создаёт ловушку: если вы вписали в конфиг ссылку с явной версией, после ротации приложение продолжит читать старый пароль. Ссылайтесь на секрет без версии, тогда всегда придёт актуальная.
Soft-delete и purge protection
Удалённое хранилище не исчезает: оно уходит в «корзину» на 90 дней (soft-delete включён принудительно и выключить его нельзя). Имя всё это время остаётся занятым. Если вы в учебном проекте удалили kv-shop-prod и пытаетесь создать его заново — получите ошибку «имя уже используется». Лечится так:
az keyvault list-deleted -o table
az keyvault purge --name kv-shop-prod --location westeurope
А вот если при создании была включена purge protection, очистить хранилище досрочно нельзя вообще никак — ни вам, ни поддержке. Имя будет мёртвым 90 дней. В проде это защита от злоумышленника, в песочнице — грабли: не включайте purge protection на учебных ресурсах.
Ротация
Ротация — это регулярная смена секрета, чтобы утёкший однажды пароль перестал работать. Key Vault даёт для неё три инструмента:
- Срок жизни. У секрета есть атрибуты
expiresиnotBefore. Просроченный секрет SDK читать откажется — это заставляет процесс ротации существовать, а не «планироваться». - События. Key Vault публикует в Event Grid события
SecretNearExpiryиSecretExpired. На них вешают Azure Function, которая генерирует новый пароль, прописывает его в базу и кладёт новую версию в хранилище. - Автоматическая ротация ключей. Для объектов типа key настраивается политика, и Azure сам создаёт новую версию по расписанию.
az keyvault secret set-attributes \
--vault-name kv-shop-prod \
--name db-connection \
--expires '2026-12-31T00:00:00Z'
Приложение при ротации не должно падать. Практический приём — кэшировать секрет в памяти на несколько минут и перечитывать по истечении TTL (а при ошибке аутентификации — принудительно). Логику кэша легко проверить на модели с «часами» вместо реального времени:
TTL = 300 # секунд
cache = {}
calls = 0
def fetch_from_vault(name):
global calls
calls += 1
return "s3cr3t-" + name
def get_secret(name, now):
entry = cache.get(name)
if entry and now - entry[1] < TTL:
return entry[0]
value = fetch_from_vault(name)
cache[name] = (value, now)
return value
for t in (0, 100, 200, 400, 500):
print(t, get_secret("db-connection", t))
print("Обращений к Key Vault:", calls)
Результат:
0 s3cr3t-db-connection
100 s3cr3t-db-connection
200 s3cr3t-db-connection
400 s3cr3t-db-connection
500 s3cr3t-db-connection
Обращений к Key Vault: 2
Пять обращений приложения превратились в два похода в хранилище. На нагруженном сервисе это разница между работой и упором в лимит.
Частые ошибки
- Чтение секрета на каждый HTTP-запрос. У Key Vault есть троттлинг (порядка 2000 операций с секретами за 10 секунд на хранилище). Превысили — получаете
429и лежачий сервис. Кэшируйте. - Секрет попал в лог. Достали значение и залогировали объект целиком «для отладки» — теперь пароль в Log Analytics, доступном половине команды. Никогда не логируйте
secret.value. - Contributor на Key Vault считают безопасным. В RBAC-режиме Contributor действительно не читает секреты напрямую — но он может выдать себе роль Secrets User или переключить хранилище на access policies. Не раздавайте Contributor на хранилище с продовыми секретами.
- Ссылка на секрет с явной версией. После ротации приложение молча продолжает жить со старым паролем — пока старая версия не истечёт и всё не сломается в самый неподходящий момент.
- Хранилище открыто всему интернету. Аутентификация есть, но публичная сетевая доступность лишняя: включите firewall и Private Endpoint (порядка 7–8 $ в месяц), чтобы хранилище отвечало только из вашей VNet.
- Деньги. Само хранилище бесплатно, операции с секретами стоят копейки (порядка 0,03 $ за 10 000 операций). Но Managed HSM — это выделенный кластер аппаратных модулей с оплатой по часам, счёт легко переваливает за 2000 $ в месяц. Не включайте его «посмотреть, что это»: для обычных секретов достаточно Key Vault уровня Standard.
Итоги
- Секрет в коде нельзя отозвать, сменить и проверить по журналу — переменные окружения проблему не решают.
- Key Vault хранит секреты (забираем), ключи (не покидают хранилище) и сертификаты; создавайте его с
--enable-rbac-authorization true. - Приложение получает секрет через managed identity и роль
Key Vault Secrets User— паролей в цепочке нет вообще. - Каждая запись создаёт новую версию; ссылайтесь на секрет без версии, иначе ротация пройдёт мимо приложения.
- Кэшируйте секрет в памяти: троттлинг Key Vault реален.
- Soft-delete держит имя 90 дней, purge protection делает досрочную очистку невозможной — на учебных стендах её не включают.