これは何?
最近ですがAWS SignerによってLambdaなどにコード署名をくわえることができるるのか検証する機会があったので、今回はその検証した内容についていろいろ記載していきたいと思います。
シナリオとしては、未署名のCloudformationで管理しているLambda(Python3.11)関数を、コード署名するまでの流れを検証してみました。
1. 事前準備
コード署名の設定していない関数をコード署名を有効化し、コードの改竄防止を確認しようと思います。
この章では、事前準備としてコード署名設定をしていないLambda関数の作成までをしようと思います。
フォルダ構成
Cloudformation を用いてデプロイしようと思うため、以下のようフォルダ構成になります。
./
├── event.json
├── function/
│ └── main.py
└── template.yml
S3バケットの生成
Pythonコードをパッケージング(cloudformation packageコマンド)及びコード署名格納用のS3バケット(ここではtest-python-packages
)を作成します。
S3バケットの条件としてはバージョニングの有効と、パブリックアクセスを無効化します。
https://docs.aws.amazon.com/ja_jp/signer/latest/developerguide/s3-source-lambda.html
今回のバケットのプレフィックスは以下の通りになります。
./test-python-packages
├── lambda/ ... lambda関数を格納
│ └── test_lambda_function/
│ └── (cloudformation packageで格納されるファイル)
│
└── signer/ ... コード署名したファイル
└── test_lambda_function/
└── (コード署名用したファイル)
Lambda 関数の作成
今回はランタイムはPython 3.11のLambdaを作成します。以下のリンクからサンプル関数を作成
https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/python-handler.html
import logging
logger = logging.getLogger()
logger.setLevel(logging.INFO)
def lambda_handler(event, context):
message = 'Hello {} {}!'.format(event['first_name'], event['last_name'])
return {
'message' : message
}
Lambda関数の実行引数としてをJSON形式で予め作成しておきます。
{
"first_name": "John",
"last_name": "Smith"
}
Cloudformation テンプレート
次にCloudformation テンプレートを作成していきます。
Lambda関数を作成し、AWS SAMにて AWS::Serverless::Function によってLambda関数をデプロイしようと思います。
AWSTemplateFormatVersion: '2010-09-09'
Transform: 'AWS::Serverless-2016-10-31'
Parameters:
CodeSigningProfileVersionArn:
Type: String
Description: Versioned CodeSigningProfile Arn
Resources:
function:
Type: AWS::Serverless::Function
Properties:
FunctionName: "test_lambda_function"
Handler: main.lambda_handler
Runtime: python3.11
CodeUri: ./function/.
Description: Test Lambda Function
Timeout: 10
Policies:
- AWSLambdaBasicExecutionRole
- AWSLambda_ReadOnlyAccess
以下のコマンドを実行して、Cloudformationを実行してLambda関数を作成しようと思います。 package
コマンドでLambda関数で使用するコードをS3バケットに格納して、deploy
コマンドにてスタックを作成/更新していきます。
# Package
aws cloudformation package --template-file template.yml --s3-bucket test-python-packages --s3-prefix lambda/test_lambda_function --output-template-file out.yml
# Deploy
aws cloudformation deploy --template-file out.yml --stack-name test-lambda-python11 --capabilities CAPABILITY_NAMED_IAM
上記を実行することで、Lamdba関数test_lambda_function
が正常作成されることを確認したら一旦終わりです。
動作確認
以下のコマンドを実行して、LambdaをInvokeします。出力結果がout.jsonが正常に出力され、以下の内容が確認できれば下準備が完了です。
aws lambda invoke --function-name test_lambda_function --payload file://event.json out.json
cat out.json
# {"message": "Hello John Smith!"} と出力
2. AWS Signerの設定
署名プロファイルの作成 & 署名ジョブの実行
Cloudformationでも生成することはできますが、今回は手動で作成してみようと思います。
AWS Signerを始めるためには先ず、署名プロファイルを作成する必要があります。
プロファイルを作成しましたら、署名ジョブを実行し、コード署名を実施します。
コードアセットのソース場所にはプレフィックスlambda/test_lambda_function/
内にある圧縮ファイルを選択します。
署名後の送信先バケットはプレフィックスsigner/test_lambda_function/
を指定します。
以下のようにステータスが成功と表示されると署名ジョブは成功します。
これでCode署名を実施することができました。
3. コード署名設定
CloudFormation でLambdaコード署名を実施するにはコード署名設定を設定した後、Lambda関数上でコード署名設定を関連づける必要があります。
コード署名設定はリソースAWS::Lambda::CodeSigningConfigで作成することができ、サンプルコードは以下のようになります。
ここでCodeSigningProfileVersionArn
は2章で作成したAWS Signerの署名プロファイルの「バージョン管理されたプロファイル ARN」を指定する必要があります。
template.ymlを以下のように編集します。
AWSTemplateFormatVersion: '2010-09-09'
Transform: 'AWS::Serverless-2016-10-31'
Parameters:
CodeSigningProfileVersionArn:
Type: String
Description: Versioned CodeSigningProfile Arn
Resources:
function:
Type: AWS::Serverless::Function
Properties:
FunctionName: "test_lambda_function"
Handler: main.lambda_handler
Runtime: python3.11
CodeUri: ./function/.
Description: Test Lambda Function
Timeout: 10
Policies:
- AWSLambdaBasicExecutionRole
- AWSLambda_ReadOnlyAccess
CodeSigningConfigArn: !Ref csc
csc:
Type: AWS::Lambda::CodeSigningConfig
Properties:
Description: "Code Signing Config for test_lambda_function"
AllowedPublishers:
SigningProfileVersionArns:
- !Ref CodeSigningProfileVersionArn
CodeSigningPolicies:
UntrustedArtifactOnDeployment: "Enforce"
上記の内容を追記したら、以下のコマンドを実行。変更点としてはcloudformation deploy
コマンドでパラメータ値署名プロファイルArnを指定する必要があります。
# Package
aws cloudformation package --template-file template.yml --s3-bucket test-python-packages --s3-prefix lambda/test_lambda_function --output-template-file out.yml
# Deploy
aws cloudformation deploy --template-file out.yml --stack-name test-lambda-python11 --capabilities CAPABILITY_NAMED_IAM --parameter-overrides CodeSigningProfileVersionArn=「CodeSigningProfileVersionのArn値」
これを実行すると、Lambdaコンソール上のコード署名設定画面で、署名プロファイル「test_signer」を元にしたコード署名設定を作成することができます。
変更前/変更後確認
以下はコード署名前のLambdaになります。コードソース自体中身が見えて編集することもできてしまいます。
コード署名すると以下のように関数自体確認できなくなります。
設定 > コード署名画面でもコード署名に用いられている証明書Arnや署名プロファイルが作成されていることを確認できます。
4. 動作確認
動作確認前準備
以下のようにLambda関数を更新した後、cloudformation packageコマンドで、S3上にpythonファイルを圧縮したオブジェクトをアップロードします。
(before)
message = 'Hello {} {}!'.format(event['first_name'], event['last_name'])
(after)
message = 'Hello {} {}!!!'.format(event['first_name'], event['last_name'])
aws cloudformation package --template-file template.yml --s3-bucket test-python-packages --s3-prefix lambda/test_lambda_function --output-template-file out.yml
これでS3バケット上に変更後のスクリプトをアップロードできました。しかし、これは未だSignerの署名ジョブによってコード署名されていないオブジェクトになります。
[失敗パターン] 未署名のファイルでデプロイでしてみる
Lambdaコンソールからアップロードされた未署名のオブジェクトを指定して、Lambda関数を更新してみます。
すると以下のようにLambda cannot deploy the function. The function or layer might be signed using a signature that the client is not configured to accept.
のエラーが発生しました。
未署名のオブジェクトだったためエラーになったものかと。
また、CLI上からcloudformation deploy
を実行して、Lambdaが更新されるのかを確認してみます。
すると以下のようなエラーになります。未署名のコードでデプロイしようとするとロールバック失敗になり大惨事になってしまいます。。
[成功パターン]署名済みのファイルでデプロイでしてみる
成功させるには、事前に署名ジョブを実行してコード署名済みのオブジェクトにする必要があります。
こちらの手順でCloudformation上のロールバックを完了させてから、実行するようにしましょう。
ロールバックが完了したら、先ほどのようなcloudformationとは異なり、AWS SAMを用いてデプロイしていきます。SAM を用いたコード署名はこちらを参考に。
何故、CloudformationコマンドではなくSAMなのかの理由としては以下の通りです。
cloudformation pacakgeコマンドで出力されるout.ymlが、どうやってもコード署名されたファイルを指定できなかった。。
sedとかで書き換えることも可能ではあるが、SAMを使った方が--signing-profilesオプションを提供しているため、簡単にデプロイできる。
以下のコマンドを実行することで既存のCfnスタックをSAMでデプロイすることができます。
sam package --signing-profiles "function=test_signer" --template-file template.yml --s3-bucket 「バケット名」 --s3-prefix lambda/test_lambda_function --output-template-file out.yml
sam deploy --template-file out.yml --stack-name test-lambda-python11 --capabilities CAPABILITY_NAMED_IAM
無事デプロイすることができました。
デプロイ後LambdaをInvokeして変更した内容が反映できていればOKです!
終わりに
既存のCfnテンプレートから作成したLambdaでも簡単にコード署名することができました。
また、AWS SAMを利用すれば--signing-profiles
などのオプションを利用することで、コード署名からデプロイまでをワンストップで実施することができるため、デプロイの際はAWS SAMを利用するようにした方が良いかと思います。
[補足] CLIで署名ジョブの実行。
署名ジョブを実行するにはstart-signing-job
を実行する必要があります。start-signing-jobではS3オブジェクトの現行VersionIdも必要なため、以下のようにlist-object-versionsでバージョンを取得してから実行します。
# バケットのバージョンIDを確認。
BUCKET_NAME="「バケット名」"
KEY="Prefix名"
version =`aws s3api list-object-versions --bucket $BUCKET_NAME --prefix $KEY --query "Versions[0].VersionId"`
# 署名ジョブの実行
aws signer start-signing-job \
--source "s3={bucketName=$BUCKET_NAME, key=$KEY, version=$version}" \
--destination "s3={bucketName=$BUCKET_NAME, prefix=signer/test_lambda_functi
on/}" \
--profile-name test_signer