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
|
||||
- [X] Client using autocert root certificate
|
||||
- [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
|
||||
- [ ] TLS stack configuration loaded from `step-ca`
|
||||
- [ ] 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"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
|
@ -18,8 +19,34 @@ const (
|
|||
autocertKey = "/var/run/autocert.step.sm/site.key"
|
||||
autocertRoot = "/var/run/autocert.step.sm/root.crt"
|
||||
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) {
|
||||
root, err := ioutil.ReadFile(autocertRoot)
|
||||
if err != nil {
|
||||
|
@ -37,34 +64,64 @@ func loadRootCertPool() (*x509.CertPool, error) {
|
|||
func main() {
|
||||
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
|
||||
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)
|
||||
}
|
||||
|
||||
// Create an HTTPS client using our cert, key & pool
|
||||
client := &http.Client{
|
||||
Transport: &http.Transport{
|
||||
TLSClientConfig: &tls.Config{
|
||||
RootCAs: roots,
|
||||
Certificates: []tls.Certificate{cert},
|
||||
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,
|
||||
},
|
||||
// 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 {
|
||||
// Make request
|
||||
r, err := client.Get(url)
|
||||
|
|
|
@ -23,14 +23,13 @@ const (
|
|||
// to automatically rotate certificates when they're renewed.
|
||||
|
||||
type rotator struct {
|
||||
sync.Mutex
|
||||
sync.RWMutex
|
||||
certificate *tls.Certificate
|
||||
}
|
||||
|
||||
func (r *rotator) getCertificate(hello *tls.ClientHelloInfo) (*tls.Certificate, error) {
|
||||
r.Lock()
|
||||
defer r.Unlock()
|
||||
|
||||
r.RLock()
|
||||
defer r.RUnlock()
|
||||
return r.certificate, nil
|
||||
}
|
||||
|
||||
|
@ -107,6 +106,8 @@ func main() {
|
|||
}
|
||||
|
||||
// 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)
|
||||
|
|
Loading…
Reference in a new issue