Add option to select algorithm to generate ACME certificates

This commit is contained in:
Michael 2018-05-16 11:44:03 +02:00 committed by Traefiker Bot
parent e691168cdc
commit 68cc826519
12 changed files with 179 additions and 23 deletions

View file

@ -14,6 +14,7 @@ import (
"time" "time"
"github.com/containous/traefik/log" "github.com/containous/traefik/log"
acmeprovider "github.com/containous/traefik/provider/acme"
"github.com/containous/traefik/types" "github.com/containous/traefik/types"
acme "github.com/xenolf/lego/acmev2" acme "github.com/xenolf/lego/acmev2"
) )
@ -23,6 +24,7 @@ type Account struct {
Email string Email string
Registration *acme.RegistrationResource Registration *acme.RegistrationResource
PrivateKey []byte PrivateKey []byte
KeyType acme.KeyType
DomainsCertificate DomainsCertificates DomainsCertificate DomainsCertificates
ChallengeCerts map[string]*ChallengeCert ChallengeCerts map[string]*ChallengeCert
HTTPChallenge map[string]map[string][]byte HTTPChallenge map[string]map[string][]byte
@ -63,7 +65,9 @@ func (a *Account) Init() error {
} }
// NewAccount creates an account // NewAccount creates an account
func NewAccount(email string, certs []*DomainsCertificate) (*Account, error) { func NewAccount(email string, certs []*DomainsCertificate, keyTypeValue string) (*Account, error) {
keyType := acmeprovider.GetKeyType(keyTypeValue)
// Create a user. New accounts need an email and private key to start // Create a user. New accounts need an email and private key to start
privateKey, err := rsa.GenerateKey(rand.Reader, 4096) privateKey, err := rsa.GenerateKey(rand.Reader, 4096)
if err != nil { if err != nil {
@ -79,6 +83,7 @@ func NewAccount(email string, certs []*DomainsCertificate) (*Account, error) {
return &Account{ return &Account{
Email: email, Email: email,
PrivateKey: x509.MarshalPKCS1PrivateKey(privateKey), PrivateKey: x509.MarshalPKCS1PrivateKey(privateKey),
KeyType: keyType,
DomainsCertificate: DomainsCertificates{Certs: domainsCerts.Certs}, DomainsCertificate: DomainsCertificates{Certs: domainsCerts.Certs},
ChallengeCerts: map[string]*ChallengeCert{}}, nil ChallengeCerts: map[string]*ChallengeCert{}}, nil
} }

View file

@ -46,6 +46,7 @@ type ACME struct {
OnHostRule bool `description:"Enable certificate generation on frontends Host rules."` OnHostRule bool `description:"Enable certificate generation on frontends Host rules."`
CAServer string `description:"CA server to use."` CAServer string `description:"CA server to use."`
EntryPoint string `description:"Entrypoint to proxy acme challenge to."` EntryPoint string `description:"Entrypoint to proxy acme challenge to."`
KeyType string `description:"KeyType used for generating certificate private key. Allow value 'EC256', 'EC384', 'RSA2048', 'RSA4096', 'RSA8192'. Default to 'RSA4096'"`
DNSChallenge *acmeprovider.DNSChallenge `description:"Activate DNS-01 Challenge"` DNSChallenge *acmeprovider.DNSChallenge `description:"Activate DNS-01 Challenge"`
HTTPChallenge *acmeprovider.HTTPChallenge `description:"Activate HTTP-01 Challenge"` HTTPChallenge *acmeprovider.HTTPChallenge `description:"Activate HTTP-01 Challenge"`
DNSProvider string `description:"(Deprecated) Activate DNS-01 Challenge"` // Deprecated DNSProvider string `description:"(Deprecated) Activate DNS-01 Challenge"` // Deprecated
@ -186,7 +187,7 @@ func (a *ACME) leadershipListener(elected bool) error {
domainsCerts = account.DomainsCertificate domainsCerts = account.DomainsCertificate
} }
account, err = NewAccount(a.Email, domainsCerts.Certs) account, err = NewAccount(a.Email, domainsCerts.Certs, a.KeyType)
if err != nil { if err != nil {
return err return err
} }
@ -395,7 +396,7 @@ func (a *ACME) buildACMEClient(account *Account) (*acme.Client, error) {
if len(a.CAServer) > 0 { if len(a.CAServer) > 0 {
caServer = a.CAServer caServer = a.CAServer
} }
client, err := acme.NewClient(caServer, account, acme.RSA4096) client, err := acme.NewClient(caServer, account, account.KeyType)
if err != nil { if err != nil {
return nil, err return nil, err
} }

View file

@ -64,6 +64,7 @@ func RemoveAccountV1Values(account *Account) error {
account.Email = "" account.Email = ""
account.Registration = nil account.Registration = nil
account.PrivateKey = nil account.PrivateKey = nil
account.KeyType = "RSA4096"
} }
} }
return nil return nil
@ -113,6 +114,7 @@ func ConvertToNewFormat(fileName string) {
PrivateKey: account.PrivateKey, PrivateKey: account.PrivateKey,
Registration: account.Registration, Registration: account.Registration,
Email: account.Email, Email: account.Email,
KeyType: account.KeyType,
} }
var newCertificates []*acme.Certificate var newCertificates []*acme.Certificate
@ -167,6 +169,7 @@ func FromNewToOldFormat(fileName string) (*Account, error) {
PrivateKey: storeAccount.PrivateKey, PrivateKey: storeAccount.PrivateKey,
Registration: storeAccount.Registration, Registration: storeAccount.Registration,
DomainsCertificate: DomainsCertificates{}, DomainsCertificate: DomainsCertificates{},
KeyType: storeAccount.KeyType,
} }
} }

View file

@ -381,6 +381,7 @@ func (gc *GlobalConfiguration) InitACMEProvider() *acmeprovider.Provider {
if gc.Cluster == nil { if gc.Cluster == nil {
provider := &acmeprovider.Provider{} provider := &acmeprovider.Provider{}
provider.Configuration = &acmeprovider.Configuration{ provider.Configuration = &acmeprovider.Configuration{
KeyType: gc.ACME.KeyType,
OnHostRule: gc.ACME.OnHostRule, OnHostRule: gc.ACME.OnHostRule,
OnDemand: gc.ACME.OnDemand, OnDemand: gc.ACME.OnDemand,
Email: gc.ACME.Email, Email: gc.ACME.Email,

View file

@ -86,6 +86,15 @@ entryPoint = "https"
# #
# caServer = "https://acme-staging-v02.api.letsencrypt.org/directory" # caServer = "https://acme-staging-v02.api.letsencrypt.org/directory"
# KeyType to use.
#
# Optional
# Default: "RSA4096"
#
# Available values : "EC256", "EC384", "RSA2048", "RSA4096", "RSA8192"
#
# KeyType = "RSA4096"
# Domains list. # Domains list.
# Only domains defined here can generate wildcard certificates. # Only domains defined here can generate wildcard certificates.
# #

View file

@ -9,7 +9,7 @@ readonly doc_file=$basedir"/docker-compose.yml"
down_environment() { down_environment() {
echo "STOP Docker environment" echo "STOP Docker environment"
! docker-compose -f $doc_file down -v &>/dev/null && \ ! docker-compose -f $doc_file down -v &>/dev/null && \
echo "[ERROR] Impossible to stop the Docker environment" && exit 11 echo "[ERROR] Unable to stop the Docker environment" && exit 11
} }
# Create and start Docker-compose environment or subpart of its services (if services are listed) # Create and start Docker-compose environment or subpart of its services (if services are listed)
@ -17,7 +17,7 @@ down_environment() {
up_environment() { up_environment() {
echo "START Docker environment" echo "START Docker environment"
! docker-compose -f $doc_file up -d $@ &>/dev/null && \ ! docker-compose -f $doc_file up -d $@ &>/dev/null && \
echo "[ERROR] Impossible to start Docker environment" && exit 21 echo "[ERROR] Unable to start Docker environment" && exit 21
} }
# Init the environment : get IP address and create needed files # Init the environment : get IP address and create needed files
@ -40,7 +40,7 @@ start_boulder() {
sleep 5 sleep 5
let waiting_counter-=1 let waiting_counter-=1
if [[ $waiting_counter -eq 0 ]]; then if [[ $waiting_counter -eq 0 ]]; then
echo "[ERROR] Impossible to start boulder container in the allowed time, the Docker environment will be stopped" echo "[ERROR] Unable to start boulder container in the allowed time, the Docker environment will be stopped"
down_environment down_environment
exit 41 exit 41
fi fi

View file

@ -2,6 +2,7 @@ package integration
import ( import (
"crypto/tls" "crypto/tls"
"crypto/x509"
"fmt" "fmt"
"net/http" "net/http"
"os" "os"
@ -24,6 +25,7 @@ type AcmeTestCase struct {
onDemand bool onDemand bool
traefikConfFilePath string traefikConfFilePath string
domainToCheck string domainToCheck string
algorithm x509.PublicKeyAlgorithm
} }
const ( const (
@ -60,7 +62,8 @@ func (s *AcmeSuite) TestACMEProviderAtStart(c *check.C) {
testCase := AcmeTestCase{ testCase := AcmeTestCase{
traefikConfFilePath: "fixtures/provideracme/acme.toml", traefikConfFilePath: "fixtures/provideracme/acme.toml",
onDemand: false, onDemand: false,
domainToCheck: acmeDomain} domainToCheck: acmeDomain,
algorithm: x509.RSA}
s.retrieveAcmeCertificate(c, testCase) s.retrieveAcmeCertificate(c, testCase)
} }
@ -70,7 +73,8 @@ func (s *AcmeSuite) TestACMEProviderAtStartInSAN(c *check.C) {
testCase := AcmeTestCase{ testCase := AcmeTestCase{
traefikConfFilePath: "fixtures/provideracme/acme_insan.toml", traefikConfFilePath: "fixtures/provideracme/acme_insan.toml",
onDemand: false, onDemand: false,
domainToCheck: "acme.wtf"} domainToCheck: "acme.wtf",
algorithm: x509.RSA}
s.retrieveAcmeCertificate(c, testCase) s.retrieveAcmeCertificate(c, testCase)
} }
@ -80,7 +84,30 @@ func (s *AcmeSuite) TestACMEProviderOnHost(c *check.C) {
testCase := AcmeTestCase{ testCase := AcmeTestCase{
traefikConfFilePath: "fixtures/provideracme/acme_onhost.toml", traefikConfFilePath: "fixtures/provideracme/acme_onhost.toml",
onDemand: false, onDemand: false,
domainToCheck: acmeDomain} domainToCheck: acmeDomain,
algorithm: x509.RSA}
s.retrieveAcmeCertificate(c, testCase)
}
// Test ACME provider with certificate at start ECDSA algo
func (s *AcmeSuite) TestACMEProviderOnHostECDSA(c *check.C) {
testCase := AcmeTestCase{
traefikConfFilePath: "fixtures/provideracme/acme_onhost_ecdsa.toml",
onDemand: false,
domainToCheck: acmeDomain,
algorithm: x509.ECDSA}
s.retrieveAcmeCertificate(c, testCase)
}
// Test ACME provider with certificate at start invalid algo default RSA
func (s *AcmeSuite) TestACMEProviderOnHostInvalidAlgo(c *check.C) {
testCase := AcmeTestCase{
traefikConfFilePath: "fixtures/provideracme/acme_onhost_invalid_algo.toml",
onDemand: false,
domainToCheck: acmeDomain,
algorithm: x509.RSA}
s.retrieveAcmeCertificate(c, testCase) s.retrieveAcmeCertificate(c, testCase)
} }
@ -90,7 +117,8 @@ func (s *AcmeSuite) TestACMEProviderOnHostWithNoACMEChallenge(c *check.C) {
testCase := AcmeTestCase{ testCase := AcmeTestCase{
traefikConfFilePath: "fixtures/acme/no_challenge_acme.toml", traefikConfFilePath: "fixtures/acme/no_challenge_acme.toml",
onDemand: false, onDemand: false,
domainToCheck: traefikDefaultDomain} domainToCheck: traefikDefaultDomain,
algorithm: x509.RSA}
s.retrieveAcmeCertificate(c, testCase) s.retrieveAcmeCertificate(c, testCase)
} }
@ -100,7 +128,8 @@ func (s *AcmeSuite) TestOnDemandRetrieveAcmeCertificateHTTP01(c *check.C) {
testCase := AcmeTestCase{ testCase := AcmeTestCase{
traefikConfFilePath: "fixtures/acme/acme_http01.toml", traefikConfFilePath: "fixtures/acme/acme_http01.toml",
onDemand: true, onDemand: true,
domainToCheck: acmeDomain} domainToCheck: acmeDomain,
algorithm: x509.RSA}
s.retrieveAcmeCertificate(c, testCase) s.retrieveAcmeCertificate(c, testCase)
} }
@ -110,7 +139,8 @@ func (s *AcmeSuite) TestOnHostRuleRetrieveAcmeCertificateHTTP01(c *check.C) {
testCase := AcmeTestCase{ testCase := AcmeTestCase{
traefikConfFilePath: "fixtures/acme/acme_http01.toml", traefikConfFilePath: "fixtures/acme/acme_http01.toml",
onDemand: false, onDemand: false,
domainToCheck: acmeDomain} domainToCheck: acmeDomain,
algorithm: x509.RSA}
s.retrieveAcmeCertificate(c, testCase) s.retrieveAcmeCertificate(c, testCase)
} }
@ -120,7 +150,8 @@ func (s *AcmeSuite) TestOnHostRuleRetrieveAcmeCertificateHTTP01WithPath(c *check
testCase := AcmeTestCase{ testCase := AcmeTestCase{
traefikConfFilePath: "fixtures/acme/acme_http01_web.toml", traefikConfFilePath: "fixtures/acme/acme_http01_web.toml",
onDemand: false, onDemand: false,
domainToCheck: acmeDomain} domainToCheck: acmeDomain,
algorithm: x509.RSA}
s.retrieveAcmeCertificate(c, testCase) s.retrieveAcmeCertificate(c, testCase)
} }
@ -130,7 +161,8 @@ func (s *AcmeSuite) TestOnDemandRetrieveAcmeCertificateWithWildcard(c *check.C)
testCase := AcmeTestCase{ testCase := AcmeTestCase{
traefikConfFilePath: "fixtures/acme/acme_provided.toml", traefikConfFilePath: "fixtures/acme/acme_provided.toml",
onDemand: true, onDemand: true,
domainToCheck: wildcardDomain} domainToCheck: wildcardDomain,
algorithm: x509.RSA}
s.retrieveAcmeCertificate(c, testCase) s.retrieveAcmeCertificate(c, testCase)
} }
@ -140,7 +172,8 @@ func (s *AcmeSuite) TestOnHostRuleRetrieveAcmeCertificateWithWildcard(c *check.C
testCase := AcmeTestCase{ testCase := AcmeTestCase{
traefikConfFilePath: "fixtures/acme/acme_provided.toml", traefikConfFilePath: "fixtures/acme/acme_provided.toml",
onDemand: false, onDemand: false,
domainToCheck: wildcardDomain} domainToCheck: wildcardDomain,
algorithm: x509.RSA}
s.retrieveAcmeCertificate(c, testCase) s.retrieveAcmeCertificate(c, testCase)
} }
@ -150,7 +183,8 @@ func (s *AcmeSuite) TestOnDemandRetrieveAcmeCertificateWithDynamicWildcard(c *ch
testCase := AcmeTestCase{ testCase := AcmeTestCase{
traefikConfFilePath: "fixtures/acme/acme_provided_dynamic.toml", traefikConfFilePath: "fixtures/acme/acme_provided_dynamic.toml",
onDemand: true, onDemand: true,
domainToCheck: wildcardDomain} domainToCheck: wildcardDomain,
algorithm: x509.RSA}
s.retrieveAcmeCertificate(c, testCase) s.retrieveAcmeCertificate(c, testCase)
} }
@ -160,7 +194,8 @@ func (s *AcmeSuite) TestOnHostRuleRetrieveAcmeCertificateWithDynamicWildcard(c *
testCase := AcmeTestCase{ testCase := AcmeTestCase{
traefikConfFilePath: "fixtures/acme/acme_provided_dynamic.toml", traefikConfFilePath: "fixtures/acme/acme_provided_dynamic.toml",
onDemand: false, onDemand: false,
domainToCheck: wildcardDomain} domainToCheck: wildcardDomain,
algorithm: x509.RSA}
s.retrieveAcmeCertificate(c, testCase) s.retrieveAcmeCertificate(c, testCase)
} }
@ -181,8 +216,9 @@ func (s *AcmeSuite) TestNoValidLetsEncryptServer(c *check.C) {
// Doing an HTTPS request and test the response certificate // Doing an HTTPS request and test the response certificate
func (s *AcmeSuite) retrieveAcmeCertificate(c *check.C, testCase AcmeTestCase) { func (s *AcmeSuite) retrieveAcmeCertificate(c *check.C, testCase AcmeTestCase) {
file := s.adaptFile(c, testCase.traefikConfFilePath, struct { file := s.adaptFile(c, testCase.traefikConfFilePath, struct {
BoulderHost string BoulderHost string
OnDemand, OnHostRule bool OnDemand bool
OnHostRule bool
}{ }{
BoulderHost: s.boulderIP, BoulderHost: s.boulderIP,
OnDemand: testCase.onDemand, OnDemand: testCase.onDemand,
@ -251,4 +287,5 @@ func (s *AcmeSuite) retrieveAcmeCertificate(c *check.C, testCase AcmeTestCase) {
c.Assert(resp.StatusCode, checker.Equals, http.StatusOK) c.Assert(resp.StatusCode, checker.Equals, http.StatusOK)
// Check Domain into response certificate // Check Domain into response certificate
c.Assert(resp.TLS.PeerCertificates[0].Subject.CommonName, checker.Equals, testCase.domainToCheck) c.Assert(resp.TLS.PeerCertificates[0].Subject.CommonName, checker.Equals, testCase.domainToCheck)
c.Assert(resp.TLS.PeerCertificates[0].PublicKeyAlgorithm, checker.Equals, testCase.algorithm)
} }

View file

@ -12,7 +12,7 @@ defaultEntryPoints = ["http", "https"]
[acme] [acme]
email = "test@traefik.io" email = "test@traefik.io"
storage = "/tmp/acme.jsonl" storage = "/tmp/acme.json"
entryPoint = "https" entryPoint = "https"
onDemand = {{.OnDemand}} onDemand = {{.OnDemand}}
onHostRule = {{.OnHostRule}} onHostRule = {{.OnHostRule}}

View file

@ -0,0 +1,38 @@
logLevel = "DEBUG"
defaultEntryPoints = ["http", "https"]
[entryPoints]
[entryPoints.http]
address = ":5002"
[entryPoints.https]
address = ":5001"
[entryPoints.https.tls]
[acme]
email = "test@traefik.io"
storage = "/tmp/acme.json"
entryPoint = "https"
onDemand = {{.OnDemand}}
onHostRule = {{.OnHostRule}}
caServer = "http://{{.BoulderHost}}:4001/directory"
keyType = "EC384"
[acme.httpChallenge]
entryPoint="http"
[api]
[file]
[backends]
[backends.backend]
[backends.backend.servers.server1]
url = "http://127.0.0.1:9010"
weight = 1
[frontends]
[frontends.frontend]
backend = "backend"
[frontends.frontend.routes.test]
rule = "Host:traefik.acme.wtf"

View file

@ -0,0 +1,38 @@
logLevel = "DEBUG"
defaultEntryPoints = ["http", "https"]
[entryPoints]
[entryPoints.http]
address = ":5002"
[entryPoints.https]
address = ":5001"
[entryPoints.https.tls]
[acme]
email = "test@traefik.io"
storage = "/tmp/acme.json"
entryPoint = "https"
onDemand = {{.OnDemand}}
onHostRule = {{.OnHostRule}}
caServer = "http://{{.BoulderHost}}:4001/directory"
keyType = "INVALID"
[acme.httpChallenge]
entryPoint="http"
[api]
[file]
[backends]
[backends.backend]
[backends.backend.servers.server1]
url = "http://127.0.0.1:9010"
weight = 1
[frontends]
[frontends.frontend]
backend = "backend"
[frontends.frontend.routes.test]
rule = "Host:traefik.acme.wtf"

View file

@ -15,6 +15,7 @@ type Account struct {
Email string Email string
Registration *acme.RegistrationResource Registration *acme.RegistrationResource
PrivateKey []byte PrivateKey []byte
KeyType acme.KeyType
} }
const ( const (
@ -23,7 +24,9 @@ const (
) )
// NewAccount creates an account // NewAccount creates an account
func NewAccount(email string) (*Account, error) { func NewAccount(email string, keyTypeValue string) (*Account, error) {
keyType := GetKeyType(keyTypeValue)
// Create a user. New accounts need an email and private key to start // Create a user. New accounts need an email and private key to start
privateKey, err := rsa.GenerateKey(rand.Reader, 4096) privateKey, err := rsa.GenerateKey(rand.Reader, 4096)
if err != nil { if err != nil {
@ -33,6 +36,7 @@ func NewAccount(email string) (*Account, error) {
return &Account{ return &Account{
Email: email, Email: email,
PrivateKey: x509.MarshalPKCS1PrivateKey(privateKey), PrivateKey: x509.MarshalPKCS1PrivateKey(privateKey),
KeyType: keyType,
}, nil }, nil
} }
@ -55,3 +59,22 @@ func (a *Account) GetPrivateKey() crypto.PrivateKey {
log.Errorf("Cannot unmarshal private key %+v", a.PrivateKey) log.Errorf("Cannot unmarshal private key %+v", a.PrivateKey)
return nil return nil
} }
// GetKeyType used to determine which algo to used
func GetKeyType(value string) acme.KeyType {
switch value {
case "EC256":
return acme.EC256
case "EC384":
return acme.EC384
case "RSA2048":
return acme.RSA2048
case "RSA4096":
return acme.RSA4096
case "RSA8192":
return acme.RSA8192
default:
log.Warnf("Unable to determine key type value %s. Use %s as default value", value, acme.RSA4096)
return acme.RSA4096
}
}

View file

@ -39,6 +39,7 @@ type Configuration struct {
CAServer string `description:"CA server to use."` CAServer string `description:"CA server to use."`
Storage string `description:"Storage to use."` Storage string `description:"Storage to use."`
EntryPoint string `description:"EntryPoint to use."` EntryPoint string `description:"EntryPoint to use."`
KeyType string `description:"KeyType used for generating certificate private key. Allow value 'EC256', 'EC384', 'RSA2048', 'RSA4096', 'RSA8192'. Default to 'RSA4096'"`
OnHostRule bool `description:"Enable certificate generation on frontends Host rules."` OnHostRule bool `description:"Enable certificate generation on frontends Host rules."`
OnDemand bool `description:"Enable on demand certificate generation. This will request a certificate from Let's Encrypt during the first TLS handshake for a hostname that does not yet have a certificate."` // Deprecated OnDemand bool `description:"Enable on demand certificate generation. This will request a certificate from Let's Encrypt during the first TLS handshake for a hostname that does not yet have a certificate."` // Deprecated
DNSChallenge *DNSChallenge `description:"Activate DNS-01 Challenge"` DNSChallenge *DNSChallenge `description:"Activate DNS-01 Challenge"`
@ -116,7 +117,7 @@ func (p *Provider) init() error {
func (p *Provider) initAccount() (*Account, error) { func (p *Provider) initAccount() (*Account, error) {
if p.account == nil || len(p.account.Email) == 0 { if p.account == nil || len(p.account.Email) == 0 {
var err error var err error
p.account, err = NewAccount(p.Email) p.account, err = NewAccount(p.Email, p.KeyType)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -246,7 +247,7 @@ func (p *Provider) getClient() (*acme.Client, error) {
caServer = p.CAServer caServer = p.CAServer
} }
log.Debugf(caServer) log.Debugf(caServer)
client, err := acme.NewClient(caServer, account, acme.RSA4096) client, err := acme.NewClient(caServer, account, account.KeyType)
if err != nil { if err != nil {
return nil, err return nil, err
} }