AWS App Mesh - Partie 1 : Terminaison TLS et chiffrement bout en bout

Comment utiliser App Mesh pour chiffrer le traffic entre le load balancer et les services

Werner Vogels, CTO de Amazon

En Septembre 2024, AWS a annoncé la fin de vie de App Mesh et propose de migrer vers ECS Service Connect ou VPC Lattice.

J’avais initialement prévu de faire une deuxième partie de cet article, mais elle ne verra finalement pas le jour. Pour de nouveaux projets, je vous recommande donc de vous orienter ces autres services.

Table des matières

Contexte

Traditionnellement, lorsque nous déployons nos applications Web, nous avons un load balancer (ou un reverse proxy) qui fait la terminaison TLS avec un certificat associé à notre domaine: https://mon-application.com. Le traffic est ainsi chiffré entre les utilisateurs et notre application. Derrière ce load balancer, nos services reçoivent les requêtes via une communication qui n’est généralement pas chiffrée: http://x.x.x.x:8080.

Schéma montrant une terminaison TLS effectuée au niveau du load balancer
Schéma montrant une terminaison TLS effectuée au niveau du load balancer

Maintenant, pour des raisons réglementaires, nous pourrions avoir besoin de chiffrer cette communication entre le load balancer vers les services pour empêcher des tiers indésirables d’intercepter nos données en transit. Dans ce cas, en plus de la terminaison TLS se faisant toujours au niveau du load balancer, nos services effectueront également une terminaison TLS permettant donc d’avoir du “chiffrement de bout en bout.”

Chiffrement de bout en bout
Chiffrement de bout en bout

Une terminaison SSL reste toujours nécessaire au niveau du load balancer applicatif qui a besoin de lire le contenu des requêtes pour prendre une décision de routage ou pour bloquer des requêtes en fonction de règles.

À noter cependant qu’en restant complètement dans le réseau AWS, il n’y a pas de risque d’attaque man-in-the-middle sachant que le traffic entre l’ALB et les targets est authentifié au niveau des paquets. Si cela est votre cas, à moins d’avoir de fortes raisons, il n’y a pas forcément besoin de faire une nouvelle terminaison TLS. Vous pouvez néanmoins continuer à lire si vous êtes curieux concernant la mise en place de ce type d’architecture avec sur AWS :-)

Plusieurs solutions s’offrent à nous pour mettre en place notre terminaison SSL dans un environment conteneurisé :

  • L’utilisation d’un sidecar consistant à un ajouter un conteneur supplémentaire et à déléguer la terminaison SSL à celui-ci plutôt qu’à nos services qui pourront se concentrer sur le besoin métier. Cette approche à l’avantage de ne pas modifier le code applicatif et est assez pratique dans un environnement technologique hétérogène. En contrepartie, nous consommerons des resources supplémentaires et paierons le coût de ce “proxy” dans le traitement de nos requêtes.
  • L’implémentation directement l’application. Cette approche est dépendante des frameworks utilisés et peut présenter un coût plus important si le parc applicatif comprend des applications hétérogènes. Pour chaque framework utilisé, il faudra implémenter les mécanismes de chiffrement et renouvellement de certificats. Elle a l’avantage de nécessiter moins de resources serveur et évite l’ajout d’un nouveau composant qu’il faudra également monitorer.

Vous l’aurez sans doute compris, dans cet article nous parlerons de l’approche sidecar avec AWS App Mesh et un service ECS derrière un load balancer. Le code source complet en Terraform de cette première partie se trouve dans ce projet GitHub.

Comment ça marche?

Voici le schéma de l’application que nous allons déployer et détailler par la suite:

Architecture technique
Architecture technique

AWS App Mesh pour la terminaison TLS et bien plus

Le composant principal de notre infrastructure est AWS App Mesh, l’offre de service mesh d’AWS. Un service mesh (maillage de services en français) est couche d’infrastructure permettant de gérer les communications entre différents services. Au lieu que les services communiquent directement entre eux, ils passent par cette couche d’infrastructure de façon généralement transparente.

Cette communication entre services se fait via un proxy qui “intercepte” les appels réseaux entre services. Dans l’exemple suivant, le traffic vers le port 5000 de notre application est redirigé vers le proxy qui écoute sur le port 15000 (ProxyIngressPort).

Fonctionnement du sidecar qui intercepte le traffic entrant en utilisant des règles iptables
Fonctionnement du sidecar qui intercepte le traffic entrant en utilisant des règles iptables

App Mesh utilise Envoy Proxy pour cela et fournit des APIs pour contrôler Envoy sans pour autant exposer toutes les fonctionnalités offertes par celui-ci : les concepts d’App Mesh sont traduits en configuration pour Envoy. Vous n’avez cependant pas besoin de connaître de Envoy pour utiliser App Mesh.

Un service mesh offre tout un tas d’avantages parmi lesquels la gestion de la sécurité (mTLS par example), gestion du traffic (routage, auto-retry, circuit breaker, tracing, déploiement canary, rate limiting). Dans cette première partie, nous allons utiliser une petite partie des fonctionnalités d’App Mesh à savoir un nœud virtual (virtual node) pour faire du chiffrement TLS.

Un nœud virtuel est la représentation de notre service ECS dans App Mesh. Lors de la création de celui-ci (le nôtre sera appelé vn-alpla), nous devons spécifier entre autres:

  • Une méthode de découverte de service (service discovery) pour que le proxy puisse communiquer avec notre service. Nous utiliserons l’intégration native avec AWS Cloud Map dans lequel notre service ECS va s’enregistrer dans un namespace dédié demo-app-mesh.local. Le service sera visible sous le host alpha.demo-app-mesh.local dans le VPC. Concrètement, le proxy se basera sur les enregistrements DNS gérés par CloudMap pour communiquer avec notre conteneur.
  • Un listener précisant le protocole exposé et le port d’écoute du service : respectivement http et 5000. La terminaison TLS se configure à ce niveau ainsi que le certificat TLS à utiliser (voir plus bas).

Intégrer un service ECS existant dans App Mesh, revient donc à rajouter un nouveau conteneur à la task definition et à le connecter au mesh. Cette opération ne nécessite pas de récréation du service dans ECS et peut ainsi se faire sans interruption en rajoutant le proxy en mode désactivé ou permissif dans un premier temps.

Côté Terraform, sera revient à rajouter le block proxy_configuration {} et un nouveau conteneur dans le container_definitions de la ressource aws_ecs_task_definition dont voici un exemple simplifié :

resource "aws_ecs_task_definition" "task_definitions" {
  # ....

  proxy_configuration {
    type           = "APPMESH"
    container_name = local.envoy_container_name
    properties = {
      AppPorts         = "5000"
      EgressIgnoredIPs = "169.254.170.2,169.254.169.254" # Addresses IP pour récupérer les méta données de la tâche/instance
      IgnoredUID       = local.envoy_side_car_uuid
      ProxyEgressPort  = 15001 # Ce port est utilisé pour intercepter le traffic sortant.  
      ProxyIngressPort = 15000
    }
  }

  container_definitions = jsonencode([
    { # 1. Conteneur applicatif
      # ...
      dependsOn : [ # Le proxy doit être healthy avant qu'on puisse démarrer le conteneur applicatif
        {
          "containerName" : local.envoy_container_name,
          "condition" : "HEALTHY"
        }
      ]
    },
    { # 2. Sidecar
      name : local.envoy_container_name,
      image : "public.ecr.aws/appmesh/aws-appmesh-envoy:v1.24.1.0-prod",
      user : local.envoy_side_car_uuid,
      essential : true,
      healthCheck : {
        "retries" : 3,
        "command" : [
          "CMD-SHELL", "curl -s http://localhost:${local.envoy_admin_port}/server_info | grep state | grep -q LIVE"
        ],
        # ...
      },
      logConfiguration : {
        # ...
      }
      environment : [
        {
          "name" : "APPMESH_RESOURCE_ARN",
          "value" : aws_appmesh_virtual_node.node.arn # ARN du noeud virtual associé au service
        },
        {
          "name" : "ENVOY_LOG_LEVEL",
          "value" : "info"
        }
      ]
    }
  ])
}

Le rôle IAM de la tâche doit également avoir les permissions pour accéder au nœud virtuel ainsi que le certificat ACM. Voir les fichiers modules/ecs-meshed-service/iam.tf et modules/ecs-meshed-service/data.tf.

AWS ACM comme autorité de certification

Pour faire nos terminaisons SSL, nous aurons besoin de deux certificats:

  • Un certificat pour notre load balancer publique utilisant l’autorité de certification publique d’Amazon. Ces certificats sont gratuits mais ne sont pas exportables en dehors de l’écosystème AWS et ne peuvent pas être utilisés par App Mesh. Il sera associé au domaine demo-app-mesh.${votre_zone_dns}.
  • Pour notre seconde terminaison TLS, nous utiliserons une autorité de certification privée via AWS Private Certificate Authority. D’autres sources de certificats sont supportées comme un certificat autosigné chargé à partir du système de fichiers. Plus d’informations ici.

Le certificat crée via cette autorité de certification devra correspondre au host associé au service via CloudMap à savoir alpha.demo-app-mesh.local. Son renouvellement et son intégration avec le proxy sera automatiquement géré par AWS.

Avant de créer une autorité de certification privée sur AWS, il est important d’en connaître le coût : 400$ par mois pour le general-purpose ou 50€ pour le short-lived mode dès la création. Vous avez un période d’essai de 30 jours pour les nouveaux comptes AWS avec la première autorité de certification où vous ne payerez que pour les certificats crées (soit moins d'1$ par mois).

ECS et l’Application Load Balancer

Notre service ECS alpha sera déployé sur Fargate avec un ALB public qui exposera quelques endpoints à des fins de démonstration :

  • GET /: Page HTML avec quelques meta données sur le conteneur applicatif et la tâche ECS
  • GET /meta: Equivalent JSON du précédent endpoint mais avec plus de données sur l’application et le proxy.
  • GET /headers: Renvoie les headers reçues
  • POST /healthcheck: Endpoint de healthcheck

Le target group associé à l’ALB écoute les requêtes sur le port 5000 en HTTPs. Les logs applicatifs sont envoyés dans deux logs groupes: demo-app-mesh-envoy-alpha pour les logs du proxy et demo-app-mesh-alpha pour l’application. L’image Docker du service sera hébergée sur ECR.

Déploiement et tests

Vous trouverez dans ce projet GitHub le code Terraform nécessaire pour déployer l’architecture décrite plus haut dans le dossier part-1. Le projet est découpé en module pour faciliter la réutilisation : un sous module common pour les ressources communes à plusieurs services (ALB, cluster ECS etc…) et autre sous module ecs-meshed-service qui crée un service ECS destiné à lancer dans un mesh.

Pour lancer cette stack, il faut au préalable avoir :

  • Un VPC avec des sous-réseaux publics et privés. On partira du principe qu’ils sont bien configurés : table de routage, nat gateway, internet gateway, communication entre les sous réseaux publics et privés, etc.
  • Une autorité de certification sur AWS PCA. J’ai préféré ne pas l’inclure dans la stack Terraform vu son coût.
  • Une zone publique sur Route53 pour accéder au load balancer en HTTPs avec une URL “user-friendly”.
  • Un utilisateur IAM pour lancer Terraform. Cet utilisateur devra au moins avoir des droits étendus sur les services ECS, IAM, Cloudwatch, Route53 et ACM.
  • Terraform >=1 pour déployer l’infrastructure. Un backend local est utilisé par défaut pour des raisons de simplicité. Dans un environnement de production, il faudra un autre backend tel que S3 avec le versioning activé.
  • Docker pour construire l’image de l’application et la pousser sur ECR et la CLI AWS pour se connecter à ECR

Ensuite, il faut lancer les commandes suivantes :

git clone https://github.com/haidaraM/aws-app-mesh-demo
cd part-1

# Export des variables obligatoires. A remplacer par les bonnes valeurs
export TF_VAR_vpc_id="vpc-xxxxxx"
export TF_VAR_private_subnets='["private-subnet-a", "private-subnet-b", "private-subnet-c"]'
export TF_VAR_public_subnets='["public-subnet-a", "public-subnet-b", "public-subnet-c"]'
export TF_VAR_private_ca_arn='arn:aws:acm-pca:eu-west-1:12345678910:certificate-authority/xxxxxx'
export TF_VAR_r53_zone_name='your-domain.com'

# Lancement de Terraform
terraform init
terraform apply

Si tout s’est bien passé, vous aurez en sortie quelque chose qui ressemble à ça:

alb_endpoint = "https://demo-app-mesh.votre_zone_dns"
ecr_repo_url = "12345678910.dkr.ecr.eu-west-1.amazonaws.com/demo-app-mesh"
logs = {
  "app" = "demo-app-mesh-alpha"
  "proxy" = "demo-app-mesh-envoy-alpha"
}

A ce stade, l’application n’est pas encore disponible, il faut construire l’image et la pousser vers ECS :

# Connexion à ECR
ECR_REPO=$(terraform output -raw ecr_repo_url)
aws ecr get-login-password | docker login --username AWS --password-stdin $ECR_REPO

cd ../app
docker build -t ${ECR_REPO} .
docker push ${ECR_REPO}

Après 2-3mn, l’application devrait être accessible sur https://demo-app-mesh.votre_zone_dns. Si cela ne marche toujours pas, accéder à la console AWS pour vérifier que le service ECS démarre correctement et regarder les logs dans CloudWatch.

Vérification

Pour vérifier que nos requêtes passent bien par le proxy, nous pouvons :

  1. Regarder les headers des réponses renvoyées. Vous devez retrouver les headers sever: envoy et x-envoy-upstream-service-time: xx. Exemple avec /headers:
    Exemple de réponse passée par le proxy
    Exemple de réponse passée par le proxy
  2. Accéder au log group du proxy dans CloudWatch demo-app-mesh-envoy-alpha. En plus des healthchecks, vous verrez également vos requêtes.

Conclusion

Dans cet article, nous avons vu comment déployer un seul service dans un mesh et faire du chiffrement au niveau du service. Dans la seconde partie, nous allons déployer un second service et les faire communiquer via le mesh en rajoutant des “backends” à nos noeuds virtuels.

Pour aller plus loin

Si vous êtes curieux, sachez qu’il est possible d’exporter la configuration d’Envoy en utilisant l’interface d’administration config_dump. Il est important de rappeler que le port d’administration ne doit être JAMAIS être exposé publiquement sachant qu’aucun mécanisme de protection n’est encore implémentée.

Pour exporter la configuration :

  1. Passer la variable Terraform expose_envoy_admin_port = true au niveau de l’appel au module ecs-meshed-service et relancer terraform apply. Ceci va rajouter une nouvelle règle au security group exposant le port d’administration 9901.
  2. Depuis une machine au sein de votre VPC, vous pouvez faire une requête vers http://alpha.demo-app-mesh.local:9901/config_dump pour dumper la configuration d’Envoy en JSON.

Références

Cet article se base sur les sources suivantes :

Vous également trouverez la roadmap de App Mesh sur GitHub.

Mohamed El Mouctar HAIDARA
Mohamed El Mouctar HAIDARA
Senior Platform Engineer
comments powered by Disqus