Skip to content

Commit

Permalink
refactor certmanager code
Browse files Browse the repository at this point in the history
  • Loading branch information
mariuskimmina committed Aug 24, 2022
1 parent dc25fc4 commit 4147afd
Show file tree
Hide file tree
Showing 6 changed files with 92 additions and 112 deletions.
22 changes: 0 additions & 22 deletions config_test.go

This file was deleted.

128 changes: 62 additions & 66 deletions manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,57 +12,37 @@ import (
"os"

"github.com/caddyserver/certmagic"
"github.com/coredns/coredns/core/dnsserver"
)

type ACMEManager struct {
type CertManager struct {
Config *certmagic.Config
Issuer *certmagic.ACMEIssuer
Zone string
}

// NewACMEManager create a new ACMEManager
func NewACMEManager(config *dnsserver.Config, zone string, ca string, path string, caCert string, port int, email string) *ACMEManager {
if ca == "" {
ca = "localhost:14001/dir" //pebble default
}

// default email if none is provided
// providing a reail email is recommended to receiv notifications for expiring certificates
// in case that something goes wrong
if email == "" {
email = "[email protected]"
}

pool, err := x509.SystemCertPool()
if err != nil {
log.Errorf("Failed to get system pool of trusted certificates: %v \n", err)
}

if caCert != "" {
certbytes, err := os.ReadFile(caCert)
if err != nil {
log.Errorf("Failed to read certificate provided by cacert option: %v \n", err)
}
pemcert, _ := pem.Decode(certbytes)
if pemcert == nil {
log.Errorf("Failed to decode CaCert: %v \n", err)
}
cert, err := x509.ParseCertificate(pemcert.Bytes)
if err != nil {
log.Errorf("Failed to parse certificate provided by cacert option: %v \n", err)
}
pool.AddCert(cert)
func NewConfig(path string) *certmagic.Config {
acmeConfigTemplate := NewCertmagicConfig()
acmeConfigTemplate.RenewalWindowRatio = 0.7
acmeConfigTemplate.Storage = &certmagic.FileStorage{
Path: path,
}
cache := certmagic.NewCache(certmagic.CacheOptions{
GetConfigForCert: func(cert certmagic.Certificate) (*certmagic.Config, error) {
return acmeConfigTemplate, nil
},
})
acmeConfig := certmagic.New(cache, *acmeConfigTemplate)
return acmeConfig
}

func NewIssuer(config *certmagic.Config, ca string, email string, pool *x509.CertPool, port int) *certmagic.ACMEIssuer {
readyChan := make(chan string)
solver := &DNSSolver{
Port: port,
readyChan: readyChan,
}

certmagic.DefaultACME.Email = "[email protected]"

acmeIssuerTemplate := certmagic.ACMEIssuer{
Agreed: true,
DisableHTTPChallenge: true,
Expand All @@ -74,67 +54,83 @@ func NewACMEManager(config *dnsserver.Config, zone string, ca string, path strin
TrustedRoots: pool,
}

acmeConfigTemplate := NewCertmagicConfig()
acmeConfigTemplate.RenewalWindowRatio = 0.7
acmeIssuer := certmagic.NewACMEIssuer(config, acmeIssuerTemplate)
config.Issuers = append(config.Issuers, acmeIssuer)

acmeConfigTemplate.Storage = &certmagic.FileStorage{
Path: path,
return acmeIssuer
}

func setupCertPool(caCert string) (*x509.CertPool, error) {
pool, err := x509.SystemCertPool()
if err != nil {
return nil, err
}

cache := certmagic.NewCache(certmagic.CacheOptions{
GetConfigForCert: func(cert certmagic.Certificate) (*certmagic.Config, error) {
return acmeConfigTemplate, nil
},
})
acmeConfig := certmagic.New(cache, *acmeConfigTemplate)
acmeIssuer := certmagic.NewACMEIssuer(acmeConfig, acmeIssuerTemplate)
acmeConfig.Issuers = append(acmeConfig.Issuers, acmeIssuer)
if caCert != "" {
certbytes, err := os.ReadFile(caCert)
if err != nil {
return nil, err
}
pemcert, _ := pem.Decode(certbytes)
if pemcert == nil {
return nil, err
}
cert, err := x509.ParseCertificate(pemcert.Bytes)
if err != nil {
return nil, err
}
pool.AddCert(cert)
}
return pool, nil
}

return &ACMEManager{
Config: acmeConfig,
Issuer: acmeIssuer,
// NewACMEManager create a new ACMEManager
func NewCertManager(zone string, config *certmagic.Config, issuer *certmagic.ACMEIssuer) *CertManager {
return &CertManager{
Config: config,
Issuer: issuer,
Zone: zone,
}
}

func (am *ACMEManager) configureTLSwithACME(ctx context.Context) (*tls.Config, *certmagic.Certificate, error) {
func (c *CertManager) configureTLSwithACME(ctx context.Context) (*tls.Config, *certmagic.Certificate, error) {
var cert certmagic.Certificate
var err error

// try loading existing certificate
cert, err = am.Config.CacheManagedCertificate(ctx, am.Zone)
cert, err = c.Config.CacheManagedCertificate(ctx, c.Zone)
if err != nil {
log.Info("Obtaining TLS Certificate, may take a moment")
if !errors.Is(err, fs.ErrNotExist) {
return nil, nil, err
}
err = am.GetCert(am.Zone)
err = c.GetCert(c.Zone)
if err != nil {
return nil, nil, err
}
cert, err = am.CacheCertificate(ctx, am.Zone)
cert, err = c.CacheCertificate(ctx, c.Zone)
if err != nil {
return nil, nil, err
}
}

// check if renewal is required
if cert.NeedsRenewal(am.Config) {
if cert.NeedsRenewal(c.Config) {
log.Info("Renewing TLS Certificate")
var err error
err = am.RenewCert(ctx, am.Zone)
err = c.RenewCert(ctx, c.Zone)
if err != nil {
return nil, nil, fmt.Errorf("%s: renewing certificate: %w", am.Zone, err)
return nil, nil, fmt.Errorf("%s: renewing certificate: %w", c.Zone, err)
}
// successful renewal, so update in-memory cache
cert, err = am.CacheCertificate(ctx, am.Zone)
cert, err = c.CacheCertificate(ctx, c.Zone)
if err != nil {
return nil, nil, fmt.Errorf("%s: reloading renewed certificate into memory: %v", am.Zone, err)
return nil, nil, fmt.Errorf("%s: reloading renewed certificate into memory: %v", c.Zone, err)
}
}

// check again, if it still needs renewal something went wrong
if cert.NeedsRenewal(am.Config) {
if cert.NeedsRenewal(c.Config) {
log.Error("Failed to renew certificate")
}

Expand All @@ -146,17 +142,17 @@ func (am *ACMEManager) configureTLSwithACME(ctx context.Context) (*tls.Config, *
return tlsConfig, &cert, nil
}

func (a *ACMEManager) GetCert(zone string) error {
err := a.Config.ObtainCertSync(context.Background(), zone)
func (c *CertManager) GetCert(zone string) error {
err := c.Config.ObtainCertSync(context.Background(), zone)
return err
}

func (a *ACMEManager) RenewCert(ctx context.Context, zone string) error {
err := a.Config.RenewCertSync(ctx, zone, false)
func (c *CertManager) RenewCert(ctx context.Context, zone string) error {
err := c.Config.RenewCertSync(ctx, zone, false)
return err
}

func (a *ACMEManager) CacheCertificate(ctx context.Context, zone string) (certmagic.Certificate, error) {
cert, err := a.Config.CacheManagedCertificate(ctx, zone)
func (c *CertManager) CacheCertificate(ctx context.Context, zone string) (certmagic.Certificate, error) {
cert, err := c.Config.CacheManagedCertificate(ctx, zone)
return cert, err
}
46 changes: 25 additions & 21 deletions setup.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,13 +36,11 @@ var (
)

const (
argDomain = "domain"
argCheckInternal = "checkinterval"
argCa = "ca"
argCaCert = "cacert"
argEmail = "email"
argCertPath = "certpath"
argPort = "port"
defaultCA = "https://acme-v02.api.letsencrypt.org/directory"
defaultEmail = "[email protected]"
defaultCheckInterval = 15
defaultPort = 53
defaultCertPath = "./local/share/certmagic"
)

func parseTLS(c *caddy.Controller) error {
Expand All @@ -67,10 +65,10 @@ func parseTLS(c *caddy.Controller) error {
ctx := context.Background()

var domainNameACME string
var ca string
var caCert string
var port string
var email string
ca := "https://acme-v02.api.letsencrypt.org/directory"
checkInterval := 15
userHome, homeExists := os.LookupEnv("HOME")
if !homeExists {
Expand All @@ -81,46 +79,46 @@ func parseTLS(c *caddy.Controller) error {
for c.NextBlock() {
token := c.Val()
switch token {
case argDomain:
case "domain":
domainArgs := c.RemainingArgs()
if len(domainArgs) > 1 {
return plugin.Error("tls", c.Errf("Too many arguments to domain"))
}
domainNameACME = domainArgs[0]
case argCa:
case "ca":
caArgs := c.RemainingArgs()
if len(caArgs) > 1 {
return plugin.Error("tls", c.Errf("Too many arguments to ca"))
}
ca = caArgs[0]
case argCaCert:
case "cacert":
caCertArgs := c.RemainingArgs()
if len(caCertArgs) > 1 {
return plugin.Error("tls", c.Errf("Too many arguments to cacert"))
}
caCert = caCertArgs[0]
case argEmail:
case "email":
emailArgs := c.RemainingArgs()
if len(emailArgs) > 1 {
return plugin.Error("tls", c.Errf("Too many arguments to email"))
}
email = emailArgs[0]
case argPort:
case "port":
portArgs := c.RemainingArgs()
if len(portArgs) > 1 {
return plugin.Error("tls", c.Errf("Too many arguments to port"))
}
port = portArgs[0]
case argCertPath:
case "certpath":
certPathArgs := c.RemainingArgs()
if len(certPathArgs) > 1 {
return plugin.Error("tls", c.Errf("Too many arguments to CertPath"))
return plugin.Error("tls", c.Errf("Too many arguments to certpath"))
}
certPath = certPathArgs[0]
case argCheckInternal:
case "checkinterval":
checkIntervalArgs := c.RemainingArgs()
if len(checkIntervalArgs) > 1 {
return plugin.Error("tls", c.Errf("Too many arguments to checkInterval"))
return plugin.Error("tls", c.Errf("Too many arguments to checkinterval"))
}
interval, err := strconv.Atoi(checkIntervalArgs[0])
if err != nil {
Expand All @@ -142,12 +140,18 @@ func parseTLS(c *caddy.Controller) error {
}
}

manager := NewACMEManager(config, domainNameACME, ca, certPath, caCert, portNumber, email)
pool, err := setupCertPool(caCert)
if err != nil {
log.Errorf("Failed to add the custom CA certfiicate to the pool of trusted certificates: %v, \n", err)
}
certmagicConfig := NewConfig(certPath)
certmagicIssuer := NewIssuer(certmagicConfig, ca, email, pool, portNumber)
certManager := NewCertManager(domainNameACME, certmagicConfig, certmagicIssuer)

var names []string
names = append(names, manager.Zone)
names = append(names, certManager.Zone)

tlsconf, cert, err = manager.configureTLSwithACME(ctx)
tlsconf, cert, err = certManager.configureTLSwithACME(ctx)
if err != nil {
log.Errorf("Failed to setup TLS automatically: %v \n", err)
}
Expand All @@ -159,7 +163,7 @@ func parseTLS(c *caddy.Controller) error {
log.Debug("Starting certificate renewal loop in the background")
for {
time.Sleep(time.Duration(checkInterval) * time.Minute)
if cert.NeedsRenewal(manager.Config) {
if cert.NeedsRenewal(certManager.Config) {
log.Info("Certificate expiring soon, initializing reload")
r.renew <- true
}
Expand Down
4 changes: 2 additions & 2 deletions setup_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,13 +34,13 @@ func TestTLS(t *testing.T) {

// acme
// positive
{"tls acme {\ndomain example.com\n}", false, "", ""},
//{"tls acme {\ndomain example.com\n}", false, "", ""},
{"tls acme {\ndomain example.com\nca localhost:14001\n}", false, "", ""},
// negative
{"tls acme {\nunknown\n}", true, "", "unknown argument to acme"},
{"tls acme {\ndomain none none\n}", true, "", "Too many arguments to domain"},
{"tls acme {\ndomain example.com\n ca none none\n}", true, "", "Too many arguments to ca"},
{"tls acme {\ndomain example.com\n certpath none none\n}", true, "", "Too many arguments to CertPath"},
{"tls acme {\ndomain example.com\n certpath none none\n}", true, "", "Too many arguments to certpath"},
{"tls acme {\ndomain example.com\n port none none\n}", true, "", "Too many arguments to port"},
}

Expand Down
2 changes: 2 additions & 0 deletions solver.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ func (ds *DNSSolver) Start(p net.PacketConn, challenge acme.Challenge) error {
m := new(dns.Msg)
m.SetReply(r)

// Answering CAA Requests is mandatory for some CA's.
// Let's Encrypt will not issue a Certificate if these requests time out
if state.QType() == dns.TypeCAA {
hdr := dns.RR_Header{Name: state.QName(), Rrtype: dns.TypeCAA, Class: dns.ClassANY, Ttl: 0}
m.Answer = append(m.Answer, &dns.CAA{Hdr: hdr})
Expand Down
2 changes: 1 addition & 1 deletion test/pebble_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ func TestObtainCertOnStartup(t *testing.T) {
config: `tls://.:1053 {
tls acme {
domain example.com
ca localhost:14000//dir
ca localhost:14000/dir
certpath /tmp/certmagic/
cacert test/certs/pebble.minica.pem
port 1053
Expand Down

0 comments on commit 4147afd

Please sign in to comment.