Создание безопасного веб-чата с помощью Redis, mTLS и GCP

Создание безопасного веб-чата с помощью Redis, mTLS и GCP

4 февраля 2023 г.

Это первая статья из этой серии, в которой я создаю веб-чат на основе бесплатных ограничений GCP. В этом посте я сосредоточусь на настройке архитектуры для создания конечной точки HTTP для выполнения следующих действий:

* Анонимные пользователи выбирают имя пользователя * Пользователи ранжируются по времени последнего доступа * Через 10 минут бездействия пользователи удаляются из системы * Максимум 100 пользователей одновременно

Архитектура и выбор компонентов

Я хочу иметь действительно бесплатную инфраструктуру, используя бессрочный бесплатный уровень GCP. Компоненты для этого поста:

* Облачная функция: я буду использовать бессерверные облачные функции на основе HTTP. a> для запуска внутреннего кода. Облачные функции имеют бесплатный лимит в 2 миллиона запросов в месяц. Я выбрал облачные функции, прежде всего потому, что для начала работы требуется небольшой код, а возможности наблюдения и масштабируемости я получаю «из коробки». * Redis: для хранения данных пользователей я буду использовать Redis, потому что он легкий в использовании, быстрый и универсальный для хранения всех видов данных, которые мне понадобятся для приложения чата. Но MemoryStore реализация Redis в GCP платная. Я обхожу это, устанавливая Redis на бесплатный вычислительный экземпляр (это несколько ограничивает масштабируемость, но у меня есть место из-за эффективности памяти Redis). * Подключение к Redis через Интернет: для подключения к остальной части моего VPC из облачных функций требуется Коннектор VPC (бессерверный доступ к VPC), который не является бесплатным. Я работаю над этим, подключаясь к Redis через Интернет, но мне нужно убедиться, что это делается через безопасное соединение.

The cost-efficient and safe architecture that runs the backend code via Cloud Functions and communicates with Redis over a secure mTLS connection

Установка Redis

Google Cloud предоставляет бесплатный экземпляр e2-micro в рамках ежемесячного уровня бесплатного пользования. Я установлю Redis как службу на вычислительном экземпляре.

* Предоставление микроэкземпляра e2 в регионе us-central1 — назовем экземпляр pelican * Зарезервируйте статический IP-адрес для использования здесь и прикрепите его к pelican. * Подключитесь к экземпляру по SSH и установите последнюю версию Redis ( не ниже версии 7.0, чтобы обеспечить наилучшую поддержку TLS) * Если вычислительный экземпляр основан на Debian, лучше обновить конфигурацию Redis, чтобы она контролировалась systemd, которая является системой инициализации по умолчанию: измените файл /etc/redis/redis. conf, чтобы обновить настройку контроля:

javascript контролируемая система * Удалите адрес привязки, чтобы он был пустым (все сетевые интерфейсы экземпляра должны иметь доступ к Redis) и порт на 12345, добавьте пароль с помощью параметр requirepass * Последнее, что нужно сделать, это разрешить управление службой Redis с помощью systemd, чтобы иметь доступ к домашнему каталогу — это необходимо только потому, что я храню некоторые файлы (созданные позже) в домашнем каталоге.

Для этого добавьте или обновите служебный файл /etc/systemd/system/redis.service, указав этот параметр только для чтения:

javascript ProtectHome=только для чтения

* Перезагрузите systemd

javascript sudo systemctl демон-перезагрузка

* Перезапустите Redis

javascript sudo systemctl перезапустить Redis

* Я могу протестировать Redis через интерфейс командной строки локально на экземпляре

javascript mourjo@pelican:~$ redis-cli -p 12345 127.0.0.1:11219> AUTH пароль ХОРОШО 127.0.0.1:11219> установить х у ХОРОШО 127.0.0.1:11219> получить х "у"

* Я хочу подключиться к Redis через Интернет, поэтому сначала разрешите доступ к порту pelican. На вкладке брандмауэра в консоли GCP создайте новый список разрешений со следующими настройками:

* Приоритет: 70 (меньше значения по умолчанию 100) * Направление: вход * Целевые теги: тег, который я буду применять к вычислительному экземпляру. * Исходные диапазоны IPv4: 0.0.0.0/0 позволяет каждому хосту подключаться к этому порту. * Протокол: TCP на порту 12345

 ![Creating a firewall rule](https://cdn.hackernoon.com/images/PyERAjWATVTJ0CA5MOShZMIP0CF2-rsj35sl.png)


  • Отредактируйте экземпляр, чтобы применить этот брандмауэр, используя параметр «сетевые теги» и тег, с которым я создал брандмауэр выше (обратите внимание, что это открывает доступ к моему экземпляру для всех, поэтому настоятельно рекомендуется установить пароль как указано выше)
  • Теперь должна быть возможность подключиться к Redis из-за пределов машины, например, с компьютера, который не подключился к экземпляру по SSH :tada:

Включение поддержки TLS для Redis

До сих пор я установил пароль для доступа к Redis, но эти данные все еще передаются по незашифрованному TCP-соединению, что имеет две проблемы:

* Очень легко перехватить трафик и прочитать пароль * Любой может подключиться и начать отправлять поток трафика даже без аутентификации, что приведет к перегрузке моего крошечного экземпляра

Обе проблемы можно решить, используя защищенное соединение через TLS, при котором и сервер, и клиент должны подтвердить свою личность еще до взаимодействия с Redis. Mutual TLS (или mTLS) гарантирует, что и клиент, и сервер проверяют друг друга. Поэтому только известные клиенты с правильными закрытыми ключами могут получить доступ к моему серверу через зашифрованное соединение.

Цифровые сертификаты и установление доверия

Цифровые сертификаты обычно используются в TLS и HTTPS для проверки личности участников. Короче говоря, сертификат — это криптографически проверяемое доказательство того, что кто-то является тем, за кого себя выдает, что подтверждается другим органом.

Если я подключаюсь к google.com, мне нужно знать, что я на самом деле подключаюсь к google.com, а не кто-то выдает себя за google.com. Это происходит через цепочку доверия, установленную между мной (браузером) и сервером (google.com): Google отправляет сертификат, который был криптографически подписан центром сертификации (CA), и эта подпись может быть проверена нами.

Важно отметить, что мне нужно неявно безоговорочно доверять ЦС. Обычно это происходит в Интернете через известных эмитентов сертификатов, встроенных в браузеры и операционные системы (например, /etc/ssl/certs хранит центры сертификации, которым неявно доверяет система Linux). Как только я увижу сертификат, подписанный кем-то, кому я доверяю, я смогу убедиться, что сторона, выдающая себя за то, кем она является (здесь google.com), на самом деле является Google, а не мошенником, пытающимся украсть у нас информацию. На самом деле могут быть промежуточные центры сертификации, которые фактически подписывают сертификаты, но сами промежуточные центры сертификации также должны иметь сертификаты, подписанные корневым центром сертификации.

The root certificate is implicitly trusted and is self-signed. All other certificates in the chain are signed by a CA that issued it. The only implicit trust that needs to be established is with the root CA.

Источник изображения.

Сертификаты содержат открытый ключ стороны, личность которой проверяется. Используя открытый ключ сервера из сертификата, клиент может зашифровать полезную нагрузку, которую может расшифровать только сервер, тем самым делая связь безопасной: никакой другой наблюдатель сети не может расшифровать передаваемую информацию.

Это называется рукопожатием mTLS. На деле чуть сложнее, но в принципе то же самое:

How mTLS works (source: https://www.cloudflare.com/en-in/learning/access-management/what-is-mutual-tls/)

Создание сертификатов

Первым шагом к подключению TLS является наличие сертификата для сервера и клиента, которые подписаны центром сертификации. Я не хочу покупать сертификат, поэтому я создам свой собственный сертификат ЦС, который не пользуется доверием в дикой природе, поэтому мне нужно будет указать серверу и клиенту принимать все сертификаты, выпущенные этим ЦС.

  1. Создайте ключ и сертификат корневого центра сертификации — им будут неявно доверять как клиент, так и сервер

  2. Создайте новый закрытый ключ для корневого ЦС (при необходимости зашифруйте его с помощью AES 256)

    баш openssl genrsa -aes256 -out ca.key 4096 2. Создайте сертификат, действительный в течение 10 лет, подписанный этим закрытым ключом

    баш openssl req -new -x509 -days 3650 -key ca.key -out ca.crt 2. Создайте сертификат клиента, подписанный корневым ЦС.

  3. Создайте новый закрытый ключ для клиента (та же команда, что и 1a)

    баш openssl genrsa -aes256 -out client.key 2048 2. Создайте запрос на подпись сертификата, содержащий параметры, с которыми будет создан сертификат

    javascript openssl req -new -key client.key -out client.csr 3. Создайте сертификат, подписанный ЦС

    баш openssl x509 -req -days 3650 -in client.csr -CA ca.crt -CAkey ca.key -out client.crt -CAcreateserial 3. Создайте сертификат сервера, подписанный корневым ЦС.

  4. Создайте новый закрытый ключ для сервера (та же команда, что и 1a)

    javascript openssl genrsa -aes256 -out server.key 2048 2. Создайте запрос на подпись сертификата (при запросе Common Name обычно используется IP-адрес сервера)

    javascript openssl req -new -key server.key -out server.csr 3. Создайте сертификат, подписанный ЦС

    javascript openssl x509 -req -days 3650 -in server.csr -CA ca.crt -CAkey ca.key -out server.crt

Настройка Redis с сертификатами

Используя сертификаты, давайте внесем следующие изменения в конфигурацию Redis:

  • Включить TLS, отключить порт TCP
  • Добавить сертификат сервера, ключ сервера
  • Добавить сертификат CA
  • Принудительная аутентификация клиентов

Обновите файл /etc/redis/redis.conf с этими настройками (вот полная конфигурация):

port 0
tls-port 12345

# server certificate and key:
tls-cert-file /home/mourjo/certs/server.crt
tls-key-file  /home/mourjo/certs/server.key

# the key file is encrypted, so add the password
tls-key-file-pass thisisredacted

# the CA certificate
tls-ca-cert-file /home/mourjo/certs/ca.crt

# make client authentication mandatory
tls-auth-clients yes

При запуске сервера должны отображаться следующие журналы

sudo systemctl start redis
tail /var/log/redis/redis-server.log
148924:M 27 Jan 2023 04:16:10.675 # Server initialized
...
148924:M 27 Jan 2023 04:16:10.676 * Ready to accept connections

С помощью s_client в OpenSSL я могу убедиться, что Redis не принимает клиентов, у которых нет действительного сертификата, подписанного моим ЦС:

# without any certificate
openssl s_client -state  -connect 35.209.163.139:11219 -servername 35.209.163.139
8610505984:error:1404C45C:SSL routines:ST_OK:reason(1116):/AppleInternal/Library/BuildRoots/810eba08-405a-11ed-86e9-6af958a02716/Library/Caches/com.apple.xbs/Sources/libressl/libressl-3.3/ssl/tls13_lib.c:129:SSL alert number 116

# with a certificate not signed by the CA
openssl s_client -state -cert random.crt -key random.key -connect <ip-address>:<port> -servername <ip-address>
8610505984:error:1404C418:SSL routines:ST_OK:tlsv1 alert unknown ca:/AppleInternal/Library/BuildRoots/810eba08-405a-11ed-86e9-6af958a02716/Library/Caches/com.apple.xbs/Sources/libressl/libressl-3.3/ssl/tls13_lib.c:129:SSL alert number 48

Я могу убедиться, что соединение SSL работает при передаче правильного сертификата:

openssl s_client -state -cert client.crt -key client.key -connect <ip-address>:<port> -servername <ip-address>

Это должно распечатать необходимую информацию о сервере и безопасности, а также позволить нам запускать команды Redis (поскольку протокол Redis удобочитаемый):

Я также могу проверить настройки SSL с помощью онлайн-инструмента, такого как sslshopper, который сообщает, что SSL-соединение настроено правильно, но выдано центром сертификации, которому не доверяют в мире, потому что я использую центр сертификации, который только я знаю.

Теперь давайте подключимся с помощью клиента командной строки Redis redis-cli из-за пределов экземпляра, передав сертификат клиента:

Redis теперь работает над TLS через Интернет! :тада:

Клиентская реализация

Я настроил сервер Redis, и он работает с клиентом по умолчанию redis-cli, но я хочу создать простую серверную часть с использованием Redis:

* Разрешить пользователю использовать свое имя пользователя — аутентификация выходит за рамки этого сообщения * Отслеживайте активных пользователей в течение 10 минут и удаляйте неактивных пользователей. * Разрешить не более 100 пользователей одновременно

Я разверну облачную функцию HTTP, которая хранит и взаимодействует с сервером Redis. Серверный код написан на Java с использованием популярной библиотеки Jedis для взаимодействия с Redis.

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

double timeoutMillis = 10 * 60 * 1000D;
int MAX_USERS = 100;

// connect with TLS, pass connect timeout and socket timeout of 10 sec
try (Jedis jedis = new Jedis(host, port, 10_000, 10_000, true)) {
    jedis.auth(redisPassword);

    // purge old users
    jedis.zremrangeByScore("recent_users", Double.NEGATIVE_INFINITY, System.currentTimeMillis() - timeoutMillis);

    // ensure that there only 100 users at max
    var total_users = jedis.zcount("recent_users", Double.NEGATIVE_INFINITY, Double.POSITIVE_INFINITY);
    if (total_users >= MAX_USERS) {
        throw new TooManyUsersException(total_users);
    }

    // add the current user to the sorted set with the timestamp
    jedis.zadd("recent_users", (double) System.currentTimeMillis(), user);

    // return a list of active users
    var activeUsers = jedis.zrangeWithScores("recent_users", 0, System.currentTimeMillis());
    Collections.reverse(activeUsers);
    return activeUsers;
}

Остальная часть клиентского кода использует Java SDK для облачных функций, которые охватывают вышеперечисленное. бизнес-логика для ответа на входящие запросы. Полный репозиторий доступен здесь.

Добавление клиентских сертификатов в хранилище ключей Java

Приведенный выше фрагмент кода не упоминает сертификаты, хотя мой сервер Redis принимает TLS-соединения только от клиентов, которым доверяет мой ЦС. Это соответствует архитектуре криптографии Java (JCA): учетные данные/сертификаты хранятся в защищенных паролем файлах, управляемых утилитой CLI keytool, которая поставляется непосредственно с установкой JDK. Таким образом, клиентский код при настройке соединения TLS прозрачно использует сертификаты, присутствующие в этих защищенных файлах.

Но прежде чем я начну импортировать свои сертификаты с помощью keytool, мне нужно преобразовать мои сертификаты из формата PEM (который используется по умолчанию в Linux) в формат PKCS12 формат

Преобразуйте корневой сертификат ЦС в формат PKCS12:

openssl pkcs12 -export -in ca.crt -inkey ca.key -out ca.p12

Преобразуйте сертификат клиента (обратите внимание, что ключ должен быть включен в файл p12):

openssl pkcs12 -export -in client.crt -inkey client.key -out client.p12

В соответствии с обычной практикой с помощью keytool будут созданы два файла: один называется keystore, а другой — truststore.

В keystore будут храниться ключи/сертификаты, необходимые клиенту для идентификации, то есть закрытый ключ клиента и сертификат клиента, которые позволят нам доказать серверу Redis, что клиенту должно быть разрешено подключение.

keytool -importkeystore -noprompt  -srckeystore client.p12 -srcstoretype PKCS12 -destkeystore keystore.jks -deststoretype PKCS12

В хранилище доверенных сертификатов будут храниться сертификаты, которым клиент будет неявно доверять, то есть сертификат корневого ЦС.

При этом, когда сервер отправляет свой сертификат, клиент будет знать, что он может доверять серверу. В хранилище доверенных сертификатов будет мой сертификат ЦС:

keytool -importkeystore -noprompt -srckeystore ca.p12 -srcstoretype PKCS12 -destkeystore truststore.jks -deststoretype PKCS12

Сообщите JVM, где она должна искать учетные данные/сертификаты, и установите следующие параметры либо как параметры CLI, либо загрузите их в код:

System.setProperty("javax.net.ssl.keyStorePassword", "redacted"); // decryption password used while generating the keystore:
System.setProperty("javax.net.ssl.keyStore", "keystore.jks");
System.setProperty("javax.net.ssl.keyStoreType", "PKCS12");

System.setProperty("javax.net.ssl.trustStorePassword", "redacted"); // decryption password used while generating the truststore
System.setProperty("javax.net.ssl.trustStore", "truststore.p12");
System.setProperty("javax.net.ssl.trustStoreType", "PKCS12");

Если все пойдет хорошо, следующее должно вернуть PONG с сервера Redis:

try (Jedis jedis = new Jedis(host, port, true)) {
  jedis.auth("theredispassword");
  System.out.println(jedis.ping());
}

Секретное хранилище

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

Для этого я буду использовать менеджер секретов GCP для хранения следующих секретов:

  • JIBBER_REDIS_PASSWORD: текстовый пароль для аутентификации в Redis.
  • JIBBER_KEYSTORE: файл хранилища ключей, сгенерированный с помощью keytool выше
  • .
  • JIBBER_KEYSTORE_PASSWORD: текстовый пароль, используемый для расшифровки файла хранилища ключей.
  • JIBBER_TRUSTSTORE: файл хранилища доверенных сертификатов, сгенерированный с помощью keytool выше
  • .
  • JIBBER_TRUSTSTORE_PASSWORD: текстовый пароль, используемый для расшифровки файла хранилища доверенных сертификатов.

Мне также нужно убедиться, что у моей облачной функции есть доступ к этим секретам. Я могу сделать это, предоставив моей учетной записи службы по умолчанию разрешение на чтение этих секретов. Я должен разрешить моему сервисному аккаунту иметь разрешение Secret Manager Secret Accessor.

Развертывание облачной функции

В дополнение к секретам мне также нужно установить несколько переменных среды (которые не являются секретными):

* Хост и порт Redis * Расположение хранилищ ключей и доверенных хранилищ

Последняя команда для развертывания в облачных функциях с секретами и переменными среды выглядит так:

gcloud functions deploy jibber-function  
  --entry-point me.mourjo.functions.Hello 
  --runtime java17 
  --trigger-http 
  --allow-unauthenticated 
  --set-secrets '/etc/keystore:/keystore=JIBBER_KEYSTORE:1,/etc/truststore:/truststore=JIBBER_TRUSTSTORE:1,TRUSTSTORE_PASS=JIBBER_TRUSTSTORE_PASSWORD:1,KEYSTORE_PASS=JIBBER_KEYSTORE_PASSWORD:1,REDIS_PASSWORD=JIBBER_REDIS_PASSWORD:1' 
  --set-env-vars 'REDIS_HOST=1.1.1.1,REDIS_PORT=12345,KEYSTORE_LOCATION=/etc/keystore/keystore,TRUSTSTORE_LOCATION=/etc/truststore/truststore'

Развертывание кода в моей облачной функции теперь должно работать! :тада:

Заключение

В этом посте я рассказал об основах веб-приложения для чата, настроив облачную функцию GCP, которая запускается через HTTP и обменивается данными через Интернет через безопасное соединение с Redis. Все это является частью GCP. бесплатный уровень.

Эта экономичная инфраструктура имеет некоторые оговорки:

  • Конечная точка работает медленно. Это частично связано с функцией Cloud, а частично — с зашифрованным подключением к Redis через Интернет, а не через локальную сеть.
  • Redis не является отказоустойчивым: любые данные, записанные в Redis, не резервируются, и нет вторичного экземпляра Redis, который мог бы заменить его, если вычислительный экземпляр pelican выйдет из строя.

В следующем посте я напишу клиент на основе браузера, который может взаимодействовать с этой облачной функцией, чтобы установить пользовательский сеанс, а затем передать его серверу WebSocket на основе Cloud Run. Оставайтесь с нами!


Оригинал
PREVIOUS ARTICLE
NEXT ARTICLE