mirror of
https://github.com/rancher/dynamiclistener.git
synced 2025-09-02 13:44:26 +00:00
Compare commits
64 Commits
v0.3.0
...
renovate/r
Author | SHA1 | Date | |
---|---|---|---|
|
bab6b67355 | ||
|
35b8be0eb9 | ||
|
d5cd913a6f | ||
|
6cb7335776 | ||
|
7d5f0cbf5e | ||
|
64779f3d67 | ||
|
d941584b20 | ||
|
f3fd177670 | ||
|
9e2c8269e9 | ||
|
64bcbbed34 | ||
|
ea3b7aa886 | ||
|
0a2d8dff62 | ||
|
0132d96ec2 | ||
|
06acb83e5d | ||
|
10456ff2e6 | ||
|
1c60bf414f | ||
|
3e7612c2c9 | ||
|
e6585da47a | ||
|
6cc9a670e1 | ||
|
8f13b193a1 | ||
|
02304047cf | ||
|
4c1ac9bd4b | ||
|
2b62d5cc69 | ||
|
2ac221e5d6 | ||
|
b7a028fe3f | ||
|
a150115362 | ||
|
7001abfa1f | ||
|
3adafb7edb | ||
|
e73d5f2fca | ||
|
401fafb7e6 | ||
|
bad953b9f0 | ||
|
8ebd77f8a4 | ||
|
fdf983a935 | ||
|
7b5997cee9 | ||
|
42d72c2ef2 | ||
|
d2b7e2aaa6 | ||
|
a30741bb53 | ||
|
4df376813d | ||
|
9b92d13bcb | ||
|
b1d65efb6f | ||
|
5e81b14c1f | ||
|
148d38076d | ||
|
43f9c3ae0a | ||
|
284cc004e8 | ||
|
120a37b97a | ||
|
bbac29e0fa | ||
|
962b635269 | ||
|
f147aa4166 | ||
|
63157c59ce | ||
|
2644a6ed16 | ||
|
27f4642299 | ||
|
cd5d71f2fe | ||
|
fb66484384 | ||
|
6b37dc1212 | ||
|
c7dd355394 | ||
|
94e22490cf | ||
|
b45d8a455e | ||
|
9865ae859c | ||
|
db883ae66a | ||
|
9dfd7df057 | ||
|
ff22834bde | ||
|
dc7452dbb8 | ||
|
86af265dcd | ||
|
f373fc1c7c |
9
.github/renovate.json
vendored
Normal file
9
.github/renovate.json
vendored
Normal file
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"extends": [
|
||||
"github>rancher/renovate-config#release"
|
||||
],
|
||||
"baseBranches": [
|
||||
"master"
|
||||
],
|
||||
"prHourlyLimit": 2
|
||||
}
|
20
.github/workflows/ci.yaml
vendored
Normal file
20
.github/workflows/ci.yaml
vendored
Normal file
@@ -0,0 +1,20 @@
|
||||
name: CI
|
||||
on:
|
||||
pull_request:
|
||||
push:
|
||||
branches:
|
||||
- 'master'
|
||||
- 'release/*'
|
||||
jobs:
|
||||
ci:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout code
|
||||
# https://github.com/actions/checkout/releases/tag/v4.1.1
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
- name: Install Go
|
||||
# https://github.com/actions/setup-go/releases/tag/v5.0.0
|
||||
uses: actions/setup-go@f111f3307d8850f501ac008e886eec1fd1932a34 # v5.3.0
|
||||
with:
|
||||
go-version-file: 'go.mod'
|
||||
- run: go test -race -cover ./...
|
25
.github/workflows/release.yaml
vendored
Normal file
25
.github/workflows/release.yaml
vendored
Normal file
@@ -0,0 +1,25 @@
|
||||
name: Release
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- v*
|
||||
|
||||
jobs:
|
||||
release:
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- name : Checkout repository
|
||||
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
|
||||
|
||||
- name: Create release on Github
|
||||
run: |
|
||||
if [[ "${{ github.ref_name }}" == *-rc* ]]; then
|
||||
gh --repo "${{ github.repository }}" release create ${{ github.ref_name }} --verify-tag --generate-notes --prerelease
|
||||
else
|
||||
gh --repo "${{ github.repository }}" release create ${{ github.ref_name }} --verify-tag --generate-notes
|
||||
fi
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
25
.github/workflows/renovate.yml
vendored
Normal file
25
.github/workflows/renovate.yml
vendored
Normal file
@@ -0,0 +1,25 @@
|
||||
name: Renovate
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
logLevel:
|
||||
description: "Override default log level"
|
||||
required: false
|
||||
default: "info"
|
||||
type: string
|
||||
overrideSchedule:
|
||||
description: "Override all schedules"
|
||||
required: false
|
||||
default: "false"
|
||||
type: string
|
||||
# Run twice in the early morning (UTC) for initial and follow up steps (create pull request and merge)
|
||||
schedule:
|
||||
- cron: '30 4,6 * * *'
|
||||
|
||||
jobs:
|
||||
call-workflow:
|
||||
uses: rancher/renovate-config/.github/workflows/renovate.yml@release
|
||||
with:
|
||||
logLevel: ${{ inputs.logLevel || 'info' }}
|
||||
overrideSchedule: ${{ github.event.inputs.overrideSchedule == 'true' && '{''schedule'':null}' || '' }}
|
||||
secrets: inherit
|
10
README.md
Normal file
10
README.md
Normal file
@@ -0,0 +1,10 @@
|
||||
# [dynamiclistener](https://github.com/rancher/dynamiclistener)
|
||||
|
||||
This `README` is a work in progress; aimed towards providing information for navigating the contents of this repository.
|
||||
|
||||
## Changing the Expiration Days for Newly Signed Certificates
|
||||
|
||||
By default, a newly signed certificate is set to expire 365 days (1 year) after its creation time and date.
|
||||
You can use the `CATTLE_NEW_SIGNED_CERT_EXPIRATION_DAYS` environment variable to change this value.
|
||||
|
||||
**Please note:** the value for the aforementioned variable must be a string representing an unsigned integer corresponding to the number of days until expiration (i.e. X509 "NotAfter" value).
|
31
cert/cert.go
31
cert/cert.go
@@ -33,7 +33,9 @@ import (
|
||||
"math"
|
||||
"math/big"
|
||||
"net"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -45,16 +47,15 @@ const (
|
||||
duration365d = time.Hour * 24 * 365
|
||||
)
|
||||
|
||||
var (
|
||||
ErrStaticCert = errors.New("cannot renew static certificate")
|
||||
)
|
||||
var ErrStaticCert = errors.New("cannot renew static certificate")
|
||||
|
||||
// Config contains the basic fields required for creating a certificate
|
||||
// Config contains the basic fields required for creating a certificate.
|
||||
type Config struct {
|
||||
CommonName string
|
||||
Organization []string
|
||||
AltNames AltNames
|
||||
Usages []x509.ExtKeyUsage
|
||||
ExpiresAt time.Duration
|
||||
}
|
||||
|
||||
// AltNames contains the domain names and IP addresses that will be added
|
||||
@@ -90,10 +91,15 @@ func NewSelfSignedCACert(cfg Config, key crypto.Signer) (*x509.Certificate, erro
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
logrus.Infof("generated self-signed CA certificate %s: notBefore=%s notAfter=%s",
|
||||
tmpl.Subject, tmpl.NotBefore, tmpl.NotAfter)
|
||||
|
||||
return x509.ParseCertificate(certDERBytes)
|
||||
}
|
||||
|
||||
// NewSignedCert creates a signed certificate using the given CA certificate and key
|
||||
// NewSignedCert creates a signed certificate using the given CA certificate and key based
|
||||
// on the given configuration.
|
||||
func NewSignedCert(cfg Config, key crypto.Signer, caCert *x509.Certificate, caKey crypto.Signer) (*x509.Certificate, error) {
|
||||
serial, err := rand.Int(rand.Reader, new(big.Int).SetInt64(math.MaxInt64))
|
||||
if err != nil {
|
||||
@@ -105,6 +111,19 @@ func NewSignedCert(cfg Config, key crypto.Signer, caCert *x509.Certificate, caKe
|
||||
if len(cfg.Usages) == 0 {
|
||||
return nil, errors.New("must specify at least one ExtKeyUsage")
|
||||
}
|
||||
expiresAt := duration365d
|
||||
if cfg.ExpiresAt > 0 {
|
||||
expiresAt = time.Duration(cfg.ExpiresAt)
|
||||
} else {
|
||||
envExpirationDays := os.Getenv("CATTLE_NEW_SIGNED_CERT_EXPIRATION_DAYS")
|
||||
if envExpirationDays != "" {
|
||||
if envExpirationDaysInt, err := strconv.Atoi(envExpirationDays); err != nil {
|
||||
logrus.Infof("[NewSignedCert] expiration days from ENV (%s) could not be converted to int (falling back to default value: %d)", envExpirationDays, expiresAt)
|
||||
} else {
|
||||
expiresAt = time.Hour * 24 * time.Duration(envExpirationDaysInt)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
certTmpl := x509.Certificate{
|
||||
Subject: pkix.Name{
|
||||
@@ -115,7 +134,7 @@ func NewSignedCert(cfg Config, key crypto.Signer, caCert *x509.Certificate, caKe
|
||||
IPAddresses: cfg.AltNames.IPs,
|
||||
SerialNumber: serial,
|
||||
NotBefore: caCert.NotBefore,
|
||||
NotAfter: time.Now().Add(duration365d).UTC(),
|
||||
NotAfter: time.Now().Add(expiresAt).UTC(),
|
||||
KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
|
||||
ExtKeyUsage: cfg.Usages,
|
||||
}
|
||||
|
16
cert/secret.go
Normal file
16
cert/secret.go
Normal file
@@ -0,0 +1,16 @@
|
||||
package cert
|
||||
|
||||
import v1 "k8s.io/api/core/v1"
|
||||
|
||||
func IsValidTLSSecret(secret *v1.Secret) bool {
|
||||
if secret == nil {
|
||||
return false
|
||||
}
|
||||
if _, ok := secret.Data[v1.TLSCertKey]; !ok {
|
||||
return false
|
||||
}
|
||||
if _, ok := secret.Data[v1.TLSPrivateKeyKey]; !ok {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
@@ -6,6 +6,7 @@ import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/rancher/dynamiclistener/cert"
|
||||
)
|
||||
@@ -16,7 +17,7 @@ func GenCA() (*x509.Certificate, crypto.Signer, error) {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
caCert, err := NewSelfSignedCACert(caKey, "dynamiclistener-ca", "dynamiclistener-org")
|
||||
caCert, err := NewSelfSignedCACert(caKey, fmt.Sprintf("dynamiclistener-ca@%d", time.Now().Unix()), "dynamiclistener-org")
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
@@ -24,18 +25,28 @@ func GenCA() (*x509.Certificate, crypto.Signer, error) {
|
||||
return caCert, caKey, nil
|
||||
}
|
||||
|
||||
// Deprecated: Use LoadOrGenCAChain instead as it supports intermediate CAs
|
||||
func LoadOrGenCA() (*x509.Certificate, crypto.Signer, error) {
|
||||
cert, key, err := loadCA()
|
||||
if err == nil {
|
||||
return cert, key, nil
|
||||
}
|
||||
|
||||
cert, key, err = GenCA()
|
||||
chain, signer, err := LoadOrGenCAChain()
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return chain[0], signer, err
|
||||
}
|
||||
|
||||
certBytes, keyBytes, err := Marshal(cert, key)
|
||||
func LoadOrGenCAChain() ([]*x509.Certificate, crypto.Signer, error) {
|
||||
certs, key, err := loadCA()
|
||||
if err == nil {
|
||||
return certs, key, nil
|
||||
}
|
||||
|
||||
cert, key, err := GenCA()
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
certs = []*x509.Certificate{cert}
|
||||
|
||||
certBytes, keyBytes, err := MarshalChain(key, certs...)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
@@ -52,14 +63,22 @@ func LoadOrGenCA() (*x509.Certificate, crypto.Signer, error) {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
return cert, key, nil
|
||||
return certs, key, nil
|
||||
}
|
||||
|
||||
func loadCA() (*x509.Certificate, crypto.Signer, error) {
|
||||
return LoadCerts("./certs/ca.pem", "./certs/ca.key")
|
||||
func loadCA() ([]*x509.Certificate, crypto.Signer, error) {
|
||||
return LoadCertsChain("./certs/ca.pem", "./certs/ca.key")
|
||||
}
|
||||
|
||||
func LoadCA(caPem, caKey []byte) (*x509.Certificate, crypto.Signer, error) {
|
||||
chain, signer, err := LoadCAChain(caPem, caKey)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return chain[0], signer, nil
|
||||
}
|
||||
|
||||
func LoadCAChain(caPem, caKey []byte) ([]*x509.Certificate, crypto.Signer, error) {
|
||||
key, err := cert.ParsePrivateKeyPEM(caKey)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
@@ -69,15 +88,24 @@ func LoadCA(caPem, caKey []byte) (*x509.Certificate, crypto.Signer, error) {
|
||||
return nil, nil, fmt.Errorf("key is not a crypto.Signer")
|
||||
}
|
||||
|
||||
cert, err := ParseCertPEM(caPem)
|
||||
certs, err := cert.ParseCertsPEM(caPem)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
return cert, signer, nil
|
||||
return certs, signer, nil
|
||||
}
|
||||
|
||||
// Deprecated: Use LoadCertsChain instead as it supports intermediate CAs
|
||||
func LoadCerts(certFile, keyFile string) (*x509.Certificate, crypto.Signer, error) {
|
||||
chain, signer, err := LoadCertsChain(certFile, keyFile)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return chain[0], signer, err
|
||||
}
|
||||
|
||||
func LoadCertsChain(certFile, keyFile string) ([]*x509.Certificate, crypto.Signer, error) {
|
||||
caPem, err := ioutil.ReadFile(certFile)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
@@ -87,5 +115,5 @@ func LoadCerts(certFile, keyFile string) (*x509.Certificate, crypto.Signer, erro
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
return LoadCA(caPem, caKey)
|
||||
return LoadCAChain(caPem, caKey)
|
||||
}
|
||||
|
@@ -10,6 +10,8 @@ import (
|
||||
"math"
|
||||
"math/big"
|
||||
"net"
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -17,7 +19,8 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
CertificateBlockType = "CERTIFICATE"
|
||||
CertificateBlockType = "CERTIFICATE"
|
||||
defaultNewSignedCertExpirationDays = 365
|
||||
)
|
||||
|
||||
func NewSelfSignedCACert(key crypto.Signer, cn string, org ...string) (*x509.Certificate, error) {
|
||||
@@ -40,6 +43,9 @@ func NewSelfSignedCACert(key crypto.Signer, cn string, org ...string) (*x509.Cer
|
||||
return nil, err
|
||||
}
|
||||
|
||||
logrus.Infof("generated self-signed CA certificate %s: notBefore=%s notAfter=%s",
|
||||
tmpl.Subject, tmpl.NotBefore, tmpl.NotAfter)
|
||||
|
||||
return x509.ParseCertificate(certDERBytes)
|
||||
}
|
||||
|
||||
@@ -82,12 +88,22 @@ func NewSignedCert(signer crypto.Signer, caCert *x509.Certificate, caKey crypto.
|
||||
return nil, err
|
||||
}
|
||||
|
||||
expirationDays := defaultNewSignedCertExpirationDays
|
||||
envExpirationDays := os.Getenv("CATTLE_NEW_SIGNED_CERT_EXPIRATION_DAYS")
|
||||
if envExpirationDays != "" {
|
||||
if envExpirationDaysInt, err := strconv.Atoi(envExpirationDays); err != nil {
|
||||
logrus.Infof("[NewSignedCert] expiration days from ENV (%s) could not be converted to int (falling back to default value: %d)", envExpirationDays, defaultNewSignedCertExpirationDays)
|
||||
} else {
|
||||
expirationDays = envExpirationDaysInt
|
||||
}
|
||||
}
|
||||
|
||||
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(),
|
||||
NotAfter: time.Now().Add(time.Hour * 24 * time.Duration(expirationDays)).UTC(),
|
||||
NotBefore: caCert.NotBefore,
|
||||
SerialNumber: serialNumber,
|
||||
Subject: pkix.Name{
|
||||
|
168
factory/gen.go
168
factory/gen.go
@@ -6,13 +6,16 @@ import (
|
||||
"crypto/elliptic"
|
||||
"crypto/rand"
|
||||
"crypto/sha1"
|
||||
"crypto/sha256"
|
||||
"crypto/x509"
|
||||
"encoding/hex"
|
||||
"encoding/pem"
|
||||
"fmt"
|
||||
"net"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/rancher/dynamiclistener/cert"
|
||||
"github.com/sirupsen/logrus"
|
||||
@@ -26,15 +29,16 @@ const (
|
||||
)
|
||||
|
||||
var (
|
||||
cnRegexp = regexp.MustCompile("^([A-Za-z0-9][-A-Za-z0-9_.]*)?[A-Za-z0-9]$")
|
||||
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
|
||||
CACert []*x509.Certificate
|
||||
CAKey crypto.Signer
|
||||
CN string
|
||||
Organization []string
|
||||
FilterCN func(...string) []string
|
||||
ExpirationDaysCheck int
|
||||
}
|
||||
|
||||
func cns(secret *v1.Secret) (cns []string) {
|
||||
@@ -68,20 +72,42 @@ func collectCNs(secret *v1.Secret) (domains []string, ips []net.IP, err error) {
|
||||
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.
|
||||
// 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 is not the same as the target Secret. Secrets with expired
|
||||
// certificates will never be returned.
|
||||
//
|
||||
// If the merge would not add any CNs to the additional Secret, the additional
|
||||
// Secret is returned, to allow for certificate rotation/regeneration.
|
||||
//
|
||||
// If the merge would not add any CNs to the target Secret, the target Secret is
|
||||
// returned; no merging is necessary.
|
||||
//
|
||||
// If neither certificate is acceptable as-is, a new certificate containing
|
||||
// the union of the two lists is generated, using the private key from the
|
||||
// first Secret. The returned Secret will contain the updated cert.
|
||||
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
|
||||
}
|
||||
// static secrets can't be altered, don't bother trying
|
||||
if IsStatic(target) {
|
||||
return target, false, nil
|
||||
}
|
||||
return secret, updated, err
|
||||
|
||||
mergedCNs := append(cns(target), cns(additional)...)
|
||||
|
||||
// if the additional secret already has all the CNs, use it in preference to the
|
||||
// current one. This behavior is required to allow for renewal or regeneration.
|
||||
if !NeedsUpdate(0, additional, mergedCNs...) && !t.IsExpired(additional) {
|
||||
return additional, true, nil
|
||||
}
|
||||
|
||||
// if the target secret already has all the CNs, continue using it. The additional
|
||||
// cert had only a subset of the current CNs, so nothing needs to be added.
|
||||
if !NeedsUpdate(0, target, mergedCNs...) && !t.IsExpired(target) {
|
||||
return target, false, nil
|
||||
}
|
||||
|
||||
// neither cert currently has all the necessary CNs or is unexpired; generate a new one.
|
||||
return t.generateCert(target, mergedCNs...)
|
||||
}
|
||||
|
||||
// Renew returns a copy of the given certificate that has been re-signed
|
||||
@@ -119,12 +145,22 @@ func (t *TLS) AddCN(secret *v1.Secret, cn ...string) (*v1.Secret, bool, error) {
|
||||
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{}
|
||||
}
|
||||
|
||||
if err := t.Verify(secret); err != nil {
|
||||
logrus.Warnf("unable to verify existing certificate: %v - signing operation may change certificate issuer", err)
|
||||
}
|
||||
|
||||
secret = populateCN(secret, cn...)
|
||||
|
||||
privateKey, err := getPrivateKey(secret)
|
||||
@@ -142,7 +178,7 @@ func (t *TLS) generateCert(secret *v1.Secret, cn ...string) (*v1.Secret, bool, e
|
||||
return nil, false, err
|
||||
}
|
||||
|
||||
certBytes, keyBytes, err := Marshal(newCert, privateKey)
|
||||
keyBytes, certBytes, err := MarshalChain(privateKey, append([]*x509.Certificate{newCert}, t.CACert...)...)
|
||||
if err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
@@ -150,6 +186,7 @@ func (t *TLS) generateCert(secret *v1.Secret, cn ...string) (*v1.Secret, bool, e
|
||||
if secret.Data == nil {
|
||||
secret.Data = map[string][]byte{}
|
||||
}
|
||||
secret.Type = v1.SecretTypeTLS
|
||||
secret.Data[v1.TLSCertKey] = certBytes
|
||||
secret.Data[v1.TLSPrivateKeyKey] = keyBytes
|
||||
secret.Annotations[fingerprint] = fmt.Sprintf("SHA1=%X", sha1.Sum(newCert.Raw))
|
||||
@@ -157,8 +194,48 @@ func (t *TLS) generateCert(secret *v1.Secret, cn ...string) (*v1.Secret, bool, e
|
||||
return secret, true, nil
|
||||
}
|
||||
|
||||
func (t *TLS) IsExpired(secret *v1.Secret) bool {
|
||||
certsPem := secret.Data[v1.TLSCertKey]
|
||||
if len(certsPem) == 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
certificates, err := cert.ParseCertsPEM(certsPem)
|
||||
if err != nil || len(certificates) == 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
expirationDays := time.Duration(t.ExpirationDaysCheck) * time.Hour * 24
|
||||
return time.Now().Add(expirationDays).After(certificates[0].NotAfter)
|
||||
}
|
||||
|
||||
func (t *TLS) Verify(secret *v1.Secret) error {
|
||||
certsPem := secret.Data[v1.TLSCertKey]
|
||||
if len(certsPem) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
certificates, err := cert.ParseCertsPEM(certsPem)
|
||||
if err != nil || len(certificates) == 0 {
|
||||
return err
|
||||
}
|
||||
|
||||
verifyOpts := x509.VerifyOptions{
|
||||
Roots: x509.NewCertPool(),
|
||||
KeyUsages: []x509.ExtKeyUsage{
|
||||
x509.ExtKeyUsageAny,
|
||||
},
|
||||
}
|
||||
for _, c := range t.CACert {
|
||||
verifyOpts.Roots.AddCert(c)
|
||||
}
|
||||
|
||||
_, err = certificates[0].Verify(verifyOpts)
|
||||
return err
|
||||
}
|
||||
|
||||
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)
|
||||
return NewSignedCert(privateKey, t.CACert[0], t.CAKey, t.CN, t.Organization, domains, ips)
|
||||
}
|
||||
|
||||
func populateCN(secret *v1.Secret, cn ...string) *v1.Secret {
|
||||
@@ -168,7 +245,7 @@ func populateCN(secret *v1.Secret, cn ...string) *v1.Secret {
|
||||
}
|
||||
for _, cn := range cn {
|
||||
if cnRegexp.MatchString(cn) {
|
||||
secret.Annotations[cnPrefix+cn] = cn
|
||||
secret.Annotations[getAnnotationKey(cn)] = cn
|
||||
} else {
|
||||
logrus.Errorf("dropping invalid CN: %s", cn)
|
||||
}
|
||||
@@ -179,7 +256,10 @@ func populateCN(secret *v1.Secret, cn ...string) *v1.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"
|
||||
if secret != nil && secret.Annotations != nil {
|
||||
return secret.Annotations[Static] == "true"
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// NeedsUpdate returns true if any of the CNs are not currently present on the
|
||||
@@ -192,7 +272,7 @@ func NeedsUpdate(maxSANs int, secret *v1.Secret, cn ...string) bool {
|
||||
}
|
||||
|
||||
for _, cn := range cn {
|
||||
if secret.Annotations[cnPrefix+cn] == "" {
|
||||
if secret.Annotations[getAnnotationKey(cn)] == "" {
|
||||
if maxSANs > 0 && len(cns(secret)) >= maxSANs {
|
||||
return false
|
||||
}
|
||||
@@ -217,14 +297,33 @@ func getPrivateKey(secret *v1.Secret) (crypto.Signer, error) {
|
||||
return NewPrivateKey()
|
||||
}
|
||||
|
||||
// MarshalChain returns given key and certificates as byte slices.
|
||||
func MarshalChain(privateKey crypto.Signer, certs ...*x509.Certificate) (keyBytes, certChainBytes []byte, err error) {
|
||||
keyBytes, err = cert.MarshalPrivateKeyToPEM(privateKey)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
for _, cert := range certs {
|
||||
if cert != nil {
|
||||
certBlock := pem.Block{
|
||||
Type: CertificateBlockType,
|
||||
Bytes: cert.Raw,
|
||||
}
|
||||
certChainBytes = append(certChainBytes, pem.EncodeToMemory(&certBlock)...)
|
||||
}
|
||||
}
|
||||
return keyBytes, certChainBytes, nil
|
||||
}
|
||||
|
||||
// Marshal returns the given cert and key as byte slices.
|
||||
func Marshal(x509Cert *x509.Certificate, privateKey crypto.Signer) ([]byte, []byte, error) {
|
||||
func Marshal(x509Cert *x509.Certificate, privateKey crypto.Signer) (certBytes, keyBytes []byte, err error) {
|
||||
certBlock := pem.Block{
|
||||
Type: CertificateBlockType,
|
||||
Bytes: x509Cert.Raw,
|
||||
}
|
||||
|
||||
keyBytes, err := cert.MarshalPrivateKeyToPEM(privateKey)
|
||||
keyBytes, err = cert.MarshalPrivateKeyToPEM(privateKey)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
@@ -236,3 +335,22 @@ func Marshal(x509Cert *x509.Certificate, privateKey crypto.Signer) ([]byte, []by
|
||||
func NewPrivateKey() (crypto.Signer, error) {
|
||||
return ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
|
||||
}
|
||||
|
||||
// getAnnotationKey return the key to use for a given CN. IPv4 addresses and short hostnames
|
||||
// are safe to store as-is, but longer hostnames and IPv6 address must be truncated and/or undergo
|
||||
// character replacement in order to be used as an annotation key. If the CN requires modification,
|
||||
// a portion of the SHA256 sum of the original value is used as the suffix, to reduce the likelihood
|
||||
// of collisions in modified values.
|
||||
func getAnnotationKey(cn string) string {
|
||||
cn = cnPrefix + cn
|
||||
cnLen := len(cn)
|
||||
if cnLen < 64 && !strings.ContainsRune(cn, ':') {
|
||||
return cn
|
||||
}
|
||||
digest := sha256.Sum256([]byte(cn))
|
||||
cn = strings.ReplaceAll(cn, ":", "_")
|
||||
if cnLen > 56 {
|
||||
cnLen = 56
|
||||
}
|
||||
return cn[0:cnLen] + "-" + hex.EncodeToString(digest[0:])[0:6]
|
||||
}
|
||||
|
66
go.mod
66
go.mod
@@ -1,11 +1,65 @@
|
||||
module github.com/rancher/dynamiclistener
|
||||
|
||||
go 1.12
|
||||
go 1.23.0
|
||||
|
||||
toolchain go1.24.2
|
||||
|
||||
require (
|
||||
github.com/rancher/wrangler v0.8.0
|
||||
github.com/sirupsen/logrus v1.4.2
|
||||
golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2
|
||||
k8s.io/api v0.18.8
|
||||
k8s.io/apimachinery v0.18.8
|
||||
github.com/rancher/wrangler v1.1.2
|
||||
github.com/sirupsen/logrus v1.9.3
|
||||
github.com/stretchr/testify v1.10.0
|
||||
golang.org/x/crypto v0.37.0
|
||||
k8s.io/api v0.27.4
|
||||
k8s.io/apimachinery v0.27.4
|
||||
k8s.io/client-go v0.27.4
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.1.2 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/emicklei/go-restful/v3 v3.9.0 // indirect
|
||||
github.com/evanphx/json-patch v4.12.0+incompatible // indirect
|
||||
github.com/go-logr/logr v1.2.3 // indirect
|
||||
github.com/go-openapi/jsonpointer v0.19.6 // indirect
|
||||
github.com/go-openapi/jsonreference v0.20.1 // indirect
|
||||
github.com/go-openapi/swag v0.22.3 // indirect
|
||||
github.com/gogo/protobuf v1.3.2 // indirect
|
||||
github.com/golang/protobuf v1.5.3 // indirect
|
||||
github.com/google/gnostic v0.5.7-v3refs // indirect
|
||||
github.com/google/go-cmp v0.5.9 // indirect
|
||||
github.com/google/gofuzz v1.2.0 // indirect
|
||||
github.com/google/uuid v1.3.0 // indirect
|
||||
github.com/josharian/intern v1.0.0 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/mailru/easyjson v0.7.7 // indirect
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.2 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/prometheus/client_golang v1.14.0 // indirect
|
||||
github.com/prometheus/client_model v0.3.0 // indirect
|
||||
github.com/prometheus/common v0.37.0 // indirect
|
||||
github.com/prometheus/procfs v0.8.0 // indirect
|
||||
github.com/rancher/lasso v0.0.0-20230830164424-d684fdeb6f29 // indirect
|
||||
golang.org/x/net v0.21.0 // indirect
|
||||
golang.org/x/oauth2 v0.10.0 // indirect
|
||||
golang.org/x/sync v0.13.0 // indirect
|
||||
golang.org/x/sys v0.32.0 // indirect
|
||||
golang.org/x/term v0.31.0 // indirect
|
||||
golang.org/x/text v0.24.0 // indirect
|
||||
golang.org/x/time v0.3.0 // indirect
|
||||
google.golang.org/appengine v1.6.7 // indirect
|
||||
google.golang.org/protobuf v1.31.0 // indirect
|
||||
gopkg.in/inf.v0 v0.9.1 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
k8s.io/klog/v2 v2.90.1 // indirect
|
||||
k8s.io/kube-openapi v0.0.0-20230501164219-8b0f38b5fd1f // indirect
|
||||
k8s.io/utils v0.0.0-20230209194617-a36077c30491 // indirect
|
||||
sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect
|
||||
sigs.k8s.io/structured-merge-diff/v4 v4.2.3 // indirect
|
||||
sigs.k8s.io/yaml v1.3.0 // indirect
|
||||
)
|
||||
|
137
listener.go
137
listener.go
@@ -27,13 +27,19 @@ type TLSFactory interface {
|
||||
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)
|
||||
}
|
||||
|
||||
// Deprecated: Use NewListenerWithChain instead as it supports intermediate CAs
|
||||
func NewListener(l net.Listener, storage TLSStorage, caCert *x509.Certificate, caKey crypto.Signer, config Config) (net.Listener, http.Handler, error) {
|
||||
return NewListenerWithChain(l, storage, []*x509.Certificate{caCert}, caKey, config)
|
||||
}
|
||||
|
||||
func NewListenerWithChain(l net.Listener, storage TLSStorage, caCert []*x509.Certificate, caKey crypto.Signer, config Config) (net.Listener, http.Handler, error) {
|
||||
if config.CN == "" {
|
||||
config.CN = "dynamic"
|
||||
}
|
||||
@@ -43,16 +49,21 @@ func NewListener(l net.Listener, storage TLSStorage, caCert *x509.Certificate, c
|
||||
if config.TLSConfig == nil {
|
||||
config.TLSConfig = &tls.Config{}
|
||||
}
|
||||
if config.ExpirationDaysCheck == 0 {
|
||||
config.ExpirationDaysCheck = 90
|
||||
}
|
||||
|
||||
dynamicListener := &listener{
|
||||
factory: &factory.TLS{
|
||||
CACert: caCert,
|
||||
CAKey: caKey,
|
||||
CN: config.CN,
|
||||
Organization: config.Organization,
|
||||
FilterCN: allowDefaultSANs(config.SANs, config.FilterCN),
|
||||
CACert: caCert,
|
||||
CAKey: caKey,
|
||||
CN: config.CN,
|
||||
Organization: config.Organization,
|
||||
FilterCN: allowDefaultSANs(config.SANs, config.FilterCN),
|
||||
ExpirationDaysCheck: config.ExpirationDaysCheck,
|
||||
},
|
||||
Listener: l,
|
||||
certReady: make(chan struct{}),
|
||||
storage: &nonNil{storage: storage},
|
||||
sans: config.SANs,
|
||||
maxSANs: config.MaxSANs,
|
||||
@@ -74,11 +85,14 @@ func NewListener(l net.Listener, storage TLSStorage, caCert *x509.Certificate, c
|
||||
setter.SetFactory(dynamicListener.factory)
|
||||
}
|
||||
|
||||
if config.ExpirationDaysCheck == 0 {
|
||||
config.ExpirationDaysCheck = 30
|
||||
if config.RegenerateCerts != nil && config.RegenerateCerts() {
|
||||
if err := dynamicListener.regenerateCerts(); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
}
|
||||
|
||||
tlsListener := tls.NewListener(dynamicListener.WrapExpiration(config.ExpirationDaysCheck), dynamicListener.tlsConfig)
|
||||
|
||||
return tlsListener, dynamicListener.cacheHandler(), nil
|
||||
}
|
||||
|
||||
@@ -129,6 +143,7 @@ type Config struct {
|
||||
MaxSANs int
|
||||
ExpirationDaysCheck int
|
||||
CloseConnOnCertChange bool
|
||||
RegenerateCerts func() bool
|
||||
FilterCN func(...string) []string
|
||||
}
|
||||
|
||||
@@ -145,6 +160,7 @@ type listener struct {
|
||||
version string
|
||||
tlsConfig *tls.Config
|
||||
cert *tls.Certificate
|
||||
certReady chan struct{}
|
||||
sans []string
|
||||
maxSANs int
|
||||
init sync.Once
|
||||
@@ -153,12 +169,18 @@ type listener struct {
|
||||
func (l *listener) WrapExpiration(days int) net.Listener {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
go func() {
|
||||
time.Sleep(30 * time.Second)
|
||||
|
||||
// wait for cert to be set, this will unblock when the channel is closed
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-l.certReady:
|
||||
}
|
||||
|
||||
for {
|
||||
wait := 6 * time.Hour
|
||||
if err := l.checkExpiration(days); err != nil {
|
||||
logrus.Errorf("failed to check and renew dynamic cert: %v", err)
|
||||
logrus.Errorf("dynamiclistener %s: failed to check and renew dynamic cert: %v", l.Addr(), 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.
|
||||
@@ -180,6 +202,30 @@ func (l *listener) WrapExpiration(days int) net.Listener {
|
||||
}
|
||||
}
|
||||
|
||||
// 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()
|
||||
@@ -225,7 +271,19 @@ func (l *listener) checkExpiration(days int) error {
|
||||
func (l *listener) Accept() (net.Conn, error) {
|
||||
l.init.Do(func() {
|
||||
if len(l.sans) > 0 {
|
||||
l.updateCert(l.sans...)
|
||||
if err := l.updateCert(l.sans...); err != nil {
|
||||
logrus.Errorf("dynamiclistener %s: failed to update cert with configured SANs: %v", l.Addr(), err)
|
||||
return
|
||||
}
|
||||
}
|
||||
if cert, err := l.loadCert(nil); err != nil {
|
||||
logrus.Errorf("dynamiclistener %s: failed to preload certificate: %v", l.Addr(), err)
|
||||
} else if cert == nil {
|
||||
// This should only occur on the first startup when no SANs are configured in the listener config, in which
|
||||
// case no certificate can be created, as dynamiclistener will not create certificates until at least one IP
|
||||
// or DNS SAN is set. It will also occur when using the Kubernetes storage without a local File cache.
|
||||
// For reliable serving of requests, callers should configure a local cache and/or a default set of SANs.
|
||||
logrus.Warnf("dynamiclistener %s: no cached certificate available for preload - deferring certificate load until storage initialization or first client request", l.Addr())
|
||||
}
|
||||
})
|
||||
|
||||
@@ -241,14 +299,12 @@ func (l *listener) Accept() (net.Conn, error) {
|
||||
|
||||
host, _, err := net.SplitHostPort(addr.String())
|
||||
if err != nil {
|
||||
logrus.Errorf("failed to parse network %s: %v", addr.Network(), err)
|
||||
logrus.Errorf("dynamiclistener %s: failed to parse connection local address %s: %v", l.Addr(), addr, 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 err := l.updateCert(host); err != nil {
|
||||
logrus.Errorf("dynamiclistener %s: failed to update cert with connection local address: %v", l.Addr(), err)
|
||||
}
|
||||
|
||||
if l.conns != nil {
|
||||
@@ -275,8 +331,9 @@ func (l *listener) wrap(conn net.Conn) net.Conn {
|
||||
|
||||
type closeWrapper struct {
|
||||
net.Conn
|
||||
id int
|
||||
l *listener
|
||||
id int
|
||||
l *listener
|
||||
ready bool
|
||||
}
|
||||
|
||||
func (c *closeWrapper) close() error {
|
||||
@@ -291,13 +348,27 @@ func (c *closeWrapper) Close() error {
|
||||
}
|
||||
|
||||
func (l *listener) getCertificate(hello *tls.ClientHelloInfo) (*tls.Certificate, error) {
|
||||
newConn := hello.Conn
|
||||
if hello.ServerName != "" {
|
||||
if err := l.updateCert(hello.ServerName); err != nil {
|
||||
logrus.Errorf("dynamiclistener %s: failed to update cert with TLS ServerName: %v", l.Addr(), err)
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return l.loadCert()
|
||||
connCert, err := l.loadCert(newConn)
|
||||
if connCert != nil && err == nil && newConn != nil && l.conns != nil {
|
||||
// if we were successfully able to load a cert and are closing connections on cert changes, mark newConn ready
|
||||
// this will allow us to close the connection if a future connection forces the cert to re-load
|
||||
wrapper, ok := newConn.(*closeWrapper)
|
||||
if !ok {
|
||||
logrus.Debugf("will not mark non-close wrapper connection from %s to %s as ready", newConn.RemoteAddr(), newConn.LocalAddr())
|
||||
return connCert, err
|
||||
}
|
||||
l.connLock.Lock()
|
||||
l.conns[wrapper.id].ready = true
|
||||
l.connLock.Unlock()
|
||||
}
|
||||
return connCert, err
|
||||
}
|
||||
|
||||
func (l *listener) updateCert(cn ...string) error {
|
||||
@@ -314,7 +385,7 @@ func (l *listener) updateCert(cn ...string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
if !factory.IsStatic(secret) && !factory.NeedsUpdate(l.maxSANs, secret, cn...) {
|
||||
if factory.IsStatic(secret) || !factory.NeedsUpdate(l.maxSANs, secret, cn...) {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -332,14 +403,16 @@ func (l *listener) updateCert(cn ...string) error {
|
||||
if err := l.storage.Update(secret); err != nil {
|
||||
return err
|
||||
}
|
||||
// clear version to force cert reload
|
||||
// Clear version to force cert reload next time loadCert is called by TLSConfig's
|
||||
// GetCertificate hook to provide a certificate for a new connection. Note that this
|
||||
// means the old certificate stays in l.cert until a new connection is made.
|
||||
l.version = ""
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (l *listener) loadCert() (*tls.Certificate, error) {
|
||||
func (l *listener) loadCert(currentConn net.Conn) (*tls.Certificate, error) {
|
||||
l.RLock()
|
||||
defer l.RUnlock()
|
||||
|
||||
@@ -360,6 +433,9 @@ func (l *listener) loadCert() (*tls.Certificate, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !cert.IsValidTLSSecret(secret) {
|
||||
return l.cert, nil
|
||||
}
|
||||
if l.cert != nil && l.version == secret.ResourceVersion && secret.ResourceVersion != "" {
|
||||
return l.cert, nil
|
||||
}
|
||||
@@ -369,16 +445,25 @@ func (l *listener) loadCert() (*tls.Certificate, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// cert has changed, close closeWrapper wrapped connections
|
||||
if l.conns != nil {
|
||||
// cert has changed, close closeWrapper wrapped connections if this isn't the first load
|
||||
if currentConn != nil && l.conns != nil && l.cert != nil {
|
||||
l.connLock.Lock()
|
||||
for _, conn := range l.conns {
|
||||
// Don't close a connection that's in the middle of completing a TLS handshake
|
||||
if !conn.ready {
|
||||
continue
|
||||
}
|
||||
_ = conn.close()
|
||||
}
|
||||
l.connLock.Unlock()
|
||||
}
|
||||
|
||||
// we can only close the ready channel once when the cert is first assigned
|
||||
canClose := l.cert == nil
|
||||
l.cert = &cert
|
||||
if canClose {
|
||||
close(l.certReady)
|
||||
}
|
||||
l.version = secret.ResourceVersion
|
||||
return l.cert, nil
|
||||
}
|
||||
@@ -398,7 +483,9 @@ func (l *listener) cacheHandler() http.Handler {
|
||||
}
|
||||
}
|
||||
|
||||
l.updateCert(h)
|
||||
if err := l.updateCert(h); err != nil {
|
||||
logrus.Errorf("dynamiclistener %s: failed to update cert with HTTP request Host header: %v", l.Addr(), err)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
225
listener_test.go
Normal file
225
listener_test.go
Normal file
@@ -0,0 +1,225 @@
|
||||
package dynamiclistener
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"net"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/rancher/dynamiclistener/factory"
|
||||
"github.com/stretchr/testify/assert"
|
||||
v1 "k8s.io/api/core/v1"
|
||||
apiError "k8s.io/apimachinery/pkg/api/errors"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
)
|
||||
|
||||
func Test_getCertificate(t *testing.T) {
|
||||
beforeKey, beforeCert, err := newCertificate()
|
||||
assert.NoError(t, err, "Error when setting up test - unable to construct before key for test")
|
||||
beforeTLSCert, err := tls.X509KeyPair(beforeCert, beforeKey)
|
||||
assert.NoError(t, err, "Error when setting up test - unable to convert before to tls.Certificate")
|
||||
afterKey, afterCert, err := newCertificate()
|
||||
assert.NoError(t, err, "Error when setting up test - unable to construct after key for test")
|
||||
afterTLSCert, err := tls.X509KeyPair(afterCert, afterKey)
|
||||
assert.NoError(t, err, "Error when setting up test - unable to convert after to tls.Certificate")
|
||||
tests := []struct {
|
||||
// input test vars
|
||||
name string
|
||||
secret *v1.Secret
|
||||
secretErr error
|
||||
cachedCert *tls.Certificate
|
||||
cachedVersion string
|
||||
currentConn *closeWrapper
|
||||
otherConns map[int]*closeWrapper
|
||||
|
||||
// output/result test vars
|
||||
closedConns []int
|
||||
expectedCert *tls.Certificate
|
||||
wantError bool
|
||||
}{
|
||||
{
|
||||
name: "no secret found",
|
||||
secret: nil,
|
||||
secretErr: apiError.NewNotFound(schema.GroupResource{
|
||||
Group: "",
|
||||
Resource: "Secret",
|
||||
}, "testSecret"),
|
||||
currentConn: &closeWrapper{id: 0},
|
||||
otherConns: map[int]*closeWrapper{},
|
||||
|
||||
expectedCert: nil,
|
||||
wantError: true,
|
||||
},
|
||||
{
|
||||
name: "secret found, and is up to date",
|
||||
secret: &v1.Secret{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
ResourceVersion: "1",
|
||||
Name: "testSecret",
|
||||
Namespace: "test",
|
||||
},
|
||||
Data: map[string][]byte{
|
||||
v1.TLSCertKey: beforeCert,
|
||||
v1.TLSPrivateKeyKey: beforeKey,
|
||||
},
|
||||
},
|
||||
cachedVersion: "1",
|
||||
cachedCert: &beforeTLSCert,
|
||||
currentConn: &closeWrapper{id: 0},
|
||||
otherConns: map[int]*closeWrapper{},
|
||||
|
||||
expectedCert: &beforeTLSCert,
|
||||
wantError: false,
|
||||
},
|
||||
{
|
||||
name: "secret found, is not up to date, but k8s secret is not valid",
|
||||
secret: &v1.Secret{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
ResourceVersion: "2",
|
||||
Name: "testSecret",
|
||||
Namespace: "test",
|
||||
},
|
||||
Data: map[string][]byte{
|
||||
v1.TLSPrivateKeyKey: []byte("strawberry"),
|
||||
},
|
||||
},
|
||||
cachedVersion: "1",
|
||||
cachedCert: &beforeTLSCert,
|
||||
currentConn: &closeWrapper{id: 0},
|
||||
otherConns: map[int]*closeWrapper{},
|
||||
|
||||
expectedCert: &beforeTLSCert,
|
||||
wantError: false,
|
||||
},
|
||||
{
|
||||
name: "secret found, but is not up to date",
|
||||
secret: &v1.Secret{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
ResourceVersion: "2",
|
||||
Name: "testSecret",
|
||||
Namespace: "test",
|
||||
},
|
||||
Data: map[string][]byte{
|
||||
v1.TLSCertKey: afterCert,
|
||||
v1.TLSPrivateKeyKey: afterKey,
|
||||
},
|
||||
},
|
||||
cachedVersion: "1",
|
||||
cachedCert: &beforeTLSCert,
|
||||
currentConn: &closeWrapper{id: 0},
|
||||
otherConns: map[int]*closeWrapper{},
|
||||
|
||||
expectedCert: &afterTLSCert,
|
||||
wantError: false,
|
||||
},
|
||||
{
|
||||
name: "secret found, is not up to date, and we have conns using current cert",
|
||||
secret: &v1.Secret{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
ResourceVersion: "2",
|
||||
Name: "testSecret",
|
||||
Namespace: "test",
|
||||
},
|
||||
Data: map[string][]byte{
|
||||
v1.TLSCertKey: afterCert,
|
||||
v1.TLSPrivateKeyKey: afterKey,
|
||||
},
|
||||
},
|
||||
cachedVersion: "1",
|
||||
cachedCert: &beforeTLSCert,
|
||||
currentConn: &closeWrapper{id: 0},
|
||||
otherConns: map[int]*closeWrapper{
|
||||
1: {
|
||||
id: 1,
|
||||
ready: false,
|
||||
Conn: &fakeConn{},
|
||||
},
|
||||
2: {
|
||||
id: 2,
|
||||
ready: true,
|
||||
Conn: &fakeConn{},
|
||||
},
|
||||
},
|
||||
|
||||
closedConns: []int{2},
|
||||
expectedCert: &afterTLSCert,
|
||||
wantError: false,
|
||||
},
|
||||
}
|
||||
for i := range tests {
|
||||
test := tests[i]
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
testConns := test.otherConns
|
||||
if testConns != nil {
|
||||
testConns[test.currentConn.id] = test.currentConn
|
||||
// make sure our conn is listed as one of the current connections
|
||||
}
|
||||
l := listener{
|
||||
cert: test.cachedCert,
|
||||
version: test.cachedVersion,
|
||||
storage: &MockTLSStorage{
|
||||
Secret: test.secret,
|
||||
SecretErr: test.secretErr,
|
||||
},
|
||||
conns: testConns,
|
||||
}
|
||||
for _, conn := range testConns {
|
||||
conn.l = &l
|
||||
}
|
||||
newCert, err := l.getCertificate(&tls.ClientHelloInfo{Conn: test.currentConn})
|
||||
if test.wantError {
|
||||
assert.Errorf(t, err, "expected an error but none was provdied")
|
||||
} else {
|
||||
assert.NoError(t, err, "did not expect an error but got one")
|
||||
}
|
||||
assert.Equal(t, test.expectedCert, newCert, "expected cert did not match actual cert")
|
||||
if test.expectedCert != nil && test.wantError == false && test.currentConn != nil && test.otherConns != nil {
|
||||
assert.True(t, test.currentConn.ready, "expected connection to be ready but it was not")
|
||||
} else {
|
||||
if test.currentConn != nil {
|
||||
assert.False(t, test.currentConn.ready, "did not expect connection to be ready")
|
||||
}
|
||||
}
|
||||
for _, closedConn := range test.closedConns {
|
||||
_, ok := l.conns[closedConn]
|
||||
assert.False(t, ok, "closed conns should not be found")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func newCertificate() ([]byte, []byte, error) {
|
||||
cert, key, err := factory.GenCA()
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
return factory.MarshalChain(key, cert)
|
||||
}
|
||||
|
||||
type MockTLSStorage struct {
|
||||
Secret *v1.Secret
|
||||
SecretErr error
|
||||
}
|
||||
|
||||
func (m *MockTLSStorage) Get() (*v1.Secret, error) {
|
||||
return m.Secret, m.SecretErr
|
||||
}
|
||||
|
||||
func (m *MockTLSStorage) Update(secret *v1.Secret) error {
|
||||
panic("Not implemented")
|
||||
}
|
||||
|
||||
// adapted from k8s.io/apimachinery@v0.18.8/pkg/util.proxy/ugradeaware_test.go
|
||||
type fakeConn struct{}
|
||||
|
||||
func (f *fakeConn) Read([]byte) (int, error) { return 0, nil }
|
||||
func (f *fakeConn) Write([]byte) (int, error) { return 0, nil }
|
||||
func (f *fakeConn) Close() error { return nil }
|
||||
func (fakeConn) LocalAddr() net.Addr { return nil }
|
||||
func (fakeConn) RemoteAddr() net.Addr { return nil }
|
||||
func (fakeConn) SetDeadline(t time.Time) error { return nil }
|
||||
func (fakeConn) SetReadDeadline(t time.Time) error { return nil }
|
||||
func (fakeConn) SetWriteDeadline(t time.Time) error { return nil }
|
@@ -21,6 +21,8 @@ import (
|
||||
)
|
||||
|
||||
type ListenOpts struct {
|
||||
CAChain []*x509.Certificate
|
||||
// Deprecated: Use CAChain instead
|
||||
CA *x509.Certificate
|
||||
CAKey crypto.Signer
|
||||
Storage dynamiclistener.TLSStorage
|
||||
@@ -64,7 +66,10 @@ func ListenAndServe(ctx context.Context, httpsPort, httpPort int, handler http.H
|
||||
}
|
||||
|
||||
tlsServer := http.Server{
|
||||
Handler: handler,
|
||||
Handler: handler,
|
||||
BaseContext: func(listener net.Listener) context.Context {
|
||||
return ctx
|
||||
},
|
||||
ErrorLog: errorLog,
|
||||
}
|
||||
|
||||
@@ -86,6 +91,9 @@ func ListenAndServe(ctx context.Context, httpsPort, httpPort int, handler http.H
|
||||
Addr: fmt.Sprintf("%s:%d", opts.BindHost, httpPort),
|
||||
Handler: handler,
|
||||
ErrorLog: errorLog,
|
||||
BaseContext: func(listener net.Listener) context.Context {
|
||||
return ctx
|
||||
},
|
||||
}
|
||||
go func() {
|
||||
logrus.Infof("Listening on %s:%d", opts.BindHost, httpPort)
|
||||
@@ -126,7 +134,7 @@ func getTLSListener(ctx context.Context, tcp net.Listener, handler http.Handler,
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
listener, dynHandler, err := dynamiclistener.NewListener(tcp, storage, caCert, caKey, opts.TLSListenerConfig)
|
||||
listener, dynHandler, err := dynamiclistener.NewListenerWithChain(tcp, storage, caCert, caKey, opts.TLSListenerConfig)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
@@ -134,13 +142,17 @@ func getTLSListener(ctx context.Context, tcp net.Listener, handler http.Handler,
|
||||
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
|
||||
func getCA(opts ListenOpts) ([]*x509.Certificate, crypto.Signer, error) {
|
||||
if opts.CAKey != nil {
|
||||
if opts.CAChain != nil {
|
||||
return opts.CAChain, opts.CAKey, nil
|
||||
} else if opts.CA != nil {
|
||||
return []*x509.Certificate{opts.CA}, opts.CAKey, nil
|
||||
}
|
||||
}
|
||||
|
||||
if opts.Secrets == nil {
|
||||
return factory.LoadOrGenCA()
|
||||
return factory.LoadOrGenCAChain()
|
||||
}
|
||||
|
||||
if opts.CAName == "" {
|
||||
@@ -155,7 +167,7 @@ func getCA(opts ListenOpts) (*x509.Certificate, crypto.Signer, error) {
|
||||
opts.CANamespace = "kube-system"
|
||||
}
|
||||
|
||||
return kubernetes.LoadOrGenCA(opts.Secrets, opts.CANamespace, opts.CAName)
|
||||
return kubernetes.LoadOrGenCAChain(opts.Secrets, opts.CANamespace, opts.CAName)
|
||||
}
|
||||
|
||||
func newStorage(ctx context.Context, opts ListenOpts) dynamiclistener.TLSStorage {
|
||||
|
@@ -2,9 +2,10 @@ package file
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"github.com/rancher/dynamiclistener"
|
||||
"k8s.io/api/core/v1"
|
||||
"os"
|
||||
|
||||
"github.com/rancher/dynamiclistener"
|
||||
v1 "k8s.io/api/core/v1"
|
||||
)
|
||||
|
||||
func New(file string) dynamiclistener.TLSStorage {
|
||||
|
@@ -11,12 +11,21 @@ import (
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
)
|
||||
|
||||
// Deprecated: Use LoadOrGenCAChain instead as it supports intermediate CAs
|
||||
func LoadOrGenCA(secrets v1controller.SecretClient, namespace, name string) (*x509.Certificate, crypto.Signer, error) {
|
||||
chain, signer, err := LoadOrGenCAChain(secrets, namespace, name)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
return chain[0], signer, err
|
||||
}
|
||||
|
||||
func LoadOrGenCAChain(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])
|
||||
return factory.LoadCAChain(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) {
|
||||
@@ -56,7 +65,7 @@ func createAndStoreClientCert(secrets v1controller.SecretClient, namespace strin
|
||||
return nil, err
|
||||
}
|
||||
|
||||
certPem, keyPem, err := factory.Marshal(cert, key)
|
||||
keyPem, certPem, err := factory.MarshalChain(key, cert, caCert)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@@ -6,6 +6,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/rancher/dynamiclistener"
|
||||
"github.com/rancher/dynamiclistener/cert"
|
||||
"github.com/rancher/wrangler/pkg/generated/controllers/core"
|
||||
v1controller "github.com/rancher/wrangler/pkg/generated/controllers/core/v1"
|
||||
"github.com/rancher/wrangler/pkg/start"
|
||||
@@ -13,6 +14,8 @@ import (
|
||||
v1 "k8s.io/api/core/v1"
|
||||
"k8s.io/apimachinery/pkg/api/errors"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/util/wait"
|
||||
"k8s.io/client-go/util/retry"
|
||||
)
|
||||
|
||||
type CoreGetter func() *core.Factory
|
||||
@@ -23,6 +26,7 @@ func Load(ctx context.Context, secrets v1controller.SecretController, namespace,
|
||||
namespace: namespace,
|
||||
storage: backing,
|
||||
ctx: ctx,
|
||||
initSync: &sync.Once{},
|
||||
}
|
||||
storage.init(secrets)
|
||||
return storage
|
||||
@@ -34,40 +38,38 @@ func New(ctx context.Context, core CoreGetter, namespace, name string, backing d
|
||||
namespace: namespace,
|
||||
storage: backing,
|
||||
ctx: ctx,
|
||||
initSync: &sync.Once{},
|
||||
}
|
||||
|
||||
// lazy init
|
||||
go func() {
|
||||
for {
|
||||
core := core()
|
||||
if core != nil {
|
||||
storage.init(core.Core().V1().Secret())
|
||||
start.All(ctx, 5, core)
|
||||
return
|
||||
wait.PollImmediateUntilWithContext(ctx, time.Second, func(cxt context.Context) (bool, error) {
|
||||
if coreFactory := core(); coreFactory != nil {
|
||||
storage.init(coreFactory.Core().V1().Secret())
|
||||
return true, start.All(ctx, 5, coreFactory)
|
||||
}
|
||||
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-time.After(time.Second):
|
||||
}
|
||||
}
|
||||
return false, nil
|
||||
})
|
||||
}()
|
||||
|
||||
return storage
|
||||
}
|
||||
|
||||
type storage struct {
|
||||
sync.Mutex
|
||||
sync.RWMutex
|
||||
|
||||
namespace, name string
|
||||
storage dynamiclistener.TLSStorage
|
||||
secrets v1controller.SecretClient
|
||||
secrets v1controller.SecretController
|
||||
ctx context.Context
|
||||
tls dynamiclistener.TLSFactory
|
||||
initialized bool
|
||||
initSync *sync.Once
|
||||
}
|
||||
|
||||
func (s *storage) SetFactory(tls dynamiclistener.TLSFactory) {
|
||||
s.Lock()
|
||||
defer s.Unlock()
|
||||
s.tls = tls
|
||||
}
|
||||
|
||||
@@ -89,30 +91,64 @@ func (s *storage) init(secrets v1controller.SecretController) {
|
||||
})
|
||||
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,
|
||||
})
|
||||
// Asynchronously sync the backing storage to the Kubernetes secret, as doing so inline may
|
||||
// block the listener from accepting new connections if the apiserver becomes unavailable
|
||||
// after the Secrets controller has been initialized. We're not passing around any contexts
|
||||
// here, nor does the controller accept any, so there's no good way to soft-fail with a
|
||||
// reasonable timeout.
|
||||
go s.syncStorage()
|
||||
}
|
||||
|
||||
func (s *storage) syncStorage() {
|
||||
var updateStorage bool
|
||||
secret, err := s.Get()
|
||||
if err == nil && cert.IsValidTLSSecret(secret) {
|
||||
// local storage had a cached secret, ensure that it exists in Kubernetes
|
||||
_, err := s.secrets.Create(&v1.Secret{
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: s.name,
|
||||
Namespace: s.namespace,
|
||||
Annotations: secret.Annotations,
|
||||
},
|
||||
Type: v1.SecretTypeTLS,
|
||||
Data: secret.Data,
|
||||
})
|
||||
if err != nil && !errors.IsAlreadyExists(err) {
|
||||
logrus.Warnf("Failed to create Kubernetes secret: %v", err)
|
||||
}
|
||||
} else {
|
||||
// local storage was empty, try to populate it
|
||||
secret, err = s.secrets.Get(s.namespace, s.name, metav1.GetOptions{})
|
||||
if err != nil {
|
||||
if !errors.IsNotFound(err) {
|
||||
logrus.Warnf("Failed to init Kubernetes secret: %v", err)
|
||||
}
|
||||
} else {
|
||||
updateStorage = true
|
||||
}
|
||||
}
|
||||
|
||||
s.Lock()
|
||||
defer s.Unlock()
|
||||
s.initialized = true
|
||||
if updateStorage {
|
||||
if err := s.storage.Update(secret); err != nil {
|
||||
logrus.Warnf("Failed to init backing storage secret: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *storage) Get() (*v1.Secret, error) {
|
||||
s.Lock()
|
||||
defer s.Unlock()
|
||||
s.RLock()
|
||||
defer s.RUnlock()
|
||||
|
||||
return s.storage.Get()
|
||||
}
|
||||
|
||||
func (s *storage) targetSecret() (*v1.Secret, error) {
|
||||
s.RLock()
|
||||
defer s.RUnlock()
|
||||
|
||||
existingSecret, err := s.secrets.Get(s.namespace, s.name, metav1.GetOptions{})
|
||||
if errors.IsNotFound(err) {
|
||||
return &v1.Secret{
|
||||
@@ -120,33 +156,62 @@ func (s *storage) targetSecret() (*v1.Secret, error) {
|
||||
Name: s.name,
|
||||
Namespace: s.namespace,
|
||||
},
|
||||
Type: v1.SecretTypeTLS,
|
||||
}, nil
|
||||
}
|
||||
return existingSecret, err
|
||||
}
|
||||
|
||||
func (s *storage) saveInK8s(secret *v1.Secret) (*v1.Secret, error) {
|
||||
if s.secrets == nil {
|
||||
if !s.initComplete() {
|
||||
// Start a goroutine to attempt to save the secret later, once init is complete.
|
||||
// If this was already handled by initComplete, it should be a no-op, or at worst get
|
||||
// merged with the Kubernetes secret.
|
||||
go s.initSync.Do(func() {
|
||||
if err := wait.Poll(100*time.Millisecond, 15*time.Minute, func() (bool, error) {
|
||||
if !s.initComplete() {
|
||||
return false, nil
|
||||
}
|
||||
_, err := s.saveInK8s(secret)
|
||||
return true, err
|
||||
}); err != nil {
|
||||
logrus.Errorf("Failed to save TLS secret after controller init: %v", err)
|
||||
}
|
||||
})
|
||||
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
|
||||
// if we don't have a TLS factory we can't create certs, so don't bother trying to merge anything,
|
||||
// in favor of just blindly replacing the fields on the Kubernetes secret.
|
||||
if s.tls != nil {
|
||||
// merge new secret with secret from backing storage, if one exists
|
||||
if existing, err := s.Get(); err == nil && cert.IsValidTLSSecret(existing) {
|
||||
if newSecret, updated, err := s.tls.Merge(existing, secret); err == nil && updated {
|
||||
secret = newSecret
|
||||
}
|
||||
}
|
||||
|
||||
// merge new secret with existing secret from Kubernetes, if one exists
|
||||
if cert.IsValidTLSSecret(targetSecret) {
|
||||
if newSecret, updated, err := s.tls.Merge(targetSecret, secret); err != nil {
|
||||
return nil, err
|
||||
} else if !updated {
|
||||
return newSecret, nil
|
||||
} else {
|
||||
secret = newSecret
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ensure that the merged secret actually contains data before overwriting the existing fields
|
||||
if !cert.IsValidTLSSecret(secret) {
|
||||
logrus.Warnf("Skipping save of TLS secret for %s/%s due to missing certificate data", secret.Namespace, secret.Name)
|
||||
return targetSecret, nil
|
||||
}
|
||||
|
||||
targetSecret.Annotations = secret.Annotations
|
||||
@@ -154,31 +219,49 @@ func (s *storage) saveInK8s(secret *v1.Secret) (*v1.Secret, error) {
|
||||
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)
|
||||
logrus.Infof("Creating new TLS secret for %s/%s (count: %d): %v", targetSecret.Namespace, 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)
|
||||
logrus.Infof("Updating TLS secret for %s/%s (count: %d): %v", targetSecret.Namespace, 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
|
||||
func (s *storage) Update(secret *v1.Secret) error {
|
||||
// Asynchronously update the Kubernetes secret, as doing so inline may block the listener from
|
||||
// accepting new connections if the apiserver becomes unavailable after the Secrets controller
|
||||
// has been initialized. We're not passing around any contexts here, nor does the controller
|
||||
// accept any, so there's no good way to soft-fail with a reasonable timeout.
|
||||
go func() {
|
||||
if err := s.update(secret); err != nil {
|
||||
logrus.Errorf("Failed to save TLS secret for %s/%s: %v", secret.Namespace, secret.Name, err)
|
||||
}
|
||||
break
|
||||
}
|
||||
}()
|
||||
return nil
|
||||
}
|
||||
|
||||
func isConflictOrAlreadyExists(err error) bool {
|
||||
return errors.IsConflict(err) || errors.IsAlreadyExists(err)
|
||||
}
|
||||
|
||||
func (s *storage) update(secret *v1.Secret) (err error) {
|
||||
var newSecret *v1.Secret
|
||||
err = retry.OnError(retry.DefaultRetry, isConflictOrAlreadyExists, func() error {
|
||||
newSecret, err = s.saveInK8s(secret)
|
||||
return err
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// update underlying storage
|
||||
return s.storage.Update(secret)
|
||||
// Only hold the lock while updating underlying storage
|
||||
s.Lock()
|
||||
defer s.Unlock()
|
||||
return s.storage.Update(newSecret)
|
||||
}
|
||||
|
||||
func (s *storage) initComplete() bool {
|
||||
s.RLock()
|
||||
defer s.RUnlock()
|
||||
return s.initialized
|
||||
}
|
||||
|
@@ -39,7 +39,7 @@ func (m *memory) Update(secret *v1.Secret) error {
|
||||
}
|
||||
}
|
||||
|
||||
logrus.Infof("Active TLS secret %s (ver=%s) (count %d): %v", secret.Name, secret.ResourceVersion, len(secret.Annotations)-1, secret.Annotations)
|
||||
logrus.Infof("Active TLS secret %s/%s (ver=%s) (count %d): %v", secret.Namespace, secret.Name, secret.ResourceVersion, len(secret.Annotations)-1, secret.Annotations)
|
||||
m.secret = secret
|
||||
}
|
||||
return nil
|
||||
|
Reference in New Issue
Block a user