Partager via


Considérations relatives à la sécurité pour les charges de travail stratégiques

Les charges de travail stratégiques doivent être sécurisées par nature : si une application, ou son infrastructure, est compromise, la disponibilité est à risque. L’objectif de cette architecture est d’optimiser la fiabilité afin que l’application reste performante et disponible en toutes circonstances. Des contrôles de sécurité sont appliqués principalement dans le but d’atténuer les menaces qui ont un impact sur la disponibilité et la fiabilité.

Notes

Vos besoins métier peuvent demander davantage de mesures de sécurité. Nous vous recommandons vivement d’étendre les contrôles dans votre implémentation conformément aux instructions fournies dans les conseils stratégiques dans le cadre Well-Architected Framework : Sécurité.

Gestion de l’identité et de l’accès

Au niveau de l’application, cette architecture utilise un schéma d’authentification simple basé sur des clés API pour certaines opérations restreintes, telles que la création d’éléments de catalogue ou la suppression de commentaires. Les scénarios avancés, tels que l’authentification utilisateur et les rôles utilisateur, dépassent l’étendue de l’architecture de base.

Si votre application nécessite l’authentification utilisateur et la gestion des comptes, suivez les principes décrits dans Microsoft Well-Architected Framework. Certaines stratégies incluent l’utilisation de fournisseurs d’identité managée, l’évitement de la gestion des identités personnalisées, l’utilisation de l’authentification sans mot de passe dès que possible, et ainsi de suite.

Accès avec le privilège minimum

Configurez des stratégies d’accès afin que les utilisateurs et les applications obtiennent le niveau minimal d’accès nécessaire pour remplir leur fonction. Les développeurs n’ont généralement pas besoin d’accéder à l’infrastructure de production, mais le pipeline de déploiement a besoin d’un accès complet. Les clusters Kubernetes n’envoient pas d’images conteneur dans un registre, mais les workflows GitHub peuvent le faire. Les API front-end n’obtiennent généralement pas de messages du répartiteur de messages et les workers back-end n’envoient pas nécessairement de nouveaux messages au répartiteur. Ces décisions dépendent de la charge de travail et la fonctionnalité de chaque composant doit être reflétée lors du choix du niveau d’accès qui doit être attribué.

Exemples de l’implémentation de référence stratégique Azure :

  • Chaque composant d’application qui fonctionne avec Event Hubs utilise une chaîne de connexion avec des autorisations Listen (BackgroundProcessor) ou Send (CatalogService). Ce niveau d’accès garantit que chaque pod dispose uniquement du minimum d’accès requis pour remplir sa fonction.
  • Le principal de service pour le pool d’agents AKS ne dispose que des autorisations Get et List pour les secrets dans Key Vault, et pas plus.
  • L’identité AKS Kubelet ne dispose que de l’autorisation AcrPull pour accéder au registre global de conteneurs.

Identités managées

Pour améliorer la sécurité d’une charge de travail stratégique, dans la mesure du possible, évitez d’utiliser des secrets basés sur le service, tels que des chaînes de connexion ou des clés API. L’utilisation d’identités managées est préférable si le service Azure prend en charge cette fonctionnalité.

L’implémentation de référence utilise l’identité managée attribuée par le service du pool d’agents AKS (« identité Kubelet ») pour accéder à Azure Container Registry global et à l’Azure Key Vault de l’empreinte. Des rôles intégrés appropriés sont utilisés pour restreindre l’accès. Par exemple, ce code Terraform attribue uniquement le rôle AcrPull à l’identité Kubelet :

resource "azurerm_role_assignment" "acrpull_role" {
  scope                = data.azurerm_container_registry.global.id
  role_definition_name = "AcrPull"
  principal_id         = azurerm_kubernetes_cluster.stamp.kubelet_identity.0.object_id
}

Secrets

Chaque empreinte de déploiement a son instance d’Azure Key Vault dédiée. Jusqu’à ce que la fonctionnalité Microsoft Entra Workload ID soit disponible, certaines parties de la charge de travail utilisent des clés pour accéder à des ressources Azure comme Azure Cosmos DB. Ces clés sont créées automatiquement pendant le déploiement et sont stockées dans Key Vault avec Terraform. Aucun opérateur humain ne peut interagir avec les secrets, à l’exception des développeurs dans les environnements e2e. De plus, les stratégies d’accès Key Vault sont configurées de manière à ce qu’aucun compte d’utilisateur ne soit autorisé à accéder aux secrets.

Notes

Cette charge de travail n’utilise pas de certificats personnalisés, mais les mêmes principes s’appliquent.

Sur le cluster AKS, le fournisseur Azure Key Vault pour Secrets Store est utilisé pour que l’application consomme des secrets. Le pilote CSI charge les clés à partir d’Azure Key Vault et les monte dans des pods individuels en tant que fichiers.

#
# /src/config/csi-secrets-driver/chart/csi-secrets-driver-config/templates/csi-secrets-driver.yaml
#
apiVersion: secrets-store.csi.x-k8s.io/v1
kind: SecretProviderClass
metadata:
  name: azure-kv
spec:
  provider: azure
  parameters:
    usePodIdentity: "false"
    useVMManagedIdentity: "true"
    userAssignedIdentityID: {{ .Values.azure.managedIdentityClientId | quote }}
    keyvaultName: {{ .Values.azure.keyVaultName | quote }}
    tenantId: {{ .Values.azure.tenantId | quote }}
    objects: |
      array:
        {{- range .Values.kvSecrets }}
        - |
          objectName: {{ . | quote }}
          objectAlias: {{ . | lower | replace "-" "_" | quote }}
          objectType: secret
        {{- end }}

L’implémentation de référence utilise Helm avec Azure Pipelines pour déployer le pilote CSI contenant tous les noms de clés d’Azure Key Vault. Le pilote est également chargé d’actualiser les secrets montés s’ils changent dans Key Vault.

Côté consommateur, les deux applications .NET utilisent la fonctionnalité intégrée pour lire la configuration à partir de fichiers (AddKeyPerFile) :

//
// /src/app/AlwaysOn.BackgroundProcessor/Program.cs
// + using Microsoft.Extensions.Configuration;
//
public static IHostBuilder CreateHostBuilder(string[] args) =>
    Host.CreateDefaultBuilder(args)
    .ConfigureAppConfiguration((context, config) =>
    {
        // Load values from k8s CSI Key Vault driver mount point.
        config.AddKeyPerFile(directoryPath: "/mnt/secrets-store/", optional: true, reloadOnChange: true);
        
        // More configuration if needed...
    })
    .ConfigureWebHostDefaults(webBuilder =>
    {
        webBuilder.UseStartup<Startup>();
    });

La combinaison du rechargement automatique du pilote CSI et de reloadOnChange: true garantit que de nouvelles valeurs sont montées sur le cluster quand les clés changent dans Key Vault. Cela ne garantit pas la rotation des secrets dans l’application. L’implémentation utilise une instance de client Azure Cosmos DB singleton qui nécessite le redémarrage du pod pour appliquer le changement.

Domaines personnalisés et TLS

La charge de travail basée sur le web doit utiliser HTTPS pour empêcher les attaques de l’intercepteur sur tous les niveaux d’interaction. Par exemple, la communication entre le client et l’API, de l’API vers l’API. La rotation des certificats doit être automatisée, car les certificats expirés sont toujours une cause courante de pannes ou d’expériences détériorées.

L’implémentation de référence prend entièrement en charge HTTPS avec des noms de domaine personnalisés, par exemple contoso.com, et applique la configuration appropriée aux environnements int et prod. Pour les environnements e2e, des domaines personnalisés peuvent également être ajoutés, mais il a été décidé de ne pas utiliser de noms de domaine personnalisés dans l’implémentation de référence en raison de la nature éphémère de e2e et de l’augmentation du temps de déploiement lors de l’utilisation de domaines personnalisés avec des certificats SSL dans Front Door.

Pour activer l’automatisation complète du déploiement, le domaine personnalisé doit être managé via une zone Azure DNS. Le pipeline de déploiement d’infrastructure crée de façon dynamique des enregistrements CNAME dans la zone Azure DNS et mappe automatiquement ces enregistrements vers une instance Azure Front Door.

Les certificats SSL gérés par Front Door sont activés, ce qui supprime la nécessité de renouvellements manuels de certificats SSL. TLS 1.2 est configuré comme version minimale.

#
# /src/infra/workload/globalresources/frontdoor.tf
#
resource "azurerm_frontdoor_custom_https_configuration" "custom_domain_https" {
  count                             = var.custom_fqdn != "" ? 1 : 0
  frontend_endpoint_id              = "${azurerm_frontdoor.main.id}/frontendEndpoints/${local.frontdoor_custom_frontend_name}"
  custom_https_provisioning_enabled = true

  custom_https_configuration {
    certificate_source = "FrontDoor"
  }
}

Les environnements qui ne sont pas approvisionnés avec des domaines personnalisés sont accessibles via le point de terminaison Front Door par défaut, par exemple env123.azurefd.net.

Notes

Sur le contrôleur d’entrée de cluster, les domaines personnalisés ne sont pas utilisés dans les deux cas. Au lieu de cela, un nom DNS fourni par Azure, tel que [prefix]-cluster.[region].cloudapp.azure.com, est utilisé avec Let’s Encrypt activé pour émettre des certificats SSL gratuits pour ces points de terminaison.

L’implémentation de référence utilise cert-manager de Jetstack pour approvisionner automatiquement des certificats SSL/TLS (à partir de Let’s Encrypt) pour les règles d’entrée. D’autres paramètres de configuration tels que ClusterIssuer (utilisés pour demander des certificats à partir de Let’s Encrypt) sont déployés via un graphique helm cert-manager-config distinct stocké dans src/config/cert-manager/chart.

Cette implémentation utilise ClusterIssuer au lieu de Issuer comme documenté ici et ici pour éviter d’avoir des émetteurs par espace de noms.

apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
  name: letsencrypt-staging
spec:
  acme:

Configuration

Toute la configuration de l’exécution de l’application est stockée dans Azure Key Vault, notamment les secrets et les paramètres non sensibles. Un magasin de configuration, tel qu’Azure App Configuration, peut être utilisé pour stocker les paramètres, cependant, avoir un seul magasin réduit le nombre de points de défaillance potentiels pour les applications stratégiques. L’utilisation de Key Vault pour la configuration de l’exécution simplifie l’implémentation globale.

Key Vault doit être renseigné par le pipeline de déploiement. Dans l’implémentation, les valeurs requises proviennent directement de Terraform (comme les chaînes de connexion de base de données) ou sont transmises en tant que variables Terraform à partir du pipeline de déploiement.

La configuration de l’infrastructure et du déploiement d’environnements individuels (e2e, int, prod) est stockée dans des fichiers de variables qui font partie du dépôt de code source. Cette approche présente deux avantages principaux :

  • Toutes les modifications d’un environnement sont suivies et passent par des pipelines de déploiement avant d’être appliquées à l’environnement.
  • Les environnements e2e individuels peuvent être configurés différemment, car le déploiement est basé sur le code d’une branche.

Une exception est le stockage de valeurs sensibles pour des pipelines. Ces valeurs sont stockées en tant que secrets dans des groupes de variables Azure DevOps.

Sécurité du conteneur

La sécurisation des images conteneur est nécessaire pour toutes les charges de travail conteneurisées.

Les conteneurs Docker de charge de travail utilisés dans l’implémentation de référence sont basés sur des images d’exécution, et non sur le SDK, pour réduire l’encombrement et la surface d’attaque potentielle. Aucun outil supplémentaire n’est installé (par exempleping, wget ou curl).

L’application s’exécute sous un utilisateur non privilégié workload, créé dans le cadre du processus de génération d’image :

RUN groupadd -r workload && useradd --no-log-init -r -g workload workload
USER workload

L’implémentation de référence utilise Helm pour empaqueter les manifestes YAML nécessaires pour déployer des composants individuels ensemble, y compris leur déploiement Kubernetes, leurs services, leur configuration de mise à l’échelle automatique (HPA) et le contexte de sécurité. Tous les graphiques Helm contiennent des mesures de sécurité fondamentales qui suivent les meilleures pratiques Kubernetes.

Ces mesures de sécurité sont les suivantes :

  • readOnlyFilesystem : le système de fichiers racine / de chaque conteneur est défini en lecture seule pour empêcher l’écriture du conteneur dans le système de fichiers hôte. Cette restriction empêche les attaquants de télécharger plus d’outils et d’assurer la persistance du code dans le conteneur. Les répertoires qui nécessitent un accès en lecture-écriture sont montés en tant que volumes.
  • privileged : tous les conteneurs sont définis pour s’exécuter en tant que non privilégiés. L’exécution d’un conteneur en tant que privilégié fournit toutes les fonctionnalités au conteneur et lève également toutes les limitations appliquées par le contrôleur du groupe de contrôle de l’appareil.
  • allowPrivilegeEscalation : empêche l’intérieur d’un conteneur d’obtenir plus de privilèges que son processus parent.

Ces mesures de sécurité sont également configurées pour les conteneurs tiers et les graphiques Helm (autrement dit, cert-manager) lorsque cela est possible et audité par Azure Policy.

#
# Example:
# /src/app/charts/backgroundprocessor/values.yaml
#
containerSecurityContext:
  privileged: false
  readOnlyRootFilesystem: true
  allowPrivilegeEscalation: false

Chaque environnement (prod, int, chaque e2e) a une instance dédiée d’Azure Container Registry, avec une réplication globale vers chacune des régions où les empreintes sont déployées.

Notes

L’implémentation de référence actuelle n’utilise pas l’analyse des vulnérabilités des images Docker. Il est recommandé d’utiliser Microsoft Defender pour les registres de conteneurs, potentiellement avec GitHub Actions.

Entrée du trafic

Azure Front Door est l’équilibreur de charge global dans cette architecture. Toutes les requêtes web sont acheminées via Front Door, qui sélectionne le back-end approprié. L’application stratégique doit tirer parti d’autres fonctionnalités de Front Door, telles que Web Application Firewall (WAF).

Pare-feu d’applications web

Le service Web Application Firewall (WAF) est une fonctionnalité importante qui permet à Front Door d’inspecter le trafic en transit. En mode Prévention, toutes les demandes suspectes sont bloquées. Dans l’implémentation, deux ensembles de règles sont configurés : Microsoft_DefaultRuleSet et Microsoft_BotManagerRuleSet.

Conseil

Lors du déploiement de Front Door avec WAF, il est recommandé de commencer par le mode Détection, de surveiller étroitement son comportement avec le trafic naturel de l’utilisateur final et d’ajuster les règles de détection. Une fois les faux positifs éliminés ou rares, basculez vers le mode Prévention. Cela est nécessaire, car chaque application est différente et certaines charges utiles peuvent être considérées comme malveillantes, tout en étant complètement légitimes pour cette charge de travail particulière.

Routage

Seules les requêtes qui transitent par Azure Front Door seront acheminées vers les conteneurs d’API (CatalogService et HealthService). Ce comportement est appliqué à l’aide d’une configuration d’entrée Nginx, qui vérifie l’en-tête X-Azure-FDID, non seulement la présence de celui-ci, mais aussi s’il s’agit de l’instance Front Door globale d’un environnement particulier.

#
# /src/app/charts/catalogservice/templates/ingress.yaml
#
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  # ...
  annotations:
    # To restrict traffic coming only through our Front Door instance, we use a header check on the X-Azure-FDID
    # The value gets injected by the pipeline. Hence, this ID should be treated as a sensitive value
    nginx.ingress.kubernetes.io/modsecurity-snippet: |
      SecRuleEngine On
      SecRule &REQUEST_HEADERS:X-Azure-FDID \"@eq 0\"  \"log,deny,id:106,status:403,msg:\'Front Door ID not present\'\"
      SecRule REQUEST_HEADERS:X-Azure-FDID \"@rx ^(?!{{ .Values.azure.frontdoorid }}).*$\"  \"log,deny,id:107,status:403,msg:\'Wrong Front Door ID\'\"
  # ...

Les pipelines de déploiement garantissent que cet en-tête est correctement renseigné, mais il est également nécessaire de contourner cette restriction pour les tests de détection de fumée, car ils sondent directement chaque cluster, sans passer par Front Door. L’implémentation de référence utilise le fait que les tests de détection de fumée sont déclenchés dans le cadre du déploiement, et par conséquent, la valeur d’en-tête est connue et peut être ajoutée aux requêtes HTTP de test de détection de fumée :

#
# /.ado/pipelines/scripts/Run-SmokeTests.ps1
#
$header = @{
  "X-Azure-FDID" = "$frontdoorHeaderId"
  "TEST-DATA"  = "true" # Header to indicate that posted comments and rating are just for test and can be deleted again by the app
}

Déploiements sécurisés

En suivant les principes Well-architected de base pour l’excellence opérationnelle, tous les déploiements doivent être entièrement automatisés et il ne doit pas y avoir d’étapes manuelles requises, à l’exception du déclenchement de l’exécution ou de l’approbation d’une porte.

Les tentatives malveillantes ou la mauvaise configuration accidentelle qui peuvent désactiver les mesures de sécurité doivent être évitées. L’implémentation de référence utilise le même pipeline pour le déploiement d’infrastructure et d’application, ce qui force une restauration automatisée de toute dérive de configuration potentielle, en conservant l’intégrité de l’infrastructure et l’alignement avec le code de l’application. Toute modification est ignorée lors du déploiement suivant.

Les valeurs sensibles pour le déploiement sont générées pendant l’exécution du pipeline (par Terraform) ou fournies en tant que secrets Azure DevOps. Ces valeurs sont protégées avec des restrictions d’accès en fonction du rôle.

Notes

Les flux de travail GitHub offrent un concept similaire de magasin distinct pour les valeurs de secret. Les secrets sont des variables environnementales chiffrées qui peuvent être utilisées par GitHub Actions.

Il est important de prêter attention aux artefacts produits par le pipeline, car ceux-ci peuvent contenir des valeurs de secret ou des informations sur les fonctionnements internes de l’application. Le déploiement Azure DevOps de l’implémentation de référence génère deux fichiers avec des sorties Terraform : une pour l’empreinte et une pour l’infrastructure globale. Ces fichiers ne contiennent pas de mots de passe, ce qui permettrait une compromission directe de l’infrastructure. Toutefois, ils peuvent être considérés comme semi-sensibles, car ils révèlent des informations sur l’infrastructure : ID de cluster, adresses IP, noms de compte de stockage, noms Key Vault, nom de base de données Azure Cosmos DB, ID d’en-tête Front Door, etc.

Pour les charges de travail qui utilisent Terraform, un effort supplémentaire doit être fait pour protéger le fichier d’état, car il contient un contexte de déploiement complet, y compris des secrets. Le fichier d’état est généralement stocké dans un compte de stockage qui doit avoir un cycle de vie distinct de la charge de travail et doit être accessible uniquement à partir d’un pipeline de déploiement. Tout autre accès à ce fichier doit être journalisé et les alertes envoyées au groupe de sécurité approprié.

Mises à jour des dépendances

Les bibliothèques, infrastructures et outils utilisés par l’application sont mis à jour au fil du temps et il est important de suivre ces mises à jour régulièrement, car elles contiennent souvent des correctifs de sécurité, ce qui pourrait permettre aux attaquants d’avoir un accès non autorisé au système.

L’implémentation de référence utilise les mises à jour de dépendance Dependabot pour NuGet, Docker, npm, Terraform et GitHub Actions de GitHub. Le fichier config dependabot.yml est généré automatiquement avec un script PowerShell, en raison de la complexité des différentes parties de l’application (par exemple, chaque module Terraform a besoin d’une entrée distincte).

#
# /.github/dependabot.yml
#
version: 2
updates:
- package-ecosystem: "nuget"
  directory: "/src/app/AlwaysOn.HealthService"
  schedule:
    interval: "monthly" 
  target-branch: "component-updates" 

- package-ecosystem: "docker"
  directory: "/src/app/AlwaysOn.HealthService"
  schedule:
    interval: "monthly" 
  target-branch: "component-updates" 

# ... the rest of the file...
  • Des mises à jour sont déclenchées mensuellement comme un compromis entre le fait d'avoir les bibliothèques les plus à jour et de garder la surcharge maintenable. De plus, les outils clés (Terraform) sont surveillés en continu et les mises à jour importantes sont exécutées manuellement.
  • Des demandes de tirage ciblent la branche component-updates, au lieu de main.
  • Des bibliothèques npm sont configurées pour ne vérifier que les dépendances qui vont vers l’application compilée, et non vers les outils de prise en charge comme @vue-cli.

Dependabot crée une demande de tirage (PR) distincte pour chaque mise à jour, ce qui peut être écrasant pour l’équipe en charge des opérations. L’implémentation de référence collecte d’abord un lot de mises à jour dans la branche component-updates, puis exécute des tests dans l’environnement e2e et, si elle réussit, une autre demande de tirage est créée dans la branche main.

Codage défensif

Les appels d’API peuvent échouer pour diverses raisons, erreurs de code, déploiements défectueux, défaillances d’infrastructure ou autres. Dans ce cas, l’appelant (application cliente) ne doit pas recevoir d’informations de débogage étendues, car cela peut fournir aux adversaires des points de données utiles sur l’application.

L’implémentation de référence illustre ce principe en retournant uniquement l’ID de corrélation dans la réponse ayant échoué et ne partage pas la raison de l’échec (comme le message d’exception ou le rapport des appels de procédure). En utilisant cet ID (et avec l’aide de l’en-tête Server-Location), un opérateur est en mesure d’examiner l’incident à l’aide d’Application Insights

//
// Example ASP.NET Core middleware which adds the Correlation ID to every API response.
//
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
   // ...

    app.Use(async (context, next) =>
    {
        context.Response.OnStarting(o =>
        {
            if (o is HttpContext ctx)
            {
                context.Response.Headers.Add("Server-Name", Environment.MachineName);
                context.Response.Headers.Add("Server-Location", sysConfig.AzureRegion);
                context.Response.Headers.Add("Correlation-ID", Activity.Current?.RootId);
                context.Response.Headers.Add("Requested-Api-Version", ctx.GetRequestedApiVersion()?.ToString());
            }
            return Task.CompletedTask;
        }, context);
        await next();
    });
    
    // ...
}

Suivant

Déployez l’implémentation de référence pour comprendre pleinement les ressources, ainsi que leur configuration.