Add option to select algorithm to generate ACME certificates
This commit is contained in:
parent
e691168cdc
commit
68cc826519
12 changed files with 179 additions and 23 deletions
|
@ -14,6 +14,7 @@ import (
|
|||
"time"
|
||||
|
||||
"github.com/containous/traefik/log"
|
||||
acmeprovider "github.com/containous/traefik/provider/acme"
|
||||
"github.com/containous/traefik/types"
|
||||
acme "github.com/xenolf/lego/acmev2"
|
||||
)
|
||||
|
@ -23,6 +24,7 @@ type Account struct {
|
|||
Email string
|
||||
Registration *acme.RegistrationResource
|
||||
PrivateKey []byte
|
||||
KeyType acme.KeyType
|
||||
DomainsCertificate DomainsCertificates
|
||||
ChallengeCerts map[string]*ChallengeCert
|
||||
HTTPChallenge map[string]map[string][]byte
|
||||
|
@ -63,7 +65,9 @@ func (a *Account) Init() error {
|
|||
}
|
||||
|
||||
// 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
|
||||
privateKey, err := rsa.GenerateKey(rand.Reader, 4096)
|
||||
if err != nil {
|
||||
|
@ -79,6 +83,7 @@ func NewAccount(email string, certs []*DomainsCertificate) (*Account, error) {
|
|||
return &Account{
|
||||
Email: email,
|
||||
PrivateKey: x509.MarshalPKCS1PrivateKey(privateKey),
|
||||
KeyType: keyType,
|
||||
DomainsCertificate: DomainsCertificates{Certs: domainsCerts.Certs},
|
||||
ChallengeCerts: map[string]*ChallengeCert{}}, nil
|
||||
}
|
||||
|
|
|
@ -46,6 +46,7 @@ type ACME struct {
|
|||
OnHostRule bool `description:"Enable certificate generation on frontends Host rules."`
|
||||
CAServer string `description:"CA server to use."`
|
||||
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"`
|
||||
HTTPChallenge *acmeprovider.HTTPChallenge `description:"Activate HTTP-01 Challenge"`
|
||||
DNSProvider string `description:"(Deprecated) Activate DNS-01 Challenge"` // Deprecated
|
||||
|
@ -186,7 +187,7 @@ func (a *ACME) leadershipListener(elected bool) error {
|
|||
domainsCerts = account.DomainsCertificate
|
||||
}
|
||||
|
||||
account, err = NewAccount(a.Email, domainsCerts.Certs)
|
||||
account, err = NewAccount(a.Email, domainsCerts.Certs, a.KeyType)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -395,7 +396,7 @@ func (a *ACME) buildACMEClient(account *Account) (*acme.Client, error) {
|
|||
if len(a.CAServer) > 0 {
|
||||
caServer = a.CAServer
|
||||
}
|
||||
client, err := acme.NewClient(caServer, account, acme.RSA4096)
|
||||
client, err := acme.NewClient(caServer, account, account.KeyType)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
|
@ -64,6 +64,7 @@ func RemoveAccountV1Values(account *Account) error {
|
|||
account.Email = ""
|
||||
account.Registration = nil
|
||||
account.PrivateKey = nil
|
||||
account.KeyType = "RSA4096"
|
||||
}
|
||||
}
|
||||
return nil
|
||||
|
@ -113,6 +114,7 @@ func ConvertToNewFormat(fileName string) {
|
|||
PrivateKey: account.PrivateKey,
|
||||
Registration: account.Registration,
|
||||
Email: account.Email,
|
||||
KeyType: account.KeyType,
|
||||
}
|
||||
|
||||
var newCertificates []*acme.Certificate
|
||||
|
@ -167,6 +169,7 @@ func FromNewToOldFormat(fileName string) (*Account, error) {
|
|||
PrivateKey: storeAccount.PrivateKey,
|
||||
Registration: storeAccount.Registration,
|
||||
DomainsCertificate: DomainsCertificates{},
|
||||
KeyType: storeAccount.KeyType,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -381,6 +381,7 @@ func (gc *GlobalConfiguration) InitACMEProvider() *acmeprovider.Provider {
|
|||
if gc.Cluster == nil {
|
||||
provider := &acmeprovider.Provider{}
|
||||
provider.Configuration = &acmeprovider.Configuration{
|
||||
KeyType: gc.ACME.KeyType,
|
||||
OnHostRule: gc.ACME.OnHostRule,
|
||||
OnDemand: gc.ACME.OnDemand,
|
||||
Email: gc.ACME.Email,
|
||||
|
|
|
@ -86,6 +86,15 @@ entryPoint = "https"
|
|||
#
|
||||
# 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.
|
||||
# Only domains defined here can generate wildcard certificates.
|
||||
#
|
||||
|
|
|
@ -9,7 +9,7 @@ readonly doc_file=$basedir"/docker-compose.yml"
|
|||
down_environment() {
|
||||
echo "STOP Docker environment"
|
||||
! 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)
|
||||
|
@ -17,7 +17,7 @@ down_environment() {
|
|||
up_environment() {
|
||||
echo "START Docker environment"
|
||||
! 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
|
||||
|
@ -40,7 +40,7 @@ start_boulder() {
|
|||
sleep 5
|
||||
let waiting_counter-=1
|
||||
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
|
||||
exit 41
|
||||
fi
|
||||
|
|
|
@ -2,6 +2,7 @@ package integration
|
|||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
|
@ -24,6 +25,7 @@ type AcmeTestCase struct {
|
|||
onDemand bool
|
||||
traefikConfFilePath string
|
||||
domainToCheck string
|
||||
algorithm x509.PublicKeyAlgorithm
|
||||
}
|
||||
|
||||
const (
|
||||
|
@ -60,7 +62,8 @@ func (s *AcmeSuite) TestACMEProviderAtStart(c *check.C) {
|
|||
testCase := AcmeTestCase{
|
||||
traefikConfFilePath: "fixtures/provideracme/acme.toml",
|
||||
onDemand: false,
|
||||
domainToCheck: acmeDomain}
|
||||
domainToCheck: acmeDomain,
|
||||
algorithm: x509.RSA}
|
||||
|
||||
s.retrieveAcmeCertificate(c, testCase)
|
||||
}
|
||||
|
@ -70,7 +73,8 @@ func (s *AcmeSuite) TestACMEProviderAtStartInSAN(c *check.C) {
|
|||
testCase := AcmeTestCase{
|
||||
traefikConfFilePath: "fixtures/provideracme/acme_insan.toml",
|
||||
onDemand: false,
|
||||
domainToCheck: "acme.wtf"}
|
||||
domainToCheck: "acme.wtf",
|
||||
algorithm: x509.RSA}
|
||||
|
||||
s.retrieveAcmeCertificate(c, testCase)
|
||||
}
|
||||
|
@ -80,7 +84,30 @@ func (s *AcmeSuite) TestACMEProviderOnHost(c *check.C) {
|
|||
testCase := AcmeTestCase{
|
||||
traefikConfFilePath: "fixtures/provideracme/acme_onhost.toml",
|
||||
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)
|
||||
}
|
||||
|
@ -90,7 +117,8 @@ func (s *AcmeSuite) TestACMEProviderOnHostWithNoACMEChallenge(c *check.C) {
|
|||
testCase := AcmeTestCase{
|
||||
traefikConfFilePath: "fixtures/acme/no_challenge_acme.toml",
|
||||
onDemand: false,
|
||||
domainToCheck: traefikDefaultDomain}
|
||||
domainToCheck: traefikDefaultDomain,
|
||||
algorithm: x509.RSA}
|
||||
|
||||
s.retrieveAcmeCertificate(c, testCase)
|
||||
}
|
||||
|
@ -100,7 +128,8 @@ func (s *AcmeSuite) TestOnDemandRetrieveAcmeCertificateHTTP01(c *check.C) {
|
|||
testCase := AcmeTestCase{
|
||||
traefikConfFilePath: "fixtures/acme/acme_http01.toml",
|
||||
onDemand: true,
|
||||
domainToCheck: acmeDomain}
|
||||
domainToCheck: acmeDomain,
|
||||
algorithm: x509.RSA}
|
||||
|
||||
s.retrieveAcmeCertificate(c, testCase)
|
||||
}
|
||||
|
@ -110,7 +139,8 @@ func (s *AcmeSuite) TestOnHostRuleRetrieveAcmeCertificateHTTP01(c *check.C) {
|
|||
testCase := AcmeTestCase{
|
||||
traefikConfFilePath: "fixtures/acme/acme_http01.toml",
|
||||
onDemand: false,
|
||||
domainToCheck: acmeDomain}
|
||||
domainToCheck: acmeDomain,
|
||||
algorithm: x509.RSA}
|
||||
|
||||
s.retrieveAcmeCertificate(c, testCase)
|
||||
}
|
||||
|
@ -120,7 +150,8 @@ func (s *AcmeSuite) TestOnHostRuleRetrieveAcmeCertificateHTTP01WithPath(c *check
|
|||
testCase := AcmeTestCase{
|
||||
traefikConfFilePath: "fixtures/acme/acme_http01_web.toml",
|
||||
onDemand: false,
|
||||
domainToCheck: acmeDomain}
|
||||
domainToCheck: acmeDomain,
|
||||
algorithm: x509.RSA}
|
||||
|
||||
s.retrieveAcmeCertificate(c, testCase)
|
||||
}
|
||||
|
@ -130,7 +161,8 @@ func (s *AcmeSuite) TestOnDemandRetrieveAcmeCertificateWithWildcard(c *check.C)
|
|||
testCase := AcmeTestCase{
|
||||
traefikConfFilePath: "fixtures/acme/acme_provided.toml",
|
||||
onDemand: true,
|
||||
domainToCheck: wildcardDomain}
|
||||
domainToCheck: wildcardDomain,
|
||||
algorithm: x509.RSA}
|
||||
|
||||
s.retrieveAcmeCertificate(c, testCase)
|
||||
}
|
||||
|
@ -140,7 +172,8 @@ func (s *AcmeSuite) TestOnHostRuleRetrieveAcmeCertificateWithWildcard(c *check.C
|
|||
testCase := AcmeTestCase{
|
||||
traefikConfFilePath: "fixtures/acme/acme_provided.toml",
|
||||
onDemand: false,
|
||||
domainToCheck: wildcardDomain}
|
||||
domainToCheck: wildcardDomain,
|
||||
algorithm: x509.RSA}
|
||||
|
||||
s.retrieveAcmeCertificate(c, testCase)
|
||||
}
|
||||
|
@ -150,7 +183,8 @@ func (s *AcmeSuite) TestOnDemandRetrieveAcmeCertificateWithDynamicWildcard(c *ch
|
|||
testCase := AcmeTestCase{
|
||||
traefikConfFilePath: "fixtures/acme/acme_provided_dynamic.toml",
|
||||
onDemand: true,
|
||||
domainToCheck: wildcardDomain}
|
||||
domainToCheck: wildcardDomain,
|
||||
algorithm: x509.RSA}
|
||||
|
||||
s.retrieveAcmeCertificate(c, testCase)
|
||||
}
|
||||
|
@ -160,7 +194,8 @@ func (s *AcmeSuite) TestOnHostRuleRetrieveAcmeCertificateWithDynamicWildcard(c *
|
|||
testCase := AcmeTestCase{
|
||||
traefikConfFilePath: "fixtures/acme/acme_provided_dynamic.toml",
|
||||
onDemand: false,
|
||||
domainToCheck: wildcardDomain}
|
||||
domainToCheck: wildcardDomain,
|
||||
algorithm: x509.RSA}
|
||||
|
||||
s.retrieveAcmeCertificate(c, testCase)
|
||||
}
|
||||
|
@ -181,8 +216,9 @@ func (s *AcmeSuite) TestNoValidLetsEncryptServer(c *check.C) {
|
|||
// Doing an HTTPS request and test the response certificate
|
||||
func (s *AcmeSuite) retrieveAcmeCertificate(c *check.C, testCase AcmeTestCase) {
|
||||
file := s.adaptFile(c, testCase.traefikConfFilePath, struct {
|
||||
BoulderHost string
|
||||
OnDemand, OnHostRule bool
|
||||
BoulderHost string
|
||||
OnDemand bool
|
||||
OnHostRule bool
|
||||
}{
|
||||
BoulderHost: s.boulderIP,
|
||||
OnDemand: testCase.onDemand,
|
||||
|
@ -251,4 +287,5 @@ func (s *AcmeSuite) retrieveAcmeCertificate(c *check.C, testCase AcmeTestCase) {
|
|||
c.Assert(resp.StatusCode, checker.Equals, http.StatusOK)
|
||||
// Check Domain into response certificate
|
||||
c.Assert(resp.TLS.PeerCertificates[0].Subject.CommonName, checker.Equals, testCase.domainToCheck)
|
||||
c.Assert(resp.TLS.PeerCertificates[0].PublicKeyAlgorithm, checker.Equals, testCase.algorithm)
|
||||
}
|
||||
|
|
|
@ -12,7 +12,7 @@ defaultEntryPoints = ["http", "https"]
|
|||
|
||||
[acme]
|
||||
email = "test@traefik.io"
|
||||
storage = "/tmp/acme.jsonl"
|
||||
storage = "/tmp/acme.json"
|
||||
entryPoint = "https"
|
||||
onDemand = {{.OnDemand}}
|
||||
onHostRule = {{.OnHostRule}}
|
||||
|
|
38
integration/fixtures/provideracme/acme_onhost_ecdsa.toml
Normal file
38
integration/fixtures/provideracme/acme_onhost_ecdsa.toml
Normal 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"
|
|
@ -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"
|
|
@ -15,6 +15,7 @@ type Account struct {
|
|||
Email string
|
||||
Registration *acme.RegistrationResource
|
||||
PrivateKey []byte
|
||||
KeyType acme.KeyType
|
||||
}
|
||||
|
||||
const (
|
||||
|
@ -23,7 +24,9 @@ const (
|
|||
)
|
||||
|
||||
// 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
|
||||
privateKey, err := rsa.GenerateKey(rand.Reader, 4096)
|
||||
if err != nil {
|
||||
|
@ -33,6 +36,7 @@ func NewAccount(email string) (*Account, error) {
|
|||
return &Account{
|
||||
Email: email,
|
||||
PrivateKey: x509.MarshalPKCS1PrivateKey(privateKey),
|
||||
KeyType: keyType,
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
@ -55,3 +59,22 @@ func (a *Account) GetPrivateKey() crypto.PrivateKey {
|
|||
log.Errorf("Cannot unmarshal private key %+v", a.PrivateKey)
|
||||
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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -39,6 +39,7 @@ type Configuration struct {
|
|||
CAServer string `description:"CA server to use."`
|
||||
Storage string `description:"Storage 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."`
|
||||
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"`
|
||||
|
@ -116,7 +117,7 @@ func (p *Provider) init() error {
|
|||
func (p *Provider) initAccount() (*Account, error) {
|
||||
if p.account == nil || len(p.account.Email) == 0 {
|
||||
var err error
|
||||
p.account, err = NewAccount(p.Email)
|
||||
p.account, err = NewAccount(p.Email, p.KeyType)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -246,7 +247,7 @@ func (p *Provider) getClient() (*acme.Client, error) {
|
|||
caServer = p.CAServer
|
||||
}
|
||||
log.Debugf(caServer)
|
||||
client, err := acme.NewClient(caServer, account, acme.RSA4096)
|
||||
client, err := acme.NewClient(caServer, account, account.KeyType)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue