Sécuriser un écosystème WebSocket sur AWS

le 07/02/2020 par Malek Zemni
Tags: Software Engineering, Back, SRE

Les API exposent un ensemble de services qui vont être utilisés par des applications. Quel que soit le type d’API, il faut prendre en compte la sécurité de celle-ci et contrôler l’accès à ses services.

Dans le cadre du paradigme Serverless, le contrôle d’accès doit idéalement reposer sur des services managés qui sont soit fournis par le cloud provider ou qui lui sont externes.

Dans cet article, nous aborderons les architectures Serverless Réactives à travers la plateforme AWS, en particulier dans le contexte d'une application construite à l'aide des services API Gateway, API WebSocket et Lambda.

Nous explorerons les solutions d’authentification et d’autorisation proposées par le service AWS Cognito et les limitations rencontrées, notamment concernant la gestion fine du contrôle d’accès par utilisateur.

Présentation des API WebSocket

En 2018, la plateforme AWS a intégré une nouvelle API WebSocket pour répondre au besoin d’une communication bidirectionnelle entre une application cliente et un backend Serverless/Lambdas.

Cette API est notamment destinée aux applications réactives où le serveur doit pouvoir pousser des informations de manière autonome, sans intervention de la part du client.

Ceci n’était pas possible dû au fait que les fonctions Serverless sont stateless, c’est à dire qu’elles ne maintiennent pas de contexte entre les exécutions, et donc pas de connexion avec le client.

Comme le montre le schéma suivant, c’est API Gateway WebSocket qui va se charger de gérer et maintenir ouverte les connexions WebSocket avec nos clients. Ceci nous enlève la nécessité de gérer un serveur dédié pour maintenir ces connexions persistantes. AWS nous expose, côté backend, une API HTTP nous permettant de pousser des messages à nos clients, depuis notre application Serverless, via la connexion WebSocket maintenue avec eux.

Application Serverless de chat temps réel, basée sur une API WebSocket de AWSA serverless real-time chat application using WebSocket API on Amazon API Gateway

Ces API, comme toute autre API, vont être exposées pour pouvoir offrir leurs services aux applications. Nous allons donc mettre en place un mécanisme d’authentification et d’autorisation, en utilisant le service AWS Cognito.

AWS Cognito

Cognito est un service managé de AWS qui fournit l’authentification, l’autorisation et la gestion des utilisateurs pour les applications. Il permet de simplifier la mise en place des fonctionnalités d’inscription, de connection et de contrôle d’accès. Il est composé de deux services : User Pools et Identity Pools.

User Pools - Groupe d’utilisateurs

Ce service constitue un répertoire d’utilisateurs sécurisé, scalable (pas de gestion d’infrastructure) et entièrement configurable (vérification par email, MFA, etc.). Il permet l’authentification et la gestion des utilisateurs (stockage d’informations, etc.).

Cognito User Pools est un fournisseur d’identité (Identity Provider) normalisé. Il prend en charge les normes de gestion des identités et des accès telles que Oauth 2.0, SAML 2.0 et OpenID Connect. Par conséquent, les utilisateurs pourront s’authentifier directement via Cognito User Pools ou bien fédérer la connexion à un fournisseur d’identité tiers pris en charge par Cognito.

Authentification avec Cognito User PoolsAuthentification avec Cognito User Pools

Quel que soit le fournisseur d’identité, celui-ci va renvoyer au client un ensemble de tokens (jeton d’accès, jeton d’identité et jeton d’actualisation) lorsque l’authentification est réussie.

Identity Pools - Groupes d’identité

Ce service permet de contrôler l’accès aux ressources et services AWS, en fournissant des permissions temporaires à travers la création d’un rôle et d’une policy pour les utilisateurs. Il prend en charge les utilisateurs authentifiés (via User Pools ou un fournisseur d’identité fédéré) et aussi anonymes.

Utilisation

Nous pouvons utiliser les User Pools et les Identity Pools séparément ou conjointement. Dans le cadre de cet article, nous utiliserons Cognito User Pools directement pour s’authentifier et récupérer un access token. Ce token sera par la suite utilisé pour accéder aux ressources, en particulier à notre API WebSocket.

User Pools, authentification et accès aux ressourcesUser Pools, authentification et accès aux ressources

Authentification via Lambda Authorizer

AWS Cognito a l’avantage de s’intégrer très facilement avec les services managés d’API (API Gateway, AppSync)  dans AWS. La méthode la plus simple consiste à configurer l’API pour qu’elle utilise Cognito User Pools directement comme authorizer. Cependant, cette solution n’est pas compatible avec l’API WebSocket.

La solution consiste donc à créer un Lambda authorizer dans API Gateway, c’est à dire une fonction Lambda qui va valider l’identité fournie. Bien qu’elle nécessite un travail supplémentaire pour mettre en place et maintenir un Lambda authorizer personnalisé, cette solution a l’avantage d'être indépendante du fournisseur d’identité utilisé.

Dans la partie suivante, nous explorerons les Lambda authorizer et le flux d’autorisation dans API Gateway en général. Nous détaillerons certains points particuliers liés aux API WebSocket, qui ne prend pas en charge toutes les fonctionnalités des Lambda authorizer.

Types de Lambda authorizer

Quand un client effectue une requête à une fonction Lambda, API Gateway va appeler le Lambda authorizer, qui récupère l’identité du client et retourne une policy IAM.

Il existe 2 types de Lambda authorizer :

  • Token-based : reçoit l’identité dans un bearer token
  • Request parameter-based : reçoit l’identité dans une combinaison de headers, query string parameters, variables de stage et de contexte

Avec une API WebSocket, on ne peut utiliser qu’un Request parameter-based authorizer, et plus particulièrement le passage par query string.  La raison est qu’on ne peut pas mettre un header dans une requête WebSocket contrairement au HTTP. WebSocket implique en effet un changement de protocole et donc l’abandon des sémantiques HTTP.

Cependant, tant que l’appel à la WebSocket est protégé par du TLS (wss), le secret contenu dans l’URL est chiffré et on ne s’expose pas à une faille de sécurité particulière.

En réalité, les secrets présents dans les URL ont plus de chances de se retrouver dans les logs d’accès, et ceci peut être considéré comme une fuite d’informations confidentielles.

Flux d'autorisation dans API Gateway

L’autorisation dans API Gateway avec un Lambda authorizer passe par 5 étapes :

Flux d'authorisation dans API GatewayFlux d'authorisation dans API Gateway

1. Le client effectue une requête à travers API Gateway en fournissant un bearer token ou des request parameters pour prouver son identité.

Nous avons déjà souligné que la seule source d’identité pour une API WebSocket est le query string. Il est possible, par exemple, d'envoyer un champ token dans le query string qui portera les informations d’identification du client.

2. API Gateway vérifie si un Lambda authorizer est bien configuré pour cette route particulière. Dans ce cas, l’authorizer est appelé.

Ici aussi, un point important concernant les API WebSocket, est qu'il n’est pas possible de définir un authorizer différent pour chaque route, à la manière d’une API REST.

Dans API Gateway, les routes sont les composants d’une API WebSocket qui relient une requête client à une fonction Lambda. On peut les voir comme des événements.

En effet, il n'est possible de configurer qu’un seul authorizer qui sera rattaché à la route $connect. Cette configuration sera appliquée à toute l’API : la route $connect va protéger toutes les autres routes puisqu’il s’agit de la route appelée automatiquement par API Gateway lors de d’une tentative d’ouverture du tunnel WebSocket. Les autres routes pourront valider l’accès automatiquement grâce aux informations portées par l'événement.

Nous constatons ici que la granularité des permissions est très grossière : il est seulement possible d’autoriser ou refuser l’accès à toute l’API.

3. Le Lambda authorizer vérifie l’identité du client.

Le Lambda authorizer joue un rôle d’abstraction de l’identity provider, ainsi si l’on souhaite changer de fournisseur d’identité on aura simplement à agir au niveau de cette fonction. L’authorizer, à partir du token fourni en requête, pourra donc récupérer la clé publique auprès du bon identity provider, et ainsi valider la signature dudit token.

4. Le Lambda authorizer retourne un objet contenant une policy IAM et un identificateur principal. Voici un exemple de policy IAM qui autorise l’accès à toute l’API :

"Statement": [  {     "Action": "execute-api:Invoke",     "Effect": "Allow",     "Resource": "arn:aws:execute-api:region:id-compte:id-api/stage/$connect"  } ]

Ici, la seule ressource contrôlable est la route $connect, qui va autoriser ou refuser l’accès à toute l’API. Ajouter d’autres Statements pour contrôler des routes particulières n’aura aucun effet.

5. API Gateway évalue la policy :

  • Si l’accès est refusé, API Gateway retourne un code HTTP 401 ou 403.
  • Si l’accès est autorisé, API Gateway exécute la requête. API Gateway peut aussi mettre en cache la policy pour que l’authorizer ne soit pas réinvoqué.

Limitations dans la gestion des autorisations

Le service AWS Cognito permet de mettre en place un mécanisme de contrôle d’accès assez rapidement, tout en facilitant la gestion des utilisateurs et de leurs permissions. Comme nous l’avons vu à travers la méthode précédente des Lambda authorizers, le contrôle d’accès ne se passe qu’au niveau global de l’API WebSocket.

Nous allons aborder ici une alternative possible pour la gestion d’authentification via API Gateway WebSocket permettant une gestion plus fine des droits d’accès.

Par définition, l’authentification valide l’identité de la personne alors que l’autorisation contrôle les permissions de cette personne vis à vis des ressources auxquelles elle cherche à accéder.

Il n’est malheureusement pas possible de définir un Lambda authorizer particulier pour chaque route WebSocket (selon la terminologie AWS) ce qui nous aurait permis d’utiliser les User Pools Groups pour rassembler les utilisateurs selon leurs permissions. Cette information sur le groupe aurait ensuite été transférée dans le token d’identité et vérifiée dans le Lambda authorizer.

Une solution alternative proposée par AWS consiste à utiliser la méthode d'autorisation IAM pour l’API en passant par le service Cognito Identity Pools.

Comme décrit dans le schéma suivant, Identity Pool va fournir des informations d’identification (credentials) temporaires à un utilisateur qui s’est déjà authentifié auprès de User Pool.

Accès aux ressources via Identity PoolsAccès aux ressources via Identity Pools

Nous pourrons donc associer une policy IAM à ces credentials temporaires, qui vont contenir toutes les permissions accordées à cet utilisateur temporaire. Ainsi, nous pouvons contrôler de manière précise l’accès aux ressources, en particulier les différentes routes autorisées dans une API WebSocket.

"Statement": [  {     "Effect": "Allow",     "Action": [ "execute-api:Invoke" ],     "Resource": [ "arn:aws:execute-api:region:id-compte:id-api/stage/*" ]  },  {     "Effect": "Deny",     "Action": [ "execute-api:Invoke" ],     "Resource": [ "arn:aws:execute-api:region:id-compte:id-api/stage/secret" ]  }, ]

La méthode de l’autorisation IAM semble plus appropriée que celle du Lambda authorizer, par contre, sa mise en place est assez complexe puisqu'il est nécessaire de signer la requête WebSocket avec les credentials..

Takeaway

API Gateway WebSocket n’est pas encore mature, mais elle bénéficie déjà d’un bon nombre d’intégrations aux services mis à disposition par le Cloud provider. On parvient ainsi à mettre en place des mécanismes d’authentification et d’autorisation, mais pas avec autant de facilité que pour les autres API.

Cela dit, les API WebSocket ne sont pas destinées à prendre en charge toute la logique métier d’une application. Elles se contentent généralement de fournir certaines fonctionnalités réactives visant à améliorer l’expérience utilisateur, pouvant se révéler particulièrement intéressantes dans le cadre d’une architecture Serverless.

En réalité, nos applications seront principalement constituées d’API REST que l’on pourrait sécuriser par les mécanismes déjà matures d’AWS Cognito ainsi que d’un ensemble plus limité d’API WebSocket pour des fonctionnalités bien précises.

Par ailleurs, il est bon de noter que dans la majeure partie des cas, une communication unidirectionnelle du serveur vers les clients, telle que les Server Sent Events, est suffisante pour accomplir la fonctionnalité désirée.

Malheureusement à l’heure actuelle AWS ne propose pas de mécanisme similaire à API Gateway WebSocket permettant de maintenir simplement, et sans infrastructure dédiée, une connexion HTTP SSE avec nos clients web.