Update dynamiclistener

Signed-off-by: Brad Davidson <brad.davidson@rancher.com>
This commit is contained in:
Brad Davidson 2020-07-22 19:34:51 -07:00
parent 169ee63907
commit 3e8141dc65
12 changed files with 361 additions and 45 deletions

2
go.mod
View File

@ -95,7 +95,7 @@ require (
github.com/opencontainers/runc v1.0.0-rc10
github.com/opencontainers/selinux v1.3.1-0.20190929122143-5215b1806f52
github.com/pkg/errors v0.9.1
github.com/rancher/dynamiclistener v0.2.0
github.com/rancher/dynamiclistener v0.2.1-0.20200418023342-52ede5ec9234
github.com/rancher/helm-controller v0.6.3
github.com/rancher/kine v0.4.0
github.com/rancher/remotedialer v0.2.0

4
go.sum
View File

@ -620,8 +620,8 @@ github.com/rancher/cri v1.3.0-k3s.6 h1:jeom53pNYUJHlZBHpax8vpQeBoW19vSVGAQn9jPyI
github.com/rancher/cri v1.3.0-k3s.6/go.mod h1:Ht5T1dIKzm+4NExmb7wDVG6qR+j0xeXIjjhCv1d9geY=
github.com/rancher/cri-tools v1.18.0-k3s1 h1:pLYthxpSu6k3Up9tNAMA0MK2ERqB6FC1sZQPRSW1qSg=
github.com/rancher/cri-tools v1.18.0-k3s1/go.mod h1:Ij/GWNRcEDP6zVN6eQpvN/s0nhuJVtPQFy7RAdl+Wu8=
github.com/rancher/dynamiclistener v0.2.0 h1:KucYwJXVVGhZ/NndfMCeQoCafT/VN7kvqSGgmlX8Lxk=
github.com/rancher/dynamiclistener v0.2.0/go.mod h1:fs/dxyNcB3YT6W9fVz4bDGfhmSQS17QQup6BIcGF++s=
github.com/rancher/dynamiclistener v0.2.1-0.20200418023342-52ede5ec9234 h1:wZ1Zh7fI7B9hfZw9Ouhz7171CZKu6XffM3ysUhhO6i0=
github.com/rancher/dynamiclistener v0.2.1-0.20200418023342-52ede5ec9234/go.mod h1:9WusTANoiRr8cDWCTtf5txieulezHbpv4vhLADPp0zU=
github.com/rancher/flannel v0.12.0-k3s1 h1:P23dWSk/9mGT1x2rDWW9JXNrF/0kjftiHwMau/+ZLGM=
github.com/rancher/flannel v0.12.0-k3s1/go.mod h1:zQ/9Uhaw0yV4Wh6ljVwHVT1x5KuhenZA+6L8lRzOJEY=
github.com/rancher/go-powershell v0.0.0-20200701182037-6845e6fcfa79 h1:UeC0rjrIel8hHz92cdVN09Cm4Hz+BhsPP/ZvQnPOr58=

View File

@ -33,12 +33,13 @@ func (c *Cluster) newListener(ctx context.Context) (net.Listener, http.Handler,
return dynamiclistener.NewListener(tcp, storage, cert, key, dynamiclistener.Config{
CN: version.Program,
Organization: []string{version.Program},
TLSConfig: tls.Config{
TLSConfig: &tls.Config{
ClientAuth: tls.RequestClientCert,
MinVersion: c.config.TLSMinVersion,
CipherSuites: c.config.TLSCipherSuites,
},
SANs: append(c.config.SANs, "localhost", "kubernetes", "kubernetes.default", "kubernetes.default.svc."+c.config.ClusterDomain),
SANs: append(c.config.SANs, "localhost", "kubernetes", "kubernetes.default", "kubernetes.default.svc."+c.config.ClusterDomain),
ExpirationDaysCheck: 90,
})
}

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

@ -10,27 +10,37 @@ import (
"encoding/hex"
"encoding/pem"
"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"
Static = "listener.cattle.io/static"
hashKey = "listener.cattle.io/hash"
)
var (
cnRegexp = regexp.MustCompile("^([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9]$")
)
type TLS struct {
CACert *x509.Certificate
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)
@ -61,8 +71,23 @@ func collectCNs(secret *v1.Secret) (domains []string, ips []net.IP, hash string,
return
}
func (t *TLS) Merge(secret, other *v1.Secret) (*v1.Secret, bool, error) {
return t.AddCN(secret, cns(other)...)
func (t *TLS) Merge(target, additional *v1.Secret) (*v1.Secret, bool, error) {
return t.AddCN(target, cns(additional)...)
}
func (t *TLS) Refresh(secret *v1.Secret) (*v1.Secret, error) {
cns := cns(secret)
secret = secret.DeepCopy()
secret.Annotations = map[string]string{}
secret, _, err := t.AddCN(secret, cns...)
return secret, err
}
func (t *TLS) Filter(cn ...string) []string {
if t.FilterCN == nil {
return cn
}
return t.FilterCN(cn...)
}
func (t *TLS) AddCN(secret *v1.Secret, cn ...string) (*v1.Secret, bool, error) {
@ -70,10 +95,17 @@ func (t *TLS) AddCN(secret *v1.Secret, cn ...string) (*v1.Secret, bool, error) {
err error
)
if !NeedsUpdate(secret, cn...) {
cn = t.Filter(cn...)
if !NeedsUpdate(0, secret, cn...) {
return secret, false, nil
}
secret = secret.DeepCopy()
if secret == nil {
secret = &v1.Secret{}
}
secret = populateCN(secret, cn...)
privateKey, err := getPrivateKey(secret)
@ -116,18 +148,29 @@ 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" {
func NeedsUpdate(maxSANs int, secret *v1.Secret, cn ...string) bool {
if secret == nil {
return true
}
if secret.Annotations[Static] == "true" {
return false
}
for _, cn := range cn {
if secret.Annotations[cnPrefix+cn] == "" {
if maxSANs > 0 && len(cns(secret)) >= maxSANs {
return false
}
return true
}
}

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,6 +9,7 @@ import (
"net/http"
"strings"
"sync"
"time"
"github.com/rancher/dynamiclistener/factory"
"github.com/sirupsen/logrus"
@ -19,15 +21,15 @@ type TLSStorage interface {
Update(secret *v1.Secret) error
}
type SetFactory interface {
SetFactory(tls *factory.TLS)
type TLSFactory interface {
Refresh(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
}
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 +39,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 +49,169 @@ 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.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
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(5 * time.Minute)
for {
wait := 6 * time.Hour
if err := l.checkExpiration(days); err != nil {
logrus.Errorf("failed to check and refresh dynamic cert: %v", err)
wait = 5 + time.Minute
}
select {
case <-ctx.Done():
return
case <-time.After(wait):
}
}
}()
return &cancelClose{
cancel: cancel,
Listener: l,
}
}
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
}
cert, err := tls.X509KeyPair(secret.Data[v1.TLSCertKey], secret.Data[v1.TLSPrivateKeyKey])
if err != nil {
return err
}
certParsed, err := x509.ParseCertificate(cert.Certificate[0])
if err != nil {
return err
}
if time.Now().UTC().Add(time.Hour * 24 * time.Duration(days)).After(certParsed.NotAfter) {
secret, err := l.factory.Refresh(secret)
if err != nil {
return err
}
return l.storage.Update(secret)
}
return nil
}
func (l *listener) Accept() (net.Conn, error) {
l.init.Do(func() {
if len(l.sans) > 0 {
@ -97,13 +237,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 +291,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 +304,7 @@ func (l *listener) updateCert(cn ...string) error {
return err
}
if !factory.NeedsUpdate(secret, cn...) {
if !factory.NeedsUpdate(l.maxSANs, secret, cn...) {
return nil
}
@ -143,6 +324,13 @@ func (l *listener) updateCert(cn ...string) error {
}
// clear version to force cert reload
l.version = ""
if l.conns != nil {
l.connLock.Lock()
for _, conn := range l.conns {
_ = conn.close()
}
l.connLock.Unlock()
}
}
return nil
@ -179,6 +367,7 @@ func (l *listener) loadCert() (*tls.Certificate, error) {
}
l.cert = &cert
l.version = secret.ResourceVersion
return l.cert, nil
}
@ -191,6 +380,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

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

View File

@ -0,0 +1,59 @@
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 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
}
if err := createAndStore(secrets, namespace, name); err != nil {
return nil, err
}
return secrets.Get(namespace, name, metav1.GetOptions{})
}
func createAndStore(secrets v1controller.SecretClient, namespace string, name string) error {
ca, cert, err := factory.GenCA()
if err != nil {
return err
}
certPem, keyPem, err := factory.Marshal(ca, cert)
if err != nil {
return err
}
secret := &v1.Secret{
ObjectMeta: metav1.ObjectMeta{
Name: name,
Namespace: namespace,
},
Data: map[string][]byte{
v1.TLSCertKey: certPem,
v1.TLSPrivateKeyKey: keyPem,
},
Type: v1.SecretTypeTLS,
}
secrets.Create(secret)
return nil
}

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

2
vendor/modules.txt vendored
View File

@ -717,7 +717,7 @@ github.com/prometheus/procfs/xfs
# github.com/rakelkar/gonetsh v0.0.0-20190930180311-e5c5ffe4bdf0
github.com/rakelkar/gonetsh/netroute
github.com/rakelkar/gonetsh/netsh
# github.com/rancher/dynamiclistener v0.2.0
# github.com/rancher/dynamiclistener v0.2.1-0.20200418023342-52ede5ec9234
github.com/rancher/dynamiclistener
github.com/rancher/dynamiclistener/cert
github.com/rancher/dynamiclistener/factory