💨

IoTデバイスから直接いろんなAWSサービスにアクセスするのが便利

2023/11/12に公開

Credential Provider

Credential Providerを使うと、タイトルのことができる。これはかなり便利。アイデアとしては以下のような感じ。

Credential Provider

以下に具体的な方法を記載する。

IAMロールを作る

Trust Policy trust-policy.json を作っておく。

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "Service": "credentials.iot.amazonaws.com"
      },
      "Action": "sts:AssumeRole"
    }
  ]
}

Roleを作って、それにTrust Policyをアタッチ

aws iam create-role \
--role-name s3_role \
--assume-role-policy-document file://trust-policy.json

# 確認
aws iam get-role --role-name s3_role

S3アクセス用の Policy を作っておく。

s3-access-policy.json

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Action": [
        "s3:PutObject"
      ],
      "Resource": "*"
    }
  ]
}

上記 Policy を使って、s3_access_policy を作成する。

export S3_POLICY_ARN=$(aws iam create-policy \
--policy-name s3_access_policy \
--policy-document file://s3-access-policy.json \
--query Policy.Arn \
--output text)

PolicyをRole s3_role にアタッチ

aws iam attach-role-policy \
--role-name s3_role \
--policy-arn $S3_POLICY_ARN

# 確認
aws iam list-attached-role-policies --role-name s3_role

RoleのARNを取得

export S3_ROLE_ARN=$(aws iam list-roles --query "Roles[?RoleName=='s3_role'].Arn" --output text)

ロールエイリアスを作成

デバイスがトークンをリクエストするときに、ロールを直接指定してしまうと、たとえばロール名が変わったときにファームウェア側のアップグレードが必要になるなど面倒なことになるので、ロールにエイリアスを作って、デバイス側からはロール名の変更が見えないようにしようというもの[1]

aws iot create-role-alias \
--role-alias s3_role_alias \
--role-arn $S3_ROLE_ARN
export S3_ROLE_ALIAS_ARN=$(aws iot describe-role-alias \
--role-alias s3_role_alias \
--query roleAliasDescription.roleAliasArn \
--output text)
echo $S3_ROLE_ALIAS_ARN

作成したロールエイリアスが証明書での認証後にでAssume RoleできるようにIoTポリシーのファイルを作る。

cat << END > iot-policy.json
{
  "Version": "2012-10-17",
  "Statement": {
    "Effect": "Allow",
    "Action": "iot:AssumeRoleWithCertificate",
    "Resource": "$S3_ROLE_ALIAS_ARN"
  }
}
END

iot_policy を作成

aws iot create-policy \
--policy-name iot_policy \
--policy-document file://iot-policy.json

Thingを作る

Thingを作る

aws iot create-thing --thing-name thing_x
{
    "thingName": "thing_x",
    "thingArn": "arn:aws:iot:ap-northeast-1:<ACCOUNT_ID>:thing/thing_x",
    "thingId": "d62b589c-77d0-48a5-b71d-5be6c844667f"
}

クライアントの証明書と秘密鍵を取得する

export CERTIFICATE_ARN=$(aws iot create-keys-and-certificate \
    --set-as-active \
    --certificate-pem-outfile "./device.pem.crt" \
    --public-key-outfile "./public.pem.key" \
    --private-key-outfile "./private.pem.key" \
    --query "certificateArn" --output text)

証明書にThingとIoT Policyを紐づける

aws iot attach-thing-principal --thing-name thing_x --principal $CERTIFICATE_ARN
aws iot attach-policy --target $CERTIFICATE_ARN --policy iot_policy

IoT Coreのルート証明書を取得

wget -q https://www.amazontrust.com/repository/AmazonRootCA1.pem

トークンをリクエストしてみる

トークンをリクエストするエンドポイントを取得する。エンドポイントタイプは iot:CredentialProvider である。

export ENDPOINT_NAME=$(aws iot describe-endpoint \
--endpoint-type iot:CredentialProvider \
--query endpointAddress \
--output text)

# 確認
echo $ENDPOINT_NAME

トークンの取得

curl --cert device.pem.crt \
--key private.pem.key \
-H "x-amzn-iot-thingname: thing_x" \
--cacert AmazonRootCA1.pem \
https://$ENDPOINT_NAME/role-aliases/s3_role_alias/credentials
{"credentials":{"accessKeyId":"ADIASYRA4ZVER27QKYXQ","secretAccessKey":"rxvDAueXJKu9S4JUGP32a/zqfQzP8yV6C9aQGxsT","sessionToken":"IQoJb3JpZ2luX2VjEKb//////////wEaDmFwLW5vcnRoZWFzdC0xIkYwRAIgI5yspZ554ktQM0fUUYTyksJKfuFF7v43TBH5I4IqPZwCIAMWEs2wMXudF6qBWpaExVnmRo3fmx3n4BBgFiVDsCmWKsMDCND//////////wEQARoMNTMzNzE4NjcwNjY1IgyuC6Gnjd4LN+WSExgqlwNiqF8PE4XL3SXmKdri2sSKpnAzAlFMHAmHcFDQJ+3HflCyWDJ8JtdF5IMu2f1h/GTi5WmwuurIXl55D25NNbL/JpJ60T0YE1B0vzPV8I94vxFquMsazlamzO58mGyi5hHbR7PJfkiajGMe9hUG982+EUA1rYvRq0bEmNJRLJzWaXFiOmk0MCSnIf9JJT0fC9zkF8mRRaKuxTixX/sjCMUtTVKFA1PLPwR891RFGAe7ruYvqQCKhzmy60EVrAPNsFKIr3AaHLJayyzvq9nA/oLJG3epLsSciiuXfDgSg7oV3aoQ6HiACpBv4voBv/bnm6d0sBuD1byOmifxptQ/iav/skA43sfstO6SA8svZoWoUfp9gPIUD5iMWwmRUhnAIlPwtJapepIkyR+MP7nOPo1BKTwQyKW0A+/d69SUTsg26rgroy1f5yKBxk/xquj961MQnBBXlY09M7Ah0v3POx9S/F6xBi9ElDsNIz1Ya997tvOldbEvEm8Z2ztZ5cYEjq5kZGLe6JTRPPXg7QeUmqiin1Lb/0+33zCnl/2pBjqXAd7/yxIqIbU63rdR6OvXCy0x3n62iRqRN+Uduq7rpdQ/R9p4h+oimXiKNVKapLAwzKOX1PkpSilc8O4tiLk9zhFKuGfpd0/gzrG6+2Es4QrBLsbAfunwtcu7ZtnuGxqDk2OkKkITFn8f3sRBaHpL4sx7/Xg5AZs3Z9R32IhzZj0df7BMHPzw0uJzfW0GI86QOhrXUcKZemg=","expiration":"2023-10-30T07:22:31Z"}}% 

S3にアップロードしてみる

例えば、デバイス上のファイルをS3にアップロードするRubyクライアントコード[2]

require 'net/http'
require 'json'
require 'aws-sdk-s3'

ENDPOINT_URL = ENV["ENDPOINT_NAME"]
THING_NAME = "thing_x"
ROLE_ALIAS = "s3_role_alias"

uri = URI.parse("https://#{ENDPOINT_URL}/role-aliases/#{ROLE_ALIAS}/credentials")
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = true
http.key = OpenSSL::PKey::RSA.new(File.read("private.pem.key"))
http.cert = OpenSSL::X509::Certificate.new(File.read("device.pem.crt"))
http.ca_file = 'AmazonRootCA1.pem'
http.verify_mode = OpenSSL::SSL::VERIFY_PEER

request = Net::HTTP::Get.new(uri.request_uri)
request["x-amzn-iot-thingname"] = THING_NAME

response = http.request(request)

if response.code.to_i != 200
    puts "Error: #{response.code} - #{response.message}"
    exit 1
end

credential_data = JSON.parse(response.body)

Aws.config.update({
    region: 'ap-northeast-1',
    credentials: Aws::Credentials.new(
        credential_data['credentials']['accessKeyId'],
        credential_data['credentials']['secretAccessKey'],
        credential_data['credentials']['sessionToken']
    )
})

s3 = Aws::S3::Resource.new
obj = s3.bucket('2heozhb6').object('file_y')
obj.upload_file('./file_y')

参考

脚注
  1. エイリアスなしでもできるかなと思い、直接ロールを指定してやってみようと思うが、デバイスがリクエストするエンドポイントのパスがロールエイリアスを指定するような構成になっているので、ロールエイリアスを使わないとできないと思われる。 ↩︎

  2. 本来はAWS IoT Device SDKを使うべきだと思うが、Java と C++ でしかできないようなのでここはAWS SDKでお茶を濁しておく。 ↩︎

Discussion