Compare commits

..

2 Commits

Author SHA1 Message Date
Hussein Galal
715d540640 Merge pull request #52 from galal-hussein/add_cert_rotation_to_v0.23
[v0.2.3-patch] Add ability to force cert regeneration
2021-12-03 22:14:42 +02:00
Brian Downs
11104ec0ea Add ability to force cert regeneration (#43)
* add ability to force cert regeneration
2021-12-03 21:18:17 +02:00
25 changed files with 320 additions and 1487 deletions

32
.github/renovate.json vendored
View File

@@ -1,32 +0,0 @@
{
"extends": [
"github>rancher/renovate-config#release"
],
"baseBranchPatterns": [
"main",
"release/v0.3",
"release/v0.4",
"release/v0.5"
],
"prHourlyLimit": 2,
"packageRules": [
{
"enabled": false,
"matchPackageNames": [
"/k8s.io/*/",
"/sigs.k8s.io/*/",
"/github.com/prometheus/*/"
]
},
{
"matchUpdateTypes": [
"major",
"minor"
],
"enabled": false,
"matchPackageNames": [
"/github.com/rancher/wrangler/*/"
]
}
]
}

View File

@@ -1,20 +0,0 @@
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/VERSION
uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- name: Install Go
# https://github.com/actions/setup-go/releases/tag/VERSION
uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0
with:
go-version-file: 'go.mod'
- run: go test -v -race -cover ./...

View File

@@ -1,25 +0,0 @@
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 }}

View File

@@ -1,29 +0,0 @@
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 * * *'
permissions:
contents: read
id-token: write
jobs:
call-workflow:
uses: rancher/renovate-config/.github/workflows/renovate-vault.yml@release
with:
logLevel: ${{ inputs.logLevel || 'info' }}
overrideSchedule: ${{ github.event.inputs.overrideSchedule == 'true' && '{''schedule'':null}' || '' }}
secrets: inherit

View File

@@ -1 +0,0 @@
* @rancher/rancher-squad-frameworks

View File

@@ -1,16 +0,0 @@
# [dynamiclistener](https://github.com/rancher/dynamiclistener)
DynamicListener allows you to setup a server with automatically generated (and re-generated) TLS certs with kubernetes secrets integration.
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).
# Versioning
See [VERSION.md](VERSION.md).

View File

@@ -1,11 +0,0 @@
DynamicListener follows a pre-release (v0.x) strategy of semver. There is limited compatibility between releases, though we do aim to avoid breaking changes on minor version lines. DynamicListener aims to support a limited set of Kubernetes minor versions (all values below are inclusive in start and end). The current supported versions of DynamicListener are as follows:
The current supported release lines are:
| DynamicListener Branch | DynamicListener Minor version | Kubernetes Version Range | Wrangler Version |
|------------------------|-------------------------------|--------------------------|------------------------------------------------|
| main | v0.7 | v1.27+ | v3 |
| release/v0.6 | v0.6 | v1.27 - v1.32 | v3 |
| release/v0.5 | v0.5 | v1.26 - v1.30 | v3 |
| release/v0.4 | v0.4 | v1.25 - v1.28 | v2 |
| release/v0.3 | v0.3 | v1.23 - v1.27 | v2 |

View File

@@ -33,9 +33,7 @@ import (
"math"
"math/big"
"net"
"os"
"path/filepath"
"strconv"
"strings"
"time"
@@ -47,15 +45,16 @@ 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
@@ -73,15 +72,15 @@ func NewPrivateKey() (*rsa.PrivateKey, error) {
// NewSelfSignedCACert creates a CA certificate
func NewSelfSignedCACert(cfg Config, key crypto.Signer) (*x509.Certificate, error) {
notBefore := CalculateNotBefore(nil)
now := time.Now()
tmpl := x509.Certificate{
SerialNumber: new(big.Int).SetInt64(0),
Subject: pkix.Name{
CommonName: cfg.CommonName,
Organization: cfg.Organization,
},
NotBefore: notBefore,
NotAfter: notBefore.Add(duration365d * 10),
NotBefore: now.UTC(),
NotAfter: now.Add(duration365d * 10).UTC(),
KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign,
BasicConstraintsValid: true,
IsCA: true,
@@ -91,15 +90,10 @@ 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 based
// on the given configuration.
// NewSignedCert creates a signed certificate using the given CA certificate and key
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 {
@@ -111,21 +105,7 @@ 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)
}
}
}
notBefore := CalculateNotBefore(caCert)
certTmpl := x509.Certificate{
Subject: pkix.Name{
CommonName: cfg.CommonName,
@@ -134,8 +114,8 @@ func NewSignedCert(cfg Config, key crypto.Signer, caCert *x509.Certificate, caKe
DNSNames: cfg.AltNames.DNSNames,
IPAddresses: cfg.AltNames.IPs,
SerialNumber: serial,
NotBefore: notBefore,
NotAfter: notBefore.Add(expiresAt),
NotBefore: caCert.NotBefore,
NotAfter: time.Now().Add(duration365d).UTC(),
KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
ExtKeyUsage: cfg.Usages,
}
@@ -187,8 +167,8 @@ func GenerateSelfSignedCertKey(host string, alternateIPs []net.IP, alternateDNS
// <host>_<ip>-<ip>_<alternateDNS>-<alternateDNS>.key
// Certs/keys not existing in that directory are created.
func GenerateSelfSignedCertKeyWithFixtures(host string, alternateIPs []net.IP, alternateDNS []string, fixtureDirectory string) ([]byte, []byte, error) {
notBefore := CalculateNotBefore(nil)
maxAge := time.Hour * 24 * 365 // one year self-signed certs
validFrom := time.Now().Add(-time.Hour) // valid an hour earlier to avoid flakes due to clock skew
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 := filepath.Join(fixtureDirectory, baseName+".crt")
@@ -215,8 +195,8 @@ func GenerateSelfSignedCertKeyWithFixtures(host string, alternateIPs []net.IP, a
Subject: pkix.Name{
CommonName: fmt.Sprintf("%s-ca@%d", host, time.Now().Unix()),
},
NotBefore: notBefore,
NotAfter: notBefore.Add(maxAge),
NotBefore: validFrom,
NotAfter: validFrom.Add(maxAge),
KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign,
BasicConstraintsValid: true,
@@ -243,8 +223,8 @@ func GenerateSelfSignedCertKeyWithFixtures(host string, alternateIPs []net.IP, a
Subject: pkix.Name{
CommonName: fmt.Sprintf("%s@%d", host, time.Now().Unix()),
},
NotBefore: notBefore,
NotAfter: notBefore.Add(maxAge),
NotBefore: validFrom,
NotAfter: validFrom.Add(maxAge),
KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},

View File

@@ -1,16 +0,0 @@
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
}

View File

@@ -1,39 +0,0 @@
/*
Copyright 2014 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package cert
import (
"crypto/x509"
"time"
clockutil "k8s.io/utils/clock"
)
var clock clockutil.PassiveClock = &clockutil.RealClock{}
// CalculateNotBefore calculates a NotBefore time of 1 hour in the past, or the
// NotBefore time of the optionally provided *x509.Certificate, whichever is greater.
func CalculateNotBefore(ca *x509.Certificate) time.Time {
// Subtract 1 hour for clock skew
now := clock.Now().UTC().Add(-time.Hour)
// It makes no sense to return a time before the CA itself is valid.
if ca != nil && now.Before(ca.NotBefore) {
return ca.NotBefore
}
return now
}

View File

@@ -1,77 +0,0 @@
/*
Copyright 2014 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package cert
import (
"crypto/x509"
"testing"
"time"
clocktest "k8s.io/utils/clock/testing"
)
func TestCalculateNotBefore(t *testing.T) {
baseTime := time.Date(2025, 9, 29, 12, 0, 0, 0, time.UTC)
tests := []struct {
name string
ca *x509.Certificate
now time.Time
expected time.Time
}{
{
name: "nil CA returns 1h ago",
ca: nil,
now: baseTime,
expected: baseTime.Add(-time.Hour),
},
{
name: "CA notBefore before now returns 1h ago",
ca: &x509.Certificate{
NotBefore: baseTime.Add(-2 * time.Hour),
},
now: baseTime,
expected: baseTime.Add(-time.Hour),
},
{
name: "CA notBefore after now returns CA.NotBefore",
ca: &x509.Certificate{
NotBefore: baseTime.Add(2 * time.Hour),
},
now: baseTime,
expected: baseTime.Add(2 * time.Hour),
},
{
name: "CA notBefore equal to now returns now",
ca: &x509.Certificate{
NotBefore: baseTime,
},
now: baseTime,
expected: baseTime,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
clock = clocktest.NewFakePassiveClock(tt.now)
result := CalculateNotBefore(tt.ca)
if !result.Equal(tt.expected) {
t.Errorf("Expected %v, got %v", tt.expected, result)
}
})
}
}

View File

@@ -6,7 +6,6 @@ import (
"fmt"
"io/ioutil"
"os"
"time"
"github.com/rancher/dynamiclistener/cert"
)
@@ -17,7 +16,7 @@ func GenCA() (*x509.Certificate, crypto.Signer, error) {
return nil, nil, err
}
caCert, err := NewSelfSignedCACert(caKey, fmt.Sprintf("dynamiclistener-ca@%d", time.Now().Unix()), "dynamiclistener-org")
caCert, err := NewSelfSignedCACert(caKey, "dynamiclistener-ca", "dynamiclistener-org")
if err != nil {
return nil, nil, err
}
@@ -25,28 +24,18 @@ 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) {
chain, signer, err := LoadOrGenCAChain()
if err != nil {
return nil, nil, err
}
return chain[0], signer, err
}
func LoadOrGenCAChain() ([]*x509.Certificate, crypto.Signer, error) {
certs, key, err := loadCA()
cert, key, err := loadCA()
if err == nil {
return certs, key, nil
return cert, key, nil
}
cert, key, err := GenCA()
cert, key, err = GenCA()
if err != nil {
return nil, nil, err
}
certs = []*x509.Certificate{cert}
certBytes, keyBytes, err := MarshalChain(key, certs...)
certBytes, keyBytes, err := Marshal(cert, key)
if err != nil {
return nil, nil, err
}
@@ -63,22 +52,14 @@ func LoadOrGenCAChain() ([]*x509.Certificate, crypto.Signer, error) {
return nil, nil, err
}
return certs, key, nil
return cert, key, nil
}
func loadCA() ([]*x509.Certificate, crypto.Signer, error) {
return LoadCertsChain("./certs/ca.pem", "./certs/ca.key")
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) {
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
@@ -88,24 +69,15 @@ func LoadCAChain(caPem, caKey []byte) ([]*x509.Certificate, crypto.Signer, error
return nil, nil, fmt.Errorf("key is not a crypto.Signer")
}
certs, err := cert.ParseCertsPEM(caPem)
cert, err := ParseCertPEM(caPem)
if err != nil {
return nil, nil, err
}
return certs, signer, nil
return cert, 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
@@ -115,5 +87,5 @@ func LoadCertsChain(certFile, keyFile string) ([]*x509.Certificate, crypto.Signe
return nil, nil, err
}
return LoadCAChain(caPem, caKey)
return LoadCA(caPem, caKey)
}

View File

@@ -10,28 +10,23 @@ import (
"math"
"math/big"
"net"
"os"
"strconv"
"strings"
"time"
"github.com/rancher/dynamiclistener/cert"
"github.com/sirupsen/logrus"
)
const (
CertificateBlockType = "CERTIFICATE"
defaultNewSignedCertExpirationDays = 365
CertificateBlockType = "CERTIFICATE"
)
func NewSelfSignedCACert(key crypto.Signer, cn string, org ...string) (*x509.Certificate, error) {
notBefore := cert.CalculateNotBefore(nil)
now := time.Now()
tmpl := x509.Certificate{
BasicConstraintsValid: true,
IsCA: true,
KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign,
NotBefore: notBefore,
NotAfter: notBefore.Add(time.Hour * 24 * 365 * 10),
NotAfter: now.Add(time.Hour * 24 * 365 * 10).UTC(),
NotBefore: now.UTC(),
SerialNumber: new(big.Int).SetInt64(0),
Subject: pkix.Name{
CommonName: cn,
@@ -44,9 +39,6 @@ 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)
}
@@ -56,24 +48,17 @@ func NewSignedClientCert(signer crypto.Signer, caCert *x509.Certificate, caKey c
return nil, err
}
notBefore := cert.CalculateNotBefore(caCert)
parent := x509.Certificate{
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth},
KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
NotBefore: notBefore,
NotAfter: notBefore.Add(time.Hour * 24 * 365),
NotAfter: time.Now().Add(time.Hour * 24 * 365).UTC(),
NotBefore: caCert.NotBefore,
SerialNumber: serialNumber,
Subject: pkix.Name{
CommonName: cn,
},
}
parts := strings.Split(cn, ",o=")
if len(parts) > 1 {
parent.Subject.CommonName = parts[0]
parent.Subject.Organization = parts[1:]
}
cert, err := x509.CreateCertificate(rand.Reader, &parent, caCert, signer.Public(), caKey)
if err != nil {
return nil, err
@@ -90,24 +75,13 @@ 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
}
}
notBefore := cert.CalculateNotBefore(caCert)
parent := x509.Certificate{
DNSNames: domains,
ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth},
IPAddresses: ips,
KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature,
NotBefore: notBefore,
NotAfter: notBefore.Add(time.Hour * 24 * time.Duration(expirationDays)),
NotAfter: time.Now().Add(time.Hour * 24 * 365).UTC(),
NotBefore: caCert.NotBefore,
SerialNumber: serialNumber,
Subject: pkix.Name{
CommonName: cn,

View File

@@ -6,16 +6,13 @@ 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"
@@ -25,20 +22,19 @@ import (
const (
cnPrefix = "listener.cattle.io/cn-"
Static = "listener.cattle.io/static"
Fingerprint = "listener.cattle.io/fingerprint"
fingerprint = "listener.cattle.io/fingerprint"
)
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
ExpirationDaysCheck int
CACert *x509.Certificate
CAKey crypto.Signer
CN string
Organization []string
FilterCN func(...string) []string
}
func cns(secret *v1.Secret) (cns []string) {
@@ -72,42 +68,20 @@ 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 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.
// 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) {
// static secrets can't be altered, don't bother trying
if IsStatic(target) {
return target, false, nil
secret, updated, err := t.AddCN(target, cns(additional)...)
if !updated {
if target.Annotations[fingerprint] != additional.Annotations[fingerprint] {
secret = additional
updated = true
}
}
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...)
return secret, updated, err
}
// Renew returns a copy of the given certificate that has been re-signed
@@ -157,10 +131,6 @@ func (t *TLS) generateCert(secret *v1.Secret, cn ...string) (*v1.Secret, bool, e
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)
@@ -178,7 +148,7 @@ func (t *TLS) generateCert(secret *v1.Secret, cn ...string) (*v1.Secret, bool, e
return nil, false, err
}
keyBytes, certBytes, err := MarshalChain(privateKey, append([]*x509.Certificate{newCert}, t.CACert...)...)
certBytes, keyBytes, err := Marshal(newCert, privateKey)
if err != nil {
return nil, false, err
}
@@ -186,60 +156,15 @@ 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))
secret.Annotations[fingerprint] = fmt.Sprintf("SHA1=%X", sha1.Sum(newCert.Raw))
return secret, true, nil
}
func (t *TLS) IsExpired(secret *v1.Secret) bool {
if secret == nil {
return false
}
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[0], t.CAKey, t.CN, t.Organization, domains, ips)
return NewSignedCert(privateKey, t.CACert, t.CAKey, t.CN, t.Organization, domains, ips)
}
func populateCN(secret *v1.Secret, cn ...string) *v1.Secret {
@@ -249,7 +174,7 @@ func populateCN(secret *v1.Secret, cn ...string) *v1.Secret {
}
for _, cn := range cn {
if cnRegexp.MatchString(cn) {
secret.Annotations[getAnnotationKey(cn)] = cn
secret.Annotations[cnPrefix+cn] = cn
} else {
logrus.Errorf("dropping invalid CN: %s", cn)
}
@@ -260,10 +185,7 @@ 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 {
if secret != nil && secret.Annotations != nil {
return secret.Annotations[Static] == "true"
}
return false
return secret.Annotations[Static] == "true"
}
// NeedsUpdate returns true if any of the CNs are not currently present on the
@@ -276,7 +198,7 @@ func NeedsUpdate(maxSANs int, secret *v1.Secret, cn ...string) bool {
}
for _, cn := range cn {
if secret.Annotations[getAnnotationKey(cn)] == "" {
if secret.Annotations[cnPrefix+cn] == "" {
if maxSANs > 0 && len(cns(secret)) >= maxSANs {
return false
}
@@ -301,33 +223,14 @@ 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) (certBytes, keyBytes []byte, err error) {
func Marshal(x509Cert *x509.Certificate, privateKey crypto.Signer) ([]byte, []byte, 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
}
@@ -339,22 +242,3 @@ func Marshal(x509Cert *x509.Certificate, privateKey crypto.Signer) (certBytes, k
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]
}

View File

@@ -1,12 +0,0 @@
package dynamiclistener
func OnlyAllow(str string) func(...string) []string {
return func(s2 ...string) []string {
for _, s := range s2 {
if s == str {
return []string{s}
}
}
return nil
}
}

66
go.mod
View File

@@ -1,64 +1,12 @@
module github.com/rancher/dynamiclistener
go 1.24.0
go 1.12
require (
github.com/rancher/wrangler/v3 v3.3.0-rc.2
github.com/sirupsen/logrus v1.9.3
github.com/stretchr/testify v1.11.1
golang.org/x/crypto v0.42.0
k8s.io/api v0.34.1
k8s.io/apimachinery v0.34.1
k8s.io/client-go v0.34.1
k8s.io/utils v0.0.0-20251002143259-bc988d571ff4
)
require (
github.com/beorn7/perks v1.0.1 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/emicklei/go-restful/v3 v3.12.2 // indirect
github.com/evanphx/json-patch v5.9.11+incompatible // indirect
github.com/fxamacker/cbor/v2 v2.9.0 // indirect
github.com/go-logr/logr v1.4.2 // indirect
github.com/go-openapi/jsonpointer v0.21.0 // indirect
github.com/go-openapi/jsonreference v0.21.0 // indirect
github.com/go-openapi/swag v0.23.0 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/google/gnostic-models v0.7.0 // indirect
github.com/google/go-cmp v0.7.0 // indirect
github.com/google/uuid v1.6.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/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // 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.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/prometheus/client_golang v1.22.0 // indirect
github.com/prometheus/client_model v0.6.1 // indirect
github.com/prometheus/common v0.62.0 // indirect
github.com/prometheus/procfs v0.15.1 // indirect
github.com/rancher/lasso v0.2.5-rc.1 // indirect
github.com/x448/float16 v0.8.4 // indirect
go.yaml.in/yaml/v2 v2.4.2 // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect
golang.org/x/net v0.44.0 // indirect
golang.org/x/oauth2 v0.27.0 // indirect
golang.org/x/sync v0.17.0 // indirect
golang.org/x/sys v0.36.0 // indirect
golang.org/x/term v0.35.0 // indirect
golang.org/x/text v0.29.0 // indirect
golang.org/x/time v0.9.0 // indirect
google.golang.org/protobuf v1.36.8 // indirect
gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
k8s.io/klog/v2 v2.130.1 // indirect
k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b // indirect
sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect
sigs.k8s.io/randfill v1.0.0 // indirect
sigs.k8s.io/structured-merge-diff/v6 v6.3.0 // indirect
sigs.k8s.io/yaml v1.6.0 // indirect
github.com/rancher/wrangler v0.1.4
github.com/rancher/wrangler-api v0.2.0
github.com/sirupsen/logrus v1.4.1
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2
k8s.io/api v0.0.0-20190409021203-6e4e0e4f393b
k8s.io/apimachinery v0.0.0-20190404173353-6a84e37a896d
)

279
go.sum
View File

@@ -1,179 +1,132 @@
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
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/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/emicklei/go-restful/v3 v3.12.2 h1:DhwDP0vY3k8ZzE0RunuJy8GhNpPL6zqLkDf9B/a0/xU=
github.com/emicklei/go-restful/v3 v3.12.2/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc=
github.com/evanphx/json-patch v5.9.11+incompatible h1:ixHHqfcGvxhWkniF1tWxBHA0yb4Z+d1UQi45df52xW8=
github.com/evanphx/json-patch v5.9.11+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk=
github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM=
github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ=
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ=
github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY=
github.com/go-openapi/jsonreference v0.21.0 h1:Rs+Y7hSXT83Jacb7kFyjn4ijOuVGSvOdF2+tg1TRrwQ=
github.com/go-openapi/jsonreference v0.21.0/go.mod h1:LmZmgsrTkVg9LG4EaHeY8cBDslNPMo06cago5JNLkm4=
github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE=
github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ=
github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/google/gnostic-models v0.7.0 h1:qwTtogB15McXDaNqTZdzPJRHvaVJlAl+HVQnLmJEJxo=
github.com/google/gnostic-models v0.7.0/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
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/google/pprof v0.0.0-20241029153458-d1b30febd7db h1:097atOisP2aRj7vFgYQBbFN4U4JNXUNYpxael3UzMyo=
github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
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/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0=
github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
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/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 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee h1:W5t00kpgFdJifH4BDsTlE89Zl93FEloxaWZfGcifgq8=
github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/onsi/ginkgo/v2 v2.21.0 h1:7rg/4f3rB88pb5obDgNZrNHrQ4e6WpjonchcpuBRnZM=
github.com/onsi/ginkgo/v2 v2.21.0/go.mod h1:7Du3c42kxCUegi0IImZ1wUQzMBVecgIHjR1C+NkhLQo=
github.com/onsi/gomega v1.35.1 h1:Cwbd75ZBPxFSuZ6T+rN/WCb/gOc6YgFBXLlZLhC7Ds4=
github.com/onsi/gomega v1.35.1/go.mod h1:PvZbdDc8J6XJEpDK4HCuRBm8a6Fzp9/DmhC9C7yFlog=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
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/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q=
github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0=
github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
github.com/prometheus/common v0.62.0 h1:xasJaQlnWAeyHdUBeGjXmutelfJHWMRr+Fg4QszZ2Io=
github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I=
github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc=
github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk=
github.com/rancher/lasso v0.2.5-rc.1 h1:E88eG3FDYGmccJjGDjv7HrknSaYQz4WNGgJ/gGjmGFc=
github.com/rancher/lasso v0.2.5-rc.1/go.mod h1:71rWfv+KkdSmSxZ9Ly5QYhxAu0nEUcaq9N2ByjcHqAM=
github.com/rancher/wrangler/v3 v3.3.0-rc.2 h1:o/jNO0uQmcAUTxIwu7q3VBTkS82XDh7gb0Ofv0DpVoQ=
github.com/rancher/wrangler/v3 v3.3.0-rc.2/go.mod h1:0RpxDgbQ4Lgzfuy7JPNk5jZfTKJQoCN6qUhlrDgNY9E=
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
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.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
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/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI=
go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU=
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
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-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI=
golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.44.0 h1:evd8IRDyfNBMBTTY5XRF1vaZlD+EmWx6x8PkhR04H/I=
golang.org/x/net v0.44.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY=
golang.org/x/oauth2 v0.27.0 h1:da9Vo7/tDv5RH/7nZDz1eMGS/q1Vv1N/7FCrBhI9I3M=
golang.org/x/oauth2 v0.27.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8=
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/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
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/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k=
golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.35.0 h1:bZBVKBudEyhRcajGcNc3jIfWPqV4y/Kt2XcoigOWtDQ=
golang.org/x/term v0.35.0/go.mod h1:TPGtkTLesOwf2DE8CgVYiZinHAOuy5AYUYT1lENIZnA=
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.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk=
golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4=
golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY=
golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE=
golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.37.0 h1:DVSRzp7FwePZW356yEAChSdNcQo6Nsp+fex1SUW09lE=
golang.org/x/tools v0.37.0/go.mod h1:MBN5QPQtLMHVdvsbtarmTNukZDdgwdwlO5qGacAzF0w=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/protobuf v1.36.8 h1:xHScyCOEuuwZEc6UtSOvPbAT4zRh0xcNRYekJwfqyMc=
google.golang.org/protobuf v1.36.8/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=
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/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/evanphx/json-patch.v4 v4.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSPG+6V4=
gopkg.in/evanphx/json-patch.v4 v4.12.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M=
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.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
k8s.io/api v0.34.1 h1:jC+153630BMdlFukegoEL8E/yT7aLyQkIVuwhmwDgJM=
k8s.io/api v0.34.1/go.mod h1:SB80FxFtXn5/gwzCoN6QCtPD7Vbu5w2n1S0J5gFfTYk=
k8s.io/apimachinery v0.34.1 h1:dTlxFls/eikpJxmAC7MVE8oOeP1zryV7iRyIjB0gky4=
k8s.io/apimachinery v0.34.1/go.mod h1:/GwIlEcWuTX9zKIg2mbw0LRFIsXwrfoVxn+ef0X13lw=
k8s.io/client-go v0.34.1 h1:ZUPJKgXsnKwVwmKKdPfw4tB58+7/Ik3CrjOEhsiZ7mY=
k8s.io/client-go v0.34.1/go.mod h1:kA8v0FP+tk6sZA0yKLRG67LWjqufAoSHA2xVGKw9Of8=
k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk=
k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE=
k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b h1:MloQ9/bdJyIu9lb1PzujOPolHyvO06MXG5TUIj2mNAA=
k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b/go.mod h1:UZ2yyWbFTpuhSbFhv24aGNOdoRdJZgsIObGBUaYVsts=
k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 h1:SjGebBtkBqHFOli+05xYbK8YF1Dzkbzn+gDM4X9T4Ck=
k8s.io/utils v0.0.0-20251002143259-bc988d571ff4/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 h1:gBQPwqORJ8d8/YNZWEjoZs7npUVDpVXUUOFfW6CgAqE=
sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg=
sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU=
sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY=
sigs.k8s.io/structured-merge-diff/v6 v6.3.0 h1:jTijUJbW353oVOd9oTlifJqOGEkUw2jB/fXCbTiQEco=
sigs.k8s.io/structured-merge-diff/v6 v6.3.0/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE=
sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs=
sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4=
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=

View File

@@ -34,12 +34,7 @@ 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"
}
@@ -49,21 +44,16 @@ func NewListenerWithChain(l net.Listener, storage TLSStorage, caCert []*x509.Cer
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),
ExpirationDaysCheck: config.ExpirationDaysCheck,
CACert: caCert,
CAKey: caKey,
CN: config.CN,
Organization: config.Organization,
FilterCN: allowDefaultSANs(config.SANs, config.FilterCN),
},
Listener: l,
certReady: make(chan struct{}),
storage: &nonNil{storage: storage},
sans: config.SANs,
maxSANs: config.MaxSANs,
@@ -91,6 +81,10 @@ func NewListenerWithChain(l net.Listener, storage TLSStorage, caCert []*x509.Cer
}
}
if config.ExpirationDaysCheck == 0 {
config.ExpirationDaysCheck = 30
}
tlsListener := tls.NewListener(dynamicListener.WrapExpiration(config.ExpirationDaysCheck), dynamicListener.tlsConfig)
return tlsListener, dynamicListener.cacheHandler(), nil
@@ -160,7 +154,6 @@ type listener struct {
version string
tlsConfig *tls.Config
cert *tls.Certificate
certReady chan struct{}
sans []string
maxSANs int
init sync.Once
@@ -169,18 +162,12 @@ type listener struct {
func (l *listener) WrapExpiration(days int) net.Listener {
ctx, cancel := context.WithCancel(context.Background())
go func() {
// wait for cert to be set, this will unblock when the channel is closed
select {
case <-ctx.Done():
return
case <-l.certReady:
}
time.Sleep(30 * time.Second)
for {
wait := 6 * time.Hour
if err := l.checkExpiration(days); err != nil {
logrus.Errorf("dynamiclistener %s: failed to check and renew dynamic cert: %v", l.Addr(), err)
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.
@@ -271,19 +258,7 @@ func (l *listener) checkExpiration(days int) error {
func (l *listener) Accept() (net.Conn, error) {
l.init.Do(func() {
if len(l.sans) > 0 {
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())
l.updateCert(l.sans...)
}
})
@@ -299,12 +274,14 @@ func (l *listener) Accept() (net.Conn, error) {
host, _, err := net.SplitHostPort(addr.String())
if err != nil {
logrus.Errorf("dynamiclistener %s: failed to parse connection local address %s: %v", l.Addr(), addr, err)
logrus.Errorf("failed to parse network %s: %v", addr.Network(), err)
return conn, nil
}
if err := l.updateCert(host); err != nil {
logrus.Errorf("dynamiclistener %s: failed to update cert with connection local address: %v", l.Addr(), err)
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 {
@@ -331,9 +308,8 @@ func (l *listener) wrap(conn net.Conn) net.Conn {
type closeWrapper struct {
net.Conn
id int
l *listener
ready bool
id int
l *listener
}
func (c *closeWrapper) close() error {
@@ -348,27 +324,13 @@ 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
}
}
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
return l.loadCert()
}
func (l *listener) updateCert(cn ...string) error {
@@ -385,7 +347,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
}
@@ -403,16 +365,14 @@ func (l *listener) updateCert(cn ...string) error {
if err := l.storage.Update(secret); err != nil {
return err
}
// 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.
// clear version to force cert reload
l.version = ""
}
return nil
}
func (l *listener) loadCert(currentConn net.Conn) (*tls.Certificate, error) {
func (l *listener) loadCert() (*tls.Certificate, error) {
l.RLock()
defer l.RUnlock()
@@ -433,9 +393,6 @@ func (l *listener) loadCert(currentConn net.Conn) (*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
}
@@ -445,25 +402,16 @@ func (l *listener) loadCert(currentConn net.Conn) (*tls.Certificate, error) {
return nil, err
}
// cert has changed, close closeWrapper wrapped connections if this isn't the first load
if currentConn != nil && l.conns != nil && l.cert != nil {
// cert has changed, close closeWrapper wrapped connections
if l.conns != 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
}
@@ -483,9 +431,7 @@ func (l *listener) cacheHandler() http.Handler {
}
}
if err := l.updateCert(h); err != nil {
logrus.Errorf("dynamiclistener %s: failed to update cert with HTTP request Host header: %v", l.Addr(), err)
}
l.updateCert(h)
}
})
}

View File

@@ -1,225 +0,0 @@
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 }

View File

@@ -1,13 +1,11 @@
package server
import (
"bytes"
"context"
"crypto"
"crypto/tls"
"crypto/x509"
"fmt"
"io"
"log"
"net"
"net/http"
@@ -17,14 +15,12 @@ import (
"github.com/rancher/dynamiclistener/storage/file"
"github.com/rancher/dynamiclistener/storage/kubernetes"
"github.com/rancher/dynamiclistener/storage/memory"
v1 "github.com/rancher/wrangler/v3/pkg/generated/controllers/core/v1"
v1 "github.com/rancher/wrangler-api/pkg/generated/controllers/core/v1"
"github.com/sirupsen/logrus"
"golang.org/x/crypto/acme/autocert"
)
type ListenOpts struct {
CAChain []*x509.Certificate
// Deprecated: Use CAChain instead
CA *x509.Certificate
CAKey crypto.Signer
Storage dynamiclistener.TLSStorage
@@ -38,53 +34,20 @@ type ListenOpts struct {
BindHost string
NoRedirect bool
TLSListenerConfig dynamiclistener.Config
// Override legacy behavior where server logs written to the application's logrus object
// were dropped unless logrus was set to debug-level (such as by launching steve with '--debug').
// Setting this to true results in server logs appearing at an ERROR level.
DisplayServerLogs bool
IgnoreTLSHandshakeError bool
}
var TLSHandshakeError = []byte("http: TLS handshake error")
var _ io.Writer = &TLSErrorDebugger{}
type TLSErrorDebugger struct{}
func (t *TLSErrorDebugger) Write(p []byte) (n int, err error) {
p = bytes.TrimSpace(p)
if bytes.HasPrefix(p, TLSHandshakeError) {
logrus.Debug(string(p))
} else {
logrus.Error(string(p))
}
return len(p), err
}
func ListenAndServe(ctx context.Context, httpsPort, httpPort int, handler http.Handler, opts *ListenOpts) error {
logger := logrus.StandardLogger()
writer := logger.WriterLevel(logrus.DebugLevel)
if opts == nil {
opts = &ListenOpts{}
}
if opts.DisplayServerLogs {
writer = logger.WriterLevel(logrus.ErrorLevel)
}
var errorLog *log.Logger
if opts.IgnoreTLSHandshakeError {
debugWriter := &TLSErrorDebugger{}
errorLog = log.New(debugWriter, "", 0)
} else {
// Otherwise preserve legacy behaviour of displaying server logs only in debug mode.
errorLog = log.New(writer, "", 0)
}
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 {
@@ -101,10 +64,7 @@ func ListenAndServe(ctx context.Context, httpsPort, httpPort int, handler http.H
}
tlsServer := http.Server{
Handler: handler,
BaseContext: func(listener net.Listener) context.Context {
return ctx
},
Handler: handler,
ErrorLog: errorLog,
}
@@ -126,9 +86,6 @@ 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)
@@ -169,7 +126,7 @@ func getTLSListener(ctx context.Context, tcp net.Listener, handler http.Handler,
return nil, nil, err
}
listener, dynHandler, err := dynamiclistener.NewListenerWithChain(tcp, storage, caCert, caKey, opts.TLSListenerConfig)
listener, dynHandler, err := dynamiclistener.NewListener(tcp, storage, caCert, caKey, opts.TLSListenerConfig)
if err != nil {
return nil, nil, err
}
@@ -177,17 +134,13 @@ 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.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
}
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.LoadOrGenCAChain()
return factory.LoadOrGenCA()
}
if opts.CAName == "" {
@@ -202,7 +155,7 @@ func getCA(opts ListenOpts) ([]*x509.Certificate, crypto.Signer, error) {
opts.CANamespace = "kube-system"
}
return kubernetes.LoadOrGenCAChain(opts.Secrets, opts.CANamespace, opts.CAName)
return kubernetes.LoadOrGenCA(opts.Secrets, opts.CANamespace, opts.CAName)
}
func newStorage(ctx context.Context, opts ListenOpts) dynamiclistener.TLSStorage {

View File

@@ -1,175 +0,0 @@
package server
import (
"bytes"
"context"
"fmt"
"net"
"net/http"
"sync"
"testing"
"time"
"github.com/sirupsen/logrus"
assertPkg "github.com/stretchr/testify/assert"
)
type alwaysPanicHandler struct {
msg string
}
func (z *alwaysPanicHandler) ServeHTTP(_ http.ResponseWriter, _ *http.Request) {
panic(z.msg)
}
// safeWriter is used to allow writing to a buffer-based log in a web server
// and safely read from it in the client (i.e. this test code)
type safeWriter struct {
writer *bytes.Buffer
mutex *sync.Mutex
}
func newSafeWriter(writer *bytes.Buffer, mutex *sync.Mutex) *safeWriter {
return &safeWriter{writer: writer, mutex: mutex}
}
func (s *safeWriter) Write(p []byte) (n int, err error) {
s.mutex.Lock()
defer s.mutex.Unlock()
return s.writer.Write(p)
}
func TestTLSHandshakeErrorWriter(t *testing.T) {
tests := []struct {
name string
ignoreTLSHandshakeError bool
message []byte
expectedLevel logrus.Level
}{
{
name: "TLS handshake error is logged as debug",
message: []byte("http: TLS handshake error: EOF"),
expectedLevel: logrus.DebugLevel,
},
{
name: "other errors are logged as error",
message: []byte("some other server error"),
expectedLevel: logrus.ErrorLevel,
},
}
var baseLogLevel = logrus.GetLevel()
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
assert := assertPkg.New(t)
var buf bytes.Buffer
logrus.SetOutput(&buf)
logrus.SetLevel(logrus.DebugLevel)
debugger := &TLSErrorDebugger{}
n, err := debugger.Write(tt.message)
assert.Nil(err)
assert.Equal(len(tt.message), n)
logOutput := buf.String()
assert.Contains(logOutput, "level="+tt.expectedLevel.String())
assert.Contains(logOutput, string(tt.message))
})
}
logrus.SetLevel(baseLogLevel)
}
func TestHttpServerLogWithLogrus(t *testing.T) {
assert := assertPkg.New(t)
message := "debug-level writer"
msg := fmt.Sprintf("panicking context: %s", message)
var buf bytes.Buffer
var mutex sync.Mutex
safeWriter := newSafeWriter(&buf, &mutex)
err := doRequest(safeWriter, message, logrus.ErrorLevel)
assert.Nil(err)
mutex.Lock()
s := buf.String()
assert.Greater(len(s), 0)
assert.Contains(s, msg)
assert.Contains(s, "panic serving 127.0.0.1")
mutex.Unlock()
}
func TestHttpNoServerLogsWithLogrus(t *testing.T) {
assert := assertPkg.New(t)
message := "error-level writer"
var buf bytes.Buffer
var mutex sync.Mutex
safeWriter := newSafeWriter(&buf, &mutex)
err := doRequest(safeWriter, message, logrus.DebugLevel)
assert.Nil(err)
mutex.Lock()
s := buf.String()
if len(s) > 0 {
assert.NotContains(s, message)
}
mutex.Unlock()
}
func doRequest(safeWriter *safeWriter, message string, logLevel logrus.Level) error {
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
host := "127.0.0.1"
httpPort := 9012
httpsPort := 0
msg := fmt.Sprintf("panicking context: %s", message)
handler := alwaysPanicHandler{msg: msg}
listenOpts := &ListenOpts{
BindHost: host,
DisplayServerLogs: logLevel == logrus.ErrorLevel,
}
logrus.StandardLogger().SetOutput(safeWriter)
if err := ListenAndServe(ctx, httpsPort, httpPort, &handler, listenOpts); err != nil {
return err
}
addr := fmt.Sprintf("%s:%d", host, httpPort)
return makeTheHttpRequest(addr)
}
func makeTheHttpRequest(addr string) error {
url := fmt.Sprintf("%s://%s/", "http", addr)
waitTime := 10 * time.Millisecond
totalTime := 0 * time.Millisecond
const maxWaitTime = 10 * time.Second
// Waiting for server to be ready..., max of maxWaitTime
for {
conn, err := net.Dial("tcp", addr)
if err == nil {
conn.Close()
break
} else if totalTime > maxWaitTime {
return fmt.Errorf("timed out waiting for the server to start after %d msec", totalTime/1e6)
}
time.Sleep(waitTime)
totalTime += waitTime
waitTime += 10 * time.Millisecond
}
client := &http.Client{
Timeout: 30 * time.Second,
}
req, err := http.NewRequest("GET", url, nil)
if err != nil {
return fmt.Errorf("error creating request: %w", err)
}
resp, err := client.Do(req)
if err == nil {
return fmt.Errorf("server should have panicked on request")
}
if resp != nil {
defer resp.Body.Close()
}
return nil
}

View File

@@ -2,10 +2,9 @@ package file
import (
"encoding/json"
"os"
"github.com/rancher/dynamiclistener"
v1 "k8s.io/api/core/v1"
"k8s.io/api/core/v1"
"os"
)
func New(file string) dynamiclistener.TLSStorage {
@@ -32,7 +31,7 @@ func (s *storage) Get() (*v1.Secret, error) {
}
func (s *storage) Update(secret *v1.Secret) error {
f, err := os.OpenFile(s.file, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0600)
f, err := os.Create(s.file)
if err != nil {
return err
}

View File

@@ -5,27 +5,18 @@ import (
"crypto/x509"
"github.com/rancher/dynamiclistener/factory"
v1controller "github.com/rancher/wrangler/v3/pkg/generated/controllers/core/v1"
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"
)
// 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.LoadCAChain(secret.Data[v1.TLSCertKey], secret.Data[v1.TLSPrivateKeyKey])
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) {
@@ -65,7 +56,7 @@ func createAndStoreClientCert(secrets v1controller.SecretClient, namespace strin
return nil, err
}
keyPem, certPem, err := factory.MarshalChain(key, cert, caCert)
certPem, keyPem, err := factory.Marshal(cert, key)
if err != nil {
return nil, err
}

View File

@@ -2,47 +2,29 @@ package kubernetes
import (
"context"
"maps"
"sync"
"time"
"github.com/rancher/dynamiclistener"
"github.com/rancher/dynamiclistener/cert"
"github.com/rancher/wrangler/v3/pkg/generated/controllers/core"
v1controller "github.com/rancher/wrangler/v3/pkg/generated/controllers/core/v1"
"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"
"k8s.io/apimachinery/pkg/fields"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/util/wait"
"k8s.io/apimachinery/pkg/watch"
"k8s.io/client-go/tools/cache"
toolswatch "k8s.io/client-go/tools/watch"
"k8s.io/client-go/util/retry"
"k8s.io/client-go/util/workqueue"
)
type CoreGetter func() *core.Factory
type storage struct {
namespace, name string
storage dynamiclistener.TLSStorage
secrets v1controller.SecretController
tls dynamiclistener.TLSFactory
queue workqueue.TypedInterface[string]
queuedSecret *v1.Secret
}
func Load(ctx context.Context, secrets v1controller.SecretController, namespace, name string, backing dynamiclistener.TLSStorage) dynamiclistener.TLSStorage {
storage := &storage{
name: name,
namespace: namespace,
storage: backing,
queue: workqueue.NewTyped[string](),
ctx: ctx,
}
storage.runQueue()
storage.init(ctx, secrets)
storage.init(secrets)
return storage
}
@@ -51,109 +33,83 @@ func New(ctx context.Context, core CoreGetter, namespace, name string, backing d
name: name,
namespace: namespace,
storage: backing,
queue: workqueue.NewTyped[string](),
ctx: ctx,
}
storage.runQueue()
// lazy init
go func() {
wait.PollImmediateUntilWithContext(ctx, time.Second, func(cxt context.Context) (bool, error) {
if coreFactory := core(); coreFactory != nil {
storage.init(ctx, coreFactory.Core().V1().Secret())
return true, nil
for {
core := core()
if core != nil {
storage.init(core.Core().V1().Secret())
start.All(ctx, 5, core)
return
}
return false, nil
})
select {
case <-ctx.Done():
return
case <-time.After(time.Second):
}
}
}()
return storage
}
// always return secret from backing storage
func (s *storage) Get() (*v1.Secret, error) {
return s.storage.Get()
}
type storage struct {
sync.Mutex
// sync secret to Kubernetes and backing storage via workqueue
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.
s.queuedSecret = secret
s.queue.Add(s.name)
return nil
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(ctx context.Context, secrets v1controller.SecretController) {
s.secrets = secrets
func (s *storage) init(secrets v1controller.SecretController) {
s.Lock()
defer s.Unlock()
// Watch just the target secret, instead of using a wrangler OnChange handler
// which watches all secrets in all namespaces. Changes to the secret
// will be sent through the workqueue.
go func() {
fieldSelector := fields.Set{"metadata.name": s.name}.String()
lw := &cache.ListWatch{
ListFunc: func(options metav1.ListOptions) (object runtime.Object, e error) {
options.FieldSelector = fieldSelector
return secrets.List(s.namespace, options)
},
WatchFunc: func(options metav1.ListOptions) (i watch.Interface, e error) {
options.FieldSelector = fieldSelector
return secrets.Watch(s.namespace, options)
},
secrets.OnChange(s.ctx, "tls-storage", func(key string, secret *v1.Secret) (*v1.Secret, error) {
if secret == nil {
return nil, nil
}
_, _, watch, done := toolswatch.NewIndexerInformerWatcher(lw, &v1.Secret{})
defer func() {
s.queue.ShutDown()
watch.Stop()
<-done
}()
for {
select {
case <-ctx.Done():
return
case ev := <-watch.ResultChan():
if secret, ok := ev.Object.(*v1.Secret); ok {
s.queuedSecret = secret
s.queue.Add(secret.Name)
}
if secret.Namespace == s.namespace && secret.Name == s.name {
if err := s.Update(secret); err != nil {
return nil, err
}
}
}()
// enqueue initial sync of the backing secret
s.queuedSecret, _ = s.Get()
s.queue.Add(s.name)
}
return secret, nil
})
s.secrets = secrets
// runQueue starts a goroutine to process secrets updates from the workqueue
func (s *storage) runQueue() {
go func() {
for s.processQueue() {
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,
})
}
}()
}
}
// processQueue processes the secret update queue.
// The key doesn't actually matter, as we are only handling a single secret with a single worker.
func (s *storage) processQueue() bool {
key, shutdown := s.queue.Get()
if shutdown {
return false
}
func (s *storage) Get() (*v1.Secret, error) {
s.Lock()
defer s.Unlock()
defer s.queue.Done(key)
if err := s.update(); err != nil {
logrus.Errorf("Failed to update Secret %s/%s: %v", s.namespace, s.name, err)
}
return true
return s.storage.Get()
}
func (s *storage) targetSecret() (*v1.Secret, error) {
@@ -164,90 +120,65 @@ func (s *storage) targetSecret() (*v1.Secret, error) {
Name: s.name,
Namespace: s.namespace,
},
Type: v1.SecretTypeTLS,
}, nil
}
return existingSecret, err
}
// saveInK8s handles merging the provided secret with the kubernetes secret.
// This includes calling the tls factory to sign a new certificate with the
// merged SAN entries, if possible. Note that the provided secret could be
// either from Kubernetes due to the secret being changed by another client, or
// from the listener trying to add SANs or regenerate the cert.
func (s *storage) saveInK8s(secret *v1.Secret) (*v1.Secret, error) {
// secret controller not initialized yet, just return the current secret.
// if there is an existing secret in Kubernetes, that will get synced by the
// list/watch once the controller is initialized.
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 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
}
}
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", s.namespace, s.name)
return targetSecret, nil
}
// Any changes to the cert will change the fingerprint annotation, so we can use that
// for change detection, and skip updating an existing secret if it has not changed.
changed := !maps.Equal(targetSecret.Annotations, secret.Annotations)
targetSecret.Type = v1.SecretTypeTLS
targetSecret.Annotations = secret.Annotations
targetSecret.Type = v1.SecretTypeTLS
targetSecret.Data = secret.Data
if targetSecret.UID == "" {
logrus.Infof("Creating new TLS secret for %s/%s (count: %d): %v", targetSecret.Namespace, targetSecret.Name, len(targetSecret.Annotations)-1, targetSecret.Annotations)
logrus.Infof("Creating new TLS secret for %v (count: %d): %v", targetSecret.Name, len(targetSecret.Annotations)-1, targetSecret.Annotations)
return s.secrets.Create(targetSecret)
} else if changed {
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)
}
return targetSecret, nil
logrus.Infof("Updating TLS secret for %v (count: %d): %v", targetSecret.Name, len(targetSecret.Annotations)-1, targetSecret.Annotations)
return s.secrets.Update(targetSecret)
}
func isConflictOrAlreadyExists(err error) bool {
return errors.IsConflict(err) || errors.IsAlreadyExists(err)
}
func (s *storage) Update(secret *v1.Secret) (err error) {
s.Lock()
defer s.Unlock()
// update wraps a conflict retry around saveInK8s, which handles merging the
// queued secret with the Kubernetes secret. Only after successfully
// updating the Kubernetes secret will the backing storage be updated.
func (s *storage) update() (err error) {
var newSecret *v1.Secret
if err := retry.OnError(retry.DefaultRetry, isConflictOrAlreadyExists, func() error {
newSecret, err = s.saveInK8s(s.queuedSecret)
return err
}); err != nil {
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
}
return s.storage.Update(newSecret)
// update underlying storage
return s.storage.Update(secret)
}

View File

@@ -2,7 +2,6 @@ package memory
import (
"github.com/rancher/dynamiclistener"
"github.com/rancher/dynamiclistener/factory"
"github.com/sirupsen/logrus"
v1 "k8s.io/api/core/v1"
)
@@ -33,34 +32,15 @@ func (m *memory) Get() (*v1.Secret, error) {
}
func (m *memory) Update(secret *v1.Secret) error {
if isChanged(m.secret, secret) {
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/%s (ver=%s) (count %d): %v", secret.Namespace, secret.Name, secret.ResourceVersion, len(secret.Annotations)-1, secret.Annotations)
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
}
func isChanged(old, new *v1.Secret) bool {
if new == nil {
return false
}
if old == nil {
return true
}
if old.ResourceVersion == "" {
return true
}
if old.ResourceVersion != new.ResourceVersion {
return true
}
if old.Annotations[factory.Fingerprint] != new.Annotations[factory.Fingerprint] {
return true
}
return false
}