From 631079a12fb8577f8fd6e8484054967445d1bfc2 Mon Sep 17 00:00:00 2001 From: nmengin Date: Mon, 19 Jun 2017 13:22:41 +0200 Subject: [PATCH] feature: Add provided certificates check before to generate ACME certificate when OnHostRule is activated - ADD TI to check the new behaviour with onHostRule and provided certificates - ADD TU on the getProvidedCertificate method --- acme/acme.go | 54 +++++++++--- acme/acme_test.go | 17 ++++ integration/acme_test.go | 92 +++++++++++++++++--- integration/fixtures/acme/README.md | 37 ++++++++ integration/fixtures/acme/acme.toml | 3 +- integration/fixtures/acme/acme_provided.toml | 35 ++++++++ integration/fixtures/acme/ssl/wildcard.crt | 19 ++++ integration/fixtures/acme/ssl/wildcard.key | 27 ++++++ 8 files changed, 258 insertions(+), 26 deletions(-) create mode 100644 integration/fixtures/acme/README.md create mode 100644 integration/fixtures/acme/acme_provided.toml create mode 100644 integration/fixtures/acme/ssl/wildcard.crt create mode 100644 integration/fixtures/acme/ssl/wildcard.key diff --git a/acme/acme.go b/acme/acme.go index 8a4197c1b..6eeaadd09 100644 --- a/acme/acme.go +++ b/acme/acme.go @@ -328,14 +328,11 @@ func (a *ACME) CreateLocalConfig(tlsConfig *tls.Config, checkOnDemandDomain func func (a *ACME) getCertificate(clientHello *tls.ClientHelloInfo) (*tls.Certificate, error) { domain := types.CanonicalDomain(clientHello.ServerName) account := a.store.Get().(*Account) - //use regex to test for wildcard certs that might have been added into TLSConfig - for k := range a.TLSConfig.NameToCertificate { - selector := "^" + strings.Replace(k, "*.", "[^\\.]*\\.?", -1) + "$" - match, _ := regexp.MatchString(selector, domain) - if match { - return a.TLSConfig.NameToCertificate[k], nil - } + + if providedCertificate := a.getProvidedCertificate([]string{domain}); providedCertificate != nil { + return providedCertificate, nil } + if challengeCert, ok := a.challengeProvider.getCertificate(domain); ok { log.Debugf("ACME got challenge %s", domain) return challengeCert, nil @@ -520,8 +517,20 @@ func (a *ACME) loadCertificateOnDemand(clientHello *tls.ClientHelloInfo) (*tls.C // LoadCertificateForDomains loads certificates from ACME for given domains func (a *ACME) LoadCertificateForDomains(domains []string) { a.jobs.In() <- func() { - log.Debugf("LoadCertificateForDomains %s...", domains) + log.Debugf("LoadCertificateForDomains %v...", domains) + + if len(domains) == 0 { + // no domain + return + } + domains = fun.Map(types.CanonicalDomain, domains).([]string) + + // Check provided certificates + if a.getProvidedCertificate(domains) != nil { + return + } + operation := func() error { if a.client == nil { return fmt.Errorf("ACME client still not built") @@ -540,11 +549,7 @@ func (a *ACME) LoadCertificateForDomains(domains []string) { } account := a.store.Get().(*Account) var domain Domain - if len(domains) == 0 { - // no domain - return - - } else if len(domains) > 1 { + if len(domains) > 1 { domain = Domain{Main: domains[0], SANs: domains[1:]} } else { domain = Domain{Main: domains[0]} @@ -578,6 +583,29 @@ func (a *ACME) LoadCertificateForDomains(domains []string) { } } +// Get provided certificate which check a domains list (Main and SANs) +func (a *ACME) getProvidedCertificate(domains []string) *tls.Certificate { + // Use regex to test for provided certs that might have been added into TLSConfig + providedCertMatch := false + log.Debugf("Look for provided certificate to validate %s...", domains) + for k := range a.TLSConfig.NameToCertificate { + selector := "^" + strings.Replace(k, "*.", "[^\\.]*\\.?", -1) + "$" + for _, domainToCheck := range domains { + providedCertMatch, _ = regexp.MatchString(selector, domainToCheck) + if !providedCertMatch { + break + } + } + if providedCertMatch { + log.Debugf("Got provided certificate for domains %s", domains) + return a.TLSConfig.NameToCertificate[k] + + } + } + log.Debugf("No provided certificate found for domains %s, get ACME certificate.", domains) + return nil +} + func (a *ACME) getDomainsCertificates(domains []string) (*Certificate, error) { domains = fun.Map(types.CanonicalDomain, domains).([]string) log.Debugf("Loading ACME certificates %s...", domains) diff --git a/acme/acme_test.go b/acme/acme_test.go index 49ea1c014..493d4fc64 100644 --- a/acme/acme_test.go +++ b/acme/acme_test.go @@ -1,6 +1,7 @@ package acme import ( + "crypto/tls" "encoding/base64" "net/http" "net/http/httptest" @@ -9,6 +10,7 @@ import ( "testing" "time" + "github.com/stretchr/testify/assert" "github.com/xenolf/lego/acme" ) @@ -277,3 +279,18 @@ cijFkALeQp/qyeXdFld2v9gUN3eCgljgcl0QweRoIc=---`) t.Errorf("No change to acme.PreCheckDNS when meant to be adding enforcing override function.") } } + +func TestAcme_getProvidedCertificate(t *testing.T) { + mm := make(map[string]*tls.Certificate) + mm["*.containo.us"] = &tls.Certificate{} + mm["traefik.acme.io"] = &tls.Certificate{} + + a := ACME{TLSConfig: &tls.Config{NameToCertificate: mm}} + + domains := []string{"traefik.containo.us", "trae.containo.us"} + certificate := a.getProvidedCertificate(domains) + assert.NotNil(t, certificate) + domains = []string{"traefik.acme.io", "trae.acme.io"} + certificate = a.getProvidedCertificate(domains) + assert.Nil(t, certificate) +} diff --git a/integration/acme_test.go b/integration/acme_test.go index 485e42673..878eadccc 100644 --- a/integration/acme_test.go +++ b/integration/acme_test.go @@ -2,32 +2,45 @@ package main import ( "crypto/tls" + "errors" "net/http" "os" "os/exec" "time" - "github.com/go-check/check" - - "errors" "github.com/containous/traefik/integration/utils" + "github.com/go-check/check" checker "github.com/vdemeester/shakers" ) // ACME test suites (using libcompose) type AcmeSuite struct { BaseSuite + boulderIP string } +// Acme tests configuration +type AcmeTestCase struct { + onDemand bool + traefikConfFilePath string + domainToCheck string +} + +// Domain to check +const acmeDomain = "traefik.acme.wtf" + +// Wildcard domain to chekc +const wildcardDomain = "*.acme.wtf" + func (s *AcmeSuite) SetUpSuite(c *check.C) { s.createComposeProject(c, "boulder") s.composeProject.Start(c) - boulderHost := s.composeProject.Container(c, "boulder").NetworkSettings.IPAddress + s.boulderIP = s.composeProject.Container(c, "boulder").NetworkSettings.IPAddress // wait for boulder err := utils.Try(120*time.Second, func() error { - resp, err := http.Get("http://" + boulderHost + ":4000/directory") + resp, err := http.Get("http://" + s.boulderIP + ":4000/directory") if err != nil { return err } @@ -47,9 +60,48 @@ func (s *AcmeSuite) TearDownSuite(c *check.C) { } } -func (s *AcmeSuite) TestRetrieveAcmeCertificate(c *check.C) { - boulderHost := s.composeProject.Container(c, "boulder").NetworkSettings.IPAddress - file := s.adaptFile(c, "fixtures/acme/acme.toml", struct{ BoulderHost string }{boulderHost}) +// Test OnDemand option with none provided certificate +func (s *AcmeSuite) TestOnDemandRetrieveAcmeCertificate(c *check.C) { + aTestCase := AcmeTestCase{ + traefikConfFilePath: "fixtures/acme/acme.toml", + onDemand: true, + domainToCheck: acmeDomain} + s.retrieveAcmeCertificate(c, aTestCase) +} + +// Test OnHostRule option with none provided certificate +func (s *AcmeSuite) TestOnHostRuleRetrieveAcmeCertificate(c *check.C) { + aTestCase := AcmeTestCase{ + traefikConfFilePath: "fixtures/acme/acme.toml", + onDemand: false, + domainToCheck: acmeDomain} + s.retrieveAcmeCertificate(c, aTestCase) +} + +// Test OnDemand option with a wildcard provided certificate +func (s *AcmeSuite) TestOnDemandRetrieveAcmeCertificateWithWildcard(c *check.C) { + aTestCase := AcmeTestCase{ + traefikConfFilePath: "fixtures/acme/acme_provided.toml", + onDemand: true, + domainToCheck: wildcardDomain} + s.retrieveAcmeCertificate(c, aTestCase) +} + +// Test onHostRule option with a wildcard provided certificate +func (s *AcmeSuite) TestOnHostRuleRetrieveAcmeCertificateWithWildcard(c *check.C) { + aTestCase := AcmeTestCase{ + traefikConfFilePath: "fixtures/acme/acme_provided.toml", + onDemand: false, + domainToCheck: wildcardDomain} + s.retrieveAcmeCertificate(c, aTestCase) +} + +// Doing an HTTPS request and test the response certificate +func (s *AcmeSuite) retrieveAcmeCertificate(c *check.C, a AcmeTestCase) { + file := s.adaptFile(c, a.traefikConfFilePath, struct { + BoulderHost string + OnDemand, OnHostRule bool + }{s.boulderIP, a.onDemand, !a.onDemand}) defer os.Remove(file) cmd := exec.Command(traefikBinary, "--configFile="+file) err := cmd.Start() @@ -77,16 +129,32 @@ func (s *AcmeSuite) TestRetrieveAcmeCertificate(c *check.C) { tr = &http.Transport{ TLSClientConfig: &tls.Config{ InsecureSkipVerify: true, - ServerName: "traefik.acme.wtf", + ServerName: acmeDomain, }, } client = &http.Client{Transport: tr} req, _ := http.NewRequest("GET", "https://127.0.0.1:5001/", nil) - req.Host = "traefik.acme.wtf" - req.Header.Set("Host", "traefik.acme.wtf") + req.Host = acmeDomain + req.Header.Set("Host", acmeDomain) req.Header.Set("Accept", "*/*") - resp, err := client.Do(req) + + var resp *http.Response + // Retry to send a Request which uses the LE generated certificate + err = utils.Try(60*time.Second, func() error { + resp, err = client.Do(req) + // /!\ If connection is not closed, SSLHandshake will only be done during the first trial /!\ + req.Close = true + if err != nil { + return err + } else if resp.TLS.PeerCertificates[0].Subject.CommonName != a.domainToCheck { + return errors.New("Domain " + resp.TLS.PeerCertificates[0].Subject.CommonName + " found in place of " + a.domainToCheck) + } + return nil + }) c.Assert(err, checker.IsNil) + // Check Domain into response certificate + c.Assert(resp.TLS.PeerCertificates[0].Subject.CommonName, checker.Equals, a.domainToCheck) // Expected a 200 c.Assert(resp.StatusCode, checker.Equals, 200) + } diff --git a/integration/fixtures/acme/README.md b/integration/fixtures/acme/README.md new file mode 100644 index 000000000..e9eedd4c0 --- /dev/null +++ b/integration/fixtures/acme/README.md @@ -0,0 +1,37 @@ +# How to generate the self-signed wildcard certificate + +```bash +#!/usr/bin/env bash + +# Specify where we will install +# the wildcard certificate +SSL_DIR="./ssl" + +# Set the wildcarded domain +# we want to use +DOMAIN="*.acme.wtf" + +# A blank passphrase +PASSPHRASE="" + +# Set our CSR variables +SUBJ=" +C=FR +ST=MP +O= +localityName=Toulouse +commonName=$DOMAIN +organizationalUnitName=Traefik +emailAddress= +" + +# Create our SSL directory +# in case it doesn't exist +sudo mkdir -p "$SSL_DIR" + +# Generate our Private Key, CSR and Certificate +sudo openssl genrsa -out "$SSL_DIR/wildcard.key" 2048 +sudo openssl req -new -subj "$(echo -n "$SUBJ" | tr "\n" "/")" -key "$SSL_DIR/wildcard.key" -out "$SSL_DIR/wildcard.csr" -passin pass:$PASSPHRASE +sudo openssl x509 -req -days 3650 -in "$SSL_DIR/wildcard.csr" -signkey "$SSL_DIR/wildcard.key" -out "$SSL_DIR/wildcard.crt" +sudo rm -f "$SSL_DIR/wildcard.csr" +``` \ No newline at end of file diff --git a/integration/fixtures/acme/acme.toml b/integration/fixtures/acme/acme.toml index e1e637bf0..2c15e8636 100644 --- a/integration/fixtures/acme/acme.toml +++ b/integration/fixtures/acme/acme.toml @@ -14,7 +14,8 @@ defaultEntryPoints = ["http", "https"] email = "test@traefik.io" storage = "/dev/null" entryPoint = "https" -onDemand = true +onDemand = {{.OnDemand}} +OnHostRule = {{.OnHostRule}} caServer = "http://{{.BoulderHost}}:4000/directory" [file] diff --git a/integration/fixtures/acme/acme_provided.toml b/integration/fixtures/acme/acme_provided.toml new file mode 100644 index 000000000..dcd067df4 --- /dev/null +++ b/integration/fixtures/acme/acme_provided.toml @@ -0,0 +1,35 @@ +logLevel = "DEBUG" + +defaultEntryPoints = ["http", "https"] + +[entryPoints] + [entryPoints.http] + address = ":8080" + [entryPoints.https] + address = ":5001" + [entryPoints.https.tls] + [[entryPoints.https.tls.certificates]] + CertFile = "fixtures/acme/ssl/wildcard.crt" + KeyFile = "fixtures/acme/ssl/wildcard.key" + +[acme] +email = "test@traefik.io" +storage = "/dev/null" +entryPoint = "https" +onDemand = {{.OnDemand}} +OnHostRule = {{.OnHostRule}} +caServer = "http://{{.BoulderHost}}:4000/directory" + +[file] + +[backends] + [backends.backend] + [backends.backend.servers.server1] + url = "http://127.0.0.1:9010" + + +[frontends] + [frontends.frontend] + backend = "backend" + [frontends.frontend.routes.test] + rule = "Host:traefik.acme.wtf" diff --git a/integration/fixtures/acme/ssl/wildcard.crt b/integration/fixtures/acme/ssl/wildcard.crt new file mode 100644 index 000000000..8257703a9 --- /dev/null +++ b/integration/fixtures/acme/ssl/wildcard.crt @@ -0,0 +1,19 @@ +-----BEGIN CERTIFICATE----- +MIIDJDCCAgwCCQCS90TE7NuTqzANBgkqhkiG9w0BAQsFADBUMQswCQYDVQQGEwJG +UjELMAkGA1UECAwCTVAxETAPBgNVBAcMCFRvdWxvdXNlMRMwEQYDVQQDDAoqLmFj +bWUud3RmMRAwDgYDVQQLDAdUcmFlZmlrMB4XDTE3MDYyMzE0NTE0MVoXDTI3MDYy +MTE0NTE0MVowVDELMAkGA1UEBhMCRlIxCzAJBgNVBAgMAk1QMREwDwYDVQQHDAhU +b3Vsb3VzZTETMBEGA1UEAwwKKi5hY21lLnd0ZjEQMA4GA1UECwwHVHJhZWZpazCC +ASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAODqsVCLhauFZPhPXqZDIKST +wqoJST+jO5O/WmA7oC4S6JlecRoNsHAXyddd3cQW3yZqB0ryOHrMOpMX0PPXf3jS +OOXoXA6xsq+RXlR4hDrBkOrj/LR/g62Eiuj2JVO2uy6tKJIetSB/Wzl6OgRkY/um +EXIc7zQS81/QKg+pg7Z4AYJht5J88nOFHJ3RspUMaH1vJ6LhH3MOUkgFj+I1OiqX +Tnkd7EDWbkYxAJa0xI2qbmY5VYv8dsIUN+IlPFDtBt87Fc2qv5dQkOz11FDYxWnz ++kxX6+MESLBaTvJjXvG+bzTfh9xCExFQFiN+Us0JuLX8HKQ4MqWL2IiVLsko2osC +AwEAATANBgkqhkiG9w0BAQsFAAOCAQEAl2jTX2yzUpiufrJ6WtZjKIAH8GF817hS +dWvt2eyLrBPvllMUj8zqCE5uNVUDVuXQvOhOyx+3zZzfcgfYqbTD8G8amNWcSiRA +vonoOn1p1pW2OonSi32h3qv5i4gCyh/6cBneYi03lkQ7uLCsJK9+dXTAvoKL6s23 +IXhZGS0Qkvs4vkORA2MX9tyJdyfCCaCx3GpPCGkKrKJ8ePTEvq1ZE2xdhERnV5pz +L1PRY2QthXXVjMz7AXw0gkHvAbtrKVKR1Tv4ZK34bFBh/kyGAjkcn0zdeFKITqTF +tCoXWEArmiRqGuXwbqU3mEA9Cv6aMM+0YX89K2InhOnBU80OWs0uMQ== +-----END CERTIFICATE----- diff --git a/integration/fixtures/acme/ssl/wildcard.key b/integration/fixtures/acme/ssl/wildcard.key new file mode 100644 index 000000000..fe8580b8d --- /dev/null +++ b/integration/fixtures/acme/ssl/wildcard.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEpAIBAAKCAQEA4OqxUIuFq4Vk+E9epkMgpJPCqglJP6M7k79aYDugLhLomV5x +Gg2wcBfJ113dxBbfJmoHSvI4esw6kxfQ89d/eNI45ehcDrGyr5FeVHiEOsGQ6uP8 +tH+DrYSK6PYlU7a7Lq0okh61IH9bOXo6BGRj+6YRchzvNBLzX9AqD6mDtngBgmG3 +knzyc4UcndGylQxofW8nouEfcw5SSAWP4jU6KpdOeR3sQNZuRjEAlrTEjapuZjlV +i/x2whQ34iU8UO0G3zsVzaq/l1CQ7PXUUNjFafP6TFfr4wRIsFpO8mNe8b5vNN+H +3EITEVAWI35SzQm4tfwcpDgypYvYiJUuySjaiwIDAQABAoIBAQCs9Ex9v4x+pQlL +2NzTxXLom6dp0dI92WwK5W696Zv3UhsDNRiMDFLNH73amxfZnizjAU2yWCkOZNX2 +Hq5TlDc11ZJjWRbRRdw+He8HzdUAybCCr+a3dgbv+6hGFGIHydCOyCEWm/50ivq/ +bDoI/pnT/ZQUyCM5TAlSeGSfvp7GRHi9v3HOl85H1Pn2Dvyk9gj4y3BIFrKuv8fJ +o6aEzlfgWGROCzshU2m8fB9P0B4hWDlJsc1D01sW60zhjLo9+XoWznmw5mczz7sc +S5sdDh47rSJsNRuFd7YDjeLzJWPqLrKVB5nn6nRbvrnBqhfsknkO4VIXhmEMSs1u +RMYOJ9ShAoGBAPinA6ktIeez1t5IsfxGwbCeZzFI1suZqZeX6ezNKaMpeykyAPuh +CqN7H+a4NCKsinsgHJowU98ckHeAsQ22s7R8dFZhyxEXkcBawY2soK29eq2aJHnY +lqKOwjOA7wgElRHwLkNFniQ5lKFPMly8a9NVAqg+Th/J3uR+7wE2t+b1AoGBAOeQ +H/vVkdaNB2ovnCxMh+OfxpcjkfF6KnD2jpn/TKsbR5BtnrtyRLc5+qt52D0CEgSy +qU3zrsZebShej3OIBPrEwIcPN+LezaxnLMf9RXdOde+wWrQLWLkShJaSTwSoGqZB +fcO0/sc1lzhGxm++ByP5mWbHr/VM9IdTQQH5Bct/AoGBAMhmOrIXeNL4Az2FU0Vi +dWp2T+7NqKfRAXj264Z5V4xzuxpZfadPhHZ7nhth7Erhyn4vRD4UoxQXPmvB4XCP +Bkh5YX3ZNUNiPorL2mDnd1xvcLcHm0xEfisnaWb/DCbnIomhjHeVXT4O1jYn0Qwi +o7hgNFMKXAaMuUJo9xGAWzkdAoGASxC4nY2tOiz7k1udt+qTPqHj4cjhHbOpoHb8 +4UUWmH0+ZL50b3Vqey8raH0WMSjDqIw2QBPXu2yO3EBTJnOYkaZIdz/isQPjDplf +tfEPnM5tgubbcHQhLdWn75u8S9km0nB2kYPR98gSnmarGzwx2mKmbOAc1Vs+BcRi +VX5hd4cCgYAubBq0VsFT0KVU3Rva3dgPR1K5bp4r4hE5cGXm4HvLiOgv995CwPy1 +27eONF9GN7hvjI6C17jA1Gyx5sN0QrsMv/1BZqiGaragMOPXFD+tVecWuKH4lZQi +VbKTOWHlGkrDCpiYWpfetQAjouj+0c6d+wigcoC8e5dwxBPI2f3rGw== +-----END RSA PRIVATE KEY-----