BigQueryデータセットのパブリックアクセス制限の設定について

このブログシリーズ 「クラウドセキュリティ 実践集」 では、一般的なセキュリティ課題を取り上げ、「なぜ危険なのか?」 というリスクの解説から、 「どうやって直すのか?」 という具体的な修復手順(コンソール、gcloud CLI、Terraformなど)まで、分かりやすく解説します。

この記事では、BigQueryデータセットがパブリックアクセス可能な権限設定で構成されている問題について、リスクと対策を解説します。

ポリシーの説明

BigQueryデータセットへのパブリックアクセス(allUsersまたはallAuthenticatedUsers)を制限し、機密データの露出を防止します。BigQueryデータセットに「allUsers」や「allAuthenticatedUsers」といったパブリックアクセスを許可するIAMメンバーが設定されていると、インターネット上の誰でもデータセット内のビジネスデータにアクセスできる状態となります。BigQueryには通常、企業の重要な分析データや機密情報が格納されているため、この設定は深刻なセキュリティリスクとなります。

修復方法

コンソールでの修復手順

Google Cloud コンソールを使用して、BigQueryデータセットのパブリックアクセスを制限します。

前提条件

  • データセットの権限を表示するにはbigquery.datasets.get権限が必要です
  • データセットの権限を更新するにはbigquery.datasets.update権限が必要です
  • カスタムロールを使用している場合は、必要な権限が含まれていることを確認してください
  1. Google Cloud BigQueryコンソールにアクセス
  2. 対象データセットの選択
    • 左側のエクスプローラーペインでプロジェクトを展開
    • パブリックアクセスが設定されているデータセットをクリックして選択
  3. 権限設定画面へのアクセス
    • データセット情報パネルで「共有」ボタンをクリック
    • または、3点メニュー(⋮)から「権限を開く」を選択
  4. パブリックアクセス権限の特定
    • 権限パネルで以下のプリンシパルを探します:
      • allUsers: 認証済み・未認証を問わず、インターネット上のすべてのユーザー
      • allAuthenticatedUsers: Googleアカウントでサインインできるすべてのユーザー(組織外も含む)
    • これらのプリンシパルに付与されているロールを確認(通常は「BigQuery データ閲覧者」など)
  5. パブリックアクセス権限の削除
    • allUsersまたはallAuthenticatedUsersの行にカーソルを合わせる
    • 右側に表示される削除アイコン(ゴミ箱)をクリック
    • 確認ダイアログで「削除」をクリック
    • すべてのパブリックアクセス権限を削除するまで繰り返す
  6. 適切なアクセス権限の設定
    • 「プリンシパルを追加」をクリック
    • 新しいプリンシパルに適切なメンバーを入力:
      • 組織ドメイン: @yourdomain.com
      • 特定のユーザー: user@yourdomain.com
      • サービスアカウント: service-account@project-id.iam.gserviceaccount.com
      • Google グループ: group@yourdomain.com
    • ロールを選択:
      • BigQuery データ閲覧者: 読み取り専用
      • BigQuery データ編集者: 読み書き可能
      • BigQuery データオーナー: 完全な管理権限
    • 「保存」をクリック
  7. 変更の確認と監査
    • 権限パネルで、パブリックアクセスが完全に削除されたことを確認
    • Cloud Loggingで権限変更の監査ログを確認
    • 必要に応じて、他のデータセットも同様に確認・修正

Terraformでの修復手順

BigQueryデータセットのパブリックアクセスを防止するTerraformコードと、主要な修正ポイントを説明します。

# プロバイダー設定
terraform {
  required_providers {
    google = {
      source  = "hashicorp/google"
      version = "~> 5.0"
    }
  }
}

# セキュアなBigQueryデータセットの作成
resource "google_bigquery_dataset" "secure_dataset" {
  dataset_id                  = "${var.environment}_secure_analytics"
  friendly_name               = "Secure Analytics Dataset - ${var.environment}"
  description                 = "機密データを含む分析用データセット(アクセス制限あり)"
  location                    = var.location
  delete_contents_on_destroy  = false  # 誤削除防止

  # データ保持期間の設定(90日後に自動削除)
  default_table_expiration_ms = var.table_expiration_days * 24 * 60 * 60 * 1000

  # パーティション有効期限(7日)
  default_partition_expiration_ms = 7 * 24 * 60 * 60 * 1000

  # デフォルトの暗号化設定(CMEK)
  default_encryption_configuration {
    kms_key_name = google_kms_crypto_key.bigquery_key.id
  }

  # アクセス制御の明示的な設定(パブリックアクセスなし)
  # 重要: allUsersやallAuthenticatedUsersは絶対に使用しない

  # データセットオーナー(管理者)
  access {
    role          = "OWNER"
    user_by_email = google_service_account.dataset_owner.email
  }

  # データエンジニア(書き込み権限)
  access {
    role          = "WRITER"
    user_by_email = google_service_account.data_engineer.email
  }

  # 組織ドメインユーザー(読み取り専用)
  access {
    role   = "READER"
    domain = var.organization_domain  # 例: "example.com"
  }

  # 特定のGoogleグループ(アナリストチーム)
  access {
    role           = "READER"
    group_by_email = "data-analysts@${var.organization_domain}"
  }

  # ラベル(データガバナンス)
  labels = {
    environment         = var.environment
    data_classification = "confidential"
    compliance         = "gdpr-pci"
    cost_center        = var.cost_center
    managed_by         = "terraform"
  }

  lifecycle {
    prevent_destroy = true  # 本番環境では削除防止
  }
}

# IAMバインディング(より細かい権限制御)
# データ閲覧者ロール(特定ユーザー)
resource "google_bigquery_dataset_iam_member" "data_viewer" {
  dataset_id = google_bigquery_dataset.secure_dataset.dataset_id
  role       = "roles/bigquery.dataViewer"
  member     = "user:analyst@${var.organization_domain}"

  condition {
    title       = "weekday_business_hours"
    description = "平日の営業時間内のみアクセス可能"
    expression  = <<-EOT
      request.time.getDayOfWeek("Asia/Tokyo") >= 1 &&
      request.time.getDayOfWeek("Asia/Tokyo") <= 5 &&
      request.time.getHours("Asia/Tokyo") >= 9 &&
      request.time.getHours("Asia/Tokyo") <= 18
    EOT
  }
}

# ETLサービスアカウント(書き込み権限)
resource "google_bigquery_dataset_iam_member" "etl_writer" {
  dataset_id = google_bigquery_dataset.secure_dataset.dataset_id
  role       = "roles/bigquery.dataEditor"
  member     = "serviceAccount:${google_service_account.etl_service.email}"
}

# ジョブユーザーロール(クエリ実行権限)
resource "google_bigquery_dataset_iam_member" "job_user" {
  dataset_id = google_bigquery_dataset.secure_dataset.dataset_id
  role       = "roles/bigquery.jobUser"
  member     = "group:data-scientists@${var.organization_domain}"
}

# 認可ビューのための別データセット
resource "google_bigquery_dataset" "authorized_views" {
  dataset_id    = "${var.environment}_authorized_views"
  friendly_name = "Authorized Views - ${var.environment}"
  description   = "制限されたビューを提供するデータセット"
  location      = var.location

  # ビューデータセットへのアクセス(より広範囲)
  access {
    role   = "READER"
    domain = var.organization_domain
  }
}

# 認可ビューの設定
resource "google_bigquery_dataset_access" "authorized_view" {
  dataset_id = google_bigquery_dataset.secure_dataset.dataset_id
  view {
    project_id = var.project_id
    dataset_id = google_bigquery_dataset.authorized_views.dataset_id
    table_id   = google_bigquery_table.filtered_view.table_id
  }
}

# フィルタリングされたビューの作成
resource "google_bigquery_table" "filtered_view" {
  dataset_id = google_bigquery_dataset.authorized_views.dataset_id
  table_id   = "customer_summary_view"

  view {
    query = <<-EOT
      SELECT
        customer_id,
        country,
        total_purchases,
        last_purchase_date
      FROM
        `${var.project_id}.${google_bigquery_dataset.secure_dataset.dataset_id}.customers`
      WHERE
        -- 個人情報を除外し、集計データのみ公開
        deletion_requested = FALSE
        AND country IN ('JP', 'US', 'EU')
    EOT
    use_legacy_sql = false
  }
}

# VPCサービスコントロールによる追加保護
resource "google_access_context_manager_service_perimeter" "bigquery_perimeter" {
  parent = "accessPolicies/${var.access_policy_id}"
  name   = "accessPolicies/${var.access_policy_id}/servicePerimeters/bigquery_${var.environment}"
  title  = "BigQuery Security Perimeter - ${var.environment}"

  status {
    restricted_services = [
      "bigquery.googleapis.com",
      "bigquerystorage.googleapis.com",
      "bigqueryreservation.googleapis.com"
    ]

    resources = [
      "projects/${data.google_project.current.number}",
    ]

    # アクセスレベル(企業ネットワークからのみ)
    access_levels = [
      google_access_context_manager_access_level.corp_network.name,
    ]

    # VPCアクセシブルサービス
    vpc_accessible_services {
      enable_restriction = true
      allowed_services   = ["bigquery.googleapis.com"]
    }
  }
}

# アクセスレベルの定義
resource "google_access_context_manager_access_level" "corp_network" {
  parent = "accessPolicies/${var.access_policy_id}"
  name   = "accessPolicies/${var.access_policy_id}/accessLevels/corp_network_${var.environment}"
  title  = "Corporate Network Access"

  basic {
    conditions {
      ip_subnetworks = var.corporate_ip_ranges

      # 追加のデバイスポリシー
      device_policy {
        require_screen_lock     = true
        require_admin_approval  = true
        allowed_encryption_statuses = ["ENCRYPTED"]

        os_constraints {
          os_type = "DESKTOP_WINDOWS"
          minimum_version = "10.0.0"
        }

        os_constraints {
          os_type = "DESKTOP_MAC"
          minimum_version = "12.0.0"
        }
      }
    }
  }
}

# データポリシーによる列レベルセキュリティ
resource "google_bigquery_datapolicy_data_policy" "pii_masking" {
  location         = var.location
  data_policy_id   = "pii_masking_policy"
  policy_tag       = google_data_catalog_policy_tag.pii.name
  data_policy_type = "DATA_MASKING_POLICY"

  data_masking_policy {
    predefined_expression = "SHA256"
  }
}

# ポリシータグの作成
resource "google_data_catalog_policy_tag" "pii" {
  taxonomy     = google_data_catalog_taxonomy.data_classification.id
  display_name = "PII"
  description  = "個人識別情報"
}

# 監査ログシンク(BigQuery専用)
resource "google_logging_project_sink" "bigquery_audit" {
  name        = "bigquery-audit-sink"
  destination = "bigquery.googleapis.com/projects/${var.project_id}/datasets/${google_bigquery_dataset.audit_logs.dataset_id}"
  project     = var.project_id

  filter = <<-EOT
    protoPayload.serviceName="bigquery.googleapis.com"
    AND (
      protoPayload.methodName="datasetservice.update"
      OR protoPayload.methodName="tableservice.update"
      OR protoPayload.authorizationInfo.permission="bigquery.datasets.setIamPolicy"
    )
  EOT

  unique_writer_identity = true

  bigquery_options {
    use_partitioned_tables = true
  }
}

# 組織ポリシー(ドメイン制限)
resource "google_organization_policy" "domain_restricted_sharing" {
  count = var.organization_id != "" ? 1 : 0

  org_id     = var.organization_id
  constraint = "iam.allowedPolicyMemberDomains"

  list_policy {
    allow {
      values = [var.customer_id]  # C0xxxxxxx形式のカスタマーID
    }
  }
}

# カスタムクォータ(コスト管理)
resource "google_bigquery_reservation" "slot_commitment" {
  count = var.enable_reservations ? 1 : 0

  name     = "${var.environment}-reservation"
  location = var.location

  slot_capacity     = var.slot_capacity
  commitment_plan   = "FLEX"  # FLEX, MONTHLY, ANNUAL
  edition          = "STANDARD"  # STANDARD, ENTERPRISE, ENTERPRISE_PLUS

  autoscale {
    max_slots = var.max_slots
  }
}

# 変数定義
variable "environment" {
  description = "環境名"
  type        = string
  validation {
    condition     = contains(["dev", "stg", "prd"], var.environment)
    error_message = "環境は dev, stg, prd のいずれかである必要があります。"
  }
}

variable "location" {
  description = "BigQueryデータセットのロケーション"
  type        = string
  default     = "asia-northeast1"
}

variable "organization_domain" {
  description = "組織のドメイン名"
  type        = string
}

variable "corporate_ip_ranges" {
  description = "企業ネットワークのIPレンジ"
  type        = list(string)
}

variable "table_expiration_days" {
  description = "テーブルの自動削除までの日数"
  type        = number
  default     = 90
}

variable "customer_id" {
  description = "Google Cloud カスタマーID (C0xxxxxxx)"
  type        = string
}

# 出力
output "dataset_id" {
  description = "作成されたデータセットID"
  value       = google_bigquery_dataset.secure_dataset.dataset_id
}

output "dataset_url" {
  description = "BigQueryコンソールでのデータセットURL"
  value       = "<https://console.cloud.google.com/bigquery?project=${var.project_id}&ws=!1m4!1m3!3m2!1s${var.project_id}!2s${google_bigquery_dataset.secure_dataset.dataset_id}>"
}

 

まとめ

BigQueryデータセットのパブリックアクセスは、組織にとって最も危険なセキュリティリスクの一つです。

特に重要なポイント:

  • allUsersallAuthenticatedUsers絶対に使用しない
  • VPCサービスコントロールでネットワークレベルの保護を追加
  • 認可ビューを活用してデータの直接公開を避ける
  • Cloud Loggingですべてのアクセスを記録・監視

情報漏洩のみならず予期しない高額なクエリコストの発生や、競合他社への情報流出は組織に致命的な影響を与えます。定期的なアクセス権限の監査と、ゼロトラストセキュリティモデルの実装により、安全なデータ分析環境を維持しましょう。

この問題の検出は弊社が提供するSecurifyのCSPM機能で簡単に検出及び管理する事が可能です。 運用が非常に楽に出来る製品になっていますので、ぜひ興味がある方はお問い合わせお待ちしております。 最後までお読みいただきありがとうございました。この記事が皆さんの役に立てば幸いです。

参考情報

Google Cloud公式ドキュメント

この記事をシェアする

クラウドセキュリティ対策実践集一覧へ戻る

貴社の利用状況に合わせた見積もりを作成します。

料金プランを詳しく見る