s3の署名付きURLが有効期限より前に見れなくなってしまう

公開日時

CloudWatchメトリクスのグラフ画像をSlackに定期通知するLambdaを作るにて毎朝通知を行うようにしたが、有効期限を指定しているにも関わらず1日立つと画像が見れなくなってしまう現象に遭遇した。

原因

画像URLにアクセスしてみると、有効期限切れではなくトークンの期限切れというエラーが発生していた。

ExpiredToken
The provided token has expired.

最大7日間なのかーという部分しか確認せずにExpiresIn=604800を指定してみましたが、残念ながら有効期限より前に失効する状況は改善しませんでした。

それもそのはずで、よく読めば利用している認証情報で有効期限の最大値が決まり、その最大値で有効期限が失効するということになります。

AWS LambdaだとIAM role を利用しているので、AssumeRole アクションにてAWS STS から一時トークンを取得し、その一時トークンで認証を行なっていることになります。

そのため、上記ドキュメントに記載のSTS 利用時に合致し指定している7日を待たずに失効する形になっていました。

上記記事の方と同じで、IAMユーザではなくIAMロールで[[s3.getSignedUrl]]を実行していたため、有効期限より前にトークンの期限切れが発生してしまっていた。

対策

専用のIAMユーザを作成して、SSMから認証情報を読み込み署名付きURLを作成するように修正した。

  • 専用IAMユーザの作成し、指定のS3バケットのみにアクセスできるポリシーを付与
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "s3:*"
            ],
            "Resource": [
                "arn:aws:s3:::your-bucket-name",
                "arn:aws:s3:::your-bucket-name/*"
            ]
        }
    ]
}
  • SSMのパラメータストアにSecureStringでアクセスキーとシークレットキーを保存

  • SAMのtemplate.yamlにSSMのポリシーを付与

Resources:
  HelloWorldFunction:
    Type: AWS::Serverless::Function
    Properties:
      Policies:
        - SSMParameterReadPolicy:
            ParameterName: !Ref SsmIamUserAccessKeyName
        - SSMParameterReadPolicy:
            ParameterName: !Ref SsmIamUserSecretKeyName
      Environment:
        Variables:
          SSM_IAM_USER_ACCESS_KEY_NAME: !Ref SsmIamUserAccessKeyName
          SSM_IAM_USER_SECRET_KEY_NAME: !Ref SsmIamUserSecretKeyName
          
Parameters:
  SsmIamUserAccessKeyName:
    Type: String
    Default: YourSsmIamKey
  SsmIamUserSecretKeyName:
    Type: String
    Default: YourSsmIamSecret
  • LambdaコードにてSSMからIAMユーザ情報を取得し、S3 SDKのインスタンス生成時にIAMユーザを指定するように修正
const aws = require("aws-sdk")
const ssm = new aws.SSM()

const iamUserInfo = async () => {
  const accessKeyName = process.env.SSM_IAM_USER_ACCESS_KEY_NAME;
  const secretKeyName = process.env.SSM_IAM_USER_SECRET_KEY_NAME;
  const param = {
    Names: [accessKeyName, secretKeyName],
    WithDecryption: true
  };
  const data = await ssm.getParameters(param).promise();
  const config = {};
  for (const i of data.Parameters) {
    if (i.Name === accessKeyName) {
      config.accessKeyId = i.Value;
    } else if (i.Name === secretKeyName) {
      config.secretAccessKey = i.Value;
    }
  }
  return config;
};

exports.lambdaHandler = async (event, context) => {
  const iamUser = await iamUserInfo();
  const s3 = new aws.S3({ signatureVersion: "v4", ...iamUser });
  
  try {
    const downloadUrl = s3.getSignedUrl("getObject", {
      Bucket: bucket,
      Key: key,
      Expires: 60 * 60 * 24 * 7 // max 7days
    });
  } catch (err) {
    console.log(err);
    return err;
  }
};

これで有効期限指定ができるようになった。

参考


Related #aws

RDSを定期的に停止するLambdaを作る

手動起動は大変なので

AWS SESの受信メールを暗号化してs3に保存しLambdaで読み込む

jsの場合、複合処理を独自実装する必要がある

CloudWatchアラームを一時的に無効化する

AWS CLIで設定する必要がある

Alexaに気温と二酸化炭素濃度を教えてもらう

「Alexa、気温」でセンサー情報を教えてくれるようになった

CloudWatchのカスタムメトリクスを減らす

あまりチェックしないセンサー値の送信を止めた