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

1. Présentation

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

Objectifs de l'atelier

En apportant de légères modifications aux étapes par défaut minimales du déploiement d'une application sur Cloud Run, vous pouvez renforcer considérablement sa sécurité. Vous allez modifier les étapes de déploiement d'une application existante et ses instructions de déploiement afin d'améliorer la sécurité de l'application déployée.

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

Il ne s'agit pas d'un aperçu exhaustif de la sécurité du déploiement d'applications, mais plutôt des modifications que vous pourrez apporter à tous vos futurs déploiements d'applications pour améliorer leur sécurité avec un minimum d'efforts.

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 modifier à 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. généralement, vous ne vous souciez pas de ce que c’est. Dans la plupart des ateliers de programmation, vous devrez référencer l'ID du projet (il est généralement identifié comme PROJECT_ID). Si l'ID généré ne vous convient pas, vous pouvez en générer un autre au hasard. Vous pouvez également essayer la vôtre pour voir si elle est disponible. Il ne peut pas être modifié après cette étape et restera actif pendant toute la durée du projet.
  • Pour votre information, il existe une troisième valeur, le numéro de projet, utilisé par certaines API. 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 arrêter les ressources afin d'éviter que des frais ne vous soient facturés au-delà de ce tutoriel, vous pouvez supprimer les ressources que vous avez créées ou l'ensemble du projet. 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 (en dessous de la ligne de flottaison) vous explique de quoi il s'agit. Dans ce cas, cliquez sur Continuer (elle ne s'affichera plus). 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

Dans cet atelier, vous allez exécuter des commandes dans la ligne de commande Cloud Shell. Vous pouvez généralement copier les commandes et les coller telles quelles, mais dans certains cas, vous devrez corriger les valeurs des espaces réservés.

  1. Définissez une variable d'environnement sur l'ID du projet. Vous l'utiliserez dans des 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 de service Cloud Run qui exécutera votre application, l'API Firestore qui fournira le stockage de données NoSQL, l'API Cloud Build qui sera utilisée par la commande de déploiement et Artifact Registry qui contiendra le conteneur d'application lors de sa création:
gcloud services enable \
  run.googleapis.com \
  firestore.googleapis.com \
  cloudbuild.googleapis.com \
  artifactregistry.googleapis.com
  1. Initialiser la base de données Firestore en mode natif Cette commande utilise l'API App Engine. Vous devez donc d'abord l'activer.

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, ainsi qu'une région pour la base de données. Nous allons utiliser 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 maximisent 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. Clonez le dépôt de l'exemple d'application et accédez au répertoire.
git clone https://github.com/GoogleCloudPlatform/cymbal-eats.git

cd cymbal-eats/partner-registration-service

3. Consultez 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 nécessiter la prise en compte de décisions de sécurité implicites ou explicites. Vous allez modifier certains de ces choix pour améliorer la sécurité de votre application déployée, comme décrit ici:

Étape 3 : Exécutez 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é sur la chaîne d'approvisionnement logicielle est pertinente pour créer des logiciels, et pas seulement des applications déployées dans Cloud Run. Cet atelier étant consacré au déploiement, il n'aborde pas ce sujet, mais vous pouvez étudier ce sujet séparément.

Étapes 4 et 5 : Modifiez et exécutez deploy.sh

Cette procédure permet de déployer l'application sur Cloud Run en laissant la plupart des options 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 ce type de test pendant l'exploration, mais il s'agit d'un service Web destiné aux partenaires commerciaux, qui doit toujours authentifier ses utilisateurs.
  2. Spécifiez que l'application doit utiliser un compte de service dédié doté uniquement des 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 que l'on appelle le principe du moindre privilège. Il s'agit d'un concept fondamental de la sécurité des applications.

Étapes 6 à 11 : Créer des exemples de requêtes Web pour vérifier le comportement correct

É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é du demandeur. Au lieu de modifier ces fichiers, vous effectuerez des requêtes directement à partir de 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: n'autorise pas l'accès non authentifié et l'utilisation d'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 référencer ce compte de service et interdire les accès non authentifiés, puis déployer le service en exécutant le script modifié avant que nous puissions exécuter le script deploy.sh modifié.

Créer un compte de service et lui accorder 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 pour spécifier le nouveau compte de service(–service-account) pour l'application déployée. Remplacez GOOGLE_PROJECT_ID par l'ID de votre propre projet.

Vous allez supprimer les deux premières lignes et modifier les trois autres 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

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

./deploy.sh

Une fois le déploiement terminé, la dernière ligne du résultat 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 de récupérer une commande dans l'application à l'aide de l'outil curl:

curl -i -X GET $SERVICE_URL/partners

L'option -i de la commande curl lui indique d'inclure les en-têtes de réponse dans la sortie. La première ligne du résultat doit se présenter comme suit:

HTTP/2 403

L'application a été déployée avec la possibilité d'interdire les requêtes non authentifiées. Cette commande curl ne contient aucune information d'authentification. Cloud Run la refuse donc. L'application déployée ne s'exécute même pas et ne reçoit même pas de données de cette requête.

5. Envoyer des requêtes authentifiées

L'application déployée est appelée via des requêtes Web, qui doivent maintenant ê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 encodée à court terme signée de manière cryptographique, émise par un fournisseur d'authentification de confiance. Dans ce cas, vous devez fournir un jeton d'identité valide et valide émis par Google.

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 afin d'obtenir un jeton d'identité pour votre propre compte et enregistrez-le 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 créer la requête qui a été rejeté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 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 pour l'instant.

Enregistrez les partenaires à l'aide des exemples 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 demande GET précédente pour afficher tous les partenaires enregistrés:

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

Vous devriez voir des données JSON avec beaucoup plus de contenu, donnant des informations sur les deux partenaires enregistrés.

Envoyer une demande en tant que compte non autorisé

La requête authentifiée effectuée lors de la dernière étape a réussi non seulement parce qu'elle a été authentifiée, mais aussi parce que l'utilisateur authentifié (votre compte) a été 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 demande précédente a été autorisé, car il s'agit du compte qui a créé le projet contenant l'application et qui lui a donné par défaut l'autorisation d'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. Plutôt que de le faire maintenant, vous allez créer un compte de service sans droit ni rôle attribué, et l'utiliser 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 obtiendrez un jeton d'identité pour ce nouveau compte de la même manière que vous en avez eu un précédemment pour votre compte par défaut. Toutefois, cela nécessite que votre compte par défaut soit autorisé à emprunter l'identité des 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 afin d'enregistrer un jeton d'identité pour ce nouveau compte dans la variable d'environnement TEST_IDENTITY. Si la commande affiche un message d'erreur, patientez 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. Effectuez la requête Web authentifiée comme précédemment, mais en utilisant ce jeton d'identité:
curl -i -X GET $SERVICE_URL/partners \
  -H "Authorization: Bearer $TEST_TOKEN"

Le résultat de la commande commencera à nouveau par HTTP/2 403, car la requête, bien qu'elle soit 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 de 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 minute ou deux pour que le nouveau rôle soit mis à jour, répétez la requête authentifiée. Enregistrez un nouveau TEST_TOKEN si cela fait au moins une heure qu'il a été enregistré pour la première fois.

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

Le résultat de la commande commence maintenant 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 les programmes et authentifier les utilisateurs

Les requêtes authentifiées que vous avez imaginées jusqu'à présent ont utilisé l'outil de ligne de commande curl. D'autres outils et langages de programmation auraient pu être utilisés à la place. Toutefois, les requêtes Cloud Run authentifiées ne peuvent pas être effectuées à l'aide d'un navigateur Web avec des pages Web standards. Si un utilisateur clique sur un lien ou sur un bouton pour envoyer un formulaire dans une page Web, le navigateur n'ajoutera 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é à être utilisé par des programmes, et non par des utilisateurs finaux.

Remarque:

Cloud Run peut héberger des applications Web visibles par les utilisateurs, mais ces types d'applications doivent configurer Cloud Run pour autoriser les requêtes non authentifiées émanant des utilisateurs les navigateurs Web. Si les applications nécessitent une authentification de l'utilisateur, l'application doit gérer cette opération 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 procédure à suivre n'entre pas dans le cadre de cet atelier de programmation.

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

Requêtes authentifiées provenant d'un programme Python

Un programme peut envoyer des requêtes authentifiées à une application Cloud Run sécurisée via des requêtes Web HTTP standards, mais avec un en-tête Authorization. Le seul nouveau défi pour 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 de programmation que les programmes peuvent utiliser pour demander l'émission d'un tel jeton. La bibliothèque cliente utilisée dans cet exemple est google.auth Python. Il existe plusieurs bibliothèques Python pour effectuer des requêtes Web en général. Cet exemple utilise le module populaire requests.

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 une interface de commande telle que Cloud Shell ou l'interface système de terminal standard sur votre propre ordinateur, l'utilisateur par défaut est celui qui s'est authentifié dans cette interface système. 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 aura pas d'utilisateur par défaut et ce code échouera.

Dans le cas d'un programme effectuant des demandes pour un autre programme, vous ne souhaitez généralement pas utiliser l'identité d'une personne, mais celle du programme à l'origine de la demande. 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 effectuer des requêtes API (vers Cloud Firestore, par exemple). Lorsqu'un programme s'exécute sur Google Cloud Platform, les bibliothèques clientes utilisent automatiquement le compte de service qui leur est attribué comme identité par défaut. Ainsi, le même code de programme fonctionne dans les deux situations.

Le code Python permettant d'effectuer une requête avec un en-tête "Authorization" supplémentaire est le suivant:

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

Le programme Python complet suivant enverra une requête authentifiée au service Cloud Run pour récupérer tous les partenaires enregistrés, puis imprimer leur nom et leur ID attribué. 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 exécuterez ce programme avec une commande shell. Vous devrez d'abord vous authentifier en tant qu'utilisateur par défaut, afin 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 doit ressembler à ceci:

10102: Zippy food delivery
67292: Foodful

La requête du programme a atteint le service Cloud Run, car celui-ci a été authentifié avec votre identité. Vous êtes le propriétaire de ce projet et vous ê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'elle est exécutée 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é à la place d'un compte personnel.

7. Félicitations !

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

Étapes suivantes :

Découvrez d'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.