I. Contexte
Comme vous le savez, je dispose d'un nom de domaine (romainpiquard.fr). Cependant, mon hébergeur ne me propose pas de certificat SSL gratuit pour celui-ci.
Après quelques recherches, je suis tombé sur le merveilleux site Let's Encrypt qui permet justement de générer des certificats SSL gratuitement.
II. Fonctionnement de Let's Encrypt
Le fonctionnement de celui-ci est assez simple. Vous demandez à Let's Encrypt de vous fournir un certificat pour le nom de domaine de votre choix et il vous le fourni pour une validité de 3 mois ! Il vous suffit donc tous les 3 mois (plutôt tous les 2 mois selon les recommandations de Let's Encrypt) de faire une demande pour obtenir un nouveau certificat à jour.
Pourquoi seulement 3 mois me direz-vous ? Cette durée est volontairement courte afin de forcer les administrateurs des sites d'automatiser la mise à jour des certificats. En effet, un certificat d'un an pourrait très bien être généré à la main via Let's Encrypt puis déployé sur son site, toujours à la main. Par ailleurs, il est fort possible que Let's Encrypt diminue encore cette durée dans un futur proche.
Dans les faits, l'obtention du certificat est un tout petit peu plus compexe que cela car Let's Encrypt ne vous fournira le certificat que si vous pouvez prouver que le domaine vous appartient. Pour cela, il existe plusieurs "challenges" (c'est-à-dire des méthodes de vérification) dont voici les 2 principales :
- Challenge HTTP-01 : Let's Encrypt vous fourni un token que vous devez placer à l'adresse "http://<YOUR_DOMAIN>/.well-known/acme-challenge/<TOKEN>". Il va ensuite valider que le token s'y trouve.
- Challenge DNS-01 : Let's Encrypt vous fourni également un token sauf que vous devez cette fois-ci le placer dans un record TXT à l'adresse "_acme-challenge.<YOUR_DOMAIN>". Cette solution permet de générer des certificats wildcards (*) qui valident alors l'ensemble des sous-domaines contrairement au challenge précédent qui ne va valider que des sous-domaines spécifiques.
Avec mon hébergeur, je ne peux pas automatiser la création de records TXT. J'utilise donc uniquement le challenge HTTP-01. C'est ce challenge que j'ai scripté afin de l'automatiser et dont vous aurez le détail dans les chapitres suivants.
III. Outils pour l'implémentation de Let's Encrypt
Sur le site de Let's Encrypt, vous pourrez trouver plusieurs dizaines d'outils permettant de générer automatiquement le certificat pour le domaine de votre choix. Ils vont s'occuper de valider le challenge pour vous et même de déployer le certificat sur le site de votre choix.
Cette liste se trouve à cette adresse : https://letsencrypt.org/docs/client-options/
Pour les moins curieux d'entre vous ou qui souhaitent déployer le certificat simplement, je peux vous recommander les deux clients suivants :
- Win-ACME : https://www.win-acme.com/ (Pour Windows et IIS/Apache)
- Certbot : https://certbot.eff.org/ (Pour Linux et Apache/Nginx/Plesk/...)
Dans notre cas, nous allons utiliser la bibliothèque ACME-PS (https://github.com/PKISharp/ACMESharpCore-PowerShell) qui va nous permettre de faire un script PowerShell permettant la génération et le déploiement d'un certificat sur nos différents sites.
IV. Configuration IIS
Une configuration au niveau IIS est nécessaire pour autoriser la création d'un fichier sans extension au niveau de votre site web (qui contiendra le token fourni par Let's Encrypt). Pour cela, dans IIS, sélectionnez votre serveur puis cliquez sur MIME Types.
Si l'extension "." n'existe pas encore, ajoutez-la avec comme MIME type : text/plain
V. Implémentation du script ACME-PS
La première étape consiste à installer le module sur la machine qui héberge votre site web (et qui exécutera donc le script). L'installation se fait dans PowerShell via la commande suivante :
Install-Module -Name ACME-PS
Par ailleurs, toute la documentation concernant ce module est disponible à cette adresse : https://github.com/PKISharp/ACMESharpCore-PowerShellUne fois fait, vous pouvez récupérer le code ci-dessous et le coller dans un fichier .ps1 sur toujours sur la même machine :
Param(
[bool]$CreateAccount = $false,
[Parameter(Mandatory=$true)]
[string[]]$Domains,
[Parameter(Mandatory=$true)]
[string]$SiteFolderName,
[Parameter(Mandatory=$true)]
[string]$SiteName
)
#region Configurations
$stateDir = "C:\AcmeState"
$serviceName = "LetsEncrypt" #Pour faire des tests, utiliser "LetsEncrypt-Staging" qui générera des certificats de test
$contactMail = "[email protected]"
$wwwRoot = "C:\inetpub\wwwroot"
#endregion
Import-Module ACME-PS
#Création d'un compte ACME
function CreateAccount
{
$state = New-ACMEState -Path $stateDir
Get-ACMEServiceDirectory $state -ServiceName $serviceName -PassThru
New-ACMENonce $state
New-ACMEAccountKey $state -PassThru
New-ACMEAccount $state -EmailAddresses $contactMail -AcceptTOS
}
#Nettoie le dossier AcmeState
function CleanOldCertificate
{
Param(
[string]$DnsName
)
Get-ChildItem $stateDir -Recurse -File -Force -Filter $DnsName* | Remove-Item
}
#Création du certificat
function GenerateCertificate
{
Param(
[string[]]$DnsNames,
[string]$SiteFolderName
)
$DnsName = $DnsNames[0]
#Supprime les anciens certificats
CleanOldCertificate -DnsName $DnsName
$state = Get-ACMEState -Path $stateDir
New-ACMENonce $state -PassThru
$identifiers = @()
for ($i = 0; $i -lt $DnsNames.Count; $i++)
{
$identifiers += New-ACMEIdentifier $DnsNames[$i]
}
$order = New-ACMEOrder $state -Identifiers $identifiers
$allAuthz = Get-ACMEAuthorization -State $state -Order $order
foreach ($authZ in $allAuthz)
{
$challenge = Get-ACMEChallenge $state $authZ "http-01"
$challenge.Data
$fileName = $wwwRoot + "\" + $SiteFolderName + $challenge.Data.RelativeUrl
$challengePath = [System.IO.Path]::GetDirectoryName($filename)
if(-not (Test-Path $challengePath)) {
New-Item -Path $challengePath -ItemType Directory
}
Set-Content -Path $fileName -Value $challenge.Data.Content -NoNewLine
Invoke-WebRequest $challenge.Data.AbsoluteUrl
$challenge | Complete-ACMEChallenge $state
}
while($order.Status -notin ("ready","invalid")) {
Start-Sleep -Seconds 10
$order | Update-ACMEOrder $state -PassThru
}
$certKey = New-ACMECertificateKey -Path "$stateDir\$DnsName.key.xml"
Complete-ACMEOrder $state -Order $order -CertificateKey $certKey
while(-not $order.CertificateUrl) {
Start-Sleep -Seconds 15
$order | Update-Order $state -PassThru
}
Export-ACMECertificate $state -Order $order -CertificateKey $certKey -Path "$stateDir\$DnsName.pfx"
}
#Installation du certificat en local
function InstallCertificateLocal
{
Param(
[string]$CertificatePath,
[string]$CertificateName,
[string[]]$DnsNames,
[string]$SiteName
)
$certRootStore = "LocalMachine"
$certStore = "WebHosting"
$pfxPass = ""
#Installe le certificat dans le store
$pfx = New-Object System.Security.Cryptography.X509Certificates.X509Certificate2
$pfx.Import("$CertificatePath\$CertificateName", $pfxPass, "Exportable,PersistKeySet,MachineKeySet")
$store = New-Object System.Security.Cryptography.X509Certificates.X509Store($certStore, $certRootStore)
$store.Open("ReadWrite")
$store.Add($pfx)
$store.Close()
$certThumbprint = $pfx.Thumbprint
#Installe le certificat dans le binding IIS
DeployCertificateIIS -SiteName $SiteName -CertThumprint $certThumbprint -DnsNames $DnsNames
#Supprime les certificats expirés
RemoveExpiredCertificate
}
#Déploie le certificat dans IIS
function DeployCertificateIIS
{
Param(
[string]$SiteName,
[string]$CertThumprint,
[string[]]$DnsNames
)
for ($i = 0; $i -lt $DnsNames.Count; $i++)
{
$currentDnsName = $DnsNames[$i]
$webBinding = Get-WebBinding -Name $SiteName -Port 443 -Protocol "https" -HostHeader $currentDnsName
if ($webBinding -eq $null)
{
New-WebBinding -Name $SiteName -Port 443 -Protocol "https" -HostHeader $currentDnsName -SslFlags 1
$webBinding = Get-WebBinding -Name $SiteName -Port 443 -Protocol "https" -HostHeader $currentDnsName
}
$webBinding.AddSslCertificate($CertThumprint, "WebHosting")
}
}
#Supprime les certificats expirés
function RemoveExpiredCertificate
{
$today = Get-Date
$certs = Get-Item Cert:\LocalMachine\WebHosting
$certs.Open([System.Security.Cryptography.X509Certificates.OpenFlags]::ReadWrite)
$expiredList = Get-ChildItem $certs.PSPath | Where-Object { $_.NotAfter -lt $today }
ForEach ($cert in $expiredList)
{
$certs.Remove($cert)
}
$certs.Close()
}
#Main
if ($CreateAccount)
{
CreateAccount
}
GenerateCertificate -DnsName $Domains -SiteFolderName $SiteFolderName
InstallCertificateLocal -CertificatePath "$stateDir" -CertificateName ($Domains[0]+".pfx") -DnsName $Domains -SiteName $SiteName
Avant d'exécuter le script pour la première fois, il est nécessaire d'effectuer les opérations suivantes :
- Créer le dossier C:\AcmeState qui contiendra toutes les informations sur vos certificats ainsi que les certificats générés
- S'assurer que les lignes 12 à 15 sont correctement configurées. L'adresse email est importante car elle permet à Let's Encrypt de vous envoyer un mail en cas de failles liées à la génération de certificats par exemple (cf. https://community.letsencrypt.org/t/revoking-certain-certificates-on-march-4/114864).
Il faudra bien faire toutes les exécutions suivantes avec le paramètre "CreateAccount" à $false, même pour de nouveaux domaines.
L'exécution du script se fait comme suit :
.\CreateCertificate.ps1 -CreateAccount $false -Domains "votredomaine.fr" -SiteFolderName "DossierDuSite" -SiteName "NomDuSite"
Je détaille ci-dessous les paramètres :
- CreateCertificate.ps1 : Le nom de votre script
- CreateAccount : $true lors de la toute première exécution, puis $false toutes les autres fois
- Domains : Domaine pour lequel vous souhaitez générer un certificat. Vous pouvez également générer un certificat pour un sous-domaine (exemple : "test.mondomaine.fr") ou même générer un certificat pour plusieurs domaines si ceux-ci pointent tous vers le même site (exemple : "mondomaine.fr","www.mondomaine.fr")
- SiteFolderName : Nom du dossier qui contient le site. Il s'agit du dossier situé directement sous "wwwroot" dans la configuration par défaut IIS
- SiteName : Nom du site dans IIS
VI. Automatiser l'exécution du script tous les 60 jours
Pour automatiser la mise à jour des certificats, rien de plus simple, il vous suffit de créer une tâche planifiée pour chacun de vos sites, configurée comme suit (tout ce qui n'est pas mentionné peut rester par défaut) :
- General :
- Run whether user is logged on or not
- Run with highest privileges
- Trigger :
- Begin the task : On a schedule
- Settings : Daily, recur every 60 days
- Action : Start a program
- Program/script : powershell.exe
- Add arguments : -Command "& 'CHEMIN_VERS_VOTRE_SCRIPT\CreateCertificate.ps1' -CreateAccount $false -Domains 'votredomaine.fr' -SiteFolderName 'DossierDuSite' -SiteName 'NomDuSite'"
- Settings :
- Run task as soon as possible after a scheduled start is missed
VII. Sources
- Pourquoi un certificat de 3 mois ? : https://letsencrypt.org/2015/11/09/why-90-days.html
- Module "ACME-PS" : https://github.com/PKISharp/ACMESharpCore-PowerShell