💭

Cloud SQL に Cloud Run から SSL 接続する

に公開

Google Cloud の Security Command Center (SCC) で、Cloud SQL に関する脆弱性 SSL not enforced が検知されました。

当初はプライベートネットワーク構成(Private IPのみ)だったので許容していましたが、アラート対応として SSL 接続を強制する設定に変更しました。

対応には Cloud SQL Node.js Connector を使用しました。
今後の備忘録として手順を残します。

前提: インフラ構成

インフラは Terraform で管理しており、以下のようなプライベート構成をとっていました。

  1. Cloud SQLの完全なプライベート化
    • パブリックIPなし
    • VPCピアリング経由のみアクセス可能
  2. Cloud Runの保護
    • 内部ロードバランサー経由のみアクセス可能
    • Serverless VPC Access Connector 経由でプライベートネットワークに接続
  3. Bastionへのアクセス制御
    • IAP (Identity-Aware Proxy) 経由でのみSSH接続可能
    • プライベートIPでCloud SQLに接続

データフロー図

  ┌────────────────────────────────────────────────────────────┐
  │  VPC Network                                               │
  │                                                            │
  │  ┌───────────────────────────────────────────────────────┐ │
  │  │ Subnetwork:                                           │ │
  │  │                                                       │ │
  │  │  ┌──────────────┐         ┌──────────────────────┐    │ │
  │  │  │   Bastion    │         │   Cloud SQL Proxy    │    │ │
  │  │  │   Tag: iap   │─────────>   Port: 5432         │    │ │
  │  │  └──────────────┘         └──────────┬───────────┘    │ │
  │  │                                      │                │ │
  │  └──────────────────────────────────────┼────────────────┘ │
  │                                         │                  │
  │  ┌────────────────────────────────────┐ │                  │
  │  │ VPC Connector:                     │ │                  │
  │  │ (for Cloud Run)                    │ │                  │
  │  │  ┌───────────────┐                 │ │                  │
  │  │  │   Cloud Run   │─────────────────│─│                  │
  │  │  └───────────────┘                 │ │                  │ 
  │  └────────────────────────────────────┘ │                  │
  │                                         │                  │
  │  ┌──────────────────────────────────────▼────────────────┐ │
  │  │ VPC Peering (Service Networking)                      │ │
  │  │  Private IP Range: /24                                │ │
  │  │                                                       │ │
  │  │  ┌────────────────────────────────────────────────┐   │ │
  │  │  │  Cloud SQL (POSTGRES_15)                       │   │ │
  │  │  │  - プライベートIPのみ (ipv4_enabled: false)       │   │ │
  │  │  └────────────────────────────────────────────────┘   │ │
  │  └───────────────────────────────────────────────────────┘ │
  └────────────────────────────────────────────────────────────┘

対応方針

今回修正が必要な点は以下の2点です。

  1. Cloud SQL の設定で、SSL モードを「SSL 接続のみを許可する」に変更する。
  2. アプリケーション(Cloud Run)からの接続を SSL に対応させる。

接続方式の検討

SSL 接続に対応させるにあたり、コンテナのサイドカーとして Cloud SQL Auth Proxy を動かす方法もありますが、管理などが面倒になりそうなため避けたいです。

代わりに、Cloud SQL Node.js Connector を採用しました。
これを利用することで、サイドカー等の追加なしで容易に IAM 認証および SSL 接続が可能になります。

https://github.com/GoogleCloudPlatform/cloud-sql-nodejs-connector

前提条件:

  • Cloud Run のサービスアカウントに roles/cloudsql.client が付与されていること。
  • Cloud SQL Admin API が有効化されていること。

今回の環境では上記権限周りは設定済みでした。

1. Terraform の修正

Cloud SQL Connector を利用するには「接続名」が必要です。
これを環境変数として Cloud Run に渡すよう修正します。

DB接続名を output に追加

modules/cloud_sql/output.tf
output "sql_connection" {
  description = "The SQL connection"
  value       = google_sql_database_instance.instance.connection_name
}

Cloud Run の環境変数に追加

environment/develop/main.tf
module "api" {
  source = "../../modules/cloud_run"
  # ...
  env_vars = {
    "DB_CONNECTION_NAME"  = module.cloud_sql.sql_connection
  }
  # ...
}

SSLモードを「ENCRYPTED_ONLY」に変更

モジュール側で ssl_mode を受け取れるようにし、呼び出し元で指定します。

modules/cloud_sql/main.tf
resource "google_sql_database_instance" "instance" {
  # ...
    ip_configuration {
      ipv4_enabled    = var.ipv4_enabled
      private_network = var.private_network
      ssl_mode        = var.ssl_mode # <- 追加
    }
  # ...
}
environment/develop/main.tf
module "cloud_sql" {
  source = "../../modules/cloud_sql"
  # ...
  ssl_mode = "ENCRYPTED_ONLY" # ENCRYPTED_ONLYを指定
  # ...
}

これでインフラ側の変更は完了です。

2. アプリケーションコードの修正 (NestJS)

API は NestJS + node-postgres で構築しており、ORM は使用していません。

まず、コネクタをインストールします。

npm install @google-cloud/cloud-sql-connector

DB接続モジュールを書き換えます。

変更前のコード:

src/db/db.module.ts
import { Global, Module } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { Pool } from 'pg';

@Global()
@Module({
  exports: ['PG_POOL'],
  providers: [
    {
      inject: [ConfigService],
      provide: 'PG_POOL',
      useFactory: async (configService: ConfigService) => {
        const pool = new Pool({
          database: configService.get('DB_NAME'),
          host: configService.get('DB_HOST'),
          password: configService.get('DB_SECRET'),
          port: Number(configService.get('DB_PORT')),
          user: configService.get('DB_USER'),
        });
        
        return pool;
      },
    },
  ],
})
export class DbModule {}

変更後のコード:

src/db/db.module.ts
import { Connector, IpAddressTypes } from '@google-cloud/cloud-sql-connector';
import { Global, Module, OnApplicationShutdown } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { Pool } from 'pg';

let pool: Pool | null = null;
let connector: Connector | null = null;

@Global()
@Module({
  exports: ['PG_POOL'],
  providers: [
    {
      inject: [ConfigService],
      provide: 'PG_POOL',
      useFactory: async (configService: ConfigService) => {
        connector = new Connector();

        const clientOpts = await connector.getOptions({
          instanceConnectionName: configService.get('DB_CONNECTION_NAME'),
          ipType: IpAddressTypes.PRIVATE,
        });

        pool = new Pool({
          ...clientOpts,
          user: configService.get('DB_USER'),
          password: configService.get('DB_SECRET'),
          database: configService.get('DB_NAME'),
        });

        return pool;
      },
    },
  ],
})
export class DbModule implements OnApplicationShutdown {
  async onApplicationShutdown() {
    await pool?.end();
    connector?.close();
  }
}

結果

Cloud Run に新しいコンテナをデプロイしたところ、無事に API アクセスが成功しました。
また、SCC で検知されていた SSL not enforced のアラートも解消されました。

Discussion