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 прямо.
Проверьте себя
1. Приложение на Cloud Run должно читать один бакет. Какой вариант доступа правильный?
AВыдать сервис-аккаунту приложения roles/editor на проект — так точно заработает
BСоздать отдельный сервис-аккаунт и дать ему roles/storage.objectViewer на этот бакет
CСкачать JSON-ключ владельца проекта и положить его в Docker-образ
DСделать бакет публичным через allUsers, чтобы не возиться с ролями
2. Пользователю выдали roles/viewer на уровне проекта. Можно ли отобрать у него доступ к одному конкретному бакету, изменив политику этого бакета?
AДа, привязка на уровне бакета перекрывает проектную
BНет: права наследуются вниз и суммируются, отозвать их можно только там, где выдали
CДа, но только если бакет в другом регионе
DНет, но роль автоматически перестанет действовать через 90 дней