diff --git a/autocert/examples/hello-mtls/README.md b/autocert/examples/hello-mtls/README.md index f2b0fe99..d228bf13 100644 --- a/autocert/examples/hello-mtls/README.md +++ b/autocert/examples/hello-mtls/README.md @@ -64,6 +64,20 @@ languages are appreciated! - [ ] 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 + [curl/](curl/) - [X] Client - [X] mTLS (send client certificate if server asks for it) diff --git a/autocert/examples/hello-mtls/go-grpc/client/Dockerfile.client b/autocert/examples/hello-mtls/go-grpc/client/Dockerfile.client new file mode 100644 index 00000000..1e8cd10d --- /dev/null +++ b/autocert/examples/hello-mtls/go-grpc/client/Dockerfile.client @@ -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"] diff --git a/autocert/examples/hello-mtls/go-grpc/client/client.go b/autocert/examples/hello-mtls/go-grpc/client/client.go new file mode 100644 index 00000000..d3cc79c7 --- /dev/null +++ b/autocert/examples/hello-mtls/go-grpc/client/client.go @@ -0,0 +1,156 @@ +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: r.getClientCertificate, + } + + // Schedule periodic re-load of certificate + 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) + } +} diff --git a/autocert/examples/hello-mtls/go-grpc/client/hello-mtls.client.yaml b/autocert/examples/hello-mtls/go-grpc/client/hello-mtls.client.yaml new file mode 100644 index 00000000..c4546df1 --- /dev/null +++ b/autocert/examples/hello-mtls/go-grpc/client/hello-mtls.client.yaml @@ -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 diff --git a/autocert/examples/hello-mtls/go-grpc/hello/hello.pb.go b/autocert/examples/hello-mtls/go-grpc/hello/hello.pb.go new file mode 100644 index 00000000..875dabea --- /dev/null +++ b/autocert/examples/hello-mtls/go-grpc/hello/hello.pb.go @@ -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, +} diff --git a/autocert/examples/hello-mtls/go-grpc/hello/hello.proto b/autocert/examples/hello-mtls/go-grpc/hello/hello.proto new file mode 100644 index 00000000..1a332c08 --- /dev/null +++ b/autocert/examples/hello-mtls/go-grpc/hello/hello.proto @@ -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; +} diff --git a/autocert/examples/hello-mtls/go-grpc/server/Dockerfile.server b/autocert/examples/hello-mtls/go-grpc/server/Dockerfile.server new file mode 100644 index 00000000..99f443d6 --- /dev/null +++ b/autocert/examples/hello-mtls/go-grpc/server/Dockerfile.server @@ -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"] diff --git a/autocert/examples/hello-mtls/go-grpc/server/hello-mtls.server.yaml b/autocert/examples/hello-mtls/go-grpc/server/hello-mtls.server.yaml new file mode 100644 index 00000000..15853340 --- /dev/null +++ b/autocert/examples/hello-mtls/go-grpc/server/hello-mtls.server.yaml @@ -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}} diff --git a/autocert/examples/hello-mtls/go-grpc/server/server.go b/autocert/examples/hello-mtls/go-grpc/server/server.go new file mode 100644 index 00000000..b858cf7d --- /dev/null +++ b/autocert/examples/hello-mtls/go-grpc/server/server.go @@ -0,0 +1,139 @@ +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" + + "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}, 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}, nil +} + +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 + 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) + } +}