Conserver des informations secrètes avec Secret Manager et Cloud Run

Publié le jeudi 15 avril 2021
This post thumbnail

Conserver des informations secrètes avec Secret Manager et Cloud Run

Lorsque nous développons et exploitons un service, il y a des informations que pour des raisons de sécurité que nous voulons conserver à l'abri du regard par des personnes non autorisées.

Dans cet article, nous allons sécuriser l'information secrète d'une clé privée d'API à l'aide de Google Cloud Secret Manager sous Google Cloud Run.

Pour ce faire, nous allons modifier un exemple conçu par Google qui est généré lors de l'utilisation du plugin Cloud Run dans VS Code. Les données actuelles de la météo à Montréal seront affichées sur la page html générée par le service.

Nous allons utiliser Google Cloud Secret Manager pour conserver la clé secrète de l'API publique du service Open Weather Map.

Google Cloud Secret Manager

Google Cloud Secret Manager facilite la gestion de secrets par son interface de gestion et la disponibilité de librairies pour récupérer en toute sécurité les informations protégées.

Dans notre cas, cela va permettre de mettre en pratique le principe d'architecture du moindre privilège. Il consiste à ne donner que les droits nécessaires pour faire un travail requis. Cela consiste par exemple à ce que les développeurs n'aient pas accès aux environnements de production. Dans ce cas, les développeurs n'auront pas la clé secrète de production du service, elle pourra être gérée par l'équipe de sécurité. Autre avantage de la solution, c'est qu'il est possible à l'équipe de sécurité de faire la rotation de la clé sans avoir à redéployer le service.

Étape 1 Créer le squelette du service à l'aide du générateur

Si vous n'avez pas déjà ajouté l'extension Cloud Code, installez-la maintenant.

Par la suite, vous pouvez cliquer sur "Cloud Code" au bas de votre fenêtre et créer une nouvelle application. Il vous sera possible de spécifier une application Cloud Run et par la suite choisir Node.js comme environnement d'exécution.

Vous aurez l'ensemble des fichiers requis pour déployer le service sous Cloud Run incluant le Dockerfile pour bâtir l'image qui sera déployée en production.

Dans les étapes suivantes, nous allons modifier le service pour afficher la météo actuelle à Montréal.

Étape 2 Créer le compte de service

Nous créer un compte de service afin de restreindre les rôles et permissions du service. Et ce afin de répondre au principe du moindre privilège, car nous pourrons assigner seulement les rôles nécessaires requis pour son bon fonctionnement.

À l'aide de la console, en choisir dans le menu de droite: Cloud IAM Admin - Service account

Cloud IAM Admin - Service Account Menu Choisir Cloud IAM Admin - Service Account Menu

Par la suite, créer le compte de service :

Créer le compte de service Créer le compte de service qui sera utilisé par Cloud Run

Ajouter une rolel Choisir Add a role

Ajouter le role Secret Manager Secret Accessor Ajouter le role Secret Manager Secret Accessor

Étape 3 Créer le service

Nous allons créer le service et y assigner un compte de service et rendre le service accessible à tous. Le compte de service désigné va permettre aussi de répondre au principe du moindre privilège, car nous pourrons assigner seulement les rôles nécessaires requis pour son bon fonctionnement.

Il est possible de le faire directement via l'extension Cloud Run :

Créer un service Cloud Run Créer un service Cloud Run

S'assurer de sélectionner la case Allow unauthenticated invocations pour permettre l'accès à publique au service. Il est toujours possible de permettre l'accès publique après la création du service Cloud Run à l'aide de la commande gcloud sur GoogleCloud Shell :

gcloud run services add-iam-policy-binding NOM-DU-SERVICE \
    --member="allUsers" \
    --role="roles/run.invoker"

Inscrire le nom du compte de service complet dans la case service account :

Spécifier le nomdu compte de service Spécifier le nomdu compte de service

Une fois le service créé, vous obtiendrez un URL pour y accéder, la page suivante devrait s'afficher :

Vérification du déploiement Première invocation du service

Nous allons maintenant pouvoir modifier le service pour afficher les données de la météo.

Étape 4 S'inscrire à OpenWeather pour obtenir une clé d'API

L’organisme OpenWeather offre aux développeurs accès à diverses données météorologiques. La version gratuite offre l’accès à plusieurs types de données et prévisions météorologiques, dont l'accès à la météo actuelle. Elle permet jusqu’à 60 appels par minute. Pour obtenir une clé d’accès, il suffit d’aller s’inscrire sur le site pour obtenir une clé OpenWeatherMap. Nous allons par la suite, utiliser Secret Manager pour protéger l’accès à cette clé.

Étape 5 Activer l'API Secret Manager pour votre projet

Il est possible de le faire directement à l'aide de l'extension Cloud Code Secret Manager. La première fois que vous allez y accéder, il va vous offrir d'activer l'API.

Étape 6 Créer le secret pour conserver la clé d'API

Nous pouvons créer directement le secret à l'aide de l'extension Cloud Code. Pour notre exemple :

Créer le secret Créer le secret à l'aide de l'extension Cloud Code Secret Manager

Ou bien à l'aide de la commande suivante dans Cloud Shell en spécifiant votre valeur et un nom de clé.

echo "VOTRE-CLE-SECRETE" | gcloud secrets create openweathermapapikey --replication-policy=automatic

Étape 7 Ajouter les librairies au projet

À cette étape, il faut ajouter la librairie Secret Manager pour Node.js. Nous allons en profiter pour ajouter la librairie Axios pour obtenir la météo actuelle.

npm install --save @google-cloud/secret-manager
npm install -save axios

Étape 8 Modifier index.js et index.html.hbs pour obtenir le secret, obtenir la météo

Maintenant, nous allons modifier le code pour obtenir la clé de l'API via Secret Manager à l'initialisation du service Node.js sous Cloud Run. Et allons inclure les modifications requises pour obtenir les données météo actuelles pour la ville de Montréal. En spécifiant la version latest du secret, cela permet de faire la mise à jour de la clé sans avoir à modifier le service.

Nous allons aussi modifier le fichier index.html.hbs qui effectue la présentation des données afin d'y inclure la température actuelle.

Voici le contenu du fichier index.js modifié :

const express = require('express');
const {readFileSync} = require('fs');
const handlebars = require('handlebars');
const axios = require('axios');

const {SecretManagerServiceClient} = require('@google-cloud/secret-manager');
const secretManagerServiceClient = new SecretManagerServiceClient();

const getOpenWeatherMapApiKey = async () => {
  const name =
    'projects/70080025061/secrets/openweathermapapikey/versions/latest';
  const [version] = await secretManagerServiceClient.accessSecretVersion({
    name,
  });
  const payload = version.payload.data.toString();
  return payload;
};

const KelvinToCelciusDiff = 273.15;

/**
 * @typedef {Object<string, any>} ICurrentWeather
 * @property {string} city The city's name.
 * @property {number=} currentTempInC The city's current weather temp in Celcuis.
 */

/**
 * Function extractCurrentWeather
 * @param {*} data OpenWeatherMap current weather data structure
 * @returns  ICurrentWeather
 */
function extractCurrentWeather(data) {
  return {
    city: data.name,
    currentTempInC: (data.main.temp - KelvinToCelciusDiff).toFixed(1), // convert actual value in Kelvin to Celcius
  };
}

async function getCurrentWeather() {
  try {
    const key = await getOpenWeatherMapApiKey();
    // fetch data from a url endpoint
    const response = await axios.get(
      'http://api.openweathermap.org/data/2.5/weather?q=Montreal,QC,CA&appid=' +
        key
    );
    console.log(`OpenWeather Map Data:` + JSON.stringify(response.data));
    return extractCurrentWeather(response.data);
  } catch (e) {
    console.error('Error getCurrentWeather : ' + e.toString());
    throw e;
  }
}

const app = express();
// Serve the files in /assets at the URI /assets.
app.use('/assets', express.static('assets'));

// The HTML content is produced by rendering a handlebars template.
// The template values are stored in global state for reuse.
const data = {
  service: process.env.K_SERVICE || '???',
  revision: process.env.K_REVISION || '???',
};
let template;

app.get('/', async (req, res) => {
  //
  let meteo;

  try {
    meteo = await getCurrentWeather();
  } catch (e) {
    console.error('Error getCurrentweather : ' + e.toString());
  }
  // The handlebars template is stored in global state so this will only once.
  if (!template) {
    // Load Handlebars template from filesystem and compile for use.
    try {
      template = handlebars.compile(readFileSync('index.html.hbs', 'utf8'));
    } catch (e) {
      console.error(e);
      res.status(500).send('Internal Server Error');
    }
  }

  // Apply the template to the parameters to generate an HTML string.
  try {
    const output = template({...data, ...meteo});
    res.status(200).send(output);
  } catch (e) {
    console.error(e);
    res.status(500).send('Internal Server Error');
  }
});

const PORT = process.env.PORT || 8080;
app.listen(PORT, () => {
  console.log(
    `Hello from Cloud Run Secret Manager Example! The container started successfully and is listening for HTTP requests on ${PORT}`
  );
});

Voici le contenu du fichier index.html.hbs modifié :

<!doctype html>
<html lang=fr>

<head>
	<meta charset=utf-8>
	<meta name="viewport" content="width=device-width, initial-scale=1">
	<title>Congratulations | Cloud Run</title>
	<link href="https://fonts.googleapis.com/css?family=Roboto" rel="preload" as="font">
	<link href="/assets/cloud-run-32-color.png" rel="icon" type="image/png" />
	<link href="/assets/style.css" rel="stylesheet" type="text/css" />
</head>

<body>
	<div class="container">
		<div class="hero">
			<div style="text-align:center;">
				<picture>
					<source srcset="/assets/celebration-dark.svg" media="(prefers-color-scheme: dark)">
					<img src="/assets/celebration.svg" alt="A group celebrating" width="427" height="231">
				</picture>
			</div>

			<div class="message">
                
				<h1>Il fait actuellement {{currentTempInC}}&#8451; à {{city}}</h1>
                
				<h2></h2>
			</div>
		</div>

		<div class="details">
			<p>
				Ceci est un exemple de service d'appel d'API OpenWeather sous Cloud Run
				et utilisation de Google Secret Manager pour conserver la clé secrète d'API.
			</p>
		</div>
	</div>
</body>

</html>

Voici la nouvelle page d'accueil du service :

Page d'accueil du service avec météo actuelle Page d'accueil du service avec météo actuelle

Conclusion

Secret Manager est un outil essentiel pour conserver en toute sécurité des informations sensibles pour lesquelles nous voulons compartimenter les rôles.

Dans cet article, nous démontré comme il est aisé de sécurisé l'information secrète d'une clé privée d'API à l'aide de Google Cloud Secret Manager sous Google Cloud Run.

Cela permet de séparer les domaines d'expertise du développement et de l'exploitation. Cela permet de mettre en pratique le principe d'architecture du moindre privilège.

Dans un prochain article, nous pourrons explorer la mise en cache de la clé de service afin de réduire les coûts d'utilisation de Secret Manager. Car à ce moment, il en coûte 0,03 $ pour 10 000 opération d'accès à la clé. Dans notre cas, ici, c'est marginal.

Références

https://cloud.google.com/secret-manager/docs

https://openweathermap.org/api

https://cloud.google.com/secret-manager/docs/quickstart

https://cloud.google.com/secret-manager/docs/access-control

https://cloud.google.com/code/docs/vscode/secret-manager

https://cloud.google.com/secret-manager/docs/reference/libraries

https://cloud.google.com/run/docs/authenticating/public#gcloud

https://cloud.google.com/run/docs/reference/iam/roles

https://cloud.google.com/secret-manager/docs/configuring-secret-manager

https://cloud.google.com/secret-manager/pricing?hl=fr