type holyshared = Engineer<mixed>

技術的なことなど色々

Cloud CDNでの署名付きCookie、署名付きURL

Cloud Storageに格納したファイルをCDNで配信する時にアクセスを限定したい場合があります
その際に署名付きCookie、署名付きURLが使えます

設定自体はterraformなどで、自動で設定して置けるようにすると楽です

resource "google_storage_bucket" "public-images" {
  project  = var.project_id
  name     = "public-images-${var.project_id}"
  location = "asia-northeast1"

  force_destroy = true

  cors {
    origin          = ["https://${var.domain}"]
    method          = ["GET"]
    response_header = ["*"]
  }
}

resource "google_storage_bucket_iam_member" "signed-url-user" {
  bucket = google_storage_bucket.public-images.name
  role   = "roles/storage.objectViewer"
  member = "serviceAccount:service-${var.project_number}@cloud-cdn-fill.iam.gserviceaccount.com"

  depends_on = [
    google_compute_backend_bucket_signed_url_key.backend_key
  ]
}

resource "google_compute_backend_bucket" "cdn-backend-bucket" {
  project     = var.project_id
  name        = "backend-${google_storage_bucket.public-images.name}"
  bucket_name = "public-images-${var.project_id}"
  enable_cdn  = true
}

resource "random_id" "url_signature" {
  byte_length = 16
}

resource "google_compute_backend_bucket_signed_url_key" "backend_key" {
  project        = var.project_id
  name           = "public-backend-bucket-key"
  key_value      = random_id.url_signature.b64_url
  backend_bucket = google_compute_backend_bucket.cdn-backend-bucket.name
}

resource "google_compute_url_map" "cdn" {
  project         = var.project_id
  name            = "public-images-cdn"
  default_service = google_compute_backend_bucket.cdn-backend-bucket.id
}

resource "google_compute_target_https_proxy" "cdn-proxy" {
  project          = var.project_id
  name             = "proxy-${var.project_id}"
  url_map          = google_compute_url_map.cdn.id
  ssl_certificates = [google_compute_managed_ssl_certificate.tls.id]
}

resource "google_compute_managed_ssl_certificate" "tls" {
  project  = var.project_id
  provider = google-beta
  name     = "public-images-${var.project_id}"
  managed {
    domains = [var.domain]
  }
}

resource "google_compute_global_forwarding_rule" "cdn" {
  project    = var.project_id
  name       = "public-images-${var.project_id}"
  target     = google_compute_target_https_proxy.cdn-proxy.self_link
  port_range = 443
}

生成された鍵はSecretManagerなどに登録しておくといいと思います

署名付きCookie

cloud.google.com

署名付きCookieは署名付きリクエストの鍵を使ってCookieに署名します
生成した値はCloud-CDN-Cookieに指定して利用できます

import crypto from 'crypto';
import dayjs from 'dayjs';

export const ENCODED_URL_PREFIX = (Buffer.from(process.env.URL_PREFIX))
  .toString("base64")
  .replace(/\+/g, "-")
  .replace(/\//g, "_");

export const signedCookie = (seconds: number) => {
  const unixTimestamp = dayjs().add(seconds, 'seconds').unix();
  const input = `URLPrefix=${process.env.ENCODED_URL_PREFIX}:Expires=${unixTimestamp}:KeyName=${process.env.SIGNED_URL_KEY_NAME}`;
  const keyValue = Buffer.from(process.env.SIGNED_URL_KEY_VALUE, 'base64') 
  const signature =  crypto.createHmac('sha1', keyValue)
    .update(input)
    .digest('base64')
    .replace(/\+/g, "-")
    .replace(/\//g, "_");

  return `${input}:Signature=${signature}`;
}

署名付きCookieの場合はCookieのパスの指定ミスなどで期待通りブラウザがCookie送ってくれなかったり、原因の切り分けを早くできた方がいいので、 実装が済んでしまったら先に正しい署名がCookieについているか確認しておくと良いと思います

curl --cookie='Cloud-CDN-Cookie=[生成した値]' \
  -D dump-headers.txt \
  --output=example.jpg \
  https://cdn.example.com/example.jpg

署名付きURL

cloud.google.com

署名付きURLはURLに対して署名を付加できるものです
Cookieと違って対象のURLが多い場合面倒ですが、実装自体は簡単なので導入しやすいです

import crypto from 'crypto';
import dayjs from 'dayjs';

export const signedURL = (url: string, seconds: number) => {
  const unixTimestamp = dayjs().add(seconds, 'seconds').unix();

  const segments = [
    `?Expires=${unixTimestamp}`,
    `&KeyName=${process.env.SIGNED_URL_KEY_NAME}`
  ];

  const signURL = `${url}${segments.join('')}`
  const keyValue = Buffer.from(process.env.SIGNED_URL_KEY_VALUE, 'base64') 
  const signature =  crypto.createHmac('sha1', keyValue)
    .update(signURL)
    .digest('base64')
    .replace(/\+/g, "-")
    .replace(/\//g, "_");

  segments.push(`&Signature=${signature}`);

  const singedQuery = segments.join('');

  return `${url}${singedQuery}`;
}

こちらも実装が済んだら検証しておきます

curl --output=example.jpg [署名付きURL]