Almacenamiento seguro de variables de entorno en GAE con app.yaml

98

Necesito almacenar claves de API y otra información confidencial app.yamlcomo variables de entorno para la implementación en GAE. El problema con esto es que si presiono app.yamla GitHub, esta información se vuelve pública (no es buena). No quiero almacenar la información en un almacén de datos, ya que no se adapta al proyecto. Más bien, me gustaría intercambiar los valores de un archivo que se enumera en .gitignorecada implementación de la aplicación.

Aquí está mi archivo 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}

¿Algunas ideas?

Ben
fuente
73
Me gustaría que GAE agregara la opción de configurar las variables de entorno de instancia a través de la consola del desarrollador (como cualquier otra PaaS con la que estoy familiarizado).
Tren de España
4
Puede usar el almacén de datos. Consulte esta respuesta: stackoverflow.com/a/35254560/1027846
Mustafa İlhan
Ampliando el comentario de mustilica anterior sobre el uso del almacén de datos. Vea mi respuesta a continuación para ver el código que uso en mis proyectos para hacer esto: stackoverflow.com/a/35261091#35261091 . De hecho, le permite editar las variables de entorno desde la consola del desarrollador y los valores de marcador de posición se crean automáticamente.
Martin Omander
Gracias mustilica y Martin. De hecho, hemos estado usando el enfoque del almacén de datos por un tiempo y estoy de acuerdo en que es la mejor solución para este problema. Es más fácil de hacer con una configuración de CI / CD que el enfoque de archivo json, en mi opinión.
Tren de España
1
2019 y GAE aún no ha solucionado este problema: /
Josh Noe

Respuestas:

53

Si se trata de datos confidenciales, no debe almacenarlos en el código fuente, ya que se registrarán en el control de fuentes. Las personas equivocadas (dentro o fuera de su organización) pueden encontrarlo allí. Además, su entorno de desarrollo probablemente use valores de configuración diferentes de su entorno de producción. Si estos valores se almacenan en código, tendrá que ejecutar código diferente en desarrollo y producción, lo cual es complicado y una mala práctica.

En mis proyectos, coloco datos de configuración en el almacén de datos usando esta clase:

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

Su aplicación haría esto para obtener un valor:

API_KEY = Settings.get('API_KEY')

Si hay un valor para esa clave en el almacén de datos, lo obtendrá. Si no lo hay, se creará un registro de marcador de posición y se lanzará una excepción. La excepción le recordará que vaya a Developers Console y actualice el registro de marcador de posición.

Encuentro que esto elimina las conjeturas al establecer los valores de configuración. Si no está seguro de qué valores de configuración establecer, simplemente ejecute el código y se lo dirá.

El código anterior usa la biblioteca ndb que usa memcache y el almacén de datos debajo del capó, por lo que es rápido.


Actualizar:

jelder preguntó cómo encontrar los valores del almacén de datos en la consola de App Engine y configurarlos. Aquí es cómo:

  1. Vaya a https://console.cloud.google.com/datastore/

  2. Seleccione su proyecto en la parte superior de la página si aún no está seleccionado.

  3. En el cuadro desplegable Tipo , seleccione Configuración .

  4. Si ejecutó el código anterior, aparecerán sus claves. Todos tendrán el valor NO ESTABLECIDO . Haga clic en cada uno y establezca su valor.

¡Espero que esto ayude!

Su configuración, creada por la clase Configuración

Haz click para editar

Ingrese el valor real y ahorre

Martín Omander
fuente
2
De todas las respuestas proporcionadas, esta parece más cercana a cómo maneja Heroku las cosas. Siendo bastante nuevo en GAE, no entiendo muy bien dónde encontrar el registro de marcador de posición en Developers Console. ¿Puede explicarnos, o para obtener puntos extra, publicar capturas de pantalla?
jelder
2
dam ~… con el debido respeto a gcloud, parece bastante malo tener que usar otro servicio para esta necesidad específica. Además de eso, Google proporciona un enfoque "100% herokuish" para las variables env dentro de las funciones de firebase, pero no para las funciones de gcloud (al menos indocumentadas ... si no me equivoco)
Ben
1
Aquí hay una esencia basada en su enfoque que agrega singularidad y alternativa de variable de entorno: gist.github.com/SpainTrain/6bf5896e6046a5d9e7e765d0defc8aa8
Tren de España
3
Las funciones de @Ben Non-Firebase admiten env vars (ahora, al menos).
NReilingh
3
@obl: una aplicación de App Engine se autentica automáticamente en su propio almacén de datos, no se necesitan detalles de autenticación. Es bastante ordenado :-)
Martin Omander
49

Esta solución es simple, pero puede que no sea adecuada para todos los equipos.

Primero, coloque las variables de entorno en un env_variables.yaml , por ejemplo,

env_variables:
  SECRET: 'my_secret'

Luego, incluya esto env_variables.yamlen elapp.yaml

includes:
  - env_variables.yaml

Finalmente, agregue el env_variables.yamla .gitignore, para que las variables secretas no existan en el repositorio.

En este caso, las env_variables.yamlnecesidades deben compartirse entre los administradores de implementación.

Shih-Wen Su
fuente
1
Solo para agregar lo que puede no ser obvio para algunos, sus variables de entorno se encontrarán process.env.MY_SECRET_KEYy si necesita estas variables de entorno en su entorno de desarrollo local, puede usar el dotenvpaquete de nodos
Dave Kiss
2
Cómo env_variables.yamlllegar a todas las instancias es una pieza faltante del rompecabezas.
Christopher Oezbek
1
Además: ¿Cómo usar esto localmente?
Christopher Oezbek
@ChristopherOezbek 1. ¿Cómo implementar? Solo utilícelo gcloud app deploycomo lo hace normalmente para implementar en Google Cloud. 2. ¿Cómo establecer localmente variables de entorno secretas? Hay muchas maneras. Puede usarlo exporten el símbolo del sistema o usar cualquier herramienta como @DaveKiss sugirió.
Shih-Wen Su
Ésta es la solución más sencilla. Se puede acceder a los secretos en su aplicación a través de os.environ.get('SECRET').
Quinn acusado
19

Mi enfoque es almacenar los secretos del cliente solo dentro de la propia aplicación de App Engine. Los secretos del cliente no están en el control de código fuente ni en ninguna computadora local. Esto tiene la ventaja de que cualquier colaborador de App Engine puede implementar cambios de código sin tener que preocuparse por los secretos del cliente.

Almaceno los secretos del cliente directamente en Datastore y uso Memcache para mejorar la latencia al acceder a los secretos. Las entidades de Datastore solo deben crearse una vez y persistirán en futuras implementaciones. por supuesto, la consola de App Engine se puede utilizar para actualizar estas entidades en cualquier momento.

Hay dos opciones para realizar la creación única de la entidad:

  • Usa el shell interactivo de la API remota de App Engine para crear las entidades.
  • Cree un controlador de solo administrador que inicializará las entidades con valores ficticios. Invoca manualmente este controlador de administración y, luego, usa la consola de App Engine para actualizar las entidades con los secretos del cliente de producción.
Bernd Verst
fuente
7
No es nada complicado. Gracias motor de aplicaciones.
courtimas
17

Esto no existía cuando publicaste, pero para cualquier otra persona que se tropiece aquí, Google ahora ofrece un servicio llamado Administrador secreto .

Es un servicio REST simple (con SDK que lo envuelven, por supuesto) para almacenar sus secretos en una ubicación segura en la plataforma en la nube de Google. Este es un enfoque mejor que Data Store, ya que requiere pasos adicionales para ver los secretos almacenados y tiene un modelo de permisos más detallado: puede proteger secretos individuales de manera diferente para diferentes aspectos de su proyecto, si es necesario.

Ofrece control de versiones, por lo que puede manejar los cambios de contraseña con relativa facilidad, así como una capa sólida de consulta y administración que le permite descubrir y crear secretos en tiempo de ejecución, si es necesario.

SDK de Python

Uso de ejemplo:

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
Randolpho
fuente
3
Esta debería ser la nueva respuesta correcta. Secret Manager todavía está en Beta, pero este es el camino a seguir cuando se trabaja con variables de entorno.
King Leon
@KingLeon, ¿el uso de esto significaría tener que refactorizar un montón de os.getenv('ENV_VAR')s?
Alejandro
Pongo un código similar al anterior en una función, luego uso algo como SECRET_KEY = env('SECRET_KEY', default=access_secret_version(GOOGLE_CLOUD_PROJECT_ID, 'SECRET_KEY', 1)). Configuración predeterminada para usaraccess_secret_version
King Leon
Además, estoy usando django-environment. github.com/joke2k/django-environ
King Leon
16

La mejor manera de hacerlo es almacenar las claves en un archivo client_secrets.json y excluir que se carguen en git al incluirlas en su archivo .gitignore. Si tiene diferentes claves para diferentes entornos, puede usar app_identity api para determinar cuál es la identificación de la aplicación y cargarla de manera adecuada.

Hay un ejemplo bastante completo aquí:> https://developers.google.com/api-client-library/python/guide/aaa_client_secrets .

Aquí hay un código de ejemplo:

# 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()
Gwyn Howell
fuente
2
Definitivamente en la dirección correcta, pero esto no resuelve el problema de cambiar los valores en la app.yamlimplementación de la aplicación. ¿Alguna idea allí?
Ben
1
Así que tenga un archivo client_secrets diferente para cada entorno. Por ejemplo, client_secrets_live.json, client_secrets_dev.json, client_secrets_pilot.json, etc., luego use la lógica de Python para determinar en qué servidor se encuentra y cargue el archivo json apropiado. El método app_identity.get_application_id () puede ser útil para detectar automáticamente en qué servidor se encuentra. ¿Es este el tipo de cosas que quieres decir?
Gwyn Howell
@BenGrunfeld mira mi respuesta. Mi solución hace exactamente esto. No veo cómo esta respuesta resuelve la pregunta. Supongo que el objetivo es mantener la configuración secreta fuera de git y usar git como parte de la implementación. Aquí, este archivo aún debe estar en algún lugar y debe insertarse en el proceso de implementación. Esto puede ser algo que haces en tu aplicación, pero solo usarías las técnicas que resalté, quizás almacenando en otro archivo si quieres usar esto vs. app.yaml. Si entiendo la pregunta, es algo parecido a enviar una aplicación de código abierto con el secreto o producto real del creador de la biblioteca. llave.
therewillbesnacks
1
Me tomó un tiempo entenderlo, pero creo que este es el enfoque correcto. No está mezclando la configuración de la aplicación ( app.yaml) con claves secretas e información confidencial, y lo que realmente me gusta es que está utilizando el flujo de trabajo de Google para realizar la tarea. Gracias @GwynHowell. =)
Ben
1
Un enfoque similar sería colocar ese archivo JSON en una ubicación conocida en el depósito GCS predeterminado de la aplicación ( cloud.google.com/appengine/docs/standard/python/… ).
Tren España
15

Esta solución se basa en el obsoleto appcfg.py

Puede usar la opción de línea de comando -E de appcfg.py para configurar las variables de entorno cuando implemente su aplicación en GAE (actualización de 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).
...
jla
fuente
¿Puede consultar esas variables de entorno en algún lugar después de la implementación? (Espero que no.)
Ztyx
¿Hay alguna manera de pasar variables de entorno de esta manera usando la gcloudutilidad?
Trevor
6

La mayoría de las respuestas están desactualizadas. El uso del almacén de datos en la nube de Google es un poco diferente en este momento. https://cloud.google.com/python/getting-started/using-cloud-datastore

He aquí un ejemplo:

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')

Esto supone que el nombre de la entidad es 'TWITTER_APP_KEY', el tipo es 'configuración' y 'valor' es una propiedad de la entidad TWITTER_APP_KEY.

Jason F
fuente
3

Parece que puedes hacer algunos enfoques. Tenemos un problema similar y hacemos lo siguiente (adaptado a su caso de uso):

  • Cree un archivo que almacene los valores dinámicos de app.yaml y colóquelo en un servidor seguro en su entorno de compilación. Si está realmente paranoico, puede cifrar asimétricamente los valores. Incluso puede mantener esto en un repositorio privado si necesita control de versión / extracción dinámica, o simplemente use un script de shells para copiarlo / extraerlo del lugar apropiado.
  • Extraiga de git durante el script de implementación
  • Después del git pull, modifique app.yaml leyéndolo y escribiéndolo en python puro usando una biblioteca yaml

La forma más sencilla de hacerlo es utilizar un servidor de integración continua como Hudson , Bamboo o Jenkins . Simplemente agregue algún complemento, paso de guión o flujo de trabajo que realice todos los elementos anteriores que mencioné. Puede pasar variables de entorno que están configuradas en el propio Bamboo, por ejemplo.

En resumen, simplemente introduzca los valores durante su proceso de compilación en un entorno al que solo tiene acceso. Si aún no está automatizando sus compilaciones, debería hacerlo.

Otra opción es lo que dijiste, ponlo en la base de datos. Si su razón para no hacerlo es que las cosas son demasiado lentas, simplemente inserte los valores en Memcache como una caché de segunda capa y fije los valores a las instancias como una caché de primera capa. Si los valores pueden cambiar y necesita actualizar las instancias sin reiniciarlas, simplemente mantenga un hash que pueda verificar para saber cuándo cambian o activarlo de alguna manera cuando algo que haga cambie los valores. Eso debería ser todo.

habrá bocadillos
fuente
1
FWIW, este enfoque sigue más de cerca el factor de configuración en las pautas de la aplicación 12 Factor ( 12factor.net )
Tren de España
3

Debe cifrar las variables con google kms e incrustarlo en su código fuente. ( 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

coloque el valor codificado (cifrado y codificado en base64) en su variable de entorno (en el archivo yaml).

Algún código pythonish para comenzar a descifrar.

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
Anders Elton
fuente
3

La respuesta de @Jason F basada en el uso de Google Datastore es cercana, pero el código está un poco desactualizado según el uso de muestra en los documentos de la biblioteca . Aquí está el fragmento que funcionó para mí:

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'])

Inspirado en parte por esta publicación de Medium

kip2
fuente
2

Solo quería notar cómo resolví este problema en javascript / nodejs. Para el desarrollo local, utilicé el paquete npm 'dotenv' que carga variables de entorno desde un archivo .env a process.env. Cuando comencé a usar GAE, aprendí que las variables de entorno deben configurarse en un archivo 'app.yaml'. Bueno, no quería usar 'dotenv' para el desarrollo local y 'app.yaml' para GAE (y duplicar mis variables de entorno entre los dos archivos), así que escribí un pequeño script que carga las variables de entorno app.yaml en el proceso .env, para el desarrollo local. Espero que esto ayude a alguien:

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]
        })
    }
})()

Ahora incluya este archivo lo antes posible en su código y listo:

require('../yaml_env')
gbruins
fuente
¿Sigue siendo así? Porque estoy usando un .envarchivo con las variables secretas. No los estoy duplicando en mi app.yamlarchivo y mi código implementado todavía funciona. Sin .envembargo, me preocupa lo que suceda con el archivo en la nube. ¿Se cifra o algo así? ¿Cómo puedo asegurarme de que nadie acceda a las .envvariables de archivo de gcloud una vez implementado?
Gus
Esto no es necesario en absoluto porque GAE agrega automáticamente todas las variables definidas en el archivo app.yaml al entorno del nodo. Básicamente, esto es lo mismo que hace dotenv con las variables definidas en el paquete .env. Pero me pregunto cómo debe configurar el CD, ya que no puede enviar app.yaml con env vars a un VCS o una canalización ...
Jornve
1

Ampliando la respuesta 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
JSBach
fuente
1

Existe un paquete de pypi llamado gae_env que te permite guardar las variables de entorno de appengine en Cloud Datastore. Debajo del capó, también usa Memcache, por lo que es rápido

Uso:

import gae_env

API_KEY = gae_env.get('API_KEY')

Si hay un valor para esa clave en el almacén de datos, se devolverá. Si no lo hay, se __NOT_SET__creará un registro de marcador de posición y se ValueNotSetErrorlanzará un . La excepción le recordará que vaya a Developers Console y actualice el registro de marcador de posición.


Similar a la respuesta de Martin, aquí se explica cómo actualizar el valor de la clave en Datastore:

  1. Vaya a la sección Datastore en la consola de desarrolladores

  2. Seleccione su proyecto en la parte superior de la página si aún no está seleccionado.

  3. En el cuadro desplegable Tipo , seleccione GaeEnvSettings.

  4. Las claves para las que se generó una excepción tendrán valor __NOT_SET__.

Su configuración, creada por la clase Configuración

Haz click para editar

Ingrese el valor real y ahorre


Vaya a la página de GitHub del paquete para obtener más información sobre el uso / configuración

Príncipe Odame
fuente