Поднимаем TLS для gRPC в Go

Обложка: Поднимаем TLS для gRPC в Go

gRPC — это высокопроизводительный RPC-фреймворк с открытым исходным кодом от Google, который использует HTTP/2 и Protocol Buffers для обмена данными между сервисами. Для защиты такого трафика применяется TLS (Transport Layer Security) — протокол шифрования, обеспечивающий конфиденциальность и целостность передаваемых данных. В этой статье мы рассмотрим, как поднять gRPC сервер и клиент для него на Go, если у вас самоподписанный сертификат. Кстати, если вы опытный тестировщик и хотите присоединиться к команде, не пропустите вакансию Fullstack QA в Сбер.

Для начала мы обсудим, как поднять gRPC сервер и клиент без шифрования. Затем посмотрим, как мог бы выглядеть код клиента, если сервер использует сертификат, подписанный доверенным CA.

В конце я расскажу, что можно сделать, если вы хотите поднять server и client с TLS, если у вас самоподписанный сертификат.

Ключевые выводы

gRPC в Go поддерживает три режима соединения: без шифрования (insecure), TLS с сертификатом доверенного CA и TLS с самоподписанным сертификатом.

Для незащищённого соединения клиент явно передаёт опцию insecure.NewCredentials() — без неё gRPC-подключение не установится.

При самоподписанном сертификате нужно добавить свой CA в доверенные на стороне клиента через credentials.NewClientTLSFromFile().

Сервер загружает пару cert+key через credentials.NewServerTLSFromFile() и передаёт credentials при создании gRPC-сервера.

Самоподписанные сертификаты генерируются через openssl в четыре команды; для production рекомендуется сертификат от доверенного CA или mTLS.

Insecure connection

Давайте начнём с опции, когда вам не нужно шифрование. Допустим, server и client находятся в одном закрытом кластере и кроме них там никого нет. На самом деле, даже в этом случае я рекомендую поднять TLS, но давайте рассмотрим такую опцию. Подробнее о практическом применении gRPC в Go читайте в статье «Создаём микросервис обработки изображений на Go с gRPC».

Начнём с сервера, и тут всё просто: если нам не нужен TLS, то мы просто нигде не должны его настраивать.

			listener, err := net.Listen("tcp", ":50051")
if err != nil {
   log.Fatalf("net.Listen error: %v", err)
}

s := grpc.NewServer()

pb.RegisterGreeterServer(s, &server{})

log.Fatal(s.Serve(listener))

		

С клиентом всё немного сложнее: если мы не хотим использовать TLS, то при создании gRPC соединения нам это явно нужно указать опцией grpc.WithTransportCredentials(insecure.NewCredentials()).

			var (
   ctx  = context.Background()
   addr = "localhost:50051"
)

conn, err := grpc.DialContext(ctx, addr, grpc.WithTransportCredentials(insecure.NewCredentials()))
if err != nil {
   log.Fatal(err)
}

client := pb.NewGreeterClient(conn)
		

Secure connection

Что, если опция незащищённого соединения нам не подходит? О выборе между gRPC и другими протоколами читайте в материале «Post-GraphQL мир: стоит ли переходить на gRPC и tRPC».

Давайте для начала вспомним, как работает TLS. Одним из шагов TLS рукопожатия является отправка от сервера его сертификата. Получив сертификат, клиент должен проверить его подпись. То есть получив сертификат клиент смотрит на подпись сертификата и проверяет, что эта подпись создана одним из доверенных CA. Список доверенных CA есть в системе. 

Так происходит например при открытии любой https страницы в браузере.

Давайте теперь вернёмся к нашим gRPC серверу и клиенту. Представим, что наш сервер использует сертификат, подписанный доверенным CA, как научить нашего клиента распознавать это?

На первом шаге, когда мы создавали незащищённое соединение, мы указали опцию insecure для transport credentials. Без этой опции клиент не поднялся бы. Для защищённого соединения нам тоже нужно будет указать опцию transport credentials, но передать в неё системные CA. В Go это можно сделать следующим образом:

			import (
  "crypto/tls"
  "crypto/x509"

  "google.golang.org/grpc/credentials"
)

func generateTLSCreds() (credentials.TransportCredentials, error) {
   systemRoots, err := x509.SystemCertPool()
   if err != nil {
      return nil, err
   }


   return credentials.NewTLS(&tls.Config{
      RootCAs: systemRoots,
   }), nil
}

		
			tlsCreds, err := generateTLSCreds()
if err != nil {
   log.Fatal(err)
}

		

Теперь вместо grpc.WithTransportCredentials(insecure.NewCredentials()) нам нужно указать grpc.WithTransportCredentials(tlsCreds)

Так клиент научится верифицировать подпись серверного сертификата системными CA.

Secure connection + self-signed certificate

Давайте теперь подумаем, что нам делать, если мы хотим использовать самоподписанный сертификат на сервере. Если мы поднимем сервер с самоподписанным сертификатом и оставим код клиента, как мы сделали на предыдущем шаге, то ничего работать не будет, так как на этапе проверки подписи серверного сертификата на стороне клиента клиент посчитает сертификат невалидным, так как он подписанным неизвестным ему CA. В итоге TLS рукопожатие не пройдёт и соединение установлено не будет. 

Давайте попробуем это исправить, но для начала научимся генерировать самоподписанные сертификаты.

Воспользуемся openssl, чтобы сгенерировать CA сертификат и серверный сертификат. Для автоматизации создания API на gRPC см. статью «Генерируем CRUD для gRPC по схеме БД».

			openssl genrsa -out ca.key 2048
openssl req -new -x509 -days 365 -key ca.key -subj "/C=CN/ST=GD/L=SZ/O=Acme, Inc./CN=Acme Root CA" -out ca.crt
openssl req -newkey rsa:2048 -nodes -keyout server.key -subj "/C=CN/ST=GD/L=SZ/O=Acme, Inc./CN=localhost" -out server.csr
openssl x509 -req -extfile <(printf "subjectAltName=DNS:localhost") -days 365 -in server.csr -CA ca.crt -CAkey ca.key -CAcreateserial -out server.crt
		

На выходе у нас должно получиться 6 файлов:

ca.crt, ca.key, ca.srl, server.crt, server.csr, server.key

Хорошо, теперь давайте поднимем сервер с этим сертификатом.

			func generateTLSCreds() (credentials.TransportCredentials, error) {
   // Здесь нужно указать полные пути к файлам
   certFile := "server.crt"
   keyFile := "server.key"


   return credentials.NewServerTLSFromFile(certFile, keyFile)
}

		
			listener, err := net.Listen("tcp", ":50051")
if err != nil {
   log.Fatalf("failed to listen: %v", err)
}


tlsCreds, err := generateTLSCreds()
if err != nil {
   log.Fatalf("failed to generate tls creds: %v", err)
}


s := grpc.NewServer(grpc.Creds(tlsCreds))
		

Следующим шагом нам нужно научить клиента верифицировать подпись самоподписанного сертификата, то есть добавить наш CA в список его доверенных CA. На стороне клиента нужно сделать следующее:

			func generateTLSCreds() (credentials.TransportCredentials, error) {
   // Здесь нужно указать полный путь к файлу
   certFile := "ca.crt"

   return credentials.NewClientTLSFromFile(certFile, "")
}

		
			var (
   ctx  = context.Background()
   addr = "localhost:50051"
)


tlsCreds, err := generateTLSCreds()
if err != nil {
   log.Fatal(err)
}


conn, err := grpc.DialContext(ctx, addr, grpc.WithTransportCredentials(tlsCreds))
if err != nil {
   log.Fatal(err)
}

		

Теперь клиент и сервер могут общаться по TLS соединению с самоподписанным сертификатом.

Часто задаваемые вопросы
1
Что такое TLS в gRPC и зачем он нужен?

TLS (Transport Layer Security) — криптографический протокол, который обеспечивает шифрование, аутентификацию и целостность данных при передаче по сети. В gRPC TLS используется для защиты RPC-вызовов: без него трафик между сервером и клиентом передаётся в открытом виде и может быть перехвачен. Даже внутри закрытого кластера TLS рекомендован как дополнительный уровень защиты на случай компрометации инфраструктуры.

2
В чём разница между самоподписанным сертификатом и сертификатом от доверенного CA?

Сертификат от доверенного CA (Certificate Authority) подписан организацией, которой доверяет операционная система. Клиент может проверить его подпись автоматически, используя системный пул CA. Самоподписанный сертификат подписан вашим собственным CA, который не входит в системный пул: клиент должен явно получить и доверять этому CA. В production предпочтительны сертификаты от доверенных CA (Let's Encrypt, DigiCert и др.) или mTLS для взаимной аутентификации сервисов.

3
Как настроить mTLS (mutual TLS) для gRPC в Go?

Mutual TLS (mTLS) — режим, при котором и сервер, и клиент предъявляют друг другу сертификаты для взаимной аутентификации. Чтобы настроить mTLS в Go, нужно: 1) сгенерировать клиентский сертификат, подписанный тем же CA; 2) на сервере указать в tls.Config поля ClientAuth: tls.RequireAndVerifyClientCert и ClientCAs; 3) на клиенте передать свой сертификат в tls.Config через поле Certificates. Этот подход широко используется в микросервисных архитектурах (service mesh) для zero-trust безопасности.

4
Почему gRPC клиент не подключается к серверу с самоподписанным сертификатом?

Это стандартная ошибка TLS-рукопожатия: клиент получает сертификат сервера, пытается проверить его подпись по системному пулу CA, не находит там вашего самоподписанного CA и отклоняет соединение с ошибкой certificate signed by unknown authority. Решение: явно передать путь к файлу CA сертификата через credentials.NewClientTLSFromFile("ca.crt", ""). Использовать InsecureSkipVerify: true не рекомендуется, так как это отключает проверку сертификата полностью.

Заключение

В этой статье мы рассмотрели три опции настройки TLS между сервером и клиентом в Go: незащищённое соединение, соединение с сертификатом, подписанным доверенным CA и соединение с самоподписанным сертификатом.