はじめに

Azure マシン構成(旧称: ゲスト構成)は、Azure VM や Arc 対応サーバーの OS 内部設定を監査・修復できる機能です。

参考:Azure マシンの構成とは

今回は以下の 3 点を実際に動作確認しました。

  1. 組み込みポリシーによる監査(パスワード複雑性)
  2. カスタム GuestConfiguration パッケージの作成と登録
  3. 修復タスクによる設定の自動修復(ApplyAndAutoCorrect モード)

組み込みポリシーによる監査

まず、組み込みポリシー「Audit Windows machines that do not have the password complexity setting enabled」(ID: bf16e0bb-31e1-4646-8202-60a235cc7e74)を RG スコープに割り当てました。

$builtinDef = Get-AzPolicyDefinition -BuiltIn | Where-Object { $_.Name -eq "bf16e0bb-31e1-4646-8202-60a235cc7e74" }
$scope = "/subscriptions/<subscriptionId>/resourceGroups/rg-guestconfig-test"
New-AzPolicyAssignment `
  -Name gc-builtin-test `
  -PolicyDefinition $builtinDef `
  -Scope $scope `
  -IdentityType SystemAssigned `
  -Location japaneast `
  -PolicyParameterObject @{ IncludeArcMachines = "false" }

コンプライアンス評価スキャンを実行後に確認すると NonCompliant となりました。検証用 VM はデフォルト設定でパスワード複雑性が無効のため、正しく検出されています。

$job = Start-AzPolicyComplianceScan -ResourceGroupName rg-guestconfig-test -AsJob
Get-AzPolicyState -ResourceGroupName "rg-guestconfig-test" |
  Where-Object { $_.PolicyAssignmentName -eq "gc-builtin-test" } |
  Select-Object PolicyAssignmentName, ComplianceState

PolicyAssignmentName ComplianceState
-------------------- ---------------
gc-builtin-test      NonCompliant

カスタム GuestConfiguration パッケージの作成

DSC 設定の記述

カスタムパッケージは PowerShell DSC で設定を記述し、MOF にコンパイルしてからパッケージ化します。今回は 3 つの設定を作成しました。

1つ目はタイムゾーンの設定です。DSCでは ComputerManagementDsc モジュールの TimeZone リソースを使用します。

注意点として、タイムゾーンの設定には PSDscResources ではなく ComputerManagementDsc モジュールが必要です。PSDscResources には TimeZone リソースが含まれていないため、誤ったモジュールを指定するとコンパイルが失敗します。

Configuration CheckTimeZone {
    Import-DscResource -ModuleName ComputerManagementDsc
    Node localhost {
        TimeZone SetTimeZone {
            IsSingleInstance = "Yes"
            TimeZoneName     = "Tokyo Standard Time"
        }
    }
}
CheckTimeZone -OutputPath "output/temp/mof/CheckTimeZone"

2つ目は最小パスワード長(MinPasswordLength)の設定です。DSC では SecurityPolicyDsc モジュールの AccountPolicy リソースを使用します。

Configuration MinPasswordLength {
    Import-DscResource -ModuleName SecurityPolicyDsc
    Node localhost {
        AccountPolicy SetMinPasswordLength {
            Name                    = 'MinPasswordLength'
            Minimum_Password_Length = 14
        }
    }
}
MinPasswordLength -OutputPath "mof/MinPasswordLength"

3つ目はレジストリ設定(DontDisplayLastUsername)です。実装には PSDscResources モジュールの Registry リソースを使用します。

Configuration DontDisplayLastUsername {
    Import-DscResource -ModuleName PSDscResources
    Node localhost {
        Registry DontDisplayLastUsername {
            Key       = 'HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System'
            ValueName = 'DontDisplayLastUserName'
            ValueData = '1'
            ValueType = 'Dword'
            Ensure    = 'Present'
        }
    }
}
DontDisplayLastUsername -OutputPath "mof/DontDisplayLastUsername"

MOF のコンパイルは PowerShell 5.1 で実行します。DSC 設定ファイルを .ps1 として保存し、PowerShell 5.1 で実行することで localhost.mof が生成されます。

# PowerShell 5.1 で実行
powershell.exe -Command {
    . .\CheckTimeZone.ps1
    CheckTimeZone -OutputPath "output/temp/mof/CheckTimeZone"
}

実行すると指定した出力先に localhost.mof が生成されます。

    Directory: output\temp\mof\CheckTimeZone

Mode                 LastWriteTime         Length Name
----                 -------------         ------ ----
-a----        2026/03/07     xx:xx           xxxx localhost.mof

パッケージの作成とアップロード

MOF をコンパイルした後、PowerShell 7 の GuestConfiguration モジュールでパッケージを作成します。-Type AuditAndSet を指定することで監査だけでなく設定の修復(Set)も行えます。

Import-Module GuestConfiguration
New-GuestConfigurationPackage -Name CheckTimeZone `
  -Configuration "output/temp/mof/CheckTimeZone/localhost.mof" `
  -Type AuditAndSet -Path "output/temp/packages" -Force
New-GuestConfigurationPackage -Name MinPasswordLength `
  -Configuration "mof/MinPasswordLength/localhost.mof" `
  -Type AuditAndSet -Path "output/temp/packages" -Force
New-GuestConfigurationPackage -Name DontDisplayLastUsername `
  -Configuration "mof/DontDisplayLastUsername/localhost.mof" `
  -Type AuditAndSet -Path "output/temp/packages" -Force

今回は、パッケージをプライベートエンドポイント経由でのみアクセス可能なストレージアカウントに保存しました。そのうえで、VM のシステム割り当てマネージド ID に Storage Blob Data Reader ロールを付与することで、GuestConfig エージェントがプライベートエンドポイント経由でパッケージを取得できるようにします

New-AzStorageAccount -ResourceGroupName rg-guestconfig-test -Name "<storage-account-name>" -Location japaneast -SkuName Standard_LRS -Kind StorageV2 -PublicNetworkAccess Disabled

ただし、仮想マシンのマネージド ID を直接 RBAC で使用すると上限の4000 に抵触しやすくなってしまうため。システム割り当てマネージド ID をグループにまとめて、ストレージアカウントに対して Storage Blob Data Reader を付与しました。

$gcGroupId = "72fa1ccc-f653-4222-a10e-a316595a22ba"
$storageId = (Get-AzStorageAccount -ResourceGroupName rg-guestconfig-test -Name <storage-account-name>).Id
New-AzRoleAssignment -ObjectId $gcGroupId -RoleDefinitionName "Storage Blob Data Reader" -Scope $storageId

カスタムポリシー定義と割り当て

ポリシー定義 JSON の生成

New-GuestConfigurationPolicy コマンドレットを使用してポリシー定義 JSON を生成します。-Mode ApplyAndAutoCorrect を指定することで、修復タスクが実行された際に設定が自動適用されます。

Import-Module GuestConfiguration

New-GuestConfigurationPolicy `
  -LocalContentPath 'output\temp\packages\CheckTimeZone.zip' `
  -ContentUri "https://<storage-account-name>.blob.core.windows.net/guestconfig-packages/CheckTimeZone.zip" `
  -DisplayName 'Audit Tokyo Standard Time' `
  -Description 'Checks whether the time zone is set to Tokyo Standard Time' `
  -PolicyId "6ee089f1-f126-4cec-9c2d-9ab26c928b40" `
  -Path 'output\temp\policies\CheckTimeZone' `
  -Platform Windows `
  -PolicyVersion '1.0.0' `
  -Mode ApplyAndAutoCorrect `
  -UseSystemAssignedIdentity

New-GuestConfigurationPolicy `
  -LocalContentPath 'output\temp\packages\MinPasswordLength.zip' `
  -ContentUri "https://<storage-account-name>.blob.core.windows.net/guestconfig-packages/MinPasswordLength.zip" `
  -DisplayName 'Audit Minimum Password Length (14)' `
  -Description 'Checks whether minimum password length is set to 14 or more' `
  -PolicyId "<policy-id>" `
  -Path 'output\temp\policies\MinPasswordLength' `
  -Platform Windows `
  -PolicyVersion '1.0.0' `
  -Mode ApplyAndAutoCorrect `
  -UseSystemAssignedIdentity

New-GuestConfigurationPolicy `
  -LocalContentPath 'output\temp\packages\DontDisplayLastUsername.zip' `
  -ContentUri "https://<storage-account-name>.blob.core.windows.net/guestconfig-packages/DontDisplayLastUsername.zip" `
  -DisplayName 'Audit DontDisplayLastUserName Registry Setting' `
  -Description 'Checks whether DontDisplayLastUserName registry value is set to 1' `
  -PolicyId "<policy-id>" `
  -Path 'output\temp\policies\DontDisplayLastUsername' `
  -Platform Windows `
  -PolicyVersion '1.0.0' `
  -Mode ApplyAndAutoCorrect `
  -UseSystemAssignedIdentity

ポリシー定義の登録

生成した JSON を使ってポリシー定義を登録します。

$subId = "<subscriptionId>"
New-AzPolicyDefinition -Name gc-custom-timezone `
  -Policy "output/temp/policies/CheckTimeZone/CheckTimeZone_DeployIfNotExists.json" `
  -SubscriptionId $subId -Mode All
New-AzPolicyDefinition -Name gc-custom-minpassword `
  -Policy "output/temp/policies/MinPasswordLength/MinPasswordLength_DeployIfNotExists.json" `
  -SubscriptionId $subId -Mode All
New-AzPolicyDefinition -Name gc-custom-dontdisplaylast `
  -Policy "output/temp/policies/DontDisplayLastUsername/DontDisplayLastUsername_DeployIfNotExists.json" `
  -SubscriptionId $subId -Mode All

ポリシーの割り当てと Contributor ロールの付与

ポリシーを割り当てる際、-IdentityType SystemAssigned でシステム割り当てマネージド ID を指定します。このマネージド ID が deployIfNotExists 効果の実行(= GuestConfigurationAssignment リソースのデプロイ)を行うために RBAC が必要になります。今回は共同作成者ロールを使いましたが、実際にはカスタムロールを利用して権限を最小化しましょう。

$subId = "<subscriptionId>"
$scope = "/subscriptions/<subscriptionId>/resourceGroups/rg-guestconfig-test"
$policyDef      = Get-AzPolicyDefinition -Name gc-custom-timezone        -SubscriptionId $subId
$minPwdDef      = Get-AzPolicyDefinition -Name gc-custom-minpassword     -SubscriptionId $subId
$dontDisplayDef = Get-AzPolicyDefinition -Name gc-custom-dontdisplaylast -SubscriptionId $subId

New-AzPolicyAssignment `
  -Name gc-custom-timezone-test `
  -PolicyDefinition $policyDef `
  -Scope $scope `
  -IdentityType SystemAssigned `
  -Location japaneast
New-AzPolicyAssignment `
  -Name gc-minpassword-test `
  -PolicyDefinition $minPwdDef `
  -Scope $scope `
  -IdentityType SystemAssigned `
  -Location japaneast
New-AzPolicyAssignment `
  -Name gc-dontdisplaylast-test `
  -PolicyDefinition $dontDisplayDef `
  -Scope $scope `
  -IdentityType SystemAssigned `
  -Location japaneast

$tzA = Get-AzPolicyAssignment -Name gc-custom-timezone-test  -Scope $scope
$mpA = Get-AzPolicyAssignment -Name gc-minpassword-test      -Scope $scope
$ddA = Get-AzPolicyAssignment -Name gc-dontdisplaylast-test  -Scope $scope
New-AzRoleAssignment `
  -RoleDefinitionName Contributor `
  -ObjectId $tzA.IdentityPrincipalId `
  -Scope $scope
New-AzRoleAssignment `
  -RoleDefinitionName Contributor `
  -ObjectId $mpA.IdentityPrincipalId `
  -Scope $scope
New-AzRoleAssignment `
  -RoleDefinitionName Contributor `
  -ObjectId $ddA.IdentityPrincipalId `
  -Scope $scope

修復前の状態確認(Before)

修復タスクを実行する前に WinRM HTTPS (Port 5986) 経由で PSSession を確立し、VM 内の設定値を確認しました。

$so = New-PSSessionOption -SkipCACheck -SkipCNCheck -SkipRevocationCheck
$sess = New-PSSession -ComputerName "10.1.0.18" -Credential $cred -UseSSL -SessionOption $so -Port 5986

Invoke-Command -Session $sess -ScriptBlock {
    Write-Host "TimeZone: $((Get-TimeZone).Id)"
    Write-Host (net accounts | Where-Object { $_ -like "*Minimum password length*" })
    Write-Host "DontDisplayLastUserName: $((Get-ItemProperty 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System' -Name DontDisplayLastUserName -ErrorAction SilentlyContinue).DontDisplayLastUserName)"
}

TimeZone: UTC
Minimum password length: 0
DontDisplayLastUserName: 0

タイムゾーンが UTC、最小パスワード長が 0、ログイン画面の前回ユーザー名表示が有効(0)という状態です。

修復タスクの実行

コンプライアンス評価スキャン

修復タスクを実行する前にコンプライアンス評価スキャンを完了させる必要があります。スキャンが完了していない状態で修復タスクを作成すると TotalDeployments = 0 となり修復が実行されません。

$job = Start-AzPolicyComplianceScan -ResourceGroupName "rg-guestconfig-test" -AsJob

スキャン完了後(約 10 分)に確認すると、カスタムポリシー 3 つが NonCompliant と評価されました。

PolicyAssignmentName    ComplianceState
--------------------    ---------------
gc-minpassword-test     NonCompliant
gc-dontdisplaylast-test NonCompliant
gc-custom-timezone-test NonCompliant
gc-builtin-test         NonCompliant

修復タスクの作成

Start-AzPolicyRemediation で修復タスクを作成します。-PolicyAssignmentId には Get-AzPolicyAssignment で取得した割り当てオブジェクトの .Id プロパティを使います。

$scope = "/subscriptions/<subscriptionId>/resourceGroups/rg-guestconfig-test"
$allAssigns = Get-AzPolicyAssignment -Scope $scope

$minPwdAssign = $allAssigns | Where-Object { $_.Name -eq "gc-minpassword-test" }
$dontDisplayAssign = $allAssigns | Where-Object { $_.Name -eq "gc-dontdisplaylast-test" }
$tzAssign = $allAssigns | Where-Object { $_.Name -eq "gc-custom-timezone-test" }

Start-AzPolicyRemediation -Name "gc-remediation-minpwd" `
  -PolicyAssignmentId $minPwdAssign.Id `
  -ResourceDiscoveryMode ExistingNonCompliant `
  -ResourceGroupName "rg-guestconfig-test"

Start-AzPolicyRemediation -Name "gc-remediation-dontdisplay" `
  -PolicyAssignmentId $dontDisplayAssign.Id `
  -ResourceDiscoveryMode ExistingNonCompliant `
  -ResourceGroupName "rg-guestconfig-test"

Start-AzPolicyRemediation -Name "gc-remediation-timezone" `
  -PolicyAssignmentId $tzAssign.Id `
  -ResourceDiscoveryMode ExistingNonCompliant `
  -ResourceGroupName "rg-guestconfig-test"

約 2 分後に確認すると、3 つの修復タスクが SucceededTotalDeployments = 1SuccessfulDeployments = 1 になりました。

Get-AzPolicyRemediation -ResourceGroupName "rg-guestconfig-test" |
  Select-Object Name, ProvisioningState, `
    @{N='Total';E={$_.DeploymentSummary.TotalDeployments}}, `
    @{N='Succeeded';E={$_.DeploymentSummary.SuccessfulDeployments}}

Name                       ProvisioningState Total Succeeded
----                       ----------------- ----- ---------
gc-remediation-timezone    Succeeded         1     1
gc-remediation-dontdisplay Succeeded         1     1
gc-remediation-minpwd      Succeeded         1     1

修復の仕組み

修復タスクが deployIfNotExists ポリシーを実行すると、VM 上に GuestConfigurationAssignment リソースが作成されます。この Assignment の assignmentTypeApplyAndAutoCorrect となっており、VM 上の Guest Configuration Service(GCService)がポーリング間隔(デフォルト 5 分)で設定を自動適用します。

GuestConfigurationAssignment の状態は REST API で確認できます。名前に $ 文字が含まれる場合は URL エンコードが必要です($%24)。

az rest --method get `
  --url "https://management.azure.com/subscriptions/<subscriptionId>/resourceGroups/rg-guestconfig-test/providers/Microsoft.Compute/virtualMachines/vm-guestconfig-azure/providers/Microsoft.GuestConfiguration/guestConfigurationAssignments/MinPasswordLength%24pidptko63snurv7y?api-version=2022-01-25" `
  --query "{compliance:properties.complianceStatus, assignmentType:properties.guestConfiguration.assignmentType}" -o json
{
  "assignmentType": "ApplyAndAutoCorrect",
  "compliance": "Compliant"
}

修復後の状態確認(After)

修復タスク完了後に再度 PSSession で VM 内の設定値を確認しました。

Invoke-Command -Session $sess -ScriptBlock {
    Write-Host "=== After Values ==="
    Write-Host "TimeZone: $((Get-TimeZone).Id)"
    Write-Host (net accounts | Where-Object { $_ -like "*Minimum password length*" })
    Write-Host "DontDisplayLastUserName: $((Get-ItemProperty 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System' -Name DontDisplayLastUserName -ErrorAction SilentlyContinue).DontDisplayLastUserName)"
}
=== After Values ===
TimeZone: Tokyo Standard Time
Minimum password length: 14
DontDisplayLastUserName: 1

3 項目すべてが期待通りに変更されました。

設定項目 Before After
タイムゾーン UTC Tokyo Standard Time
最小パスワード長 0 14
ログイン画面の前回ユーザー名表示 0(表示する) 1(表示しない)

最終コンプライアンス状態

コンプライアンス再スキャン後(約 10 分)に確認すると、カスタムポリシー 3 つが Compliant になりました。

$job3 = Start-AzPolicyComplianceScan -ResourceGroupName "rg-guestconfig-test" -AsJob
Get-AzPolicyState -ResourceGroupName "rg-guestconfig-test" |
  Where-Object { $_.PolicyAssignmentName -like "gc-*" } |
  Select-Object PolicyAssignmentName, ComplianceState |
  Format-Table -AutoSize

PolicyAssignmentName    ComplianceState
--------------------    ---------------
gc-dontdisplaylast-test Compliant
gc-minpassword-test     Compliant
gc-custom-timezone-test Compliant
gc-builtin-test         NonCompliant

gc-builtin-test は Audit のみのポリシーで修復対象外のため NonCompliant のままです。

まとめ

Azure マシン構成を使うと、Azure Policy の deployIfNotExists 効果と GuestConfiguration パッケージを組み合わせることで仮想マシン内の OS 設定を自動的に監査・修復できます。

ただし、ポータルぽちぽちで設定できないので利用のハードルは少し高めです。ご利用は計画的に

参考リンク

  • 当サイトは個人のブログです。このブログに示されている見解や意見は個人的なものであり、所属組織の見解や意見を表明するものではありません。
  • 公開情報を踏まえて正確な情報を掲載するよう努めますが、その内容の完全性や正確性、有用性、安全性、最新性について一切保証しません。
  • 添付文章やリンク先などを含む本サイトの内容は作成時点でのものであり、予告なく変更される場合があります。