認証情報プロバイダーのポリシー変数を使用してAPI Gatewayへのアクセスをセキュアにする

認証情報プロバイダーのポリシー変数を使用してAPI Gatewayへのアクセスをセキュアにする方法について書きました。
2023.01.10

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

こんにちは。CX事業本部Delivery部の木村です。 だいぶ久しぶりの投稿になってしまいしました。今年はできるだけブログ投稿していこうと思います。

概要

早速ですが、本題です。

IoTデバイスからAmazon APIGatewayにセキュアにアクセスしたいケースはよくあると思います。 Amazon APIGatewayにIAMによる認可を設定し、認証情報プロバイダーの仕組みを使用することで実現が可能です。

公式ドキュメント

[AWS IoT] 既存の証明書だけでMQTT以外の各種AWSリソ−スにアクセスする (Authorizing Direct Calls)

今回はさらに踏み込んで、以下のようなAPIパスについて、IoT Coreに登録済みの証明書を所有しているデバイスが、自分自身のThingNameのみアクセスできる状態を実現したいと思います。

「GET /devices/{thingName}/certain-command」

CDKのコード

早速ですが、CDKv2のサンプルコードは以下です。

/lib/sample-stack.ts

import * as cdk from 'aws-cdk-lib';
import * as apigateway from 'aws-cdk-lib/aws-apigateway';
import * as iam from 'aws-cdk-lib/aws-iam';
import * as iot from 'aws-cdk-lib/aws-iot';
import * as lambda from 'aws-cdk-lib/aws-lambda';
import { NodejsFunction } from 'aws-cdk-lib/aws-lambda-nodejs';
import { Construct } from 'constructs';

export class SampleStack extends cdk.Stack {
  constructor(scope: Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    const awsAccountId = cdk.Stack.of(this).account;
    const awsRegion = cdk.Stack.of(this).region;

    // ----- Lambda -----
    const sampleFunction = new NodejsFunction(this, 'sampleFunction', {
      functionName: `sample-function`,
      entry: `./functions/sample-handler.ts`,
      handler: 'handler',
      environment: {},
      runtime: lambda.Runtime.NODEJS_16_X,
      timeout: cdk.Duration.seconds(30),
    });

    // ----- RestApi -----
    const restApi = new apigateway.RestApi(this, 'SampleApi', {
      restApiName: `sample-api`,
      endpointTypes: [apigateway.EndpointType.REGIONAL],
      // [1]
      defaultMethodOptions: {
        authorizationType: apigateway.AuthorizationType.IAM,
      },
    });

    const devicesResource = restApi.root.addResource('devices');
    const devicesThingNameResource = devicesResource.addResource('{thingName}');
    const devicesThingNameCertainCommandResource =
      devicesThingNameResource.addResource('certain-command');

    devicesThingNameCertainCommandResource.addMethod(
      'GET',
      new apigateway.LambdaIntegration(sampleFunction),
    );

    // ----- IAM -----
    const ioTCoreCredentialProviderRole = new iam.Role(
      this,
      'IoTCoreCredentialProviderRole',
      {
        roleName: `credential-provider-sample-role`,
        assumedBy: new iam.ServicePrincipal('credentials.iot.amazonaws.com'),
      },
    );

    ioTCoreCredentialProviderRole.addToPolicy(
      new iam.PolicyStatement({
        actions: ['execute-api:Invoke'],
        effect: iam.Effect.ALLOW,
        resources: [
          // [3]
          `arn:aws:execute-api:${awsRegion}:${awsAccountId}:${restApi.restApiId}/${restApi.deploymentStage.stageName}/*/devices/\${credentials-iot:ThingName}/certain-command`,
        ],
      }),
    );

    const roleAlias = new iot.CfnRoleAlias(
      this,
      'IoTCoreCredentialProviderRoleAlias',
      {
        roleArn: ioTCoreCredentialProviderRole.roleArn,
        credentialDurationSeconds: 3600,
        roleAlias: `sample-role-alias`,
      },
    );

    // ----- IoT -----
    new iot.CfnPolicy(this, 'iotPolicy', {
      policyDocument: {
        Version: '2012-10-17',
        Statement: [
          {
            Effect: 'Allow',
            Action: ['iot:AssumeRoleWithCertificate'],
            Resource: [
              `arn:aws:iot:${awsRegion}:${awsAccountId}:rolealias/${roleAlias.roleAlias}`,
            ],
          },
        ],
      },
      policyName: 'sampleIotPolicy',
    });
  }
}

説明

[1] 認可について

AWS_IAMによる認可を設定し、SigV4署名をアクセス時に求めるようにします。

[2] ポリシー変数の使用

「credentials-iot:ThingName」というポリシー変数を使用することで認証情報取得時にthingNameを動的に評価することができます。 「execute-api」における「Resources」の書き方は以下のような規則で書きます。 公式ドキュメント

arn:aws:execute-api:<region>:<account-id>:<api-id>/<stage>/<METHOD_HTTP_VERB>/<Resource-path>

そのうち「Resource-path」について次のように記述しています。

/devices/${credentials-iot:ThingName}/certain-command

${credentials-iot:ThingName}の部分が、認証情報プロバイダーが動作する際に、実際のThingNameに置き換えられるイメージです。

公式ドキュメント

動作確認

1. Thingを作成

細かい手順は割愛しますが、任意の方法でThingを作成し、証明書をアタッチし、上記CDKで作成したIoT Policyをその証明書にアタッチします。 証明書(以下のコマンド例では「cert.pem」)、秘密鍵(以下のコマンド例では「private.key」)をローカルに保存します。

2. クレデンシャルエンドポイント

$ aws iot describe-endpoint --endpoint-type iot:CredentialProvider

3. セッショントークンの取得

$ curl --cert ./cert.pem --key ./private.key -H "x-amzn-iot-thingname: <thingName>" https://<※1>/role-aliases/<※2>/credentials | jq

※1: 2.で取得したクレデンシャルエンドポイント ※2: CDKで作成したroleAlias名

4. ポストマンでSigV4署名してリクエスト

  1. 「Authorization」を指定
  2. 「AWS Signature」を選択
  3. 取得したAccessKey, SecretKey, Session Tokenを入力します。

成功した場合のレスポンス例

Hello from lambda

自分以外のThingNameを指定した場合のレスポンス例

{
    "Message": "User: arn:aws:sts:::assumed-role// is not authorized to perform: execute-api:Invoke on resource: arn:aws:execute-api:ap-northeast-1:/prod/GET/devices//certain-command"
}

まとめ

Well AtchitectedフレームワークのIoTレンズの「最小限の権限を確認」という項目にあるとおり、きめ細かなアクセス許可を設定することで、個別のデバイスが攻撃されたときにその影響を限定することができます。

今回はthingNameに関するポリシー変数を指定しましたが、「credentials-iot:ThingTypeName」「credentials-iot:AwsCertificateId」といった指定も可能ですのでそれぞれのユースケースにそった方法で柔軟な設定ができそうです。

また、今回はAPIGatewayのアクセス制御に使用しましたが、S3からのファイル配信やDynamoDBなどにも応用できそうですね。

認証情報プロバイダーのポリシー変数を利用してIoTデバイスからAWSサービスの直接呼び出しをセキュアに制御する

以上、どなたかのお役に立てれば幸いです。