シームレスSSOを構成後、Active Directory上に「AZUREASSSOACC」というオブジェクトが構成されます。
Microsoftの公開情報によるとキーのロールオーバーは必須ではありませんが、セキュリティ上の理由から少なくとも30日ごとに、コンピューターアカウントにあるAZUREADSSOACCをロールオーバーすることを強く推奨されています。
作業内容としてはPowerShellでコマンドを2行程実行するだけで内容としては難易度が高いものではありませんが、30日間に1回、毎回手動で実行するとなるとそれなりに管理者の負担になると思います。
そこで、今回は実行用のスクリプトを作成し、それをタスクスケジューラーで決まった日に実行することで、AZUREADSSOACCオブジェクトキーのローテーションを行う方法を紹介したいと思います。
AZUREADSSOACCの役割の紹介
シームレスSSOは、オンプレミスのActive Directory環境におけるKerberos認証を利用して、ユーザーがEntra IDにアクセスする際に資格情報の再入力を不要にする機能になります。
AZUREADSSOACCオブジェクトは、このKerberos認証プロセスで発行されるチケットを復号化するために使用されるオブジェクトになります。
AZUREADSSOACCオブジェクトには、シームレスSSOで使用されるMicrosoft Entra IDのSPNの情報が登録されており、SPNはKerberos認証においてサービスを識別されるために使用されます。
これにより、クライアントは正しいサービスに対して認証を要求できます。
※ シームレスSSOの詳細については下記のURLも参考にしてください。
AZUREADSSOACCキーのローテーション方法
本節ではAZUREADSSOACCキーのローテーションの方法について記載します。
ここで紹介する方法はタスクスケジューラーを使用して定期的に決まったタイミングで今回のスクリプトを実行する方法になります。
毎回スクリプトを実行する方法でも問題なく実行することはできますが、管理者の負担を減らすためにも、今回はタスクスケジューラーを使用した方法を紹介したいと思います。
パスワードファイルの作成
ここではパスワードファイルの作成方法について記載します。
スクリプト内に平文のパスワード情報を記載しても実行は可能ですが、平文のままパスワード情報を記載するのはセキュリティ上の観点から望ましくないため、ここではパスワードファイルを作成してからスクリプトを実行します。
- 管理者権限でPowerShellを起動します。
- 下記のコマンドを実行し、クレデンシャル情報を$Cred変数に格納します。
$Cred = Get-Credential
- 資格情報のウィンドウが表示されたらパスワード情報を暗号化したいユーザーのユーザー名とパスワードを入力し、[OK]をクリックします。
- 取得したクレデンシャル情報をConvertFrom-SecureStringコマンドを使用して暗号化された文字列に変換し、指定したファイルパスに出力します。
$Cred.Password | ConvertFrom-SecureString | Set-Content C:\TestPass.pass
※今回はCドライブのWORKフォルダ直下に「TestPass.pass」という形で出力しますが、パスとファイル名に関しては各々で変更してください。
- 実行できたら、暗号化されたパスワードファイルが出力されているか確認します。
※ 正しく実行されていると、パスワードが暗号化された文字列が出力されています。
スクリプトの概要
AZUREADSSOACCのキーのロールオーバーを自動化するスクリプト
$ErrorActionPreference = "Stop"
. "C:\Utility\Common\Log.ps1"
#こちらは今回作成したスクリプトパスを指定するコマンドになります。ログを置いてあるファイルパスを入力してください。
$Log = New-Object Log("C:\Utility\AZUREADSSOACCKeyRollOver\AZUREADSSOACCKeyRollOver.ps1")
$Log.Write("Execeute Start")
try{
#このスクリプトはスクリプトファイルを実行するユーザーを指定してください。
$AzureADUser = 'testuser@example.com'
$ADUser = 'domain\testuser'
#ここでは前章で作成した暗号化されたファイルパスを読み込み、パスワード情報を復号化します。パスではパスワードファイルが置いてあるパスを指定してください。
$AzureADPass = Get-Content C:\Utility\Credential\TestPass.pass | ConvertTo-SecureString
$ADPass = Get-Content C:\Utility\Credential\TestPass2.pass | ConvertTo-SecureString
$AzureADCred = New-Object System.Management.Automation.PSCredential($AzureADUser, $AzureADPass)
$ADCred = New-Object System.Management.Automation.PSCredential($ADUser, $ADPass)
#AzureADSSO.ps1のモジュールを読み込みます。基本的にはMicrosoft Entra Connectで実施することを想定しておりますが、Microsoft Entra Connect以外で実施する場合はMicrosoft Entra Connectからこちらのモジュールを持ってきてください。
Import-Module "C:\Program Files\Microsoft Azure Active Directory Connect\AzureADSSO.psd1"
$Log.Write("Executing New-AzureADSSOAuthenticationContent")
New-AzureADSSOAuthenticationContext -CloudCredentials $AzureADCred
#特定のADフォレスト内でAZUREADSSOコンピューターアカウントのKerberos復号化キーを更新し、Microsoft EntraID内でこのAzureADSSOACCキーを更新します。
$Log.Write("Executing Update-AzureADSSOForest")
Update-AzureADSSOForest -OnPremCredentials $ADCred -PreserveCustomPremissionsOnDesktopSsoAccount
}
catch {
$Log.WriteError($_)
Exit 1
}
$Log.Write("Execute End")
スクリプトの実行状況やエラーをログファイルに記録するスクリプト
class Log {
$ScriptName
$ScriptPath
$StackTrace
$CommandName
$WritedString
$SilenceCounter
Log() {
$this.StackTrace = Get-PSCallStack
$maches = [regex]::Matches($this.StackTrace,'[A-Z]:\\.*?\.PS1',[System.Text.RegularExpressions.RegexOptions]::IgnoreCase)
$this.ScriptName = $maches[$maches.Count -1].Value
if ([string]::IsNullOrEmpty($this.ScriptName)) {
throw "呼び出し元情報が無い状態でLogクラスが生成されました。"
}
$this.Initialize()
}
Log([string]$Out_Path) {
$this.ScriptName = $Out_Path
$this.Initialize()
}
Initialize() {
$this.ScriptPath = Split-Path $this.ScriptName -parent
$this.CommandName = [System.IO.Path]::GetFileNameWithoutExtension($this.ScriptName)
$this.WritedString = ""
$this.SilenceCounter = 0
}
Write([string]$message) {
$this.WriteExecute($message, $false)
}
WriteError([string]$message) {
$this.WriteExecute($message, $true)
}
WriteOnceEvery10Times([string]$message) {
if ($this.SilenceCounter -ge 9) {
$this.WriteExecute('[W1E10]' + $message, $false)
$this.SilenceCounter = 0
} else {
$this.SilenceCounter += 1
}
}
WriteOnceEvery10Times([string]$message, $VerbosePreference) {
if ($this.SilenceCounter -ge 9) {
$this.WriteExecute('[W1E10]' + $message, $false)
$this.SilenceCounter = 0
} else {
$this.SilenceCounter += 1
if ($VerbosePreference -eq 'Continue') {
$this.WriteExecute('[W1E10 Verb]' + $message, $false)
}
}
}
ResetWriteOnce() {
$this.SilenceCounter = 0
}
SwitchLogfileIfBig() {
$SizeLimit = 500000
$DateStr = (get-date).ToString("yyyyMMdd")
$InfoOutName = "{0}\{1}.log" -f $this.ScriptPath, ($this.commandName + "_info")
$ErrorOutName = "{0}\{1}.log" -f $this.ScriptPath, ($this.commandName + "_error")
if ((Get-Item $InfoOutName).Length -ge $SizeLimit) {
Move-Item -Path $InfoOutName -Destination ($InfoOutName.Replace(".log", "$DateStr.log"))
}
if ((Get-Item $ErrorOutName).Length -ge $SizeLimit) {
Move-Item -Path $ErrorOutName -Destination ($ErrorOutName.Replace(".log", "$DateStr.log"))
}
}
Crlf() {
$outName = "{0}\{1}.log" -f $this.ScriptPath, ($this.commandName + "_info")
"" | Out-File -FilePath $outName -Append -Encoding default
}
hidden WriteExecute([string]$message, [bool]$IsError) {
$now = Get-Date
$str = "{0} {1}:{2}" -f $now, $this.commandName, $message
$outName = "{0}\{1}.log" -f $this.ScriptPath, ($this.commandName + "_info")
$str | Out-File -FilePath $outName -Append -Encoding default
if ($IsError) {
$outName = "{0}\{1}.log" -f $this.ScriptPath, ($this.commandName + "_error")
$str | Out-File -FilePath $outName -Append -Encoding default
Write-Host $str -ForegroundColor Red
} else {
Write-Host $str
}
$this.WritedString += $str + "`r`n"
}
CommandWrite([string]$comStr, [string]$comment = "") {
$outName = "{0}\{1}.log" -f $this.ScriptPath, ($this.commandName + "_Command")
$comStr | Out-File -FilePath $outName -Append -Encoding default
if (-not [string]::IsNullOrEmpty($comment)) {
"#" + $comment | Out-File -FilePath $outName -Append -Encoding default
}
$this.WritedString += $comStr + "`r`n"
}
CommandLogDelete() {
$outName = "{0}\{1}.log" -f $this.ScriptPath, ($this.commandName + "_Command")
if (Test-Path $outName) { Remove-Item $outName }
}
}
タスクスケジューラーへの登録方法
本章では作成したスクリプトをタスクスケジューラーへ登録する方法を記載します。
※タスクはMicrosoft Entra Connectサーバーに保存されている「Microsoft Azure Active Directory Connect」のモジュールが必要になるため、Microsoft Entra Connect上でタスクを実行します。
- 上記のスクリプト「AZUREADSSOACCキーのロールオーバーを自動化するスクリプト」をメモ帳に貼り、拡張子を「AZUREADSSOACCKeyRollOver.ps1」にして保存します。
- 上記スクリプト「スクリプトの実行状況やエラーをログファイルに記録するスクリプト」も同様にメモ帳に貼り、拡張子を「Log.ps1」にして保存します。
- [Explorer]を起動し、「C:\Utility」の配下に「AZUREADSSOACCKeyRollOver」のフォルダーを作成します。
- 作成したフォルダーの配下に上記「1」で作成した、「AZUREADSSOACCKeyRollOver.ps1」のスクリプトファイルを置きます。
- 「C:\Utility」の配下に「Common」フォルダーを作成します。
- 「Common」フォルダーの配下に、「2」で作成した「Log.ps1」のスクリプトファイルを配置します。
- [タスクスケジューラー]を起動します。
- [タスク スケジューラ ライブラリ]を右クリックし、[タスクの作成]をクリックします。
- 「タスク名」を入力します。また、今回はユーザーがログオンしていないときでもタスクを実行させたいので「ユーザーがログオンしているかどうかにかかわらず実行する」を選択します。
- [トリガー]タブを選択し、[新規]をクリックします。
- 定期的にタスクを実行するスケジュールを入力し、[OK]をクリックします。
- [操作]タブで[新規]をクリックします。
- [プログラム/スクリプト]では下記に記載のファイルパスを入力し、引数関数オプションでは「AZUREADSSOACCKeyRollOver.ps1」を配置しているパスを指定し、[OK]をクリックします。
C:\Windows\System32\WindowsPowerShell\1.0\powershell.exe
- [条件]と[設定]は必要に応じて設定し、[OK]をクリックします。
- タスクを実行する資格情報を入力し、[OK]をクリックします。
動作確認
- 管理者権限でMicrosoft Entra IDにサインインします。
- [ハイブリッド管理]>[Microsoft Entra Connect]をクリックします。
- [Connect同期]>[シームレスなシングルサインオン]をクリックします。
- 「キーの作成日」がタスクスケジューラーでタスクを実行した日になっていることを確認します。
最後に
以上がAZUREADSSOACCキーをタスクスケジューラーを使用してロールオーバーする手順になります。
実施は必須ではありませんが、もし何らかの手段で攻撃者にAZUREADSSOACCが保持しているキーの情報が漏洩した場合、攻撃者はそのキーを使用してユーザーのなりすましをしたり、不正なアクセスを試みたりする可能性があります。
定期的にキーをロールオーバーすることで、たとえキーが侵害されたとしても、そのキーが有効な期間を最長でも30日間に限定することが出来ます。
金山 翔太(日本ビジネスシステムズ株式会社)
Microsoft製品(主にMicrosoft 365やIntune)などの設計に携わったことがあります。趣味はゲームとキャンプです。業務上で学んだことを発信していきます。
担当記事一覧