摘自:http://bbengfort.github.io/programmer/2017/03/03/secure-grpc.html


Secure gRPC with TLS/SSL

03 Mar 2017

One of the primary requirements for the systems we build is something we call the “minimum security requirement”. Although our systems are not designed specifically for high security applications, they must use minimum standards of encryption and authentication. For example, it seems obvious to me that a web application that stores passwords or credit card information would encrypt their data on disk on a per-record basis with a salted hash. In the same way, a distributed system must be able to handle encrypted blobsencrypt all inter-node communication, and authenticate and sign all messages. This adds some overhead to the system but the cost of overhead is far smaller than the cost of a breach, and if minimum security is the baseline then the overhead is just an accepted part of doing business.

For inter-replica communication we are currently using gRPC, an multi-platform RPC framework that uses protocol buffers for message serialization (we have also used zeromq in the past). The nice part about gRPC is that it has authentication baked-in and promotes the use of SSL/TLS to authenticate and encrypt exchanges. The not so nice part is that while the gRPC tutorial has examples in RubyC++C#PythonJavaNode.js, and PHP there is no guide for Go (at the time of this post). This post is my attempt to figure it out.

From the documentation:

gRPC has SSL/TLS integration and promotes the use of SSL/TLS to authenticate the server, and encrypt all the data exchanged between the client and the server. Optional mechanisms are available for clients to provide certificates for mutual authentication.

I’m primarily interested in the first part — authenticate the server and encrypt the data exchanged. However, the idea of mutual TLS is something I hadn’t considered before this investigation. My original plan was to use Hawk client authentication and message signatures. But potentially that’s not something I have to do. So this post has two phases:

  1. Encrypted communication using TLS/SSL from the server
  2. Authenticated, mutual TLS using a certificate authority

Since all replicas in my system are both servers and clients, I think that it wouldn’t make much sense not to do mutual TLS. After all, we’re already creating certificates and exchanging keys and whatnot.

Creating SSL/TLS Certificates

It seems like step one is to generate certificates and key files for encrypting communication. I thought this would be fairly straightforward using openssl from the command line, and it is (kind of) though there are a lot of things to consider. First, the files we need to generate:

  • server.key: a private RSA key to sign and authenticate the public key
  • server.pem/server.crt: self-signed X.509 public keys for distribution
  • rootca.crt: a certificate authority public key for signing .csr files
  • host.csr: a certificate signing request to access the CA

So there are a lot of files and a lot of extensions, many of which are duplicates or synonyms (or simply different encodings). I think that’s primarily what’s made this process so difficult. So to generate some simple .key/.crt pairs using openssl:

$ openssl genrsa -out server.key 2048
$ openssl req -new -x509 -sha256 -key server.key \
-out server.crt -days 3650

The first command will generate a 2048 bit RSA key (stronger keys are available as well). The second command will generate the certificate, and will also prompt you for some questions about the location, organization, and contact of the certificate holder. These fields are pretty straight forward, but probably the most important field is the “Common Name” which is typically composed of the host, domain, or IP address related to the certificate. The name is then used during verification and if the host doesn’t match the common name a warning is raised.

Finally, to generate a certificate signing request (.csr) using openssl:

$ openssl req -new -sha256 -key server.key -out server.csr
$ openssl x509 -req -sha256 -in server.csr -signkey server.key \
-out server.crt -days 3650

So this is pretty straightforward on the command line. However, it may be simpler to use certstrap, a simple certificate manager written in Go by the folks at Square. The app avoids dealing with openssl (and therefore raises questions about security in implementation), but has a very simple workflow: create a certificate authority, sign certificates with it.

To create a new certificate authority:

$ certstrap init --common-name "umd.fluidfs.com"
Created out/umd.fluidfs.com.key
Created out/umd.fluidfs.com.crt
Created out/umd.fluidfs.com.crl

To request a certificate for a specific host:

$ certstrap request-cert -ip 192.168.1.18
Created out/192.168.1.18.key
Created out/192.168.1.18.csr

And finally to generate the certificate for the host:

$ certstrap sign 192.168.1.18 --CA umd.fluidfs.com
Created out/192.168.1.18.crt from out/192.168.1.18.csr signed by
out/umd.fluidfs.com.key

Probably the most interesting opportunity for me is the ability to use certstrapprogrammatically to automatically generate keys. However, some review will have to be done into how safe it is.

Encrypted Server

The simplest method to encrypt communication using gRPC is to use server-side TLS. This means that the server needs to be initialized with a public/private key pair and the client needs to have the server’s public key in order to make the connection. I’ve created a small application called sping (secure ping) that basically does an echo request from a client to a server (example repository). The server code is as follows:


var (
crt = "server.crt"
key = "server.key"
) func (s *PingServer) Serve(addr string) error { // Create the channel to listen on
lis, err := net.Listen("tcp", addr)
if err != nil {
return fmt.Errorf("could not list on %s: %s", addr, err)
} // Create the TLS credentials
creds, err := credentials.NewServerTLSFromFile(crt, key)
if err != nil {
return fmt.Errorf("could not load TLS keys: %s", err)
} // Create the gRPC server with the credentials
srv := grpc.NewServer(grpc.Creds(creds)) // Register the handler object
pb.RegisterSecurePingServer(srv, s) // Serve and Listen
if err := srv.Serve(lis); err != nil {
return fmt.Errorf("grpc serve error: %s", err)
} return nil
}

So the steps to the server are pretty straight forward. First, create a TCP connection on the desired address (e.g. pass in ":3264" to listen on the external address on port 3264). Second, load the TLS credentials from their respective key files (both the private and the public keys), then initialize the grpc server with the credentials. Finally, register the handler for the service you implemented (here I’m using a method call on a struct that does implement the handler) and serve.

To get the client connected, you need to give it the server.crt (or server.pem) public key. In normal operation, this key can be fetched from a certificate authority, but since we’re doing internal RPC, the public key must be shipped with the application.

var cert = "server.crt"

func (c *PingClient) Ping(addr string, ping *pb.Ping) error {

    // Create the client TLS credentials
creds, err := credentials.NewClientTLSFromFile(cert, "")
if err != nil {
return fmt.Errorf("could not load tls cert: %s", err)
} // Create a connection with the TLS credentials
conn, err := grpc.Dial(addr, grpc.WithTransportCredentials(creds))
if err != nil {
return fmt.Errorf("could not dial %s: %s", addr, err)
} // Initialize the client and make the request
client := pb.NewSecurePingClient(conn)
pong, err := client.Echo(context.Background(), ping)
if err != nil {
return fmt.Errof("could not ping %s: %s", addr, err)
} // Log the ping
log.Printf("%s\n", pong.String())
return nil
}

Again, this is a fairly straight forward process that adds only three lines and modifies one from the original code. First load the server public key from a file into the credentials object, then pass the transport credentials into the grpc dialer. This will cause GRPC to initiate the TLS handshake every time it sends an echo RPC.

Mutual TLS with Certificate Authority

The real problem with using the above method and HAWK authentication is that every single replica will have to maintain both a server public key and a HAWK key for every other node in the system. That frankly sounds like a headache to me. Instead, we’ll have every replica (client and server both) load their own public/private key pairs, then load the public keys of a CA (certificate authority) .crt file. Because all client public keys are signed by the CA key, the server and client can exchange and authenticate private keys during communication.

CAVEAT: when a client connects to a server, it must know the ServerNameproperty to pass into the tls.Config object. This ServerName appears to have to be in agreement with the common name in the certificate.

The server code is now modified to create X.509 key pairs directly and to create a certificate pool based on the certificate authority public key.

var (
crt = "server.crt"
key = "server.key"
ca = "ca.crt"
) func (s *PingServer) Serve(addr string) error { // Load the certificates from disk
certificate, err := tls.LoadX509KeyPair(crt, key)
if err != nil {
return fmt.Errorf("could not load server key pair: %s", err)
} // Create a certificate pool from the certificate authority
certPool := x509.NewCertPool()
ca, err := ioutil.ReadFile(ca)
if err != nil {
return fmt.Errorf("could not read ca certificate: %s", err)
} // Append the client certificates from the CA
if ok := certPool.AppendCertsFromPEM(ca); !ok {
return errors.New("failed to append client certs")
} // Create the channel to listen on
lis, err := net.Listen("tcp", addr)
if err != nil {
return fmt.Errorf("could not list on %s: %s", addr, err)
} // Create the TLS credentials
creds := credentials.NewTLS(&tls.Config{
ClientAuth: tls.RequireAndVerifyClientCert,
Certificates: []tls.Certificate{certificate},
ClientCAs: certPool,
}) // Create the gRPC server with the credentials
srv := grpc.NewServer(grpc.Creds(creds)) // Register the handler object
pb.RegisterSecurePingServer(srv, s) // Serve and Listen
if err := srv.Serve(lis); err != nil {
return fmt.Errorf("grpc serve error: %s", err)
} return nil
}

So quite a bit more work here than in the first version. First, we load the server key pair from disk into a tls.Certificate struct. Then we create a certificate pool, read the CA certificate from disk and append it to the pool. That done, we can create our TLS credentials. Importantly, our server will require client certificates for verification, and we specify the pool as our client certificate authority. Finally we pass our certificates into the configuration and create new TLS grpc server options, passing them into the grpc.NewServer function. The client code is very similar:

var (
crt = "client.crt"
key = "client.key"
ca = "ca.crt"
) func (c *PingClient) Ping(addr string, ping *pb.Ping) error { // Load the client certificates from disk
certificate, err := tls.LoadX509KeyPair(crt, key)
if err != nil {
return fmt.Errorf("could not load client key pair: %s", err)
} // Create a certificate pool from the certificate authority
certPool := x509.NewCertPool()
ca, err := ioutil.ReadFile(ca)
if err != nil {
return fmt.Errorf("could not read ca certificate: %s", err)
} // Append the certificates from the CA
if ok := certPool.AppendCertsFromPEM(ca); !ok {
return errors.New("failed to append ca certs")
} creds := credentials.NewTLS(&tls.Config{
ServerName: addr, // NOTE: this is required!
Certificates: []tls.Certificate{certificate},
RootCAs: certPool,
}) // Create a connection with the TLS credentials
conn, err := grpc.Dial(addr, grpc.WithTransportCredentials(creds))
if err != nil {
return fmt.Errorf("could not dial %s: %s", addr, err)
} // Initialize the client and make the request
client := pb.NewSecurePingClient(conn)
pong, err := client.Echo(context.Background(), ping)
if err != nil {
return fmt.Errof("could not ping %s: %s", addr, err)
} // Log the ping
log.Printf("%s\n", pong.String())
return nil
}

The primary difference here being that we load client certificates as opposed to the server certificate and that we specify RootCAs instead of ClientCAs in the TLS config. One final, important point, is that we also must specify the ServerName, whose value must match the common name on the certificate.

Go Client

In this section, I will describe the method for a client connecting to a secure RPC in the same style as the gRPC authentication examples. These examples use the greeter quick start code and perhaps they can be contributed back to the grpc.io documentation. Frankly, though, they’re just a guess so hopefully the PR I submitted gets reviewed thoroughly.

Base case - No encryption or authentication

import (
"google.golang.org/grpc"
pb "google.golang.org/grpc/examples/helloworld/helloworld"
) channel, _ := grpc.Dial("localhost:50051", grpc.WithInsecure())
client := pb.NewGreeterClient(channel)

With server authentication SSL/TLS

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

creds := credentials.NewClientTLSFromFile("roots.pem", "")
channel, _ := grpc.Dial(
"localhost:443", grpc.WithTransportCredentials(creds)
)
client := pb.NewGreeterClient(channel)

Authenticate with Google

import "google.golang.org/grpc/credentials/oauth"

auth, _ := oauth.NewApplicationDefault(context.Background(), "")
channel, _ := grpc.Dial(
"greeter.googleapis.com", grpc.WithPerRPCCredentials(auth)
)
client := pb.NewGreeterClient(channel)

Conclusion

Always use SSL/TLS to encrypt communications and authenticate nodes. It is an open question about how to manage certificates in a larger system, but potentially an internal certificate authority resolves these problems. Getting secure communications up and running isn’t necessarily the easiest part of distributed systems, but it is worth taking the time out to do it right. And finally, gRPC, please update your documentation.

Other Resources:

Secure gRPC with TLS/SSL的更多相关文章

  1. How to secure remote desktop connections using TLS/SSL

    How to secure remote desktop connections using TLS/SSL based authentication Requirement When you ena ...

  2. Go gRPC进阶-TLS认证+自定义方法认证(七)

    前言 前面篇章的gRPC都是明文传输的,容易被篡改数据.本章将介绍如何为gRPC添加安全机制,包括TLS证书认证和Token认证. TLS证书认证 什么是TLS TLS(Transport Layer ...

  3. Node.js TLS/SSL

    Stability: 3 - Stable 可以使用 require('tls') 来访问这个模块. tls 模块 使用 OpenSSL 来提供传输层(Transport Layer)安全性和(或)安 ...

  4. Android : 关于HTTPS、TLS/SSL认证以及客户端证书导入方法

    一.HTTPS 简介 HTTPS 全称 HTTP over TLS/SSL(TLS就是SSL的新版本3.1).TLS/SSL是在传输层上层的协议,应用层的下层,作为一个安全层而存在,翻译过来一般叫做传 ...

  5. TLS协议工作过程;如何应用TLS/SSL协议为WEB流量提供安全

      SSL/TLS协议的基本过程是这样的: 客户端向服务器端索要并验证公钥. 双方协商生成"对话密钥". 双方采用"对话密钥"进行加密通信.   上面过程的前两 ...

  6. TLS/SSL 梳理

    数据加密通篇都是为了防止第三方的劫持伪造,保证连接安全, 毫无遮掩的明文传输只有民风淳朴的时候才是安全的. 先是一些基础的内容: 对称加密 最开始为了对数据进行加密,使用的是对称加密算法,即双方协商好 ...

  7. 你不可不知的WEB安全知识(第一部分:HTTPS, TLS, SSL, CORS, CSP)

    译   原文地址:https://dev.to/ahmedatefae/web-security-knowledge-you-must-understand-it-part-i-https-tls-s ...

  8. 使用sslsplit嗅探tls/ssl连接

    首先发一个从youtube弄到的sslsplit的使用教程 http://v.qq.com/page/x/k/s/x019634j4ks.html 我最近演示了如何使用mitmproxty执行中间人攻 ...

  9. SSH/HTTPS安全的本质都依赖于TLS/SSL

    1.SSH/HTTPS的安全本质是TLS/SSL. 2.1990年互联网上的网页主要是静态内容,作为信息发布,使用HTTP明文传输是可以的.不过,后来很多公司开始使用网页进行金融交易,例如:股票,于是 ...

随机推荐

  1. windows下配置ssh访问github

    一.说明 一直使用HTTPS的方式访问github的代码,用的时间长了,发现这是效率很低的一种开发行为,因为每次git push的时候都要输入username和password.今天就介绍如何在win ...

  2. Android中创建倒影效果的工具类

                     一.有时候我们需要创建倒影的效果,我们接触最多的都是图片能够创建倒影,而布局依然可以创建倒影.       二.工具类代码 import android.graphi ...

  3. git for windows配置SSH key

    0. 前言 之前用过一段时间的git,后来迁移系统导致电脑中的git bash消失了,由于在上家公司版本管理用的svn,所以一直没有重新配置,目前工作中版本管理用的gitLab,后期计划将工作之外的精 ...

  4. java struts2入门学习---自定义类型转换

    自定义类型转换器的作用就是将struts无法识别的类型转换成自己所需要的. 比如输入:广东-东莞-虎门,对应的输出时能输出:广东省 东莞市 虎门(镇/区) 这里涉及到的知识点即是将String转换为任 ...

  5. 你想要的iOS 小技巧总结

    UITableView的Group样式下顶部空白处理 //分组列表头部空白处理 UIView *view = [[UIView alloc] initWithFrame:CGRectMake(, , ...

  6. memcached全面剖析--4. memcached的分布式算法

    我是Mixi的长野. 第2次.第3次由前坂介绍了memcached的内部情况.本次不再介绍memcached的内部结构,开始介绍memcached的分布式. memcached的分布式 正如第1次中介 ...

  7. nginx 中文和英文资料

    http://www.nginx.cn/doc/ http://manual.51yip.com/nginx/ http://tool.oschina.net/apidocs/apidoc?api=n ...

  8. log4j(二)——如何控制日志信息的输出?

    一:测试环境与log4j(一)——为什么要使用log4j?一样,这里不再重述 二:先看栗子再来下结论 import org.apache.log4j.*; import test.log4j.bean ...

  9. OpenCV 学习笔记 02 处理文件、摄像头和图形用户界面

    在处理文件前需要引入OpenCV库,同时也引入unmpy库 import cv2 import numpy as np 1 基本的读写操作 1.1 图像文件的读写操作 1.1.1 图像文件的读取操作 ...

  10. linux手工释放内存

    先使用sync命令以确保文件系统的完整性,sync 命令运行 sync 子例程,将所有未写的系统缓冲区写到磁盘中,包含已修改的 i-node.已延迟的块 I/O 和读写映射文件. 再执行下面任意一条命 ...