Безопасное хранение переменных среды в GAE с помощью app.yaml

101

Мне нужно хранить ключи 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}

Любые идеи?

Бен
источник
77
Я бы хотел, чтобы GAE добавила возможность устанавливать переменные env экземпляра через консоль разработчика (как и любой другой PaaS, с которым я знаком).
Spain Train
5
Вы можете использовать хранилище данных. См. Этот ответ: stackoverflow.com/a/35254560/1027846
Мустафа Ильхан
Расширение комментария mustilica выше об использовании хранилища данных. См. Мой ответ ниже для кода, который я использую в своих проектах для этого: stackoverflow.com/a/35261091#35261091 . По сути, он позволяет редактировать переменные среды из консоли разработчика, а значения-заполнители создаются автоматически.
Мартин Омандер
Спасибо mustilica и Мартину. Мы действительно какое-то время использовали подход хранилища данных, и я согласен, что это лучшее решение этой проблемы. Легче сделать с настройкой CI / CD, чем с использованием файла json, IMO.
Spain Train
1
2019 и GAE до сих пор не исправили эту проблему: /
Джош Ноэ

Ответы:

54

Если это конфиденциальные данные, вы не должны хранить их в исходном коде, поскольку они будут проверены в системе управления версиями. Не те люди (внутри или вне вашей организации) могут найти его там. Кроме того, ваша среда разработки, вероятно, использует другие значения конфигурации из вашей производственной среды. Если эти значения хранятся в коде, вам придется запускать другой код при разработке и производстве, что является беспорядочной и плохой практикой.

В своих проектах я помещаю данные конфигурации в хранилище данных с помощью этого класса:

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 и хранилище данных под капотом, так что это быстро.


Обновить:

Джелдер спросил, как найти значения хранилища данных в консоли App Engine и установить их. Вот как:

  1. Перейдите на https://console.cloud.google.com/datastore/.

  2. Выберите свой проект вверху страницы, если он еще не выбран.

  3. В раскрывающемся списке Тип выберите Настройки .

  4. Если вы запустили приведенный выше код, ваши ключи появятся. Все они будут иметь значение NOT SET . Щелкните каждый из них и установите его значение.

Надеюсь это поможет!

Ваши настройки, созданные классом Settings

Нажмите, чтобы редактировать

Введите реальную стоимость и сохраните

Мартин Омандер
источник
3
Из всех предоставленных ответов это кажется наиболее близким к тому, как Heroku обрабатывает вещи. Поскольку я новичок в GAE, я не совсем понимаю, где в Developers Console найти запись-заполнитель. Можете объяснить или за бонусные баллы выложить скриншоты?
jelder
3
dam ~… при всем уважении к gcloud, похоже, довольно плохо использовать другую службу для этой конкретной потребности. Кроме того, Google предлагает "100% -ый героический" подход для переменных env в функциях firebase, но не для функций gcloud (по крайней мере, недокументированных ... если я не ошибаюсь)
Бен
2
Вот суть, основанная на вашем подходе, который добавляет уникальность и резервное значение переменных среды - gist.github.com/SpainTrain/6bf5896e6046a5d9e7e765d0defc8aa8
Spain Train
3
Функции @Ben, отличные от Firebase, действительно поддерживают переменные env (по крайней мере, сейчас).
NReilingh 01
3
@obl - приложение App Engine автоматически аутентифицируется в собственном хранилище данных, никаких данных аутентификации не требуется. Это довольно здорово :-)
Мартин Омандер
57

Это простое решение, которое может подойти не всем командам.

Сначала поместите переменные среды в env_variables.yaml , например,

env_variables:
  SECRET: 'my_secret'

Затем включите это env_variables.yamlвapp.yaml

includes:
  - env_variables.yaml

Наконец, добавьте env_variables.yamlк .gitignore, так что секретные переменные не будут существовать в хранилище.

В этом случае env_variables.yamlнеобходимо разделить между менеджерами развертывания.

Ши-Вен Су
источник
2
Просто чтобы добавить то, что может быть неочевидным для некоторых, затем будут найдены ваши переменные среды, process.env.MY_SECRET_KEYи если вам нужны эти переменные среды в вашей локальной среде разработки, вы можете использовать dotenvпакет node
Дэйв Кисс
3
Как env_variables.yamlдобраться до всех экземпляров - это недостающий кусок головоломки.
Christopher Oezbek
1
Также: как использовать это локально?
Кристофер Озбек
@ChristopherOezbek 1. Как развернуть? Просто используйте, gcloud app deployкак обычно, для развертывания в Google Cloud. 2. Как установить секретные переменные окружения локально? Есть много способов. Вы можете просто использовать exportв командной строке или использовать любые инструменты, такие как @DaveKiss.
Ши-Вэнь Су
1
Это самое простое решение. Секреты могут быть доступны в вашем приложении через os.environ.get('SECRET').
Куинн Комендант
20

Мой подход заключается в хранении секретов клиента только в самом приложении App Engine. Секреты клиента не находятся ни в системе управления версиями, ни на каких-либо локальных компьютерах. Это дает то преимущество, что любой сотрудник App Engine может развертывать изменения кода, не беспокоясь о секретах клиента.

Я храню секреты клиентов непосредственно в Datastore и использую Memcache для уменьшения задержки доступа к секретам. Сущности хранилища данных необходимо создать только один раз, и они сохранятся при последующих развертываниях. конечно, консоль App Engine можно использовать для обновления этих объектов в любое время.

Есть два варианта выполнения одноразового создания сущности:

  • Используйте интерактивную оболочку App Engine Remote API для создания сущностей.
  • Создайте обработчик только для администратора, который будет инициализировать объекты фиктивными значениями. Вручную вызовите этот обработчик администратора, а затем с помощью консоли App Engine обновите сущности секретами рабочего клиента.
Бернд Верст
источник
7
Совсем не сложно. Спасибо движку приложения.
courtimas
19

На момент публикации этого не существовало, но для всех, кто здесь наткнется, Google предлагает услугу под названием Secret Manager .

Это простая служба REST (конечно, с пакетами SDK) для хранения ваших секретов в безопасном месте на облачной платформе Google. Это лучший подход, чем хранилище данных, требующий дополнительных действий для просмотра хранимых секретов и наличия более детальной модели разрешений - вы можете защитить отдельные секреты по-разному для разных аспектов вашего проекта, если вам нужно.

Он предлагает управление версиями, так что вы можете относительно легко обрабатывать изменения пароля, а также надежный уровень запросов и управления, позволяющий при необходимости обнаруживать и создавать секреты во время выполнения.

SDK Python

Пример использования:

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
источник
3
Это должен быть новый правильный ответ. Secret Manager все еще находится в бета-версии, но это путь вперед при работе с переменными среды.
Король Леон
@KingLeon, будет ли это означать необходимость рефакторинга кучи os.getenv('ENV_VAR')s?
Алехандро,
Я помещаю код, аналогичный приведенному выше, в функцию, затем использую что-то вроде SECRET_KEY = env('SECRET_KEY', default=access_secret_version(GOOGLE_CLOUD_PROJECT_ID, 'SECRET_KEY', 1)). Установка по умолчанию для использованияaccess_secret_version
Король Леон
Кроме того, я использую django-environment. github.com/joke2k/django-environ
Король Леон,
Мне нужно задать глупый вопрос, но то, что вы называете secret_id = 'my_secret_key', отсутствует в вашем контроле версий?
Дьерре
16

Лучший способ сделать это - сохранить ключи в файле client_secrets.json и исключить их из загрузки в git, указав их в вашем файле .gitignore. Если у вас разные ключи для разных сред, вы можете использовать app_identity api, чтобы определить идентификатор приложения и загрузить его соответствующим образом.

Здесь есть довольно подробный пример -> 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()
Гвин Хауэлл
источник
2
Определенно в правильном направлении, но это не решает проблему замены значений при app.yamlразвертывании приложения. Есть какие-нибудь идеи?
Ben
1
Поэтому создайте отдельный файл client_secrets для каждой среды. Например, client_secrets_live.json, client_secrets_dev.json, client_secrets_pilot.json и т. Д., Затем используйте логику Python, чтобы определить, на каком сервере вы находитесь, и загрузить соответствующий файл json. Метод app_identity.get_application_id () может быть полезен для автоматического определения того, на каком сервере вы находитесь. Вы это имеете в виду?
Gwyn Howell
@BenGrunfeld, посмотри мой ответ. Мое решение делает именно это. Я не понимаю, как этот ответ решает вопрос. Я предполагаю, что цель состоит в том, чтобы сохранить секретную конфигурацию из git и использовать git как часть развертывания. Здесь этот файл по-прежнему должен быть где-то и отправлен в процесс развертывания. Это может быть что-то, что вы делаете в своем приложении, но вы должны просто использовать методы, которые я выделил, возможно, сохраняя в другом файле, если вы хотите использовать это вместо app.yaml. Если я понимаю вопрос, это что-то вроде доставки приложения с открытым исходным кодом с фактическим секретом клиента или продуктом производителя библиотеки. ключ.
therewillbesnacks
1
Мне потребовалось время, чтобы осмыслить это, но я думаю, что это правильный подход. Вы не смешиваете настройки приложения ( app.yaml) с секретными ключами и конфиденциальной информацией, и что мне действительно нравится, так это то, что вы используете рабочий процесс Google для выполнения этой задачи. Спасибо @GwynHowell. =)
Бен
1
Аналогичный подход заключался бы в размещении этого файла JSON в известном месте в корзине GCS приложения по умолчанию ( cloud.google.com/appengine/docs/standard/python/… ).
Spain Train
15

Это решение полагается на устаревший appcfg.py

Вы можете использовать параметр командной строки -E в appcfg.py для настройки переменных среды при развертывании приложения в GAE (обновление 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
источник
Можете ли вы запросить эти переменные среды где-нибудь после развертывания? (Надеюсь, что нет.)
Ztyx
Есть ли способ передать переменные среды таким образом с помощью gcloudутилиты?
Trevor
6

Большинство ответов устарели. Сейчас использование облачного хранилища данных Google немного отличается. 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, тип - settings, а значение - это свойство объекта TWITTER_APP_KEY.

Джейсон Ф
источник
4

Вам следует зашифровать переменные с помощью 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).

Немного питонского кода, чтобы вы начали расшифровывать.

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

Похоже, вы можете сделать несколько подходов. У нас есть аналогичная проблема, и мы делаем следующее (адаптировано к вашему варианту использования):

  • Создайте файл, в котором хранятся все динамические значения app.yaml, и поместите его на защищенный сервер в среде сборки. Если вы действительно параноик, вы можете асимметрично зашифровать значения. Вы даже можете сохранить это в частном репо, если вам нужен контроль версий / динамическое извлечение, или просто использовать сценарий оболочки, чтобы скопировать его / вытащить из подходящего места.
  • Вытяните из git во время развертывания скрипта
  • После git pull измените app.yaml, прочитав и записав его на чистом Python с использованием библиотеки yaml.

Самый простой способ сделать это - использовать сервер непрерывной интеграции, такой как Hudson , Bamboo или Jenkins . Просто добавьте какой-нибудь плагин, шаг скрипта или рабочий процесс, который выполняет все упомянутые выше элементы. Вы можете передать переменные среды, которые, например, настроены в самом Bamboo.

Таким образом, просто введите значения в процессе сборки в среде, к которой у вас есть доступ. Если вы еще не автоматизируете свои сборки, вам следует это сделать.

Другой вариант вариант - то, что вы сказали, положить в базу. Если причина этого не в том, что все идет слишком медленно, просто вставьте значения в кэш памяти как кеш второго уровня и закрепите значения в экземплярах как кеш первого уровня. Если значения могут измениться и вам нужно обновить экземпляры без их перезагрузки, просто сохраните хэш, который вы можете проверить, чтобы знать, когда они меняются, или запускать его каким-либо образом, когда что-то, что вы делаете, изменяет значения. Так и должно быть.

закуски
источник
1
FWIW, этот подход наиболее точно соответствует коэффициенту конфигурации в рекомендациях по 12 Factor App ( 12factor.net )
Spain Train
3

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

Частично навеян этой публикацией на Medium

2 кипы
источник
2

Просто хотел отметить, как я решил эту проблему в javascript / nodejs. Для локальной разработки я использовал пакет npm 'dotenv', который загружает переменные среды из файла .env в process.env. Когда я начал использовать GAE, я узнал, что переменные среды необходимо устанавливать в файле app.yaml. Что ж, я не хотел использовать dotenv для локальной разработки и app.yaml для GAE (и дублировать переменные среды между двумя файлами), поэтому я написал небольшой скрипт, который загружает переменные среды app.yaml в процесс. .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')
gbruins
источник
Так ли это до сих пор? Потому что я использую .envфайл с секретными переменными. Я не дублирую их в своем app.yamlфайле, и мой развернутый код все еще работает. Но меня беспокоит, что происходит с .envфайлом в облаке. Он зашифрован или что-то в этом роде? Как я могу убедиться, что никто не получит доступ к .envпеременным файла gcloud после его развертывания?
Gus
В этом нет необходимости, поскольку GAE автоматически добавляет все переменные, определенные в файле app.yaml, в среду узла. В основном это то же самое, что dotenv делает с переменными, определенными в пакете .env. Но мне интересно, как вам нужно настроить компакт-диск, если вы не можете отправить app.yaml с переменными env в VCS или в конвейер ...
Йорнв
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
Дж. С. Бах
источник
1

Существует пакет pypi под названием gae_env, который позволяет сохранять переменные среды appengine в Cloud Datastore. Под капотом он также использует Memcache, поэтому он быстро

Применение:

import gae_env

API_KEY = gae_env.get('API_KEY')

Если в хранилище данных есть значение для этого ключа, оно будет возвращено. Если нет, __NOT_SET__будет создана запись-заполнитель и ValueNotSetErrorбудет выдан. Исключение будет напоминать вам перейти в консоль разработчика и обновить запись заполнителя.


Как и в ответе Мартина, вот как обновить значение ключа в Datastore:

  1. Перейдите в раздел Datastore в консоли разработчика.

  2. Выберите свой проект вверху страницы, если он еще не выбран.

  3. В раскрывающемся списке Тип выберите GaeEnvSettings.

  4. Ключи, для которых возникло исключение, будут иметь значение __NOT_SET__.

Ваши настройки, созданные классом Settings

Нажмите, чтобы редактировать

Введите реальную стоимость и сохраните


Перейдите на страницу пакета GitHub для получения дополнительной информации об использовании / настройке.

Принц Одаме
источник