Как не продолбать пароли в Python скриптах
Хранение паролей всегда было головной болью. В классическом варианте у вас есть пользователь, который очень старается не забыть жутко секретный «qwerty123» и информационная система, которая хранит хеш от этого пароля. Хорошая система еще и заботливо солит хеши, чтобы отравить жизнь нехорошим людям, которые могут украсть базу с хешированными паролями. Тут все понятно. Какие-то пароли храним в голове, а какие-то засовываем в зашифрованном виде в keepass.
Все меняется, когда мы убираем из схемы человека, который старательно вводит ключ с бумажки. При взаимодействии двух информационных систем, на клиентской стороне в любом случае должен храниться пароль в открытом для системы виде, чтобы его можно было передать и сравнить с эталонным хешем. И вот на этом этапе админы обычно открывают местный филиал велосипедостроительного завода и начинают старательно прятать, обфусцировать и закапывать секретный ключ в коде скриптов. Многие из этих вариантов не просто бесполезны, но и опасны. Я попробую предложить удобное и безопасное решение этой проблемы для python. И чуть затронем powershell.
Как делать не надо
Всем знакома концепция «временного скриптика». Вот буквально только данные по-быстрому распарсить из базы и удалить. А потом внезапно выясняется, что скрипт уже из dev-зоны мигрировал куда-то в продакшен. И тут начинают всплывать неприятные сюрпризы от изначальной «одноразовости».
Чаще всего встречается вариант в стиле:
db_login = 'john.doe'
password = 'password!'
Проблема в том, что здесь пароль светится в открытом виде и достаточно просто обнаруживается среди залежей старых скриптов автоматическим поиском. Чуть более сложный вариант идет по пути security through obscurity, с хранением пароля в зашифрованном виде прямо в коде. При этом расшифровка обратно должна выполняться тут же, иначе клиент не сможет предъявить этот пароль серверной стороне. Такой способ спасет максимум от случайного взгляда, но любой серьезный разбор кода вручную позволит без проблем вытащить секретный ключ. Код ниже спасет только от таких «shoulder surfers»:
>>> import base64
>>> print base64.b64encode("password")
cGFzc3dvcmQ=
>>> print base64.b64decode("cGFzc3dvcmQ=")
password
Самый неприятный сценарий — использования систем контроля версии, например git, для таких файлов с чувствительной информацией. Даже, если автор решит вычистить все пароли — они останутся в истории репозитория. Фактически, если вы запушили в git файл с секретными данными — можете автоматически считать их скомпрометированными и немедленно начинать процедуру замены всех затронутых credentials.
Использование системного хранилища
Есть крутейшая библиотека keyring. Основной принцип работы строится на том, что у каждого пользователя ОС есть свое зашифрованное хранилище, доступ к которому возможен только после входа пользователя в систему. Она кроссплатформенная и будет использовать тот бэкенд для хранения паролей, который предоставлен операционной системой:
- KDE4 & KDE5 KWallet (требуется dbus)
- Freedesktop Secret Service — множество DE, включая GNOME (требуется secretstorage)
- Windows Credential Locker
- macOS Keychain
Также можно использовать альтернативные бэкенды или написать свой, если уж совсем что-то странное требуется.
Сравним сложность атаки
При хранении пароля непосредственно в скрипте нужно:
- Похитить сам код (легко)
- Деобфусцировать при необходимости (легко)
При использовании локального keyring злоумышленнику нужно:
- Похитить сам код (легко)
- Деобфусцировать при необходимости (легко)
- Скомпрометировать локальную машину, залогинившись под атакуемым пользователем (сложно)
Теоретически, доступ к локальному хранилищу сможет получить любая локальная программа, работающая от имени текущего пользователя, если будет знать параметры доступа к секретному паролю. Однако, это не является проблемой, так как в случае компрометации учетной записи злоумышленник и так сможет перехватить все чувствительные данные. Другие пользователи и их ПО не будет иметь доступа к локальному хранилищу ключей.
Пример использования
import argparse
import getpass
import keyring
def parse_arguments():
parser = argparse.ArgumentParser()
parser.add_argument("-n", "--newpass", required=False, help="Set new password", action="store_true")
arguments = parser.parse_args()
return arguments
def fake_db_connection():
# Функция, имитирующая подключение к базе данных или что-то подобное
db_name = 'very_important_db'
db_host = '147.237.0.71'
passwd = keyring.get_password(namespace, username)
print('Connecting to db: {}'.format(db_name))
print('Using very secret password from vault: {}'.format(passwd))
print('Doing something important...')
print('Erasing the database...')
print('Task completed')
# Объявляем дефолтные переменные
namespace = 'my_key_vault'
username = 'meklon'
args = parse_arguments()
# Записываем в хранилище пароль, если активирован параметр --newpass
if args.newpass:
# Безопасно запрашиваем ввод пароля в CLI
password = getpass.getpass(prompt="Enter secret password:")
# Пишем полученный пароль в хранилище ключей
try:
keyring.set_password(namespace, username, password)
except Exception as error:
print('Error: {}'.format(error))
# Подключаемся к базе с помощью пароля из системного хранилища
fake_db_connection()
Безопасный ввод пароля
Еще один частый вариант утечки секретных паролей — история командной строки. Использование стандартного input здесь недопустимо:
age = input("What is your age? ")
print "Your age is: ", age
type(age)
>>output
What is your age? 100
Your age is: 100
type 'int'>
В примере выше я уже упоминал библиотеку getpass:
# Безопасно запрашиваем ввод пароля в CLI
password = getpass.getpass(prompt="Enter secret password:")
Ввод данных при ее использовании аналогичен классическому *nix подходу при входе в систему. Ни в какие системные логи данные не пишутся и не отображаются на экране.
Немного о Powershell
Для Powershell правильным вариантом является использование штатного Windows Credential Locker.
Реализуется это модулем CredentialManager.
Пример использования:
Install-Module CredentialManager -force
New-StoredCredential -Target $url -Username $ENV:Username -Pass ....
Get-StoredCredential -Target ....