Secret Manager
Пароль базы данных не должен лежать ни в коде, ни в git, ни в переменных окружения на ноутбуке разработчика. Разбираемся, куда его класть в Google Cloud.
Secret Manager — управляемое хранилище секретов в GCP: пароли, API-ключи и токены лежат зашифрованными, доступ к каждому выдаётся через IAM, а каждое обращение попадает в аудит-лог.
История, которую видел каждый: пароль от продовой базы попал в config.py, файл ушёл в git, репозиторий стал публичным «на пять минут» — и через полчаса в базе майнер. Проблема не в невнимательности, а в том, что секрету физически негде жить, если для него нет специального места. Secret Manager — это такое место.
Зачем отдельный сервис
Кажется, что достаточно переменной окружения. Но у «просто env-переменной» есть неприятные свойства:
- Значение всё равно откуда-то берётся — из
.env, из скрипта деплоя, из настроек CI. И вот эти файлы и живут в git. - Сменить пароль нельзя без передеплоя всех, кто его знает — а кто именно знает, никто не помнит.
- Нет ответа на вопрос «кто и когда читал этот пароль». А после инцидента это первый вопрос.
Secret Manager закрывает все три пункта: одно место хранения, версионирование, доступ по IAM и аудит. Цена символическая: платят за активную версию секрета в месяц (порядка $0,06) и за операции доступа (порядка $0,03 за 10 000 обращений). Есть и бесплатный уровень — несколько активных версий и десятки тысяч обращений в месяц. Для типичного приложения это единицы центов, но об этом ниже есть подвох.
Секрет и его версии
Это главная идея сервиса. Секрет — это контейнер с именем и настройками доступа. Значение лежит не в секрете, а в его версии. Версии нумеруются с единицы, они неизменяемы: поменять значение нельзя, можно только добавить новую версию.
# Не забудьте включить API — шаг, на котором спотыкаются все
gcloud services enable secretmanager.googleapis.com
# 1. Создаём контейнер секрета (значения в нём пока нет)
gcloud secrets create db-password --replication-policy=automatic
# 2. Кладём значение — это версия 1.
# ВНИМАНИЕ на -n у echo: без него в пароль попадёт символ перевода строки!
echo -n "S3cret-P@ss" | gcloud secrets versions add db-password --data-file=-
# 3. Читаем значение (нужна роль secretAccessor)
gcloud secrets versions access latest --secret=db-password
# 4. Ротация: добавили новую версию, старую отключили
echo -n "N3w-P@ss-2026" | gcloud secrets versions add db-password --data-file=-
gcloud secrets versions disable 1 --secret=db-password
gcloud secrets versions list db-password
Результат:
NAME STATE CREATED
2 enabled 2026-07-14T09:12:04
1 disabled 2026-07-11T18:40:55
Состояний у версии три: enabled (читается), disabled (временно закрыта — так проверяют, не сломается ли что-то после ротации) и destroyed (значение стёрто безвозвратно). Алиас latest всегда указывает на самую свежую включённую версию.
Про echo без -n стоит сказать отдельно, потому что на эти грабли наступают буквально все:
raw_with_newline = "S3cret-P@ss\n" # так сохранит echo БЕЗ -n
raw_clean = "S3cret-P@ss" # так сохранит echo -n
for name, value in [("echo без -n", raw_with_newline), ("echo -n ", raw_clean)]:
print(f"{name}: {value!r}, длина = {len(value)}")
print("Пароли совпадают?", raw_with_newline == raw_clean)
Результат:
echo без -n: 'S3cret-P@ss\n', длина = 12
echo -n : 'S3cret-P@ss', длина = 11
Пароли совпадают? False
База ответит password authentication failed, а пароль в консоли будет выглядеть абсолютно правильным. Часы отладки на один пропущенный флаг.
Доступ из приложения
Секрет читает не «приложение», а сервис-аккаунт, от имени которого оно работает. Роль называется roles/secretmanager.secretAccessor, и выдавать её надо на конкретный секрет, а не на проект:
gcloud secrets add-iam-policy-binding db-password \
--member="serviceAccount:api-runner@my-proj.iam.gserviceaccount.com" \
--role="roles/secretmanager.secretAccessor"
Дальше есть два пути. Первый — декларативный, и он лучше: пусть секрет подставит сама платформа. Cloud Run умеет пробросить версию секрета как переменную окружения или как файл — код о Secret Manager вообще не знает:
# Секрет как переменная окружения DB_PASSWORD
gcloud run deploy api \
--image=europe-docker.pkg.dev/my-proj/app/api:1.0 \
--service-account=api-runner@my-proj.iam.gserviceaccount.com \
--set-secrets=DB_PASSWORD=db-password:latest \
--region=europe-west1
# Или как файл, смонтированный в контейнер (безопаснее: не утечёт в дамп env)
gcloud run deploy api \
--image=europe-docker.pkg.dev/my-proj/app/api:1.0 \
--set-secrets=/secrets/db=db-password:2 \
--region=europe-west1
Второй путь — читать секрет из кода через клиентскую библиотеку. Нужен, когда секрет надо перечитывать без передеплоя (например, после ротации) или когда секретов много и они выбираются динамически:
import os
from functools import lru_cache
from google.cloud import secretmanager
PROJECT_ID = os.environ["GOOGLE_CLOUD_PROJECT"]
@lru_cache(maxsize=32) # кэшируем: каждый access — платная операция и лишние 50 мс
def get_secret(name: str, version: str = "latest") -> str:
client = secretmanager.SecretManagerServiceClient()
path = f"projects/{PROJECT_ID}/secrets/{name}/versions/{version}"
response = client.access_secret_version(request={"name": path})
return response.payload.data.decode("UTF-8")
db_password = get_secret("db-password")
print("Пароль получен, длина:", len(db_password)) # само значение НИКОГДА не логируем
Аутентификация тут снова через ADC: сервис-аккаунт привязан к Cloud Run, токен приезжает из метаданных, ключей нет. На ноутбуке то же самое заработает после gcloud auth application-default login.
Как это работает
Значение версии шифруется на стороне Google (по умолчанию — ключами Google; при желании можно подключить свои через Cloud KMS) и реплицируется. Политика репликации automatic раскладывает секрет по регионам сама — это разумный выбор по умолчанию; user-managed нужна, когда по требованиям регулятора данные не должны покидать конкретные регионы.
Каждое обращение access_secret_version — это вызов API: проверка IAM, расшифровка, ответ. Отсюда два следствия:
- Каждый вызов стоит денег и времени. Читать секрет на каждый HTTP-запрос — плохая идея: получите лишние миллисекунды задержки и счёт за миллионы операций. Читайте один раз при старте (или кэшируйте, как в примере выше).
- Каждый вызов виден в аудите. В Cloud Logging остаётся запись: кто, когда и какую версию читал. Это и есть та самая ценность «кто трогал пароль», ради которой всё затевалось.
Частые ошибки
echoбез-n. Лишний\nв конце пароля. Симптом — «пароль правильный, но не подходит». Лечится-nили чтением из файла.- Роль
secretAccessorвыдана на весь проект. Тогда сервис-аккаунт фронтенда читает и ключ платёжной системы, и пароль от прода. Права — на конкретный секрет. - Секрет попал в лог.
print(db_password)или подробный вывод ошибки — и секрет уже в Cloud Logging, откуда его видит любой, у кого естьlogging.viewer. Логируйте факт получения, но не значение. - Секрет в Docker-образе или в build-аргументах. Слои образа хранят всё, включая удалённые файлы. Секрет должен появляться в контейнере во время выполнения, а не при сборке.
- Терраформ-стейт с секретами. Если создавать версию секрета через Terraform, значение окажется в открытом виде в
terraform.tfstate. Кладите значение отдельно (черезgcloudили консоль), а в IaC описывайте только сам контейнер секрета и права на него. - Уничтожили версию, которая ещё используется.
destroyнеобратим. Правильный порядок ротации: добавить новую версию → передеплоить/перезапустить потребителей →disableстарую → подождать → только потомdestroy. - Забыли включить API.
PERMISSION_DENIED: Secret Manager API has not been used in project ...— это не про IAM, это проgcloud services enable secretmanager.googleapis.com. - Ошибка, стоящая денег: тысяча секретов «на всякий случай» и чтение секрета на каждый запрос в нагруженном сервисе. Хранение копеечное, но платные операции доступа при 10 млн запросов в месяц превращаются во вполне заметную строку счёта.
Итоги
- Секрет — это контейнер, значение живёт в неизменяемой версии; изменить значение = добавить новую версию.
- Доступ выдаётся ролью
roles/secretmanager.secretAccessorна конкретный секрет и конкретному сервис-аккаунту. - Лучший способ доставить секрет в Cloud Run —
--set-secrets: платформа подставит его сама, код о Secret Manager не знает. - В коде читайте секрет один раз при старте и кэшируйте: каждый
access— платная операция и лишняя задержка. - Ротация без простоя: новая версия → перезапуск потребителей →
disableстарой → и только потомdestroy. echo -n. Всегда-n.