diff --git a/.travis.yml b/.travis.yml index a76027f..3dd42b8 100644 --- a/.travis.yml +++ b/.travis.yml @@ -16,8 +16,8 @@ env: global: secure: RNLZ70wx1XKx4bj4SctarZVpHc++cgjmJGVvruNNLfEePdPJn19iiDGvGCLAnD554JNWlaL28FQFQFg0kOo7B1e7pJvUNOq7VEBtHOWS757Oi+avlPO5diR8g0Qcc1yZw73UJkr+XnRE/sUwdqay/NgbJAeV+Kseg79JfZnZRexjLWvGNj6GZMMo/7kzYUIcf6YaU9+kdp5lPBA2zqHLL2qOuxEp3+swYrMccnQ8tFO+BgrfvX+ger2n51csoU5AoRgBLhwhGkaGRk67ucaxR8QMtuVMYCKFQvbulRL4BgBhuyeAnp6DbSPP5169JRfcGJXnapH2dpfx1wEeuokIilLCFPmpKRNxxUt12vkIPFYNUhXbXu960oWAlAbUJ683jznVAm51k77ViFFq0gBUMoE/QG77QnPsExqrwd2sWes3qNZEzUyA4sCwhJDZ5k5BTxo0Q8n5vfKtCfZU/o7aD8QVStSVl/EwjJ9oz/Sqoz6LELWwTitx4QciPGU1MGvtXH/iQp4lDc3E7zxJRAWcOWwSPYE3pySxg2aBjNBGTXIghDkZMMqrdBUR4hPj2w3nv6QrskeJx8s7oMxCiwTOZzEQekkYDTqCOz2IEROCB9Ciqaf8hS2ihms5a8ncSWtfYCG1GPpUgot7djdMyqqhuGg+jowIZhhR3BPVgNRcHzI= -after_success: - - bash <(curl -s https://codecov.io/bash) +#after_success: +# - bash <(curl -s https://codecov.io/bash) # calls goreleaser deploy: diff --git a/README.md b/README.md index b6dbe71..7ec300c 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# intercert [![Build Status](https://travis-ci.org/evenh/intercert.svg?branch=master)](https://travis-ci.org/evenh/intercert) [![Go Report Card](https://goreportcard.com/badge/github.com/evenh/intercert)](https://goreportcard.com/report/github.com/evenh/intercert) [![codecov](https://codecov.io/gh/evenh/intercert/branch/master/graph/badge.svg)](https://codecov.io/gh/evenh/intercert) +# intercert [![Build Status](https://travis-ci.org/evenh/intercert.svg?branch=master)](https://travis-ci.org/evenh/intercert) [![Go Report Card](https://goreportcard.com/badge/github.com/evenh/intercert)](https://goreportcard.com/report/github.com/evenh/intercert) _Brings Let's Encrypt to LAN and other locked down environments._ diff --git a/api/api.pb.go b/api/api.pb.go index b2f411b..bbe4a9d 100644 --- a/api/api.pb.go +++ b/api/api.pb.go @@ -202,32 +202,119 @@ func (m *PingResponse) GetMsg() string { return "" } +type CertificateRenewalNotificationRequest struct { + // A list of DNS names to monitor for renewals + DnsNames []string `protobuf:"bytes,1,rep,name=dnsNames,proto3" json:"dnsNames,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *CertificateRenewalNotificationRequest) Reset() { *m = CertificateRenewalNotificationRequest{} } +func (m *CertificateRenewalNotificationRequest) String() string { return proto.CompactTextString(m) } +func (*CertificateRenewalNotificationRequest) ProtoMessage() {} +func (*CertificateRenewalNotificationRequest) Descriptor() ([]byte, []int) { + return fileDescriptor_00212fb1f9d3bf1c, []int{4} +} + +func (m *CertificateRenewalNotificationRequest) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_CertificateRenewalNotificationRequest.Unmarshal(m, b) +} +func (m *CertificateRenewalNotificationRequest) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_CertificateRenewalNotificationRequest.Marshal(b, m, deterministic) +} +func (m *CertificateRenewalNotificationRequest) XXX_Merge(src proto.Message) { + xxx_messageInfo_CertificateRenewalNotificationRequest.Merge(m, src) +} +func (m *CertificateRenewalNotificationRequest) XXX_Size() int { + return xxx_messageInfo_CertificateRenewalNotificationRequest.Size(m) +} +func (m *CertificateRenewalNotificationRequest) XXX_DiscardUnknown() { + xxx_messageInfo_CertificateRenewalNotificationRequest.DiscardUnknown(m) +} + +var xxx_messageInfo_CertificateRenewalNotificationRequest proto.InternalMessageInfo + +func (m *CertificateRenewalNotificationRequest) GetDnsNames() []string { + if m != nil { + return m.DnsNames + } + return nil +} + +// Response for a certificate that has been renewed on the server +type RenewedCertificateEvent struct { + // Example: foo.bar.com + DnsName string `protobuf:"bytes,1,opt,name=dnsName,proto3" json:"dnsName,omitempty"` + XXX_NoUnkeyedLiteral struct{} `json:"-"` + XXX_unrecognized []byte `json:"-"` + XXX_sizecache int32 `json:"-"` +} + +func (m *RenewedCertificateEvent) Reset() { *m = RenewedCertificateEvent{} } +func (m *RenewedCertificateEvent) String() string { return proto.CompactTextString(m) } +func (*RenewedCertificateEvent) ProtoMessage() {} +func (*RenewedCertificateEvent) Descriptor() ([]byte, []int) { + return fileDescriptor_00212fb1f9d3bf1c, []int{5} +} + +func (m *RenewedCertificateEvent) XXX_Unmarshal(b []byte) error { + return xxx_messageInfo_RenewedCertificateEvent.Unmarshal(m, b) +} +func (m *RenewedCertificateEvent) XXX_Marshal(b []byte, deterministic bool) ([]byte, error) { + return xxx_messageInfo_RenewedCertificateEvent.Marshal(b, m, deterministic) +} +func (m *RenewedCertificateEvent) XXX_Merge(src proto.Message) { + xxx_messageInfo_RenewedCertificateEvent.Merge(m, src) +} +func (m *RenewedCertificateEvent) XXX_Size() int { + return xxx_messageInfo_RenewedCertificateEvent.Size(m) +} +func (m *RenewedCertificateEvent) XXX_DiscardUnknown() { + xxx_messageInfo_RenewedCertificateEvent.DiscardUnknown(m) +} + +var xxx_messageInfo_RenewedCertificateEvent proto.InternalMessageInfo + +func (m *RenewedCertificateEvent) GetDnsName() string { + if m != nil { + return m.DnsName + } + return "" +} + func init() { proto.RegisterType((*CertificateRequest)(nil), "api.CertificateRequest") proto.RegisterType((*CertificateResponse)(nil), "api.CertificateResponse") proto.RegisterType((*PingRequest)(nil), "api.PingRequest") proto.RegisterType((*PingResponse)(nil), "api.PingResponse") + proto.RegisterType((*CertificateRenewalNotificationRequest)(nil), "api.CertificateRenewalNotificationRequest") + proto.RegisterType((*RenewedCertificateEvent)(nil), "api.RenewedCertificateEvent") } func init() { proto.RegisterFile("api.proto", fileDescriptor_00212fb1f9d3bf1c) } var fileDescriptor_00212fb1f9d3bf1c = []byte{ - // 234 bytes of a gzipped FileDescriptorProto - 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x64, 0x90, 0x4f, 0x4b, 0x03, 0x31, - 0x10, 0xc5, 0xdd, 0xae, 0x7f, 0xd8, 0xa9, 0x87, 0x76, 0x14, 0x0c, 0x3d, 0xe8, 0x92, 0x53, 0x2f, - 0xee, 0x41, 0xbf, 0x80, 0xe0, 0x49, 0x04, 0x91, 0xfd, 0x06, 0xb1, 0x8e, 0x25, 0x87, 0x64, 0x63, - 0x26, 0x15, 0xbc, 0xfb, 0xc1, 0x25, 0xd9, 0xac, 0xa6, 0xf4, 0x36, 0xf3, 0xe6, 0x97, 0xbc, 0xc7, - 0x83, 0x46, 0x39, 0xdd, 0x39, 0x3f, 0x84, 0x01, 0x6b, 0xe5, 0xb4, 0xec, 0x00, 0x1f, 0xc9, 0x07, - 0xfd, 0xa1, 0x37, 0x2a, 0x50, 0x4f, 0x9f, 0x3b, 0xe2, 0x80, 0x02, 0xce, 0xde, 0x2d, 0xbf, 0x28, - 0x43, 0xa2, 0x6a, 0xab, 0x75, 0xd3, 0x4f, 0xab, 0x34, 0x70, 0xb1, 0xc7, 0xb3, 0x1b, 0x2c, 0x13, - 0xb6, 0x30, 0xdf, 0xfc, 0xcb, 0xf9, 0x51, 0x29, 0xe1, 0x35, 0x80, 0xf3, 0xfa, 0x4b, 0x05, 0x7a, - 0xa6, 0x6f, 0x31, 0x4b, 0x40, 0xa1, 0xe0, 0x25, 0x9c, 0x58, 0x65, 0x88, 0x45, 0xdd, 0xd6, 0xeb, - 0xa6, 0x1f, 0x17, 0x79, 0x03, 0xf3, 0x57, 0x6d, 0xb7, 0x53, 0xae, 0x05, 0xd4, 0x86, 0xb7, 0xf9, - 0xfb, 0x38, 0xca, 0x16, 0xce, 0x47, 0x20, 0x07, 0xc9, 0xc4, 0xec, 0x8f, 0xb8, 0xfb, 0xa9, 0x60, - 0x59, 0x44, 0x7e, 0x62, 0xde, 0x91, 0xc7, 0x07, 0x68, 0xd2, 0x14, 0x2f, 0x78, 0xd5, 0xc5, 0x56, - 0x0e, 0x7b, 0x58, 0x89, 0xc3, 0xc3, 0xe8, 0x23, 0x8f, 0xf0, 0x16, 0x8e, 0xa3, 0x33, 0x2e, 0x12, - 0x53, 0xa4, 0x5c, 0x2d, 0x0b, 0x65, 0xc2, 0xdf, 0x4e, 0x53, 0xe9, 0xf7, 0xbf, 0x01, 0x00, 0x00, - 0xff, 0xff, 0xb3, 0x12, 0x61, 0x77, 0x81, 0x01, 0x00, 0x00, + // 300 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x7c, 0x92, 0x6f, 0x4b, 0xc3, 0x30, + 0x10, 0xc6, 0xd7, 0xd5, 0x7f, 0xbd, 0xf9, 0x62, 0x3b, 0x07, 0x0b, 0x45, 0xb4, 0x04, 0x84, 0x21, + 0x58, 0xc4, 0x7d, 0x01, 0x61, 0xf8, 0x42, 0x84, 0x29, 0xfd, 0x06, 0x71, 0x3d, 0x47, 0xc0, 0xa6, + 0xb5, 0xe9, 0x26, 0x7e, 0x65, 0x3f, 0x85, 0x34, 0x4d, 0xb7, 0x8c, 0x32, 0xdf, 0xe5, 0x9e, 0xfc, + 0x72, 0x7d, 0xee, 0xb9, 0x42, 0x20, 0x0a, 0x19, 0x17, 0x65, 0x5e, 0xe5, 0xe8, 0x8b, 0x42, 0xf2, + 0x18, 0x70, 0x4e, 0x65, 0x25, 0x3f, 0xe4, 0x52, 0x54, 0x94, 0xd0, 0xd7, 0x9a, 0x74, 0x85, 0x0c, + 0x4e, 0x53, 0xa5, 0x17, 0x22, 0x23, 0xe6, 0x45, 0xde, 0x34, 0x48, 0xda, 0x92, 0x67, 0x70, 0xb1, + 0xc7, 0xeb, 0x22, 0x57, 0x9a, 0x30, 0x82, 0xc1, 0x72, 0x27, 0xdb, 0x47, 0xae, 0x84, 0x57, 0x00, + 0x45, 0x29, 0x37, 0xa2, 0xa2, 0x17, 0xfa, 0x61, 0x7d, 0x03, 0x38, 0x0a, 0x8e, 0xe1, 0x58, 0x89, + 0x8c, 0x34, 0xf3, 0x23, 0x7f, 0x1a, 0x24, 0x4d, 0xc1, 0xaf, 0x61, 0xf0, 0x26, 0xd5, 0xaa, 0xf5, + 0x35, 0x04, 0x3f, 0xd3, 0x2b, 0xdb, 0xbe, 0x3e, 0xf2, 0x08, 0xce, 0x1b, 0xc0, 0x1a, 0xb1, 0x44, + 0x7f, 0x47, 0xcc, 0xe1, 0x66, 0xcf, 0xb1, 0xa2, 0x6f, 0xf1, 0xb9, 0xc8, 0xad, 0x20, 0x73, 0xd5, + 0x36, 0x0f, 0xe1, 0xcc, 0x4e, 0xa9, 0x99, 0x67, 0x4c, 0x6c, 0x6b, 0x3e, 0x83, 0x89, 0x79, 0x49, + 0xa9, 0xd3, 0xeb, 0x69, 0x43, 0xea, 0x9f, 0xac, 0x1e, 0x7e, 0x3d, 0x18, 0x39, 0xf8, 0xb3, 0xd6, + 0x6b, 0x2a, 0xf1, 0x11, 0x02, 0x73, 0xaa, 0x6f, 0x70, 0x12, 0xd7, 0xfb, 0xe8, 0x6e, 0x20, 0x64, + 0xdd, 0x8b, 0x66, 0x42, 0xde, 0xc3, 0x3b, 0x38, 0xaa, 0x67, 0xc6, 0xa1, 0x61, 0x9c, 0x7c, 0xc2, + 0x91, 0xa3, 0x6c, 0xf1, 0x14, 0xc6, 0xaf, 0xaa, 0x1b, 0x01, 0xde, 0x76, 0x3f, 0x71, 0x28, 0x9b, + 0xf0, 0xd2, 0xb0, 0x07, 0x22, 0xe0, 0xbd, 0x7b, 0xef, 0xfd, 0xc4, 0xfc, 0x54, 0xb3, 0xbf, 0x00, + 0x00, 0x00, 0xff, 0xff, 0xb7, 0x86, 0xa7, 0x99, 0x61, 0x02, 0x00, 0x00, } // Reference imports to suppress errors if they are not otherwise used. @@ -244,6 +331,7 @@ const _ = grpc.SupportPackageIsVersion4 type CertificateIssuerClient interface { IssueCert(ctx context.Context, in *CertificateRequest, opts ...grpc.CallOption) (*CertificateResponse, error) Ping(ctx context.Context, in *PingRequest, opts ...grpc.CallOption) (*PingResponse, error) + OnCertificateRenewal(ctx context.Context, in *CertificateRenewalNotificationRequest, opts ...grpc.CallOption) (CertificateIssuer_OnCertificateRenewalClient, error) } type certificateIssuerClient struct { @@ -272,10 +360,43 @@ func (c *certificateIssuerClient) Ping(ctx context.Context, in *PingRequest, opt return out, nil } +func (c *certificateIssuerClient) OnCertificateRenewal(ctx context.Context, in *CertificateRenewalNotificationRequest, opts ...grpc.CallOption) (CertificateIssuer_OnCertificateRenewalClient, error) { + stream, err := c.cc.NewStream(ctx, &_CertificateIssuer_serviceDesc.Streams[0], "/api.CertificateIssuer/OnCertificateRenewal", opts...) + if err != nil { + return nil, err + } + x := &certificateIssuerOnCertificateRenewalClient{stream} + if err := x.ClientStream.SendMsg(in); err != nil { + return nil, err + } + if err := x.ClientStream.CloseSend(); err != nil { + return nil, err + } + return x, nil +} + +type CertificateIssuer_OnCertificateRenewalClient interface { + Recv() (*RenewedCertificateEvent, error) + grpc.ClientStream +} + +type certificateIssuerOnCertificateRenewalClient struct { + grpc.ClientStream +} + +func (x *certificateIssuerOnCertificateRenewalClient) Recv() (*RenewedCertificateEvent, error) { + m := new(RenewedCertificateEvent) + if err := x.ClientStream.RecvMsg(m); err != nil { + return nil, err + } + return m, nil +} + // CertificateIssuerServer is the server API for CertificateIssuer service. type CertificateIssuerServer interface { IssueCert(context.Context, *CertificateRequest) (*CertificateResponse, error) Ping(context.Context, *PingRequest) (*PingResponse, error) + OnCertificateRenewal(*CertificateRenewalNotificationRequest, CertificateIssuer_OnCertificateRenewalServer) error } func RegisterCertificateIssuerServer(s *grpc.Server, srv CertificateIssuerServer) { @@ -318,6 +439,27 @@ func _CertificateIssuer_Ping_Handler(srv interface{}, ctx context.Context, dec f return interceptor(ctx, in, info, handler) } +func _CertificateIssuer_OnCertificateRenewal_Handler(srv interface{}, stream grpc.ServerStream) error { + m := new(CertificateRenewalNotificationRequest) + if err := stream.RecvMsg(m); err != nil { + return err + } + return srv.(CertificateIssuerServer).OnCertificateRenewal(m, &certificateIssuerOnCertificateRenewalServer{stream}) +} + +type CertificateIssuer_OnCertificateRenewalServer interface { + Send(*RenewedCertificateEvent) error + grpc.ServerStream +} + +type certificateIssuerOnCertificateRenewalServer struct { + grpc.ServerStream +} + +func (x *certificateIssuerOnCertificateRenewalServer) Send(m *RenewedCertificateEvent) error { + return x.ServerStream.SendMsg(m) +} + var _CertificateIssuer_serviceDesc = grpc.ServiceDesc{ ServiceName: "api.CertificateIssuer", HandlerType: (*CertificateIssuerServer)(nil), @@ -331,6 +473,12 @@ var _CertificateIssuer_serviceDesc = grpc.ServiceDesc{ Handler: _CertificateIssuer_Ping_Handler, }, }, - Streams: []grpc.StreamDesc{}, + Streams: []grpc.StreamDesc{ + { + StreamName: "OnCertificateRenewal", + Handler: _CertificateIssuer_OnCertificateRenewal_Handler, + ServerStreams: true, + }, + }, Metadata: "api.proto", } diff --git a/api/api.proto b/api/api.proto index 8f10ec2..0b97928 100644 --- a/api/api.proto +++ b/api/api.proto @@ -27,9 +27,21 @@ message PingResponse { string msg = 2; } +message CertificateRenewalNotificationRequest { + // A list of DNS names to monitor for renewals + repeated string dnsNames = 1; +} + +// Response for a certificate that has been renewed on the server +message RenewedCertificateEvent { + // Example: foo.bar.com + string dnsName = 1; +} + service CertificateIssuer { rpc IssueCert (CertificateRequest) returns (CertificateResponse) { } rpc Ping (PingRequest) returns (PingResponse) { } + rpc OnCertificateRenewal (CertificateRenewalNotificationRequest) returns (stream RenewedCertificateEvent) {} } diff --git a/client/client.go b/client/client.go index 2b5ae79..0c5282c 100644 --- a/client/client.go +++ b/client/client.go @@ -30,7 +30,13 @@ func StartClient(config *config.ClientConfig, userAgent string) { log.Infof("Configuring connection to %s for gRPC operations", config.GetDialAddr()) // Configure connection - conn, err := grpc.Dial(config.GetDialAddr(), grpc.WithInsecure(), grpc.WithUserAgent(userAgent+";")) // TODO: Not run insecure + // TODO: Not run insecure + conn, err := grpc.Dial( + config.GetDialAddr(), + grpc.WithBackoffConfig(grpc.DefaultBackoffConfig), + grpc.WithInsecure(), + grpc.WithUserAgent(userAgent+";"), + ) if err != nil { log.Warnf("Could not configure connection to host: %v", err) @@ -61,7 +67,7 @@ func StartClient(config *config.ClientConfig, userAgent string) { } // Set up scheduled tasks - tasks := configureTasks(config, certStorage) + tasks := configureTasks(client, config, certStorage) // Handle termination configureTermination(tasks) @@ -105,20 +111,17 @@ func validateConfig(c *config.ClientConfig) error { return errors.New("hostname was empty") } - if c.RenewalThreshold > (24*time.Hour)*30 { - return errors.New("renewal threshold can't exceed 30 days") - } - return nil } -func configureTasks(config *config.ClientConfig, storage *CertStorage) []Job { +func configureTasks(client api.CertificateIssuerClient, config *config.ClientConfig, storage *CertStorage) []Job { var tasks []Job - expiryCheck := *Register(findExpiredCerts(config.RenewalThreshold), "Expired certs watcher", config.ExpiryCheckAt, false) - desiredCheck := *Register(ensureCertsFromConfig(storage, config.Domains), "Ensure configured domains is present", 8*time.Hour, true) + pinger := *Register(pingServer(client), "Ping intercert host", 10*time.Minute, false) + renewalHandler := *Register(watchForEvents(config.Domains, client), "Watch for certificate renewal events", 0*time.Second, true) + desiredCheck := *Register(ensureCertsFromConfig(storage, config.Domains), "Ensure configured domains is present", 1*time.Hour, true) - tasks = append(tasks, expiryCheck, desiredCheck) + tasks = append(tasks, pinger, renewalHandler, desiredCheck) return tasks } diff --git a/client/scheduler.go b/client/scheduler.go index 2896a7f..fd645b2 100644 --- a/client/scheduler.go +++ b/client/scheduler.go @@ -42,21 +42,23 @@ func (j *Job) start() { } j.firstRun = false - go func() { - for { - // Sleep for the predetermined time. - time.Sleep(j.delay) - - select { - // Check for the 'stop' signal. - case <-j.stop: - return - // Execute the function. - default: - j.fn() + if j.delay > 0*time.Second { + go func() { + for { + // Sleep for the predetermined time. + time.Sleep(j.delay) + + select { + // Check for the 'stop' signal. + case <-j.stop: + return + // Execute the function. + default: + j.fn() + } } - } - }() + }() + } } // Register schedules a function for execution, to be invoked repeated with a delay of diff --git a/client/storage.go b/client/storage.go index fd4f829..580a0a3 100644 --- a/client/storage.go +++ b/client/storage.go @@ -25,7 +25,7 @@ type CertStorage struct { // NewCertStorage constructs an instance of the CertStorage struct, with validation func NewCertStorage(storageDirectory string) *CertStorage { if _, err := os.Stat(storageDirectory); os.IsNotExist(err) { - err = os.Mkdir(storageDirectory, 0777) + err = os.MkdirAll(storageDirectory, 0777) if err != nil { log.Warnf("Could not create directory for certs: %v", err) diff --git a/client/tasks.go b/client/tasks.go index 7ff0a1c..6c52ed5 100644 --- a/client/tasks.go +++ b/client/tasks.go @@ -1,15 +1,54 @@ package client import ( - "time" - + "context" + "github.com/evenh/intercert/api" "github.com/go-acme/lego/log" + "io" + "os" ) -// Find all expired certificates and ensure they are queued up for renewal -func findExpiredCerts(renewalThreshold time.Duration) func() { +func pingServer(client api.CertificateIssuerClient) func() { return func() { - log.Infof("Scanning for expired certificates (NOT IMPLEMENTED YET)") + _, err := client.Ping(context.Background(), &api.PingRequest{Msg: "ping"}) + + if err != nil { + log.Warnf("Could not ping intercert host: %v", err) + } + } +} + +func watchForEvents(domains []string, client api.CertificateIssuerClient) func() { + return func() { + renewalStream, err := client.OnCertificateRenewal(context.Background(), &api.CertificateRenewalNotificationRequest{ + DnsNames: domains, + }) + + if err != nil { + log.Fatalf("Could not subscribe to renewal events: %v", err) + os.Exit(1) + } + + log.Infof("Listening for certificate renewal events") + + go func() { + for { + in, err := renewalStream.Recv() + + if err == io.EOF { + break + } + + if err != nil { + log.Warnf("Got error while listening for renewal events: %v", err) + } + + log.Infof("Got notice from server that certificate for %s has been renewed. Queuing up re-fetch!", in.DnsName) + job := NewCertReq(in.DnsName, true) + job.Submit() + } + }() + } } diff --git a/cmd/client.go b/cmd/client.go index dae7c06..356cf4f 100644 --- a/cmd/client.go +++ b/cmd/client.go @@ -1,8 +1,6 @@ package cmd import ( - "time" - "github.com/evenh/intercert/client" "github.com/evenh/intercert/config" "github.com/spf13/cobra" @@ -17,11 +15,9 @@ func init() { clientCmd.PersistentFlags().IntP("port", "p", 6300, "The port the server will listen on") clientCmd.PersistentFlags().StringP("storage", "s", DefaultIntercertDir+"/client-data", "The place to store certificates and other data") clientCmd.PersistentFlags().StringArrayP("domains", "D", []string{}, "The domains to request certs for") - clientCmd.PersistentFlags().DurationP("expiry", "e", 30*time.Minute, "How often to check for expired certificates") - clientCmd.PersistentFlags().DurationP("renewalThreshold", "r", 24*time.Hour, "How early before expiry shall certificates be renewed") // Load clientCmd.PersistentFlags() values from config - bindPrefixedFlags(clientCmd, "client", "host", "port", "storage", "domains", "expiry", "renewalThreshold") + bindPrefixedFlags(clientCmd, "client", "host", "port", "storage", "domains") } var clientCmd = &cobra.Command{ @@ -31,12 +27,10 @@ var clientCmd = &cobra.Command{ Run: func(cmd *cobra.Command, args []string) { c := config.ClientConfig{ - Hostname: viper.GetString("client.host"), - Port: viper.GetInt("client.port"), - Storage: viper.GetString("client.storage"), - Domains: viper.GetStringSlice("client.domains"), - ExpiryCheckAt: viper.GetDuration("client.expiry"), - RenewalThreshold: viper.GetDuration("client.renewalThreshold"), + Hostname: viper.GetString("client.host"), + Port: viper.GetInt("client.port"), + Storage: viper.GetString("client.storage"), + Domains: viper.GetStringSlice("client.domains"), } client.StartClient(&c, UserAgent()) diff --git a/cmd/serve.go b/cmd/serve.go index 2dc4d66..912b713 100644 --- a/cmd/serve.go +++ b/cmd/serve.go @@ -2,6 +2,7 @@ package cmd import ( "errors" + "time" "github.com/go-acme/lego/log" @@ -22,9 +23,11 @@ func init() { serveCmd.PersistentFlags().StringSliceP("domains", "d", nil, "Domains to whitelist") serveCmd.PersistentFlags().StringP("email", "e", "", "The email to register with the ACME provider") serveCmd.PersistentFlags().StringP("storage", "s", DefaultIntercertDir+"/server-data", "The place to store certificates and other data") + serveCmd.PersistentFlags().DurationP("expiry", "x", 30*time.Minute, "How often to check for expired certificates") + serveCmd.PersistentFlags().DurationP("renewalThreshold", "r", (24*time.Hour)*15, "How early before expiry shall certificates be renewed") // Load serveCmd.PersistentFlags() values from config - bindPrefixedFlags(serveCmd, "server", "port", "agree", "directory", "dns-provider", "domains", "storage", "email") + bindPrefixedFlags(serveCmd, "server", "port", "agree", "directory", "dns-provider", "domains", "storage", "email", "expiry", "renewalThreshold") } var serveCmd = &cobra.Command{ @@ -34,20 +37,26 @@ var serveCmd = &cobra.Command{ Run: func(cmd *cobra.Command, args []string) { c := config.ServerConfig{ - Port: viper.GetInt("server.port"), - Agree: viper.GetBool("server.agree"), - Directory: viper.GetString("server.directory"), - DNSProvider: viper.GetString("server.dns-provider"), - Domains: viper.GetStringSlice("server.domains"), - Email: viper.GetString("server.email"), - Storage: viper.GetString("server.storage"), + Port: viper.GetInt("server.port"), + Agree: viper.GetBool("server.agree"), + Directory: viper.GetString("server.directory"), + DNSProvider: viper.GetString("server.dns-provider"), + Domains: viper.GetStringSlice("server.domains"), + Email: viper.GetString("server.email"), + Storage: viper.GetString("server.storage"), + ExpiryCheckAt: viper.GetDuration("server.expiry"), + RenewalThreshold: viper.GetDuration("server.renewalThreshold"), } if !c.Agree { PrintErrorAndExit(errors.New("the ACME ToS must be agreed to")) } - server.StartServer(&c) + if c.RenewalThreshold > (24*time.Hour)*60 { + PrintErrorAndExit(errors.New("renewal threshold can't exceed 60 days")) + } + + server.StartServer(&c, UserAgent()) log.Infof("Listening on port %v", c.Port) }, } diff --git a/config/config.go b/config/config.go index 464768d..22c1d90 100644 --- a/config/config.go +++ b/config/config.go @@ -25,6 +25,10 @@ type ServerConfig struct { // The location on disk to save certificates and other data // that the server produces. Storage string + // How often to check for expired certificates + ExpiryCheckAt time.Duration + // How early before expiry shall certificates be renewed? + RenewalThreshold time.Duration } // ClientConfig holds configuration that is required for creating a client @@ -38,10 +42,6 @@ type ClientConfig struct { Storage string // The domains to request certs for Domains []string - // How often to check for expired certificates - ExpiryCheckAt time.Duration - // How early before expiry shall certificates be renewed? - RenewalThreshold time.Duration } // GetDialAddr gets the formatted address to dial a new gRPC connection diff --git a/server/issuer.go b/server/issuer.go index e84c175..88a8ccd 100644 --- a/server/issuer.go +++ b/server/issuer.go @@ -5,6 +5,8 @@ import ( "crypto" "crypto/ecdsa" "crypto/rsa" + "fmt" + "sort" "time" "github.com/go-acme/lego/certcrypto" @@ -26,13 +28,17 @@ import ( // IssuerService issues certificates to clients type IssuerService struct { - client *certmagic.Config - whitelist Whitelist + client *certmagic.Config + whitelist Whitelist + renewalEvents chan string } // NewIssuerService constructs a new instance with a predefined config -func NewIssuerService(config *config.ServerConfig) *IssuerService { +func NewIssuerService(config *config.ServerConfig, userAgent string) *IssuerService { + certmagic.UserAgent = userAgent + issuer := new(IssuerService) + issuer.renewalEvents = make(chan string) // Configure DNS provider by delegating to xenolf/lego factory dnsProvider, err := dns.NewDNSChallengeProviderByName(config.DNSProvider) @@ -51,11 +57,18 @@ func NewIssuerService(config *config.ServerConfig) *IssuerService { MustStaple: false, DNSProvider: dnsProvider, Storage: createStorage(config.Storage), + RenewDurationBefore: config.RenewalThreshold, + OnEvent: func(eventName string, payload interface{}) { + // For now, we'll only care about certificates that are renewed + if eventName == "acme_cert_renewed" { + issuer.renewalEvents <- fmt.Sprintf("%s", payload) + } + }, } // Construct the new certmagic instance magic := certmagic.New(certmagic.NewCache(certmagic.CacheOptions{ - RenewCheckInterval: 10 * time.Minute, + RenewCheckInterval: config.ExpiryCheckAt, OCSPCheckInterval: 1 * time.Hour, GetConfigForCert: func(certificate certmagic.Certificate) (certmagic.Config, error) { return *certmagicConfig, nil @@ -68,7 +81,7 @@ func NewIssuerService(config *config.ServerConfig) *IssuerService { whitelist := NewWhitelist(config.Domains) issuer.whitelist = whitelist - log.Infof("Certificate issuer service configured") + log.Infof("Certificate issuer service configured - certificates will be renewed %v before expiry", config.RenewalThreshold) return issuer } @@ -137,6 +150,30 @@ func (s IssuerService) Ping(ctx context.Context, req *api.PingRequest) (*api.Pin return &api.PingResponse{Msg: "pong"}, nil } +// OnCertificateRenewal notifies the client about certificates that has been renewed server side +func (s IssuerService) OnCertificateRenewal(req *api.CertificateRenewalNotificationRequest, res api.CertificateIssuer_OnCertificateRenewalServer) error { + logClient(res.Context(), "OnCertificateRenewal") + names := req.DnsNames + sort.Strings(names) + + for event := range s.renewalEvents { + for _, name := range names { + if strings.ToLower(name) == strings.ToLower(event) { + err := res.Send(&api.RenewedCertificateEvent{DnsName: event}) + + if err != nil { + log.Warnf("[%s] Could not send renewal event to clients", name) + return err + } + + log.Infof("[%s] Notified clients that certificate has been renewed", name) + } + } + } + + return nil +} + // from certmagic: encodePrivateKey marshals a EC or RSA private key into a PEM-encoded array of bytes. func pemEncodeKey(key crypto.PrivateKey) ([]byte, error) { var pemType string diff --git a/server/server.go b/server/server.go index e8fb162..d8ac325 100644 --- a/server/server.go +++ b/server/server.go @@ -11,10 +11,10 @@ import ( ) // StartServer spawns a server instance given a server config -func StartServer(config *config.ServerConfig) { +func StartServer(config *config.ServerConfig, userAgent string) { s := grpc.NewServer() - issuerService := NewIssuerService(config) + issuerService := NewIssuerService(config, userAgent) api.RegisterCertificateIssuerServer(s, issuerService)