Stockage sécurisé des variables d'environnement dans GAE avec app.yaml


98

J'ai besoin de stocker les clés API et d'autres informations sensibles en app.yamltant que variables d'environnement pour le déploiement sur GAE. Le problème avec ceci est que si je pousse app.yamlvers GitHub, ces informations deviennent publiques (pas bonnes). Je ne veux pas stocker les informations dans une banque de données car cela ne convient pas au projet. Je souhaite plutôt échanger les valeurs d'un fichier répertorié dans .gitignorechaque déploiement de l'application.

Voici mon fichier app.yaml:

application: myapp
version: 3 
runtime: python27
api_version: 1
threadsafe: true

libraries:
- name: webapp2
  version: latest
- name: jinja2
  version: latest

handlers:
- url: /static
  static_dir: static

- url: /.*
  script: main.application  
  login: required
  secure: always
# auth_fail_action: unauthorized

env_variables:
  CLIENT_ID: ${CLIENT_ID}
  CLIENT_SECRET: ${CLIENT_SECRET}
  ORG: ${ORG}
  ACCESS_TOKEN: ${ACCESS_TOKEN}
  SESSION_SECRET: ${SESSION_SECRET}

Des idées?


73
Je souhaite que GAE ajoute l'option de définir les variables d'environnement d'instance via la console du développeur (comme tous les autres PaaS avec lesquels je suis familier).
Train d'Espagne

4
Vous pouvez utiliser la banque de données. Veuillez vous référer à cette réponse: stackoverflow.com/a/35254560/1027846
Mustafa İlhan

Extension du commentaire de mustilica ci-dessus sur l'utilisation du magasin de données. Voir ma réponse ci-dessous pour le code que j'utilise dans mes projets pour ce faire: stackoverflow.com/a/35261091#35261091 . En effet, il vous permet de modifier les variables d'environnement à partir de la console du développeur et les valeurs d'espace réservé sont créées automatiquement.
Martin Omander

Merci Mustilica et Martin. Nous avons en fait utilisé l'approche du magasin de données pendant un certain temps et je suis d'accord que c'est la meilleure solution à ce problème. Plus facile à faire avec une configuration CI / CD qu'avec l'approche de fichier json, IMO.
Train d'Espagne

1
2019 et GAE n'a toujours pas résolu ce problème: /
Josh Noe

Réponses:


53

S'il s'agit de données sensibles, vous ne devez pas les stocker dans le code source car elles seront archivées dans le contrôle de code source. Les mauvaises personnes (à l'intérieur ou à l'extérieur de votre organisation) peuvent y trouver. En outre, votre environnement de développement utilise probablement des valeurs de configuration différentes de celles de votre environnement de production. Si ces valeurs sont stockées dans le code, vous devrez exécuter un code différent en développement et en production, ce qui est compliqué et une mauvaise pratique.

Dans mes projets, je place les données de configuration dans le magasin de données en utilisant cette classe:

from google.appengine.ext import ndb

class Settings(ndb.Model):
  name = ndb.StringProperty()
  value = ndb.StringProperty()

  @staticmethod
  def get(name):
    NOT_SET_VALUE = "NOT SET"
    retval = Settings.query(Settings.name == name).get()
    if not retval:
      retval = Settings()
      retval.name = name
      retval.value = NOT_SET_VALUE
      retval.put()
    if retval.value == NOT_SET_VALUE:
      raise Exception(('Setting %s not found in the database. A placeholder ' +
        'record has been created. Go to the Developers Console for your app ' +
        'in App Engine, look up the Settings record with name=%s and enter ' +
        'its value in that record\'s value field.') % (name, name))
    return retval.value

Votre application ferait ceci pour obtenir une valeur:

API_KEY = Settings.get('API_KEY')

S'il existe une valeur pour cette clé dans le magasin de données, vous l'obtiendrez. Si ce n'est pas le cas, un enregistrement d'espace réservé sera créé et une exception sera levée. L'exception vous rappellera d'aller dans la Developers Console et de mettre à jour l'enregistrement d'espace réservé.

Je trouve que cela évite de deviner la définition des valeurs de configuration. Si vous n'êtes pas sûr des valeurs de configuration à définir, exécutez simplement le code et il vous le dira!

Le code ci-dessus utilise la bibliothèque ndb qui utilise Memcache et la banque de données sous le capot, donc c'est rapide.


Mettre à jour:

jelder demandé comment trouver les valeurs Datastore dans la console App Engine et les définir. Voici comment:

  1. Aller à https://console.cloud.google.com/datastore/

  2. Sélectionnez votre projet en haut de la page s'il n'est pas déjà sélectionné.

  3. Dans la liste déroulante Genre , sélectionnez Paramètres .

  4. Si vous avez exécuté le code ci-dessus, vos clés s'afficheront. Ils auront tous la valeur NOT SET . Cliquez sur chacun d'eux et définissez sa valeur.

J'espère que cela t'aides!

Vos paramètres, créés par la classe Paramètres

Cliquez pour modifier

Entrez la valeur réelle et enregistrez


2
De toutes les réponses fournies, cela semble le plus proche de la façon dont Heroku gère les choses. Étant plutôt nouveau dans GAE, je ne comprends pas très bien où trouver l'enregistrement d'espace réservé dans la Developers Console. Pouvez-vous expliquer, ou pour les points bonus, poster des captures d'écran?
jelder

2
dam ~… avec tout le respect que je dois à gcloud, il semble assez mauvais de devoir utiliser un autre service pour ce besoin spécifique. En plus de cela, Google fournit une approche "100% -herokuish" pour les variables d'environnement dans les fonctions de firebase, mais pas pour les fonctions gcloud (au moins non documentées ... si je ne me trompe pas)
Ben

1
Voici un résumé basé sur votre approche qui ajoute l'unicité et le remplacement de la
Train Espagne

3
Les fonctions @Ben non-Firebase prennent en charge les variables d'environnement (maintenant, au moins).
NReilingh

3
@obl - Une application App Engine est automatiquement authentifiée auprès de sa propre banque de données, aucun détail d'authentification n'est nécessaire. C'est plutôt
chouette

49

Cette solution est simple mais peut ne pas convenir à toutes les différentes équipes.

Tout d'abord, placez les variables d'environnement dans un env_variables.yaml , par exemple,

env_variables:
  SECRET: 'my_secret'

Ensuite, incluez ceci env_variables.yamldans leapp.yaml

includes:
  - env_variables.yaml

Enfin, ajoutez le env_variables.yamlà .gitignore, afin que les variables secrètes n'existent pas dans le référentiel.

Dans ce cas, les env_variables.yamlbesoins doivent être partagés entre les gestionnaires de déploiement.


1
Juste pour ajouter ce qui peut ne pas être évident pour certains, vos variables d'environnement seraient alors trouvées dans process.env.MY_SECRET_KEYet si vous avez besoin de ces variables d'environnement dans votre environnement de développement local, vous pouvez utiliser le dotenvpackage de nœuds
Dave Kiss

2
Comment env_variables.yamlarriver à toutes les instances est une pièce manquante du puzzle.
Christopher Oezbek

1
Aussi: comment l'utiliser localement?
Christopher Oezbek

@ChristopherOezbek 1. Comment déployer? Utilisez simplement gcloud app deploycomme vous le faites normalement pour déployer sur Google Cloud. 2. Comment définir localement des variables d'environnement secrètes? Il existe de nombreuses manières. Vous pouvez simplement utiliser exportl'invite de commande ou utiliser des outils tels que @DaveKiss suggéré.
Shih-Wen Su

C'est la solution la plus simple. Les secrets sont accessibles dans votre application via os.environ.get('SECRET').
Quinn Comendant

19

Mon approche consiste à stocker les secrets des clients uniquement dans l'application App Engine elle-même. Les secrets client ne sont ni dans le contrôle de code source ni sur aucun ordinateur local. Cela présente l'avantage que tout collaborateur App Engine peut déployer des modifications de code sans avoir à se soucier des secrets du client.

Je stocke les secrets des clients directement dans Datastore et j'utilise Memcache pour une latence améliorée pour accéder aux secrets. Les entités Datastore ne doivent être créées qu'une seule fois et persisteront lors des déploiements futurs. bien entendu, la console App Engine peut être utilisée pour mettre à jour ces entités à tout moment.

Il existe deux options pour effectuer la création d'entité ponctuelle:

  • Utilisez le shell interactif de l' API App Engine Remote pour créer les entités.
  • Créez un gestionnaire Admin uniquement qui initialisera les entités avec des valeurs factices. Appelez manuellement ce gestionnaire d'administration, puis utilisez la console App Engine pour mettre à jour les entités avec les secrets du client de production.

7
Pas compliqué du tout. Merci le moteur de l'application.
courtsimas

17

Cela n'existait pas lorsque vous avez posté, mais pour toute personne qui trébuche ici, Google propose désormais un service appelé Secret Manager .

C'est un simple service REST (avec des SDK qui l'enveloppent, bien sûr) pour stocker vos secrets dans un emplacement sécurisé sur la plate-forme cloud Google. Il s'agit d'une meilleure approche que le magasin de données, qui nécessite des étapes supplémentaires pour voir les secrets stockés et un modèle d'autorisation plus fin - vous pouvez sécuriser les secrets individuels différemment pour différents aspects de votre projet, si nécessaire.

Il offre une gestion des versions, afin que vous puissiez gérer les modifications de mot de passe avec une relative facilité, ainsi qu'une couche de requête et de gestion robuste vous permettant de découvrir et de créer des secrets au moment de l'exécution, si nécessaire.

SDK Python

Exemple d'utilisation:

from google.cloud import secretmanager_v1beta1 as secretmanager

secret_id = 'my_secret_key'
project_id = 'my_project'
version = 1    # use the management tools to determine version at runtime

client = secretmanager.SecretManagerServiceClient()

secret_path = client.secret_verion_path(project_id, secret_id, version)
response = client.access_secret_version(secret_path)
password_string = response.payload.data.decode('UTF-8')

# use password_string -- set up database connection, call third party service, whatever

3
Cela devrait être la nouvelle bonne réponse. Secret Manager est toujours en version bêta, mais c'est la voie à suivre lorsque vous travaillez avec des variables d'environnement.
King Leon le

@KingLeon, l'utilisation de cela signifierait-elle avoir à refactoriser un tas de os.getenv('ENV_VAR')s?
Alejandro

Je mets du code similaire à ci-dessus dans une fonction, puis j'utilise quelque chose comme SECRET_KEY = env('SECRET_KEY', default=access_secret_version(GOOGLE_CLOUD_PROJECT_ID, 'SECRET_KEY', 1)). Définition de la valeur par défaut pour utiliser leaccess_secret_version
King Leon

De plus, j'utilise django-environ. github.com/joke2k/django-environ
King Leon

16

La meilleure façon de le faire est de stocker les clés dans un fichier client_secrets.json et d'exclure cela du téléchargement vers git en le listant dans votre fichier .gitignore. Si vous avez des clés différentes pour différents environnements, vous pouvez utiliser app_identity api pour déterminer l'ID de l'application et le charger de manière appropriée.

Il y a un exemple assez complet ici -> https://developers.google.com/api-client-library/python/guide/aaa_client_secrets .

Voici un exemple de code:

# declare your app ids as globals ...
APPID_LIVE = 'awesomeapp'
APPID_DEV = 'awesomeapp-dev'
APPID_PILOT = 'awesomeapp-pilot'

# create a dictionary mapping the app_ids to the filepaths ...
client_secrets_map = {APPID_LIVE:'client_secrets_live.json',
                      APPID_DEV:'client_secrets_dev.json',
                      APPID_PILOT:'client_secrets_pilot.json'}

# get the filename based on the current app_id ...
client_secrets_filename = client_secrets_map.get(
    app_identity.get_application_id(),
    APPID_DEV # fall back to dev
    )

# use the filename to construct the flow ...
flow = flow_from_clientsecrets(filename=client_secrets_filename,
                               scope=scope,
                               redirect_uri=redirect_uri)

# or, you could load up the json file manually if you need more control ...
f = open(client_secrets_filename, 'r')
client_secrets = json.loads(f.read())
f.close()

2
Certainement dans la bonne direction, mais cela ne résout pas le problème de l'échange des valeurs lors app.yamldu déploiement de l'application. Des idées là-bas?
Ben

1
Ayez donc un fichier client_secrets différent pour chaque environnement. Par exemple, client_secrets_live.json, client_secrets_dev.json, client_secrets_pilot.json etc., puis utilisez la logique python pour déterminer sur quel serveur vous êtes et chargez le fichier json approprié. La méthode app_identity.get_application_id () peut être utile pour détecter automatiquement sur quel serveur vous êtes. Est-ce le genre de chose que tu veux dire?
Gwyn Howell

@BenGrunfeld voyez ma réponse. Ma solution fait exactement cela. Je ne vois pas comment cette réponse résout la question. Je suppose que l'objectif est de garder la configuration secrète hors de git et d'utiliser git dans le cadre du déploiement. Ici, ce fichier doit toujours être quelque part et être poussé dans le processus de déploiement. Cela peut être quelque chose que vous faites dans votre application, mais vous utiliseriez simplement les techniques que j'ai mises en évidence, peut-être en les stockant dans un autre fichier si vous souhaitez utiliser ceci par rapport à app.yaml. Si je comprends la question, c'est quelque chose qui s'apparente à l'envoi d'une application open source avec le secret client ou le produit réel du fabricant de la bibliothèque. clé.
therewillbesnacks

1
Il m'a fallu un certain temps pour comprendre, mais je pense que c'est la bonne approche. Vous ne mélangez pas les paramètres d'application ( app.yaml) avec des clés secrètes et des informations confidentielles, et ce que j'aime vraiment, c'est que vous utilisez le flux de travail de Google pour accomplir la tâche. Merci @GwynHowell. =)
Ben

1
Une approche similaire consisterait à placer ce fichier JSON dans un emplacement connu dans le bucket GCS par défaut de l'application ( cloud.google.com/appengine/docs/standard/python/… ).
Train d'Espagne

15

Cette solution s'appuie sur l'appcfg.py obsolète

Vous pouvez utiliser l'option de ligne de commande -E de appcfg.py pour configurer les variables d'environnement lorsque vous déployez votre application sur GAE (mise à jour appcfg.py)

$ appcfg.py
...
-E NAME:VALUE, --env_variable=NAME:VALUE
                    Set an environment variable, potentially overriding an
                    env_variable value from app.yaml file (flag may be
                    repeated to set multiple variables).
...

Pouvez-vous interroger ces variables d'environnement quelque part après le déploiement? (J'espère que non.)
Ztyx

Existe-t-il un moyen de transmettre des variables d'environnement de cette manière à l'aide de l' gcloudutilitaire?
Trevor

6

La plupart des réponses sont dépassées. Utiliser google cloud datastore est en fait un peu différent pour le moment. https://cloud.google.com/python/getting-started/using-cloud-datastore

Voici un exemple:

from google.cloud import datastore
client = datastore.Client()
datastore_entity = client.get(client.key('settings', 'TWITTER_APP_KEY'))
connection_string_prod = datastore_entity.get('value')

Cela suppose que le nom de l'entité est 'TWITTER_APP_KEY', le genre est 'settings' et 'value' est une propriété de l'entité TWITTER_APP_KEY.


3

Il semble que vous puissiez faire quelques approches. Nous avons un problème similaire et faisons ce qui suit (adapté à votre cas d'utilisation):

  • Créez un fichier qui stocke toutes les valeurs dynamiques app.yaml et placez-le sur un serveur sécurisé dans votre environnement de build. Si vous êtes vraiment paranoïaque, vous pouvez crypter les valeurs de manière asymétrique. Vous pouvez même le conserver dans un référentiel privé si vous avez besoin d'un contrôle de version / extraction dynamique, ou simplement utiliser un script shells pour le copier / extraire de l'endroit approprié.
  • Extraire de git pendant le script de déploiement
  • Après le git pull, modifiez le fichier app.yaml en le lisant et en l'écrivant en python pur à l'aide d'une bibliothèque yaml

Le moyen le plus simple de le faire est d'utiliser un serveur d'intégration continue tel que Hudson , Bamboo ou Jenkins . Ajoutez simplement un plug-in, une action de script ou un flux de travail qui effectue tous les éléments ci-dessus que j'ai mentionnés. Vous pouvez transmettre des variables d'environnement qui sont configurées dans Bamboo lui-même par exemple.

En résumé, insérez simplement les valeurs pendant votre processus de construction dans un environnement auquel vous n'avez accès que. Si vous n'automatisez pas déjà vos builds, vous devriez l'être.

Une autre option est ce que vous avez dit, mettez-le dans la base de données. Si la raison pour laquelle vous ne faites pas cela est que les choses sont trop lentes, poussez simplement les valeurs dans Memcache en tant que cache de deuxième couche et épinglez les valeurs aux instances en tant que cache de première couche. Si les valeurs peuvent changer et que vous devez mettre à jour les instances sans les redémarrer, gardez simplement un hachage que vous pouvez vérifier pour savoir quand elles changent ou le déclenchent d'une manière ou d'une autre lorsque quelque chose que vous faites change les valeurs. Ça devrait être ça.


1
FWIW, cette approche suit de près le facteur de configuration dans les directives de l'application 12 Factor ( 12factor.net )
Train d'Espagne

3

Vous devez crypter les variables avec google kms et l'intégrer dans votre code source. ( https://cloud.google.com/kms/ )

echo -n the-twitter-app-key | gcloud kms encrypt \
> --project my-project \
> --location us-central1 \
> --keyring THEKEYRING \
> --key THECRYPTOKEY \
> --plaintext-file - \
> --ciphertext-file - \
> | base64

mettez la valeur brouillée (cryptée et encodée en base64) dans votre variable d'environnement (dans le fichier yaml).

Du code python pour vous aider à déchiffrer.

kms_client = kms_v1.KeyManagementServiceClient()
name = kms_client.crypto_key_path_path("project", "global", "THEKEYRING", "THECRYPTOKEY")

twitter_app_key = kms_client.decrypt(name, base64.b64decode(os.environ.get("TWITTER_APP_KEY"))).plaintext

3

La réponse de @Jason F basée sur l'utilisation de Google Datastore est proche, mais le code est un peu dépassé en fonction de l'exemple d'utilisation des documents de la bibliothèque . Voici l'extrait qui a fonctionné pour moi:

from google.cloud import datastore

client = datastore.Client('<your project id>')
key = client.key('<kind e.g settings>', '<entity name>') # note: entity name not property
# get by key for this entity
result = client.get(key)
print(result) # prints all the properties ( a dict). index a specific value like result['MY_SECRET_KEY'])

En partie inspiré par ce post Medium


2

Je voulais juste noter comment j'ai résolu ce problème dans javascript / nodejs. Pour le développement local, j'ai utilisé le package npm «dotenv» qui charge les variables d'environnement d'un fichier .env dans process.env. Lorsque j'ai commencé à utiliser GAE, j'ai appris que les variables d'environnement devaient être définies dans un fichier «app.yaml». Eh bien, je ne voulais pas utiliser 'dotenv' pour le développement local et 'app.yaml' pour GAE (et dupliquer mes variables d'environnement entre les deux fichiers), j'ai donc écrit un petit script qui charge les variables d'environnement app.yaml dans le processus .env, pour le développement local. J'espère que cela aide quelqu'un:

yaml_env.js:

(function () {
    const yaml = require('js-yaml');
    const fs = require('fs');
    const isObject = require('lodash.isobject')

    var doc = yaml.safeLoad(
        fs.readFileSync('app.yaml', 'utf8'), 
        { json: true }
    );

    // The .env file will take precedence over the settings the app.yaml file
    // which allows me to override stuff in app.yaml (the database connection string (DATABASE_URL), for example)
    // This is optional of course. If you don't use dotenv then remove this line:
    require('dotenv/config');

    if(isObject(doc) && isObject(doc.env_variables)) {
        Object.keys(doc.env_variables).forEach(function (key) {
            // Dont set environment with the yaml file value if it's already set
            process.env[key] = process.env[key] || doc.env_variables[key]
        })
    }
})()

Incluez maintenant ce fichier le plus tôt possible dans votre code, et vous avez terminé:

require('../yaml_env')

Est-ce toujours le cas? Parce que j'utilise un .envfichier avec les variables secrètes. Je ne les duplique pas dans mon app.yamlfichier et mon code déployé fonctionne toujours. Je m'inquiète cependant de ce qu'il advient du .envfichier dans le cloud. Est-il crypté ou quelque chose? Comment puis-je m'assurer que personne n'accède aux .envvariables du fichier gcloud une fois qu'il est déployé?
Gus le

Cela n'est pas du tout nécessaire car GAE ajoute automatiquement toutes les variables définies dans le fichier app.yaml à l'environnement de nœud. Fondamentalement, c'est la même chose que dotenv avec les variables définies dans le package .env. Mais je me demande comment vous devez installer le CD car vous ne pouvez pas pousser app.yaml avec env vars vers un VCS ou un pipeline ...
Jornve

1

Prolonger la réponse de Martin

from google.appengine.ext import ndb

class Settings(ndb.Model):
    """
    Get sensitive data setting from DataStore.

    key:String -> value:String
    key:String -> Exception

    Thanks to: Martin Omander @ Stackoverflow
    https://stackoverflow.com/a/35261091/1463812
    """
    name = ndb.StringProperty()
    value = ndb.StringProperty()

    @staticmethod
    def get(name):
        retval = Settings.query(Settings.name == name).get()
        if not retval:
            raise Exception(('Setting %s not found in the database. A placeholder ' +
                             'record has been created. Go to the Developers Console for your app ' +
                             'in App Engine, look up the Settings record with name=%s and enter ' +
                             'its value in that record\'s value field.') % (name, name))
        return retval.value

    @staticmethod
    def set(name, value):
        exists = Settings.query(Settings.name == name).get()
        if not exists:
            s = Settings(name=name, value=value)
            s.put()
        else:
            exists.value = value
            exists.put()

        return True

1

Il existe un package pypi appelé gae_env qui vous permet d'enregistrer les variables d'environnement appengine dans Cloud Datastore. Sous le capot, il utilise également Memcache donc c'est rapide

Usage:

import gae_env

API_KEY = gae_env.get('API_KEY')

S'il existe une valeur pour cette clé dans la banque de données, elle sera renvoyée. S'il n'y en a pas, un enregistrement d'espace réservé __NOT_SET__sera créé et un ValueNotSetErrorsera jeté. L'exception vous rappellera d'aller dans la Developers Console et de mettre à jour l'enregistrement d'espace réservé.


Semblable à la réponse de Martin, voici comment mettre à jour la valeur de la clé dans Datastore:

  1. Accédez à la section Datastore dans la console des développeurs

  2. Sélectionnez votre projet en haut de la page s'il n'est pas déjà sélectionné.

  3. Dans la liste déroulante Genre , sélectionnez GaeEnvSettings.

  4. Les clés pour lesquelles une exception a été déclenchée auront une valeur __NOT_SET__.

Vos paramètres, créés par la classe Paramètres

Cliquez pour modifier

Entrez la valeur réelle et enregistrez


Accédez à la page GitHub du package pour plus d'informations sur l'utilisation / la configuration

En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.