はじめに
目的
この記事はローカルのPowerShellスクリプトから、Microsoft Defender for EndpointのAPIにJWT認証でアクセスすることが目的です。
Defender for Endpointと言いつつ実質的にDefenderポータルの機能にアクセス可能です。例えばデバイス一覧を取得するURLはこちらです。
https://api.securitycenter.microsoft.com/api/machines
[参考]
Supported Microsoft Defender for Endpoint APIs
背景
まずEntra ID上にMicrosoft Defender for Endpointへアクセスするためのアプリケーション(サービスプリンシパル)を登録しますが、その際、資格情報として証明書とクライアントシークレットを使えます。
クライアントシークレットの認証手順の方がシンプルなためこちらを選ぶことが多いと思いますが、今回はアクセス元ホストを制限するためにあえて証明書を使用します。
そして証明書認証を使用するとJWT認証を使う必要が出てくるため、PowerShellでJWT認証するためのスクリプトを書くことになるというわけです。
ちなみにクライアントシークレットを使用する場合はこのように非常にシンプルです。
$TenantId = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
$AppId = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
$AppSecret = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
$Scope = 'https://api.securitycenter.microsoft.com'
$AuthUri = "https://login.microsoftonline.com/$tenantId/oauth2/token"
$authBody = [Ordered] @{
resource = "$Scope"
client_id = "$AppId"
client_secret = "$AppSecret"
grant_type = 'client_credentials'
}
$res = Invoke-RestMethod -Method Post -Uri $AuthUri -Body $authBody -ErrorAction Stop
クライアントシークレットの場合はこのようにシンプルですが、証明書認証の場合JWT(JSON Web Token)を作成する必要があるため、非常に複雑なスクリプトになります。
JWTの詳細についてここでは説明しませんが、JWT生成の大まかな流れは以下のとおりです。
- JWTヘッダとJWTペイロードを作成
- JWTヘッダ、JWTペイロードをそれぞれをBase64エンコードしてドット「.」で接続
- その全体を証明書の秘密鍵で署名してBase64エンコードし、JWT署名を作成
- 最後にJWTヘッダ、JWTペイロード、JWT署名をドット「.」で接続
参考にした記事
この記事は以下の記事を参考にしています。
How to get access token from client certificate? Can we use client thumbprint directly?
事前準備
サービスプリンシパルの作成
詳細は省略します。Entra IDから一般的な手順で「アプリの登録」をしてください。
自己署名証明書の作成
手動で作成する方法もありますが、以下のPowerShellスクリプトで簡単に作成します。後でサービスプリンシパルにアップロードする必要があるため「.cer」形式ファイルにエクスポートもしておきます。
$certname = "SampleCertificate"
$cert = New-SelfSignedCertificate -Subject "CN=$certname" `
-CertStoreLocation "Cert:\CurrentUser\My" `
-KeyExportPolicy Exportable -KeySpec Signature -KeyLength 2048 `
-KeyAlgorithm RSA -HashAlgorithm SHA256
Export-Certificate -Cert $cert -FilePath ("D:\Temp\" + $certname + ".cer")
作成された証明書の拇印を後ほど使いますので、「ユーザー証明書の管理」から確認しておきます。(スタートメニューから「ファイル名を指定して実行」を選択し「certmgr.msc」で起動)
「証明書 - 現在のユーザー>個人>証明書」を開き、上で作成した証明書をダブルクリックで開いて「詳細」タブのフィールド「拇印」で確認します。
証明書のアップロード
Entra IDで先ほど作成したアプリを開き、左メニュー「証明書とシークレット」をクリック、「証明書」タブの「証明書のアップロード」から、上記で書き出した「.cer」形式ファイルを選択、アップロードします。
以上で準備ができました。
PowerShellスクリプトの作成
定数類の定義
まずスクリプトの冒頭で認証に使う各種定数を定義します。
# テナントID
$TenantId = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
# アプリ(サービスプリンシパル)ID
$AppId = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
# Microsoft Defenderのapi URL
$Scope = "https://api.securitycenter.microsoft.com"
# OAuth認証のためのURL
$AuthUri = "https://login.microsoftonline.com/$TenantId/oauth2/token"
JWTヘッダの作成
証明書のハッシュを取得
作成した証明書の拇印で証明書を取得し、そのハッシュ値をBase64エンコードした文字列を作成します。
$Certificate = Get-Item -Path "Cert:\CurrentUser\My\(40桁英数字の拇印)"
$CertificateBase64Hash = [System.Convert]::ToBase64String($Certificate.GetCertHash())
証明書を取得する際に以下の方法を使うとエラーになるため要注意です。
# 誤った例
$Certificate = Get-ChildItem -Path "Cert:\CurrentUser\My" |
Where-Object Subject -eq "CN=SampleCertificate"
JWTヘッダの組み立て
JWTヘッダの各行(各クレーム)についての詳細は説明しません。「alg」は後ほど実行する署名のアルゴリズムですが、ここではRSAとSHA256の組み合わせを使用するため「RS256」です。「typ」は「JWT」固定です。
「x5t」に上記で作成した証明書のハッシュ値のBase64エンコード文字列を指定します。
ただし、以下Base64でエンコードされた文字列をJWTに使用する場合は、つねに「+」「/」「=」の3文字を削除する必要があります。そのため-replace '\+','-' -replace '/','_' -replace '='
という部分が必要になります。
$JWTHeader = @{
alg = "RS256"
typ = "JWT"
x5t = $CertificateBase64Hash -replace '\+','-' -replace '/','_' -replace '='
}
JWTペイロードの作成
JWT有効期限の指定
JWTペイロードを作成する際はJWTの有効期間を設定する必要があるため、まずその部分から記述します。ここではエポック時間で現時点から2分後までとします。
# 失効日時
$StartDate = (Get-Date "1970-01-01T00:00:00Z" ).ToUniversalTime()
$JWTExpirationTimeSpan = (New-TimeSpan -Start $StartDate -End (Get-Date).ToUniversalTime().AddMinutes(2)).TotalSeconds
$JWTExpiration = [math]::Round($JWTExpirationTimeSpan,0)
# 有効開始日時
$NotBeforeExpirationTimeSpan = (New-TimeSpan -Start $StartDate -End ((Get-Date).ToUniversalTime())).TotalSeconds
$NotBefore = [math]::Round($NotBeforeExpirationTimeSpan,0)
JWTペイロードの作成
有効期限が決まればJWTペイロードを作成できます。
$JWTPayLoad = @{
# 認証のためのエンドポイント
aud = $AuthUri
# Issuer: JWTの発行者 = サービスプリンシパルのアプリケーションID (クライアントID)
iss = $AppId
# JWT Subject: JWTの用途 = サービスプリンシパルのアプリケーションID (クライアントID)
sub = $AppId
# Not Before: JWTの有効開始日時 (いつから)
nbf = $NotBefore
# Expiration Time: JWTの失効日時 (いつまで)
exp = $JWTExpiration
# JWT ID: ランダムなGUIDを生成
jti = [guid]::NewGuid()
}
JWTヘッダ、JWTペイロードのBase64エンコード
以上で作成したJWTヘッダ、JWTペイロードをBase64エンコードしてドットでつなげます。
# JWTヘッダをBase64エンコード
$JWTHeaderToByte = [System.Text.Encoding]::UTF8.GetBytes(($JWTHeader | ConvertTo-Json))
$EncodedHeader = [System.Convert]::ToBase64String($JWTHeaderToByte)
# JWTペイロードをBase64エンコード
$JWTPayLoadToByte = [System.Text.Encoding]::UTF8.GetBytes(($JWTPayload | ConvertTo-Json))
$EncodedPayload = [System.Convert]::ToBase64String($JWTPayLoadToByte)
# Base64エンコードしたJWTヘッダとJWTペイロードをドットでつなげる
$JWT = $EncodedHeader + "." + $EncodedPayload
ここまででJWTの前半、署名以外の部分が出来ました。次はこの$JWT
を証明書の秘密鍵で署名したものをBase64エンコードし、$JWTのさらに後ろにドットでつなげます。
"JWTヘッダ.JWTペイロード"を署名
証明書から秘密鍵の取り出し
署名に必要な秘密鍵を証明書から取り出します。証明書は先ほど$Certificate
という変数に代入してあります。
$PrivateKey = ([System.Security.Cryptography.X509Certificates.RSACertificateExtensions]::GetRSAPrivateKey($Certificate))
署名に使うハッシュアルゴリズムとパディング方式を指定
次に、署名に使うハッシュアルゴリズムとパディング方式を指定します。パディングについては正直よく理解していないため各種資料をあたっていただければと思います。
$HashAlgorithm = [Security.Cryptography.HashAlgorithmName]::SHA256
$RSAPadding = [Security.Cryptography.RSASignaturePadding]::Pkcs1
署名の作成
これで署名の準備ができたので署名を作成します。
JWTヘッダとJWTペイロードをドット(.)でつなげた文字列を、秘密鍵を使って指定のハッシュアルゴリズムとパティング方式で署名し、Base64エンコードしてから、例によって「+」「/」「=」の3文字を削除します。
$SignedData = $PrivateKey.SignData([System.Text.Encoding]::UTF8.GetBytes($JWT),$HashAlgorithm,$RSAPadding)
$Signature = [Convert]::ToBase64String($SignedData) -replace '\+','-' -replace '/','_' -replace '='
JWTの完成
こうして出来た署名をJWTにさらにドットでつなげてJWTの完成です。
$JWT = $JWT + "." + $Signature
トークンの取得
ここまででJWTが完成したので認証エンドポイントに対して認証を実行し、トークンを取得します。認証の実行にはヘッダ部分とボディー部分が必要ですが、JWTはヘッダ部分とボディー部分の両方に使います。
認証ヘッダの作成
認証ヘッダはAuthorization
のBearer
としてここまでで作成したJWTを渡すだけです。
$Header = @{
Authorization = "Bearer $JWT"
}
認証ボディーの作成
$Body = @{
# サービスプリンシパルのアプリケーションID (クライアントID)
client_id = $AppId
client_assertion = $JWT
client_assertion_type = "urn:ietf:params:oauth:client-assertion-type:jwt-bearer"
resource = $Scope
grant_type = "client_credentials"
}
認証の実行とトークンの取得
上記の認証ヘッダ、認証ボディーで認証を実行します。
$req = Invoke-RestMethod -Uri $AuthUri -Method Post `
-Headers $Header -Body $Body `
-ContentType 'application/x-www-form-urlencoded'
$token = $req.access_token
以上でDefender for EndpointのAPIを呼び出すためのトークンが取得できました。
Defender for Endpoint APIの呼び出し
一例としてDefender for Endpointの全アラートを取得してみます。上記で取得したトークンの頭に'Bearer 'を付けて、ヘッダーのAuthorizationに設定して、呼び出すだけです。
アラートを取得するためのAPIのURLはhttps://api.securitycenter.microsoft.com/api/alerts
です。
$headers = @{
'Content-Type' = 'application/json'
Authorization = ("Bearer " + $token)
}
$reqURL = 'https://api.securitycenter.microsoft.com/api/alerts'
$res = Invoke-RestMethod -Uri $reqURL -Headers $headers -Method Get
$items = $res.value
別の例として、Defender for Endpointが認識しているデバイス(オンボードされているかどうかにかかわらず)をすべて取得するスクリプトは以下のようになります。APIのURLの末尾をmachines
に変えるだけです。
$headers = @{
'Content-Type' = 'application/json'
Authorization = ("Bearer " + $token)
}
$reqURL = 'https://api.securitycenter.microsoft.com/api/machines'
$res = Invoke-RestMethod -Uri $reqURL -Headers $headers -Method Get
$items = $res.value
まとめ
サービスプリンシパルのアプリケーションID(クライアントID)とクライアントシークレットを利用すると、認証の手順は非常にシンプルなのですが、証明書を使う場合はここまで述べたように非常に煩雑になります。
クライアントシークレットは漏えいすると第三者が任意の端末から認証できてしまうため、特定の端末で作成した自己署名証明書による認証の方が、認証元の端末を制限でき、セキュリティレベルは一段階上がります。
サービスプリンシパル経由でAPIを呼び出す端末を限定できる場合は、スクリプトが煩雑になっても、JWTを利用した証明書認証の方がよさそうです。
以上です。