Archives de catégorie : ServerLess

Azure API Management 5/5 – PowerShell Time

C’est la dernière ligne droite et aussi le temps du PowerShell. Pour Parler PowerShell, tout comme pour Azure Function, cela reposera sur un Invoke-WebRequest. C’est donc presque pareil, à quelques exceptions. La première chose qui change, c’est l’URL. Pour rappel, voilà l’URL de notre Azure Function exposée via notre instance du service Azure API Management.

clip_image001

 

La seconde chose qui change, c’est la clé d’authentification. Nous ne passons plus la clé d’authentification de notre Azure Function (c’est géré au niveau des policies d’Azure API Management) mais une des deux clés dont nous disposons dans la souscription au produit qui présente notre Azure Function. En tant que consommateur, nous pouvons retrouver cette information dans le portail développeur mis à disposition par l’instance du service Azure API Management. Cette clé, nous allons la référencer sous le nom « Ocp-Apim-Subscription-Key »

clip_image003

 

Nous avons toutes les informations, ne nous reste plus qu’à construire notre requête.

$url = « https://tpapimanagement.azure-api.net/AzuretableValetKeyAPI/api/AzuretableValetKeyAPI »

Invoke-RestMethod -Uri $Url -Method POST -ContentType « application/json » -Headers @{« Ocp-Apim-Subscription-Key »= »2fab81140f42490683XXXXXXXXa26ac0 »}

clip_image005

 

Nous y sommes. Nous avons réussi à consommer notre Azure Function exposée par Azure API Management.

 

BenoîtS – Simple and secure by design but Business compliant (with disruptive flag enabled)

Azure API Management 3/5 – Exposition de notre première API

A partir de maintenant, tout va se passer dans Azure API Management. Notre Azure Function est prête à être référencée comme API. Dans l’interface de gestion du service, en cliquant sur « API », on arrive à l’interface ci-dessous :

clip_image001

Azure API Management prend en charge plusieurs langages de description pour nos API. Par chance, l’intégration avec Azure Function est nativement supportée ou presque. L’interface nous propose bien de sélectionner notre Function App (ce qui porte notre fonction) mais pas la fonction en elle-même. C’est pour cela que je spécifie le paramètre API Url suffixe correspondant le nom de la fonction.

clip_image002

Autre sujet important, l’accès authentifié à notre Azure Function. Pour rappel, chaque fonction que nous avons développée dispose de sa propre URL et clé d’accès :

clip_image003

Si je ne fais rien, aucune authentification sera présentée à mon Azure Function. Il faut donc pouvoir intégrer ce code d’authentification dans l’appel depuis Azure API Management. Vu que c’est un secret, on va commencer par le stocker dans une Named Value. Au moment de l’écriture de cette série de billets, il n’est pas encore possible de stocker ses secrets dans un Azure KeyVault. A ce jour, c’est un feedback qui a été remonté à Microsoft : Integration with Azure KeyVault. En attendant KeyVault, nous allons utiliser les Named Values.

clip_image004

Azure API Management permet d’injecter des paramètres additionnels dans le Header. C’est une des nombreuses fonctionnalités offertes par les Azure Policies d’Azure API Management. Nous allons travailler au niveau « Inbound Processing »

clip_image005

Notre opération consistera à ajouter un nouveau paramètre à notre requête HTTP. Pour éditer ma policy, je suis passé en mode « Code-View » pour intégrer une règle de transformation nommée « Set query request parameter ». De cette manière, on va injecter le paramètre « Code » avec la valeur contenue dans la Named Value {{AzureFunctionAPICode}}. Reste à ne pas oublier de cliquer sur le bouton Save.

clip_image006

De retour dans la view « From View », c’est plus simple à comprendre.

clip_image007

Attention, l’utilisation du Named Parameters, dans la vue form view ne conservera pas le named parameters mais va juste remplacer le contenu. D’ou le passage en mode « Form View ».

BenoîtS – Simple and secure by design but Business compliant (with disruptive flag enabled)

Azure API Management 2/5 – Préparer l’exposition de notre Azure Function

Notre service Azure API Management est maintenant posé. Nous allons y revenir. Pour commencer, nous allons rendre notre Azure Function consommable par mon service API Management. Il faut un peu de préparation. A ce stade, il manque une description contenant :

  • Ce que produit notre API
  • Les paramètres qu’elle attend en entrée
  • Les paramètres qu’elle retourne
  • La ou ses différentes versions

Première étape, on va commencer par limiter ce que va accepter notre Azure Function en termes de verbes. Au niveau du nœud « Integrate » de notre fonction. Nous allons affiner la listes méthodes HTTP pour ne retenir que POST sans oublier de sauvegarder notre modification.

clip_image001

 

Prochaine étape, documenter notre API. Dans le contexte Azure Function, c’est le Framework Swagger qui a été retenu par Microsoft pour l’intégration avec Azure API management. Le Framework Swagger utiliser les spécifications OpenAPI. Allons configurer la définition de notre API en cliquant sur « API Definition ».

clip_image002

 

Azure Function peut être exposé sous forme d’API de deux manières :

  • Function (Preview pour l’intégration avec Azure API Management)
  • External URL (Utilisation d’un service tierce)

 

Azure API Management étant un service interne Azure, nous allons cliquer sur « Function (Preview) ».

clip_image003

 

Remarque importante, cela ne change strictement rien sur la méthode d’authentification de notre Azure Function. Nous pouvons toujours la solliciter ne direct dès lors que nous disposons de toutes les informations. C’est un sujet sur lequel nous allons revenir dans un prochain billet de la série.

clip_image004

 

Pour décrire notre API, nous allons utiliser les spécifications OpenAPI. L’intégration avec Azure est bien faite puisqu’on nous propose de créer un squelette JSON en cliquant sur le bouton « Generate API Definition template ».

clip_image005

 

OpenAPI va nous permettre de produire une description qui sera consommée par Azure API Management. Heureusement que mon API est simple. Globalement, j’ai indiqué :

  • Que mon API ne supportera que le HTTPS pas le HTTP
  • Que mon API ne supportait que la méthode POST via une URL bien précise
  • Qu’elle consomme ses paramètres au format JSON (même s’il n’y en a pas dans mon exemple)
  • Quelle produira du contenu au format JSON avec un seul message documenté

clip_image006

 

N’oubliez pas d’appuyer sur « Save ».

Avant d’aller plus loin dans l’intégration, nous allons déjà valider que cela fonctionne. Pour cela nous avons besoin d’une clé d’accès pour notre Azure Function. Dans le nœud « Manage » de notre Azure Function, nous allons récupérer une clé d’authentification en cliquant sur le bouton « Copy ».

clip_image007

 

De retour dans la configuration de la définition de notre API, dans la zone droite de l’interface, nous avons la possibilité de valider l’accès à notre future API. Cliquons sur « Change Authentication ».

clip_image008

 

Dans l’interface ci-dessous, nous allons renseigner la clé d’accès précédemment obtenus puis cliquer sur le bouton « Authenticate ».

clip_image009

 

Un peu plus bas dans l’interface, on constate la présence de la description de la seule méthode supportée par notre API : « Post ». En cliquant sur « Try this operation », nous allons réaliser un appel à notre future API avec la clé d’authentification.

clip_image010

 

Côté Request, mon API étant assez simpliste (aucun paramètre en entrée), ayant limité les choix d’utilisation à HTTPS seulement et imposé le JSON pour les paramètres, il ne nous reste qu’à appuyer sur le bouton « Send Request ».

clip_image011

 

Si tout se passe bien, nous devrions avoir le résultat retourné par notre Azure Function.

clip_image012

 

Côté Azure Function, les prérequis sont en place. Prochaine étape, l’exposition de notre première API.

BenoîtS – Simple and secure by design but Business compliant (with disruptive flag enabled)

Azure Functions – Introduction au ServerLess en PowerShell

Je suis rentré dans Azure Function un peu par hasard. Je cherchais une alternative aux Webhooks de Azure Automation. Le principe est bien mais pose quand même quelques contraintes :

  • Impossibilité de retrouver l’URL du WebHook une fois créée, faut avoir conservé l’information
  • Pas d’authentification. Si on connait l’URL, c’est accessible en mode open-bar
  • Impossibilité de personnaliser paramètres une fois le WebHook créé (le plus problématique pour moi)

C’est pour cette raison que je suis arrivé à Azure Function. En plus, je n’étais pas en terrain inconnu avec du PowerShell :

clip_image001

Certes, c’est encore expérimental mais de ce que j’ai pu tester, c’est assez stable pour envisager l’utiliser. Mon besoin était de disposer d’un mécanisme équivalent aux WebHooks mais avec :

  • Une méthode d’authentification un peu plus évoluée
  • La capacité à passer des paramètres pour exécution
  • La possibilité de retourner les résultats d’exécution du Runbook Azure Automation

Mise en œuvre

Côté mise en œuvre, nous devons commencer avec la mise en place d’une instance du service Function App. On retrouve pas mal de paramètres connus, on ne va s’attarder sur l’un d’entre eux :

clip_image002

 

Pour le Hosting Plan. On a le choix entre :

  • Consumption Plan
  • App Service Plan

Avec Azure Function, on est capable de fonctionner sans infrastructure aussi dit « server-less ». Dans le mode  » App Service Plan », les Azure Functions seront exécutées dans le contexte d’un App Service Plan. Si on paie déjà pour pour héberger un site web pourquoi pas mais mon besoin, c’est juste de pouvoir exposer une API pour exécuter un Runbook Azure Automation. J’ai donc retenu le mode « Consumption Plan ». Une instance du service App Service Plan sera mise à disposition uniquement quand un appel à mon API sera réalisé. Pour les coûts, c’est bien. Par contre, ce n’est pas forcément le mode de fonctionnement qui offrira les performances les plus élevées.

Le mode « App Service Plan » présente certains avantages. Vu que c’est un App Plan, on peut configurer toutes ses fonctionnalités (scale-out, sauvegarde, authentification, …). Personnellement en dessous de S1, ce n’est pas pour de la production.

Créer une Azure Function

On a le choix du langage. Ce qui est intéressant, c’est qu’une Azure Function a nécessairement un déclencheur :

  • Un Trigger HTTP (Déclencher l’appel sur une URL donnée)
  • Un trigger Timer (Déclencher l’appel selon une planification horaire)
  • Un Trigger EventGrid (tout nouveau avec le service Event grid)
  • L’arrivée du message dans une Azure Queue
  • L’arrivée d’un message dans topic Service Bus
  • Autres déclencheurs

En PowerShell, tout ça c’est encore expérimental, nos choix sont pour l’instant limités :

clip_image001[1]

 

Pour mon besoin, le HttpTrigger fera parfaitement l’affaire. Il ne nous reste qu’à trouver un nom à notre première Azure Function.

clip_image003

 

Avant de poursuivre, arrêtons-nous sur la notion d’Authorization Level. C’est l’aspect authentification qui me manquait dans les WebHooks d’Azure Automation. Cela repose sur une clé qui peut être identique à toutes les Azure function (Admin level) ou une clé spécifique à chaque Azure Function (Function level). Je passe sur Anonymous, je pense que vous avez compris, … Ce que ne montre pas l’interface, c’est qu’il est aussi possible d’utiliser toutes les méthodes d’authentification disponibles dans un Web App (si on a choisi le mode de déploiement (App Service plan). Par exemple, il est possible de déporter l’authentification au niveau d’une Azure API management Gateway, .. Dans mon contexte le niveau « Function » sera amplement suffisant pour ce billet.

Par défaut, il nous est proposé un exemple gendre « Hello World ». A droite, on voit bien le paramètre passé en paramètre et le résultat de l’appel. Pour l’instant, à l’exception du passage de paramètre, cela ressemble beaucoup à Azure Automation.

clip_image004

 

On rentre dans le côté Sexy d’Azure Function. Mon déclencheur est une WebRequest mais on peut compléter cet appel en liant l’Azure Function à une source de données tel qu’Azure Table, Azure Queue, Service Bus ou même CosmoDB, même principe pour la sortie. Dans mon contexte, nous allons nous contenter des paramètres par défaut. Ils conviennent pour mon usage :

clip_image005

 

Dans la rubrique Manage, on gère la disponibilité de la fonction mais aussi les fameuses clés d’authentification utilisables. Certes, si on connait l’URL avec la clé, on a accès comme pour les WebHooks dans Azure Automation. Je suis d’accord mais c’est pour cela que l’intégration à un App Service plan est intéressante car on peut alors choisir la méthode d’authentification qui nous intéresse, voire jusqu’à aller utiliser Azure API Gateway.

clip_image006

 

Azure Function propose un mécanisme de supervision. Il a le mérite d’exister mais une intégration avec Azure Application Insight sera bien plus utile. On y reviendra plus tard.

clip_image007

 

Nous avons un socle pour poser notre Azure Function. Pour commencer, la première chose dont nous allons avoir besoin c’est d’authentification. Le code PowerShell que nous allons exécuter devra déclencher un Runbook dans Azure Automation. On va tout de suite commencer par bannir l’idée du compte et mot de passe en clair dans le code PowerShell, c’est juste moche. Nous avons rencontré la même problématique avec Azure Automation dans mon billet Parlons d’identité avec Azure Automation. Ce sera la même approche à deux différences près :

  • Nous allons stocker les informations nécessaires non plus dans Azure Automation mais dans la Web Application, dans les Applications Settings
  • On n’utilisera pas un certificat mais un secret pour l’authentification du Service principal

 

Préparer une identité pour notre Azure Function

Utiliser un certificat serait certainement possible mais faudra que je creuse plus la question. En tout cas, on sait déjà qu’il ne sera pas la peine d’essayer aller le chercher dans un Azure Key Vault. Accéder à un Key Vault implique une authentification préalable, … Ce sera donc une application tout ce qu’il y a de classique avec juste un secret. Nous verrons plus tard comment offusquer ce secret.

$ADApplicationName = « myazurefunction991 »

$ADHomePage = « https://$ADApplicationName.azurewebsite.net« 

$IdentifierUris = $ADHomePage

$azureADApplication = New-AzureRmADApplication -DisplayName $ADApplicationName -HomePage $ADHomePage -IdentifierUris $IdentifierUris -Password « MyVerySecureP@ssw0rd »

$azureADApplication

clip_image008

 

Ensuite, nous allons lier cette application Azure AD nouvellement déclarée à un Service Principal.

$ServicePrincipal = New-AzureRmADServicePrincipal -ApplicationId $azureADApplication.ApplicationId

$ServicePrincipal

clip_image009

 

Ne reste plus qu’à positionner des permissions à notre Service Principal. Dans mon contexte, les ressources que mon Service Principal doit pouvoir manipuler sont dans un groupe de ressources nommé « AzureFunctionLabs ».

New-AzureRmRoleAssignment -RoleDefinitionName Contributor -ServicePrincipalName $azureAdApplication.ApplicationId.Guid -Scope (Get-AzureRmResourceGroup -Name « AzureFunctionLabs »).resourceId

clip_image010

 

Pour finir, on se met de côté quelques informations. Nous en aurons besoin pour mettre en place l’authentification dans notre Azure Function :

$AzureApplicationID = $azureADApplication.ApplicationId.Guid

$AzureADTenantID = (Get-AzureRmTenant).tenantid

$SubscriptionId = (Get-AzureRmContext).Subscription.SubscriptionId

$AzureApplicationID

$AzureADTenantID

$SubscriptionId

clip_image011

 

Avant de passer à Azure Function, on va commencer par poser le code PowerShell. Nous sommes en présence d’un Azure AD Service Principal. Autant pour la construction de la chaine SecureString, rien de change, c’est au niveau de la commande Add-AzureRmAccount que cela change :

$AzureApplicationID = « 0eb602cc-4243-4104-815a-dda4b6f7378a »

$AzureADTenantID = « dfe6b267-4f0f-45e5-a9ed-d0540af967dd »

$subscriptionID = « 85e3593c-6a07-4e81-a447-a06d6f5e6f89 »

$azurePassword = ConvertTo-SecureString « MyVerySecureP@ssw0rd » -AsPlainText -Force

$psCred = New-Object System.Management.Automation.PSCredential($AzureApplicationID, $azurePassword)

$psCred

Add-AzureRmAccount -Credential $psCred -TenantId $AzureADTenantID -ServicePrincipal -SubscriptionId $SubscriptionID

clip_image012

 

L’authentification fonctionne, ne reste plus qu’à transposer cela dans notre Azure Function. Le code PowerShell est strictement identique, juste le Out-File pour retourner le contexte Azure :

$SubscriptionID = ’85e3593c-6a07-4e81-a447-a06d6f5e6f89′

$AzureApplicationID = ‘0eb602cc-4243-4104-815a-dda4b6f7378a’

$TenantID = ‘dfe6b267-4f0f-45e5-a9ed-d0540af967dd’

$azurePassword = ConvertTo-SecureString « MyVerySecureP@ssw0rd » -AsPlainText -Force

$psCred = New-Object System.Management.Automation.PSCredential($AzureApplicationID, $azurePassword)

Add-AzureRmAccount -Credential $psCred -TenantId $TenantID -ServicePrincipal -SubscriptionId $SubscriptionID

$context = Get-AzureRMContext

Out-File -Encoding Ascii -FilePath $res -inputObject $context

clip_image013

 

Nous avons quelque chose de fonctionnel mais qui d’un point de vue sécurité laisse à désirer. Laisser trainer des informations comme l’identifiant de l’application Azure AD ou même son secret associé, ce n’est pas génial. Nous allons déplacer ces informations dans des variables au niveau des Application Settings

Au lieu d’avoir nos paramètres dans le code de la fonction, l’ai retenu de les placer directement dans les Applications Settings, donc au niveau de l’Azure App Service.

clip_image014

 

Certains me dirons que ce n’est pas encore parfait. Tout de suite, on pense à Azure Key Vault mais encore une fois, il faut être authentifié pour accéder à Azure Key Vault. Dans l’état actuel des choses, la seule amélioration de sécurité envisageable serait de ne pas s’authentifier avec un secret mais un certificat. C’est un sujet que j’explorerai ultérieurement.

Pour notre code, ces variables sont disponibles dans PowerShell dans les variables d’environnements. C’est déjà beaucoup mieux pour notre code.

$AzureADPassword = $env:AzureADPassword

$SubscriptionID = $Env:SubscriptionID

$TenantID = $Env:TenantID

$AzureApplicationID = $env:AzureApplicationID

$SecureString = ConvertTo-SecureString $AzureADPassword -AsPlainText -Force

$psCred = New-Object System.Management.Automation.PSCredential($AzureApplicationID, $SecureString)

Add-AzureRmAccount -Credential $psCred -TenantId $TenantID -ServicePrincipal -SubscriptionId $SubscriptionID

$context = Get-AzureRMContext

Out-File -Encoding Ascii -FilePath $res -inputObject $context

clip_image015

 

Retour au besoin initial

Maintenant que nous sommes authentifiés dans Azure, revenons à mon besoin initial : Exécuter un Runbook dans Azure depuis une Azure Function avec passage de paramètre et pouvoir suivre l’avancement.

Pour le Runbook, j’ai pris ce qu’il y a de plus minimaliste possible pour ma démonstration :

param (

    [Parameter(Mandatory=$true)]

    [String]$Name

)

Write-Output « Hello $name »

Je vous laisse comprendre le paramètre en entrée, …

clip_image016

 

Pas grand-chose à dire de plus côté Azure Automation, ce qu’il nous faut maintenant, c’est d’intégrer l’appel du Runbook à notre Azure Function. J’ai commencé par ajouter un paramètre en entrée qui sera obligatoire. Après, ce n’est que du PowerShell tout ce qu’il y a de plus classique :

$requestbody = Get-Content $req -raw | ConvertFrom-JSON

$name = $requestBody.Name

If ($name -ne $null)

{

$AzureADPassword = $env:AzureADPassword

$SubscriptionID = $Env:SubscriptionID

$TenantID = $Env:TenantID

$AzureApplicationID = $env:AzureApplicationID

$SecureString = ConvertTo-SecureString $AzureADPassword -AsPlainText -Force

$psCred = New-Object System.Management.Automation.PSCredential($AzureApplicationID, $SecureString)

Add-AzureRmAccount -Credential $psCred -TenantId $TenantID -ServicePrincipal -SubscriptionId $SubscriptionID

$context = Get-AzureRMContext

$params = @{« Name »=$name}

$AutomationAccount = « MyAutomation992 »

$resourceGroup = « PersonalBackup »

$RunbookName = « HelloAutomationWorld »

$job = Start-AzureRmAutomationRunbook –AutomationAccountName $AutomationAccount –Name $RunbookName -ResourceGroupName $resourceGroup –Parameters $params

Out-File -Encoding Ascii -FilePath $res -inputObject $job

}

Else

{

Out-File -Encoding Ascii -FilePath $res -inputObject « Invalid Parameter. »

}

<Je suis un boulet, AKA Kevin le boulet> : A ceux qui se demandent pourquoi il utilise une instance Azure Automation qui n’est pas dans le groupe de ressources pour lequel le Service Principal créé a obtenu des permissions, c’est que je suis un boulet. En exécutant L’Azure Function la première fois il m’a été clairement indiqué un manque de permissions. J’ai donc ajouter les permissions uniquement sur l’instance Azure Automation référencé dans mon code </Je suis un boulet, AKA Kevin le boulet>.

C’est maintenant qu’on comprend pourquoi il y a deux boutons « Run » dans l’éditeur. Celui-qui se trouve en bas à droite permet d’appeler notre Azure Function avec le ou les paramètres nécessaires à l’exécution.

clip_image017

 

Volontairement, j’ai retenu de récupérer le JobID du job qui a exécuté le Runbook. Attendre la bonne exécution du Runbook n’a pas de sens avec Azure Function car vous êtes limités dans le temps (voir à la fin du billet). Par contre, ayant récupéré le JobID, rien ne vous interdit de revenir plus tard pour vérifier le bon déroulement de votre Job.

Test en condition réelle

Tester en condition réelle, c’est juste quatre lignes de PowerShell pour invoquer notre Azure Function. Pour cela, encore faut-il connaître l’URL de notre Azure Function avec sa clé.

clip_image018

 

Après, ce n’est que du PowerShell :

$url = « « 

$parameters = @{Name=’BenoitS’}

$json = $parameters |ConvertTo-Json

Invoke-RestMethod -Uri $Url -Method POST -ContentType ‘application/json’ -Body $json

clip_image019

 

Je vous laisse deviner le résultat de l’exécution du Runbook, …

 

Supervision

Pour ceux qui se sont demandé à quoi pouvait bien servir la variable APPINSIGHTS_INSTRUMENTATIONKEY dans les Application Settings, c’est la clé pour permettre de connecter Azure Function à Application Insight. L’intégration entre les deux composants est automatique. Le problème, c’est que pour le portail, cela veut dire de créer une nouvelle instance du service Application Insights ;). Je vous recommande donc de créer votre instance du service Azure Function App puis de la raccorder à Application Insights ultérieurement : Azure Functions now has direct integration with Application Insights. Une fois l’intégration en place, on peut suivre pas mal d’indicateurs de performance.

clip_image020

 

Côté performance, avec un peu de tunning, on obtient des performances plus acceptables pour une API.

clip_image021

 

Quelques astuces pour finir

La liste des trucs sur lesquels j’ai buté avant de comprendre. Pour certains d’entre eux, ça a juste été des heures de perdues :

  • La version actuellement supportée de PowerShell est la version 4.0. Pensez-y.
  • Si un Runbook Azure Automation peut s’exécuter pendant 300 minutes, une Azure function, c’est limité à 300 secondes. Voilà pourquoi je ne voulais pas attendre le résultat de l’exécution de mon Runbook.
  • Le paramètre NoNewLine pour Out-File qui n’existe qu’en PowerShell 5 : The PowerShell 5 NoNewLine Parameter (Grand merci Mr Scripting Guy pour toutes ces années depuis VbScript)
  • Le contournement à l’absence du Paramètre NoNewLine en PowerShell 4.0 : HTTP Binding in PowerShell Azure Functions
  • Ne cherchez pas à importer les modules Powershell, uploader les en FTP: Using PowerShell Modules in Azure Functions
  • Penser à configurer votre App Service Plan pour la plateforme X64 et non comme par défaut, c’est meilleur pour les performances.

 

Conclusion

Maintenant avec Azure Function, je suis capable d’appeler un Runbook Azure Automation avec authentification et surtout en étant capable de passer aussi un paramètre. Ce qui va nous intéresser d’autant plus, c’est l’aspect facturation. Pour une instance Azure Automation 500 minutes d’exécution dans un mois, c’est juste 1€, soit dans mon échelle trois capsules de Nespresso :

clip_image022

 

Avec Function App, pour un usage peu consommateur en mémoire et des API bien écrites, on a de grandes chances de ne même pas être facturé ou alors pas assez pour payer un café.

clip_image023

 

Ça change grandement la perception sur la conception d’applications, … A bientôt pour d’autres sujets Azure Function, toujours en PowerShell, …

BenoitS – Simple and Secure by design but Business compliant (with disruptive flag enabled)