Amazon API Gateway の API キーに有効期限を実装してみた

2023.02.22

いわさです。

Amazon API Gateway の API キーには有効期限の概念がありません。
そのため、発行した API キーを無効化したい場合は手動で無効化あるいは削除の操作を行う必要があります。

無いなら作ればいいじゃないということで今回は API キーの発行と有効期限の管理・自動でキーの削除を行うサーバーレスアプリケーションを作ってみました。

全体の構造としては以下のようになっています。

1. 制限された API

前提として API キーの利用が必要な REST API があり、使用量プランが設定済みです。
ここに作成した API キーに有効期限を持たせたいと思います。

2. キーの発行と期限管理

別の API でキーの発行操作をしてもらいます。
この API 自体も保護が必要ですが、今回は IAM オーソライザーを使っています。
API キーの発行処理の中で先程の制限された API に対して新しい API キーを発行し既存の使用量プランに関連付けします。

作成した API キーの情報は DynamoDB テーブルで管理します。
このテーブルでは TTL と DynamoDB Streams を有効化しておきます。
API キーの項目作成時に TTL 属性も設定し、自動で項目が削除される方式を取ります。
ちなみに、ニアリアルタイムな無効化が必要な場合は TTL だと適していない場合があります。(後述)

API キーは 1 アカウント 1 リージョンあたり最大 10,000 件までで上限緩和は出来ません。
ただし、使用量プランに関連付け出来るキーの数に上限はありません。(逆はある)

3. 期限切れキー無効化

DynamoDB Streams から TTL で削除された項目の情報を Lambda 関数で受け取り、制限された API 向けに作成した API キーの実体を削除します。
最初 EventBridge 経由にしようとしていたのですが DynamoDB Streams を直接イベントソースに指定出来ることを知って嬉しかったです。

ソースコード

以下のリポジトリにソースコード一式を格納しています。
ただし、残念ながら検証用のためいくつかハードコードしているパラメータがありこのままではデプロイ出来ないと思います。あくまでも参考までにというところです。

AWS SAM でインフラ構築しています。
ランタイムはもちろん、みなさんの大好きな .NET 6 です。

実行の様子

制限付きサンプル API を確認

SAM テンプレート上で API キーが必須な API と初期使用量プランを作成しています。

hoge0220restrictapi/template.yaml

:
Resources:
:
  Api:
    Type: AWS::Serverless::Api
    Properties:
      Name: hoge0220restrictapi
      StageName: hoge0220stage
      Auth:
        ApiKeyRequired: true
        UsagePlan:
          CreateUsagePlan: PER_API
          UsagePlanName: hoge0220usageplan

デプロイ後にリソースへ試しにアクセスしてみましょう。

% curl https://qn7vo53mvf.execute-api.ap-northeast-1.amazonaws.com/hoge0220stage/hello
{"message":"Forbidden"}

アクセスが出来ませんでしたね。

この時点でデプロイしたステージに関連付いた使用量プランが作成されていますが、API キーはひとつも関連づいていません。

API キー発行サービスを確認

「2. キーの発行と期限管理」をデプロイしたのち、エンドポイントへアクセスしてみましょう。
このサービスではAmazon.Lambda.AspNetCoreServer.Hostingを使っており、素の ASP.NET Core MVC アプリケーションのようなコード構成になっています。

Controller にルーティング設定と各種処理が実装されています。
POST メソッドに使用量プラン ID をパラメータとして渡すことで、新しい API キーを指定した使用量プランに関連付いた状態で作成することが出来ます。 GET メソッドで登録済みの(管理された)API キー一式が取得出来ます。

% curl -X POST "https://thtsbs9rv1.execute-api.ap-northeast-1.amazonaws.com/api/apikeyitems/0bk246"                                                   
true

% curl "https://thtsbs9rv1.execute-api.ap-northeast-1.amazonaws.com/api/apikeyitems/" | jq
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100   138  100   138    0     0    555      0 --:--:-- --:--:-- --:--:--   567
[
  {
    "apiKeyId": "2j7f0nf2cd",
    "apiKeyValue": "8lu0jnma5i9mxd75tceD98aRFeB2Je2UpWdY9K60",
    "creationTime": 1677018371,
    "expirationTime": 1677018671
  }
]

期限が設定されていると思いますが、これは API キー作成後に DynamoDB 項目として作成する際に有効期限を設定しています。 これはソースコード上で固定で 300 秒後を期限に設定しています。つまりこの API キーは 5 分間しか使えません。

hoge0220createkey/src/hoge0220createkey/Repositories/ApiKeyItemRepository.cs

:
    var newKey = new ApiKeyItem();
    newKey.ApiKeyId = createApiKeyResult.Id;
    newKey.ApiKeyValue = createApiKeyResult.Value;
    newKey.CreationTime = GetUnixTimeSecondsFromDateTime(DateTime.Now);
    newKey.ExpirationTime = GetUnixTimeSecondsFromDateTime(DateTime.Now.AddMinutes(5));
    await context.SaveAsync(newKey);
:

以下のように実際に API キーが関連づいていることも確認出来ます。

実際に API キーを使って、先程の制限された API へアクセスしてみましょう。

% curl -H "x-api-key:8lu0jnma5i9mxd75tceD98aRFeB2Je2UpWdY9K60" https://qn7vo53mvf.execute-api.ap-northeast-1.amazonaws.com/hoge0220stage/hello        
{"message":"hello world","location":"54.178.26.165"}

アクセスすることが出来ましたね。

DynamoDB テーブルの作成と TTL + DynamoDB Streams の有効化は SAM テンプレート上で行っていますが、SAM リソースだと期待したインターフェースがどれも公開されていなかったため、今回はここのみ CloudFormation リソースを使っています。

hoge0220createkey/template.yaml

:
  SampleTable:
    Type: AWS::DynamoDB::Table
    Properties: 
      TableName: ApiKeyTable
      TableClass: STANDARD
      BillingMode: PAY_PER_REQUEST
      AttributeDefinitions: 
        - AttributeName: ApiKeyId
          AttributeType: S
      KeySchema: 
        - AttributeName: ApiKeyId
          KeyType: HASH
      StreamSpecification: 
        StreamViewType: KEYS_ONLY
      TimeToLiveSpecification: 
        AttributeName: ExpirationTime
        Enabled: true
:

期限切れ後

300 秒が TTL なのですが、実際は DynamoDB の項目が削除されるまで 10 分くらいかかっていました。
後述しますが、DynamoDB の TTL は属性値に従って即削除となるものではありません。

次のように API キー管理サービスでキー一覧から取得できなくなるまで待ちます。

% curl "https://thtsbs9rv1.execute-api.ap-northeast-1.amazonaws.com/api/apikeyitems/" | jq
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100     2  100     2    0     0      1      0  0:00:02  0:00:01  0:00:01     1
[]

表示されなくなりましたね。
では、API キーを使って制限付き API へアクセスしてみましょう。

% curl -H "x-api-key:8lu0jnma5i9mxd75tceD98aRFeB2Je2UpWdY9K60" https://qn7vo53mvf.execute-api.ap-northeast-1.amazonaws.com/hoge0220stage/hello
{"message":"Forbidden"}

アクセスが拒否されました!

使用量プランを見てみると API キーが実際に削除されていることもわかります。

DynamoDB Streams をイベントソースにした Lambda 関数で API キー ID を取得して、API キーの実体を削除しています。
イベントソースの設定箇所は以下の様な形です。ここは公式ドキュメントを参考に SAM リソースのプロパティに落とし込んでいます。

hoge0220removekey/template.yaml

Resources:
  helloFromLambdaFunction:
    Type: AWS::Serverless::Function
    Properties:
:
      Policies:
        - AWSLambdaBasicExecutionRole
        - DynamoDBCrudPolicy:
            TableName: ApiKeyTable
        - AmazonAPIGatewayAdministrator
      Events:
        DynamoDBTTL:
          Type: DynamoDB
          Properties:
            Stream: arn:aws:dynamodb:ap-northeast-1:123456789012:table/ApiKeyTable/stream/2023-02-21T19:30:43.786
            BatchSize: 10
            Enabled: true
            FilterCriteria:
              Filters:
                - Pattern: "{\"userIdentity\":{\"type\":[\"Service\"],\"principalId\":[\"dynamodb.amazonaws.com\"]}}"
            StartingPosition: LATEST

要検討事項

API キーに期限を設定して運用するという目的は達成出来ていますが、実際にはいくつか検討すべき点がいくつかあります。

DynamoDB TTL は 48時間以内削除のベストエフォート

公式ドキュメント上は TTL を迎えた項目は 48 時間以内に削除される。とされています。
さらにそれもベストエフォートであるため場合によっては削除に時間がかかる場合もあるそうです。
具体的には削除リクエストが大量に存在している場合は時間がかかるそうです。

私が今回何度か試した限りでは全て 15 分以内に処理されている様子でした。

ここをもう少し有効期限に厳密に処理したい場合は DynamoDB の TTL 機能に頼るのではなく、定期的に削除処理をバックグラウンドプロセスで実行するなど別の仕組みが必要です。

API Gateway リソース操作の API クォータに注意する

基本的に AWS リソースを操作する類の API は、クォータが渋めでレートリミットに達しやすいです。
今回のように AWS の API に頼ったつくりの場合はそのあたりを気にする必要があります。

上記からすると今回の場合だと以下は気にする必要があります。
また、これらは全て上限緩和が出来ないので、改善する方法としては間に SQS キューを挟む、リトライ処理を行うなどが考えられます。

アクション クォータ
CreateApiKey アカウントあたり 1 秒ごとに 5 リクエスト
DeleteApiKey アカウントあたり 1 秒ごとに 5 リクエスト

API キーの作成・削除だと 5 リクエスト/秒ですが、API キーインポートだと 30 秒に 1 リクエストまでとなっているので、API キーの作成にインポートなど別の方式を使う場合は注意が必要です。

API Gateway のポータル画面や CLI 経由で直接 API キーを作成すると管理外になる

用意した管理用のサービスを経由する時だけ DynamoDB で管理される形となっています。
そのため、それ以外から API キーを作成した場合は管理外となり、定期的な削除の対象外となります。

まぁこれは良し悪しあると思いますが、もしそのあたりも管理したいのであれば EventBridge で API キー作成イベントを拾って処理してやる必要などがありそうです。

さいごに

本日は Amazon API Gateway の API キーに有効期限を設定してみました。

今回は検証を兼ねて一通りとりあえず作ってみたという形なので荒いところがたくさんありそうですが、標準機能でサポートされていないものも AWS API から操作出来る範囲で自動化すると間接的に要件を実現出来る場合があります。
また、TTL の 48 時間の件は作ってるうちに気がついたのですが、今回 SAM であっという間に作成出来たのでまずは一度通して動かすと考慮必要なものに気がつけたりするので良いですね。