使用app.yaml在GAE中安全地存储环境变量

时间:2014-03-26 18:08:24

标签: python google-app-engine python-2.7 environment-variables

我需要在app.yaml中存储API密钥和其他敏感信息作为环境变量,以便在GAE上进行部署。这个问题是如果我将app.yaml推送到GitHub,这个信息就会公开(不好)。我不想将信息存储在数据存储区中,因为它不适合项目。相反,我想在应用程序的每个部署中交换.gitignore中列出的文件中的值。

这是我的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}

有什么想法吗?

14 个答案:

答案 0 :(得分:38)

如果是敏感数据,则不应将其存储在源代码中,因为它会被检入源代码管理中。错误的人(组织内部或外部)可能会在那里找到它。此外,您的开发环境可能使用生产环境中的不同配置值。如果这些值存储在代码中,则必须在开发和生产中运行不同的代码,这是一种混乱和不良的做法。

在我的项目中,我使用此类将配置数据放在数据存储区中:

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

您的应用程序会执行此操作以获取值:

API_KEY = Settings.get('API_KEY')

如果数据存储区中存在该键的值,您将获得该值。如果没有,将创建占位符记录并抛出异常。该异常将提醒您转到开发人员控制台并更新占位符记录。

我发现这可以避免设置配置值。如果您不确定要设置的配置值,只需运行代码即可告诉您!

上面的代码使用了ndb库,它使用了内存中的memcache和数据存储区,所以它很快。

<强>更新

jelder 询问如何在App Engine控制台中查找数据存储区值并进行设置。方法如下:

  1. 转到https://console.cloud.google.com/datastore/

  2. 如果项目尚未被选中,请选择页面顶部的项目。

  3. Kind 下拉框中,选择设置

  4. 如果您运行上面的代码,您的密钥就会显示出来。它们都具有值 NOT SET 。单击每个并设置其值。

  5. 希望这有帮助!

    Your settings, created by the Settings class

    Click to edit

    Enter the real value and save

答案 1 :(得分:19)

发布时不存在此功能,但对于在这里偶然发现的其他人,Google现在提供一项名为Secret Manager的服务。

这是一个简单的REST服务(当然,使用SDK包装),可以将您的机密存储在Google云平台上的安全位置。这是比数据存储更好的方法,它需要额外的步骤来查看存储的机密并具有更细粒度的权限模型-如果需要,您可以针对项目的不同方面以不同的方式保护单个机密。

它提供了版本控制功能,因此您可以相对轻松地处理密码更改,以及强大的查询和管理层,使您可以在必要时在运行时发现和创建机密。

Python SDK

用法示例:

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

答案 2 :(得分:18)

我的方法是在App Engine应用程序本身中存储客户机密码 。客户端机密既不在源代码控制中,也不在任何本地计算机上。这样做的好处是任何 App Engine协作者都可以部署代码更改,而无需担心客户端机密。

我将客户端机密直接存储在数据存储区中,并使用Memcache来改善访问机密的延迟。数据存储区实体只需创建一次,并将在未来的部署中保留。当然,App Engine控制台可以随时用于更新这些实体。

执行一次性实体创建有两个选项:

  • 使用App Engine Remote API交互式shell创建实体。
  • 创建一个仅限Admin的处理程序,用于初始化具有虚拟值的实体。手动调用此管理处理程序,然后使用App Engine控制台更新具有生产客户机密钥的实体。

答案 3 :(得分:16)

最好的方法是将密钥存储在client_secrets.json文件中,并通过将其列在.gitignore文件中将其排除在上传到git之外。如果您为不同的环境使用不同的密钥,则可以使用app_identity api来确定应用程序ID是什么,并正确加载。

这里有一个相当全面的例子 - &gt; https://developers.google.com/api-client-library/python/guide/aaa_client_secrets

以下是一些示例代码:

# 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()

答案 4 :(得分:15)

您可以使用appcfg.py的-E命令行选项在将应用程序部署到GAE时设置环境变量(appcfg.py update)

$ 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).
...

答案 5 :(得分:12)

此解决方案很简单,但可能不适合所有不同的团队。

首先,将环境变量放入 env_variables.yaml 中,例如

env_variables:
  SECRET: 'my_secret'

然后,将此env_variables.yaml放入app.yaml

includes:
  - env_variables.yaml

最后,将env_variables.yaml添加到.gitignore,以使秘密变量在存储库中不存在。

在这种情况下,env_variables.yaml需要在部署管理器之间共享。

答案 6 :(得分:3)

听起来你可以采取一些方法。我们有类似的问题并执行以下操作(适应您的用例):

  • 创建一个存储任何动态app.yaml值的文件,并将其放在构建环境中的安全服务器上。如果你真的是偏执狂,你可以非对称地加密这些值。如果您需要版本控制/动态拉动,或者只是使用shell脚本将其复制/从适当的位置拉出来,您甚至可以将其保存在私人仓库中。
  • 在部署脚本中从git中提取
  • 在git pull之后,通过使用yaml库在纯python中读取和编写来修改app.yaml

最简单的方法是使用持续集成服务器,例如HudsonBambooJenkins。只需添加一些插件,脚本步骤或工作流程,完成我提到的所有上述项目。例如,您可以传入在Bamboo本身中配置的环境变量。

总之,只需在构建过程中在您只能访问的环境中输入值。如果您还没有自动化构建,那么您应该是。

另一个选项是你说的,把它放在数据库中。如果您不这样做的原因是事情太慢,只需将值作为第二层缓存推送到memcache中,并将值作为第一层缓存固定到实例。如果值可以更改并且您需要更新实例而不重新启动它们,只需保留一个哈希值,您可以检查它们何时更改或以某种方式触发它时更改值。应该是它。

答案 7 :(得分:2)

基于Google数据存储的

@Jason F的answer已经接近,但是根据library docs上的示例用法,代码有些过时了。这是对我有用的代码段:

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

部分受此Medium post

的启发

答案 8 :(得分:1)

只是想知道我是如何在javascript / nodejs中解决这个问题的。对于本地开发,我使用了&#39; dotenv&#39; npm包,它将.env文件中的环境变量加载到process.env中。当我开始使用GAE时,我了解到需要在app.yaml&#39;中设置环境变量。文件。好吧,我不想使用&#39; dotenv&#39;本地开发和app.yaml&#39;对于GAE(并在两个文件之间复制我的环境变量),所以我写了一个小脚本,将app.yaml环境变量加载到process.env中,用于本地开发。希望这有助于某人:

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

现在尽早在您的代码中包含此文件,并且您已完成:

require('../yaml_env')

答案 9 :(得分:1)

扩展马丁的答案

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

答案 10 :(得分:1)

有一个名为gae_env的pypi程序包,可用于将Appengine环境变量保存在Cloud Datastore中。在后台,它还使用了Memcache,因此速度很快

用法:

import gae_env

API_KEY = gae_env.get('API_KEY')

如果数据存储区中有该键的值,则将其返回。 如果没有,将创建一个占位符记录 __NOT_SET__ 并抛出ValueNotSetError。该异常会提醒您转到Developers Console并更新占位符记录。


类似于Martin的答案,这是如何更新数据存储区中键的值:

  1. 在开发人员控制台中转到Datastore Section

  2. 如果尚未选择项目,则在页面顶部选择它。

  3. 种类下拉框中,选择GaeEnvSettings

  4. 引发异常的键的值为__NOT_SET__

Your settings, created by the Settings class

Click to edit

Enter the real value and save


转到package's GitHub page了解有关使用/配置的更多信息

答案 11 :(得分:0)

大多数答案已过时。实际上,现在使用Google Cloud Datastore有点不同。 https://cloud.google.com/python/getting-started/using-cloud-datastore

这是一个例子:

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

这假定实体名称为“ TWITTER_APP_KEY”,种类为“设置”,并且“值”为TWITTER_APP_KEY实体的属性。

答案 12 :(得分:0)

您应该使用google kms加密变量,并将其嵌入到源代码中。 (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

将加扰后的值(经过加密和base64编码)放入您的环境变量(在yaml文件中)。

一些Python式代码可帮助您开始解密。

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

答案 13 :(得分:0)

使用 github 操作而不是谷歌云触发器(谷歌云触发器无法找到自己的 app.yaml 并自行管理该死的环境变量。)

操作方法如下:

我的环境: 应用引擎, 标准(非弹性), Nodejs Express 应用程序, 一个 PostgreSQL CloudSql

首先设置:

1. Create a new Google Cloud Project (or select an existing project).

2. Initialize your App Engine app with your project.

[Create a Google Cloud service account][sa] or select an existing one.

3. Add the the following Cloud IAM roles to your service account:

    App Engine Admin - allows for the creation of new App Engine apps

    Service Account User - required to deploy to App Engine as service account

    Storage Admin - allows upload of source code

    Cloud Build Editor - allows building of source code

[Download a JSON service account key][create-key] for the service account.

4. Add the following [secrets to your repository's secrets][gh-secret]:

    GCP_PROJECT: Google Cloud project ID

    GCP_SA_KEY: the downloaded service account key

app.yaml

runtime: nodejs14
env: standard
env_variables:
  SESSION_SECRET: $SESSION_SECRET
beta_settings:
  cloud_sql_instances: SQL_INSTANCE

然后是github动作

name: Build and Deploy to GKE

on: push

env:
  PROJECT_ID: ${{ secrets.GKE_PROJECT }}
  DATABASE_URL: ${{ secrets.DATABASE_URL}}
jobs:
  setup-build-publish-deploy:
    name: Setup, Build, Publish, and Deploy
    runs-on: ubuntu-latest

steps:
 - uses: actions/checkout@v2
 - uses: actions/setup-node@v2
   with:
    node-version: '12'
 - run: npm install
 - uses: actions/checkout@v1
 - uses: ikuanyshbekov/app-yaml-env-compiler@v1.0
   env:
    SESSION_SECRET: ${{ secrets.SESSION_SECRET }}  
 - shell: bash
   run: |
        sed -i 's/SQL_INSTANCE/'${{secrets.DATABASE_URL}}'/g' app.yaml
 - uses: actions-hub/gcloud@master
   env:
    PROJECT_ID: ${{ secrets.GKE_PROJECT }}
    APPLICATION_CREDENTIALS: ${{ secrets.GCLOUD_AUTH }}
    CLOUDSDK_CORE_DISABLE_PROMPTS: 1
   with:
    args: app deploy app.yaml

要将秘密添加到 github 操作中,您必须转到:设置/秘密

请注意,我可以使用 bash 脚本处理所有替换。所以我不会依赖github项目“ikuanyshbekov/app-yaml-env-compiler@v1.0”

很遗憾 GAE 没有提供一种最简单的方法来处理 app.yaml 的环境变量。我不想使用 KMS,因为我需要更新 beta-settings/cloud sql 实例。我真的需要将所有内容替换到 app.yaml 中。

通过这种方式,我可以针对正确的环境执行特定操作并管理机密。