Skip to content
This repository has been archived by the owner on Aug 26, 2021. It is now read-only.

Supporting ECC Certificates #190

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
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
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,8 @@ Please note:
| `LEGO_LOG_LEVEL` | n | `info` | Set log level (`debug`, `info`, `warn` or `error`) |
| `LEGO_KUBE_ANNOTATION` | n | `kubernetes.io/tls-acme` | Set the ingress annotation used by this instance of kube-lego to get certificate for from Let's Encrypt. Allows you to run kube-lego against staging and production LE |
| `LEGO_WATCH_NAMESPACE` | n | `` | Namespace that kube-lego should watch for ingresses and services |
| `LEGO_KEY_TYPE` | n | `rsa` | Set your preferred private key type (`ecc`, `rsa`) to generate certificates with. |
| `LEGO_KEY_SIZE` | n | `ecc`: `384` <br><br>`rsa`: `2048` | Set your preferred key size based on the key type selected. <br><br>For `ecc` keys, you may select curves (`224`, `256`, `384`, `521`) , `384` being the default. <br><br>For `rsa` keys, you may provide a reasonable size, `2048` being the default. |

## Full deployment examples

Expand Down Expand Up @@ -147,7 +149,7 @@ Capture 20 seconds of the execution trace:

`$ wget http://localhost:8080/debug/pprof/trace?seconds=20 -O kube-lego.trace`

You can inspect the trace sample running
You can inspect the trace sample by running:

`$ go tool trace kube-lego.trace`

Expand Down
68 changes: 67 additions & 1 deletion pkg/acme/acme_e2e_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,12 @@ import (

"github.com/Sirupsen/logrus"
"github.com/golang/mock/gomock"
kubelego "github.com/jetstack/kube-lego/pkg/kubelego_const"
"github.com/jetstack/kube-lego/pkg/mocks"
"k8s.io/apimachinery/pkg/util/intstr"
)

func TestAcme_E2E(t *testing.T) {
func TestAcme_E2E_ECC(t *testing.T) {
logrus.SetLevel(logrus.DebugLevel)
log := logrus.WithField("context", "test-mock")

Expand All @@ -30,6 +31,8 @@ func TestAcme_E2E(t *testing.T) {
mockKL.EXPECT().LegoURL().MinTimes(1).Return("https://acme-staging.api.letsencrypt.org/directory")
mockKL.EXPECT().LegoEmail().MinTimes(1).Return("kube-lego-e2e@example.com")
mockKL.EXPECT().SaveAcmeUser(gomock.Any()).MinTimes(1).Return(nil)
mockKL.EXPECT().LegoKeyType().MinTimes(1).Return(kubelego.KeyTypeEcc)
mockKL.EXPECT().LegoKeySize().MinTimes(1).Return(kubelego.EccKeySize)

// set up ngrok
command := []string{"ngrok", "http", "--bind-tls", "false", "8181"}
Expand Down Expand Up @@ -75,5 +78,68 @@ func TestAcme_E2E(t *testing.T) {

log.Infof("trying to obtain a certificate for the domain")
a.ObtainCertificate([]string{domain})
}

func TestAcme_E2E_RSA(t *testing.T) {
logrus.SetLevel(logrus.DebugLevel)
log := logrus.WithField("context", "test-mock")

ctrl := gomock.NewController(t)
defer ctrl.Finish()
// mock kube lego
mockKL := mocks.NewMockKubeLego(ctrl)
mockKL.EXPECT().Log().AnyTimes().Return(log)
mockKL.EXPECT().Version().AnyTimes().Return("mocked-version")
mockKL.EXPECT().LegoHTTPPort().AnyTimes().Return(intstr.FromInt(8182))
mockKL.EXPECT().AcmeUser().MinTimes(1).Return(nil, errors.New("I am only mocked"))
mockKL.EXPECT().LegoURL().MinTimes(1).Return("https://acme-staging.api.letsencrypt.org/directory")
mockKL.EXPECT().LegoEmail().MinTimes(1).Return("kube-lego-e2e@example.com")
mockKL.EXPECT().SaveAcmeUser(gomock.Any()).MinTimes(1).Return(nil)
mockKL.EXPECT().LegoKeyType().MinTimes(1).Return(kubelego.KeyTypeRsa)
mockKL.EXPECT().LegoKeySize().MinTimes(1).Return(kubelego.RsaKeySize)

// set up ngrok
command := []string{"ngrok", "http", "--bind-tls", "false", "8182"}
cmdNgrok := exec.Command(command[0], command[1:]...)
err := cmdNgrok.Start()
if err != nil {
t.Skip("Skipping e2e test as ngrok executable is not available: ", err)
}
defer cmdNgrok.Process.Kill()

// get domain main for forwarding the acme validation
regexDomain := regexp.MustCompile("http://([a-z0-9]+.\\.ngrok\\.io)")
var domain string
for {
time.Sleep(100 * time.Millisecond)
resp, err := http.Get("http://localhost:4040/status")
if err != nil {
log.Warn(err)
continue
}
defer resp.Body.Close()

body, err := ioutil.ReadAll(resp.Body)
if err != nil {
log.Warn(err)
continue
}

matched := regexDomain.FindStringSubmatch(string(body))
if matched == nil {
continue
}

domain = matched[1]
log.Infof("kube-lego domain is %s", domain)

break
}

stopCh := make(chan struct{})
a := New(mockKL)
go a.RunServer(stopCh)

log.Infof("trying to obtain a certificate for the domain")
a.ObtainCertificate([]string{domain})
}
52 changes: 48 additions & 4 deletions pkg/acme/user.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
package acme

import (
"crypto"
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/rsa"
"crypto/x509"
Expand Down Expand Up @@ -124,15 +127,56 @@ func (a *Acme) validateUser(client *acme.Client, accountURI string) (account *ac
return account, nil
}

func (a *Acme) generatePrivateKey() ([]byte, *rsa.PrivateKey, error) {
func (a *Acme) generatePrivateKey() ([]byte, crypto.Signer, error) {

if a.kubelego.LegoKeyType() == kubelego.KeyTypeRsa {

privateKey, err := rsa.GenerateKey(rand.Reader, a.kubelego.LegoKeySize())
if err != nil {
return []byte{}, nil, err
}

block := &pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(privateKey)}

return pem.EncodeToMemory(block), privateKey, nil
}

var ecpk *ecdsa.PrivateKey
var err error

switch a.kubelego.LegoKeySize() {
case 224:
ecpk, err = ecdsa.GenerateKey(elliptic.P224(), rand.Reader)
break

case 256:
ecpk, err = ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
break

default:
ecpk, err = ecdsa.GenerateKey(elliptic.P384(), rand.Reader)
break

case 384:
ecpk, err = ecdsa.GenerateKey(elliptic.P384(), rand.Reader)
break

case 521:
ecpk, err = ecdsa.GenerateKey(elliptic.P521(), rand.Reader)
break

}

privateKey, err := rsa.GenerateKey(rand.Reader, kubelego.RsaKeySize)
if err != nil {
return []byte{}, nil, err
}

block := &pem.Block{Type: "RSA PRIVATE KEY", Bytes: x509.MarshalPKCS1PrivateKey(privateKey)}
b, err := x509.MarshalECPrivateKey(ecpk)
if err != nil {
return []byte{}, nil, err
}

return pem.EncodeToMemory(block), privateKey, nil
block := &pem.Block{Type: "EC PRIVATE KEY", Bytes: b}

return pem.EncodeToMemory(block), ecpk, nil
}
62 changes: 60 additions & 2 deletions pkg/kubelego/kubelego.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import (

"github.com/jetstack/kube-lego/pkg/acme"
"github.com/jetstack/kube-lego/pkg/ingress"
"github.com/jetstack/kube-lego/pkg/kubelego_const"
kubelego "github.com/jetstack/kube-lego/pkg/kubelego_const"
"github.com/jetstack/kube-lego/pkg/provider/gce"
"github.com/jetstack/kube-lego/pkg/provider/nginx"
"github.com/jetstack/kube-lego/pkg/secret"
Expand Down Expand Up @@ -200,6 +200,14 @@ func (kl *KubeLego) LegoKubeApiURL() string {
return kl.legoKubeApiURL
}

func (kl *KubeLego) LegoKeyType() string {
return kl.legoKeyType
}

func (kl *KubeLego) LegoKeySize() int {
return kl.legoKeySize
}

func (kl *KubeLego) acmeSecret() *secret.Secret {
return secret.New(kl, kl.LegoNamespace(), kl.legoSecretName)
}
Expand Down Expand Up @@ -259,7 +267,7 @@ func (kl *KubeLego) paramsLego() error {
kl.legoServiceNameGce = "kube-lego-gce"
}

kl.legoSupportedIngressClass = strings.Split(os.Getenv("LEGO_SUPPORTED_INGRESS_CLASS"),",")
kl.legoSupportedIngressClass = strings.Split(os.Getenv("LEGO_SUPPORTED_INGRESS_CLASS"), ",")
if len(kl.legoSupportedIngressClass) == 1 {
kl.legoSupportedIngressClass = kubelego.SupportedIngressClasses
}
Expand Down Expand Up @@ -340,5 +348,55 @@ func (kl *KubeLego) paramsLego() error {
} else {
kl.legoWatchNamespace = watchNamespace
}

keyType := os.Getenv("LEGO_KEY_TYPE")
switch keyType {
case kubelego.KeyTypeRsa:
kl.legoKeyType = kubelego.KeyTypeRsa
break
case kubelego.KeyTypeEcc:
kl.legoKeyType = kubelego.KeyTypeEcc
break
default:
kl.legoKeyType = kubelego.KeyTypeRsa
break
}

keySize := os.Getenv("LEGO_KEY_SIZE")
if keyType == kubelego.KeyTypeEcc {
switch keySize {
case "":
// Setting default as it is not provided
kl.legoKeySize = kubelego.EccKeySize
break
case "224", "256", "384", "521":
ks, err := strconv.Atoi(keySize)
if err != nil {
return errors.New("Please provide a valid key size for ECC certificates")
}

kl.legoKeySize = ks
break
default:
return errors.New("Please provide a valid key size for ECC certificates")
}
} else if keyType == kubelego.KeyTypeRsa {
if keySize == "" {
// Setting default as it is not provided
kl.legoKeySize = kubelego.RsaKeySize
} else {
ks, err := strconv.Atoi(keySize)
if err != nil {
return errors.New("Please provide a valid key size for RSA certificates")
}

if ks < 0 {
return errors.New("Please provide a valid key size for RSA certificates")
}

kl.legoKeySize = ks
}
}

return nil
}
4 changes: 4 additions & 0 deletions pkg/kubelego/type.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,10 @@ type KubeLego struct {
version string
acmeClient kubelego.Acme

// Key Generation
legoKeyType string
legoKeySize int

// stop channel for services
stopCh chan struct{}

Expand Down
5 changes: 5 additions & 0 deletions pkg/kubelego_const/consts.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,12 @@ import (
k8sApi "k8s.io/client-go/pkg/api/v1"
)

const KeyTypeRsa = "rsa"
const RsaKeySize = 2048

const KeyTypeEcc = "ecc"
const EccKeySize = 384

const AcmeRegistration = "acme-registration.json"
const AcmeRegistrationUrl = "acme-registration-url"
const AcmePrivateKey = k8sApi.TLSPrivateKeyKey
Expand Down
2 changes: 2 additions & 0 deletions pkg/kubelego_const/interfaces.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ type KubeLego interface {
LegoCheckInterval() time.Duration
LegoMinimumValidity() time.Duration
LegoPodIP() net.IP
LegoKeyType() string
LegoKeySize() int
IngressProvider(string) (IngressProvider, error)
Version() string
AcmeUser() (map[string][]byte, error)
Expand Down
20 changes: 20 additions & 0 deletions pkg/mocks/mocks.go
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,26 @@ func (_mr *_MockKubeLegoRecorder) LegoPodIP() *gomock.Call {
return _mr.mock.ctrl.RecordCall(_mr.mock, "LegoPodIP")
}

func (_m *MockKubeLego) LegoKeyType() string {
ret := _m.ctrl.Call(_m, "LegoKeyType")
ret0, _ := ret[0].(string)
return ret0
}

func (_mr *_MockKubeLegoRecorder) LegoKeyType() *gomock.Call {
return _mr.mock.ctrl.RecordCall(_mr.mock, "LegoKeyType")
}

func (_m *MockKubeLego) LegoKeySize() int {
ret := _m.ctrl.Call(_m, "LegoKeySize")
ret0, _ := ret[0].(int)
return ret0
}

func (_mr *_MockKubeLegoRecorder) LegoKeySize() *gomock.Call {
return _mr.mock.ctrl.RecordCall(_mr.mock, "LegoKeySize")
}

func (_m *MockKubeLego) IngressProvider(_param0 string) (IngressProvider, error) {
ret := _m.ctrl.Call(_m, "IngressProvider", _param0)
ret0, _ := ret[0].(IngressProvider)
Expand Down