mirror of
https://github.com/rancher/dynamiclistener.git
synced 2025-09-02 13:44:26 +00:00
Compare commits
57 Commits
v0.1.0
...
v0.2.3-k3s
Author | SHA1 | Date | |
---|---|---|---|
|
715d540640 | ||
|
11104ec0ea | ||
|
fc8cf5f3ea | ||
|
3878ff2a1f | ||
|
1b2460c151 | ||
|
e34610a1ae | ||
|
7c224dcdfb | ||
|
53f6b38760 | ||
|
479ab335d6 | ||
|
2bfb7bd0cb | ||
|
94e23c7edb | ||
|
52ede5ec92 | ||
|
5c222d5753 | ||
|
74a61a850d | ||
|
4436fc6b48 | ||
|
4bac3f291f | ||
|
c992ce309c | ||
|
763229ddcd | ||
|
171fcf6b79 | ||
|
05d7922a86 | ||
|
1e67d402dc | ||
|
7e3fc0c594 | ||
|
111c5b43e9 | ||
|
bd73d0d4bc | ||
|
5276ad483a | ||
|
8545ce98db | ||
|
3f92468568 | ||
|
5ba69b1c5f | ||
|
6281628cd4 | ||
|
0b114dc0c2 | ||
|
ece289ed54 | ||
|
bc68bf5499 | ||
|
795bb90214 | ||
|
dcc205f52d | ||
|
4e8035fa46 | ||
|
a75e84bc81 | ||
|
ab900b5268 | ||
|
f1484a07b3 | ||
|
b6b942bff0 | ||
|
3c2990b7c5 | ||
|
ccf76b35ea | ||
|
988d8dd3f4 | ||
|
736b5d5d8b | ||
|
655c08132d | ||
|
02b97e01f1 | ||
|
aaa5bc0d2a | ||
|
6c7ccae2fc | ||
|
36c5023d47 | ||
|
245f86cc34 | ||
|
f570529af6 | ||
|
bcf3a564c5 | ||
|
9adf776973 | ||
|
6224794ef3 | ||
|
5878218dc0 | ||
|
af04867843 | ||
|
8a2488bc86 | ||
|
583d996366 |
31
cert/cert.go
31
cert/cert.go
@@ -33,9 +33,11 @@ import (
|
||||
"math"
|
||||
"math/big"
|
||||
"net"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -43,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
|
||||
@@ -117,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
|
||||
@@ -159,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 {
|
||||
@@ -267,3 +279,14 @@ func ipsToStrings(ips []net.IP) []string {
|
||||
}
|
||||
return ss
|
||||
}
|
||||
|
||||
// IsCertExpired checks if the certificate about to expire
|
||||
func IsCertExpired(cert *x509.Certificate, days int) bool {
|
||||
expirationDate := cert.NotAfter
|
||||
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
|
||||
}
|
||||
|
@@ -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)
|
||||
}
|
||||
|
||||
|
91
factory/ca.go
Normal file
91
factory/ca.go
Normal file
@@ -0,0 +1,91 @@
|
||||
package factory
|
||||
|
||||
import (
|
||||
"crypto"
|
||||
"crypto/x509"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
|
||||
"github.com/rancher/dynamiclistener/cert"
|
||||
)
|
||||
|
||||
func GenCA() (*x509.Certificate, crypto.Signer, error) {
|
||||
caKey, err := NewPrivateKey()
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
caCert, err := NewSelfSignedCACert(caKey, "dynamiclistener-ca", "dynamiclistener-org")
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
return caCert, caKey, nil
|
||||
}
|
||||
|
||||
func LoadOrGenCA() (*x509.Certificate, crypto.Signer, error) {
|
||||
cert, key, err := loadCA()
|
||||
if err == nil {
|
||||
return cert, key, nil
|
||||
}
|
||||
|
||||
cert, key, err = GenCA()
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
certBytes, keyBytes, err := Marshal(cert, key)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
if err := os.MkdirAll("./certs", 0700); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
if err := ioutil.WriteFile("./certs/ca.pem", certBytes, 0600); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
if err := ioutil.WriteFile("./certs/ca.key", keyBytes, 0600); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
return cert, key, nil
|
||||
}
|
||||
|
||||
func loadCA() (*x509.Certificate, crypto.Signer, error) {
|
||||
return LoadCerts("./certs/ca.pem", "./certs/ca.key")
|
||||
}
|
||||
|
||||
func LoadCA(caPem, caKey []byte) (*x509.Certificate, crypto.Signer, error) {
|
||||
key, err := cert.ParsePrivateKeyPEM(caKey)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
signer, ok := key.(crypto.Signer)
|
||||
if !ok {
|
||||
return nil, nil, fmt.Errorf("key is not a crypto.Signer")
|
||||
}
|
||||
|
||||
cert, err := ParseCertPEM(caPem)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
119
factory/cert_utils.go
Normal file
119
factory/cert_utils.go
Normal file
@@ -0,0 +1,119 @@
|
||||
package factory
|
||||
|
||||
import (
|
||||
"crypto"
|
||||
"crypto/rand"
|
||||
"crypto/x509"
|
||||
"crypto/x509/pkix"
|
||||
"encoding/pem"
|
||||
"fmt"
|
||||
"math"
|
||||
"math/big"
|
||||
"net"
|
||||
"time"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
const (
|
||||
CertificateBlockType = "CERTIFICATE"
|
||||
)
|
||||
|
||||
func NewSelfSignedCACert(key crypto.Signer, cn string, org ...string) (*x509.Certificate, error) {
|
||||
now := time.Now()
|
||||
tmpl := x509.Certificate{
|
||||
BasicConstraintsValid: true,
|
||||
IsCA: true,
|
||||
KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign,
|
||||
NotAfter: now.Add(time.Hour * 24 * 365 * 10).UTC(),
|
||||
NotBefore: now.UTC(),
|
||||
SerialNumber: new(big.Int).SetInt64(0),
|
||||
Subject: pkix.Name{
|
||||
CommonName: cn,
|
||||
Organization: org,
|
||||
},
|
||||
}
|
||||
|
||||
certDERBytes, err := x509.CreateCertificate(rand.Reader, &tmpl, &tmpl, key.Public(), key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
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) {
|
||||
|
||||
serialNumber, err := rand.Int(rand.Reader, new(big.Int).SetInt64(math.MaxInt64))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
parent := x509.Certificate{
|
||||
DNSNames: domains,
|
||||
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
|
||||
IPAddresses: ips,
|
||||
KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
|
||||
NotAfter: time.Now().Add(time.Hour * 24 * 365).UTC(),
|
||||
NotBefore: caCert.NotBefore,
|
||||
SerialNumber: serialNumber,
|
||||
Subject: pkix.Name{
|
||||
CommonName: cn,
|
||||
Organization: orgs,
|
||||
},
|
||||
}
|
||||
|
||||
cert, err := x509.CreateCertificate(rand.Reader, &parent, caCert, signer.Public(), caKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
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) {
|
||||
var pemBlock *pem.Block
|
||||
for {
|
||||
pemBlock, pemCerts = pem.Decode(pemCerts)
|
||||
if pemBlock == nil {
|
||||
break
|
||||
}
|
||||
|
||||
if pemBlock.Type == CertificateBlockType {
|
||||
return x509.ParseCertificate(pemBlock.Bytes)
|
||||
}
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("pem does not include a valid x509 cert")
|
||||
}
|
244
factory/gen.go
Normal file
244
factory/gen.go
Normal file
@@ -0,0 +1,244 @@
|
||||
package factory
|
||||
|
||||
import (
|
||||
"crypto"
|
||||
"crypto/ecdsa"
|
||||
"crypto/elliptic"
|
||||
"crypto/rand"
|
||||
"crypto/sha1"
|
||||
"crypto/x509"
|
||||
"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"
|
||||
fingerprint = "listener.cattle.io/fingerprint"
|
||||
)
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func collectCNs(secret *v1.Secret) (domains []string, ips []net.IP, err error) {
|
||||
var (
|
||||
cns = cns(secret)
|
||||
)
|
||||
|
||||
sort.Strings(cns)
|
||||
|
||||
for _, v := range cns {
|
||||
ip := net.ParseIP(v)
|
||||
if ip == nil {
|
||||
domains = append(domains, v)
|
||||
} else {
|
||||
ips = append(ips, ip)
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
// 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...)
|
||||
|
||||
privateKey, err := getPrivateKey(secret)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
|
||||
domains, ips, err := collectCNs(secret)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
|
||||
newCert, err := t.newCert(domains, ips, privateKey)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
|
||||
certBytes, keyBytes, err := Marshal(newCert, privateKey)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
|
||||
if secret.Data == nil {
|
||||
secret.Data = map[string][]byte{}
|
||||
}
|
||||
secret.Data[v1.TLSCertKey] = certBytes
|
||||
secret.Data[v1.TLSPrivateKeyKey] = keyBytes
|
||||
secret.Annotations[fingerprint] = fmt.Sprintf("SHA1=%X", sha1.Sum(newCert.Raw))
|
||||
|
||||
return secret, true, nil
|
||||
}
|
||||
|
||||
func (t *TLS) newCert(domains []string, ips []net.IP, privateKey crypto.Signer) (*x509.Certificate, error) {
|
||||
return NewSignedCert(privateKey, t.CACert, t.CAKey, t.CN, t.Organization, domains, ips)
|
||||
}
|
||||
|
||||
func populateCN(secret *v1.Secret, cn ...string) *v1.Secret {
|
||||
secret = secret.DeepCopy()
|
||||
if secret.Annotations == nil {
|
||||
secret.Annotations = map[string]string{}
|
||||
}
|
||||
for _, cn := range cn {
|
||||
if cnRegexp.MatchString(cn) {
|
||||
secret.Annotations[cnPrefix+cn] = cn
|
||||
} else {
|
||||
logrus.Errorf("dropping invalid CN: %s", cn)
|
||||
}
|
||||
}
|
||||
return secret
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
func getPrivateKey(secret *v1.Secret) (crypto.Signer, error) {
|
||||
keyBytes := secret.Data[v1.TLSPrivateKeyKey]
|
||||
if len(keyBytes) == 0 {
|
||||
return NewPrivateKey()
|
||||
}
|
||||
|
||||
privateKey, err := cert.ParsePrivateKeyPEM(keyBytes)
|
||||
if signer, ok := privateKey.(crypto.Signer); ok && err == nil {
|
||||
return signer, nil
|
||||
}
|
||||
|
||||
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,
|
||||
Bytes: x509Cert.Raw,
|
||||
}
|
||||
|
||||
keyBytes, err := cert.MarshalPrivateKeyToPEM(privateKey)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
return pem.EncodeToMemory(&certBlock), keyBytes, nil
|
||||
}
|
||||
|
||||
// NewPrivateKey returnes a new ECDSA key
|
||||
func NewPrivateKey() (crypto.Signer, error) {
|
||||
return ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
||||
}
|
12
go.mod
12
go.mod
@@ -3,12 +3,10 @@ module github.com/rancher/dynamiclistener
|
||||
go 1.12
|
||||
|
||||
require (
|
||||
github.com/hashicorp/golang-lru v0.5.1
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.2 // indirect
|
||||
github.com/rancher/wrangler v0.1.4
|
||||
github.com/rancher/wrangler-api v0.2.0
|
||||
github.com/sirupsen/logrus v1.4.1
|
||||
github.com/stretchr/testify v1.3.0 // indirect
|
||||
golang.org/x/crypto v0.0.0-20190506204251-e1dfcc566284
|
||||
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c // indirect
|
||||
golang.org/x/sys v0.0.0-20190509141414-a5b02f93d862 // indirect
|
||||
golang.org/x/text v0.3.2 // indirect
|
||||
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
|
||||
)
|
||||
|
124
go.sum
124
go.sum
@@ -1,38 +1,132 @@
|
||||
cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
github.com/NYTimes/gziphandler v0.0.0-20170623195520-56545f4a5d46/go.mod h1:3wb06e3pkSAbeQ52E9H9iFoQsEEwGN64994WTCIhntQ=
|
||||
github.com/PuerkitoBio/purell v1.0.0/go.mod h1:c11w/QuzBsJSee3cPx9rAFu61PvFxuPbtSwDGJws/X0=
|
||||
github.com/PuerkitoBio/urlesc v0.0.0-20160726150825-5bd2802263f2/go.mod h1:uGdkoq3SwY9Y+13GIhn11/XLaGBb4BfwItxLd5jeuXE=
|
||||
github.com/davecgh/go-spew v0.0.0-20151105211317-5215b55f46b2/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/emicklei/go-restful v0.0.0-20170410110728-ff4f55a20633/go.mod h1:otzb+WCGbkyDHkqmQmT5YD2WR4BBwUdeQoFo8l/7tVs=
|
||||
github.com/evanphx/json-patch v4.2.0+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk=
|
||||
github.com/ghodss/yaml v0.0.0-20150909031657-73d445a93680/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
|
||||
github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04=
|
||||
github.com/go-openapi/jsonpointer v0.0.0-20160704185906-46af16f9f7b1/go.mod h1:+35s3my2LFTysnkMfxsJBAMHj/DoqoB9knIWoYG/Vk0=
|
||||
github.com/go-openapi/jsonreference v0.0.0-20160704190145-13c6e3589ad9/go.mod h1:W3Z9FmVs9qj+KR4zFKmDPGiLdk1D9Rlm7cyMvf57TTg=
|
||||
github.com/go-openapi/spec v0.0.0-20160808142527-6aced65f8501/go.mod h1:J8+jY1nAiCcj+friV/PDoE1/3eeccG9LYBs0tYvLOWc=
|
||||
github.com/go-openapi/swag v0.0.0-20160704191624-1d0bd113de87/go.mod h1:DXUve3Dpr1UfpPtxFw+EFuQ41HhCWZfha5jSVRG7C7I=
|
||||
github.com/gogo/protobuf v1.2.1 h1:/s5zKNz0uPFCZ5hddgPdo2TK2TVrUNMn0OOX8/aZMTE=
|
||||
github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4=
|
||||
github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef h1:veQD95Isof8w9/WXiA+pa3tz3fJXkt5B7QaRBrM62gk=
|
||||
github.com/golang/groupcache v0.0.0-20190129154638-5b532d6fd5ef/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||
github.com/golang/protobuf v0.0.0-20161109072736-4bd1920723d7/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.1 h1:YF8+flBXS5eO826T4nzqPrxfhQThhXl0YzfuUPu4SBg=
|
||||
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-containerregistry v0.0.0-20190617215043-876b8855d23c/go.mod h1:yZAFP63pRshzrEYLXLGPmUt0Ay+2zdjmMN1loCnRLUk=
|
||||
github.com/google/gofuzz v0.0.0-20161122191042-44d81051d367/go.mod h1:HP5RmnzzSNb993RKQDq4+1A4ia9nllfqcQFTQJedwGI=
|
||||
github.com/google/gofuzz v1.0.0 h1:A8PeW59pxE9IoFRqBp37U+mSNaQoZ46F1f0f863XSXw=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/googleapis/gnostic v0.0.0-20170426233943-68f4ded48ba9/go.mod h1:sJBsCZ4ayReDTBIg8b9dl28c5xFWyhBTVRp3pOg5EKY=
|
||||
github.com/googleapis/gnostic v0.2.0 h1:l6N3VoaVzTncYYW+9yOz2LJJammFZGBO13sqgEhpy9g=
|
||||
github.com/googleapis/gnostic v0.2.0/go.mod h1:sJBsCZ4ayReDTBIg8b9dl28c5xFWyhBTVRp3pOg5EKY=
|
||||
github.com/hashicorp/golang-lru v0.5.1 h1:0hERBMJE1eitiLkihrMvRVBYAkpHzc/J3QdDN+dAcgU=
|
||||
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
|
||||
github.com/imdario/mergo v0.3.7/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
|
||||
github.com/jetstack/cert-manager v0.7.2/go.mod h1:nbddmhjWxYGt04bxvwVGUSeLhZ2PCyNvd7MpXdq+yWY=
|
||||
github.com/json-iterator/go v0.0.0-20180612202835-f2b4162afba3/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
|
||||
github.com/json-iterator/go v1.1.6 h1:MrUvLMLTMxbqFJ9kzlvat/rYZqZnW3u4wkLzWTaFwKs=
|
||||
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
|
||||
github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q=
|
||||
github.com/kisielk/gotool v1.0.0 h1:AV2c/EiW3KqPNT9ZKl07ehoAGi4C5/01Cfbblndcapg=
|
||||
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
|
||||
github.com/knative/build v0.6.0/go.mod h1:/sU74ZQkwlYA5FwYDJhYTy61i/Kn+5eWfln2jDbw3Qo=
|
||||
github.com/knative/pkg v0.0.0-20190514205332-5e4512dcb2ca/go.mod h1:7Ijfhw7rfB+H9VtosIsDYvZQ+qYTz7auK3fHW/5z4ww=
|
||||
github.com/knative/serving v0.6.1/go.mod h1:ljvMfwQy2qanaM/8xnBSK4Mz3Vv2NawC2fo5kFRJS1A=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.2 h1:DB17ag19krx9CFsz4o3enTrPXyIXCl+2iCXH/aMAp9s=
|
||||
github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
|
||||
github.com/mailru/easyjson v0.0.0-20160728113105-d5b7844b561a/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
|
||||
github.com/matryer/moq v0.0.0-20190312154309-6cfb0558e1bd/go.mod h1:9ELz6aaclSIGnZBoaSLZ3NAl1VTufbOrXBPvtcy6WiQ=
|
||||
github.com/mattbaird/jsonpatch v0.0.0-20171005235357-81af80346b1a/go.mod h1:M1qoD/MqPgTZIk0EWKB38wE28ACRfVcn+cU08jyArI0=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v0.0.0-20180320133207-05fbef0ca5da/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
github.com/modern-go/reflect2 v1.0.1 h1:9f412s+6RmYXLWZSEzVVgPGK7C2PphHj5RJrvfx9AWI=
|
||||
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
|
||||
github.com/munnerz/goautoneg v0.0.0-20120707110453-a547fc61f48d/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
|
||||
github.com/onsi/ginkgo v0.0.0-20170829012221-11459a886d9c/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
github.com/onsi/gomega v0.0.0-20170829124025-dcabb60a477c/go.mod h1:C1qb7wdrVGGVU+Z6iS04AVkA3Q65CEZX59MT0QO5uiA=
|
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v0.0.0-20151028094244-d8ed2627bdf0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/rancher/wrangler v0.1.4 h1:bdzBw4H9JKQhXPBPNp4eHbmrkA24+VII865VLiVWcw8=
|
||||
github.com/rancher/wrangler v0.1.4/go.mod h1:EYP7cqpg42YqElaCm+U9ieSrGQKAXxUH5xsr+XGpWyE=
|
||||
github.com/rancher/wrangler-api v0.2.0 h1:VR7hLNnDrKykKLqthtwZ58pDDtUa9ijSNToPaJLEkWc=
|
||||
github.com/rancher/wrangler-api v0.2.0/go.mod h1:zTPdNLZO07KvRaVOx6XQbKBSV55Fnn4s7nqmrMPJqd8=
|
||||
github.com/sirupsen/logrus v1.4.1 h1:GL2rEmy6nsikmW0r8opw9JIRScdMF5hA8cOYLH7In1k=
|
||||
github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q=
|
||||
github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk=
|
||||
github.com/spf13/pflag v0.0.0-20170130214245-9ff6c6923cff h1:VARhShG49tiji6mdRNp7JTNDtJ0FhuprF93GBQ37xGU=
|
||||
github.com/spf13/pflag v0.0.0-20170130214245-9ff6c6923cff/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v0.0.0-20151208002404-e3a8ff8ce365/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/tektoncd/pipeline v0.4.0/go.mod h1:IZzJdiX9EqEMuUcgdnElozdYYRh0/ZRC+NKMLj1K3Yw=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2 h1:VklqNMn3ovrHsnt90PveolxSbWFaJdECFbxSq0Mqo2M=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20190506204251-e1dfcc566284 h1:rlLehGeYg6jfoyz/eDqDU1iRXLKfR42nnNh57ytKEWo=
|
||||
golang.org/x/crypto v0.0.0-20190506204251-e1dfcc566284/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3 h1:0GoQqolDA55aaLxZyTzK/Y2ePZzZTUrRacwib7cNsYQ=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c h1:uOCk1iQW6Vc18bnC13MfzScl+wdKBmM9Y9kU7Z83/lw=
|
||||
golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20170114055629-f2499483f923/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a h1:oWX7TPOiFAMXLq8o0ikBYfCJVlRHBcsciT5bXOrH628=
|
||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/oauth2 v0.0.0-20190402181905-9f3314589c9a h1:tImsplftrFpALCYumobsd0K86vlAs/eXGFms2txfJfA=
|
||||
golang.org/x/oauth2 v0.0.0-20190402181905-9f3314589c9a/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw=
|
||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58 h1:8gQV6CLnAEikrhgkHFbMAEhagSSnXWGV915qUMm9mrU=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sys v0.0.0-20170830134202-bb24a47a89ea/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33 h1:I6FyU15t786LL7oL/hn43zqTuEGr4PN7F4XJ1p4E3Y8=
|
||||
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a h1:1BGLXjeY4akVXGgbC9HugT3Jv3hCI0z56oJR5vAMgBU=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d h1:+R4KGOnez64A81RvjARKc4UT5/tI9ujCIVX+P5KiHuI=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190509141414-a5b02f93d862 h1:rM0ROo5vb9AdYJi1110yjWGMej9ITfKddS89P3Fkhug=
|
||||
golang.org/x/sys v0.0.0-20190509141414-a5b02f93d862/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/text v0.0.0-20160726164857-2910a502d2bf/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
|
||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4 h1:SvFZT6jyqRaOeXpc5h/JSfZenJ2O330aBsf7JfSUXmQ=
|
||||
golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||
golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20181011042414-1f849cf54d09/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
google.golang.org/appengine v1.4.0 h1:/wp5JvzpHIxhs/dumFmF7BXTf3Z+dd4uXta4kVyO508=
|
||||
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc=
|
||||
gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=
|
||||
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
k8s.io/api v0.0.0-20190409021203-6e4e0e4f393b h1:aBGgKJUM9Hk/3AE8WaZIApnTxG35kbuQba2w+SXqezo=
|
||||
k8s.io/api v0.0.0-20190409021203-6e4e0e4f393b/go.mod h1:iuAfoD4hCxJ8Onx9kaTIt30j7jUFS00AXQi6QMi99vA=
|
||||
k8s.io/apiextensions-apiserver v0.0.0-20190409022649-727a075fdec8/go.mod h1:IxkesAMoaCRoLrPJdZNZUQp9NfZnzqaVzLhb2VEQzXE=
|
||||
k8s.io/apimachinery v0.0.0-20190404173353-6a84e37a896d h1:Jmdtdt1ZnoGfWWIIik61Z7nKYgO3J+swQJtPYsP9wHA=
|
||||
k8s.io/apimachinery v0.0.0-20190404173353-6a84e37a896d/go.mod h1:ccL7Eh7zubPUSh9A3USN90/OzHNSVN6zxzde07TDCL0=
|
||||
k8s.io/client-go v11.0.1-0.20190409021438-1a26190bd76a+incompatible h1:U5Bt+dab9K8qaUmXINrkXO135kA11/i5Kg1RUydgaMQ=
|
||||
k8s.io/client-go v11.0.1-0.20190409021438-1a26190bd76a+incompatible/go.mod h1:7vJpHMYJwNQCWgzmNV+VYUl1zCObLyodBc8nIyt8L5s=
|
||||
k8s.io/code-generator v0.0.0-20190311093542-50b561225d70/go.mod h1:MYiN+ZJZ9HkETbgVZdWw2AsuAi9PZ4V80cwfuf2axe8=
|
||||
k8s.io/gengo v0.0.0-20190128074634-0689ccc1d7d6/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0=
|
||||
k8s.io/gengo v0.0.0-20190327210449-e17681d19d3a/go.mod h1:ezvh/TsK7cY6rbqRK0oQQ8IAqLxYwwyPxAX1Pzy0ii0=
|
||||
k8s.io/klog v0.0.0-20181102134211-b9b56d5dfc92/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk=
|
||||
k8s.io/klog v0.3.0 h1:0VPpR+sizsiivjIfIAQH/rl8tan6jvWkS7lU+0di3lE=
|
||||
k8s.io/klog v0.3.0/go.mod h1:Gq+BEi5rUBO/HRz0bTSXDUcqjScdoY3a9IHpCEIOOfk=
|
||||
k8s.io/kube-openapi v0.0.0-20190502190224-411b2483e503/go.mod h1:iU+ZGYsNlvU9XKUSso6SQfKTCCw7lFduMZy26Mgr2Fw=
|
||||
k8s.io/utils v0.0.0-20190506122338-8fab8cb257d5 h1:VBM/0P5TWxwk+Nw6Z+lAw3DKgO76g90ETOiA6rfLV1Y=
|
||||
k8s.io/utils v0.0.0-20190506122338-8fab8cb257d5/go.mod h1:sZAwmy6armz5eXlNoLmJcl4F1QuKu7sr+mFQ0byX7Ew=
|
||||
sigs.k8s.io/structured-merge-diff v0.0.0-20190426204423-ea680f03cc65/go.mod h1:wWxsB5ozmmv/SG7nM11ayaAW51xMvak/t1r0CSlcokI=
|
||||
sigs.k8s.io/yaml v1.1.0 h1:4A07+ZFc2wgJwo8YNlQpr1rVlgUDlxXHhPJciaPY5gs=
|
||||
sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o=
|
||||
|
460
listener.go
Normal file
460
listener.go
Normal file
@@ -0,0 +1,460 @@
|
||||
package dynamiclistener
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"net"
|
||||
"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"
|
||||
)
|
||||
|
||||
type TLSStorage interface {
|
||||
Get() (*v1.Secret, error)
|
||||
Update(secret *v1.Secret) error
|
||||
}
|
||||
|
||||
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 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) {
|
||||
if config.CN == "" {
|
||||
config.CN = "dynamic"
|
||||
}
|
||||
if len(config.Organization) == 0 {
|
||||
config.Organization = []string{"dynamic"}
|
||||
}
|
||||
if config.TLSConfig == nil {
|
||||
config.TLSConfig = &tls.Config{}
|
||||
}
|
||||
|
||||
dynamicListener := &listener{
|
||||
factory: &factory.TLS{
|
||||
CACert: caCert,
|
||||
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)
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
conns map[int]*closeWrapper
|
||||
connID int
|
||||
connLock sync.Mutex
|
||||
|
||||
factory TLSFactory
|
||||
storage TLSStorage
|
||||
version string
|
||||
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 {
|
||||
l.updateCert(l.sans...)
|
||||
}
|
||||
})
|
||||
|
||||
conn, err := l.Listener.Accept()
|
||||
if err != nil {
|
||||
return conn, err
|
||||
}
|
||||
|
||||
addr := conn.LocalAddr()
|
||||
if addr == nil {
|
||||
return conn, nil
|
||||
}
|
||||
|
||||
host, _, err := net.SplitHostPort(addr.String())
|
||||
if err != nil {
|
||||
logrus.Errorf("failed to parse network %s: %v", addr.Network(), err)
|
||||
return conn, nil
|
||||
}
|
||||
|
||||
if !strings.Contains(host, ":") {
|
||||
if err := l.updateCert(host); err != nil {
|
||||
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 {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return l.loadCert()
|
||||
}
|
||||
|
||||
func (l *listener) updateCert(cn ...string) error {
|
||||
cn = l.factory.Filter(cn...)
|
||||
if len(cn) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
l.RLock()
|
||||
defer l.RUnlock()
|
||||
|
||||
secret, err := l.storage.Get()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !factory.IsStatic(secret) && !factory.NeedsUpdate(l.maxSANs, secret, cn...) {
|
||||
return nil
|
||||
}
|
||||
|
||||
l.RUnlock()
|
||||
l.Lock()
|
||||
defer l.RLock()
|
||||
defer l.Unlock()
|
||||
|
||||
secret, updated, err := l.factory.AddCN(secret, append(l.sans, cn...)...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if updated {
|
||||
if err := l.storage.Update(secret); err != nil {
|
||||
return err
|
||||
}
|
||||
// clear version to force cert reload
|
||||
l.version = ""
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (l *listener) loadCert() (*tls.Certificate, error) {
|
||||
l.RLock()
|
||||
defer l.RUnlock()
|
||||
|
||||
secret, err := l.storage.Get()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if l.cert != nil && l.version == secret.ResourceVersion && secret.ResourceVersion != "" {
|
||||
return l.cert, nil
|
||||
}
|
||||
|
||||
defer l.RLock()
|
||||
l.RUnlock()
|
||||
l.Lock()
|
||||
defer l.Unlock()
|
||||
|
||||
secret, err = l.storage.Get()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if l.cert != nil && l.version == secret.ResourceVersion && secret.ResourceVersion != "" {
|
||||
return l.cert, nil
|
||||
}
|
||||
|
||||
cert, err := tls.X509KeyPair(secret.Data[v1.TLSCertKey], secret.Data[v1.TLSPrivateKeyKey])
|
||||
if err != nil {
|
||||
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
|
||||
}
|
||||
|
||||
func (l *listener) cacheHandler() http.Handler {
|
||||
return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) {
|
||||
h, _, err := net.SplitHostPort(req.Host)
|
||||
if err != nil {
|
||||
h = req.Host
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
type nonNil struct {
|
||||
sync.Mutex
|
||||
storage TLSStorage
|
||||
}
|
||||
|
||||
func (n *nonNil) Get() (*v1.Secret, error) {
|
||||
n.Lock()
|
||||
defer n.Unlock()
|
||||
|
||||
s, err := n.storage.Get()
|
||||
if err != nil || s == nil {
|
||||
return &v1.Secret{}, err
|
||||
}
|
||||
return s, nil
|
||||
}
|
||||
|
||||
func (n *nonNil) Update(secret *v1.Secret) error {
|
||||
n.Lock()
|
||||
defer n.Unlock()
|
||||
|
||||
return n.storage.Update(secret)
|
||||
}
|
50
read.go
50
read.go
@@ -1,50 +0,0 @@
|
||||
package dynamiclistener
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
func ReadTLSConfig(userConfig *UserConfig) error {
|
||||
var err error
|
||||
|
||||
path := userConfig.CertPath
|
||||
|
||||
userConfig.CACerts, err = readPEM(filepath.Join(path, "cacerts.pem"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
userConfig.Key, err = readPEM(filepath.Join(path, "key.pem"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
userConfig.Cert, err = readPEM(filepath.Join(path, "cert.pem"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
valid := false
|
||||
if userConfig.Key != "" && userConfig.Cert != "" {
|
||||
valid = true
|
||||
} else if userConfig.Key == "" && userConfig.Cert == "" {
|
||||
valid = true
|
||||
}
|
||||
|
||||
if !valid {
|
||||
return fmt.Errorf("invalid SSL configuration found, please set cert/key, cert/key/cacerts, cacerts only, or none")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func readPEM(path string) (string, error) {
|
||||
content, err := ioutil.ReadFile(path)
|
||||
if err != nil {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
return string(content), nil
|
||||
}
|
46
redirect.go
Normal file
46
redirect.go
Normal file
@@ -0,0 +1,46 @@
|
||||
package dynamiclistener
|
||||
|
||||
import (
|
||||
"net"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Approach taken from letsencrypt, except manglePort is specific to us
|
||||
func HTTPRedirect(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(
|
||||
func(rw http.ResponseWriter, r *http.Request) {
|
||||
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)
|
||||
return
|
||||
}
|
||||
if r.Method != "GET" && r.Method != "HEAD" {
|
||||
http.Error(rw, "Use HTTPS", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
target := "https://" + manglePort(r.Host) + r.URL.RequestURI()
|
||||
http.Redirect(rw, r, target, http.StatusFound)
|
||||
})
|
||||
}
|
||||
|
||||
func manglePort(hostport string) string {
|
||||
host, port, err := net.SplitHostPort(hostport)
|
||||
if err != nil {
|
||||
return hostport
|
||||
}
|
||||
|
||||
portInt, err := strconv.Atoi(port)
|
||||
if err != nil {
|
||||
return hostport
|
||||
}
|
||||
|
||||
portInt = ((portInt / 1000) * 1000) + 443
|
||||
|
||||
return net.JoinHostPort(host, strconv.Itoa(portInt))
|
||||
}
|
551
server.go
551
server.go
@@ -1,551 +0,0 @@
|
||||
package dynamiclistener
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto"
|
||||
"crypto/ecdsa"
|
||||
"crypto/rsa"
|
||||
"crypto/tls"
|
||||
"crypto/x509"
|
||||
"encoding/base64"
|
||||
"encoding/pem"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
cert "github.com/rancher/dynamiclistener/cert"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type server struct {
|
||||
sync.Mutex
|
||||
|
||||
userConfig UserConfig
|
||||
listenConfigStorage ListenerConfigStorage
|
||||
tlsCert *tls.Certificate
|
||||
|
||||
ips map[string]bool
|
||||
domains map[string]bool
|
||||
cn string
|
||||
|
||||
listeners []net.Listener
|
||||
servers []*http.Server
|
||||
|
||||
activeCA *x509.Certificate
|
||||
activeCAKey crypto.Signer
|
||||
}
|
||||
|
||||
func NewServer(listenConfigStorage ListenerConfigStorage, config UserConfig) (ServerInterface, error) {
|
||||
s := &server{
|
||||
userConfig: config,
|
||||
listenConfigStorage: listenConfigStorage,
|
||||
cn: "cattle",
|
||||
}
|
||||
|
||||
s.ips = map[string]bool{}
|
||||
s.domains = map[string]bool{}
|
||||
|
||||
if err := s.userConfigure(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
lc, err := listenConfigStorage.Get()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return s, s.Update(lc)
|
||||
}
|
||||
|
||||
func (s *server) CACert() (string, error) {
|
||||
if s.userConfig.NoCACerts {
|
||||
return "", nil
|
||||
}
|
||||
if s.userConfig.CACerts != "" {
|
||||
return s.userConfig.CACerts, nil
|
||||
}
|
||||
return "", fmt.Errorf("ca cert not found")
|
||||
}
|
||||
|
||||
func marshalPrivateKey(privateKey crypto.Signer) (string, []byte, error) {
|
||||
var (
|
||||
keyType string
|
||||
bytes []byte
|
||||
err error
|
||||
)
|
||||
if key, ok := privateKey.(*ecdsa.PrivateKey); ok {
|
||||
keyType = cert.ECPrivateKeyBlockType
|
||||
bytes, err = x509.MarshalECPrivateKey(key)
|
||||
} else if key, ok := privateKey.(*rsa.PrivateKey); ok {
|
||||
keyType = cert.RSAPrivateKeyBlockType
|
||||
bytes = x509.MarshalPKCS1PrivateKey(key)
|
||||
} else {
|
||||
keyType = cert.PrivateKeyBlockType
|
||||
bytes, err = x509.MarshalPKCS8PrivateKey(privateKey)
|
||||
}
|
||||
if err != nil {
|
||||
logrus.Errorf("Unable to marshal private key: %v", err)
|
||||
}
|
||||
return keyType, bytes, err
|
||||
}
|
||||
|
||||
func newPrivateKey() (crypto.Signer, error) {
|
||||
caKeyBytes, err := cert.MakeEllipticPrivateKeyPEM()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
caKeyIFace, err := cert.ParsePrivateKeyPEM(caKeyBytes)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return caKeyIFace.(crypto.Signer), nil
|
||||
}
|
||||
|
||||
func (s *server) save() (_err error) {
|
||||
defer func() {
|
||||
if _err != nil {
|
||||
logrus.Errorf("Saving cert error: %s", _err)
|
||||
}
|
||||
}()
|
||||
|
||||
certStr, err := certToString(s.tlsCert)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cfg, err := s.listenConfigStorage.Get()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cfg.GeneratedCerts = map[string]string{s.cn: certStr}
|
||||
|
||||
_, err = s.listenConfigStorage.Set(cfg)
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *server) userConfigure() error {
|
||||
if s.userConfig.HTTPSPort == 0 {
|
||||
s.userConfig.HTTPSPort = 8443
|
||||
}
|
||||
|
||||
for _, d := range s.userConfig.Domains {
|
||||
s.domains[d] = true
|
||||
}
|
||||
|
||||
for _, ip := range s.userConfig.KnownIPs {
|
||||
if netIP := net.ParseIP(ip); netIP != nil {
|
||||
s.ips[ip] = true
|
||||
}
|
||||
}
|
||||
|
||||
if bindAddress := net.ParseIP(s.userConfig.BindAddress); bindAddress != nil {
|
||||
s.ips[s.userConfig.BindAddress] = true
|
||||
}
|
||||
|
||||
if s.activeCA == nil && s.activeCAKey == nil {
|
||||
if s.userConfig.CACerts != "" && s.userConfig.CAKey != "" {
|
||||
ca, err := cert.ParseCertsPEM([]byte(s.userConfig.CACerts))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
key, err := cert.ParsePrivateKeyPEM([]byte(s.userConfig.CAKey))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
s.activeCA = ca[0]
|
||||
s.activeCAKey = key.(crypto.Signer)
|
||||
} else {
|
||||
ca, key, err := genCA()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
s.activeCA = ca
|
||||
s.activeCAKey = key
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func genCA() (*x509.Certificate, crypto.Signer, error) {
|
||||
caKey, err := newPrivateKey()
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
caCert, err := cert.NewSelfSignedCACert(cert.Config{
|
||||
CommonName: "k3s-ca",
|
||||
Organization: []string{"k3s-org"},
|
||||
}, caKey)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
return caCert, caKey, nil
|
||||
}
|
||||
|
||||
func (s *server) Update(status *ListenerStatus) (_err error) {
|
||||
s.Lock()
|
||||
defer func() {
|
||||
s.Unlock()
|
||||
if _err != nil {
|
||||
logrus.Errorf("Update cert error: %s", _err)
|
||||
}
|
||||
if s.tlsCert == nil {
|
||||
s.getCertificate(&tls.ClientHelloInfo{ServerName: "localhost"})
|
||||
}
|
||||
}()
|
||||
|
||||
certString := status.GeneratedCerts[s.cn]
|
||||
tlsCert, err := stringToCert(certString)
|
||||
if err != nil {
|
||||
logrus.Errorf("Update cert unable to convert string to cert: %s", err)
|
||||
s.tlsCert = nil
|
||||
}
|
||||
if tlsCert != nil {
|
||||
s.tlsCert = tlsCert
|
||||
for i, certBytes := range tlsCert.Certificate {
|
||||
cert, err := x509.ParseCertificate(certBytes)
|
||||
if err != nil {
|
||||
logrus.Errorf("Update cert %d parse error: %s", i, err)
|
||||
s.tlsCert = nil
|
||||
break
|
||||
}
|
||||
|
||||
ips := map[string]bool{}
|
||||
for _, ip := range cert.IPAddresses {
|
||||
ips[ip.String()] = true
|
||||
}
|
||||
|
||||
domains := map[string]bool{}
|
||||
for _, domain := range cert.DNSNames {
|
||||
domains[domain] = true
|
||||
}
|
||||
|
||||
if !(reflect.DeepEqual(ips, s.ips) && reflect.DeepEqual(domains, s.domains)) {
|
||||
subset := true
|
||||
for ip := range s.ips {
|
||||
if !ips[ip] {
|
||||
subset = false
|
||||
break
|
||||
}
|
||||
}
|
||||
if subset {
|
||||
for domain := range s.domains {
|
||||
if !domains[domain] {
|
||||
subset = false
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if !subset {
|
||||
s.tlsCert = nil
|
||||
}
|
||||
for ip := range ips {
|
||||
s.ips[ip] = true
|
||||
}
|
||||
for domain := range domains {
|
||||
s.domains[domain] = true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return s.reload()
|
||||
}
|
||||
|
||||
func (s *server) shutdown() error {
|
||||
for _, listener := range s.listeners {
|
||||
if err := listener.Close(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
s.listeners = nil
|
||||
|
||||
for _, server := range s.servers {
|
||||
go server.Shutdown(context.Background())
|
||||
}
|
||||
s.servers = nil
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *server) reload() error {
|
||||
if len(s.listeners) > 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := s.shutdown(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := s.serveHTTPS(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *server) getCertificate(hello *tls.ClientHelloInfo) (_servingCert *tls.Certificate, _err error) {
|
||||
s.Lock()
|
||||
changed := false
|
||||
|
||||
defer func() {
|
||||
defer s.Unlock()
|
||||
|
||||
if _err != nil {
|
||||
logrus.Errorf("Get certificate error: %s", _err)
|
||||
return
|
||||
}
|
||||
|
||||
if changed {
|
||||
s.save()
|
||||
}
|
||||
}()
|
||||
|
||||
if hello.ServerName != "" && !s.domains[hello.ServerName] {
|
||||
s.tlsCert = nil
|
||||
s.domains[hello.ServerName] = true
|
||||
}
|
||||
|
||||
if s.tlsCert != nil {
|
||||
return s.tlsCert, nil
|
||||
}
|
||||
|
||||
ips := []net.IP{}
|
||||
for ipStr := range s.ips {
|
||||
if ip := net.ParseIP(ipStr); ip != nil {
|
||||
ips = append(ips, ip)
|
||||
}
|
||||
}
|
||||
|
||||
dnsNames := []string{}
|
||||
for domain := range s.domains {
|
||||
dnsNames = append(dnsNames, domain)
|
||||
}
|
||||
|
||||
cfg := cert.Config{
|
||||
CommonName: s.cn,
|
||||
Organization: s.activeCA.Subject.Organization,
|
||||
Usages: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
|
||||
AltNames: cert.AltNames{
|
||||
DNSNames: dnsNames,
|
||||
IPs: ips,
|
||||
},
|
||||
}
|
||||
|
||||
key, err := newPrivateKey()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
cert, err := cert.NewSignedCert(cfg, key, s.activeCA, s.activeCAKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
tlsCert := &tls.Certificate{
|
||||
Certificate: [][]byte{
|
||||
cert.Raw,
|
||||
},
|
||||
PrivateKey: key,
|
||||
}
|
||||
|
||||
changed = true
|
||||
s.tlsCert = tlsCert
|
||||
return tlsCert, nil
|
||||
}
|
||||
|
||||
func (s *server) cacheHandler(handler http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) {
|
||||
h, _, err := net.SplitHostPort(req.Host)
|
||||
if err != nil {
|
||||
h = req.Host
|
||||
}
|
||||
|
||||
s.Lock()
|
||||
if ip := net.ParseIP(h); ip != nil {
|
||||
if !s.ips[h] {
|
||||
s.ips[h] = true
|
||||
s.tlsCert = nil
|
||||
}
|
||||
} else {
|
||||
if !s.domains[h] {
|
||||
s.domains[h] = true
|
||||
s.tlsCert = nil
|
||||
}
|
||||
}
|
||||
s.Unlock()
|
||||
|
||||
handler.ServeHTTP(resp, req)
|
||||
})
|
||||
}
|
||||
|
||||
func (s *server) serveHTTPS() error {
|
||||
conf := &tls.Config{
|
||||
ClientAuth: tls.RequestClientCert,
|
||||
GetCertificate: s.getCertificate,
|
||||
PreferServerCipherSuites: true,
|
||||
}
|
||||
|
||||
listener, err := s.newListener(s.userConfig.BindAddress, s.userConfig.HTTPSPort, conf)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
logger := logrus.StandardLogger()
|
||||
server := &http.Server{
|
||||
Handler: s.cacheHandler(s.Handler()),
|
||||
ErrorLog: log.New(logger.WriterLevel(logrus.DebugLevel), "", log.LstdFlags),
|
||||
}
|
||||
|
||||
s.servers = append(s.servers, server)
|
||||
s.startServer(listener, server)
|
||||
|
||||
if s.userConfig.HTTPPort > 0 {
|
||||
httpListener, err := s.newListener(s.userConfig.BindAddress, s.userConfig.HTTPPort, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
httpServer := &http.Server{
|
||||
Handler: s.cacheHandler(httpRedirect(s.Handler())),
|
||||
ErrorLog: log.New(logger.WriterLevel(logrus.DebugLevel), "", log.LstdFlags),
|
||||
}
|
||||
|
||||
s.servers = append(s.servers, httpServer)
|
||||
s.startServer(httpListener, httpServer)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Approach taken from letsencrypt, except manglePort is specific to us
|
||||
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" ||
|
||||
strings.HasPrefix(r.URL.Path, "/ping") ||
|
||||
strings.HasPrefix(r.URL.Path, "/health") {
|
||||
next.ServeHTTP(rw, r)
|
||||
return
|
||||
}
|
||||
if r.Method != "GET" && r.Method != "HEAD" {
|
||||
http.Error(rw, "Use HTTPS", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
target := "https://" + manglePort(r.Host) + r.URL.RequestURI()
|
||||
http.Redirect(rw, r, target, http.StatusFound)
|
||||
})
|
||||
}
|
||||
|
||||
func manglePort(hostport string) string {
|
||||
host, port, err := net.SplitHostPort(hostport)
|
||||
if err != nil {
|
||||
return hostport
|
||||
}
|
||||
|
||||
portInt, err := strconv.Atoi(port)
|
||||
if err != nil {
|
||||
return hostport
|
||||
}
|
||||
|
||||
portInt = ((portInt / 1000) * 1000) + 443
|
||||
|
||||
return net.JoinHostPort(host, strconv.Itoa(portInt))
|
||||
}
|
||||
|
||||
func (s *server) startServer(listener net.Listener, server *http.Server) {
|
||||
go func() {
|
||||
if err := server.Serve(listener); err != nil {
|
||||
logrus.Errorf("server on %v returned err: %v", listener.Addr(), err)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func (s *server) Handler() http.Handler {
|
||||
return s.userConfig.Handler
|
||||
}
|
||||
|
||||
func (s *server) newListener(ip string, port int, config *tls.Config) (net.Listener, error) {
|
||||
addr := fmt.Sprintf("%s:%d", ip, port)
|
||||
l, err := net.Listen("tcp", addr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
l = tcpKeepAliveListener{l.(*net.TCPListener)}
|
||||
|
||||
if config != nil {
|
||||
l = tls.NewListener(l, config)
|
||||
}
|
||||
|
||||
s.listeners = append(s.listeners, l)
|
||||
logrus.Info("Listening on ", addr)
|
||||
return l, nil
|
||||
}
|
||||
|
||||
func stringToCert(certString string) (*tls.Certificate, error) {
|
||||
parts := strings.Split(certString, "#")
|
||||
if len(parts) != 2 {
|
||||
return nil, errors.New("Unable to split cert into two parts")
|
||||
}
|
||||
|
||||
certPart, keyPart := parts[0], parts[1]
|
||||
keyBytes, err := base64.StdEncoding.DecodeString(keyPart)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
key, err := cert.ParsePrivateKeyPEM(keyBytes)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
certBytes, err := base64.StdEncoding.DecodeString(certPart)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &tls.Certificate{
|
||||
Certificate: [][]byte{certBytes},
|
||||
PrivateKey: key,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func certToString(cert *tls.Certificate) (string, error) {
|
||||
keyType, keyBytes, err := marshalPrivateKey(cert.PrivateKey.(crypto.Signer))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
privateKeyPemBlock := &pem.Block{
|
||||
Type: keyType,
|
||||
Bytes: keyBytes,
|
||||
}
|
||||
pemBytes := pem.EncodeToMemory(privateKeyPemBlock)
|
||||
|
||||
certString := base64.StdEncoding.EncodeToString(cert.Certificate[0])
|
||||
keyString := base64.StdEncoding.EncodeToString(pemBytes)
|
||||
return certString + "#" + keyString, nil
|
||||
}
|
||||
|
||||
type tcpKeepAliveListener struct {
|
||||
*net.TCPListener
|
||||
}
|
||||
|
||||
func (ln tcpKeepAliveListener) Accept() (c net.Conn, err error) {
|
||||
tc, err := ln.AcceptTCP()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
tc.SetKeepAlive(true)
|
||||
tc.SetKeepAlivePeriod(3 * time.Minute)
|
||||
return tc, nil
|
||||
}
|
220
server/server.go
Normal file
220
server/server.go
Normal file
@@ -0,0 +1,220 @@
|
||||
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"
|
||||
)
|
||||
|
||||
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 {
|
||||
tlsTCPListener, err := dynamiclistener.NewTCPListener(opts.BindHost, httpsPort)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
tlsTCPListener, handler, err = getTLSListener(ctx, tlsTCPListener, handler, *opts)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if !opts.NoRedirect {
|
||||
handler = dynamiclistener.HTTPRedirect(handler)
|
||||
}
|
||||
|
||||
tlsServer := http.Server{
|
||||
Handler: handler,
|
||||
ErrorLog: errorLog,
|
||||
}
|
||||
|
||||
go func() {
|
||||
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)
|
||||
}
|
||||
}()
|
||||
go func() {
|
||||
<-ctx.Done()
|
||||
tlsServer.Shutdown(context.Background())
|
||||
}()
|
||||
}
|
||||
|
||||
if httpPort > 0 {
|
||||
httpServer := http.Server{
|
||||
Addr: fmt.Sprintf("%s:%d", opts.BindHost, httpPort),
|
||||
Handler: handler,
|
||||
ErrorLog: errorLog,
|
||||
}
|
||||
go func() {
|
||||
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)
|
||||
}
|
||||
}()
|
||||
go func() {
|
||||
<-ctx.Done()
|
||||
httpServer.Shutdown(context.Background())
|
||||
}()
|
||||
}
|
||||
|
||||
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
|
||||
}
|
41
storage/file/file.go
Normal file
41
storage/file/file.go
Normal file
@@ -0,0 +1,41 @@
|
||||
package file
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"github.com/rancher/dynamiclistener"
|
||||
"k8s.io/api/core/v1"
|
||||
"os"
|
||||
)
|
||||
|
||||
func New(file string) dynamiclistener.TLSStorage {
|
||||
return &storage{
|
||||
file: file,
|
||||
}
|
||||
}
|
||||
|
||||
type storage struct {
|
||||
file string
|
||||
}
|
||||
|
||||
func (s *storage) Get() (*v1.Secret, error) {
|
||||
f, err := os.Open(s.file)
|
||||
if os.IsNotExist(err) {
|
||||
return nil, nil
|
||||
} else if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
secret := v1.Secret{}
|
||||
return &secret, json.NewDecoder(f).Decode(&secret)
|
||||
}
|
||||
|
||||
func (s *storage) Update(secret *v1.Secret) error {
|
||||
f, err := os.Create(s.file)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
return json.NewEncoder(f).Encode(secret)
|
||||
}
|
103
storage/kubernetes/ca.go
Normal file
103
storage/kubernetes/ca.go
Normal 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)
|
||||
}
|
184
storage/kubernetes/controller.go
Normal file
184
storage/kubernetes/controller.go
Normal file
@@ -0,0 +1,184 @@
|
||||
package kubernetes
|
||||
|
||||
import (
|
||||
"context"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/rancher/dynamiclistener"
|
||||
"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/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,
|
||||
namespace: namespace,
|
||||
storage: backing,
|
||||
ctx: ctx,
|
||||
}
|
||||
|
||||
// lazy init
|
||||
go func() {
|
||||
for {
|
||||
core := core()
|
||||
if core != nil {
|
||||
storage.init(core.Core().V1().Secret())
|
||||
start.All(ctx, 5, core)
|
||||
return
|
||||
}
|
||||
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-time.After(time.Second):
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
return storage
|
||||
}
|
||||
|
||||
type storage struct {
|
||||
sync.Mutex
|
||||
|
||||
namespace, name string
|
||||
storage dynamiclistener.TLSStorage
|
||||
secrets v1controller.SecretClient
|
||||
ctx context.Context
|
||||
tls dynamiclistener.TLSFactory
|
||||
}
|
||||
|
||||
func (s *storage) SetFactory(tls dynamiclistener.TLSFactory) {
|
||||
s.tls = tls
|
||||
}
|
||||
|
||||
func (s *storage) init(secrets v1controller.SecretController) {
|
||||
s.Lock()
|
||||
defer s.Unlock()
|
||||
|
||||
secrets.OnChange(s.ctx, "tls-storage", func(key string, secret *v1.Secret) (*v1.Secret, error) {
|
||||
if secret == nil {
|
||||
return nil, nil
|
||||
}
|
||||
if secret.Namespace == s.namespace && secret.Name == s.name {
|
||||
if err := s.Update(secret); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return secret, nil
|
||||
})
|
||||
s.secrets = secrets
|
||||
|
||||
if secret, err := s.storage.Get(); err == nil && secret != nil && len(secret.Data) > 0 {
|
||||
// just ensure there is a secret in k3s
|
||||
if _, err := s.secrets.Get(s.namespace, s.name, metav1.GetOptions{}); errors.IsNotFound(err) {
|
||||
_, _ = s.secrets.Create(&v1.Secret{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: s.name,
|
||||
Namespace: s.namespace,
|
||||
Annotations: secret.Annotations,
|
||||
},
|
||||
Type: v1.SecretTypeTLS,
|
||||
Data: secret.Data,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *storage) Get() (*v1.Secret, error) {
|
||||
s.Lock()
|
||||
defer s.Unlock()
|
||||
|
||||
return s.storage.Get()
|
||||
}
|
||||
|
||||
func (s *storage) targetSecret() (*v1.Secret, error) {
|
||||
existingSecret, err := s.secrets.Get(s.namespace, s.name, metav1.GetOptions{})
|
||||
if errors.IsNotFound(err) {
|
||||
return &v1.Secret{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: s.name,
|
||||
Namespace: s.namespace,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
return existingSecret, err
|
||||
}
|
||||
|
||||
func (s *storage) saveInK8s(secret *v1.Secret) (*v1.Secret, error) {
|
||||
if s.secrets == nil {
|
||||
return secret, nil
|
||||
}
|
||||
|
||||
if existing, err := s.storage.Get(); err == nil && s.tls != nil {
|
||||
if newSecret, updated, err := s.tls.Merge(existing, secret); err == nil && updated {
|
||||
secret = newSecret
|
||||
}
|
||||
}
|
||||
|
||||
targetSecret, err := s.targetSecret()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
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
|
||||
targetSecret.Type = v1.SecretTypeTLS
|
||||
targetSecret.Data = secret.Data
|
||||
|
||||
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)
|
||||
}
|
||||
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) {
|
||||
s.Lock()
|
||||
defer s.Unlock()
|
||||
|
||||
for i := 0; i < 3; i++ {
|
||||
secret, err = s.saveInK8s(secret)
|
||||
if errors.IsConflict(err) {
|
||||
continue
|
||||
} else if err != nil {
|
||||
return err
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// update underlying storage
|
||||
return s.storage.Update(secret)
|
||||
}
|
46
storage/memory/memory.go
Normal file
46
storage/memory/memory.go
Normal file
@@ -0,0 +1,46 @@
|
||||
package memory
|
||||
|
||||
import (
|
||||
"github.com/rancher/dynamiclistener"
|
||||
"github.com/sirupsen/logrus"
|
||||
v1 "k8s.io/api/core/v1"
|
||||
)
|
||||
|
||||
func New() dynamiclistener.TLSStorage {
|
||||
return &memory{}
|
||||
}
|
||||
|
||||
func NewBacked(storage dynamiclistener.TLSStorage) dynamiclistener.TLSStorage {
|
||||
return &memory{storage: storage}
|
||||
}
|
||||
|
||||
type memory struct {
|
||||
storage dynamiclistener.TLSStorage
|
||||
secret *v1.Secret
|
||||
}
|
||||
|
||||
func (m *memory) Get() (*v1.Secret, error) {
|
||||
if m.secret == nil && m.storage != nil {
|
||||
secret, err := m.storage.Get()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
m.secret = secret
|
||||
}
|
||||
|
||||
return m.secret, nil
|
||||
}
|
||||
|
||||
func (m *memory) Update(secret *v1.Secret) error {
|
||||
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
|
||||
}
|
||||
return nil
|
||||
}
|
36
storage/static/static.go
Normal file
36
storage/static/static.go
Normal 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
|
||||
}
|
38
tcp.go
Normal file
38
tcp.go
Normal file
@@ -0,0 +1,38 @@
|
||||
package dynamiclistener
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"reflect"
|
||||
"time"
|
||||
)
|
||||
|
||||
func NewTCPListener(ip string, port int) (net.Listener, error) {
|
||||
l, err := net.Listen("tcp", fmt.Sprintf("%s:%d", ip, port))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
tcpListener, ok := l.(*net.TCPListener)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("wrong listener type: %v", reflect.TypeOf(tcpListener))
|
||||
}
|
||||
|
||||
return tcpKeepAliveListener{
|
||||
TCPListener: tcpListener,
|
||||
}, nil
|
||||
}
|
||||
|
||||
type tcpKeepAliveListener struct {
|
||||
*net.TCPListener
|
||||
}
|
||||
|
||||
func (ln tcpKeepAliveListener) Accept() (c net.Conn, err error) {
|
||||
tc, err := ln.AcceptTCP()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
tc.SetKeepAlive(true)
|
||||
tc.SetKeepAlivePeriod(3 * time.Minute)
|
||||
return tc, nil
|
||||
}
|
63
types.go
63
types.go
@@ -1,63 +0,0 @@
|
||||
package dynamiclistener
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
)
|
||||
|
||||
type ListenerConfigStorage interface {
|
||||
Set(*ListenerStatus) (*ListenerStatus, error)
|
||||
Get() (*ListenerStatus, error)
|
||||
}
|
||||
|
||||
type ServerInterface interface {
|
||||
Update(status *ListenerStatus) error
|
||||
CACert() (string, error)
|
||||
}
|
||||
|
||||
type UserConfig struct {
|
||||
// Required fields
|
||||
|
||||
Handler http.Handler
|
||||
HTTPPort int
|
||||
HTTPSPort int
|
||||
CertPath string
|
||||
|
||||
// Optional fields
|
||||
|
||||
KnownIPs []string
|
||||
Domains []string
|
||||
Mode string
|
||||
NoCACerts bool
|
||||
CACerts string
|
||||
CAKey string
|
||||
Cert string
|
||||
Key string
|
||||
BindAddress string
|
||||
}
|
||||
|
||||
type ListenerStatus struct {
|
||||
Revision string `json:"revision,omitempty"`
|
||||
CACert string `json:"caCert,omitempty"`
|
||||
CAKey string `json:"caKey,omitempty"`
|
||||
GeneratedCerts map[string]string `json:"generatedCerts" norman:"nocreate,noupdate"`
|
||||
KnownIPs map[string]bool `json:"knownIps" norman:"nocreate,noupdate"`
|
||||
}
|
||||
|
||||
func (l *ListenerStatus) DeepCopyInto(t *ListenerStatus) {
|
||||
t.Revision = l.Revision
|
||||
t.CACert = l.CACert
|
||||
t.CAKey = l.CAKey
|
||||
t.GeneratedCerts = copyMap(t.GeneratedCerts)
|
||||
t.KnownIPs = map[string]bool{}
|
||||
for k, v := range l.KnownIPs {
|
||||
t.KnownIPs[k] = v
|
||||
}
|
||||
}
|
||||
|
||||
func copyMap(m map[string]string) map[string]string {
|
||||
ret := map[string]string{}
|
||||
for k, v := range m {
|
||||
ret[k] = v
|
||||
}
|
||||
return ret
|
||||
}
|
Reference in New Issue
Block a user