Securely storing environment variables in GAE with app.yaml

我需要将 API 密钥和其他敏感信息存储在 app.yaml中,作为部署在 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}

有什么想法吗?

54222 次浏览

最好的方法是将密钥存储在 client _ secret 中。Json 文件,并通过在您的。吉蒂诺档案。如果对于不同的环境有不同的键,那么可以使用 app _ Identity api 来确定 app id 是什么,并适当地加载。

这里有一个相当全面的例子-> 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()

听起来你可以采取一些方法。我们有一个类似的问题,并执行以下操作(根据您的用例进行调整) :

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

最简单的方法是使用连续集成服务器,如 哈德森竹子Jenkins。只需添加一些插件、脚本步骤或工作流来完成上面提到的所有项目。例如,您可以传入在竹子本身中配置的环境变量。

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

另一个选项是您所说的,将其放入数据库中。如果不这样做的原因是速度太慢,只需将值作为第二层缓存推送到 memcache 中,并将值作为第一层缓存固定到实例中。如果值可以更改,并且您需要更新实例而不需要重新启动它们,那么只需保留一个散列,以便在它们更改时进行检查,或者在您确实更改了值时以某种方式触发它。应该就是这样。

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

我直接在 Datastore 存储客户机密,并使用 Memcache 提高访问机密的延迟。Datastore 实体只需要创建一次,并将在以后的部署中持久化。当然,App Engine 控制台可以随时更新这些实体。

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

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

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

在我的项目中,我使用这个类将配置数据放到数据存储中:

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

如果数据存储中有该键的值,您将获得它。如果没有,将创建一个占位符记录,并引发异常。此异常将提醒您转到 Developers Console 并更新占位符记录。

我发现这样可以省去设置配置值时的猜测。如果您不确定要设置什么配置值,只需运行代码,它就会告诉您!

上面的代码使用了 ndb 库,它使用 memcache 和底层的数据存储,因此速度很快。


更新:

Jelder 询问如何在 App Engine 控制台中查找和设置 Datastore 值:

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

  2. 如果您的项目尚未选定,请在页面顶部选择它。

  3. 善良下拉框中,选择 Settings

  4. 如果您运行上面的代码,您的密钥将显示出来。它们的值都是 没有设置。单击每一个并设置其值。

Hope this helps!

Your settings, created by the Settings class

Click to edit

Enter the real value and save

此解决方案依赖于已废弃的 appcfg.py

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

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

Just wanted to note how I solved this problem in javascript/nodejs. For local development I used the 'dotenv' npm package which loads environment variables from a .env file into process.env. When I started using GAE I learned that environment variables need to be set in a 'app.yaml' file. Well, I didn't want to use 'dotenv' for local development and 'app.yaml' for GAE (and duplicate my environment variables between the two files), so I wrote a little script that loads app.yaml environment variables into process.env, for local development. Hope this helps someone:

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

扩展马丁的回答

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

There is a pypi package called 你好 that allows you to save appengine environment variables in Cloud Datastore. Under the hood, it also uses Memcache so its fast

用法:

import gae_env


API_KEY = gae_env.get('API_KEY')

如果数据存储中存在该键的值,则将返回该值。 如果没有,则将创建占位符记录 __NOT_SET__并抛出 ValueNotSetError。此异常将提醒您转到 开发者控制台并更新占位符记录。


与 Martin 的回答类似,下面是如何更新 Datastore 中键的值:

  1. 在开发人员控制台中转到 数据存储部分

  2. 如果您的项目尚未选定,请在页面顶部选择它。

  3. In the 善良 dropdown box, select GaeEnvSettings.

  4. 引发异常的键将具有值 __NOT_SET__

Your settings, created by the Settings class

Click to edit

Enter the real value and save


转到 软件包的 GitHub 页面获取更多关于使用/配置的信息

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

首先,将环境变量放在 Env _ variable. yaml中,例如,

env_variables:
SECRET: 'my_secret'

然后,在 app.yaml中包含这个 env_variables.yaml

includes:
- env_variables.yaml

最后,将 env_variables.yaml添加到 .gitignore,这样秘密变量就不会存在于存储库中。

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

大多数答案都过时了。现在使用谷歌云数据存储实际上有点不同

这里有一个例子:

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 实体的属性。

你应该加密的变量与谷歌 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 文件)。

一些蟒蛇密码,让你开始解密。

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

@ Jason F 的基于 Google Datastore 的 回答已经很接近了,但是基于 图书馆文件上的样例使用情况,代码有点过时了。下面是对我有用的片段:

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

部分灵感来自 中柱

这在你发帖的时候还不存在,但是对于其他在这里跌跌撞撞的人来说,Google 现在提供了一项名为 秘密经理的服务。

这是一个简单的 REST 服务(当然包含 SDK) ,可以将您的秘密存储在谷歌云平台上的一个安全位置。这是一种比 Data Store 更好的方法,需要额外的步骤来查看存储的秘密并拥有更细粒度的权限模型——如果需要,您可以针对项目的不同方面以不同的方式保护个人秘密。

它提供了版本控制,因此您可以相对轻松地处理密码更改,还提供了一个健壮的查询和管理层,使您能够在运行时发现和创建秘密(如果需要的话)。

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_version_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

用 github 操作代替 Google 云触发器(Google 云触发器无法找到自己的 app.yaml 并自己管理该死的环境变量)

下面是如何做到这一点:

我的环境: App engine, 标准(非弹性) , 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

The 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-Editor@v1.0”

遗憾的是,GAE 没有为 app.yaml 提供一种简单的环境变量处理方法。我不想使用 KMS,因为我需要更新 beta 设置/Cloud sql 实例。.我真的需要把所有东西都替换到 app.yaml 里。

这样我就可以在合适的环境下做出具体的行动,管理秘密。

我的解决方案是通过 github action 和 github secret 替换 app.yaml 文件中的 secret。

Yaml (App Engine)

env_variables:
SECRET_ONE: $SECRET_ONE
ANOTHER_SECRET: $ANOTHER_SECRET

workflow.yaml (Github)

steps:
- uses: actions/checkout@v2
- uses: 73h/gae-app-yaml-replace-env-variables@v0.1
env:
SECRET_ONE: $\{\{ secrets.SECRET_ONE }}
ANOTHER_SECRET: $\{\{ secrets.ANOTHER_SECRET }}

在这里,您可以找到 Github 操作。
Https://github.com/73h/gae-app-yaml-replace-env-variables

在本地开发时,我将秘密写入一个. env 文件。