Loupe

AppService - créer / uploader un certificat à la volée avec Azure App Fluent.

Alors que certains sont des grands fans d'ARM, j'ai tendance à préférer la librairie .Net Azure Fluent. Mes environnements Azure sont donc décrits et générés via une application console avec le strict minimum d'opérations manuelles. Dans cet article, nous verrons comment créer un certificat auto-signé depuis votre code et l'utiliser dans votre AppService !

Création du certificat

La première étape consiste à générer un certificat que l'on peut utiliser sur notre AppService. Pour cela, je n'ai rien inventé, on passe par les classes du namespace System.Security.Cryptography et notamment la classe X509Certificate2 :

private X509Certificate2 BuildSelfSignedServerCertificate
  (string certName, string password)
{
  var distinguishedName = new X500DistinguishedName($"CN={certName}");
  using (RSA rsa = RSA.Create(2048))
  {
    var request = new CertificateRequest(
      distinguishedName,
      rsa,
      HashAlgorithmName.SHA256,
      RSASignaturePadding.Pkcs1);
    X509KeyUsageFlags x509KeyUsageFlags 
      = X509KeyUsageFlags.DataEncipherment
        | X509KeyUsageFlags.KeyEncipherment
        | X509KeyUsageFlags.DigitalSignature;
    
    request.CertificateExtensions.Add(
      new X509KeyUsageExtension(
        x509KeyUsageFlags,
        false));
    request.CertificateExtensions.Add(
      new X509EnhancedKeyUsageExtension(
      new OidCollection { new Oid("1.3.6.1.5.5.7.3.1") }, false));
    var sanBuilder = new SubjectAlternativeNameBuilder();
    request.CertificateExtensions.Add(sanBuilder.Build());
    var certificate = request.CreateSelfSigned(
      new DateTimeOffset(DateTime.UtcNow.AddDays(-1)),
      new DateTimeOffset(DateTime.UtcNow.AddDays(360)));
    var rawData = certificate.Export(X509ContentType.Pfx, password);
    return new X509Certificate2(
      rawData, 
      password, 
      X509KeyStorageFlags.Exportable);
  }
}

Upload du certificat

Azure Fluent nous permet d'uploader un certificat sur un AppService en une seule opération. C'est chouette mais il faut faire attention quand même : les certificats sont en réalité associés à votre AppService Plan. Cela implique que lorsque vous renseignez le nom du ResourceGroup où placer le certificat il faut bien mettre celui du plan et non celui de l'AppService. Le cas contraire, vous n'auriez aucun message d'erreur mais votre certificat ne serait visible nulle part et... inutile.

var password = CreateRandomPasswordWithRandomLength();
var certificate = BuildSelfSignedServerCertificate(hostName, password);

// On récupère le pfx à téléverser
var pfx = certificate.Export(X509ContentType.Pfx, password);

var existing = _azure.WebApps
   .ListByResourceGroup(webApp.ResourceGroupName)
  .First(app => app.Id == webApp.Id);

var appServiceCertificate = existing.Manager.AppServiceCertificates
    .Define($"{certificate.Thumbprint}-{resourceGroupName}")
    .WithRegion(existing.Region)
    .WithExistingResourceGroup(resourceGroupName)
    .WithPfxByteArray(pfx)
    .WithPfxPassword(password)
    .Create();

Gestion automatique des dates d'expirations 

Nous savons maintenant uploader un certificat dans notre AppService. Pour éviter de le faire à chaque exécution de notre outil de création d'un environnement, on va se servir d'Azure Fluent pour effectuer ces opérations : lister les certificats existants sur notre AppService, vérifier leur date d'expiration et si la date d'expiration est trop proche, supprimer l'ancien certificat pour le remplacer par un nouveau.

L'algorithme se base notamment sur le fait que l'on utilise toujours le même nom d'hôte (qui peut être n'importe quelle chaîne de caractère). Cela s'écrit, encore une fois, de façon très rapide avec Azure Fluent. 

 var allCertificates = _azure.AppServices.AppServiceCertificates
   .ListByResourceGroup(appPlanResourceGroupName);

      var toDelete = allCertificates
        .Where(c => c.HostNames.Any(hn => hn == hostname))
        .OrderBy(a => a.ExpirationDate).ToArray();

      if (toDelete.Any())
      {
        for (var index = 0; index < toDelete.Length; index++)
        {
          var isLast = index == toDelete.Length - 1;
          var appCert = toDelete[index];

          //    EXP. DATE   NOW
          // =========|============|=================
          if (isLast 
            && appCert.ExpirationDate.AddDays(30) >= DateTime.Now)
          {
            // cceriticat existant est valide  
            return null;
          }

          // suppression de l'ancien qui n'est plus valide.
          _azure.AppServices.appCert
            .DeleteById(appCert.Id);
        }
      }

 

Mise à disposition dans votre API

Une fois ceci fait, il reste nécessaire de rendre votre certificat disponible à votre API en mettant son thumbprint dans la configuration de l'AppService sous la clef WEBSITE_LOAD_CERTIFICATES. Ceci est documenté sur le site de Microsoft si vous souhaitez en savoir plus et je l'avais déjà utilisé dans mon article expliquant la mise en place d'OpenId sur une WebApp Azure.

Le renseignement de la configuration sur l'AppService se fait en quelques lignes de code : 

var thumb = certificate.Thumbprint;

webApp.Update()
  .WithAppSetting("WEBSITE_LOAD_CERTIFICATES", thumb)
  .Apply();

Il ne reste donc plus alors qu'à vous assurer de lancer votre outil de déploiement d'environnement au moins une fois par mois (on vérifie si le certificat n'expire pas d'ici 30 jours).


Happy coding !

Photo de profil

Ces billets pourraient aussi vous intéresser

Vous nous direz ?!

Commentaires

comments powered by Disqus