Miroslav Holec
Premium

Azure Cloud Service: programový import a nastavení SSL certifikátů ve Web Roli

Miroslav Holec   13. září 2018

Tento článek je již zastaralý. Článek nemusí popisovat aktuální stav technologie, ideální řešení a můj současný pohled na dané téma.

Před nedávným časem jsem vytvářel řešení pro HAVIT, v rámci kterého se našemu zákazníkovi automatizovaně generovali SSL certifikáty a následně je připojovali ve službě Cloud Service během provisioningu webové role. Díky tomu si mohou tisíce uživatelů SaaS řešení zapnout HTTPS v nasatavení svých webových stránkek, které běží na jedné společné Azure Cloud Service. Pokud budete chtít nasadit takové řešení do cloudu, narazíte na několik potíží, které se poměrně obtížně ladí. V článku popíšu většinu z nich, včetně možného řešení.

Pokud by vás více zajímala problematika jak automatizovaně generovat SSL certifikáty pro stovky webových stránek a ty pak následně připojovat ve službě Azure Cloud Service, podívejte se na případovou studii, kde jsem celé řešení prezentoval.

Certificate Store a Service Manager

Chcete-li si napsat vlastní workflow, během kterého budete v rámci Web Role pracovat se Server Manager nebo Certificate Store, čekají vás mnohá překvapení. V mém případě jsem skončil u Microsoft Azure supportu s jehož pomocí jsem sice chytil trochu vítr do plachet, ale o výrazné nápomocnosti se mluvit rozhodně nedá. MS Support jsem nakonec vzdal a vydal se cestou ladění, pokusů a omylů.

Certificate Store je úložiště certifikátů, kam si budete chtít certifikáty importovat z jiného úložiště. Zdrojem může být například databáze, azure storage nebo úplně jiné místo. Importovaný certifikát budete moci následně nabindovat v IIS k vybrané Site. Ovšem pozor. Aby byl certifikát v IIS použitelný, musíte splnit dvě pravidla

  1. certifikát je importován do Local Machine (NE User Store)
  2. certifikát je exportovatelný - obsahuje veřejný i privátní klíč

Pokud není splněna podmínka č. 1, certifikát v IIS nebudete vidět. Pokud není splněna podmínka č. 2, certifikát sice uvidíte, ale nepodaří se jej připojit.

Během ladění na lokálním počítači se vám podaří podmínky splnit, ale po nasazení do Cloud Service začne nekonečné ladění.

Import do Local Machine Store

Pro práci s certifikáty jsem se rozhodl použít typické místo WebRole.cs a metodu OnStart(), která zajistí při stagingu nastavení všeho potřebného. Pro update (nové certifikáty) nebo revoke lze použít metodu Run(), která provede různé změny za běhu.

Komunikaci s Certificate Store zajišťuje třída X509Store, která je součástí NuGet balíčku Microsoft.Web.Administration. Pomocí této třídy je možné importovat certifikát v podobě instance X509Certificate2. Prakticky to vypadá například takto:

public byte[] ImportCertificateToLocalStore(byte[] pfx, string password)
{
    using (var x509Certificate = new X509Certificate2())
    {
    	x509Certificate.Import(pfx, password, X509KeyStorageFlags.Exportable | 
	    X509KeyStorageFlags.PersistKeySet | X509KeyStorageFlags.MachineKeySet);

	    X509Store store = new X509Store(StoreName.My, StoreLocation.LocalMachine);
	    store.Open(OpenFlags.ReadWrite);
	    store.Add(x509Certificate);
	    store.Close();

	    return x509Certificate.GetCertHash();
    }
}

Používám certifikát uložený v byte[]. Už tady je důležité, aby tento certifikát v paměti obsahoval oba klíče a byl exportovatelný. Pomocí metody x509Certificate2.Import předávám certifikát, jeho heslo a doplňuji nezbytné flagy. Na dalších řádcích kódu pak otevírám LocalMachine certificate store, kam certifikát přidám.

Nedostatečná práva pro NetworkService

Po nasazení do Cloud Service jsem zjistil, že řešení není funkční, kvůli nedostatečným právům. Týkalo se to nejen práce s Certificate Store ale i při sahání na IIS pomocí Server Manageru.

Všechny webové služby, IIS atd běží pod "uživatelem" NetworkService. Jako jediné funkční řešení jsem s pomocí kolegy zvolil přidání tohoto účtu mezi administrátory. To jsem vyřešil pomocí StartupTasku, který se spouští před zavedením Web Role. Definice se provádí v ServiceDefinition.csdef

<WebRole name="Web" vmsize="Standard_D1_v2">
    <Startup>
      <Task commandLine="Startup.cmd" executionContext="elevated" taskType="simple" />
    </Startup>

Důležité je mít nastavený executionContext jako elevated, aby měl proces dostatečná práva pro tyto změny. Samotná přidání práv vypadají takto:

Add-LocalGroupMember -Group Administrators -Member NetworkService -ErrorAction SilentlyContinue  -Verbose

Zařazení mezi administrátory je potřeba zvážit z hlediska bezpečnosti. Výrazně lepší je vyrobit si speciální administrátorský účet, který má nezbytná privilegia (správa Certificate Store + IIS).

Chování na různých OS

Nastavení práv však také selhávalo, za čímž jsem našel dvě příčiny. Tou první je verze OS, která je na virtuálním stroji přítomná. V tomto případě jsem měnil ServiceConfiguration.Cloud.cscfg, protože zákazníkovi běžela aplikace na verzi osFamily=3.

<ServiceConfiguration serviceName="CloudServiceCertificates" osFamily="5" osVersion="*" schemaVersion="2015-04.2.6">
  <Role name="Web">

Více o tématu se nebudu rozepisovat, celkem hodnotná je v tomto směru oficiální dokumentace. Změnou na osFamily = 5 jsme zahostovali na Windows Server 2016 a překonali další potíž. Je možné, že tento krok se dal obejít i jinak.

Aliasy

Po nastavení jiného OS jsem objevil další divné chování. Automatizace nefungovala správně, ale po ručním přidělení NetworkService do skupiny Administrators bylo vše funkční. Náhodou mě napadlo nahradit tedy NetworkService za NT AUTHORITY\NETWORK SERVICE a bylo po problému. Někdy zkrátka nemá smysl na znacích šetřit :)

Práva zápisu do složek

Mnoho času jsem strávil řešením záhady, která mi nedávala smysl. Tedy dokud jsem nepochopil v čem je zakopaný pes. Můžete si projít odrážky níže a hádat, kde byl problém.

  • automatizovaně importovaný certifikát nejde připojit v IIS
  • ukazuje se, že není exportable
  • importuji certifikát ručně a ukazuje se, že exportable je
  • hodiny řeším, co tam x509store dělá, že to prostě není exportable, když import proběhne bez chyb
  • utíkají další hodiny a ze zoufalství tam tlačím certifikáty napůl ručně a napůl automatizovaně
  • aby to nebylo málo, občas to najednou zafunguje
  • dělám reimage a zase všechno rozhašený

Klíčová otázka byla, "proč tam není privátní klíč"? A pak jsem si vzpomněl na Microsoft support a naše hrabání se ve složkách na lokálním disku. Klíče se totiž ukládají do určitých složek na disku a pro zápis do těchto složek musí mít uživatel práva na toto místo zapsat. Když práva nemá, tak metoda X509Store.Import nevyhodí výjimku, ale chová se jako by bylo všechno OK.

Tím se vysvětluje, proč "to občas fungovalo". Totiž když jsem certifikát naimportoval ručně, jako adminovi se mi uložil správně i privátní klíč. Když jsem spustil automatizaci se stejným certifikátem (stejný SN), tak už tam ten klíč prostě byl a vše fungovalo. Po reimage nebo při použití jiného certifikátu samozřejmě řešení nebylo funkční. Logický.

Jako jediné funkční řešení v rámci tohoto workflow, které mi doporučil support Microsoftu bylo tedy umožnit pro NetworkService zápis na toto místo. Možná trochu kontroverzní, ale funkční. Použil jsem opět již jednou zmíněný StartupTask a doplnil pár řádků:

$Acl = Get-Acl "D:\ProgramData\Microsoft\Crypto\RSA\MachineKeys"
New-Object  System.Security.AccessControl.FileSystemAccessRule("NT AUTHORITY\NETWORK SERVICE","FullControl", "ContainerInherit,ObjectInherit", "None", "Allow")
$Acl.SetAccessRule($Ar) 
Set-Acl "D:\ProgramData\Microsoft\Crypto\RSA\MachineKeys" $Acl -Verbose

A to je všechno. Pokud plánujete dělat podobné kejkle s certifikáty, zkuste dát raději přednost službě App Service, kde je možné dělat podobné věci elegantněji, rychleji a možná i trochu bezpečněji než zpřístupněním vyšších oprávnění. Pokud máte za sebou podobné experimentování a zkušenosti a máte elegantnější řešení, podělte se o něj v diskusi.