Utiliser un environnement d’exécution de confiance « on-premise »

Dans un précédent article, nous avons exposé les avantages des conteneurs confidentiels et leur architecture dans le projet « CoCo. » Dans cet article, nous approfondissons notre propos en détaillant le fonctionnement de certains aspects de CoCo et en décrivant notre installation sur notre propre matériel.

Attestation de conteneurs

Les capsules Kubernetes, utilisées comme abstraction pour les charges de travail conteneurisées confidentielles, introduisent plusieurs défis. Leur nature dynamique — création, suppression, mise à jour de conteneurs — et l’influence de l’environnement Kubernetes (variables d’environnement, contrôleurs d’admission, etc.) rendent difficile la garantie que seul le code prévu par l’utilisateur sera exécuté. Par exemple, l’injection de variables malveillantes ou la modification de la spécification d’une capsule avant son lancement peuvent compromettre la confidentialité.

Le projet CoCo propose une solution élégante qui consiste à utiliser un moteur de politiques de sécurité, intégré à l’environnement d’exécution du conteneur dans l’environnement d’exécution de confiance (EEC), qui applique des règles définies par l’utilisateur. Ce moteur peut, par exemple, autoriser uniquement certaines images ou commandes, et rejeter les appels problématiques (comme l’exécution de processus non autorisés). La Figure 1 montre un exemple d’une telle politique.

package agent_policy

# Seules certaines images de conteneurs peuvent être exécutées
default CreateContainerRequest := false
CreateContainerRequest if {
    every storage in input.storages {
    some allowed_image in policy_data.allowed_images
    storage.source == allowed_image
  }
}

# Seules certaines commandes peuvent être exécutées
# via ‘kubectl exec’ dans les images de conteneurs
default ExecProcessRequest := false
ExecProcessRequest if {
  input_command = concat(" ", input.process.Args)
      some allowed_command in policy_data.allowed_commands
      input_command == allowed_command
}

policy_data := {
    "allowed_commands": [
        "ls",
        "cat",
    ],
    "allowed_images": [
        "pause",
        "my-registry.be/,my-app@sha256:5ed86f469bbc40026a0235dd92e2b0b0c7ce54e3b254132e271a9b9e85d5f220
",
    ],
}

Figure 1 – Exemple de politique de sécurité (langage REGO) restreignant les images pouvant être exécutées et les commandes pouvant être invoquées dans l’image. Cette politique est appliquée par un agent inclus dans la machine virtuelle confidentielle.

Quatre composants de la machine virtuelle confidentielle invitée sont systématiquement mesurés pour assurer leur intégrité : le micrologiciel (e.g., OVMF), le noyau du système d’exploitation, la ligne de commande du noyau et le système de fichiers racine (Figure 2). Une entité externe de confiance, généralement appelée Trustee, atteste de l’intégrité de l’invité, renforçant ainsi la chaîne de confiance.

Figure 2 – Composition de la « mesure » calculée par le système SEV du microprocesseur AMD lors de l’attestation. La mesure est la valeur de hachage cryptographique d’une zone de la mémoire chiffrée où se trouve le micrologiciel (e.g., OVMF) dans lequel ont été injectées les valeurs de hachage cryptographique du noyau du système d’exploitation de la machine virtuelle attestée, de la ligne de commande utilisée pour lancer ce noyau et enfin du système de fichier racine.

Cependant, les conteneurs confidentiels nécessitent généralement des données d’initialisation qui ne peuvent pas être intégrées directement dans l’image de la machine virtuelle ou du conteneur applicatif, comme les certificats, les adresses des services d’attestation ou les politiques de sécurité à appliquer. Ces données, bien que non secrètes, doivent être protégées contre toute altération.

Ces données d’initialisation appelées init-data peuvent être spécifiées sous forme de dictionnaire (e.g., fichiers JSON, TOML, YAML), encodé en base64 et passé à la capsule Kubernetes via une annotation Kubernetes (Figure 3). Afin de garantir leur intégrité, leur valeur de hachage cryptographique est fournie par l’agent d’attestation (fonctionnant dans la machine virtuelle confidentielle) en donnée d’entrée pour le calcul de l’attestation (cela peut se faire en utilisant le champ « HostData » de SEV-SNP). Il est alors possible de comparer les données d’initialisation envoyées à la machine hôte pour le lancement du conteneur avec la valeur de hachage reçue au moment de l’attestation, assurant ainsi que toute modification sera détectée lors de l’attestation à distance.

version = "0.1.0"
algorithm = "sha256"

[data]

# Configuration de l’agent d’attestation
"aa.toml" = '''
[token_configs]
[token_configs.kbs]
url = "${KBS_ADDRESS}"
'''

# Configuration du gestionnaire de données secrètes
"cdh.toml" = '''
[kbc]
name = "cc_kbc"
url = "${KBS_ADDRESS}"

[image]
authenticated_registry_credentials_uri = "kbs:///${REGISTRY_AUTH_KBS_PATH}"
image_security_policy_uri = "${SECURITY_POLICY_KBS_URI}"
'''

# Politique de sécurité restreignant l’environnement du conteneur
"policy.rego"= '''
[Voir Figure 1 ci-dessus]
'''

Figure 3 – Exemple de données d’initialisation fournies (sous forme encodée) via une annotation Kubernetes à l’agent invité CoCo dans la machine virtuelle confidentielle.

Gestion de clés

Un service extérieur de médiation de clés, qui peut être connecté à une boîte noire transactionnelle, permet au conteneur d’obtenir dynamiquement des ressources nécessaires à son fonctionnement. Si le client n’est pas déjà en possession d’un témoin de connexion précédemment obtenu du service de médiation de clés, il doit d’abord s’authentifier et le service de médiation de clés lui répond avec un défi auquel il doit répondre (Figure 4).

Le client génère une paire de clés cryptographiques et demande au processeur de lui fournir une attestation en incluant la valeur de hachage de sa clé publique et une valeur aléatoire unique envoyée par le service dans son défi. L’attestation qui lie clé publique du client, valeur aléatoire unique envoyée par le service et mesure de la VM confidentielle contenant le client est signée par le processeur. Le service fait appel à un agent d’attestation qui vérifie l’attestation en vérifiant la signature et en comparant la mesure à une valeur de référence.

Figure 4 – Protocole d’authentification de la machine virtuelle confidentielle auprès du service extérieur « Trustee » composé d’un service de médiation de clés et d’un service d’attestation : afin de pouvoir obtenir une valeur stockée (secret, clé, etc.) par le service de médiation, le client doit d’abord prouver son authenticité via l’attestation. Ce protocole suit le modèle RATS (RFC9334).

Installation et tests

Afin de tester l’environnement CoCo, nous avons choisi d’utiliser un microprocesseur EPYC 9335 de la société AMD. Il met en œuvre la technologie SEV-SNP de chiffrement et de protection de l’intégrité de la mémoire vive. Nous avons construit une machine avec une carte mère prenant en charge ce microprocesseur (Supermicro MBD-H13SSL-NT-O) et 128 Go de mémoire vive. Il a ensuite fallu configurer le BIOS afin que les fonctionnalités souhaitées de sécurité du microprocesseur soient bien activées. Nous avons aussi opté pour la distribution Ubuntu 24.04.3 LTS du système d’exploitation Linux. Avant de pouvoir tester les fonctionnalités de sécurité du processeur, nous avons enfin dû recompiler le noyau du système d’exploitation. L’opération est en fait relativement simple grâce aux scripts fournis par AMD.

Une fois le système configuré, il est alors possible d’y installer la plateforme Docker afin de pouvoir créer des images de conteneurs, l’interface d’exécution de conteneur containerd (incluse dans la distribution de Docker) et le système de gestion Kubernetes. La configuration de ces outils est assez délicate et sensible aux version. Plusieurs scripts permettant de faciliter cette installation sont fournis ici.

Une fois le système installé, il nous a été possible de déployer une application existante dans des conteneurs confidentiels : il suffit en fait de changer le nom de classe d’exécution utilisé par Kubernetes (runtimeClassName) dans le fichier YAML de configuration de Kubernetes pour l’une des classes de CoCo (e.g., kata-qemu-snp). Bien sûr ce changement simple ne suffit pas à bénéficier des fonctionnalités de sécurité de CoCo. Il est nécessaire de modifier le cycle de production afin d’ajouter les étapes suivantes :

  • Chiffrement de l’image du conteneur
  • Signature de l’image du conteneur
  • Mise à disposition des clés de chiffrement et de signature

Une fois l’image du conteneur créée de la manière habituelle, par exemple avec docker build, celle-ci peut être chiffrée avec l’outil skopeo qui prend en charge différents algorithmes : JWE (RFC7516), PGP (RFC4880), et PKCS7 (RFC2315). Cette image chiffrée peut ensuite être signée avec l’outil cosign et enfin chargée sur un registre d’images.

Au moment du lancement du conteneur, les composants CoCo inclus dans la machine virtuelle confidentielle devront pouvoir vérifier la signature et déchiffrer son image. Pour cela, il est nécessaire de mettre à disposition les clé requises. C’est là que le système de médiation de clés intervient. Comme nous l’avons vu précédemment, celui effectue un protocole d’attestation avant de fournir les clés.

Le déploiement des conteneurs confidentiels est transparent vis-à-vis de l’utilisateur de Kubernetes. Une fois l’invocation de la commande habituelle kubectl apply, une machine virtuelle légère Kata est créée. Celle-ci doit récupérer auprès du médiateur de clés, la clé d’accès au registre d’image (si celui-ci n’est pas public), la politique de sécurité à appliquer, la clé de vérification de signature et la clé de déchiffrement de l’image. Ces informations ne sont fournies qu’après l’attestation de la machine virtuelle (voir plus haut). Les agents inclus dans la machine virtuelle peuvent alors appliquer la politique de sécurité, télécharger l’image, vérifier sa signature et la déchiffrer avant de lancer le conteneur applicatif dans la machine virtuelle.

En ce qui concerne la communication de l’application conteneurisée avec des services extérieurs, il convient d’établir des clés de chiffrement mutuellement reconnues. Une première possibilité est que le conteneur confidentiel crée une paire de clé cryptographiques à son lancement et fournisse la valeur de hachage cryptographique de cette clé publique lors de l’attestation. C’est ce qui est utilisé dans le protocole d’authentification présenté dans la Figure 4. Une autre option est de fournir la clé publique d’une autorité de certification dans l’image chiffrée-puis-signée. Le conteneur pourra alors vérifier les certificats signés par cette autorité et accepter des clés de chiffrement. Une troisième option consiste à s’appuyer sur le service de médiation de clés : celui-ci permet au conteneur de récupérer des secrets de manière sécurisée. En fonction de l’option choisie, il conviendra de modifier plus ou moins le code de l’application.

Protection vis-à-vis d’un administrateur

Que peut faire un administrateur de la machine hôte ? A priori, pas grand-chose, à part lancer le conteneur.

En effet, le mécanisme d’attestation l’empêche de substituer ou de simuler les composants de la machine virtuelle utilisée pour le lancement des conteneurs. Le chiffrement de la mémoire allouée à la machine virtuelle le bloque dans l’observation des données traitées dans la machine virtuelle et le conteneur. Le chiffrement et la signature de l’image du conteneur ne lui permettent ni de substituer un autre conteneur, ni de connaître la nature du conteneur. En supposant que l’application soit configurée pour communiquer de manière chiffrée avec les services extérieurs avec lesquelles elle doit interagir, l’administrateur ne peut pas non plus accéder aux données sensibles en observant le trafic réseau, sauf s’il a également un accès privilégié au système de création des clés. Enfin, il ne peut pas non plus interroger le conteneur via la commande kubectl exec car celle-ci peut être restreinte via une politique de sécurité (voir Figure 1).

En revanche, l’administrateur peut lire les journaux applicatifs enregistrés par Kubernetes sur l’hôte. Par conséquent, il est important que le fournisseur de la charge de travail prenne soin que son code ne divulgue pas des informations sensibles dans les messages journalisés de l’application.

Enfin, comme nous l’avons rappelé dans l’article précédent, les environnements d’exécution de confiance ne sont pas parfaits et leur modèle de sécurité ne tient généralement pas compte des attaques physiques. Dans un environnement comme le G-Cloud, leur ajout offre de nombreuses possibilités. En revanche, dans un environnement où ni SMALS, ni ses clients, ni même l’État belge n’ont le moindre contrôle technique ou juridique sur l’infrastructure, il existe des risques importants qu’il convient d’évaluer sérieusement.

Conclusion

À travers cet article et le précédent, nous avons mis en avant les avantages réels en termes de sécurité que pourraient apporter des microprocesseurs permettant de créer des environnements d’exécution de confiance au sein d’une infrastructure informatique. En particulier, leur utilisation « on-premise » permet de mieux protéger des applications conteneurisées d’administrateurs malveillants ou d’intrus et donc d’offrir des garanties encore plus fortes à nos clients.

Plus simples d’utilisation que les méthodes cryptographiques avancées, de tels systèmes pourraient aussi nous permettre de résoudre des problèmes plus génériques que la cryptographie ou des problèmes que nous ne pouvions pas résoudre jusqu’à présent.

_________________________

Ce post est une contribution individuelle de Fabien A. P. Petitcolas, spécialisé en sécurité informatique chez Smals Research. Cet article est écrit en son nom propre et n’impacte en rien le point de vue de Smals.


More posts