0
0

AWS Signerを使って既存のLambda関数をコード署名してみた

Posted at

これは何?

最近ですが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

function/main.py
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形式で予め作成しておきます。

event.json
{
    "first_name": "John",
    "last_name": "Smith"
}

Cloudformation テンプレート

次にCloudformation テンプレートを作成していきます。
Lambda関数を作成し、AWS SAMにて AWS::Serverless::Function によってLambda関数をデプロイしようと思います。

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

以下のコマンドを実行して、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を始めるためには先ず、署名プロファイルを作成する必要があります。
image.png

プロファイルを作成しましたら、署名ジョブを実行し、コード署名を実施します。
コードアセットのソース場所にはプレフィックスlambda/test_lambda_function/内にある圧縮ファイルを選択します。

署名後の送信先バケットはプレフィックスsigner/test_lambda_function/を指定します。

image.png

以下のようにステータスが成功と表示されると署名ジョブは成功します。
image.png

これでCode署名を実施することができました。

3. コード署名設定

CloudFormation でLambdaコード署名を実施するにはコード署名設定を設定した後、Lambda関数上でコード署名設定を関連づける必要があります。
コード署名設定はリソースAWS::Lambda::CodeSigningConfigで作成することができ、サンプルコードは以下のようになります。

ここでCodeSigningProfileVersionArnは2章で作成したAWS Signerの署名プロファイルの「バージョン管理されたプロファイル ARN」を指定する必要があります。

template.ymlを以下のように編集します。

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」を元にしたコード署名設定を作成することができます。

image.png

変更前/変更後確認

以下はコード署名前のLambdaになります。コードソース自体中身が見えて編集することもできてしまいます。
image.png

コード署名すると以下のように関数自体確認できなくなります。
image.png
設定 > コード署名画面でもコード署名に用いられている証明書Arnや署名プロファイルが作成されていることを確認できます。
image.png

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. のエラーが発生しました。
未署名のオブジェクトだったためエラーになったものかと。
image.png

また、CLI上からcloudformation deployを実行して、Lambdaが更新されるのかを確認してみます。
すると以下のようなエラーになります。未署名のコードでデプロイしようとするとロールバック失敗になり大惨事になってしまいます。。

image.png

[成功パターン]署名済みのファイルでデプロイでしてみる

成功させるには、事前に署名ジョブを実行してコード署名済みのオブジェクトにする必要があります。
こちらの手順で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
0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0