AzureCloudShell(PowerShell)でAzureDNS管理のドメイン証明書をWEBサーバーなしで取得する(Let'sEncrypt)


# はじめに
今日は、AzureDNSで管理するドメインの証明書を、
PowerShellでWEBサーバーなしで Let’s Encryptに発行してもらいました。

* **2018/11 追記 PowerShellCore,ワイルドカード証明書に対応したものは[こちら](https://qiita.com/HiroyukiSakoh/items/402861a194ecbbe66312)**

# Let's Encryptとはなんぞや
* 認証局(CA)
* 自動発行プロトコルで、X.509証明書発行を無料でやってる
* 細かいことはこちらへ→[Wikipedia](https://ja.wikipedia.org/wiki/Let%27s_Encrypt)

# いろいろ調べる
* プロトコル(ACME)上、認証方式は以下２つ
    * DNSレコードに認証用のレコードを追加
    * http(s?)で該当ドメイン上のWEBサーバーに認証用のファイルを設置
* Azure + Let's Encryptで検索すると、WebAppのSiteExtentionばかり
    * **証明書だけ欲しいんだけど！ WebAppリソース立てたくない…**
* Let's Encryptのチュートリアルを調べても、Linux系ツールセットしか見つからない
    * **♡Windows**
* Windows系で見つけても、IISでファイル設置の認証パターンばっかり
    * Windows用のDNS認証に対応したクライアントはないの？！
    * **ありました → [ACMESharp](https://github.com/ebekker/ACMESharp)**

# ACMESharpの流れ（ざっくり）
1. Install&Import(PowerShellModule)
1. ACMEVaultの作成(ACMEプロトコル中のデータストア)
1. Let's Encrypt CAのアカウント登録
1. ドメイン証明書の要求を送信
1. ドメイン所有権確認の応答を受信(チャレンジ)
1. チャレンジの対処(DNSレコード追加/HTTPサーバー上にファイル配置)
1. チャレンジ確認の要求を送信
1. 証明書署名の要求を送信
1. 証明書のエクスポート

# 全部コマンド１つでやりたい！
ACMESharpはPowerShellのモジュールなので、基本的には自動で行えます。
チャレンジの対処をAzureDNSで行うため、そこも併せて１つのスクリプトにします。

## Azure Power ShellでAzureDNSをいじる
昨日のエントリのARMTemplateの経験から、作るの１コマンドだろうとおもったら２コマンドでした。

* [New-AzureRmDnsRecordConfig](https://docs.microsoft.com/en-us/powershell/module/azurerm.dns/new-azurermdnsrecordconfig?view=azurermps-6.2.0)
* [New-AzureRmDnsRecordSet](https://docs.microsoft.com/en-us/powershell/module/azurerm.dns/new-azurermdnsrecordset?view=azurermps-6.2.0)

```powershell:TXTレコード作る
$Records = @()
$Records += New-AzureRmDnsRecordConfig -Value $RecordValue
New-AzureRmDnsRecordSet -Name $RecordName -RecordType TXT -ResourceGroupName $AzureDnsResourceGroupName -TTL 3600 -ZoneName $AzureDnsZoneName -DnsRecords $Records
```

消すのは１コマンド。

* [Remove-AzureRmDnsRecordSet](https://docs.microsoft.com/en-us/powershell/module/azurerm.dns/remove-azurermdnsrecordset?view=azurermps-6.2.0)

```powershell:TXTレコード消す
Remove-AzureRmDnsRecordSet -Name $RecordName -RecordType TXT -ResourceGroupName $AzureDnsResourceGroupName -ZoneName $AzureDnsZoneName
```

## Cloud Shell上でやりたい！
* PowerShellってモジュールっていれられるんだっけ
    * →いれられました。
    * (Cloud Shell(Bash)はsudoないらしいけど、普通のACME linux系クライアント入れれるのかしら)
* ACMEVaultを永続化させないと！
    * CloudDriveに永続化しよう
        * CloudShell(PowerShell)は標準で`C:\Users\ContainerAdministrator\CloudDrive\`にマウントされるらしい(ドキュメント見つからず)
    * ACMEのVaultProfileでパスを指定可能 [Set-ACMEVaultProfile](https://github.com/ebekker/foobar/wiki/Set-ACMEVaultProfile)
        * デフォルトは以下いずれか
            * 管理者:`%ALLUSERSPROFILE%\ACMESharp\sysVault`
            * 管理者以外:`%LOCALAPPDATA%\ACMESharp\userVault`
        * CloudDriveってSMBだから秘密鍵(PEM)暗号化でコケる…[BypassEFSオプション](https://pkisharp.github.io/ACMESharp-docs/Local-Vault-EFS)

# できあがったもの

以下の通りです。スクリプト全部置いておきます。NYSLです。

コマンド一つで CloudShellのSMB(CloudDrive)ルート直下に pfx形式で証明書が吐き出されます。

```powershell:呼び出し
.\CloudShell_AzureDNS_LetsEncrypt.ps1 -AzureDnsZoneName "sakoh.example" -CertificateDomainName "test.sakoh.example" -AzureDnsResourceGroupName "DNS-RG" -AccountMailAddress "example@sakoh.example" -CertificatePassword "Password"
```

```powershell:CloudShell_AzureDNS_LetsEncrypt.ps1
param(
 [Parameter(Mandatory=$True)]
 [string]
 $AzureDnsZoneName,

 [Parameter(Mandatory=$True)]
 [string]
 $CertificateDomainName,

 [string]
 [Parameter(Mandatory=$True)]
 $AzureDnsResourceGroupName,

 [string]
 [Parameter(Mandatory=$True)]
 $AccountMailAddress,

 [string]
 [Parameter(Mandatory=$True)]
 $CertificatePassword,

 [string]
 $CloudDrivePath = "C:\Users\ContainerAdministrator\CloudDrive\"
)


$ErrorActionPreference = "Stop"

$alias = $CertificateDomainName -replace "\.","_"
$certAlias = ($CertificateDomainName -replace "\.","_")+"_cert"

#ACMESharpのインストール
Write-Host "Install ACMESharp"
Install-Module -Name ACMESharp

#ACMESharpのインポート
Write-Host "Import ACMESharp"
Import-Module ACMESharp

#CloudDrive共有上にACMEのVaultを作成する
#※PEMファイルの暗号化(EFS)が効かないことに注意
Write-Host "Create ACME-Vault to CloudDrive(SMB)"
Set-ACMEVaultProfile -ProfileName "cloudrive" -Provider "local" -VaultParameters @{RootPath=$CloudDrivePath+"sysVault";CreatePath=$True;BypassEFS = $true} -Force

#以降のACMEコマンドで -VaultProfile "cloudrive"を省略するため、環境変数に設定
Set-Item env:ACMESHARP_VAULT_PROFILE -value "cloudrive"

#ACME Vaultの存在確認
Write-Host "Check ACME Vault"
if(!(Get-ACMEVault))
{
	#ACME Vaultの初期化
	Write-Host "Initialize ACME Vault"
	Initialize-ACMEVault
}

#Let's Encrypt CAにアカウントを登録(+利用規約に同意)
Write-Host "Regist Let's Encrypt CA Account and Accept TOS"
New-ACMERegistration -Contacts ("mailto:"+$AccountMailAddress) -AcceptTos

#証明書で保護するDNSドメイン名を送信
Write-Host "Send Target Host"
New-ACMEIdentifier -Dns $CertificateDomainName -Alias $alias

Complete-ACMEChallenge $alias -ChallengeType dns-01 -Handler manual

$Challenge = $null
do{
	#ドメイン所有権を証明するためのチャレンジが来るまで待機(5秒毎)
	Write-Host "Wait until Challenge comes"
	Start-Sleep -s 5
	$Challenge = ((Update-ACMEIdentifier $alias -ChallengeType dns-01).Challenges | Where-Object {$_.Type -eq "dns-01"}).Challenge
}
while($Challenge -eq $null)

#DNS-01なので(ACMESharp的に)手動処理
$RecordName = $Challenge.RecordName -replace ("\."+$AzureDnsZoneName),""
$RecordValue = $Challenge.RecordValue

#AzureDNSのTXTレコードを作成
Write-Host "Create Azure DNS TXT record"
$Records = @()
$Records += New-AzureRmDnsRecordConfig -Value $RecordValue
New-AzureRmDnsRecordSet -Name $RecordName -RecordType TXT -ResourceGroupName $AzureDnsResourceGroupName -TTL 3600 -ZoneName $AzureDnsZoneName -DnsRecords $Records

#ドメイン所有権を証明するチャレンジレスポンスを送信
Write-Host "Send challenge response"
Submit-ACMEChallenge $alias -ChallengeType dns-01

do{
	#チャレンジの結果が来るまで待機(5秒毎)
	Write-Host "Wait until Challenge result comes"
	Start-Sleep -s 5
	if((Update-ACMEIdentifier $alias).Status -eq "invalid"){
		throw "challenge invalid"
	}
}
while((!(Get-ACMEIdentifier $alias).Status -eq "valid") -And (!(Update-ACMEIdentifier $alias).Status -eq "invalid"))

#証明書署名要求を作成
Write-Host "Create CSR"
New-ACMECertificate $alias -Generate -Alias $certAlias

#証明書署名要求を送信
Write-Host "Submit CSR"
Submit-ACMECertificate $certAlias

#発行が即時終わる場合もあるが、待つ必要があるときもある
$Certificate = $null
do{
	Write-Host "Wait until sign result comes"
	Start-Sleep -s 5
	$Certificate = (Update-ACMECertificate $certAlias)
}
while($Certificate.IssuerSerialNumber -eq $null)

#証明書をPKCS#12でエクスポート
Write-Host "export pkcs#12" 
Get-ACMECertificate $certAlias -ExportPkcs12 ($CloudDrivePath+$CertificateDomainName+".pfx") -CertificatePassword $CertificatePassword

#AzureDNSのTXTレコードを削除
Write-Host "delete Azure DNS TXT record" 
Remove-AzureRmDnsRecordSet -Name $RecordName -RecordType TXT -ResourceGroupName $AzureDnsResourceGroupName -ZoneName $AzureDnsZoneName
````

# おわりに

将来的にはKeyVaultにいれる＆Automationで自動更新とかしたいですね。
今回証明書作ったきっかけは、ApplicationGateway用で、KeyVault未対応ぽいので、現時点ではこれで終わりです。

ハマったポイントとしては、ACMESharpの使い方で ２点あって、

1. [QuickStart](https://github.com/ebekker/ACMESharp/wiki/Quick-Start)に記載の、`Complete-ACMEChallenge`の出力が[死んでる](https://github.com/ebekker/ACMESharp/issues/335)点、30分くらい無駄にした。
    * チャレンジの値はUpdate-ACMEIdentifierで更新して、そのオブジェクトを探ることで回避
1. チャレンジを受けるコマンドでオプション間違えると、その要求がACMESharp上で Submitできなくなったりして、１時間くらい無駄にした。
    * (1回目だけ?)Update-ACMEIdentifierをChallengeTypeなしで呼ぶと死ぬぽ。
    * ACMEのプロトコル理解があまり進んでないけど、理解してればすぐに解決したのだろうか…

ACMESharp中身C#で書かれてるみたいなので、いつか読んで理解したいと思いました まる