Déployer une application sur Cloud Run de façon sécurisée

1. Présentation

Vous allez modifier les étapes par défaut de déploiement d'un service sur Cloud Run pour améliorer la sécurité, puis découvrir comment accéder de manière sécurisée à l'application déployée. L'application est un "service d'enregistrement des partenaires" de l'application Cymbal Eats, qui est utilisée par les entreprises qui collaborent avec Cymbal Eats pour traiter les commandes de repas.

Objectifs de l'atelier

En apportant quelques petites modifications aux étapes minimales par défaut pour déployer une application sur Cloud Run, vous pouvez considérablement renforcer sa sécurité. Vous allez prendre une application et des instructions de déploiement existantes, et modifier les étapes de déploiement pour améliorer la sécurité de l'application déployée.

Vous découvrirez ensuite comment autoriser l'accès à l'application et effectuer des requêtes autorisées.

Il ne s'agit pas d'un examen exhaustif de la sécurité du déploiement des applications, mais plutôt des modifications que vous pouvez apporter à tous vos futurs déploiements d'applications pour améliorer leur sécurité avec très peu d'effort.

2. Préparation

Configuration de l'environnement d'auto-formation

  1. Connectez-vous à la console Google Cloud, puis créez un projet ou réutilisez un projet existant. (Si vous ne possédez pas encore de compte Gmail ou Google Workspace, vous devez en créer un.)

b35bf95b8bf3d5d8.png

a99b7ace416376c4.png

bd84a6d3004737c5.png

  • Le nom du projet est le nom à afficher pour les participants au projet. Il s'agit d'une chaîne de caractères non utilisée par les API Google. Vous pouvez le mettre à jour à tout moment.
  • L'ID du projet est unique parmi tous les projets Google Cloud et non modifiable une fois défini. La console Cloud génère automatiquement une chaîne unique (en général, vous n'y accordez d'importance particulière). Dans la plupart des ateliers de programmation, vous devrez indiquer l'ID de votre projet (généralement identifié par PROJECT_ID). Si l'ID généré ne vous convient pas, vous pouvez en générer un autre de manière aléatoire. Vous pouvez également en spécifier un et voir s'il est disponible. Après cette étape, l'ID n'est plus modifiable et restera donc le même pour toute la durée du projet.
  • Pour information, il existe une troisième valeur (le numéro de projet) que certaines API utilisent. Pour en savoir plus sur ces trois valeurs, consultez la documentation.
  1. Vous devez ensuite activer la facturation dans la console Cloud pour utiliser les ressources/API Cloud. L'exécution de cet atelier de programmation est très peu coûteuse, voire sans frais. Pour désactiver les ressources et éviter ainsi que des frais ne vous soient facturés après ce tutoriel, vous pouvez supprimer le projet ou les ressources que vous avez créées. Les nouveaux utilisateurs de Google Cloud peuvent participer au programme d'essai gratuit pour bénéficier d'un crédit de 300 $.

Activer Cloud Shell

  1. Dans Cloud Console, cliquez sur Activer Cloud Shell 853e55310c205094.png.

55efc1aaa7a4d3ad.png

Si vous n'avez jamais démarré Cloud Shell auparavant, un écran intermédiaire s'affiche en dessous de la ligne de flottaison, décrivant de quoi il s'agit. Si tel est le cas, cliquez sur Continuer. Cet écran ne s'affiche qu'une seule fois. Voici à quoi il ressemble :

9c92662c6a846a5c.png

Le provisionnement et la connexion à Cloud Shell ne devraient pas prendre plus de quelques minutes.

9f0e51b578fecce5.png

Cette machine virtuelle contient tous les outils de développement dont vous avez besoin. Elle comprend un répertoire d'accueil persistant de 5 Go et s'exécute sur Google Cloud, ce qui améliore nettement les performances du réseau et l'authentification. Vous pouvez réaliser une grande partie, voire la totalité, des activités de cet atelier dans un simple navigateur ou sur votre Chromebook.

Une fois connecté à Cloud Shell, vous êtes en principe authentifié et le projet est défini avec votre ID de projet.

  1. Exécutez la commande suivante dans Cloud Shell pour vérifier que vous êtes authentifié :
gcloud auth list

Résultat de la commande

 Credentialed Accounts
ACTIVE  ACCOUNT
*       <my_account>@<my_domain.com>

To set the active account, run:
    $ gcloud config set account `ACCOUNT`
  1. Exécutez la commande suivante dans Cloud Shell pour vérifier que la commande gcloud connaît votre projet:
gcloud config list project

Résultat de la commande

[core]
project = <PROJECT_ID>

Si vous obtenez un résultat différent, exécutez cette commande :

gcloud config set project <PROJECT_ID>

Résultat de la commande

Updated property [core/project].

Configuration de l'environnement

Pour cet atelier, vous allez exécuter des commandes dans la ligne de commande Cloud Shell. En général, vous pouvez copier les commandes et les coller telles quelles, mais dans certains cas, vous devrez remplacer les valeurs d'espace réservé par des valeurs correctes.

  1. Définissez une variable d'environnement sur l'ID de projet pour l'utiliser dans les commandes ultérieures:
export PROJECT_ID=$(gcloud config get-value project)
export REGION=us-central1
export SERVICE_NAME=partner-registration-service
  1. Activez l'API du service Cloud Run qui exécutera votre application, l'API Firestore qui fournira un stockage de données NoSQL, l'API Cloud Build qui sera utilisée par la commande de déploiement et l'Artifact Registry qui servira à contenir le conteneur de l'application lors de la compilation:
gcloud services enable \
  run.googleapis.com \
  firestore.googleapis.com \
  cloudbuild.googleapis.com \
  artifactregistry.googleapis.com
  1. Initialisez la base de données Firestore en mode natif. Cette commande utilise l'API App Engine. Vous devez donc l'activer au préalable.

La commande doit spécifier une région pour App Engine, que nous n'utiliserons pas, mais que nous devons créer pour des raisons historiques, et une région pour la base de données. Nous utiliserons us-central pour App Engine et nam5 pour la base de données. nam5 est l'emplacement multirégional des États-Unis. Les emplacements multirégionaux optimisent la disponibilité et la durabilité de la base de données.

gcloud services enable appengine.googleapis.com

gcloud app create --region=us-central
gcloud firestore databases create --region=nam5
  1. Cloner le dépôt de l'application exemple et accéder au répertoire
git clone https://github.com/GoogleCloudPlatform/cymbal-eats.git

cd cymbal-eats/partner-registration-service

3. Consulter le fichier README

Ouvrez l'éditeur et examinez les fichiers qui composent l'application. Consultez le fichier README.md, qui décrit les étapes nécessaires au déploiement de cette application. Certaines de ces étapes peuvent impliquer des décisions de sécurité implicites ou explicites à prendre en compte. Vous allez modifier certains de ces choix pour améliorer la sécurité de votre application déployée, comme décrit ci-dessous:

Étape 3 : Exécuter npm install

Il est important de connaître la provenance et l'intégrité de tout logiciel tiers utilisé dans une application. La gestion de la sécurité de la chaîne d'approvisionnement logicielle est pertinente pour la création de tout logiciel, et pas seulement pour les applications déployées sur Cloud Run. Cet atelier étant axé sur le déploiement, il ne traite pas de ce sujet. Vous pouvez toutefois vous renseigner sur ce point séparément.

Étapes 4 et 5 : Modifier et exécuter deploy.sh

Ces étapes permettent de déployer l'application sur Cloud Run, en laissant la plupart des options définies par défaut. Vous allez modifier cette étape pour renforcer la sécurité du déploiement de deux manières principales:

  1. N'autorisez pas l'accès non authentifié. Il peut être pratique d'autoriser cela pour tester des éléments lors de l'exploration, mais il s'agit d'un service Web destiné aux partenaires commerciaux et qui doit toujours authentifier ses utilisateurs.
  2. Spécifiez que l'application doit utiliser un compte de service dédié avec uniquement les droits nécessaires, au lieu d'un compte par défaut qui aura probablement plus d'accès aux API et aux ressources que nécessaire. C'est ce qu'on appelle le principe du moindre privilège, un concept fondamental de la sécurité des applications.

Étapes 6 à 11 : Envoyez des exemples de requêtes Web pour vérifier le bon fonctionnement

Étant donné que le déploiement de l'application nécessite désormais une authentification, ces requêtes doivent désormais inclure une preuve de l'identité de l'auteur de la demande. Au lieu de modifier ces fichiers, vous enverrez des requêtes directement depuis la ligne de commande.

4. Déployer le service de manière sécurisée

Deux modifications ont été identifiées comme nécessaires dans le script deploy.sh: ne pas autoriser l'accès non authentifié et utiliser un compte de service dédié avec des privilèges minimaux.

Vous allez d'abord créer un compte de service, puis modifier le script deploy.sh pour y faire référence et interdire l'accès non authentifié.Vous pourrez ensuite déployer le service en exécutant le script modifié avant d'exécuter le script deploy.sh modifié.

Créez un compte de service et accordez-lui l'accès nécessaire à Firestore/Datastore.

gcloud iam service-accounts create partner-sa

gcloud projects add-iam-policy-binding $PROJECT_ID \
  --member="serviceAccount:partner-sa@${PROJECT_ID}.iam.gserviceaccount.com" \
  --role=roles/datastore.user

Modifier deploy.sh

Modifiez le fichier deploy.sh pour interdire l'accès non authentifié(–no-allow-unauthenticated) et spécifier le nouveau compte de service(–service-account) pour l'application déployée. Corrigez GOOGLE_PROJECT_ID pour qu'il corresponde à l'ID de votre propre projet.

Vous allez supprimer les deux premières lignes et modifier trois autres lignes, comme indiqué ci-dessous.

gcloud run deploy $SERVICE_NAME \
  --source . \
  --platform managed \
  --region ${REGION} \
  --no-allow-unauthenticated \
  --project=$PROJECT_ID \
  --service-account=partner-sa@${PROJECT_ID}.iam.gserviceaccount.com

Déployer le service

Depuis la ligne de commande, exécutez le script deploy.sh:

./deploy.sh

Une fois le déploiement terminé, la dernière ligne de la sortie de la commande affiche l'URL du service de la nouvelle application. Enregistrez l'URL dans une variable d'environnement:

export SERVICE_URL=<URL from last line of command output>

Essayez maintenant d'extraire une commande de l'application à l'aide de l'outil curl:

curl -i -X GET $SERVICE_URL/partners

L'indicateur -i de la commande curl lui indique d'inclure les en-têtes de réponse dans la sortie. La première ligne de la sortie doit être la suivante:

HTTP/2 403

L'application a été déployée avec l'option d'interdire les requêtes non authentifiées. Cette commande curl ne contient aucune information d'authentification. Elle est donc refusée par Cloud Run. L'application déployée n'exécute ni ne reçoit aucune donnée de cette requête.

5. Envoyer des requêtes authentifiées

L'application déployée est appelée en effectuant des requêtes Web, qui doivent désormais être authentifiées pour que Cloud Run les autorise. Les requêtes Web sont authentifiées en incluant un en-tête Authorization au format suivant:

Authorization: Bearer identity-token

Le jeton d'identité est une chaîne codée, à durée limitée et signée de manière cryptographique, émise par un fournisseur d'authentification approuvé. Dans ce cas, vous devez fournir un jeton d'identité Google valide et non expiré.

Envoyer une demande en tant que compte utilisateur

L'outil Google Cloud CLI peut fournir un jeton pour l'utilisateur authentifié par défaut. Exécutez cette commande pour obtenir un jeton d'identité pour votre propre compte et l'enregistrer dans la variable d'environnement ID_TOKEN:

export ID_TOKEN=$(gcloud auth print-identity-token)

Par défaut, les jetons d'identité émis par Google sont valides pendant une heure. Exécutez la commande curl suivante pour effectuer la requête qui a été refusée précédemment, car elle n'était pas autorisée. Cette commande inclut l'en-tête nécessaire:

curl -i -X GET $SERVICE_URL/partners \
  -H "Authorization: Bearer $ID_TOKEN"

Le résultat de la commande doit commencer par HTTP/2 200, ce qui indique que la requête est acceptable et qu'elle est traitée. (Si vous attendez une heure et réessayez cette requête, elle échouera, car le jeton aura expiré.) Le corps de la réponse se trouve à la fin de la sortie, après une ligne vide:

{"status":"success","data":[]}

Aucun partenaire n'a encore été ajouté.

Enregistrez les partenaires à l'aide de l'exemple de données JSON dans le répertoire à l'aide de deux commandes curl:

curl -X POST \
  -H "Authorization: Bearer $ID_TOKEN" \
  -H "Content-Type: application/json" \
  -d "@example-partner.json" \
  $SERVICE_URL/partner

et

curl -X POST \
  -H "Authorization: Bearer $ID_TOKEN" \
  -H "Content-Type: application/json" \
  -d "@example-partner2.json" \
  $SERVICE_URL/partner

Répétez la requête GET précédente pour afficher tous les partenaires enregistrés:

curl -i -X GET $SERVICE_URL/partners \
  -H "Authorization: Bearer $ID_TOKEN"

Les données JSON doivent s'afficher avec beaucoup plus de contenu, fournissant des informations sur les deux partenaires enregistrés.

Envoyer une requête en tant que compte non autorisé

La requête authentifiée effectuée à l'étape précédente a réussi non seulement parce qu'elle était authentifiée, mais aussi parce que l'utilisateur authentifié (votre compte) était autorisé. Autrement dit, le compte était autorisé à appeler l'application. Tous les comptes authentifiés ne sont pas autorisés à le faire.

Le compte par défaut utilisé dans la requête précédente a été autorisé, car c'est celui qui a créé le projet contenant l'application. Par défaut, il a donc été autorisé à appeler toutes les applications Cloud Run du compte. Cette autorisation peut être révoquée si nécessaire, ce qui est souhaitable dans une application de production. Au lieu de le faire maintenant, vous allez créer un compte de service sans autorisations ni rôles attribués, et vous l'utiliserez pour essayer d'accéder à l'application déployée.

  1. Créez un compte de service appelé tester.
gcloud iam service-accounts create tester
  1. Vous recevrez un jeton d'identité pour ce nouveau compte de la même manière que vous l'avez fait pour votre compte par défaut précédemment. Toutefois, votre compte par défaut doit être autorisé à usurper l'identité de comptes de service. Accordez cette autorisation à votre compte.
export USER_EMAIL=$(gcloud config list account --format "value(core.account)")

gcloud projects add-iam-policy-binding $PROJECT_ID \
  --member="user:$USER_EMAIL" \
  --role=roles/iam.serviceAccountTokenCreator
  1. Exécutez maintenant la commande suivante pour enregistrer un jeton d'identité pour ce nouveau compte dans la variable d'environnement TEST_IDENTITY. Si un message d'erreur s'affiche, attendez une minute ou deux, puis réessayez.
export TEST_TOKEN=$( \
  gcloud auth print-identity-token \
    --impersonate-service-account \
    "tester@$PROJECT_ID.iam.gserviceaccount.com" \
)
  1. Envoyez la requête Web authentifiée comme précédemment, mais à l'aide de ce jeton d'identité:
curl -i -X GET $SERVICE_URL/partners \
  -H "Authorization: Bearer $TEST_TOKEN"

La sortie de la commande commencera à nouveau par HTTP/2 403, car la requête, bien qu'authentifiée, n'est pas autorisée. Le nouveau compte de service n'est pas autorisé à appeler cette application.

Autoriser un compte

Un utilisateur ou un compte de service doit disposer du rôle "Demandeur Cloud Run" sur un service Cloud Run pour pouvoir lui envoyer des requêtes. Attribuez ce rôle au compte de service testeur à l'aide de la commande suivante:

export REGION=us-central1
gcloud run services add-iam-policy-binding ${SERVICE_NAME} \
  --member="serviceAccount:tester@$PROJECT_ID.iam.gserviceaccount.com" \
  --role=roles/run.invoker \
  --region=${REGION}

Après avoir attendu une ou deux minutes pour que le nouveau rôle soit mis à jour, répétez la requête authentifiée. Enregistrez un nouveau TEST_TOKEN si une heure ou plus s'est écoulée depuis son premier enregistrement.

curl -i -X GET $SERVICE_URL/partners \
  -H "Authorization: Bearer $TEST_TOKEN"

La sortie de la commande commence désormais par HTTP/1.1 200 OK, et la dernière ligne contient la réponse JSON. Cette requête a été acceptée par Cloud Run et traitée par l'application.

6. Authentifier des programmes par rapport à l'authentification des utilisateurs

Les requêtes authentifiées que vous avez effectuées jusqu'à présent ont utilisé l'outil de ligne de commande curl. Vous auriez également pu utiliser d'autres outils et langages de programmation. Toutefois, les requêtes Cloud Run authentifiées ne peuvent pas être effectuées à l'aide d'un navigateur Web avec des pages Web simples. Si un utilisateur clique sur un lien ou sur un bouton pour envoyer un formulaire sur une page Web, le navigateur n'ajoute pas l'en-tête Authorization requis par Cloud Run pour les requêtes authentifiées.

Le mécanisme d'authentification intégré de Cloud Run est destiné aux programmes, et non aux utilisateurs finaux.

Remarque :

Cloud Run peut héberger des applications Web destinées aux utilisateurs, mais ces types d'applications doivent configurer Cloud Run pour autoriser les requêtes non authentifiées des navigateurs Web des utilisateurs. Si les applications nécessitent une authentification utilisateur, elles doivent la gérer au lieu de demander à Cloud Run de le faire. L'application peut le faire de la même manière que les applications Web en dehors de Cloud Run. La manière dont cela se fait n'entre pas dans le cadre de cet atelier de programmation.

Vous avez peut-être remarqué que les réponses aux exemples de requêtes jusqu'à présent étaient des objets JSON, et non des pages Web. En effet, ce service d'enregistrement des partenaires est destiné aux programmes, et le format JSON est pratique pour eux. Vous allez ensuite écrire et exécuter un programme pour consommer et utiliser ces données.

Requêtes authentifiées à partir d'un programme Python

Un programme peut effectuer des requêtes authentifiées d'une application Cloud Run sécurisée via des requêtes Web HTTP standards, mais en incluant un en-tête Authorization. Le seul défi de ces programmes est d'obtenir un jeton d'identité valide et non expiré à placer dans cet en-tête. Ce jeton sera validé par Cloud Run à l'aide de Google Cloud Identity and Access Management (IAM). Il doit donc être émis et signé par une autorité reconnue par IAM. Des bibliothèques clientes sont disponibles dans de nombreux langages que les programmes peuvent utiliser pour demander l'émission d'un tel jeton. La bibliothèque cliente utilisée dans cet exemple est celle de Python google.auth. Il existe plusieurs bibliothèques Python permettant d'effectuer des requêtes Web en général. Cet exemple utilise le module requests populaire.

La première étape consiste à installer les deux bibliothèques clientes:

pip install google-auth
pip install requests

Le code Python permettant de demander un jeton d'identité pour l'utilisateur par défaut est le suivant:

credentials, _ = google.auth.default()
credentials.refresh(google.auth.transport.requests.Request())
identity_token = credentials.id_token

Si vous utilisez un shell de commande tel que Cloud Shell ou le shell de terminal standard sur votre propre ordinateur, l'utilisateur par défaut est celui qui s'est authentifié dans ce shell. Dans Cloud Shell, il s'agit généralement de l'utilisateur connecté à Google. Dans d'autres cas, il s'agit de l'utilisateur authentifié avec gcloud auth login ou une autre commande gcloud. Si l'utilisateur ne s'est jamais connecté, il n'y a pas d'utilisateur par défaut et ce code échoue.

Pour un programme qui envoie des requêtes à un autre programme, vous ne devez généralement pas utiliser l'identité d'une personne, mais plutôt celle du programme à l'origine de la requête. C'est à cela que servent les comptes de service. Vous avez déployé le service Cloud Run avec un compte de service dédié qui fournit l'identité qu'il utilise pour envoyer des requêtes API, par exemple à Cloud Firestore. Lorsqu'un programme s'exécute sur une plate-forme Google Cloud, les bibliothèques clientes utilisent automatiquement le compte de service qui lui est attribué comme identité par défaut. Le même code de programme fonctionne donc dans les deux situations.

Le code Python permettant d'effectuer une requête avec un en-tête d'autorisation ajouté est le suivant:

auth_header = {"Authorization": "Bearer " + identity_token}
response = requests.get(url, headers=auth_header)

Le programme Python complet suivant envoie une requête authentifiée au service Cloud Run pour récupérer tous les partenaires enregistrés, puis imprime leurs noms et leurs ID attribués. Copiez et exécutez la commande ci-dessous pour enregistrer ce code dans le fichier print_partners.py.

cat > ./print_partners.py << EOF
def print_partners():
    import google.auth
    import google.auth.transport.requests
    import requests

    credentials, _ = google.auth.default()
    credentials.refresh(google.auth.transport.requests.Request())
    identity_token = credentials.id_token

    auth_header = {"Authorization": "Bearer " + identity_token}
    response = requests.get("${SERVICE_URL}/partners", headers=auth_header)

    parsed_response = response.json()
    partners = parsed_response["data"]

    for partner in partners:
        print(f"{partner['partnerId']}: {partner['name']}")


print_partners()
EOF

Vous allez exécuter ce programme avec une commande shell. Vous devez d'abord vous authentifier en tant qu'utilisateur par défaut pour que le programme puisse utiliser ces identifiants. Exécutez la commande gcloud auth ci-dessous:

gcloud auth application-default login

Suivez les instructions pour vous connecter. Exécutez ensuite le programme à partir de la ligne de commande:

python print_partners.py

Le résultat ressemble à ceci:

10102: Zippy food delivery
67292: Foodful

La requête du programme a atteint le service Cloud Run, car elle a été authentifiée avec votre identité. Vous êtes le propriétaire de ce projet et êtes donc autorisé à l'exécuter par défaut. Il est plus courant que ce programme s'exécute sous l'identité d'un compte de service. Lorsqu'il est exécuté sur la plupart des produits Google Cloud, tels que Cloud Run ou App Engine, l'identité par défaut est un compte de service et est utilisée à la place d'un compte personnel.

7. Félicitations !

Félicitations, vous avez terminé cet atelier de programmation.

Étapes suivantes :

Découvrez les autres ateliers de programmation Cymbal Eats:

Effectuer un nettoyage

Pour éviter que les ressources utilisées lors de ce tutoriel soient facturées sur votre compte Google Cloud, supprimez le projet contenant les ressources, ou conservez le projet et supprimez chaque ressource individuellement.

Supprimer le projet

Le moyen le plus simple d'empêcher la facturation est de supprimer le projet que vous avez créé pour ce tutoriel.