userCertificate 属性にセットされた証明書の種類によって Hybrid Azure AD Join されるかどうかを見極める方法

#はじめに
AD FS を利用しないマネージド ドメイン環境にて Hybrid Azure AD Join を構成する場合は、Azure AD Connect を利用して対象のコンピューター オブジェクトを Azure AD に同期することで Hybrid Azure AD Join としてデバイスを構成できるようになります。

コンピューター オブジェクトが Azure AD Connect に同期される条件として、「userCertificate」属性に証明書がセットされているかどうかを Azure AD Connect の同期ルールが判断し、証明書がセットされている場合にのみ、同期を行う動作になります。

後述しますが、「userCertificate」属性に証明書がセットされるパターンは主に 2 つあります。
また、 Azure AD Connect のバージョン 1.4.18.0 (2019/09/25 リリース) からは、本来 Hybrid Azure AD Join として構成を意図していない証明書については、同期の対象外になる同期ルールになっていましたので、この動作を確認してみたいとおもいます。

***この記事には終盤にオチというか留意点があるので、最後まで読んでいただけると幸いです***

#やってみる
まず最初に、オンプレミス AD に参加しているサーバー OS を含むコンピューター オブジェクトにおいて、「userCertificate」属性に証明書がセットされるパターンは下記 2 つが主となります。

1. SCP が構成済みの環境で Azure AD と通信可能なコンピューター上でタスク スケジューラーの「Automatic-Device-Join」が起動したとき。
2. エンタープライズ CA 環境が構成済みの環境で、対象のコンピューターが CA 証明書を要求したとき。

1.については Hybrid Azure AD Join を構成したことがある方はご存知の方も多いかと思いますが、Azure AD Connect もしくはレジストリで SCP (サービス接続ポイント) を構成済みの環境で Azure AD とネットワーク通信が可能な状態において、

 1-1. ユーザーがログオンしたタイミング
 1-2. 「User-Device-Registration」ログに「イベント ID 4096」 が記録されたタイミング
	
いずれかにおいて、「Automatic-Device-Join」が起動した際に「userCertificate」属性に証明書がセットされます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/373423/1f1240c8-44a9-9895-4ab2-92503a333517.png)

下記がトリガーになります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/373423/64e81bc6-783b-41de-d69c-85555a0a1974.png)

イベント時のトリガーの内容です。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/373423/88f336b4-ef7d-7a1c-e806-9f466e7745c0.png)

1. の証明書がセットされてからの一連の流れについては、下記過去の Blog が参考なると思います。

-参考情報
マネージド ドメイン用(AD FS なし) Hybrid Azure AD Join を一から構成する (Hybrid Azure AD Join 構成までの流れの辺りから)
URL:https://qiita.com/Shinya-Yamaguchi/items/73016fcdb5beeaefa5ba

2.については、Hybrid Azure AD Join とは本来は全く関係がないのですが、***意図せず*** Hybrid Azure AD Join としてデバイスが Azure AD に同期され、***保留中*** のまま同期されてしまうという事象がそこそこの頻度で見受けられます。
	
これは Azure AD Connect が「userCertificate」属性に証明書がセットされていれば、Azure AD に同期する同期ルールがあるためです。
	
エンタープライズ CA が構成されているサーバーで証明書テンプレートが以下のように 「Publish certificate in Active Directory (Active Directory の証明書を発行する) 」にチェックが入っている証明書テンプレートを利用した DC 証明書が、当該のコンピューターに対して発行された場合に、「userCertificate」属性に証明書がセットされます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/373423/40079c22-b557-fc52-5891-9e10402716bd.png)

上記 2. のパターンは Windows 10 コンピューターではなく、主に IIS などの Web サービスをエンタープライズ CA が発行した証明書を使って SSL 通信しようと考えている場合や、ドメインに参加している ***Windows Server*** を想定しています。

逆にいうと、本来の Hybrid Azure AD Join は Windows 10 コンピューターを中心としたクライアント OS になるので、2. のパターンにはほぼ該当しないと考えています。

ではなぜ、敢えて 2. の話をしているかというと、知らない間に Hybrid Azure AD Join としてサーバーが同期されてしまった、という理由がこの 2. のパターンに該当している可能性があるからです。

Hybrid Azure AD Join をするつもりもないサーバーが Azure AD に Hybrid Azure AD Join として同期されてしまっている場合は、エンタープライズ CA を構成している環境があるかどうかを、確認してみるのがよろしいかと思います。

次に Azure AD Connect の話に移ります。

冒頭でも触れたように、Azure AD Connect バージョン 1.4.18.0 (2019/09/25 リリース) からは、エンタープライズ CA から発行された証明書が「userCertificate」属性にセットされた コンピューターは Azure AD に同期されないようになりました。

具体的には、「Synchronization Rules Editor」にあるルール名「In from AD - Computer Join」の「Transformations」の下記ルールが「cloudFiltered」となっており、条件を満たす場合は、同期されますが、条件を満たさない場合は、Azure AD に同期されません。

「In from AD - Computer Join」の「Transformations」です。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/373423/c2d86d0b-e1df-9768-490c-613bbcbed71c.png)

下記が同ルール内の「cloudFiltered」の中身です。

```powershell

With(
  $validCerts,
  Where(
    $c,
    [userCertificate],
    IsCert($c) 
      && CertNotAfter($c) > Now() 
      && RegexIsMatch(
        CertSubject($c),
        "CN=[{]*" & StringFromGuid([objectGUID]) & "[}]*",
        "IgnoreCase"
        )
  ),
  IIF(Count($validCerts) > 0,NULL,True)
)
```

上記ルールは証明書自体の「有効期限が切れていない」ことと証明書内の「サブジェクト」に「objectGUID」が含まれているかどうかを判定しています。

具体的に証明書の中身を見てみましょう、まず、「Automatic-Device-Join」により、セットされた証明書は発行者が「MS-Organization-Access」になります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/373423/273cb07a-185e-f5fb-e4ee-4fba047c0fcf.png)

「詳細」→「サブジェクト」の順にクリックすると、下記画面ショットのようにサブジェクトに「objectGUID」がセットされていることが分かります。この値がそのまま「デバイス ID」として Azure AD に同期されます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/373423/156d086a-bd66-f693-5c81-9381423405ab.png)

一方、エンタープライズ CA により「userCertificate」にセットされた証明書の中身を見てみると、下記画面ショットのように「サブジェクト」は CA の情報が入っています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/373423/5b304ed8-63c6-7f3b-3c0e-ec25c21b58b3.png)

つまり、有効な証明書であっても、サブジェクトに「objectGUID」が含まれていなければ、同期の対象外になるという、動作になります。
Microsoft の下記公開情報にも同様の内容が記載されていますので、参考にしてみてください。

-参考情報
Azure AD Connect 1.4.xx.x およびデバイスの消失について
URL:https://docs.microsoft.com/ja-jp/azure/active-directory/hybrid/reference-connect-device-disappearance

上記公開情報に書いているスクリプトを利用することで、OU に含まれているコンピューターが同期対象なのか、同期対象ではないのか、判別してくれます。実際に試してみました。

スクリプトの中身は以下になります、このスクリプトをオンプレミス AD の任意のフォルダに配置してください。

```powershell

 [CmdletBinding()] 
    Param 
    ( 
        # Computer DistinguishedName 
        [Parameter(ParameterSetName='SingleObject', 
                   Mandatory=$true, 
                   ValueFromPipelineByPropertyName=$true, 
                   Position=0)] 
        [String] 
        $DN, 
 
        # AD OrganizationalUnit 
        [Parameter(ParameterSetName='MultipleObjects', 
                   Mandatory=$true, 
                   ValueFromPipelineByPropertyName=$true, 
                   Position=0)] 
        [String] 
        $OU, 
 
        # Output CSV filename (optional) 
        [Parameter(Mandatory=$false, 
                   ValueFromPipelineByPropertyName=$false, 
                   Position=1)] 
        [String] 
        $Filename 
 
    ) 
 
    # Generate Output filename if not provided 
    If ($Filename -eq "") 
    { 
        $Filename = [string] "$([string] $(Get-Date -Format yyyyMMddHHmmss))_ADSyncAADHybridJoinCertificateReport.csv" 
    } 
    Write-Verbose "Output filename: '$Filename'" 
     
    # Read AD object(s) 
    If ($PSCmdlet.ParameterSetName -eq 'SingleObject') 
    { 
        $directoryObjs = @(Get-ADObject $DN -Properties UserCertificate) 
        Write-Verbose "Starting report for a single object '$DN'" 
    } 
    Else 
    { 
        $directoryObjs = Get-ADObject -Filter { ObjectClass -like 'computer' } -SearchBase $OU -Properties UserCertificate 
        Write-Verbose "Starting report for $($directoryObjs.Count) computer objects in OU '$OU'" 
    } 
 
    Write-Host "Processing $($directoryObjs.Count) directory object(s). Please wait..." 
    # Check Certificates on each AD Object 
    $results = @() 
    ForEach ($obj in $directoryObjs) 
    { 
        # Read UserCertificate multi-value property 
        $objDN = [string] $obj.DistinguishedName 
        $objectGuid = [string] ($obj.ObjectGUID).Guid 
        $userCertificateList = @($obj.UserCertificate) 
        $validEntries = @() 
        $totalEntriesCount = $userCertificateList.Count 
        Write-verbose "'$objDN' ObjectGUID: $objectGuid" 
        Write-verbose "'$objDN' has $totalEntriesCount entries in UserCertificate property." 
        If ($totalEntriesCount -eq 0) 
        { 
            Write-verbose "'$objDN' has no Certificates - Skipped." 
            Continue 
        } 
 
        # Check each UserCertificate entry and build array of valid certs 
        ForEach($entry in $userCertificateList) 
        { 
            Try 
            { 
                $cert = [System.Security.Cryptography.X509Certificates.X509Certificate2] $entry 
            } 
            Catch 
            { 
                Write-verbose "'$objDN' has an invalid Certificate!" 
                Continue 
            } 
            Write-verbose "'$objDN' has a Certificate with Subject: $($cert.Subject); Thumbprint:$($cert.Thumbprint)." 
            $validEntries += $cert 
 
        } 
         
        $validEntriesCount = $validEntries.Count 
        Write-verbose "'$objDN' has a total of $validEntriesCount certificates (shown above)." 
         
        # Get non-expired Certs (Valid Certificates) 
        $validCerts = @($validEntries | Where-Object {$_.NotAfter -ge (Get-Date)}) 
        $validCertsCount = $validCerts.Count 
        Write-verbose "'$objDN' has $validCertsCount valid certificates (not-expired)." 
 
        # Check for AAD Hybrid Join Certificates 
        $hybridJoinCerts = @() 
        $hybridJoinCertsThumbprints = [string] "|" 
        ForEach ($cert in $validCerts) 
        { 
            $certSubjectName = $cert.Subject 
            If ($certSubjectName.StartsWith($("CN=$objectGuid")) -or $certSubjectName.StartsWith($("CN={$objectGuid}"))) 
            { 
                $hybridJoinCerts += $cert 
                $hybridJoinCertsThumbprints += [string] $($cert.Thumbprint) + '|' 
            } 
        } 
 
        $hybridJoinCertsCount = $hybridJoinCerts.Count 
        if ($hybridJoinCertsCount -gt 0) 
        { 
            $cloudFiltered = 'FALSE' 
            Write-verbose "'$objDN' has $hybridJoinCertsCount AAD Hybrid Join Certificates with Thumbprints: $hybridJoinCertsThumbprints (cloudFiltered=FALSE)" 
        } 
        Else 
        { 
            $cloudFiltered = 'TRUE' 
            Write-verbose "'$objDN' has no AAD Hybrid Join Certificates (cloudFiltered=TRUE)." 
        } 
         
        # Save results 
        $r = "" | Select ObjectDN, ObjectGUID, TotalEntriesCount, CertsCount, ValidCertsCount, HybridJoinCertsCount, CloudFiltered 
        $r.ObjectDN = $objDN 
        $r.ObjectGUID = $objectGuid 
        $r.TotalEntriesCount = $totalEntriesCount 
        $r.CertsCount = $validEntriesCount 
        $r.ValidCertsCount = $validCertsCount 
        $r.HybridJoinCertsCount = $hybridJoinCertsCount 
        $r.CloudFiltered = $cloudFiltered 
        $results += $r 
    } 
 
    # Export results to CSV 
    Try 
    {         
        $results | Export-Csv $Filename -NoTypeInformation -Delimiter ';' 
        Write-Host "Exported Hybrid Azure AD Domain Join Certificate Report to '$Filename'.`n" 
    } 
    Catch 
    { 
        Throw "There was an error saving the file '$Filename': $($_.Exception.Message)" 
    } 
```

PowerShell を管理者として起動し、下記のような形で OU を指定することで、CSV ファイルがカレント ディレクトリに出力されます。

```powershell

PS C:\work> .\Export-ADSyncToolsHybridAzureADjoinCertificateReport.ps1 -OU 'CN=Computers,DC=aishiabank002,DC=work'
Processing 4 directory object(s). Please wait...
Exported Hybrid Azure AD Domain Join Certificate Report to '20200412001530_ADSyncAADHybridJoinCertificateReport.csv'.
```

下記が CSV ファイルの中身です、「HybridJoinCertsCount」が Hybrid Azure AD Join として同期対象となる「userCertificate」証明書であるかどうかをカウントし、「CloudFiltered」が「FALSE」のコンピューターが Azure AD にHybrid Azure AD Join として同期されます。

下記を例にすると、2行あるうち、上の行のサーバーがパターン 2. のエンタープライズ CA から発行された証明書で、下の行のコンピューターが「Automatic-Device-Join」経由でセットされた証明書が入っている Windows 10 コンピューターになります。

一覧で取得できるので、判別の参考になると思います。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/373423/0ce4ed56-818e-05d8-593e-1cc45e24c0cc.png)

さて、ここまで読んでいただいた皆さんに残念なお知らせです。

私も検証していて気付いてしまったのですが、Azure AD Connect のバージョン 1.4.18.0 で導入された「cloudFiltered」の同期ルールですが、最新の Azure AD Connect バージョン 1.5.20.0 (2020/04/09) では同期ルールが変更になっていました。

どういう経緯で変更になったのかは不明ですが、上述で触れた、証明書自体の「有効期限が切れていない」ことと証明書内の「サブジェクト」に「objectGUID」が含まれているかどうかを判定の条件にしていた内容が、ごっそり無くなっています…。

実際に、エンタープライズ CA から発行された証明書がセットされた Windows Server が バージョン 1.5.20.0 の同期ルールを利用した環境の場合、下記画面ショットのように「保留中」として Azure AD に同期されてしまいました…。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/373423/91ac9df7-ae95-fa82-bee6-843ff39b1f88.png)

具体的に Transformation の記述は以下の通りに変更されています。

1.4.18.0の「In from AD - Computer Join」の「Transformations」

```powershell

With(
  $validCerts,
  Where(
    $c,
    [userCertificate],
    IsCert($c) 
      && CertNotAfter($c) > Now() 
      && RegexIsMatch(
        CertSubject($c),
        "CN=[{]*" & StringFromGuid([objectGUID]) & "[}]*",
        "IgnoreCase"
        )
  ),
  IIF(Count($validCerts) > 0,NULL,True)
)
```

1.5.20.0 の「In from AD - Computer Join」の「Transformations」

```powershell

IIF(
  IsNullOrEmpty([userCertificate])
  || (
    (InStr(UCase([operatingSystem]),"WINDOWS") > 0)
    && (Left([operatingSystemVersion],2) = "6.")
  ),
  True,
  NULL)
```

端的に言ってしまうと、「userCertificate」の証明書の有無だけを判定状況にしています。
繰り返しますが、私もどうして設定を変えてしまったのか不明ですし知りたいくらいです…。
少なくとも下記 release history を見た限りでは、1.5 系のバージョンの項目にこの件に関する修正は言及されていませんでした。

-参考情報
Azure AD Connect: Version release history
URL:https://docs.microsoft.com/en-us/azure/active-directory/hybrid/reference-connect-version-history#15200

###***2020/04/18 追記***

1.5.20.0 の「In from AD - Computer Join」の「Transformations」のルールの中身を確認しました。

ルールの内容としては「userCertificate」 に証明書のセットの有無を確認し、証明書がある場合でも、 Windows OS のバージョンが「6.x」台であったら cloudFiltered が true になる (同期の対象外になる) 、という意味でした。

Version number が 10.0 である OS は Windows 10, Windows Server 2019, Windows Server 2016 になります。
それ以前、Windows 8.1 や Windows Server 2012 R2 は 6.3 などの 6.x 台になります。

Operating System Version (Version number 一覧が書いてあります)
URL:https://docs.microsoft.com/ja-jp/windows/win32/sysinfo/operating-system-version?redirectedfrom=MSDN


#おわりに
今回はコンピューターオブジェクトの「userCertificate」属性にセットされるパターンとその中身の違い、また、Azure AD Connect の 1.4 系と 1.5 系における userCertificate 属性の判定方法の違いを見てきました。

Azure AD Connect が標準でセットした同期ルール (100 番以降のルール) を編集することは推奨していない (書き換えて不具合が発生してしまってもサポートできない) ので、現行最新の 1.5 系の Transformation の記述を 1.4 系の内容に書き換えることはしないでください。

現行最新の 1.5 系においては、エンタープライズ CA から発行された証明書がセットされた Windows コンピューターも Hybrid Azure AD Join として同期されてしまいますが、***同期されるもの*** として認識しておくことで、その時点で意味不明なコンピューター オブジェクトではなくなります。

また ***保留中*** のままであれば PRT というトークンも取れないので、対象のコンピューター オブジェクトを使って、条件付きアクセスのポリシー「ハイブリッド Azure AD 参加済みのデバイスが必要」 を突破されることもありません。

つまり、Azure AD に Hybrid Azure AD Join として同期されてしまっていても影響はないので、その点はご安心ください。
今回の記事が少しでも参考になれば幸いです。