Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add central certificate storage #57

Draft
wants to merge 6 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
26 changes: 24 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 @@ -79,8 +80,14 @@ func main() {
logger.Fatalln(err)
}

primaryCertProvider := cert.MultiProvider{
primaryFileCertificateProvider(config, logger),
}
if config.Certificates.RedisAddress != "" {
primaryCertProvider = append(primaryCertProvider, primaryRedisCertificateProvider(config, logger))
}
providerAdaptor := &cert.ProviderAdaptor{
PrimaryProvider: primaryCertificateProvider(config, logger),
PrimaryProvider: primaryCertProvider,
SecondaryProvider: secondaryCertProvider,
}

Expand Down Expand Up @@ -213,13 +220,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
4 changes: 2 additions & 2 deletions frontend/cert/file_provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import (
"context"
"crypto/tls"
"crypto/x509"
"errors"
"fmt"
"log"
"os"
"path"
Expand Down Expand Up @@ -37,7 +37,7 @@ func (p *FileProvider) GetCertificate(ctx context.Context, n name.ServerName) (*
return cert, err
}

return nil, errors.New("file provider can not generated certificates")
return nil, fmt.Errorf("file %w", ErrProviderGenerateUnsupported)
}

// GetExistingCertificate attempts to fetch an existing certificate for the
Expand Down
2 changes: 1 addition & 1 deletion frontend/cert/generator/package_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,5 @@ import (

func TestSuite(t *testing.T) {
gomega.RegisterFailHandler(ginkgo.Fail)
ginkgo.RunSpecs(t, "Frontend/Cert/Generator")
ginkgo.RunSpecs(t, "frontend/cert/generator")
}
41 changes: 41 additions & 0 deletions frontend/cert/multi_provider.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package cert

import (
"context"
"crypto/tls"

"github.com/icecave/honeycomb/name"
)

// MultiProvider is a provider that combines a slice of providers sequentially.
type MultiProvider []Provider

// GetCertificate attempts to fetch an existing certificate for the given
// server name. If no such certificate exists, it generates one.
func (m MultiProvider) GetCertificate(ctx context.Context, n name.ServerName) (*tls.Certificate, error) {
for _, p := range m {
// Look for an existing certificate from the provider.
if certificate, err := p.GetCertificate(ctx, n); certificate != nil || err != nil {
return certificate, err
}
}

// finally return nil, nil
return nil, nil
}

// 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 (m MultiProvider) GetExistingCertificate(ctx context.Context, n name.ServerName) (*tls.Certificate, error) {
for _, p := range m {
// Look for an existing certificate from the provider.
if certificate, err := p.GetExistingCertificate(ctx, n); certificate != nil || err != nil {
return certificate, err
}
}

// finally return nil, nil
return nil, nil
}
169 changes: 169 additions & 0 deletions frontend/cert/multi_provider_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
package cert_test

import (
"context"
"crypto/tls"
"errors"

"github.com/icecave/honeycomb/frontend/cert"
"github.com/icecave/honeycomb/name"
. "github.com/onsi/ginkgo"
. "github.com/onsi/ginkgo/extensions/table"
. "github.com/onsi/gomega"
)

var (
multiTLSCertificatePrimary = &tls.Certificate{
Certificate: [][]byte{
[]byte("imacert"),
},
}
multiTLSCertificateSecondary = &tls.Certificate{
Certificate: [][]byte{
[]byte("imanothercert"),
},
}
)

type multiTestType int

const (
primaryCertReturn multiTestType = iota
secondaryCertReturn
errorReturn
nilReturn
)

var errCertError = errors.New("error certificate")

type mockPrimaryProvider struct{}

func (m *mockPrimaryProvider) GetCertificate(ctx context.Context, n name.ServerName) (*tls.Certificate, error) {
if n.Unicode == "primary.cert" {
return multiTLSCertificatePrimary, nil
}

if n.Unicode == "primary.error" {
return nil, errCertError
}

return nil, nil
}

func (m *mockPrimaryProvider) GetExistingCertificate(ctx context.Context, n name.ServerName) (*tls.Certificate, error) {
if n.Unicode == "primary.cert" {
return multiTLSCertificatePrimary, nil
}

if n.Unicode == "primary.error" {
return nil, errCertError
}

return nil, nil
}

type mockSecondaryProvider struct{}

func (m *mockSecondaryProvider) GetCertificate(ctx context.Context, n name.ServerName) (*tls.Certificate, error) {
if n.Unicode == "secondary.cert" {
return multiTLSCertificateSecondary, nil
}

if n.Unicode == "secondary.error" {
return nil, errCertError
}

return nil, nil
}

func (m *mockSecondaryProvider) GetExistingCertificate(
ctx context.Context,
n name.ServerName,
) (*tls.Certificate, error) {
if n.Unicode == "secondary.cert" {
return multiTLSCertificateSecondary, nil
}

if n.Unicode == "secondary.error" {
return nil, errCertError
}

return nil, nil
}

var multiTestList = []TableEntry{
Entry(
"returns the primary provider certificate",
name.Parse("primary.cert"),
primaryCertReturn,
),
Entry(
"returns the secondary provider certificate",
name.Parse("secondary.cert"),
secondaryCertReturn,
),
Entry(
"returns an error from the primary provider",
name.Parse("primary.error"),
errorReturn,
),
Entry(
"returns an error from the secondary provider",
name.Parse("secondary.error"),
errorReturn,
),
Entry(
"returns no error or certificate",
name.Parse("somedomain.com"),
nilReturn,
),
}

var _ = Describe("MultiProvider", func() {
DescribeTable(
"GetCertificate",
func(n name.ServerName, tt multiTestType) {
p := &cert.MultiProvider{
&mockPrimaryProvider{},
&mockSecondaryProvider{},
}
c, err := p.GetCertificate(context.Background(), n)
switch tt {
case primaryCertReturn:
Expect(c).To(BeEquivalentTo(multiTLSCertificatePrimary))
Expect(err).NotTo(HaveOccurred())
case secondaryCertReturn:
Expect(c).To(BeEquivalentTo(multiTLSCertificateSecondary))
Expect(err).NotTo(HaveOccurred())
case errorReturn:
Expect(err).To(HaveOccurred())
case nilReturn:
Expect(err).NotTo(HaveOccurred())
}
},
multiTestList...,
)
DescribeTable(
"GetExistingCertificate",
func(n name.ServerName, tt multiTestType) {
p := &cert.MultiProvider{
&mockPrimaryProvider{},
&mockSecondaryProvider{},
}
c, err := p.GetExistingCertificate(context.Background(), n)
switch tt {
case primaryCertReturn:
Expect(c).To(BeEquivalentTo(multiTLSCertificatePrimary))
Expect(err).NotTo(HaveOccurred())
case secondaryCertReturn:
Expect(c).To(BeEquivalentTo(multiTLSCertificateSecondary))
Expect(err).NotTo(HaveOccurred())
case errorReturn:
Expect(err).To(HaveOccurred())
case nilReturn:
Expect(err).NotTo(HaveOccurred())
}
},
multiTestList...,
)
})
4 changes: 4 additions & 0 deletions frontend/cert/provider.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package cert
import (
"context"
"crypto/tls"
"errors"

"github.com/icecave/honeycomb/name"
)
Expand All @@ -19,3 +20,6 @@ type Provider interface {
// indicates a failure to find an existing certificate.
GetExistingCertificate(context.Context, name.ServerName) (*tls.Certificate, error)
}

// ErrProviderGenerateUnsupported is returned when providers do not generate new certificates.
var ErrProviderGenerateUnsupported = errors.New("provider can not generate certificates")
Loading