はじめに
S3の署名付きURL(GET用)はAWS SDKで用意されているメソッドを使うと割と簡単に生成できますが、
CloudFrontで署名付きURLを取得するには、鍵ペアを自分で管理したり、
S3とCloudFront両方でアクセス制御をしたりと
S3に比べるとやることが少し多いと思います。
今回はアプリケーションコード側でCloudFrontで
署名付きURLを生成して取得するためにやることをまとめたいと思います。
秘密鍵と公開鍵を作成
openssl genrsa -out private.pem 2048
openssl rsa -pubout -in private.pem -out public.pem
CloudFrontで署名付きURLを生成するときに使用する秘密鍵と公開鍵を生成。
CloudFrontでパブリックキーを作成
cat public.pem
公開鍵の中身をKeyとして作成。
ここでIDを控えておく。
CloudFrontでキーグループを作成
Public Keysで一つ前の手順で作ったパブリックキーを指定
CloudFrontでディストリビューションを作成
ビヘイビアにて、「ビューワーのアクセスを制限する」をNoからYesに変更し、
Trusted key groups (recommended)で上で作成したキーグループを指定。
S3でバケットを作成
S3を直接参照させたくない場合はここで「パブリックアクセスをすべてブロック」をオンにしておきます。
その場合、CloudFrontからのアクセスは許可したいので以下のようなバケットポリシーを追加します。
{
"Version": "2008-10-17",
"Id": "PolicyForCloudFrontPrivateContent",
"Statement": [
{
"Sid": "AllowCloudFrontServicePrincipal",
"Effect": "Allow",
"Principal": {
"Service": "cloudfront.amazonaws.com"
},
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::your-bucket/*",
"Condition": {
"ArnLike": {
"AWS:SourceArn": "arn:aws:cloudfront::your-account-id:distribution/your-distribution-id"
}
}
}
]
}
AWS Secrets Managerにシークレットを追加
秘密鍵を配置します。
aws secretsmanager create-secret \
--name cloudfront-private-key \
--secret-string "$(cat private.pem)"
アプリケーションコード上で使用しているIAMユーザーにカスタマーインラインポリシーを追加
Secrets Managerに追加したシークレットのARNは
「シークレット名+XXXXXX」のようになっているのでその対応が必要。
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"secretsmanager:GetSecretValue"
],
"Resource": "arn:aws:secretsmanager:ap-northeast-1:your-account-id:secret:cloudfront-private-key*"
}
]
}
AWS Systems Managerのパラメータストアに追加
AWS_CLOUDFRONT_KEY_PAIR_IDとしてCloudFrontパブリックキーIDを設定。
AWS_CLOUDFRONT_PRIVATE_KEY_SECRET_NAMEとしてAWS Secrets Managerのシークレット名を設定。
アプリケーションコードで署名を生成
package aws
import (
"context"
"crypto/rsa"
"crypto/x509"
"encoding/pem"
"os"
"time"
"github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/config"
"github.com/aws/aws-sdk-go-v2/feature/cloudfront/sign"
"github.com/aws/aws-sdk-go-v2/service/secretsmanager"
"github.com/cockroachdb/errors"
)
// CloudFrontClient はCloudFront関連の操作を行うインターフェースです。
type CloudFrontClient interface {
GenerateSignedURL(ctx context.Context, resourceURL string, duration time.Duration) (string, error)
}
type cloudFrontClient struct {
signer *sign.URLSigner
keyPairID string
}
// NewCloudFrontClient はCloudFrontクライアントを初期化します。
// 環境変数からキーペアIDと秘密鍵のシークレット名を取得します。
func NewCloudFrontClient() (CloudFrontClient, error) {
keyPairID := os.Getenv("AWS_CLOUDFRONT_KEY_PAIR_ID")
privateKeySecretName := os.Getenv("AWS_CLOUDFRONT_PRIVATE_KEY_SECRET_NAME")
if keyPairID == "" || privateKeySecretName == "" {
return nil, errors.New("AWS_CLOUDFRONT_KEY_PAIR_ID and AWS_CLOUDFRONT_PRIVATE_KEY_SECRET_NAME must be set")
}
privateKey, err := loadPrivateKey(privateKeySecretName)
if err != nil {
return nil, errors.Wrap(err, "failed to load private key for cloudfront")
}
return &cloudFrontClient{
signer: sign.NewURLSigner(keyPairID, privateKey),
keyPairID: keyPairID,
}, nil
}
func (c *cloudFrontClient) GenerateSignedURL(ctx context.Context, resourceURL string, duration time.Duration) (string, error) {
expires := time.Now().Add(duration)
// AWS SDKのユーティリティを使用して署名付きURLを生成
signedURL, err := c.signer.Sign(resourceURL, expires)
if err != nil {
return "", errors.Wrapf(err, "failed to generate signed URL for %s", resourceURL)
}
return signedURL, nil
}
// loadPrivateKey はAWS Secrets Managerから秘密鍵を読み込み、パースします。
func loadPrivateKey(secretName string) (*rsa.PrivateKey, error) {
// アプリケーション起動時の初期化処理のため、context.TODO()を使用
ctx := context.TODO()
cfg, err := config.LoadDefaultConfig(ctx)
if err != nil {
return nil, errors.Wrap(err, "failed to load AWS config")
}
client := secretsmanager.NewFromConfig(cfg)
resp, err := client.GetSecretValue(ctx, &secretsmanager.GetSecretValueInput{
SecretId: aws.String(secretName),
})
if err != nil {
return nil, errors.Wrap(err, "failed to get secret from secrets manager")
}
pemData := []byte(*resp.SecretString)
block, _ := pem.Decode(pemData)
if block == nil {
return nil, errors.New("failed to parse PEM block")
}
genericKey, err := x509.ParsePKCS8PrivateKey(block.Bytes)
if err != nil {
return nil, errors.Wrap(err, "failed to parse PKCS8 private key")
}
key, ok := genericKey.(*rsa.PrivateKey)
if !ok {
return nil, errors.New("key is not an RSA private key")
}
return key, nil
}
おわりに
以上の手順でCloudFront署名付きURLを取得することができると思います。
これによって、アクセスを制限したいコンテンツの配信や効率的な配信が可能になります。
未リリース時の実装段階では、その手軽さから、一旦S3の署名付きURLを使用するということも多いと思います。
しかしリリース時にはS3を直接参照させたくなかったり、アクセス制御を柔軟に行いたかったり、
効率的に配信したかったりで
CloudFrontの署名付きURLを使うこともあると思います。
今回はそんなCloudFrontの署名付きURLの紹介でした。
それではまた!