Armazenamento seguro de variáveis ​​de ambiente no GAE com app.yaml


97

Preciso armazenar chaves de API e outras informações confidenciais app.yamlcomo variáveis ​​de ambiente para implantação no GAE. O problema com isso é que se eu enviar app.yamlpara o GitHub, essa informação se tornará pública (não é boa). Não quero armazenar as informações em um armazenamento de dados, pois não se adequa ao projeto. Em vez disso, gostaria de trocar os valores de um arquivo que está listado em .gitignorecada implantação do aplicativo.

Aqui está meu arquivo 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}

Alguma ideia?


72
Gostaria que o GAE adicionasse a opção de definir variáveis ​​de env da instância por meio do console do desenvolvedor (como qualquer outro PaaS com o qual estou familiarizado).
Espanha Train

4
Você pode usar o armazenamento de dados. Consulte esta resposta: stackoverflow.com/a/35254560/1027846
Mustafa İlhan

Expandindo o comentário de mustilica acima sobre o uso do armazenamento de dados. Veja minha resposta abaixo para o código que uso em meus projetos para fazer isso: stackoverflow.com/a/35261091#35261091 . Na verdade, ele permite que você edite variáveis ​​de ambiente no console do desenvolvedor e os valores de espaço reservado são criados automaticamente.
Martin Omander

Obrigado Mustilica e Martin. Na verdade, estamos usando a abordagem de armazenamento de dados há algum tempo e concordo que é a melhor solução para esse problema. Mais fácil de fazer com uma configuração de CI / CD do que com a abordagem de arquivo json, IMO.
Espanha Train

1
2019 e o GAE ainda não corrigiram esse problema: /
Josh Noe

Respostas:


53

Se forem dados confidenciais, você não deve armazená-los no código-fonte, pois eles serão verificados no controle de origem. As pessoas erradas (dentro ou fora da sua organização) podem encontrá-lo lá. Além disso, seu ambiente de desenvolvimento provavelmente usa valores de configuração diferentes de seu ambiente de produção. Se esses valores forem armazenados no código, você terá que executar códigos diferentes no desenvolvimento e na produção, o que é uma prática confusa e inadequada.

Em meus projetos, coloco dados de configuração no armazenamento de dados usando esta 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

Seu aplicativo faria isso para obter um valor:

API_KEY = Settings.get('API_KEY')

Se houver um valor para essa chave no armazenamento de dados, você o obterá. Se não houver, um registro de espaço reservado será criado e uma exceção será lançada. A exceção o lembrará de ir para o Developers Console e atualizar o registro de espaço reservado.

Acho que isso elimina a suposição de definir valores de configuração. Se você não tiver certeza de quais valores de configuração definir, apenas execute o código e ele lhe dirá!

O código acima usa a biblioteca ndb que usa memcache e o armazenamento de dados por baixo do capô, por isso é rápido.


Atualizar:

jelder perguntou como encontrar os valores do Datastore no console do App Engine e defini-los. Aqui está como:

  1. Acesse https://console.cloud.google.com/datastore/

  2. Selecione seu projeto no topo da página se ainda não estiver selecionado.

  3. Na caixa suspensa Tipo , selecione Configurações .

  4. Se você executou o código acima, suas chaves aparecerão. Todos eles terão o valor NOT SET . Clique em cada um e defina seu valor.

Espero que isto ajude!

Suas configurações, criadas pela classe Configurações

Clique para editar

Insira o valor real e salve


2
De todas as respostas fornecidas, esta parece ser a mais próxima de como Heroku lida com as coisas. Sendo um tanto novo no GAE, não entendo muito bem onde no Developers Console encontrar o registro de espaço reservado. Você pode explicar ou, para ganhar pontos extras, postar capturas de tela?
jelder

2
dam ~ ... com todo o respeito ao gcloud, parece muito ruim ter que usar outro serviço para essa necessidade específica. Além disso, o Google fornece uma abordagem "100% -herokuish" para env vars dentro de funções do firebase, mas não para funções gcloud (pelo menos não documentadas ... se não estou errado)
Ben

1
Aqui está um resumo baseado em sua abordagem que adiciona exclusividade e fallback de variável de ambiente - gist.github.com/SpainTrain/6bf5896e6046a5d9e7e765d0defc8aa8
Trem da Espanha

3
As funções do @Ben não Firebase são compatíveis com env vars (agora, pelo menos).
NReilingh

3
@obl - um aplicativo do App Engine é autenticado automaticamente em seu próprio armazenamento de dados, sem detalhes de autenticação necessários. É muito legal :-)
Martin Omander

46

Esta solução é simples, mas pode não se adequar a todas as equipes diferentes.

Primeiro, coloque as variáveis ​​de ambiente em env_variables.yaml , por exemplo,

env_variables:
  SECRET: 'my_secret'

Então, inclua isso env_variables.yamlnoapp.yaml

includes:
  - env_variables.yaml

Finalmente, adicione o env_variables.yamla .gitignore, de modo que as variáveis ​​secretas não existam no repositório.

Neste caso, o env_variables.yaml precisa ser compartilhado entre os gerenciadores de implementação.


1
Apenas para adicionar o que pode não ser óbvio para alguns, suas variáveis ​​de ambiente seriam encontradas em process.env.MY_SECRET_KEYe se você precisar dessas variáveis ​​de ambiente em seu ambiente de desenvolvimento local, você pode usar o dotenvpacote de nó
Dave Kiss

2
Como env_variables.yamlconseguiria todas as instâncias é uma peça que faltava no quebra-cabeça.
Christopher Oezbek

Além disso: como usar isso localmente?
Christopher Oezbek

@ChristopherOezbek 1. Como implantar? Use gcloud app deploycomo você normalmente faz para implantar no Google Cloud. 2. Como definir variáveis ​​de ambiente secretas localmente? Existem muitos caminhos. Você pode apenas usar exportno prompt de comando ou usar qualquer ferramenta como @DaveKiss sugerido.
Shih-Wen Su

Esta é a solução mais simples. Os segredos podem ser acessados ​​em seu aplicativo via os.environ.get('SECRET').
Quinn Comendant

19

Minha abordagem é armazenar os segredos do cliente apenas no próprio aplicativo do App Engine. Os segredos do cliente não estão no controle de origem nem em nenhum computador local. Isso tem a vantagem de que qualquer colaborador do App Engine pode implantar alterações de código sem ter que se preocupar com os segredos do cliente.

Eu armazeno os segredos do cliente diretamente no Datastore e uso o Memcache para melhorar a latência ao acessar os segredos. As entidades do Datastore precisam ser criadas apenas uma vez e persistirão em implantações futuras. é claro que o console do App Engine pode ser usado para atualizar essas entidades a qualquer momento.

Existem duas opções para executar a criação única da entidade:

  • Use o shell interativo da API remota do App Engine para criar as entidades.
  • Crie um manipulador somente Admin que inicializará as entidades com valores fictícios. Chame manualmente esse gerenciador de administrador e, em seguida, use o console do App Engine para atualizar as entidades com os segredos do cliente de produção.

7
Não é nada complicado. Obrigado app engine.
courtimas

16

A melhor maneira de fazer isso é armazenar as chaves em um arquivo client_secrets.json e excluí-lo do upload para o git, listando-o em seu arquivo .gitignore. Se você tiver chaves diferentes para ambientes diferentes, você pode usar a api app_identity para determinar qual é o id do aplicativo e carregar de forma adequada.

Há um exemplo bastante abrangente aqui -> https://developers.google.com/api-client-library/python/guide/aaa_client_secrets .

Aqui está um exemplo de código:

# 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
Definitivamente, na direção certa, mas isso não resolve o problema de trocar os valores na app.yamlimplantação do aplicativo. Alguma ideia aí?
Ben

1
Portanto, tenha um arquivo client_secrets diferente para cada ambiente. Por exemplo, client_secrets_live.json, client_secrets_dev.json, client_secrets_pilot.json etc, use a lógica python para determinar em qual servidor você está e carregue o arquivo json apropriado. O método app_identity.get_application_id () pode ser útil para detectar automaticamente em qual servidor você está. É esse o tipo de coisa que você quer dizer?
Gwyn Howell

@BenGrunfeld veja minha resposta. Minha solução faz exatamente isso. Não vejo como essa resposta resolve a questão. Presumo que o objetivo seja manter a configuração secreta fora do git e usar o git como parte da implantação. Aqui, esse arquivo ainda precisa estar em algum lugar e colocado no processo de implantação. Isso pode ser algo que você faz em seu aplicativo, mas você apenas usaria as técnicas que destaquei, talvez armazenando em outro arquivo se quiser usar this vs. app.yaml. Se entendi a pergunta, é algo semelhante a enviar um aplicativo de código aberto com o segredo ou produto do cliente real do fabricante da biblioteca. chave.
therewillbesnacks

1
Demorei um pouco para entender, mas acho que essa é a abordagem correta. Você não está misturando configurações de aplicativos ( app.yaml) com chaves secretas e informações confidenciais, e o que eu realmente gosto é que você está usando o fluxo de trabalho do Google para realizar a tarefa. Obrigado @GwynHowell. =)
Ben

1
Uma abordagem semelhante seria colocar esse arquivo JSON em um local conhecido no intervalo GCS padrão do aplicativo ( cloud.google.com/appengine/docs/standard/python/… ).
Trem da Espanha

16

Isso não existia quando você postou, mas para qualquer outra pessoa que tropeçar aqui, o Google agora oferece um serviço chamado Secret Manager .

É um serviço REST simples (com SDKs que o envolvem, é claro) para armazenar seus segredos em um local seguro na plataforma em nuvem do Google. Esta é uma abordagem melhor do que o armazenamento de dados, exigindo etapas extras para ver os segredos armazenados e tendo um modelo de permissão mais refinado - você pode proteger segredos individuais de maneira diferente para diferentes aspectos do seu projeto, se necessário.

Ele oferece controle de versão, para que você possa lidar com alterações de senha com relativa facilidade, bem como uma consulta robusta e camada de gerenciamento que permite descobrir e criar segredos em tempo de execução, se necessário.

SDK Python

Exemplo de uso:

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
Esta deve ser a nova resposta correta. O Secret Manager ainda está em Beta, mas este é o caminho a seguir ao trabalhar com variáveis ​​de ambiente.
King Leon

@KingLeon, isso significaria ter que refatorar um monte de os.getenv('ENV_VAR')s?
Alejandro

Eu coloquei um código semelhante ao acima em uma função, então uso algo como SECRET_KEY = env('SECRET_KEY', default=access_secret_version(GOOGLE_CLOUD_PROJECT_ID, 'SECRET_KEY', 1)). Configurando o padrão para usar oaccess_secret_version
King Leon

Além disso, estou usando o django-amb. github.com/joke2k/django-environ
Rei Leão

15

Esta solução depende do appcfg.py obsoleto

Você pode usar a opção de linha de comando -E de appcfg.py para configurar as variáveis ​​de ambiente ao implantar seu aplicativo no GAE (atualização 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).
...

Você pode consultar essas variáveis ​​de ambiente em algum lugar após a implantação? (Espero que não.)
Ztyx

Existe uma maneira de passar variáveis ​​de ambiente dessa forma usando o gcloudutilitário?
Trevor

6

A maioria das respostas está desatualizada. Usar o armazenamento de dados em nuvem do Google é um pouco diferente agora. https://cloud.google.com/python/getting-started/using-cloud-datastore

Aqui está um exemplo:

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

Isso assume que o nome da entidade é 'TWITTER_APP_KEY', o tipo é 'configurações' e 'valor' é uma propriedade da entidade TWITTER_APP_KEY.


3

Parece que você pode fazer algumas abordagens. Temos um problema semelhante e fazemos o seguinte (adaptado ao seu caso de uso):

  • Crie um arquivo que armazene quaisquer valores dinâmicos de app.yaml e coloque-o em um servidor seguro em seu ambiente de construção. Se você for realmente paranóico, pode criptografar os valores de forma assimétrica. Você pode até mesmo mantê-lo em um repositório privado se precisar de controle de versão / pull dinâmico ou apenas usar um script shells para copiá-lo / retirá-lo do local apropriado.
  • Puxe do git durante o script de implantação
  • Após o git pull, modifique o app.yaml lendo e gravando-o em Python puro usando uma biblioteca yaml

A maneira mais fácil de fazer isso é usar um servidor de integração contínua, como Hudson , Bamboo ou Jenkins . Basta adicionar algum plug-in, etapa de script ou fluxo de trabalho que faça todos os itens acima mencionados. Você pode passar variáveis ​​de ambiente que são configuradas no próprio Bamboo, por exemplo.

Em resumo, basta inserir os valores durante o processo de construção em um ambiente ao qual você só tem acesso. Se você ainda não está automatizando suas compilações, deveria estar.

Outra opção de opção é o que você disse, coloque no banco de dados. Se o motivo para não fazer isso for porque as coisas estão muito lentas, simplesmente envie os valores para o memcache como um cache de segunda camada e fixe os valores nas instâncias como um cache de primeira camada. Se os valores podem mudar e você precisa atualizar as instâncias sem reinicializá-los, apenas mantenha um hash que você pode verificar para saber quando eles mudam ou acioná-lo de alguma forma quando algo que você faz muda os valores. Deve ser isso.


1
FWIW, essa abordagem segue mais de perto o fator de configuração nas diretrizes do aplicativo 12 Factor ( 12factor.net )
Espanha Train

3

Você deve criptografar as variáveis ​​com google kms e incorporá-las ao seu código-fonte. ( 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 o valor embaralhado (criptografado e codificado em base64) em sua variável de ambiente (no arquivo yaml).

Algum código pythonish para você começar a descriptografar.

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

@Jason F's resposta de base no uso do Google Datastore está próxima, mas o código está um pouco desatualizado com base no uso de amostra nos documentos da biblioteca . Aqui está o snippet que funcionou para mim:

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

Parcialmente inspirado por isso postagem do Medium


2

Só queria observar como resolvi esse problema em javascript / nodejs. Para o desenvolvimento local, usei o pacote npm 'dotenv' que carrega variáveis ​​de ambiente de um arquivo .env em process.env. Quando comecei a usar o GAE, aprendi que as variáveis ​​de ambiente precisam ser definidas em um arquivo 'app.yaml'. Bem, eu não queria usar 'dotenv' para desenvolvimento local e 'app.yaml' para GAE (e duplicar minhas variáveis ​​de ambiente entre os dois arquivos), então escrevi um pequeno script que carrega variáveis ​​de ambiente app.yaml no processo .env, para o desenvolvimento local. Espero que isso ajude alguém:

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

Agora inclua esse arquivo o mais cedo possível em seu código e pronto:

require('../yaml_env')

Este ainda é o caso? Porque estou usando um .envarquivo com as variáveis ​​secretas. Não os estou duplicando em meu app.yamlarquivo e meu código implantado ainda funciona. Estou preocupado com o que acontece com o .envarquivo na nuvem. É criptografado ou algo assim? Como posso garantir que ninguém acesse as .envvariáveis ​​do arquivo gcloud depois que ele for implantado?
Gus

Isso não é necessário porque o GAE adiciona automaticamente todas as variáveis ​​definidas no arquivo app.yaml ao ambiente do nó. Basicamente, isso é o mesmo que o dotenv faz com as variáveis ​​definidas no pacote .env. Mas estou me perguntando como você tem que configurar o CD, já que você não pode enviar app.yaml com env vars para um VCS ou um pipeline ...
Jornve

1

Ampliando a resposta 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

Há um pacote pypi chamado gae_env que permite salvar variáveis ​​de ambiente do appengine no Cloud Datastore. Nos bastidores, ele também usa Memcache, então é rápido

Uso:

import gae_env

API_KEY = gae_env.get('API_KEY')

Se houver um valor para essa chave no armazenamento de dados, ele será retornado. Se não houver, um registro de espaço reservado __NOT_SET__será criado e um ValueNotSetErrorserá lançado. A exceção o lembrará de ir para o Developers Console e atualizar o registro de espaço reservado.


Semelhante à resposta de Martin, aqui está como atualizar o valor da chave no Datastore:

  1. Acesse a seção Datastore no console de desenvolvedores

  2. Selecione seu projeto no topo da página se ainda não estiver selecionado.

  3. Na caixa suspensa Tipo , selecione GaeEnvSettings.

  4. Chaves para as quais uma exceção foi levantada terão valor __NOT_SET__.

Suas configurações, criadas pela classe Configurações

Clique para editar

Insira o valor real e salve


Vá para a página GitHub do pacote para obter mais informações sobre uso / configuração

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.