Compare commits

...

38 Commits

Author SHA1 Message Date
Hussein Galal
715d540640 Merge pull request #52 from galal-hussein/add_cert_rotation_to_v0.23
[v0.2.3-patch] Add ability to force cert regeneration
2021-12-03 22:14:42 +02:00
Brian Downs
11104ec0ea Add ability to force cert regeneration (#43)
* add ability to force cert regeneration
2021-12-03 21:18:17 +02:00
Hussein Galal
fc8cf5f3ea Merge pull request #33 from galal-hussein/fix_load_certs
Fixing loading certs to work with etcd only nodes
2021-03-05 22:54:49 +02:00
galal-hussein
3878ff2a1f Fixing loading certs 2021-03-05 22:39:13 +02:00
Hussein Galal
1b2460c151 Merge pull request #32 from galal-hussein/fix_resversion
Add check to update dynamic listener cert in etcd only nodes
2021-03-01 21:58:18 +02:00
galal-hussein
e34610a1ae Add check to update dynamic listener cert in etcd only nodes 2021-03-01 21:52:45 +02:00
Brad Davidson
7c224dcdfb Merge pull request #29 from brandond/force_reissue_0.2
Allow forcing cert reissuance (v0.2 backport)
2020-08-11 12:58:42 -07:00
Brad Davidson
53f6b38760 Allow forcing cert reissuance (#28)
Refreshing the cert should force renewal as opposed to returning
early if the SANs aren't changing. This is currently breaking refresh
of expired certs as per:
https://github.com/rancher/k3s/issues/1621#issuecomment-669464318

Signed-off-by: Brad Davidson <brad.davidson@rancher.com>
2020-08-10 17:12:39 -07:00
Darren Shepherd
479ab335d6 Add LoadOrGenClient to handle client cert generation 2020-08-10 17:12:39 -07:00
Darren Shepherd
2bfb7bd0cb Fix error masking issue
Also don't do an extra lookup of TLS secret after update.
2020-08-10 17:12:39 -07:00
Knic Knic
94e23c7edb fix certpath generation for windows 2020-04-25 22:59:52 -07:00
Darren Shepherd
52ede5ec92 Merge pull request #22 from ibuildthecloud/master
Always allow configured SANs regardless of the FilterCN
2020-04-17 19:33:42 -07:00
Darren Shepherd
5c222d5753 Don't parse x509 cert on each request 2020-04-17 19:31:42 -07:00
Darren Shepherd
74a61a850d Always allow configured SANs regardless of the FilterCN 2020-04-17 19:31:31 -07:00
Darren Shepherd
4436fc6b48 Merge pull request #21 from ibuildthecloud/master
Add ability to confirm adding new CNs
2020-04-02 22:10:05 -07:00
Darren Shepherd
4bac3f291f Add ability to confirm adding new CNs 2020-04-02 22:08:36 -07:00
Darren Shepherd
c992ce309c Reject bad CNs that will prevent the secret from being saved. 2020-04-02 22:07:45 -07:00
Darren Shepherd
763229ddcd Merge pull request #20 from ibuildthecloud/master
Add ability to limit the maximum number of SANs
2020-03-18 23:17:31 -07:00
Darren Shepherd
171fcf6b79 If connection closing is enabled then don't support HTTP/2 2020-03-18 23:16:38 -07:00
Darren Shepherd
05d7922a86 Add ability to limit the maximum number of SANs 2020-03-18 23:16:38 -07:00
Darren Shepherd
1e67d402dc Merge pull request #19 from ibuildthecloud/master
For web browser based requests do not consider IPs in host headers
2020-03-14 10:17:03 -07:00
Darren Shepherd
7e3fc0c594 For web browser based requests do not consider IPs in host headers 2020-03-14 10:16:11 -07:00
Darren Shepherd
111c5b43e9 Merge pull request #18 from ibuildthecloud/dropconn
Wrong lock used to protect conn map
2020-02-13 09:53:08 -07:00
Darren Shepherd
bd73d0d4bc Wrong lock used to protect conn map 2020-02-13 09:52:45 -07:00
Darren Shepherd
5276ad483a Merge pull request #17 from ibuildthecloud/dropconn
Add option to close connections on cert change
2020-02-12 14:13:44 -07:00
Darren Shepherd
8545ce98db Add option to close connections on cert change 2020-02-12 14:00:40 -07:00
Darren Shepherd
3f92468568 Merge pull request #16 from ibuildthecloud/master
Fix acme listener
2020-02-07 14:28:38 -07:00
Darren Shepherd
5ba69b1c5f Fix acme listener 2020-02-07 14:20:45 -07:00
Darren Shepherd
6281628cd4 Merge pull request #15 from ibuildthecloud/master
Add BindHost option
2020-02-05 23:12:55 -07:00
Darren Shepherd
0b114dc0c2 Add BindHost option 2020-02-05 23:11:51 -07:00
Darren Shepherd
ece289ed54 Merge pull request #14 from ibuildthecloud/master
Fix merging of the k8s secret to reduce the number of writes
2020-02-04 12:49:56 -07:00
Darren Shepherd
bc68bf5499 Fix merging of the k8s secret to reduce the number of writes 2020-02-04 12:48:38 -07:00
Darren Shepherd
795bb90214 Merge pull request #13 from ibuildthecloud/master
Add more helpers
2020-01-30 22:41:53 -07:00
Darren Shepherd
dcc205f52d mod tidy 2020-01-30 22:41:19 -07:00
Darren Shepherd
4e8035fa46 Fix go fmt/vet issues 2020-01-30 22:41:19 -07:00
Darren Shepherd
a75e84bc81 Add more helpers 2020-01-30 22:41:19 -07:00
Darren Shepherd
ab900b5268 Merge pull request #12 from ibuildthecloud/master
Add static storage and listener opts
2019-12-04 11:35:09 -07:00
Darren Shepherd
f1484a07b3 Add static storage and listener opts 2019-12-04 11:32:00 -07:00
14 changed files with 752 additions and 96 deletions

View File

@@ -33,7 +33,7 @@ import (
"math"
"math/big"
"net"
"path"
"path/filepath"
"strings"
"time"
@@ -45,6 +45,10 @@ const (
duration365d = time.Hour * 24 * 365
)
var (
ErrStaticCert = errors.New("cannot renew static certificate")
)
// Config contains the basic fields required for creating a certificate
type Config struct {
CommonName string
@@ -119,7 +123,13 @@ func NewSignedCert(cfg Config, key crypto.Signer, caCert *x509.Certificate, caKe
if err != nil {
return nil, err
}
return x509.ParseCertificate(certDERBytes)
parsedCert, err := x509.ParseCertificate(certDERBytes)
if err == nil {
logrus.Infof("certificate %s signed by %s: notBefore=%s notAfter=%s",
parsedCert.Subject, caCert.Subject, parsedCert.NotBefore, parsedCert.NotAfter)
}
return parsedCert, err
}
// MakeEllipticPrivateKeyPEM creates an ECDSA private key
@@ -161,8 +171,8 @@ func GenerateSelfSignedCertKeyWithFixtures(host string, alternateIPs []net.IP, a
maxAge := time.Hour * 24 * 365 // one year self-signed certs
baseName := fmt.Sprintf("%s_%s_%s", host, strings.Join(ipsToStrings(alternateIPs), "-"), strings.Join(alternateDNS, "-"))
certFixturePath := path.Join(fixtureDirectory, baseName+".crt")
keyFixturePath := path.Join(fixtureDirectory, baseName+".key")
certFixturePath := filepath.Join(fixtureDirectory, baseName+".crt")
keyFixturePath := filepath.Join(fixtureDirectory, baseName+".key")
if len(fixtureDirectory) > 0 {
cert, err := ioutil.ReadFile(certFixturePath)
if err == nil {
@@ -271,11 +281,11 @@ func ipsToStrings(ips []net.IP) []string {
}
// IsCertExpired checks if the certificate about to expire
func IsCertExpired(cert *x509.Certificate) bool {
func IsCertExpired(cert *x509.Certificate, days int) bool {
expirationDate := cert.NotAfter
diffDays := expirationDate.Sub(time.Now()).Hours() / 24.0
if diffDays <= 90 {
logrus.Infof("certificate will expire in %f days", diffDays)
diffDays := time.Until(expirationDate).Hours() / 24.0
if diffDays <= float64(days) {
logrus.Infof("certificate %s will expire in %f days at %s", cert.Subject, diffDays, cert.NotAfter)
return true
}
return false

View File

@@ -34,15 +34,15 @@ func CanReadCertAndKey(certPath, keyPath string) (bool, error) {
certReadable := canReadFile(certPath)
keyReadable := canReadFile(keyPath)
if certReadable == false && keyReadable == false {
if !certReadable && !keyReadable {
return false, nil
}
if certReadable == false {
if !certReadable {
return false, fmt.Errorf("error reading %s, certificate and key must be supplied as a pair", certPath)
}
if keyReadable == false {
if !keyReadable {
return false, fmt.Errorf("error reading %s, certificate and key must be supplied as a pair", keyPath)
}

View File

@@ -59,16 +59,7 @@ func loadCA() (*x509.Certificate, crypto.Signer, error) {
return LoadCerts("./certs/ca.pem", "./certs/ca.key")
}
func LoadCerts(certFile, keyFile string) (*x509.Certificate, crypto.Signer, error) {
caPem, err := ioutil.ReadFile(certFile)
if err != nil {
return nil, nil, err
}
caKey, err := ioutil.ReadFile(keyFile)
if err != nil {
return nil, nil, err
}
func LoadCA(caPem, caKey []byte) (*x509.Certificate, crypto.Signer, error) {
key, err := cert.ParsePrivateKeyPEM(caKey)
if err != nil {
return nil, nil, err
@@ -85,3 +76,16 @@ func LoadCerts(certFile, keyFile string) (*x509.Certificate, crypto.Signer, erro
return cert, signer, nil
}
func LoadCerts(certFile, keyFile string) (*x509.Certificate, crypto.Signer, error) {
caPem, err := ioutil.ReadFile(certFile)
if err != nil {
return nil, nil, err
}
caKey, err := ioutil.ReadFile(keyFile)
if err != nil {
return nil, nil, err
}
return LoadCA(caPem, caKey)
}

View File

@@ -11,6 +11,8 @@ import (
"math/big"
"net"
"time"
"github.com/sirupsen/logrus"
)
const (
@@ -40,6 +42,31 @@ func NewSelfSignedCACert(key crypto.Signer, cn string, org ...string) (*x509.Cer
return x509.ParseCertificate(certDERBytes)
}
func NewSignedClientCert(signer crypto.Signer, caCert *x509.Certificate, caKey crypto.Signer, cn string) (*x509.Certificate, error) {
serialNumber, err := rand.Int(rand.Reader, new(big.Int).SetInt64(math.MaxInt64))
if err != nil {
return nil, err
}
parent := x509.Certificate{
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth},
KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
NotAfter: time.Now().Add(time.Hour * 24 * 365).UTC(),
NotBefore: caCert.NotBefore,
SerialNumber: serialNumber,
Subject: pkix.Name{
CommonName: cn,
},
}
cert, err := x509.CreateCertificate(rand.Reader, &parent, caCert, signer.Public(), caKey)
if err != nil {
return nil, err
}
return x509.ParseCertificate(cert)
}
func NewSignedCert(signer crypto.Signer, caCert *x509.Certificate, caKey crypto.Signer, cn string, orgs []string,
domains []string, ips []net.IP) (*x509.Certificate, error) {
@@ -67,7 +94,12 @@ func NewSignedCert(signer crypto.Signer, caCert *x509.Certificate, caKey crypto.
return nil, err
}
return x509.ParseCertificate(cert)
parsedCert, err := x509.ParseCertificate(cert)
if err == nil {
logrus.Infof("certificate %s signed by %s: notBefore=%s notAfter=%s",
parsedCert.Subject, caCert.Subject, parsedCert.NotBefore, parsedCert.NotAfter)
}
return parsedCert, err
}
func ParseCertPEM(pemCerts []byte) (*x509.Certificate, error) {

View File

@@ -5,22 +5,28 @@ import (
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"crypto/sha256"
"crypto/sha1"
"crypto/x509"
"encoding/hex"
"encoding/pem"
"fmt"
"net"
"regexp"
"sort"
"strings"
"github.com/rancher/dynamiclistener/cert"
"github.com/sirupsen/logrus"
v1 "k8s.io/api/core/v1"
)
const (
cnPrefix = "listener.cattle.io/cn-"
static = "listener.cattle.io/static"
hashKey = "listener.cattle.io/hash"
cnPrefix = "listener.cattle.io/cn-"
Static = "listener.cattle.io/static"
fingerprint = "listener.cattle.io/fingerprint"
)
var (
cnRegexp = regexp.MustCompile("^([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9]$")
)
type TLS struct {
@@ -28,9 +34,13 @@ type TLS struct {
CAKey crypto.Signer
CN string
Organization []string
FilterCN func(...string) []string
}
func cns(secret *v1.Secret) (cns []string) {
if secret == nil {
return nil
}
for k, v := range secret.Annotations {
if strings.HasPrefix(k, cnPrefix) {
cns = append(cns, v)
@@ -39,16 +49,14 @@ func cns(secret *v1.Secret) (cns []string) {
return
}
func collectCNs(secret *v1.Secret) (domains []string, ips []net.IP, hash string, err error) {
func collectCNs(secret *v1.Secret) (domains []string, ips []net.IP, err error) {
var (
cns = cns(secret)
digest = sha256.New()
cns = cns(secret)
)
sort.Strings(cns)
for _, v := range cns {
digest.Write([]byte(v))
ip := net.ParseIP(v)
if ip == nil {
domains = append(domains, v)
@@ -57,22 +65,71 @@ func collectCNs(secret *v1.Secret) (domains []string, ips []net.IP, hash string,
}
}
hash = hex.EncodeToString(digest.Sum(nil))
return
}
func (t *TLS) Merge(secret, other *v1.Secret) (*v1.Secret, bool, error) {
return t.AddCN(secret, cns(other)...)
// Merge combines the SAN lists from the target and additional Secrets, and returns a potentially modified Secret,
// along with a bool indicating if the returned Secret has been updated or not. If the two SAN lists alread matched
// and no merging was necessary, but the Secrets' certificate fingerprints differed, the second secret is returned
// and the updated bool is set to true despite neither certificate having actually been modified. This is required
// to support handling certificate renewal within the kubernetes storage provider.
func (t *TLS) Merge(target, additional *v1.Secret) (*v1.Secret, bool, error) {
secret, updated, err := t.AddCN(target, cns(additional)...)
if !updated {
if target.Annotations[fingerprint] != additional.Annotations[fingerprint] {
secret = additional
updated = true
}
}
return secret, updated, err
}
func (t *TLS) AddCN(secret *v1.Secret, cn ...string) (*v1.Secret, bool, error) {
var (
err error
)
// Renew returns a copy of the given certificate that has been re-signed
// to extend the NotAfter date. It is an error to attempt to renew
// a static (user-provided) certificate.
func (t *TLS) Renew(secret *v1.Secret) (*v1.Secret, error) {
if IsStatic(secret) {
return secret, cert.ErrStaticCert
}
cns := cns(secret)
secret = secret.DeepCopy()
secret.Annotations = map[string]string{}
secret, _, err := t.generateCert(secret, cns...)
return secret, err
}
if !NeedsUpdate(secret, cn...) {
// Filter ensures that the CNs are all valid accorting to both internal logic, and any filter callbacks.
// The returned list will contain only approved CN entries.
func (t *TLS) Filter(cn ...string) []string {
if len(cn) == 0 || t.FilterCN == nil {
return cn
}
return t.FilterCN(cn...)
}
// AddCN attempts to add a list of CN strings to a given Secret, returning the potentially-modified
// Secret along with a bool indicating whether or not it has been updated. The Secret will not be changed
// if it has an attribute indicating that it is static (aka user-provided), or if no new CNs were added.
func (t *TLS) AddCN(secret *v1.Secret, cn ...string) (*v1.Secret, bool, error) {
cn = t.Filter(cn...)
if IsStatic(secret) || !NeedsUpdate(0, secret, cn...) {
return secret, false, nil
}
return t.generateCert(secret, cn...)
}
func (t *TLS) Regenerate(secret *v1.Secret) (*v1.Secret, error) {
cns := cns(secret)
secret, _, err := t.generateCert(nil, cns...)
return secret, err
}
func (t *TLS) generateCert(secret *v1.Secret, cn ...string) (*v1.Secret, bool, error) {
secret = secret.DeepCopy()
if secret == nil {
secret = &v1.Secret{}
}
secret = populateCN(secret, cn...)
@@ -81,7 +138,7 @@ func (t *TLS) AddCN(secret *v1.Secret, cn ...string) (*v1.Secret, bool, error) {
return nil, false, err
}
domains, ips, hash, err := collectCNs(secret)
domains, ips, err := collectCNs(secret)
if err != nil {
return nil, false, err
}
@@ -101,7 +158,7 @@ func (t *TLS) AddCN(secret *v1.Secret, cn ...string) (*v1.Secret, bool, error) {
}
secret.Data[v1.TLSCertKey] = certBytes
secret.Data[v1.TLSPrivateKeyKey] = keyBytes
secret.Annotations[hashKey] = hash
secret.Annotations[fingerprint] = fmt.Sprintf("SHA1=%X", sha1.Sum(newCert.Raw))
return secret, true, nil
}
@@ -116,18 +173,35 @@ func populateCN(secret *v1.Secret, cn ...string) *v1.Secret {
secret.Annotations = map[string]string{}
}
for _, cn := range cn {
secret.Annotations[cnPrefix+cn] = cn
if cnRegexp.MatchString(cn) {
secret.Annotations[cnPrefix+cn] = cn
} else {
logrus.Errorf("dropping invalid CN: %s", cn)
}
}
return secret
}
func NeedsUpdate(secret *v1.Secret, cn ...string) bool {
if secret.Annotations[static] == "true" {
return false
// IsStatic returns true if the Secret has an attribute indicating that it contains
// a static (aka user-provided) certificate, which should not be modified.
func IsStatic(secret *v1.Secret) bool {
return secret.Annotations[Static] == "true"
}
// NeedsUpdate returns true if any of the CNs are not currently present on the
// secret's Certificate, as recorded in the cnPrefix annotations. It will return
// false if all requested CNs are already present, or if maxSANs is non-zero and has
// been exceeded.
func NeedsUpdate(maxSANs int, secret *v1.Secret, cn ...string) bool {
if secret == nil {
return true
}
for _, cn := range cn {
if secret.Annotations[cnPrefix+cn] == "" {
if maxSANs > 0 && len(cns(secret)) >= maxSANs {
return false
}
return true
}
}
@@ -149,6 +223,7 @@ func getPrivateKey(secret *v1.Secret) (crypto.Signer, error) {
return NewPrivateKey()
}
// Marshal returns the given cert and key as byte slices.
func Marshal(x509Cert *x509.Certificate, privateKey crypto.Signer) ([]byte, []byte, error) {
certBlock := pem.Block{
Type: CertificateBlockType,
@@ -163,6 +238,7 @@ func Marshal(x509Cert *x509.Certificate, privateKey crypto.Signer) ([]byte, []by
return pem.EncodeToMemory(&certBlock), keyBytes, nil
}
// NewPrivateKey returnes a new ECDSA key
func NewPrivateKey() (crypto.Signer, error) {
return ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
}

1
go.mod
View File

@@ -6,6 +6,7 @@ require (
github.com/rancher/wrangler v0.1.4
github.com/rancher/wrangler-api v0.2.0
github.com/sirupsen/logrus v1.4.1
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2
k8s.io/api v0.0.0-20190409021203-6e4e0e4f393b
k8s.io/apimachinery v0.0.0-20190404173353-6a84e37a896d
)

View File

@@ -1,6 +1,7 @@
package dynamiclistener
import (
"context"
"crypto"
"crypto/tls"
"crypto/x509"
@@ -8,7 +9,9 @@ import (
"net/http"
"strings"
"sync"
"time"
"github.com/rancher/dynamiclistener/cert"
"github.com/rancher/dynamiclistener/factory"
"github.com/sirupsen/logrus"
v1 "k8s.io/api/core/v1"
@@ -19,15 +22,16 @@ type TLSStorage interface {
Update(secret *v1.Secret) error
}
type SetFactory interface {
SetFactory(tls *factory.TLS)
type TLSFactory interface {
Renew(secret *v1.Secret) (*v1.Secret, error)
AddCN(secret *v1.Secret, cn ...string) (*v1.Secret, bool, error)
Merge(target *v1.Secret, additional *v1.Secret) (*v1.Secret, bool, error)
Filter(cn ...string) []string
Regenerate(secret *v1.Secret) (*v1.Secret, error)
}
type Config struct {
CN string
Organization []string
TLSConfig tls.Config
SANs []string
type SetFactory interface {
SetFactory(tls TLSFactory)
}
func NewListener(l net.Listener, storage TLSStorage, caCert *x509.Certificate, caKey crypto.Signer, config Config) (net.Listener, http.Handler, error) {
@@ -37,6 +41,9 @@ func NewListener(l net.Listener, storage TLSStorage, caCert *x509.Certificate, c
if len(config.Organization) == 0 {
config.Organization = []string{"dynamic"}
}
if config.TLSConfig == nil {
config.TLSConfig = &tls.Config{}
}
dynamicListener := &listener{
factory: &factory.TLS{
@@ -44,34 +51,210 @@ func NewListener(l net.Listener, storage TLSStorage, caCert *x509.Certificate, c
CAKey: caKey,
CN: config.CN,
Organization: config.Organization,
FilterCN: allowDefaultSANs(config.SANs, config.FilterCN),
},
Listener: l,
storage: &nonNil{storage: storage},
sans: config.SANs,
maxSANs: config.MaxSANs,
tlsConfig: config.TLSConfig,
}
if dynamicListener.tlsConfig == nil {
dynamicListener.tlsConfig = &tls.Config{}
}
dynamicListener.tlsConfig.GetCertificate = dynamicListener.getCertificate
if config.CloseConnOnCertChange {
if len(dynamicListener.tlsConfig.Certificates) == 0 {
dynamicListener.tlsConfig.NextProtos = []string{"http/1.1"}
}
dynamicListener.conns = map[int]*closeWrapper{}
}
if setter, ok := storage.(SetFactory); ok {
setter.SetFactory(dynamicListener.factory)
}
return tls.NewListener(dynamicListener, &dynamicListener.tlsConfig), dynamicListener.cacheHandler(), nil
if config.RegenerateCerts != nil && config.RegenerateCerts() {
if err := dynamicListener.regenerateCerts(); err != nil {
return nil, nil, err
}
}
if config.ExpirationDaysCheck == 0 {
config.ExpirationDaysCheck = 30
}
tlsListener := tls.NewListener(dynamicListener.WrapExpiration(config.ExpirationDaysCheck), dynamicListener.tlsConfig)
return tlsListener, dynamicListener.cacheHandler(), nil
}
func allowDefaultSANs(sans []string, next func(...string) []string) func(...string) []string {
if next == nil {
return nil
} else if len(sans) == 0 {
return next
}
sanMap := map[string]bool{}
for _, san := range sans {
sanMap[san] = true
}
return func(s ...string) []string {
var (
good []string
unknown []string
)
for _, s := range s {
if sanMap[s] {
good = append(good, s)
} else {
unknown = append(unknown, s)
}
}
return append(good, next(unknown...)...)
}
}
type cancelClose struct {
cancel func()
net.Listener
}
func (c *cancelClose) Close() error {
c.cancel()
return c.Listener.Close()
}
type Config struct {
CN string
Organization []string
TLSConfig *tls.Config
SANs []string
MaxSANs int
ExpirationDaysCheck int
CloseConnOnCertChange bool
RegenerateCerts func() bool
FilterCN func(...string) []string
}
type listener struct {
sync.RWMutex
net.Listener
factory *factory.TLS
conns map[int]*closeWrapper
connID int
connLock sync.Mutex
factory TLSFactory
storage TLSStorage
version string
tlsConfig tls.Config
tlsConfig *tls.Config
cert *tls.Certificate
sans []string
maxSANs int
init sync.Once
}
func (l *listener) WrapExpiration(days int) net.Listener {
ctx, cancel := context.WithCancel(context.Background())
go func() {
time.Sleep(30 * time.Second)
for {
wait := 6 * time.Hour
if err := l.checkExpiration(days); err != nil {
logrus.Errorf("failed to check and renew dynamic cert: %v", err)
// Don't go into short retry loop if we're using a static (user-provided) cert.
// We will still check and print an error every six hours until the user updates the secret with
// a cert that is not about to expire. Hopefully this will prompt them to take action.
if err != cert.ErrStaticCert {
wait = 5 * time.Minute
}
}
select {
case <-ctx.Done():
return
case <-time.After(wait):
}
}
}()
return &cancelClose{
cancel: cancel,
Listener: l,
}
}
// regenerateCerts regenerates the used certificates and
// updates the secret.
func (l *listener) regenerateCerts() error {
l.Lock()
defer l.Unlock()
secret, err := l.storage.Get()
if err != nil {
return err
}
newSecret, err := l.factory.Regenerate(secret)
if err != nil {
return err
}
if err := l.storage.Update(newSecret); err != nil {
return err
}
// clear version to force cert reload
l.version = ""
return nil
}
func (l *listener) checkExpiration(days int) error {
l.Lock()
defer l.Unlock()
if days == 0 {
return nil
}
if l.cert == nil {
return nil
}
secret, err := l.storage.Get()
if err != nil {
return err
}
keyPair, err := tls.X509KeyPair(secret.Data[v1.TLSCertKey], secret.Data[v1.TLSPrivateKeyKey])
if err != nil {
return err
}
certParsed, err := x509.ParseCertificate(keyPair.Certificate[0])
if err != nil {
return err
}
if cert.IsCertExpired(certParsed, days) {
secret, err := l.factory.Renew(secret)
if err != nil {
return err
}
if err := l.storage.Update(secret); err != nil {
return err
}
// clear version to force cert reload
l.version = ""
}
return nil
}
func (l *listener) Accept() (net.Conn, error) {
l.init.Do(func() {
if len(l.sans) > 0 {
@@ -97,13 +280,49 @@ func (l *listener) Accept() (net.Conn, error) {
if !strings.Contains(host, ":") {
if err := l.updateCert(host); err != nil {
logrus.Infof("failed to create TLS cert for: %s", host)
logrus.Infof("failed to create TLS cert for: %s, %v", host, err)
}
}
if l.conns != nil {
conn = l.wrap(conn)
}
return conn, nil
}
func (l *listener) wrap(conn net.Conn) net.Conn {
l.connLock.Lock()
defer l.connLock.Unlock()
l.connID++
wrapper := &closeWrapper{
Conn: conn,
id: l.connID,
l: l,
}
l.conns[l.connID] = wrapper
return wrapper
}
type closeWrapper struct {
net.Conn
id int
l *listener
}
func (c *closeWrapper) close() error {
delete(c.l.conns, c.id)
return c.Conn.Close()
}
func (c *closeWrapper) Close() error {
c.l.connLock.Lock()
defer c.l.connLock.Unlock()
return c.close()
}
func (l *listener) getCertificate(hello *tls.ClientHelloInfo) (*tls.Certificate, error) {
if hello.ServerName != "" {
if err := l.updateCert(hello.ServerName); err != nil {
@@ -115,6 +334,11 @@ func (l *listener) getCertificate(hello *tls.ClientHelloInfo) (*tls.Certificate,
}
func (l *listener) updateCert(cn ...string) error {
cn = l.factory.Filter(cn...)
if len(cn) == 0 {
return nil
}
l.RLock()
defer l.RUnlock()
@@ -123,7 +347,7 @@ func (l *listener) updateCert(cn ...string) error {
return err
}
if !factory.NeedsUpdate(secret, cn...) {
if !factory.IsStatic(secret) && !factory.NeedsUpdate(l.maxSANs, secret, cn...) {
return nil
}
@@ -156,7 +380,7 @@ func (l *listener) loadCert() (*tls.Certificate, error) {
if err != nil {
return nil, err
}
if l.cert != nil && l.version == secret.ResourceVersion {
if l.cert != nil && l.version == secret.ResourceVersion && secret.ResourceVersion != "" {
return l.cert, nil
}
@@ -169,7 +393,7 @@ func (l *listener) loadCert() (*tls.Certificate, error) {
if err != nil {
return nil, err
}
if l.cert != nil && l.version == secret.ResourceVersion {
if l.cert != nil && l.version == secret.ResourceVersion && secret.ResourceVersion != "" {
return l.cert, nil
}
@@ -178,7 +402,17 @@ func (l *listener) loadCert() (*tls.Certificate, error) {
return nil, err
}
// cert has changed, close closeWrapper wrapped connections
if l.conns != nil {
l.connLock.Lock()
for _, conn := range l.conns {
_ = conn.close()
}
l.connLock.Unlock()
}
l.cert = &cert
l.version = secret.ResourceVersion
return l.cert, nil
}
@@ -191,6 +425,12 @@ func (l *listener) cacheHandler() http.Handler {
ip := net.ParseIP(h)
if len(ip) > 0 {
for _, v := range req.Header["User-Agent"] {
if strings.Contains(strings.ToLower(v), "mozilla") {
return
}
}
l.updateCert(h)
}
})

View File

@@ -11,8 +11,10 @@ import (
func HTTPRedirect(next http.Handler) http.Handler {
return http.HandlerFunc(
func(rw http.ResponseWriter, r *http.Request) {
if r.Header.Get("x-Forwarded-Proto") == "https" ||
if r.TLS != nil ||
r.Header.Get("x-Forwarded-Proto") == "https" ||
r.Header.Get("x-Forwarded-Proto") == "wss" ||
strings.HasPrefix(r.URL.Path, "/.well-known/") ||
strings.HasPrefix(r.URL.Path, "/ping") ||
strings.HasPrefix(r.URL.Path, "/health") {
next.ServeHTTP(rw, r)

View File

@@ -2,45 +2,75 @@ package server
import (
"context"
"crypto"
"crypto/tls"
"crypto/x509"
"fmt"
"log"
"net"
"net/http"
"github.com/rancher/dynamiclistener"
"github.com/rancher/dynamiclistener/factory"
"github.com/rancher/dynamiclistener/storage/file"
"github.com/rancher/dynamiclistener/storage/kubernetes"
"github.com/rancher/dynamiclistener/storage/memory"
v1 "github.com/rancher/wrangler-api/pkg/generated/controllers/core/v1"
"github.com/sirupsen/logrus"
"golang.org/x/crypto/acme/autocert"
)
func ListenAndServe(ctx context.Context, httpsPort, httpPort int, handler http.Handler) error {
var (
// https listener will change this if http is enabled
targetHandler = handler
)
type ListenOpts struct {
CA *x509.Certificate
CAKey crypto.Signer
Storage dynamiclistener.TLSStorage
Secrets v1.SecretController
CertNamespace string
CertName string
CANamespace string
CAName string
CertBackup string
AcmeDomains []string
BindHost string
NoRedirect bool
TLSListenerConfig dynamiclistener.Config
}
func ListenAndServe(ctx context.Context, httpsPort, httpPort int, handler http.Handler, opts *ListenOpts) error {
if opts == nil {
opts = &ListenOpts{}
}
if opts.TLSListenerConfig.TLSConfig == nil {
opts.TLSListenerConfig.TLSConfig = &tls.Config{}
}
logger := logrus.StandardLogger()
errorLog := log.New(logger.WriterLevel(logrus.DebugLevel), "", log.LstdFlags)
if httpsPort > 0 {
caCert, caKey, err := factory.LoadOrGenCA()
tlsTCPListener, err := dynamiclistener.NewTCPListener(opts.BindHost, httpsPort)
if err != nil {
return err
}
tlsTCPListener, err := dynamiclistener.NewTCPListener("0.0.0.0", httpsPort)
tlsTCPListener, handler, err = getTLSListener(ctx, tlsTCPListener, handler, *opts)
if err != nil {
return err
}
dynListener, dynHandler, err := dynamiclistener.NewListener(tlsTCPListener, memory.New(), caCert, caKey, dynamiclistener.Config{})
if err != nil {
return err
if !opts.NoRedirect {
handler = dynamiclistener.HTTPRedirect(handler)
}
targetHandler = wrapHandler(dynHandler, handler)
tlsServer := http.Server{
Handler: targetHandler,
Handler: handler,
ErrorLog: errorLog,
}
targetHandler = dynamiclistener.HTTPRedirect(targetHandler)
go func() {
logrus.Infof("Listening on 0.0.0.0:%d", httpsPort)
err := tlsServer.Serve(dynListener)
logrus.Infof("Listening on %s:%d", opts.BindHost, httpsPort)
err := tlsServer.Serve(tlsTCPListener)
if err != http.ErrServerClosed && err != nil {
logrus.Fatalf("https server failed: %v", err)
}
@@ -53,11 +83,12 @@ func ListenAndServe(ctx context.Context, httpsPort, httpPort int, handler http.H
if httpPort > 0 {
httpServer := http.Server{
Addr: fmt.Sprintf("0.0.0.0:%d", httpPort),
Handler: targetHandler,
Addr: fmt.Sprintf("%s:%d", opts.BindHost, httpPort),
Handler: handler,
ErrorLog: errorLog,
}
go func() {
logrus.Infof("Listening on 0.0.0.0:%d", httpPort)
logrus.Infof("Listening on %s:%d", opts.BindHost, httpPort)
err := httpServer.ListenAndServe()
if err != http.ErrServerClosed && err != nil {
logrus.Fatalf("http server failed: %v", err)
@@ -72,9 +103,118 @@ func ListenAndServe(ctx context.Context, httpsPort, httpPort int, handler http.H
return nil
}
func getTLSListener(ctx context.Context, tcp net.Listener, handler http.Handler, opts ListenOpts) (net.Listener, http.Handler, error) {
if len(opts.TLSListenerConfig.TLSConfig.NextProtos) == 0 {
opts.TLSListenerConfig.TLSConfig.NextProtos = []string{"h2", "http/1.1"}
}
if len(opts.TLSListenerConfig.TLSConfig.Certificates) > 0 {
return tls.NewListener(tcp, opts.TLSListenerConfig.TLSConfig), handler, nil
}
if len(opts.AcmeDomains) > 0 {
return acmeListener(tcp, handler, opts)
}
storage := opts.Storage
if storage == nil {
storage = newStorage(ctx, opts)
}
caCert, caKey, err := getCA(opts)
if err != nil {
return nil, nil, err
}
listener, dynHandler, err := dynamiclistener.NewListener(tcp, storage, caCert, caKey, opts.TLSListenerConfig)
if err != nil {
return nil, nil, err
}
return listener, wrapHandler(dynHandler, handler), nil
}
func getCA(opts ListenOpts) (*x509.Certificate, crypto.Signer, error) {
if opts.CA != nil && opts.CAKey != nil {
return opts.CA, opts.CAKey, nil
}
if opts.Secrets == nil {
return factory.LoadOrGenCA()
}
if opts.CAName == "" {
opts.CAName = "serving-ca"
}
if opts.CANamespace == "" {
opts.CANamespace = opts.CertNamespace
}
if opts.CANamespace == "" {
opts.CANamespace = "kube-system"
}
return kubernetes.LoadOrGenCA(opts.Secrets, opts.CANamespace, opts.CAName)
}
func newStorage(ctx context.Context, opts ListenOpts) dynamiclistener.TLSStorage {
var result dynamiclistener.TLSStorage
if opts.CertBackup == "" {
result = memory.New()
} else {
result = memory.NewBacked(file.New(opts.CertBackup))
}
if opts.Secrets == nil {
return result
}
if opts.CertName == "" {
opts.CertName = "serving-cert"
}
if opts.CertNamespace == "" {
opts.CertNamespace = "kube-system"
}
return kubernetes.Load(ctx, opts.Secrets, opts.CertNamespace, opts.CertName, result)
}
func wrapHandler(handler http.Handler, next http.Handler) http.Handler {
return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
handler.ServeHTTP(rw, req)
next.ServeHTTP(rw, req)
})
}
func acmeListener(tcp net.Listener, handler http.Handler, opts ListenOpts) (net.Listener, http.Handler, error) {
hosts := map[string]bool{}
for _, domain := range opts.AcmeDomains {
hosts[domain] = true
}
manager := autocert.Manager{
Cache: autocert.DirCache("certs-cache"),
Prompt: func(tosURL string) bool {
return true
},
HostPolicy: func(ctx context.Context, host string) error {
if !hosts[host] {
return fmt.Errorf("host %s is not configured", host)
}
return nil
},
}
opts.TLSListenerConfig.TLSConfig.GetCertificate = func(hello *tls.ClientHelloInfo) (*tls.Certificate, error) {
if hello.ServerName == "localhost" || hello.ServerName == "" {
newHello := *hello
newHello.ServerName = opts.AcmeDomains[0]
return manager.GetCertificate(&newHello)
}
return manager.GetCertificate(hello)
}
return tls.NewListener(tcp, opts.TLSListenerConfig.TLSConfig), manager.HTTPHandler(handler), nil
}

View File

@@ -39,4 +39,3 @@ func (s *storage) Update(secret *v1.Secret) error {
return json.NewEncoder(f).Encode(secret)
}

103
storage/kubernetes/ca.go Normal file
View File

@@ -0,0 +1,103 @@
package kubernetes
import (
"crypto"
"crypto/x509"
"github.com/rancher/dynamiclistener/factory"
v1controller "github.com/rancher/wrangler-api/pkg/generated/controllers/core/v1"
v1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
func LoadOrGenCA(secrets v1controller.SecretClient, namespace, name string) (*x509.Certificate, crypto.Signer, error) {
secret, err := getSecret(secrets, namespace, name)
if err != nil {
return nil, nil, err
}
return factory.LoadCA(secret.Data[v1.TLSCertKey], secret.Data[v1.TLSPrivateKeyKey])
}
func LoadOrGenClient(secrets v1controller.SecretClient, namespace, name, cn string, ca *x509.Certificate, key crypto.Signer) (*x509.Certificate, crypto.Signer, error) {
secret, err := getClientSecret(secrets, namespace, name, cn, ca, key)
if err != nil {
return nil, nil, err
}
return factory.LoadCA(secret.Data[v1.TLSCertKey], secret.Data[v1.TLSPrivateKeyKey])
}
func getClientSecret(secrets v1controller.SecretClient, namespace, name, cn string, caCert *x509.Certificate, caKey crypto.Signer) (*v1.Secret, error) {
s, err := secrets.Get(namespace, name, metav1.GetOptions{})
if !errors.IsNotFound(err) {
return s, err
}
return createAndStoreClientCert(secrets, namespace, name, cn, caCert, caKey)
}
func getSecret(secrets v1controller.SecretClient, namespace, name string) (*v1.Secret, error) {
s, err := secrets.Get(namespace, name, metav1.GetOptions{})
if !errors.IsNotFound(err) {
return s, err
}
return createAndStore(secrets, namespace, name)
}
func createAndStoreClientCert(secrets v1controller.SecretClient, namespace string, name, cn string, caCert *x509.Certificate, caKey crypto.Signer) (*v1.Secret, error) {
key, err := factory.NewPrivateKey()
if err != nil {
return nil, err
}
cert, err := factory.NewSignedClientCert(key, caCert, caKey, cn)
if err != nil {
return nil, err
}
certPem, keyPem, err := factory.Marshal(cert, key)
if err != nil {
return nil, err
}
secret := &v1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: namespace,
},
Data: map[string][]byte{
v1.TLSCertKey: certPem,
v1.TLSPrivateKeyKey: keyPem,
},
Type: v1.SecretTypeTLS,
}
return secrets.Create(secret)
}
func createAndStore(secrets v1controller.SecretClient, namespace string, name string) (*v1.Secret, error) {
ca, cert, err := factory.GenCA()
if err != nil {
return nil, err
}
certPem, keyPem, err := factory.Marshal(ca, cert)
if err != nil {
return nil, err
}
secret := &v1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: namespace,
},
Data: map[string][]byte{
v1.TLSCertKey: certPem,
v1.TLSPrivateKeyKey: keyPem,
},
Type: v1.SecretTypeTLS,
}
return secrets.Create(secret)
}

View File

@@ -6,19 +6,28 @@ import (
"time"
"github.com/rancher/dynamiclistener"
"github.com/rancher/dynamiclistener/factory"
"github.com/rancher/wrangler-api/pkg/generated/controllers/core"
v1controller "github.com/rancher/wrangler-api/pkg/generated/controllers/core/v1"
"github.com/rancher/wrangler/pkg/start"
"github.com/sirupsen/logrus"
v1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/equality"
"k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
type CoreGetter func() *core.Factory
func Load(ctx context.Context, secrets v1controller.SecretController, namespace, name string, backing dynamiclistener.TLSStorage) dynamiclistener.TLSStorage {
storage := &storage{
name: name,
namespace: namespace,
storage: backing,
ctx: ctx,
}
storage.init(secrets)
return storage
}
func New(ctx context.Context, core CoreGetter, namespace, name string, backing dynamiclistener.TLSStorage) dynamiclistener.TLSStorage {
storage := &storage{
name: name,
@@ -55,10 +64,10 @@ type storage struct {
storage dynamiclistener.TLSStorage
secrets v1controller.SecretClient
ctx context.Context
tls *factory.TLS
tls dynamiclistener.TLSFactory
}
func (s *storage) SetFactory(tls *factory.TLS) {
func (s *storage) SetFactory(tls dynamiclistener.TLSFactory) {
s.tls = tls
}
@@ -122,7 +131,7 @@ func (s *storage) saveInK8s(secret *v1.Secret) (*v1.Secret, error) {
}
if existing, err := s.storage.Get(); err == nil && s.tls != nil {
if newSecret, updated, err := s.tls.Merge(secret, existing); err == nil && updated {
if newSecret, updated, err := s.tls.Merge(existing, secret); err == nil && updated {
secret = newSecret
}
}
@@ -132,9 +141,12 @@ func (s *storage) saveInK8s(secret *v1.Secret) (*v1.Secret, error) {
return nil, err
}
if equality.Semantic.DeepEqual(targetSecret.Annotations, secret.Annotations) &&
equality.Semantic.DeepEqual(targetSecret.Data, secret.Data) {
return secret, nil
if newSecret, updated, err := s.tls.Merge(targetSecret, secret); err != nil {
return nil, err
} else if !updated {
return newSecret, nil
} else {
secret = newSecret
}
targetSecret.Annotations = secret.Annotations
@@ -144,10 +156,9 @@ func (s *storage) saveInK8s(secret *v1.Secret) (*v1.Secret, error) {
if targetSecret.UID == "" {
logrus.Infof("Creating new TLS secret for %v (count: %d): %v", targetSecret.Name, len(targetSecret.Annotations)-1, targetSecret.Annotations)
return s.secrets.Create(targetSecret)
} else {
logrus.Infof("Updating TLS secret for %v (count: %d): %v", targetSecret.Name, len(targetSecret.Annotations)-1, targetSecret.Annotations)
return s.secrets.Update(targetSecret)
}
logrus.Infof("Updating TLS secret for %v (count: %d): %v", targetSecret.Name, len(targetSecret.Annotations)-1, targetSecret.Annotations)
return s.secrets.Update(targetSecret)
}
func (s *storage) Update(secret *v1.Secret) (err error) {

View File

@@ -32,13 +32,15 @@ func (m *memory) Get() (*v1.Secret, error) {
}
func (m *memory) Update(secret *v1.Secret) error {
if m.storage != nil {
if err := m.storage.Update(secret); err != nil {
return err
if m.secret == nil || m.secret.ResourceVersion == "" || m.secret.ResourceVersion != secret.ResourceVersion {
if m.storage != nil {
if err := m.storage.Update(secret); err != nil {
return err
}
}
}
logrus.Infof("Active TLS secret %s (ver=%s) (count %d): %v", secret.Name, secret.ResourceVersion, len(secret.Annotations)-1, secret.Annotations)
m.secret = secret
logrus.Infof("Active TLS secret %s (ver=%s) (count %d): %v", secret.Name, secret.ResourceVersion, len(secret.Annotations)-1, secret.Annotations)
m.secret = secret
}
return nil
}

36
storage/static/static.go Normal file
View File

@@ -0,0 +1,36 @@
package static
import (
"github.com/rancher/dynamiclistener/factory"
v1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
type Storage struct {
Secret *v1.Secret
}
func New(certPem, keyPem []byte) *Storage {
return &Storage{
Secret: &v1.Secret{
ObjectMeta: metav1.ObjectMeta{
Annotations: map[string]string{
factory.Static: "true",
},
},
Data: map[string][]byte{
v1.TLSCertKey: certPem,
v1.TLSPrivateKeyKey: keyPem,
},
Type: v1.SecretTypeTLS,
},
}
}
func (s *Storage) Get() (*v1.Secret, error) {
return s.Secret, nil
}
func (s *Storage) Update(_ *v1.Secret) error {
return nil
}