SORACOM Funnelで使うAWSアクセスキーを自動更新してみる

SORACOM Funnelで使うAWSアクセスキーを自動更新してみました。 SORACOM APIを使っています。
2022.08.15

AWSアクセスキーは、90日のローテーションが推奨されています。AWS Security Hubでもローテーションのルールがあります。

今回は、SORACOMで使っているAWSアクセスキーのローテーションを自動更新するスクリプトを作ってみました。

AWSアクセスキーのローテーション自動化を推奨するものではありません。組織のルール、付与されている権限の内容、扱っている情報、などによって判断してください。

おすすめの方

  • AWSアクセスキーをプログラム等でローテーションしたい方
  • SORACOM APIを使いたい方

本記事で扱う内容

本記事では、次の内容を扱います。

  1. 新しいAWSアクセスキーを作成する
  2. 新しいSORACOM認証情報を作成する
  3. SORACOM SIMグループの一覧を取得する
  4. SORACOM Funnelの設定にあるSORACOM認証情報を更新する

なお、次の内容は扱いません。

  • 不要になったAWSアクセスキーの削除
  • 不要になったSORACOM認証情報の削除

理由は次の2つです。

  1. AWSアクセスキー更新後、数日ほど様子を見て古いAWSアクセスキーが使われていないことを確認し、古いAWSアクセスキーを削除するため、時系列が違う(数日後に実施するため)
  2. 不要になったSORACOM認証情報を削除するために、削除する認証情報IDをどこかで保持する仕組みが必要となる

事前準備

まずは、事前準備としてSORACOM FunnelでAWSアクセスキーを使っている状態にします。

  • AWS
    • IAMユーザを作成する
    • アクセスキーを取得する
    • AWS IoT Coreエンドポイント取得
  • SORACOM
    • AWS認証情報を登録する
    • SIMグループを作成する
    • SORACOM Funnelの設定をする
    • SORACOM APIを使う準備

IAMユーザを作成する

SORACOM Funnelでの利用とアクセスキー発行・削除の権限を持つIAMユーザを作成します。

iam.yaml

AWSTemplateFormatVersion: 2010-09-09
Description: IAM User for SORACOM Funnel

Resources:
  # SORACOM Funnel用のIAMユーザ
  SoracomFunnelUser:
    Type: AWS::IAM::User
    Properties:
      UserName: soracom-funnel-sample-user

  # IAMユーザに付与するIAMポリシー
  SoracomFunnelUserPolicy:
    Type: AWS::IAM::Policy
    Properties:
      PolicyName: soracom-funnel-user-policy
      PolicyDocument:
        Version: "2012-10-17"
        Statement:
          # SORACOM Funnel用
          - Effect: Allow
            Action: iot:Publish
            Resource: !Sub arn:aws:iot:${AWS::Region}:${AWS::AccountId}:topic/soracom/*
          # AWSアクセスキーの更新用
          - Effect: Allow
            Action:
              - iam:CreateAccessKey
              - iam:DeleteAccessKey
            Resource: !GetAtt SoracomFunnelUser.Arn
      Users:
        - !Ref SoracomFunnelUser

デプロイします。

aws cloudformation deploy \
    --template-file iam.yaml \
    --stack-name SORACOM-Funnel-IAM-User-Sample-Stack \
    --capabilities CAPABILITY_NAMED_IAM \
    --no-fail-on-empty-changeset

AWSアクセスキーを取得する

aws iam create-access-key \
    --user-name soracom-funnel-sample-user

AWS IoT Coreエンドポイント取得

下記コマンドで取得できます。

aws iot describe-endpoint \
    --endpoint-type iot:Data-ATS

SORACOMにAWS認証情報を登録する

SORACOM Webコンソールにアクセスし、右上のメニューにある「セキュリティ」を選択します。

セキュリティを選択

続いて、「認証情報ストア」にある「認証情報を登録」を選択します。

認証情報を登録する

種別は「AWS認証情報」を選択し、さきほど取得したアクセスキーを貼り付けて登録します。 AWSアクセスキーの変更は、この認証情報自体を登録し直すので、分かりやすいように認証情報IDに日付を記載しています。 そのためのPrefixとしてaws-soracom-funnel-sampleを利用します。

  • aws-soracom-funnel-sample-220810

SORACOMにAWS認証情報を登録する

SIMグループを作成する

適当なSIMグループを作成します。

今回は、「test-1」と「test-2」を使います。

いまのSIMグループは3つある

SORACOM Funnelの設定をする

それぞれのSIMグループに対して、SORACOM Funnelの設定を行います。これら2つのSIMグループの「AWS認証情報」をこのあと更新します。(いまは「220810」です)

SORACOM Funnelを設定する(test1)

SORACOM Funnelを設定する(test2)

SORACOM APIを使う準備

  • SAMユーザの作成
  • SAMユーザのアクセス権限を付与
  • SAMユーザの「認証キーID&認証キーシークレット」を取得

上記については、次のブログを参考に行います。

なお、今回のアクセス権限は下記となります。

{
  "statements": [
    {
      "api": [
        "Credential:createCredential",
        "Group:listGroups",
        "Group:putConfigurationParameters"
      ],
      "effect": "allow"
    }
  ]
}

SORACOM APIを使うための権限を設定する

AWSアクセスキーを自動更新する

次の処理を行うスクリプトを作成します。

  • 新しいAWSアクセスキーを作成して、SORACOM認証情報を作成して、SORACOM Funnel設定を更新する
お試し実装のため、下記となっています。本番運用時には適宜検討し、実装してください。

  • APIトークンの有効期限はデフォルト設定です。
  • レートリミットの考慮をしていません。

API トークンの有効期間について

制限事項と注意事項

スクリプトを作成する

app.py

import json
import boto3
import requests
from datetime import datetime
from typing import Tuple

SORACOM_ENDPOINT = 'https://api.soracom.io/v1'

SORACOM_KEY_ID = 'keyId-aaa'
SORACOM_SECRET = 'secret-bbb'


AWS_IAM_USER_NAME = 'soracom-funnel-sample-user'

CREDENTIAL_ID_BASE = 'aws-soracom-funnel-sample'


def main() -> None:
    # 新しいAWSアクセスキーを作成
    aws_access_key, aws_secret_access_key = create_aws_access_key()

    api_key, token = get_token()

    # SORACOM認証情報を作成
    create_credential(api_key, token, aws_access_key, aws_secret_access_key)

    # SIMグループを取得
    groups = get_groups(api_key, token)

    for item in groups:
        if 'SoracomFunnel' not in item['configuration']:
            continue
        if CREDENTIAL_ID_BASE not in item['configuration']['SoracomFunnel']['credentialsId']:
            continue
        # SORACOM Funnelの認証情報を更新する
        group_id = item['groupId']
        print(group_id)
        put_group_configuration(api_key, token, group_id)


def create_aws_access_key() -> Tuple[str, str]:
    iam = boto3.client('iam')
    resp = iam.create_access_key(
        UserName=AWS_IAM_USER_NAME
    )
    return resp['AccessKey']['AccessKeyId'], resp['AccessKey']['SecretAccessKey']


def get_credential_id():
    today = datetime.now().strftime('%y%m%d')
    return f'{CREDENTIAL_ID_BASE}-{today}'


def get_token() -> Tuple[str, str]:
    headers = {'Content-Type': 'application/json'}
    data = {
        'authKeyId': SORACOM_KEY_ID,
        'authKey': SORACOM_SECRET,
    }
    resp = requests.post(f'{SORACOM_ENDPOINT}/auth', headers=headers, data=json.dumps(data))

    d = resp.json()

    return d['apiKey'], d['token']


def create_credential(
    api_key: str, 
    token: str,
    aws_access_key_id: str,
    aws_secret_access_key: str) -> None:

    headers = {
        'Content-Type': 'application/json',
        'X-Soracom-API-Key': api_key,
        'X-Soracom-Token': token,
    }
    data = {
        'type': 'aws-credentials',
        'credentials': {
            'accessKeyId': aws_access_key_id,
            'secretAccessKey': aws_secret_access_key,
        }
    }

    credential_id = get_credential_id()
    resp = requests.post(
        f'{SORACOM_ENDPOINT}/credentials/{credential_id}',
        headers=headers,
        data=json.dumps(data)
    )


def get_groups(api_key: str, token: str) -> None:
    headers = {
        'Content-Type': 'application/json',
        'X-Soracom-API-Key': api_key,
        'X-Soracom-Token': token,
    }
    resp = requests.get(
        f'{SORACOM_ENDPOINT}/groups',
        headers=headers
    )

    return resp.json()


def put_group_configuration(api_key: str, token: str, group_id: str) -> None:
    headers = {
        'Content-Type': 'application/json',
        'X-Soracom-API-Key': api_key,
        'X-Soracom-Token': token,
    }
    data = [
        {
            'key': 'credentialsId',
            'value': get_credential_id(),
        },
    ]
    resp = requests.put(
        f'{SORACOM_ENDPOINT}/groups/{group_id}/configuration/SoracomFunnel',
        headers=headers,
        data=json.dumps(data)
    )


if __name__ == '__main__':
    main()

実行する

python app.py

結果を確認する

新しいAWSアクセスキーが発行された(8/15)

作成日が「220815」のAWSアクセスキーができました。

新しいAWSアクセスキーが発行された

新しいSORACOM認証情報が登録された(8/15)

登録日時が「220815」のAWS認証情報が登録されました。

新しいSORACOM認証情報が登録された

SIMグループのSORACOM FunnelのAWS認証情報が更新された(8/15)

それぞれのSIMグループのSORACOM Funnelの設定について、AWS認証情報が「220815」に更新されました。

SIMグループのSORACOM Funnelの認証情報が更新された(test1)

SIMグループのSORACOM Funnelの認証情報が更新された(test2)

さいごに

SORACOMで使うAWSアクセスキーを自動更新してみました。今回は手元で実行しましたが、Lambdaを使えば完全自動化もできそうです。 ただし、その場合は、失敗した際の検知&通知の仕組みを忘れずに構築すると良いですね。

参考