web-dev-qa-db-fra.com

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

J'ai besoin de stocker des clés d'API et d'autres informations sensibles dans app.yaml en tant que variables d'environnement pour le déploiement sur GAE. Le problème avec ceci est que si je pousse app.yaml vers GitHub, cette information devient publique (pas bonne). Je ne veux pas stocker les informations dans un magasin de données car cela ne convient pas au projet. J'aimerais plutôt échanger les valeurs d'un fichier répertorié dans .gitignore à chaque 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?

53
Ben

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

Dans mes projets, j'ai placé 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 cela 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. S'il n'y en a pas, un enregistrement fictif sera créé et une exception sera levée. L'exception vous rappellera d'aller à la console des développeurs et de mettre à jour l'enregistrement d'espace réservé.

Je trouve que cela élimine le choix de valeurs de configuration. Si vous ne savez pas quelles valeurs de configuration définir, lancez simplement le code et il vous le dira!

Le code ci-dessus utilise la bibliothèque ndb qui utilise memcache et le datastore sous le capot, donc c'est rapide.


Mettre à jour:

jelder a demandé comment trouver et définir les valeurs du magasin de données dans la console App Engine. Voici comment:

  1. Accédez à https://console.cloud.google.com/datastore/

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

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

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

J'espère que cela t'aides!

 Your settings, created by the Settings class

 Click to edit

 Enter the real value and save

32
Martin Omander

Mon approche consiste à stocker les secrets du client uniquement dans l'application App Engine elle-même. Les secrets du client ne sont ni dans le contrôle de source ni sur des ordinateurs locaux. 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 le magasin de données et utilise Memcache pour améliorer la latence d'accès aux secrets. Les entités du magasin de données ne doivent être créées qu'une seule fois et elles persisteront lors de futurs déploiements. 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és uniques:

  • Utilisez le moteur d'application API à distance shell interactif 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.
18
Bernd Verst

La meilleure façon de le faire est de stocker les clés dans un fichier client_secrets.json et d’empêcher que cela soit transféré à git en le répertoriant dans votre fichier .gitignore. Si vous avez différentes clés pour différents environnements, vous pouvez utiliser app_identity api pour déterminer l'identifiant de l'application et le charger correctement.

Il existe 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()
16
Gwyn Howell

Vous pouvez utiliser l'option de ligne de commande -E 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).
...
15
jla

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

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

env_variables:
  SECRET: 'my_secret'

Ensuite, incluez ce env_variables.yaml dans le app.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, le env_variables.yaml doit être partagé entre les gestionnaires de déploiement.

5
Shih-Wen Su

On dirait que vous pouvez faire quelques approches. Nous avons un problème similaire et procédons comme suit (adapté à votre cas d'utilisation):

  • Créez un fichier contenant toutes les valeurs dynamiques de app.yaml et placez-le sur un serveur sécurisé de votre environnement de construction. Si vous êtes vraiment paranoïaque, vous pouvez chiffrer les valeurs de manière asymétrique. Vous pouvez même le conserver dans un dépôt privé si vous avez besoin d'un contrôle de version/extraction dynamique, ou simplement utiliser un script de shell pour le copier/le récupérer à l'endroit approprié.
  • Extrait 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

La méthode la plus simple consiste à utiliser un serveur d'intégration continue tel que Hudson , Bamboo ou Jenkins . Ajoutez simplement un plug-in, une action de script ou un workflow prenant en charge tous les éléments susmentionnés. Vous pouvez transmettre des variables d'environnement configurées dans Bamboo lui-même, par exemple.

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

Une autre option est ce que vous avez dit, mettez-le dans la base de données. Si vous ne le faites pas, c'est que les choses sont trop lentes, insérez simplement les valeurs dans memcache en tant que cache de la deuxième couche, puis épinglez les valeurs aux instances en tant que cache de la première couche. Si les valeurs peuvent changer et que vous devez mettre à jour les instances sans les redémarrer, conservez simplement un hachage que vous pouvez vérifier pour savoir quand elles changent ou le déclencher d'une manière ou d'une autre lorsque vous modifiez les valeurs. Cela devrait être ça.

2
therewillbesnacks

Un package pypi appelé gae_env vous permet de sauvegarder des variables d'environnement de moteur d'application dans Cloud Datastore. Sous le capot, il utilise également Memcache pour sa rapidité

Usage:

import gae_env

API_KEY = gae_env.get('API_KEY')

S'il existe une valeur pour cette clé dans le magasin de données, elle sera renvoyée . S'il n'y en a pas, un espace réservé __NOT_SET__ sera créé et un ValueNotSetError sera généré. L'exception vous rappellera d'aller à la Console de développeur et de mettre à jour l'enregistrement de substitution.


Semblable à la réponse de Martin, voici comment mettre à jour la valeur de la clé dans le magasin de données:

  1. Allez à Section Datastore dans la console des développeurs.

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

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

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

Your settings, created by the Settings class

Click to edit

Enter the real value and save


Allez sur la page GitHub du paquet pour plus d'informations sur l'utilisation/la configuration

1
Prince Odame

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
JSBach

La plupart des réponses sont obsolètes. Utiliser Google Cloud Datastore est en fait un peu différent en ce 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 type est "paramètres" et "valeur" est une propriété de l'entité Twitter_APP_KEY.

0
Jason F

Vous devriez chiffrer les variables avec Google kms et les 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

placez la valeur brouillée (chiffrée et codée en base64) dans votre variable d’environnement (dans le fichier yaml).

Du code pythonish 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
0
Anders Elton

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 'dotenv' npm qui charge les variables d'environnement à partir d'un fichier .env dans process.env. Quand 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); .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]
        })
    }
})()

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

require('../yaml_env')
0
gbruins