forked from TrueCloudLab/certificates
Merge pull request #33 from smallstep/hello-go-grpc
hello-mTLS example using gRPC in go
This commit is contained in:
commit
7893f84677
11 changed files with 735 additions and 12 deletions
|
@ -59,7 +59,21 @@ languages are appreciated!
|
||||||
- [ ] Root certificate rotation
|
- [ ] Root certificate rotation
|
||||||
- [X] Client using autocert root certificate
|
- [X] Client using autocert root certificate
|
||||||
- [X] mTLS (send client certificate if server asks for it)
|
- [X] mTLS (send client certificate if server asks for it)
|
||||||
- [ ] Automatic certificate rotation
|
- [X] Automatic certificate rotation
|
||||||
|
- [X] Restrict to safe ciphersuites and TLS versions
|
||||||
|
- [ ] TLS stack configuration loaded from `step-ca`
|
||||||
|
- [ ] Root certificate rotation
|
||||||
|
|
||||||
|
[go-grpc/](go-grpc/)
|
||||||
|
- [X] Server using autocert certificate & key
|
||||||
|
- [X] mTLS (client authentication using internal root certificate)
|
||||||
|
- [X] Automatic certificate renewal
|
||||||
|
- [X] Restrict to safe ciphersuites and TLS versions
|
||||||
|
- [ ] TLS stack configuration loaded from `step-ca`
|
||||||
|
- [ ] Root certificate rotation
|
||||||
|
- [X] Client using autocert root certificate
|
||||||
|
- [X] mTLS (send client certificate if server asks for it)
|
||||||
|
- [X] Automatic certificate rotation
|
||||||
- [X] Restrict to safe ciphersuites and TLS versions
|
- [X] Restrict to safe ciphersuites and TLS versions
|
||||||
- [ ] TLS stack configuration loaded from `step-ca`
|
- [ ] TLS stack configuration loaded from `step-ca`
|
||||||
- [ ] Root certificate rotation
|
- [ ] Root certificate rotation
|
||||||
|
|
|
@ -0,0 +1,16 @@
|
||||||
|
# build stage
|
||||||
|
FROM golang:alpine AS build-env
|
||||||
|
RUN apk update
|
||||||
|
RUN apk add git
|
||||||
|
RUN mkdir /src
|
||||||
|
|
||||||
|
WORKDIR /go/src/github.com/smallstep/certificates/autocert/examples/hello-mtls/go-grpc
|
||||||
|
ADD client/client.go .
|
||||||
|
COPY hello hello
|
||||||
|
RUN go get -d -v ./...
|
||||||
|
RUN go build -o client
|
||||||
|
|
||||||
|
# final stage
|
||||||
|
FROM alpine
|
||||||
|
COPY --from=build-env /go/src/github.com/smallstep/certificates/autocert/examples/hello-mtls/go-grpc/client .
|
||||||
|
CMD ["./client"]
|
164
autocert/examples/hello-mtls/go-grpc/client/client.go
Normal file
164
autocert/examples/hello-mtls/go-grpc/client/client.go
Normal file
|
@ -0,0 +1,164 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/tls"
|
||||||
|
"crypto/x509"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"google.golang.org/grpc"
|
||||||
|
"google.golang.org/grpc/credentials"
|
||||||
|
|
||||||
|
"github.com/smallstep/certificates/autocert/examples/hello-mtls/go-grpc/hello"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
autocertFile = "/var/run/autocert.step.sm/site.crt"
|
||||||
|
autocertKey = "/var/run/autocert.step.sm/site.key"
|
||||||
|
autocertRoot = "/var/run/autocert.step.sm/root.crt"
|
||||||
|
requestFrequency = 5 * time.Second
|
||||||
|
tickFrequency = 15 * time.Second
|
||||||
|
)
|
||||||
|
|
||||||
|
// Uses techniques from https://diogomonica.com/2017/01/11/hitless-tls-certificate-rotation-in-go/
|
||||||
|
// to automatically rotate certificates when they're renewed.
|
||||||
|
|
||||||
|
type rotator struct {
|
||||||
|
sync.RWMutex
|
||||||
|
certificate *tls.Certificate
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *rotator) getClientCertificate(cri *tls.CertificateRequestInfo) (*tls.Certificate, error) {
|
||||||
|
r.RLock()
|
||||||
|
defer r.RUnlock()
|
||||||
|
return r.certificate, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *rotator) loadCertificate(certFile, keyFile string) error {
|
||||||
|
r.Lock()
|
||||||
|
defer r.Unlock()
|
||||||
|
|
||||||
|
c, err := tls.LoadX509KeyPair(certFile, keyFile)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
r.certificate = &c
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadRootCertPool() (*x509.CertPool, error) {
|
||||||
|
root, err := ioutil.ReadFile(autocertRoot)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
pool := x509.NewCertPool()
|
||||||
|
if ok := pool.AppendCertsFromPEM(root); !ok {
|
||||||
|
return nil, errors.New("Missing or invalid root certificate")
|
||||||
|
}
|
||||||
|
|
||||||
|
return pool, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func sayHello(c hello.GreeterClient) error {
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
r, err := c.SayHello(ctx, &hello.HelloRequest{Name: "world"})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
log.Printf("Greeting: %s", r.Message)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func sayHelloAgain(c hello.GreeterClient) error {
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
r, err := c.SayHelloAgain(ctx, &hello.HelloRequest{Name: "world"})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
log.Printf("Greeting: %s", r.Message)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
// Read the root certificate for our CA from disk
|
||||||
|
roots, err := loadRootCertPool()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load certificate
|
||||||
|
r := &rotator{}
|
||||||
|
if err := r.loadCertificate(autocertFile, autocertKey); err != nil {
|
||||||
|
log.Fatal("error loading certificate and key", err)
|
||||||
|
}
|
||||||
|
tlsConfig := &tls.Config{
|
||||||
|
RootCAs: roots,
|
||||||
|
MinVersion: tls.VersionTLS12,
|
||||||
|
CurvePreferences: []tls.CurveID{tls.CurveP521, tls.CurveP384, tls.CurveP256},
|
||||||
|
CipherSuites: []uint16{
|
||||||
|
tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305,
|
||||||
|
tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
|
||||||
|
},
|
||||||
|
// GetClientCertificate is called when a server requests a
|
||||||
|
// certificate from a client.
|
||||||
|
//
|
||||||
|
// In this example keep alives will cause the certificate to
|
||||||
|
// only be called once, but if we disable them,
|
||||||
|
// GetClientCertificate will be called on every request.
|
||||||
|
GetClientCertificate: r.getClientCertificate,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Schedule periodic re-load of certificate
|
||||||
|
// A real implementation can use something like
|
||||||
|
// https://github.com/fsnotify/fsnotify
|
||||||
|
done := make(chan struct{})
|
||||||
|
go func() {
|
||||||
|
ticker := time.NewTicker(tickFrequency)
|
||||||
|
defer ticker.Stop()
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ticker.C:
|
||||||
|
fmt.Println("Checking for new certificate...")
|
||||||
|
err := r.loadCertificate(autocertFile, autocertKey)
|
||||||
|
if err != nil {
|
||||||
|
log.Println("Error loading certificate and key", err)
|
||||||
|
}
|
||||||
|
case <-done:
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
defer close(done)
|
||||||
|
|
||||||
|
// Set up a connection to the server.
|
||||||
|
address := os.Getenv("HELLO_MTLS_URL")
|
||||||
|
conn, err := grpc.Dial(address, grpc.WithTransportCredentials(credentials.NewTLS(tlsConfig)))
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("did not connect: %v", err)
|
||||||
|
}
|
||||||
|
defer conn.Close()
|
||||||
|
client := hello.NewGreeterClient(conn)
|
||||||
|
|
||||||
|
for {
|
||||||
|
if err := sayHello(client); err != nil {
|
||||||
|
log.Fatalf("could not greet: %v", err)
|
||||||
|
}
|
||||||
|
if err := sayHelloAgain(client); err != nil {
|
||||||
|
log.Fatalf("could not greet: %v", err)
|
||||||
|
}
|
||||||
|
time.Sleep(requestFrequency)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,22 @@
|
||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: hello-mtls-client
|
||||||
|
labels: {app: hello-mtls-client}
|
||||||
|
spec:
|
||||||
|
replicas: 1
|
||||||
|
selector: {matchLabels: {app: hello-mtls-client}}
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
annotations:
|
||||||
|
autocert.step.sm/name: hello-mtls-client.default.pod.cluster.local
|
||||||
|
labels: {app: hello-mtls-client}
|
||||||
|
spec:
|
||||||
|
containers:
|
||||||
|
- name: hello-mtls-client
|
||||||
|
image: hello-mtls-client-go-grpc:latest
|
||||||
|
imagePullPolicy: Never
|
||||||
|
resources: {requests: {cpu: 10m, memory: 20Mi}}
|
||||||
|
env:
|
||||||
|
- name: HELLO_MTLS_URL
|
||||||
|
value: hello-mtls.default.svc.cluster.local:443
|
231
autocert/examples/hello-mtls/go-grpc/hello/hello.pb.go
Normal file
231
autocert/examples/hello-mtls/go-grpc/hello/hello.pb.go
Normal file
|
@ -0,0 +1,231 @@
|
||||||
|
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||||
|
// source: hello.proto
|
||||||
|
|
||||||
|
package hello
|
||||||
|
|
||||||
|
import proto "github.com/golang/protobuf/proto"
|
||||||
|
import fmt "fmt"
|
||||||
|
import math "math"
|
||||||
|
|
||||||
|
import (
|
||||||
|
context "golang.org/x/net/context"
|
||||||
|
grpc "google.golang.org/grpc"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Reference imports to suppress errors if they are not otherwise used.
|
||||||
|
var _ = proto.Marshal
|
||||||
|
var _ = fmt.Errorf
|
||||||
|
var _ = math.Inf
|
||||||
|
|
||||||
|
// This is a compile-time assertion to ensure that this generated file
|
||||||
|
// is compatible with the proto package it is being compiled against.
|
||||||
|
// A compilation error at this line likely means your copy of the
|
||||||
|
// proto package needs to be updated.
|
||||||
|
const _ = proto.ProtoPackageIsVersion2 // please upgrade the proto package
|
||||||
|
|
||||||
|
// The request message containing the user's name.
|
||||||
|
type HelloRequest struct {
|
||||||
|
Name string `protobuf:"bytes,1,opt,name=name" json:"name,omitempty"`
|
||||||
|
XXX_NoUnkeyedLiteral struct{} `json:"-"`
|
||||||
|
XXX_unrecognized []byte `json:"-"`
|
||||||
|
XXX_sizecache int32 `json:"-"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *HelloRequest) Reset() { *m = HelloRequest{} }
|
||||||
|
func (m *HelloRequest) String() string { return proto.CompactTextString(m) }
|
||||||
|
func (*HelloRequest) ProtoMessage() {}
|
||||||
|
func (*HelloRequest) Descriptor() ([]byte, []int) {
|
||||||
|
return fileDescriptor_hello_4c93420831fe68fb, []int{0}
|
||||||
|
}
|
||||||
|
func (m *HelloRequest) XXX_Unmarshal(b []byte) error {
|
||||||
|
return xxx_messageInfo_HelloRequest.Unmarshal(m, b)
|
||||||
|
}
|
||||||
|
func (m *HelloRequest) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
|
||||||
|
return xxx_messageInfo_HelloRequest.Marshal(b, m, deterministic)
|
||||||
|
}
|
||||||
|
func (dst *HelloRequest) XXX_Merge(src proto.Message) {
|
||||||
|
xxx_messageInfo_HelloRequest.Merge(dst, src)
|
||||||
|
}
|
||||||
|
func (m *HelloRequest) XXX_Size() int {
|
||||||
|
return xxx_messageInfo_HelloRequest.Size(m)
|
||||||
|
}
|
||||||
|
func (m *HelloRequest) XXX_DiscardUnknown() {
|
||||||
|
xxx_messageInfo_HelloRequest.DiscardUnknown(m)
|
||||||
|
}
|
||||||
|
|
||||||
|
var xxx_messageInfo_HelloRequest proto.InternalMessageInfo
|
||||||
|
|
||||||
|
func (m *HelloRequest) GetName() string {
|
||||||
|
if m != nil {
|
||||||
|
return m.Name
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// The response message containing the greetings
|
||||||
|
type HelloReply struct {
|
||||||
|
Message string `protobuf:"bytes,1,opt,name=message" json:"message,omitempty"`
|
||||||
|
XXX_NoUnkeyedLiteral struct{} `json:"-"`
|
||||||
|
XXX_unrecognized []byte `json:"-"`
|
||||||
|
XXX_sizecache int32 `json:"-"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *HelloReply) Reset() { *m = HelloReply{} }
|
||||||
|
func (m *HelloReply) String() string { return proto.CompactTextString(m) }
|
||||||
|
func (*HelloReply) ProtoMessage() {}
|
||||||
|
func (*HelloReply) Descriptor() ([]byte, []int) {
|
||||||
|
return fileDescriptor_hello_4c93420831fe68fb, []int{1}
|
||||||
|
}
|
||||||
|
func (m *HelloReply) XXX_Unmarshal(b []byte) error {
|
||||||
|
return xxx_messageInfo_HelloReply.Unmarshal(m, b)
|
||||||
|
}
|
||||||
|
func (m *HelloReply) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) {
|
||||||
|
return xxx_messageInfo_HelloReply.Marshal(b, m, deterministic)
|
||||||
|
}
|
||||||
|
func (dst *HelloReply) XXX_Merge(src proto.Message) {
|
||||||
|
xxx_messageInfo_HelloReply.Merge(dst, src)
|
||||||
|
}
|
||||||
|
func (m *HelloReply) XXX_Size() int {
|
||||||
|
return xxx_messageInfo_HelloReply.Size(m)
|
||||||
|
}
|
||||||
|
func (m *HelloReply) XXX_DiscardUnknown() {
|
||||||
|
xxx_messageInfo_HelloReply.DiscardUnknown(m)
|
||||||
|
}
|
||||||
|
|
||||||
|
var xxx_messageInfo_HelloReply proto.InternalMessageInfo
|
||||||
|
|
||||||
|
func (m *HelloReply) GetMessage() string {
|
||||||
|
if m != nil {
|
||||||
|
return m.Message
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
proto.RegisterType((*HelloRequest)(nil), "HelloRequest")
|
||||||
|
proto.RegisterType((*HelloReply)(nil), "HelloReply")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reference imports to suppress errors if they are not otherwise used.
|
||||||
|
var _ context.Context
|
||||||
|
var _ grpc.ClientConn
|
||||||
|
|
||||||
|
// This is a compile-time assertion to ensure that this generated file
|
||||||
|
// is compatible with the grpc package it is being compiled against.
|
||||||
|
const _ = grpc.SupportPackageIsVersion4
|
||||||
|
|
||||||
|
// Client API for Greeter service
|
||||||
|
|
||||||
|
type GreeterClient interface {
|
||||||
|
// Sends a greeting
|
||||||
|
SayHello(ctx context.Context, in *HelloRequest, opts ...grpc.CallOption) (*HelloReply, error)
|
||||||
|
// Sends another greeting
|
||||||
|
SayHelloAgain(ctx context.Context, in *HelloRequest, opts ...grpc.CallOption) (*HelloReply, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type greeterClient struct {
|
||||||
|
cc *grpc.ClientConn
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewGreeterClient(cc *grpc.ClientConn) GreeterClient {
|
||||||
|
return &greeterClient{cc}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *greeterClient) SayHello(ctx context.Context, in *HelloRequest, opts ...grpc.CallOption) (*HelloReply, error) {
|
||||||
|
out := new(HelloReply)
|
||||||
|
err := grpc.Invoke(ctx, "/Greeter/SayHello", in, out, c.cc, opts...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *greeterClient) SayHelloAgain(ctx context.Context, in *HelloRequest, opts ...grpc.CallOption) (*HelloReply, error) {
|
||||||
|
out := new(HelloReply)
|
||||||
|
err := grpc.Invoke(ctx, "/Greeter/SayHelloAgain", in, out, c.cc, opts...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Server API for Greeter service
|
||||||
|
|
||||||
|
type GreeterServer interface {
|
||||||
|
// Sends a greeting
|
||||||
|
SayHello(context.Context, *HelloRequest) (*HelloReply, error)
|
||||||
|
// Sends another greeting
|
||||||
|
SayHelloAgain(context.Context, *HelloRequest) (*HelloReply, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
func RegisterGreeterServer(s *grpc.Server, srv GreeterServer) {
|
||||||
|
s.RegisterService(&_Greeter_serviceDesc, srv)
|
||||||
|
}
|
||||||
|
|
||||||
|
func _Greeter_SayHello_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||||
|
in := new(HelloRequest)
|
||||||
|
if err := dec(in); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if interceptor == nil {
|
||||||
|
return srv.(GreeterServer).SayHello(ctx, in)
|
||||||
|
}
|
||||||
|
info := &grpc.UnaryServerInfo{
|
||||||
|
Server: srv,
|
||||||
|
FullMethod: "/Greeter/SayHello",
|
||||||
|
}
|
||||||
|
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||||
|
return srv.(GreeterServer).SayHello(ctx, req.(*HelloRequest))
|
||||||
|
}
|
||||||
|
return interceptor(ctx, in, info, handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
func _Greeter_SayHelloAgain_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||||
|
in := new(HelloRequest)
|
||||||
|
if err := dec(in); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if interceptor == nil {
|
||||||
|
return srv.(GreeterServer).SayHelloAgain(ctx, in)
|
||||||
|
}
|
||||||
|
info := &grpc.UnaryServerInfo{
|
||||||
|
Server: srv,
|
||||||
|
FullMethod: "/Greeter/SayHelloAgain",
|
||||||
|
}
|
||||||
|
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||||
|
return srv.(GreeterServer).SayHelloAgain(ctx, req.(*HelloRequest))
|
||||||
|
}
|
||||||
|
return interceptor(ctx, in, info, handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
var _Greeter_serviceDesc = grpc.ServiceDesc{
|
||||||
|
ServiceName: "Greeter",
|
||||||
|
HandlerType: (*GreeterServer)(nil),
|
||||||
|
Methods: []grpc.MethodDesc{
|
||||||
|
{
|
||||||
|
MethodName: "SayHello",
|
||||||
|
Handler: _Greeter_SayHello_Handler,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
MethodName: "SayHelloAgain",
|
||||||
|
Handler: _Greeter_SayHelloAgain_Handler,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Streams: []grpc.StreamDesc{},
|
||||||
|
Metadata: "hello.proto",
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() { proto.RegisterFile("hello.proto", fileDescriptor_hello_4c93420831fe68fb) }
|
||||||
|
|
||||||
|
var fileDescriptor_hello_4c93420831fe68fb = []byte{
|
||||||
|
// 141 bytes of a gzipped FileDescriptorProto
|
||||||
|
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xe2, 0xe2, 0xce, 0x48, 0xcd, 0xc9,
|
||||||
|
0xc9, 0xd7, 0x2b, 0x28, 0xca, 0x2f, 0xc9, 0x57, 0x52, 0xe2, 0xe2, 0xf1, 0x00, 0x71, 0x83, 0x52,
|
||||||
|
0x0b, 0x4b, 0x53, 0x8b, 0x4b, 0x84, 0x84, 0xb8, 0x58, 0xf2, 0x12, 0x73, 0x53, 0x25, 0x18, 0x15,
|
||||||
|
0x18, 0x35, 0x38, 0x83, 0xc0, 0x6c, 0x25, 0x35, 0x2e, 0x2e, 0xa8, 0x9a, 0x82, 0x9c, 0x4a, 0x21,
|
||||||
|
0x09, 0x2e, 0xf6, 0xdc, 0xd4, 0xe2, 0xe2, 0xc4, 0x74, 0x98, 0x22, 0x18, 0xd7, 0x28, 0x89, 0x8b,
|
||||||
|
0xdd, 0xbd, 0x28, 0x35, 0xb5, 0x24, 0xb5, 0x48, 0x48, 0x83, 0x8b, 0x23, 0x38, 0xb1, 0x12, 0xac,
|
||||||
|
0x4b, 0x88, 0x57, 0x0f, 0xd9, 0x06, 0x29, 0x6e, 0x3d, 0x84, 0x61, 0x4a, 0x0c, 0x42, 0xba, 0x5c,
|
||||||
|
0xbc, 0x30, 0x95, 0x8e, 0xe9, 0x89, 0x99, 0x79, 0xf8, 0x95, 0x27, 0xb1, 0x81, 0x9d, 0x6d, 0x0c,
|
||||||
|
0x08, 0x00, 0x00, 0xff, 0xff, 0xa6, 0x84, 0x2d, 0xb6, 0xc5, 0x00, 0x00, 0x00,
|
||||||
|
}
|
19
autocert/examples/hello-mtls/go-grpc/hello/hello.proto
Normal file
19
autocert/examples/hello-mtls/go-grpc/hello/hello.proto
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
syntax = "proto3";
|
||||||
|
|
||||||
|
// The greeting service definition.
|
||||||
|
service Greeter {
|
||||||
|
// Sends a greeting
|
||||||
|
rpc SayHello (HelloRequest) returns (HelloReply) {}
|
||||||
|
// Sends another greeting
|
||||||
|
rpc SayHelloAgain (HelloRequest) returns (HelloReply) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The request message containing the user's name.
|
||||||
|
message HelloRequest {
|
||||||
|
string name = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The response message containing the greetings
|
||||||
|
message HelloReply {
|
||||||
|
string message = 1;
|
||||||
|
}
|
|
@ -0,0 +1,15 @@
|
||||||
|
# build stage
|
||||||
|
FROM golang:alpine AS build-env
|
||||||
|
RUN apk update
|
||||||
|
RUN apk add git
|
||||||
|
|
||||||
|
WORKDIR /go/src/github.com/smallstep/certificates/autocert/examples/hello-mtls/go-grpc
|
||||||
|
ADD server/server.go .
|
||||||
|
COPY hello hello
|
||||||
|
RUN go get -d -v ./...
|
||||||
|
RUN go build -o server
|
||||||
|
|
||||||
|
# final stage
|
||||||
|
FROM alpine
|
||||||
|
COPY --from=build-env /go/src/github.com/smallstep/certificates/autocert/examples/hello-mtls/go-grpc/server .
|
||||||
|
CMD ["./server"]
|
|
@ -0,0 +1,33 @@
|
||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
labels: {app: hello-mtls}
|
||||||
|
name: hello-mtls
|
||||||
|
spec:
|
||||||
|
type: ClusterIP
|
||||||
|
ports:
|
||||||
|
- port: 443
|
||||||
|
targetPort: 443
|
||||||
|
selector: {app: hello-mtls}
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: hello-mtls
|
||||||
|
labels: {app: hello-mtls}
|
||||||
|
spec:
|
||||||
|
replicas: 1
|
||||||
|
selector: {matchLabels: {app: hello-mtls}}
|
||||||
|
template:
|
||||||
|
metadata:
|
||||||
|
annotations:
|
||||||
|
autocert.step.sm/name: hello-mtls.default.svc.cluster.local
|
||||||
|
labels: {app: hello-mtls}
|
||||||
|
spec:
|
||||||
|
containers:
|
||||||
|
- name: hello-mtls
|
||||||
|
image: hello-mtls-server-go-grpc:latest
|
||||||
|
imagePullPolicy: Never
|
||||||
|
resources: {requests: {cpu: 10m, memory: 20Mi}}
|
151
autocert/examples/hello-mtls/go-grpc/server/server.go
Normal file
151
autocert/examples/hello-mtls/go-grpc/server/server.go
Normal file
|
@ -0,0 +1,151 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/tls"
|
||||||
|
"crypto/x509"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"log"
|
||||||
|
"net"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"google.golang.org/grpc"
|
||||||
|
"google.golang.org/grpc/credentials"
|
||||||
|
"google.golang.org/grpc/peer"
|
||||||
|
|
||||||
|
"github.com/smallstep/certificates/autocert/examples/hello-mtls/go-grpc/hello"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
autocertFile = "/var/run/autocert.step.sm/site.crt"
|
||||||
|
autocertKey = "/var/run/autocert.step.sm/site.key"
|
||||||
|
autocertRoot = "/var/run/autocert.step.sm/root.crt"
|
||||||
|
tickFrequency = 15 * time.Second
|
||||||
|
)
|
||||||
|
|
||||||
|
// Uses techniques from https://diogomonica.com/2017/01/11/hitless-tls-certificate-rotation-in-go/
|
||||||
|
// to automatically rotate certificates when they're renewed.
|
||||||
|
|
||||||
|
type rotator struct {
|
||||||
|
sync.RWMutex
|
||||||
|
certificate *tls.Certificate
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *rotator) getCertificate(hello *tls.ClientHelloInfo) (*tls.Certificate, error) {
|
||||||
|
r.RLock()
|
||||||
|
defer r.RUnlock()
|
||||||
|
return r.certificate, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *rotator) loadCertificate(certFile, keyFile string) error {
|
||||||
|
r.Lock()
|
||||||
|
defer r.Unlock()
|
||||||
|
|
||||||
|
c, err := tls.LoadX509KeyPair(certFile, keyFile)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
r.certificate = &c
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func loadRootCertPool() (*x509.CertPool, error) {
|
||||||
|
root, err := ioutil.ReadFile(autocertRoot)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
pool := x509.NewCertPool()
|
||||||
|
if ok := pool.AppendCertsFromPEM(root); !ok {
|
||||||
|
return nil, errors.New("Missing or invalid root certificate")
|
||||||
|
}
|
||||||
|
|
||||||
|
return pool, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Greeter is a service that sends greetings.
|
||||||
|
type Greeter struct{}
|
||||||
|
|
||||||
|
// SayHello sends a greeting
|
||||||
|
func (g *Greeter) SayHello(ctx context.Context, in *hello.HelloRequest) (*hello.HelloReply, error) {
|
||||||
|
return &hello.HelloReply{Message: "Hello " + in.Name + " (" + getServerName(ctx) + ")"}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// SayHelloAgain sends another greeting
|
||||||
|
func (g *Greeter) SayHelloAgain(ctx context.Context, in *hello.HelloRequest) (*hello.HelloReply, error) {
|
||||||
|
return &hello.HelloReply{Message: "Hello again " + in.Name + " (" + getServerName(ctx) + ")"}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getServerName(ctx context.Context) string {
|
||||||
|
if p, ok := peer.FromContext(ctx); ok {
|
||||||
|
if tlsInfo, ok := p.AuthInfo.(credentials.TLSInfo); ok {
|
||||||
|
return tlsInfo.State.ServerName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "unknown"
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
roots, err := loadRootCertPool()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load certificate
|
||||||
|
r := &rotator{}
|
||||||
|
if err := r.loadCertificate(autocertFile, autocertKey); err != nil {
|
||||||
|
log.Fatal("error loading certificate and key", err)
|
||||||
|
}
|
||||||
|
tlsConfig := &tls.Config{
|
||||||
|
ClientAuth: tls.RequireAndVerifyClientCert,
|
||||||
|
ClientCAs: roots,
|
||||||
|
MinVersion: tls.VersionTLS12,
|
||||||
|
CurvePreferences: []tls.CurveID{tls.CurveP521, tls.CurveP384, tls.CurveP256},
|
||||||
|
PreferServerCipherSuites: true,
|
||||||
|
CipherSuites: []uint16{
|
||||||
|
tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305,
|
||||||
|
tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
|
||||||
|
},
|
||||||
|
GetCertificate: r.getCertificate,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Schedule periodic re-load of certificate
|
||||||
|
// A real implementation can use something like
|
||||||
|
// https://github.com/fsnotify/fsnotify
|
||||||
|
done := make(chan struct{})
|
||||||
|
go func() {
|
||||||
|
ticker := time.NewTicker(tickFrequency)
|
||||||
|
defer ticker.Stop()
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ticker.C:
|
||||||
|
fmt.Println("Checking for new certificate...")
|
||||||
|
err := r.loadCertificate(autocertFile, autocertKey)
|
||||||
|
if err != nil {
|
||||||
|
log.Println("Error loading certificate and key", err)
|
||||||
|
}
|
||||||
|
case <-done:
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
defer close(done)
|
||||||
|
|
||||||
|
lis, err := net.Listen("tcp", ":443")
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("failed to listen: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
srv := grpc.NewServer(grpc.Creds(credentials.NewTLS(tlsConfig)))
|
||||||
|
hello.RegisterGreeterServer(srv, &Greeter{})
|
||||||
|
|
||||||
|
log.Println("Listening on :443")
|
||||||
|
if err := srv.Serve(lis); err != nil {
|
||||||
|
log.Fatalf("failed to serve: %v", err)
|
||||||
|
}
|
||||||
|
}
|
|
@ -10,6 +10,7 @@ import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -18,8 +19,34 @@ const (
|
||||||
autocertKey = "/var/run/autocert.step.sm/site.key"
|
autocertKey = "/var/run/autocert.step.sm/site.key"
|
||||||
autocertRoot = "/var/run/autocert.step.sm/root.crt"
|
autocertRoot = "/var/run/autocert.step.sm/root.crt"
|
||||||
requestFrequency = 5 * time.Second
|
requestFrequency = 5 * time.Second
|
||||||
|
tickFrequency = 15 * time.Second
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type rotator struct {
|
||||||
|
sync.RWMutex
|
||||||
|
certificate *tls.Certificate
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *rotator) getClientCertificate(cri *tls.CertificateRequestInfo) (*tls.Certificate, error) {
|
||||||
|
r.RLock()
|
||||||
|
defer r.RUnlock()
|
||||||
|
return r.certificate, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *rotator) loadCertificate(certFile, keyFile string) error {
|
||||||
|
r.Lock()
|
||||||
|
defer r.Unlock()
|
||||||
|
|
||||||
|
c, err := tls.LoadX509KeyPair(certFile, keyFile)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
r.certificate = &c
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func loadRootCertPool() (*x509.CertPool, error) {
|
func loadRootCertPool() (*x509.CertPool, error) {
|
||||||
root, err := ioutil.ReadFile(autocertRoot)
|
root, err := ioutil.ReadFile(autocertRoot)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -37,34 +64,64 @@ func loadRootCertPool() (*x509.CertPool, error) {
|
||||||
func main() {
|
func main() {
|
||||||
url := os.Getenv("HELLO_MTLS_URL")
|
url := os.Getenv("HELLO_MTLS_URL")
|
||||||
|
|
||||||
// Read our leaf certificate and key from disk
|
|
||||||
cert, err := tls.LoadX509KeyPair(autocertFile, autocertKey)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Read the root certificate for our CA from disk
|
// Read the root certificate for our CA from disk
|
||||||
roots, err := loadRootCertPool()
|
roots, err := loadRootCertPool()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Load certificate
|
||||||
|
r := &rotator{}
|
||||||
|
if err := r.loadCertificate(autocertFile, autocertKey); err != nil {
|
||||||
|
log.Fatal("error loading certificate and key", err)
|
||||||
|
}
|
||||||
|
|
||||||
// Create an HTTPS client using our cert, key & pool
|
// Create an HTTPS client using our cert, key & pool
|
||||||
client := &http.Client{
|
client := &http.Client{
|
||||||
Transport: &http.Transport{
|
Transport: &http.Transport{
|
||||||
TLSClientConfig: &tls.Config{
|
TLSClientConfig: &tls.Config{
|
||||||
RootCAs: roots,
|
RootCAs: roots,
|
||||||
Certificates: []tls.Certificate{cert},
|
|
||||||
MinVersion: tls.VersionTLS12,
|
MinVersion: tls.VersionTLS12,
|
||||||
CurvePreferences: []tls.CurveID{tls.CurveP521, tls.CurveP384, tls.CurveP256},
|
CurvePreferences: []tls.CurveID{tls.CurveP521, tls.CurveP384, tls.CurveP256},
|
||||||
CipherSuites: []uint16{
|
CipherSuites: []uint16{
|
||||||
tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305,
|
tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305,
|
||||||
tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
|
tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
|
||||||
},
|
},
|
||||||
|
// GetClientCertificate is called when a server requests a
|
||||||
|
// certificate from a client.
|
||||||
|
//
|
||||||
|
// In this example keep alives will cause the certificate to
|
||||||
|
// only be called once, but if we disable them,
|
||||||
|
// GetClientCertificate will be called on every request.
|
||||||
|
GetClientCertificate: r.getClientCertificate,
|
||||||
},
|
},
|
||||||
|
// Add this line to get the certificate on every request.
|
||||||
|
// DisableKeepAlives: true,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Schedule periodic re-load of certificate
|
||||||
|
// A real implementation can use something like
|
||||||
|
// https://github.com/fsnotify/fsnotify
|
||||||
|
done := make(chan struct{})
|
||||||
|
go func() {
|
||||||
|
ticker := time.NewTicker(tickFrequency)
|
||||||
|
defer ticker.Stop()
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-ticker.C:
|
||||||
|
fmt.Println("Checking for new certificate...")
|
||||||
|
err := r.loadCertificate(autocertFile, autocertKey)
|
||||||
|
if err != nil {
|
||||||
|
log.Println("Error loading certificate and key", err)
|
||||||
|
}
|
||||||
|
case <-done:
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
defer close(done)
|
||||||
|
|
||||||
for {
|
for {
|
||||||
// Make request
|
// Make request
|
||||||
r, err := client.Get(url)
|
r, err := client.Get(url)
|
||||||
|
|
|
@ -23,14 +23,13 @@ const (
|
||||||
// to automatically rotate certificates when they're renewed.
|
// to automatically rotate certificates when they're renewed.
|
||||||
|
|
||||||
type rotator struct {
|
type rotator struct {
|
||||||
sync.Mutex
|
sync.RWMutex
|
||||||
certificate *tls.Certificate
|
certificate *tls.Certificate
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *rotator) getCertificate(hello *tls.ClientHelloInfo) (*tls.Certificate, error) {
|
func (r *rotator) getCertificate(hello *tls.ClientHelloInfo) (*tls.Certificate, error) {
|
||||||
r.Lock()
|
r.RLock()
|
||||||
defer r.Unlock()
|
defer r.RUnlock()
|
||||||
|
|
||||||
return r.certificate, nil
|
return r.certificate, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -107,6 +106,8 @@ func main() {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Schedule periodic re-load of certificate
|
// Schedule periodic re-load of certificate
|
||||||
|
// A real implementation can use something like
|
||||||
|
// https://github.com/fsnotify/fsnotify
|
||||||
done := make(chan struct{})
|
done := make(chan struct{})
|
||||||
go func() {
|
go func() {
|
||||||
ticker := time.NewTicker(tickFrequency)
|
ticker := time.NewTicker(tickFrequency)
|
||||||
|
|
Loading…
Reference in a new issue