IAM: роли и сервис-аккаунты
Учимся отвечать на главный вопрос облачной безопасности: кто именно и что именно имеет право сделать с вашими ресурсами — и почему роль Owner раздавать нельзя.
IAM (Identity and Access Management) — система доступа в Google Cloud, построенная на одной формуле: кто (principal) может что (role) над чем (resource).
Запомните эту тройку — из неё выводится вообще всё. Любая настройка доступа в GCP — это «привязка» (binding): к ресурсу прикрепляется список пар «принципал → роль». Не «у пользователя есть права» (как в привычных системах), а «на ресурсе висит политика, в которой упомянут пользователь». Разворот непривычный, но именно он объясняет, почему права наследуются вниз по иерархии и почему их так легко случайно раздать слишком много.
Зачем это на практике
Три задачи, которые IAM решает каждый день:
- Приложению нужен доступ к данным. Сервису на Cloud Run надо читать бакет и секрет — и только их. Не всю базу, не биллинг.
- Коллеге нужен доступ к проекту. Аналитику — только чтение BigQuery, стажёру — только логи, DevOps — деплой.
- Ошибка не должна быть фатальной. Если ключ приложения утечёт, злоумышленник должен получить возможность прочитать один бакет, а не удалить проект.
Сам IAM бесплатен — платите вы за ресурсы, а не за права на них.
Три части формулы
Кто: принципал
Принципал — это идентичность, которой выдают права. Записывается всегда с префиксом типа:
| Тип | Как выглядит | Когда используется |
user: | user:anna@example.com | живой человек |
group: | group:backend@example.com | команда — так и надо выдавать права людям |
serviceAccount: | serviceAccount:api@my-proj.iam.gserviceaccount.com | приложение, скрипт, CI |
allUsers | без префикса | вообще все, включая анонимов — только для публичных сервисов |
Что: роль
Роль — это именованный набор разрешений (permissions). Разрешение всегда вида сервис.ресурс.действие: storage.objects.get, run.services.update, secretmanager.versions.access. Напрямую разрешения не выдают — только через роли. Ролей три сорта:
- Базовые (basic):
roles/viewer,roles/editor,roles/owner. Наследие ранней эпохи GCP, гигантские по охвату. В продакшене — не использовать. - Предопределённые (predefined): сотни узких ролей вида
roles/storage.objectViewer,roles/cloudsql.client,roles/logging.viewer. Это ваш основной инструмент. - Кастомные (custom): собираете свой набор разрешений, если ни одна предопределённая не подходит. Нужны редко.
Над чем: ресурс и наследование
Политику можно повесить на организацию, папку, проект или на конкретный ресурс (бакет, секрет, топик Pub/Sub). Права наследуются вниз и суммируются: если человеку выдали roles/viewer на проект, он видит все бакеты в нём — снять это правом на уровне бакета нельзя. Отзыв работает только там, где выдача.
Отсюда золотое правило: выдавайте права на самом узком уровне, который решает задачу. Нужен доступ к одному бакету — привязывайте роль к бакету, а не к проекту.
# Роль на весь проект — широко
gcloud projects add-iam-policy-binding my-proj \
--member="group:analytics@example.com" \
--role="roles/bigquery.dataViewer"
# Роль на один бакет — узко, так лучше
gcloud storage buckets add-iam-policy-binding gs://my-reports \
--member="serviceAccount:api@my-proj.iam.gserviceaccount.com" \
--role="roles/storage.objectViewer"
# Посмотреть, кто что может в проекте
gcloud projects get-iam-policy my-proj --format=json
Сервис-аккаунт: паспорт для приложения
Сервис-аккаунт (service account) — это принципал не для человека, а для кода. У него есть e-mail-подобный идентификатор (api@my-proj.iam.gserviceaccount.com), ему выдают роли, и от его имени приложение ходит в API Google.
# 1. Создаём отдельный сервис-аккаунт под конкретный сервис
gcloud iam service-accounts create api-runner \
--display-name="Backend API (Cloud Run)"
# 2. Выдаём ровно то, что нужно: читать секрет и писать в Pub/Sub
gcloud projects add-iam-policy-binding my-proj \
--member="serviceAccount:api-runner@my-proj.iam.gserviceaccount.com" \
--role="roles/secretmanager.secretAccessor"
gcloud projects add-iam-policy-binding my-proj \
--member="serviceAccount:api-runner@my-proj.iam.gserviceaccount.com" \
--role="roles/pubsub.publisher"
# 3. Привязываем сервис-аккаунт к сервису — ключей НЕ создаём
gcloud run deploy api \
--image=europe-docker.pkg.dev/my-proj/app/api:1.0 \
--service-account=api-runner@my-proj.iam.gserviceaccount.com \
--region=europe-west1
Обратите внимание: мы не скачивали JSON-ключ. Это принципиально. Когда сервис-аккаунт привязан к Cloud Run, VM или Cloud Functions, код получает токены автоматически — библиотеки Google берут их из метаданных среды. Механизм называется ADC (Application Default Credentials), и в коде он выглядит как «магия без паролей»:
from google.cloud import storage
# Никаких ключей и паролей в коде: клиент сам найдёт учётные данные
# через ADC — из привязанного сервис-аккаунта (в облаке)
# или из `gcloud auth application-default login` (на ноутбуке).
client = storage.Client()
bucket = client.bucket("my-reports")
blob = bucket.blob("2026-07/summary.csv")
print(blob.download_as_text()[:200])
Если у сервис-аккаунта нет нужной роли, вы получите не «пустой результат», а честную ошибку — по ней сразу видно, какого разрешения не хватает:
google.api_core.exceptions.Forbidden: 403 GET https://storage.googleapis.com/...
api-runner@my-proj.iam.gserviceaccount.com does not have storage.objects.get access
to the Google Cloud Storage object.
Принцип наименьших привилегий на пальцах
Модель IAM легко смоделировать обычным словарём: политика — это «принципал → список ролей», роль — «список разрешений», а проверка доступа сводится к поиску разрешения в объединении ролей.
policy = {
"user:anna@example.com": ["roles/viewer"],
"serviceAccount:api@demo.iam.gserviceaccount.com": [
"roles/storage.objectViewer",
"roles/secretmanager.secretAccessor",
],
}
role_perms = {
"roles/viewer": ["storage.objects.get", "storage.objects.list", "run.services.get"],
"roles/storage.objectViewer": ["storage.objects.get", "storage.objects.list"],
"roles/secretmanager.secretAccessor": ["secretmanager.versions.access"],
}
def can(principal, permission):
for role in policy.get(principal, []):
if permission in role_perms.get(role, []):
return True
return False
checks = [
("serviceAccount:api@demo.iam.gserviceaccount.com", "secretmanager.versions.access"),
("serviceAccount:api@demo.iam.gserviceaccount.com", "storage.objects.delete"),
("user:anna@example.com", "storage.objects.get"),
]
for principal, perm in checks:
verdict = "ALLOW" if can(principal, perm) else "DENY"
print(f"{verdict:5} {principal} -> {perm}")
Результат:
ALLOW serviceAccount:api@demo.iam.gserviceaccount.com -> secretmanager.versions.access
DENY serviceAccount:api@demo.iam.gserviceaccount.com -> storage.objects.delete
ALLOW user:anna@example.com -> storage.objects.get
Сервис-аккаунт может прочитать объект, но не может его удалить — потому что storage.objects.delete не входит ни в одну из его ролей. Ровно так и должен выглядеть здоровый доступ: утёкший токен приводит к утечке данных, а не к их уничтожению.
Как это работает
Когда приложение вызывает API Google, оно предъявляет короткоживущий OAuth-токен (обычно живёт около часа). На VM и в Cloud Run токен выдаёт metadata server — внутренний служебный адрес 169.254.169.254, доступный только изнутри инстанса. Библиотеки Google дергают его сами; вы этого не видите.
Дальше запрос попадает в IAM, и тот собирает эффективную политику: складывает все привязки от организации через папки и проект вниз до самого ресурса, разворачивает роли в разрешения и ищет нужное. Есть разрешение — 200, нет — 403 PERMISSION_DENIED. Отдельно поверх этого могут действовать deny-политики — жёсткие запреты, которые перебивают любые allow.
Отладить «почему 403» помогает Policy Troubleshooter в консоли: вводите принципала, ресурс и разрешение — он показывает, какая привязка (не) сработала. Полезен и Recommender: он смотрит, какими правами принципал реально пользовался за 90 дней, и предлагает урезать роль.
Частые ошибки
- Роль Owner «чтобы точно заработало».
roles/owner— это не «админ проекта», это «человек, который может выдать себе что угодно, изменить биллинг, удалить проект и выгнать вас из него». Утёкший Owner = потерянный проект. Людям — узкие предопределённые роли, сервис-аккаунтам — Owner никогда. roles/editorкак «безопасная середина». Editor умеет менять почти всё: пересоздать базу, удалить бакет, переписать Cloud Run. Не сильно лучше Owner.- Дефолтный сервис-аккаунт Compute Engine. Аккаунт
PROJECT_NUMBER-compute@developer.gserviceaccount.comисторически получал рольEditorна весь проект — и любая VM, поднятая «по умолчанию», ходит в облако с этими правами. Всегда создавайте отдельный сервис-аккаунт под каждый сервис. - JSON-ключи сервис-аккаунтов. Скачанный ключ — это вечный пароль в текстовом файле. Его коммитят в git, кладут в Docker-образ, шлют в мессенджере. Боты сканируют публичные репозитории и находят такие ключи за минуты. Внутри GCP ключ вообще не нужен (привязывайте сервис-аккаунт), а для внешних CI есть Workload Identity Federation.
- Права на человека, а не на группу. Пять разработчиков — пять наборов привязок, и при увольнении вы обязательно про кого-то забудете. Выдавайте роли на
group:. - Права выданы на проект, хотя нужен один ресурс. Начинайте с уровня ресурса и поднимайтесь выше, только если действительно нужно.
allUsersна бакете. Одна привязкаallUsers+roles/storage.objectViewer— и ваши файлы читает весь интернет. Заодно вы платите за их egress-трафик.
Итоги
- Формула IAM: кто (principal) → что может (role) → над чем (resource). Права привязываются к ресурсу, а не «лежат у пользователя».
- Права наследуются вниз по иерархии организация → папка → проект → ресурс и суммируются; отозвать их можно только там, где выдали.
- Базовые роли (Owner/Editor/Viewer) — только для песочницы. В работе — предопределённые узкие роли.
- У каждого приложения — свой сервис-аккаунт с минимальным набором ролей.
- Внутри GCP не создавайте JSON-ключи: привязанный сервис-аккаунт + ADC дают короткоживущие токены автоматически.
- 403 — это не баг, а сообщение о том, какого именно разрешения не хватает. Читайте текст ошибки, он называет permission прямо.