SecretManagerServiceClient вызывает 403 Permission denied CONSUMER_INVALID в Google App Engine, но `gcloud secrets versions access` работает

Фон:

  • Я пытаюсь развернуть приложение Django в стандартной среде Google App Engine (GAE) в среде выполнения python39
  • .
  • Конфигурация базы данных хранится в секретной версии Secret Manager, аналогично учебнику Google по GAE Django (ссылка)
  • Приложение запускается от имени управляемой пользователем учетной записи службы server@myproject.iam.gserviceaccount.com, которая имеет соответствующие разрешения для доступа к секрету, что можно подтвердить с помощью gcloud secret versions access
  • .

Проблема:

  • В модуле Django settings.py, когда я пытаюсь получить доступ к секрету, используя google.cloud.secretmanager.SecretManagerServiceClient.access_secret_version(...), я получаю следующую CONSUMER_INVALID ошибку:
google.api_core.exceptions.PermissionDenied: 403 Permission denied on resource project myproject. [links {
    description: "Google developer console API key"
    url: "https://console.developers.google.com/project/myproject/apiui/credential"
  }
  , reason: "CONSUMER_INVALID"
  domain: "googleapis.com"
  metadata {
    key: "service"
    value: "secretmanager.googleapis.com"
  }
  metadata {
    key: "consumer"
    value: "projects/myproject"
  }

Моя отладка

  • Я не могу воспроизвести приведенную выше ошибку вне GAE;
    • Я могу подтвердить, что SA может получить доступ к секрету:
gcloud secrets versions access latest --secret=server_env --project myproject \
  --impersonate-service-account=server@myproject.iam.gserviceaccount.com
WARNING: This command is using service account impersonation. All API calls will be executed as [server@myproject.iam.gserviceaccount.com].

DATABASE_URL='postgres://django:...'
SECRET_KEY='...'
  • Я также подтвердил, что запускаю приложение django локально с имперсонацией учетной записи сервиса и делаю вышеуказанные вызовы access_secret_version(...)

  • В отчаянии я даже создал API ключ для проекта и жестко закодировал его в моем settings.py файле, и это также вызывает ту же ошибку

  • Я подтвердил следующие настройки в проекте:

    • приложение работает с использованием правильного SA, управляемого пользователем
    • вызов access_secret_version осуществляется с правильным SA (т.е. учетные данные извлекаются из среды GAE правильно)
    • в проекте включена услуга secretmanager.googleapis.com, включен биллинг и активен биллинговый счет
  • Если у вас есть какие-либо предложения по конфигурации или методу, чтобы помочь отладить это, я буду очень признателен!

    Релевантные фрагменты кода

    app.yaml

    service_account: server@myproject.iam.gserviceaccount.com
    
    runtime: python39
    
    handlers:
    # This configures Google App Engine to serve the files in the app's static
    # directory.
    - url: /_static
      static_dir: _static/
    
    # This handler routes all requests not caught above to your main app. It is
    # required when static routes are defined, but can be omitted (along with
    # the entire handlers section) when there are no static files defined.
    - url: /.*
      script: auto
    
    env_variables:
      ...
    
    inbound_services:
      - mail
      - mail_bounce
    
    app_engine_apis: true
    

    Создание учетной записи и разрешения

    • SA создается с помощью Terraform, как показано ниже
    • .
    • (SA не имеет роли roles/secretmanager.secretAccessor, но имеет привязку IAM непосредственно к самому секрету)
    resource "google_service_account" "frontend_server" {
      project      = google_project.project.project_id
      account_id   = "server"
      display_name = "Frontend Server Service Account"
    }
    
    resource "google_project_iam_member" "frontend_server" {
      depends_on = [
        google_service_account.frontend_server,
      ]
      for_each = toset([
        "roles/appengine.serviceAgent",
        "roles/cloudsql.client",
        "roles/cloudsql.instanceUser",
        "roles/secretmanager.viewer",
        "roles/storage.objectViewer",
      ])
      project  = google_project.project.project_id
      role     = each.key
      member   = "serviceAccount:${google_service_account.frontend_server.email}"
    }
    

    Django settings.py

    Ниже показаны соответствующие разделы приложения settings.py; access_secret_version поднимает

    import logging
    import environ
    from google.cloud import secretmanager
    import google.auth
    
    # Load secrets from secret manager; the client is auth'd by SA IAM policies
    credentials, project = google.auth.default(
      scopes=['https://www.googleapis.com/auth/cloud-platform']
    )
    secretmanager_client = secretmanager.SecretManagerServiceClient(credentials=credentials)
    
    # Load the database connection string into the environment
    secrets = [
      f"projects/{GOOGLE_CLOUD_PROJECT}/secrets/server_env/versions/latest",
    ]
    for name in secrets:
      try:
        logging.info(f"Reading secret {name} into django settings module...")
        payload = secretmanager_client.access_secret_version(name=name).payload.data.decode("UTF-8")
        env.read_env(io.StringIO(payload))
      except Exception as e:
        logging.error(f"Encountered error when accessing secret {name}: {e}")
        logging.error(f"Client credentials during error: {secretmanager_client._transport._credentials.__dict__}")
        raise e from None
    

    Вам предоставлена неправильная роль. Посмотрите страницу документации .

      Роль
    • Secret Viewer позволяет просматривать секрет и версии, но НЕ содержимое.
    • Роль Secret Accessor позволяет вам получить доступ к содержимому секретной версии.

    Вздох. Это был ужасный случай трудночитаемой опечатки в файле app.yaml. В проекте была мнемоническая подстрока с очень похожими буквами, которую я неправильно набрал и просто не заметил.

    FWIW, если кто-то столкнулся с подобной ошибкой, вы можете, по крайней мере, избежать этого источника ошибок:

    • Я передавал строку префикса проекта и строку окружения через app.yaml файл, а затем settings.py файл я конкатенировал эти строки, чтобы сделать проект
    • При запуске gcloud app deploy (правильная) конкатенированная строка проекта также существовала в моей переменной оболочки $GOOGLE_CLOUD_PROJECT, поэтому развертывание произошло правильно с правильным идентификатором проекта
    • .
    • Однако я удалил код конкатенации в settings.py в пользу переменной env GOOGLE_CLOUD_PROJECT, которая всегда присутствует в GAE (docs)
    • .

    TLDR: сухость - это хорошо...

    Вернуться на верх