🍣

S3の署名付きURLを独自ドメインで使う(CDKコード付き)

2025/01/25に公開

S3へのファイルアップロード・ダウンロードには署名付きURLを使うことが多いです。発行されるURLのドメインは バケット名.s3.ap-northeast-1.amazonaws.com といったようにAWSのドメインになります。

しかし、さまざまな事情からAWSではなく独自ドメインで署名付きURLを使いたいケースもあると思うので今回はそのやり方を紹介します。サンプルでAWS CDKコードもあります。

署名付きURLの独自ドメインはCloudFront

まず、大前提の整理からですが署名付きURLを独自ドメインで利用するには、CloudFrontが必要です。ファイルアップロード・ダウンロードのフロー図は以下のとおりです。

  • 図ではWebサーバでURL発行していますが、試すだけなら手元でURL発行可能です

S3の署名付きURLと大きく異なるのは署名するための鍵を自前で管理し、公開鍵をCloudFrontに登録する点です。

セットアップ手順イメージ

Step1: 秘密鍵と公開鍵の生成

  • URLに署名するための鍵を生成します
  • 秘密鍵はURL発行時に必要、公開鍵はCloudFrontに登録するのに必要になります
  • 当然ですが秘密鍵は秘密なので扱いには気をつけてください
openssl genrsa -out private.pem 2048
openssl rsa -pubout -in private.pem -out public.pem

Step2: 公開鍵の登録

  • CloudFrontに「キー管理」→「パブリックキー」項目があり、そこから公開鍵を登録します
  • ここで登録した公開鍵のパブリックキーのIDが、署名付きURLを発行する際に必要になります


パブリックキーのID例

Step3: キーグループの作成

  • 同様に「キー管理」→「キーグループ」を作成し、先ほど登録した公開鍵を連携します

Step4: CloudFrontにS3を追加

  • S3バケットをオリジンに登録し、ビヘイビアを追加します
  • その際に「ビューワーのアクセスを制限する」→「信頼された認可タイプ」から、先程作成したキーグループを設定します
  • これにより、普通のリクエストは制限され、公開鍵で検証可能なリクエストのみアクセス可能になります

Step5: S3への書き込み権限を付与

  • 最後にS3バケットへの書き込み権限を、CloudFrontからできるようにポリシー設定します

AWS CDKコード

上記のStep2から5までを実行するAWS CDKのコードです。必要最低限の定義しかしていないので参考にする際には適宜変更をして使ってください。実際にはACMでHTTPSの署名発行などが必要です。フルのコードはリポジトリに公開しています。

import * as fs from "node:fs";
import * as cdk from "aws-cdk-lib";
import { RemovalPolicy, type StackProps } from "aws-cdk-lib";
import type { Certificate } from "aws-cdk-lib/aws-certificatemanager";
import {
  AllowedMethods,
  CachePolicy,
  Distribution,
  HttpVersion,
  KeyGroup,
  OriginRequestPolicy,
  PriceClass,
  PublicKey,
  ViewerProtocolPolicy,
} from "aws-cdk-lib/aws-cloudfront";
import { S3BucketOrigin } from "aws-cdk-lib/aws-cloudfront-origins";
import { Effect, PolicyStatement, ServicePrincipal } from "aws-cdk-lib/aws-iam";
import { Bucket } from "aws-cdk-lib/aws-s3";
import type { Construct } from "constructs";

interface CloudfrontStackProps extends StackProps {
  certificate: Certificate;
}
export class CloudfrontStack extends cdk.Stack {
  constructor(scope: Construct, id: string, props: CloudfrontStackProps) {
    super(scope, id, props);

    // S3バケット作成
    const bucket = new Bucket(this, "Bucket", {
      bucketName: "signed-url-cloudfront-example-2025",
      removalPolicy: RemovalPolicy.RETAIN,
    });

    // 公開鍵の登録とキーグループ作成
    const publicKeyContent = fs.readFileSync("keys/public.pem", "utf-8");
    const publicKey = new PublicKey(this, "SignedUrlPublicKey", {
      encodedKey: publicKeyContent,
      comment: "UploadFile S3 Public Key",
    });
    const keyGroup = new KeyGroup(this, "SignedUrlKeyGroup", {
      keyGroupName: "ExampleS3PublicKeyGroup",
      items: [publicKey],
    });

    // CloudFrontディストリビューションの作成
    const distribution = new Distribution(this, "CF", {
      // サンプルコードではデフォルトにしているが実際は `additionalBehaviors` になることが多いと思われる
      defaultBehavior: {
        origin: S3BucketOrigin.withOriginAccessControl(bucket),
        viewerProtocolPolicy: ViewerProtocolPolicy.REDIRECT_TO_HTTPS,
        allowedMethods: AllowedMethods.ALLOW_ALL,
        cachePolicy: CachePolicy.CACHING_DISABLED,
        originRequestPolicy: OriginRequestPolicy.CORS_S3_ORIGIN,
        trustedKeyGroups: [keyGroup],
      },
      httpVersion: HttpVersion.HTTP2_AND_3,
      priceClass: PriceClass.PRICE_CLASS_200,
      domainNames: ["example.com"],
      certificate: props.certificate, // 事前にACMで取得した証明書
    });

    // CloudFrontが発行する署名付きURLでS3にファイルアップロードするための権限を追加
    const distributionArn = this.formatArn({
      service: "cloudfront",
      resource: `distribution/${distribution.distributionId}`,
      region: "",
    });
    bucket.addToResourcePolicy(
      new PolicyStatement({
        effect: Effect.ALLOW,
        principals: [new ServicePrincipal("cloudfront.amazonaws.com")],
        actions: ["s3:PutObject"],
        resources: [bucket.arnForObjects("*")],
        conditions: {
          StringEquals: {
            "AWS:SourceArn": distributionArn,
          },
        },
      }),
    );
  }
}

署名付きURLを発行するコード

  • S3の署名付きURLと同じように有効期限の指定ができます
  • URLを生成するには最初に作成した秘密鍵が必要です
  • また、CloudFrontに登録したパブリックキーIDも必要です
  • このURLはダウンロード・アップロードどちらにも使えます
import * as fs from "node:fs";
import { getSignedUrl } from "@aws-sdk/cloudfront-signer";

const url = getSingedUrl("example.png");
console.log(url);

function getSingedUrl(path: string) {
  const url = `https://example.com/${path}`;
  const privateKey = fs.readFileSync("./keys/private.pem", "utf-8");
  return getSignedUrl({
    url,
    keyPairId: "KXXXXXXXXXX", // CloudFrontに登録したパブリックキーID
    dateLessThan: expiresAt(),
    privateKey,
  });
}

function expiresAt(minutesUntilExpiration = 10) {
  const date = new Date();
  date.setMinutes(date.getMinutes() + minutesUntilExpiration);
  return date.toUTCString();
}
ムーザルちゃんねる

Discussion