Skip to content
This repository has been archived by the owner on Feb 27, 2024. It is now read-only.

Commit

Permalink
added redis provider and changed PrimaryProvider to be able to proces…
Browse files Browse the repository at this point in the history
…s multiple providers
  • Loading branch information
na4ma4 committed Oct 15, 2020
1 parent 3054b7c commit 11c90c2
Show file tree
Hide file tree
Showing 7 changed files with 439 additions and 18 deletions.
6 changes: 6 additions & 0 deletions cmd/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ type Config struct {
}

type certificateConfig struct {
RedisAddress string
RedisPassword string
RedisCacheExpire time.Duration
BasePath string
IssuerCertificate string
IssuerKey string
Expand All @@ -37,6 +40,9 @@ func GetConfigFromEnvironment() *Config {
InsecurePort: env("REDIRECT_PORT", "8080"),
DockerPollInterval: time.Duration(envInt("DOCKER_POLL_INTERVAL", 0)) * time.Second,
Certificates: certificateConfig{
RedisAddress: env("REDIS_ADDR", ""),
RedisPassword: env("REDIS_PASSWORD", ""),
RedisCacheExpire: envDuration("REDIS_CACHE_EXPIRY", time.Minute),
BasePath: env("CERTIFICATE_PATH", "/run/secrets/"),
IssuerCertificate: env("ISSUER_CERT", "honeycomb-ca.crt"),
IssuerKey: env("ISSUER_KEY", "honeycomb-ca.key"),
Expand Down
23 changes: 21 additions & 2 deletions cmd/honeycomb/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import (
"golang.org/x/net/http2"

"github.com/docker/docker/client"
"github.com/go-redis/redis/v8"
"github.com/icecave/honeycomb/backend"
"github.com/icecave/honeycomb/cmd"
"github.com/icecave/honeycomb/docker"
Expand Down Expand Up @@ -80,7 +81,10 @@ func main() {
}

providerAdaptor := &cert.ProviderAdaptor{
PrimaryProvider: primaryCertificateProvider(config, logger),
PrimaryProvider: []cert.Provider{
primaryFileCertificateProvider(config, logger),
primaryRedisCertificateProvider(config, logger),
},
SecondaryProvider: secondaryCertProvider,
}

Expand Down Expand Up @@ -213,13 +217,28 @@ func loadDefaultCertificate(config *cmd.Config) (*tls.Certificate, error) {
return &cert, err
}

func primaryCertificateProvider(
func primaryFileCertificateProvider(
config *cmd.Config,
logger *log.Logger,
) cert.Provider {
return &cert.FileProvider{
BasePath: config.Certificates.BasePath,
}
}

func primaryRedisCertificateProvider(
config *cmd.Config,
logger *log.Logger,
) cert.Provider {
rdb := redis.NewClient(&redis.Options{
Addr: config.Certificates.RedisAddress,
Password: config.Certificates.RedisPassword,
})

return &cert.RedisProvider{
Client: rdb,
Logger: logger,
CacheAge: config.Certificates.RedisCacheExpire,
}
}

Expand Down
26 changes: 14 additions & 12 deletions frontend/cert/provider_adaptor.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ const DefaultTimeout = 5 * time.Second
type ProviderAdaptor struct {
// PrimaryProvider is the certificate provider used to create certificates
// for "normal" recognized server names.
PrimaryProvider Provider
PrimaryProvider []Provider

// SecondaryProvider is used to provide default or "fallback" certificates
// so that requests may be served even when the server name is unrecognized.
Expand Down Expand Up @@ -48,18 +48,20 @@ func (adaptor *ProviderAdaptor) GetCertificate(
return nil, nil
}

// Next, look for an existing certificate from the primary provider. If such
// a certificate is available, it doesn't matter if the server name is
// recognized or not ...
certificate, err := adaptor.PrimaryProvider.GetExistingCertificate(ctx, serverName)
if certificate != nil || err != nil {
return certificate, err
}
for _, provider := range adaptor.PrimaryProvider {
// Next, look for an existing certificate from the primary provider. If such
// a certificate is available, it doesn't matter if the server name is
// recognized or not ...
certificate, err := provider.GetExistingCertificate(ctx, serverName)
if certificate != nil || err != nil {
return certificate, err
}

// If the server name is recognized, use the primary provider to get a new
// certificate for the server name ...
if adaptor.IsRecognised != nil && adaptor.IsRecognised(ctx, serverName) {
return adaptor.PrimaryProvider.GetCertificate(ctx, serverName)
// If the server name is recognized, use the primary provider to get a new
// certificate for the server name ...
if adaptor.IsRecognised != nil && adaptor.IsRecognised(ctx, serverName) {
return provider.GetCertificate(ctx, serverName)
}
}

// Finally, fallback to the secondary provider ...
Expand Down
175 changes: 175 additions & 0 deletions frontend/cert/redis_provider.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,175 @@
package cert

import (
"context"
"crypto/tls"
"errors"
"fmt"
"log"
"sync"
"time"

"github.com/go-redis/redis/v8"
"github.com/icecave/honeycomb/name"
)

// RedisProvider a certificate provider that reads certificates from a loader.
type RedisProvider struct {
Logger *log.Logger
Client *redis.Client
CacheAge time.Duration

mutex sync.RWMutex
cache map[string]*redisCacheItem
}

type redisCacheItem struct {
Certificate *tls.Certificate
LastSeen time.Time
}

// GetCertificate attempts to fetch an existing certificate for the given
// server name. If no such certificate exists, it generates one.
func (p *RedisProvider) GetCertificate(ctx context.Context, n name.ServerName) (*tls.Certificate, error) {
cert, err := p.GetExistingCertificate(ctx, n)
if err != nil {
return nil, err
} else if cert != nil {
return cert, err
}

return nil, errors.New("redis provider can not generate certificates")
}

func certificateRedisKey(key string) (o string) {
return fmt.Sprintf("ssl:%s", key)
}

func certAndKeyFromMap(m map[string]string) (cert string, key string, ok bool) {
if cert, ok = m["certificate"]; !ok {
return
}

key, ok = m["key"]

return
}

// GetExistingCertificate attempts to fetch an existing certificate for the
// given server name. It never generates new certificates. A non-nil error
// indicates an error with the provider itself; otherwise, a nil certificate
// indicates a failure to find an existing certificate.
func (p *RedisProvider) GetExistingCertificate(ctx context.Context, n name.ServerName) (*tls.Certificate, error) {
// If cache has not expired, attempt to find in cache.
if !p.expiredInCache(n) {
if cert, ok := p.findInCache(n); ok {
return cert, nil
}
}

// No cache (or expired), attempt to look up in redis
// but if redis is down or broken suddenly, we should reuse the
// cached certificate until it's replaced.
if cert, err := p.getRedisCertificate(ctx, n); err == nil {
return cert, nil
}

// fail through to getting it from the cache.
if cert, ok := p.findInCache(n); ok {
p.Logger.Printf("expired but falling through to cache for %s", n.Unicode)
return cert, nil
}

// and finally we just fail.
return nil, nil
}

func (p *RedisProvider) getRedisCertificate(ctx context.Context, n name.ServerName) (*tls.Certificate, error) {
r, err := p.Client.HGetAll(ctx, certificateRedisKey(n.Unicode)).Result()
if err != nil {
// p.Logger.Printf("failed to retrieve certificate for %s", certificateRedisKey(n.Unicode))
// p.Logger.Printf("redis error: %s", err)
return nil, err
}

if cr, ck, ok := certAndKeyFromMap(r); ok {
if cert, err := tls.X509KeyPair([]byte(cr), []byte(ck)); err == nil {
p.writeToCache(n, &cert)
return &cert, nil
}
}

return nil, errors.New("certificate not found")
}

// // FlushOldCacheItems will flush any entries from the cache that are older than the specified duration.
// func (p *RedisProvider) FlushOldCacheItems(
// d time.Duration,
// ) {
// p.mutex.Lock()
// defer p.mutex.Unlock()

// n := time.Now().Add(-1 * d)

// for k, v := range p.cache {
// if v.LastSeen.Before(n) {
// delete(p.cache, k)
// }
// }
// }

func (p *RedisProvider) expiredInCache(n name.ServerName) bool {
p.mutex.RLock()
defer p.mutex.RUnlock()

if item, ok := p.cache[n.Unicode]; ok {
if p.CacheAge > 0 && item.LastSeen.Before(time.Now().Add(-1*p.CacheAge)) {
return true
}
}

return false
}

func (p *RedisProvider) findInCache(
n name.ServerName,
) (*tls.Certificate, bool) {
p.mutex.RLock()
defer p.mutex.RUnlock()

if item, ok := p.cache[n.Unicode]; ok {
return item.Certificate, ok
}

return nil, false
}

func (p *RedisProvider) deleteFromCache(
n name.ServerName,
) (*tls.Certificate, bool) {
p.mutex.RLock()
defer p.mutex.RUnlock()

item, ok := p.cache[n.Unicode]

return item.Certificate, ok
}

func (p *RedisProvider) writeToCache(
n name.ServerName,
cert *tls.Certificate,
) {
p.mutex.Lock()
defer p.mutex.Unlock()

if p.cache == nil {
p.cache = map[string]*redisCacheItem{}
}

item := &redisCacheItem{
Certificate: cert,
LastSeen: time.Now(),
}

p.cache[n.Unicode] = item
}
9 changes: 5 additions & 4 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -13,21 +13,22 @@ require (
github.com/docker/go-connections v0.4.1-0.20180821093606-97c2040d34df // indirect
github.com/docker/go-units v0.3.3 // indirect
github.com/dustin/go-humanize v1.0.0
github.com/go-redis/redis/v8 v8.3.1
github.com/gogo/protobuf v1.2.2-0.20190306082329-c5a62797aee0 // indirect
github.com/golang/gddo v0.0.0-20181116215533-9bd4a3295021
github.com/gorilla/mux v1.7.3 // indirect
github.com/morikuni/aec v1.0.0 // indirect
github.com/onsi/ginkgo v1.9.1-0.20190828165658-66915d68818e
github.com/onsi/gomega v1.7.0
github.com/onsi/ginkgo v1.14.1
github.com/onsi/gomega v1.10.2
github.com/opencontainers/go-digest v1.0.0-rc1 // indirect
github.com/opencontainers/image-spec v1.0.2-0.20190306222905-243ea084a444 // indirect
github.com/pires/go-proxyproto v0.0.0-20190111085350-4d51b51e3bfc
github.com/pkg/errors v0.8.2-0.20190227000051-27936f6d90f9 // indirect
github.com/sirupsen/logrus v1.4.2 // indirect
go.uber.org/atomic v1.4.0 // indirect
go.uber.org/multierr v1.1.0
golang.org/x/net v0.0.0-20190311183353-d8887717615a
golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7
golang.org/x/time v0.0.0-20190921001708-c4c64cad1fd0 // indirect
google.golang.org/grpc v1.22.2 // indirect
google.golang.org/grpc v1.32.0 // indirect
gotest.tools v2.2.0+incompatible // indirect
)
Loading

0 comments on commit 11c90c2

Please sign in to comment.