├── CODEOWNERS ├── filter.go ├── cert ├── secret.go ├── validity.go ├── validity_test.go ├── csr.go ├── io.go ├── pem.go └── cert.go ├── .github ├── workflows │ ├── ci.yaml │ ├── release.yaml │ └── renovate-vault.yml └── renovate.json ├── storage ├── static │ └── static.go ├── file │ └── file.go ├── memory │ └── memory.go └── kubernetes │ ├── ca.go │ └── controller.go ├── tcp.go ├── README.md ├── VERSION.md ├── redirect.go ├── go.mod ├── factory ├── ca.go ├── cert_utils.go └── gen.go ├── server ├── server_test.go └── server.go ├── listener_test.go ├── LICENSE ├── listener.go └── go.sum /CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @rancher/rancher-squad-frameworks -------------------------------------------------------------------------------- /filter.go: -------------------------------------------------------------------------------- 1 | package dynamiclistener 2 | 3 | func OnlyAllow(str string) func(...string) []string { 4 | return func(s2 ...string) []string { 5 | for _, s := range s2 { 6 | if s == str { 7 | return []string{s} 8 | } 9 | } 10 | return nil 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /cert/secret.go: -------------------------------------------------------------------------------- 1 | package cert 2 | 3 | import v1 "k8s.io/api/core/v1" 4 | 5 | func IsValidTLSSecret(secret *v1.Secret) bool { 6 | if secret == nil { 7 | return false 8 | } 9 | if _, ok := secret.Data[v1.TLSCertKey]; !ok { 10 | return false 11 | } 12 | if _, ok := secret.Data[v1.TLSPrivateKeyKey]; !ok { 13 | return false 14 | } 15 | return true 16 | } 17 | -------------------------------------------------------------------------------- /.github/workflows/ci.yaml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | pull_request: 4 | push: 5 | branches: 6 | - 'master' 7 | - 'release/*' 8 | jobs: 9 | ci: 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Checkout code 13 | # https://github.com/actions/checkout/releases/tag/VERSION 14 | uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 15 | - name: Install Go 16 | # https://github.com/actions/setup-go/releases/tag/VERSION 17 | uses: actions/setup-go@44694675825211faa026b3c33043df3e48a5fa00 # v6.0.0 18 | with: 19 | go-version-file: 'go.mod' 20 | - run: go test -v -race -cover ./... 21 | -------------------------------------------------------------------------------- /.github/renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "github>rancher/renovate-config#release" 4 | ], 5 | "baseBranchPatterns": [ 6 | "main", 7 | "release/v0.3", 8 | "release/v0.4", 9 | "release/v0.5" 10 | ], 11 | "prHourlyLimit": 2, 12 | "packageRules": [ 13 | { 14 | "enabled": false, 15 | "matchPackageNames": [ 16 | "/k8s.io/*/", 17 | "/sigs.k8s.io/*/", 18 | "/github.com/prometheus/*/" 19 | ] 20 | }, 21 | { 22 | "matchUpdateTypes": [ 23 | "major", 24 | "minor" 25 | ], 26 | "enabled": false, 27 | "matchPackageNames": [ 28 | "/github.com/rancher/wrangler/*/" 29 | ] 30 | } 31 | ] 32 | } 33 | -------------------------------------------------------------------------------- /storage/static/static.go: -------------------------------------------------------------------------------- 1 | package static 2 | 3 | import ( 4 | "github.com/rancher/dynamiclistener/factory" 5 | v1 "k8s.io/api/core/v1" 6 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 7 | ) 8 | 9 | type Storage struct { 10 | Secret *v1.Secret 11 | } 12 | 13 | func New(certPem, keyPem []byte) *Storage { 14 | return &Storage{ 15 | Secret: &v1.Secret{ 16 | ObjectMeta: metav1.ObjectMeta{ 17 | Annotations: map[string]string{ 18 | factory.Static: "true", 19 | }, 20 | }, 21 | Data: map[string][]byte{ 22 | v1.TLSCertKey: certPem, 23 | v1.TLSPrivateKeyKey: keyPem, 24 | }, 25 | Type: v1.SecretTypeTLS, 26 | }, 27 | } 28 | } 29 | 30 | func (s *Storage) Get() (*v1.Secret, error) { 31 | return s.Secret, nil 32 | } 33 | 34 | func (s *Storage) Update(_ *v1.Secret) error { 35 | return nil 36 | } 37 | -------------------------------------------------------------------------------- /.github/workflows/release.yaml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | on: 4 | push: 5 | tags: 6 | - v* 7 | 8 | jobs: 9 | release: 10 | runs-on: ubuntu-latest 11 | permissions: 12 | contents: write 13 | steps: 14 | - name : Checkout repository 15 | uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0 16 | 17 | - name: Create release on Github 18 | run: | 19 | if [[ "${{ github.ref_name }}" == *-rc* ]]; then 20 | gh --repo "${{ github.repository }}" release create ${{ github.ref_name }} --verify-tag --generate-notes --prerelease 21 | else 22 | gh --repo "${{ github.repository }}" release create ${{ github.ref_name }} --verify-tag --generate-notes 23 | fi 24 | env: 25 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 26 | -------------------------------------------------------------------------------- /tcp.go: -------------------------------------------------------------------------------- 1 | package dynamiclistener 2 | 3 | import ( 4 | "fmt" 5 | "net" 6 | "reflect" 7 | "time" 8 | ) 9 | 10 | func NewTCPListener(ip string, port int) (net.Listener, error) { 11 | l, err := net.Listen("tcp", fmt.Sprintf("%s:%d", ip, port)) 12 | if err != nil { 13 | return nil, err 14 | } 15 | 16 | tcpListener, ok := l.(*net.TCPListener) 17 | if !ok { 18 | return nil, fmt.Errorf("wrong listener type: %v", reflect.TypeOf(tcpListener)) 19 | } 20 | 21 | return tcpKeepAliveListener{ 22 | TCPListener: tcpListener, 23 | }, nil 24 | } 25 | 26 | type tcpKeepAliveListener struct { 27 | *net.TCPListener 28 | } 29 | 30 | func (ln tcpKeepAliveListener) Accept() (c net.Conn, err error) { 31 | tc, err := ln.AcceptTCP() 32 | if err != nil { 33 | return 34 | } 35 | tc.SetKeepAlive(true) 36 | tc.SetKeepAlivePeriod(3 * time.Minute) 37 | return tc, nil 38 | } 39 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # [dynamiclistener](https://github.com/rancher/dynamiclistener) 2 | 3 | DynamicListener allows you to setup a server with automatically generated (and re-generated) TLS certs with kubernetes secrets integration. 4 | 5 | This `README` is a work in progress; aimed towards providing information for navigating the contents of this repository. 6 | 7 | ## Changing the Expiration Days for Newly Signed Certificates 8 | 9 | By default, a newly signed certificate is set to expire 365 days (1 year) after its creation time and date. 10 | You can use the `CATTLE_NEW_SIGNED_CERT_EXPIRATION_DAYS` environment variable to change this value. 11 | 12 | **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). 13 | 14 | # Versioning 15 | 16 | See [VERSION.md](VERSION.md). 17 | -------------------------------------------------------------------------------- /VERSION.md: -------------------------------------------------------------------------------- 1 | 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: 2 | 3 | The current supported release lines are: 4 | 5 | | DynamicListener Branch | DynamicListener Minor version | Kubernetes Version Range | Wrangler Version | 6 | |------------------------|-------------------------------|--------------------------|------------------------------------------------| 7 | | main | v0.7 | v1.27+ | v3 | 8 | | release/v0.6 | v0.6 | v1.27 - v1.32 | v3 | 9 | | release/v0.5 | v0.5 | v1.26 - v1.30 | v3 | 10 | | release/v0.4 | v0.4 | v1.25 - v1.28 | v2 | 11 | | release/v0.3 | v0.3 | v1.23 - v1.27 | v2 | 12 | -------------------------------------------------------------------------------- /storage/file/file.go: -------------------------------------------------------------------------------- 1 | package file 2 | 3 | import ( 4 | "encoding/json" 5 | "os" 6 | 7 | "github.com/rancher/dynamiclistener" 8 | v1 "k8s.io/api/core/v1" 9 | ) 10 | 11 | func New(file string) dynamiclistener.TLSStorage { 12 | return &storage{ 13 | file: file, 14 | } 15 | } 16 | 17 | type storage struct { 18 | file string 19 | } 20 | 21 | func (s *storage) Get() (*v1.Secret, error) { 22 | f, err := os.Open(s.file) 23 | if os.IsNotExist(err) { 24 | return nil, nil 25 | } else if err != nil { 26 | return nil, err 27 | } 28 | defer f.Close() 29 | 30 | secret := v1.Secret{} 31 | return &secret, json.NewDecoder(f).Decode(&secret) 32 | } 33 | 34 | func (s *storage) Update(secret *v1.Secret) error { 35 | f, err := os.OpenFile(s.file, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0600) 36 | if err != nil { 37 | return err 38 | } 39 | defer f.Close() 40 | 41 | return json.NewEncoder(f).Encode(secret) 42 | } 43 | -------------------------------------------------------------------------------- /.github/workflows/renovate-vault.yml: -------------------------------------------------------------------------------- 1 | name: Renovate 2 | on: 3 | workflow_dispatch: 4 | inputs: 5 | logLevel: 6 | description: "Override default log level" 7 | required: false 8 | default: "info" 9 | type: string 10 | overrideSchedule: 11 | description: "Override all schedules" 12 | required: false 13 | default: "false" 14 | type: string 15 | # Run twice in the early morning (UTC) for initial and follow up steps (create pull request and merge) 16 | schedule: 17 | - cron: '30 4,6 * * *' 18 | 19 | permissions: 20 | contents: read 21 | id-token: write 22 | 23 | jobs: 24 | call-workflow: 25 | uses: rancher/renovate-config/.github/workflows/renovate-vault.yml@release 26 | with: 27 | logLevel: ${{ inputs.logLevel || 'info' }} 28 | overrideSchedule: ${{ github.event.inputs.overrideSchedule == 'true' && '{''schedule'':null}' || '' }} 29 | secrets: inherit 30 | -------------------------------------------------------------------------------- /cert/validity.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2014 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package cert 18 | 19 | import ( 20 | "crypto/x509" 21 | "time" 22 | 23 | clockutil "k8s.io/utils/clock" 24 | ) 25 | 26 | var clock clockutil.PassiveClock = &clockutil.RealClock{} 27 | 28 | // CalculateNotBefore calculates a NotBefore time of 1 hour in the past, or the 29 | // NotBefore time of the optionally provided *x509.Certificate, whichever is greater. 30 | func CalculateNotBefore(ca *x509.Certificate) time.Time { 31 | // Subtract 1 hour for clock skew 32 | now := clock.Now().UTC().Add(-time.Hour) 33 | 34 | // It makes no sense to return a time before the CA itself is valid. 35 | if ca != nil && now.Before(ca.NotBefore) { 36 | return ca.NotBefore 37 | } 38 | return now 39 | } 40 | -------------------------------------------------------------------------------- /redirect.go: -------------------------------------------------------------------------------- 1 | package dynamiclistener 2 | 3 | import ( 4 | "net" 5 | "net/http" 6 | "strconv" 7 | "strings" 8 | ) 9 | 10 | // Approach taken from letsencrypt, except manglePort is specific to us 11 | func HTTPRedirect(next http.Handler) http.Handler { 12 | return http.HandlerFunc( 13 | func(rw http.ResponseWriter, r *http.Request) { 14 | if r.TLS != nil || 15 | r.Header.Get("x-Forwarded-Proto") == "https" || 16 | r.Header.Get("x-Forwarded-Proto") == "wss" || 17 | strings.HasPrefix(r.URL.Path, "/.well-known/") || 18 | strings.HasPrefix(r.URL.Path, "/ping") || 19 | strings.HasPrefix(r.URL.Path, "/health") { 20 | next.ServeHTTP(rw, r) 21 | return 22 | } 23 | if r.Method != "GET" && r.Method != "HEAD" { 24 | http.Error(rw, "Use HTTPS", http.StatusBadRequest) 25 | return 26 | } 27 | target := "https://" + manglePort(r.Host) + r.URL.RequestURI() 28 | http.Redirect(rw, r, target, http.StatusFound) 29 | }) 30 | } 31 | 32 | func manglePort(hostport string) string { 33 | host, port, err := net.SplitHostPort(hostport) 34 | if err != nil { 35 | return hostport 36 | } 37 | 38 | portInt, err := strconv.Atoi(port) 39 | if err != nil { 40 | return hostport 41 | } 42 | 43 | portInt = ((portInt / 1000) * 1000) + 443 44 | 45 | return net.JoinHostPort(host, strconv.Itoa(portInt)) 46 | } 47 | -------------------------------------------------------------------------------- /storage/memory/memory.go: -------------------------------------------------------------------------------- 1 | package memory 2 | 3 | import ( 4 | "github.com/rancher/dynamiclistener" 5 | "github.com/rancher/dynamiclistener/factory" 6 | "github.com/sirupsen/logrus" 7 | v1 "k8s.io/api/core/v1" 8 | ) 9 | 10 | func New() dynamiclistener.TLSStorage { 11 | return &memory{} 12 | } 13 | 14 | func NewBacked(storage dynamiclistener.TLSStorage) dynamiclistener.TLSStorage { 15 | return &memory{storage: storage} 16 | } 17 | 18 | type memory struct { 19 | storage dynamiclistener.TLSStorage 20 | secret *v1.Secret 21 | } 22 | 23 | func (m *memory) Get() (*v1.Secret, error) { 24 | if m.secret == nil && m.storage != nil { 25 | secret, err := m.storage.Get() 26 | if err != nil { 27 | return nil, err 28 | } 29 | m.secret = secret 30 | } 31 | 32 | return m.secret, nil 33 | } 34 | 35 | func (m *memory) Update(secret *v1.Secret) error { 36 | if isChanged(m.secret, secret) { 37 | if m.storage != nil { 38 | if err := m.storage.Update(secret); err != nil { 39 | return err 40 | } 41 | } 42 | 43 | logrus.Infof("Active TLS secret %s/%s (ver=%s) (count %d): %v", secret.Namespace, secret.Name, secret.ResourceVersion, len(secret.Annotations)-1, secret.Annotations) 44 | m.secret = secret 45 | } 46 | return nil 47 | } 48 | 49 | func isChanged(old, new *v1.Secret) bool { 50 | if new == nil { 51 | return false 52 | } 53 | if old == nil { 54 | return true 55 | } 56 | if old.ResourceVersion == "" { 57 | return true 58 | } 59 | if old.ResourceVersion != new.ResourceVersion { 60 | return true 61 | } 62 | if old.Annotations[factory.Fingerprint] != new.Annotations[factory.Fingerprint] { 63 | return true 64 | } 65 | return false 66 | } 67 | -------------------------------------------------------------------------------- /cert/validity_test.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2014 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package cert 18 | 19 | import ( 20 | "crypto/x509" 21 | "testing" 22 | "time" 23 | 24 | clocktest "k8s.io/utils/clock/testing" 25 | ) 26 | 27 | func TestCalculateNotBefore(t *testing.T) { 28 | baseTime := time.Date(2025, 9, 29, 12, 0, 0, 0, time.UTC) 29 | 30 | tests := []struct { 31 | name string 32 | ca *x509.Certificate 33 | now time.Time 34 | expected time.Time 35 | }{ 36 | { 37 | name: "nil CA returns 1h ago", 38 | ca: nil, 39 | now: baseTime, 40 | expected: baseTime.Add(-time.Hour), 41 | }, 42 | { 43 | name: "CA notBefore before now returns 1h ago", 44 | ca: &x509.Certificate{ 45 | NotBefore: baseTime.Add(-2 * time.Hour), 46 | }, 47 | now: baseTime, 48 | expected: baseTime.Add(-time.Hour), 49 | }, 50 | { 51 | name: "CA notBefore after now returns CA.NotBefore", 52 | ca: &x509.Certificate{ 53 | NotBefore: baseTime.Add(2 * time.Hour), 54 | }, 55 | now: baseTime, 56 | expected: baseTime.Add(2 * time.Hour), 57 | }, 58 | { 59 | name: "CA notBefore equal to now returns now", 60 | ca: &x509.Certificate{ 61 | NotBefore: baseTime, 62 | }, 63 | now: baseTime, 64 | expected: baseTime, 65 | }, 66 | } 67 | 68 | for _, tt := range tests { 69 | t.Run(tt.name, func(t *testing.T) { 70 | clock = clocktest.NewFakePassiveClock(tt.now) 71 | result := CalculateNotBefore(tt.ca) 72 | if !result.Equal(tt.expected) { 73 | t.Errorf("Expected %v, got %v", tt.expected, result) 74 | } 75 | }) 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /cert/csr.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2016 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package cert 18 | 19 | import ( 20 | cryptorand "crypto/rand" 21 | "crypto/rsa" 22 | "crypto/x509" 23 | "crypto/x509/pkix" 24 | "encoding/pem" 25 | "net" 26 | ) 27 | 28 | // MakeCSR generates a PEM-encoded CSR using the supplied private key, subject, and SANs. 29 | // All key types that are implemented via crypto.Signer are supported (This includes *rsa.PrivateKey and *ecdsa.PrivateKey.) 30 | func MakeCSR(privateKey interface{}, subject *pkix.Name, dnsSANs []string, ipSANs []net.IP) (csr []byte, err error) { 31 | template := &x509.CertificateRequest{ 32 | Subject: *subject, 33 | DNSNames: dnsSANs, 34 | IPAddresses: ipSANs, 35 | } 36 | 37 | return MakeCSRFromTemplate(privateKey, template) 38 | } 39 | 40 | // MakeCSRFromTemplate generates a PEM-encoded CSR using the supplied private 41 | // key and certificate request as a template. All key types that are 42 | // implemented via crypto.Signer are supported (This includes *rsa.PrivateKey 43 | // and *ecdsa.PrivateKey.) 44 | func MakeCSRFromTemplate(privateKey interface{}, template *x509.CertificateRequest) ([]byte, error) { 45 | t := *template 46 | t.SignatureAlgorithm = sigType(privateKey) 47 | 48 | csrDER, err := x509.CreateCertificateRequest(cryptorand.Reader, &t, privateKey) 49 | if err != nil { 50 | return nil, err 51 | } 52 | 53 | csrPemBlock := &pem.Block{ 54 | Type: CertificateRequestBlockType, 55 | Bytes: csrDER, 56 | } 57 | 58 | return pem.EncodeToMemory(csrPemBlock), nil 59 | } 60 | 61 | func sigType(privateKey interface{}) x509.SignatureAlgorithm { 62 | // Customize the signature for RSA keys, depending on the key size 63 | if privateKey, ok := privateKey.(*rsa.PrivateKey); ok { 64 | keySize := privateKey.N.BitLen() 65 | switch { 66 | case keySize >= 4096: 67 | return x509.SHA512WithRSA 68 | case keySize >= 3072: 69 | return x509.SHA384WithRSA 70 | default: 71 | return x509.SHA256WithRSA 72 | } 73 | } 74 | return x509.UnknownSignatureAlgorithm 75 | } 76 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/rancher/dynamiclistener 2 | 3 | go 1.24.0 4 | 5 | require ( 6 | github.com/rancher/wrangler/v3 v3.3.0-rc.2 7 | github.com/sirupsen/logrus v1.9.3 8 | github.com/stretchr/testify v1.11.1 9 | golang.org/x/crypto v0.42.0 10 | k8s.io/api v0.34.1 11 | k8s.io/apimachinery v0.34.1 12 | k8s.io/client-go v0.34.1 13 | k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 14 | ) 15 | 16 | require ( 17 | github.com/beorn7/perks v1.0.1 // indirect 18 | github.com/cespare/xxhash/v2 v2.3.0 // indirect 19 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect 20 | github.com/emicklei/go-restful/v3 v3.12.2 // indirect 21 | github.com/evanphx/json-patch v5.9.11+incompatible // indirect 22 | github.com/fxamacker/cbor/v2 v2.9.0 // indirect 23 | github.com/go-logr/logr v1.4.2 // indirect 24 | github.com/go-openapi/jsonpointer v0.21.0 // indirect 25 | github.com/go-openapi/jsonreference v0.21.0 // indirect 26 | github.com/go-openapi/swag v0.23.0 // indirect 27 | github.com/gogo/protobuf v1.3.2 // indirect 28 | github.com/google/gnostic-models v0.7.0 // indirect 29 | github.com/google/go-cmp v0.7.0 // indirect 30 | github.com/google/uuid v1.6.0 // indirect 31 | github.com/josharian/intern v1.0.0 // indirect 32 | github.com/json-iterator/go v1.1.12 // indirect 33 | github.com/mailru/easyjson v0.7.7 // indirect 34 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 35 | github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect 36 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect 37 | github.com/pkg/errors v0.9.1 // indirect 38 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect 39 | github.com/prometheus/client_golang v1.22.0 // indirect 40 | github.com/prometheus/client_model v0.6.1 // indirect 41 | github.com/prometheus/common v0.62.0 // indirect 42 | github.com/prometheus/procfs v0.15.1 // indirect 43 | github.com/rancher/lasso v0.2.5-rc.1 // indirect 44 | github.com/x448/float16 v0.8.4 // indirect 45 | go.yaml.in/yaml/v2 v2.4.2 // indirect 46 | go.yaml.in/yaml/v3 v3.0.4 // indirect 47 | golang.org/x/net v0.44.0 // indirect 48 | golang.org/x/oauth2 v0.27.0 // indirect 49 | golang.org/x/sync v0.17.0 // indirect 50 | golang.org/x/sys v0.36.0 // indirect 51 | golang.org/x/term v0.35.0 // indirect 52 | golang.org/x/text v0.29.0 // indirect 53 | golang.org/x/time v0.9.0 // indirect 54 | google.golang.org/protobuf v1.36.8 // indirect 55 | gopkg.in/evanphx/json-patch.v4 v4.12.0 // indirect 56 | gopkg.in/inf.v0 v0.9.1 // indirect 57 | gopkg.in/yaml.v3 v3.0.1 // indirect 58 | k8s.io/klog/v2 v2.130.1 // indirect 59 | k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b // indirect 60 | sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 // indirect 61 | sigs.k8s.io/randfill v1.0.0 // indirect 62 | sigs.k8s.io/structured-merge-diff/v6 v6.3.0 // indirect 63 | sigs.k8s.io/yaml v1.6.0 // indirect 64 | ) 65 | -------------------------------------------------------------------------------- /factory/ca.go: -------------------------------------------------------------------------------- 1 | package factory 2 | 3 | import ( 4 | "crypto" 5 | "crypto/x509" 6 | "fmt" 7 | "io/ioutil" 8 | "os" 9 | "time" 10 | 11 | "github.com/rancher/dynamiclistener/cert" 12 | ) 13 | 14 | func GenCA() (*x509.Certificate, crypto.Signer, error) { 15 | caKey, err := NewPrivateKey() 16 | if err != nil { 17 | return nil, nil, err 18 | } 19 | 20 | caCert, err := NewSelfSignedCACert(caKey, fmt.Sprintf("dynamiclistener-ca@%d", time.Now().Unix()), "dynamiclistener-org") 21 | if err != nil { 22 | return nil, nil, err 23 | } 24 | 25 | return caCert, caKey, nil 26 | } 27 | 28 | // Deprecated: Use LoadOrGenCAChain instead as it supports intermediate CAs 29 | func LoadOrGenCA() (*x509.Certificate, crypto.Signer, error) { 30 | chain, signer, err := LoadOrGenCAChain() 31 | if err != nil { 32 | return nil, nil, err 33 | } 34 | return chain[0], signer, err 35 | } 36 | 37 | func LoadOrGenCAChain() ([]*x509.Certificate, crypto.Signer, error) { 38 | certs, key, err := loadCA() 39 | if err == nil { 40 | return certs, key, nil 41 | } 42 | 43 | cert, key, err := GenCA() 44 | if err != nil { 45 | return nil, nil, err 46 | } 47 | certs = []*x509.Certificate{cert} 48 | 49 | certBytes, keyBytes, err := MarshalChain(key, certs...) 50 | if err != nil { 51 | return nil, nil, err 52 | } 53 | 54 | if err := os.MkdirAll("./certs", 0700); err != nil { 55 | return nil, nil, err 56 | } 57 | 58 | if err := ioutil.WriteFile("./certs/ca.pem", certBytes, 0600); err != nil { 59 | return nil, nil, err 60 | } 61 | 62 | if err := ioutil.WriteFile("./certs/ca.key", keyBytes, 0600); err != nil { 63 | return nil, nil, err 64 | } 65 | 66 | return certs, key, nil 67 | } 68 | 69 | func loadCA() ([]*x509.Certificate, crypto.Signer, error) { 70 | return LoadCertsChain("./certs/ca.pem", "./certs/ca.key") 71 | } 72 | 73 | func LoadCA(caPem, caKey []byte) (*x509.Certificate, crypto.Signer, error) { 74 | chain, signer, err := LoadCAChain(caPem, caKey) 75 | if err != nil { 76 | return nil, nil, err 77 | } 78 | return chain[0], signer, nil 79 | } 80 | 81 | func LoadCAChain(caPem, caKey []byte) ([]*x509.Certificate, crypto.Signer, error) { 82 | key, err := cert.ParsePrivateKeyPEM(caKey) 83 | if err != nil { 84 | return nil, nil, err 85 | } 86 | signer, ok := key.(crypto.Signer) 87 | if !ok { 88 | return nil, nil, fmt.Errorf("key is not a crypto.Signer") 89 | } 90 | 91 | certs, err := cert.ParseCertsPEM(caPem) 92 | if err != nil { 93 | return nil, nil, err 94 | } 95 | 96 | return certs, signer, nil 97 | } 98 | 99 | // Deprecated: Use LoadCertsChain instead as it supports intermediate CAs 100 | func LoadCerts(certFile, keyFile string) (*x509.Certificate, crypto.Signer, error) { 101 | chain, signer, err := LoadCertsChain(certFile, keyFile) 102 | if err != nil { 103 | return nil, nil, err 104 | } 105 | return chain[0], signer, err 106 | } 107 | 108 | func LoadCertsChain(certFile, keyFile string) ([]*x509.Certificate, crypto.Signer, error) { 109 | caPem, err := ioutil.ReadFile(certFile) 110 | if err != nil { 111 | return nil, nil, err 112 | } 113 | caKey, err := ioutil.ReadFile(keyFile) 114 | if err != nil { 115 | return nil, nil, err 116 | } 117 | 118 | return LoadCAChain(caPem, caKey) 119 | } 120 | -------------------------------------------------------------------------------- /storage/kubernetes/ca.go: -------------------------------------------------------------------------------- 1 | package kubernetes 2 | 3 | import ( 4 | "crypto" 5 | "crypto/x509" 6 | 7 | "github.com/rancher/dynamiclistener/factory" 8 | v1controller "github.com/rancher/wrangler/v3/pkg/generated/controllers/core/v1" 9 | v1 "k8s.io/api/core/v1" 10 | "k8s.io/apimachinery/pkg/api/errors" 11 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 12 | ) 13 | 14 | // Deprecated: Use LoadOrGenCAChain instead as it supports intermediate CAs 15 | func LoadOrGenCA(secrets v1controller.SecretClient, namespace, name string) (*x509.Certificate, crypto.Signer, error) { 16 | chain, signer, err := LoadOrGenCAChain(secrets, namespace, name) 17 | if err != nil { 18 | return nil, nil, err 19 | } 20 | return chain[0], signer, err 21 | } 22 | 23 | func LoadOrGenCAChain(secrets v1controller.SecretClient, namespace, name string) ([]*x509.Certificate, crypto.Signer, error) { 24 | secret, err := getSecret(secrets, namespace, name) 25 | if err != nil { 26 | return nil, nil, err 27 | } 28 | return factory.LoadCAChain(secret.Data[v1.TLSCertKey], secret.Data[v1.TLSPrivateKeyKey]) 29 | } 30 | 31 | func LoadOrGenClient(secrets v1controller.SecretClient, namespace, name, cn string, ca *x509.Certificate, key crypto.Signer) (*x509.Certificate, crypto.Signer, error) { 32 | secret, err := getClientSecret(secrets, namespace, name, cn, ca, key) 33 | if err != nil { 34 | return nil, nil, err 35 | } 36 | return factory.LoadCA(secret.Data[v1.TLSCertKey], secret.Data[v1.TLSPrivateKeyKey]) 37 | } 38 | 39 | func getClientSecret(secrets v1controller.SecretClient, namespace, name, cn string, caCert *x509.Certificate, caKey crypto.Signer) (*v1.Secret, error) { 40 | s, err := secrets.Get(namespace, name, metav1.GetOptions{}) 41 | if !errors.IsNotFound(err) { 42 | return s, err 43 | } 44 | 45 | return createAndStoreClientCert(secrets, namespace, name, cn, caCert, caKey) 46 | } 47 | 48 | func getSecret(secrets v1controller.SecretClient, namespace, name string) (*v1.Secret, error) { 49 | s, err := secrets.Get(namespace, name, metav1.GetOptions{}) 50 | if !errors.IsNotFound(err) { 51 | return s, err 52 | } 53 | 54 | return createAndStore(secrets, namespace, name) 55 | } 56 | 57 | func createAndStoreClientCert(secrets v1controller.SecretClient, namespace string, name, cn string, caCert *x509.Certificate, caKey crypto.Signer) (*v1.Secret, error) { 58 | key, err := factory.NewPrivateKey() 59 | if err != nil { 60 | return nil, err 61 | } 62 | 63 | cert, err := factory.NewSignedClientCert(key, caCert, caKey, cn) 64 | if err != nil { 65 | return nil, err 66 | } 67 | 68 | keyPem, certPem, err := factory.MarshalChain(key, cert, caCert) 69 | if err != nil { 70 | return nil, err 71 | } 72 | 73 | secret := &v1.Secret{ 74 | ObjectMeta: metav1.ObjectMeta{ 75 | Name: name, 76 | Namespace: namespace, 77 | }, 78 | Data: map[string][]byte{ 79 | v1.TLSCertKey: certPem, 80 | v1.TLSPrivateKeyKey: keyPem, 81 | }, 82 | Type: v1.SecretTypeTLS, 83 | } 84 | 85 | return secrets.Create(secret) 86 | } 87 | 88 | func createAndStore(secrets v1controller.SecretClient, namespace string, name string) (*v1.Secret, error) { 89 | ca, cert, err := factory.GenCA() 90 | if err != nil { 91 | return nil, err 92 | } 93 | 94 | certPem, keyPem, err := factory.Marshal(ca, cert) 95 | if err != nil { 96 | return nil, err 97 | } 98 | 99 | secret := &v1.Secret{ 100 | ObjectMeta: metav1.ObjectMeta{ 101 | Name: name, 102 | Namespace: namespace, 103 | }, 104 | Data: map[string][]byte{ 105 | v1.TLSCertKey: certPem, 106 | v1.TLSPrivateKeyKey: keyPem, 107 | }, 108 | Type: v1.SecretTypeTLS, 109 | } 110 | 111 | return secrets.Create(secret) 112 | } 113 | -------------------------------------------------------------------------------- /factory/cert_utils.go: -------------------------------------------------------------------------------- 1 | package factory 2 | 3 | import ( 4 | "crypto" 5 | "crypto/rand" 6 | "crypto/x509" 7 | "crypto/x509/pkix" 8 | "encoding/pem" 9 | "fmt" 10 | "math" 11 | "math/big" 12 | "net" 13 | "os" 14 | "strconv" 15 | "strings" 16 | "time" 17 | 18 | "github.com/rancher/dynamiclistener/cert" 19 | "github.com/sirupsen/logrus" 20 | ) 21 | 22 | const ( 23 | CertificateBlockType = "CERTIFICATE" 24 | defaultNewSignedCertExpirationDays = 365 25 | ) 26 | 27 | func NewSelfSignedCACert(key crypto.Signer, cn string, org ...string) (*x509.Certificate, error) { 28 | notBefore := cert.CalculateNotBefore(nil) 29 | tmpl := x509.Certificate{ 30 | BasicConstraintsValid: true, 31 | IsCA: true, 32 | KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign, 33 | NotBefore: notBefore, 34 | NotAfter: notBefore.Add(time.Hour * 24 * 365 * 10), 35 | SerialNumber: new(big.Int).SetInt64(0), 36 | Subject: pkix.Name{ 37 | CommonName: cn, 38 | Organization: org, 39 | }, 40 | } 41 | 42 | certDERBytes, err := x509.CreateCertificate(rand.Reader, &tmpl, &tmpl, key.Public(), key) 43 | if err != nil { 44 | return nil, err 45 | } 46 | 47 | logrus.Infof("generated self-signed CA certificate %s: notBefore=%s notAfter=%s", 48 | tmpl.Subject, tmpl.NotBefore, tmpl.NotAfter) 49 | 50 | return x509.ParseCertificate(certDERBytes) 51 | } 52 | 53 | func NewSignedClientCert(signer crypto.Signer, caCert *x509.Certificate, caKey crypto.Signer, cn string) (*x509.Certificate, error) { 54 | serialNumber, err := rand.Int(rand.Reader, new(big.Int).SetInt64(math.MaxInt64)) 55 | if err != nil { 56 | return nil, err 57 | } 58 | 59 | notBefore := cert.CalculateNotBefore(caCert) 60 | parent := x509.Certificate{ 61 | ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth}, 62 | KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature, 63 | NotBefore: notBefore, 64 | NotAfter: notBefore.Add(time.Hour * 24 * 365), 65 | SerialNumber: serialNumber, 66 | Subject: pkix.Name{ 67 | CommonName: cn, 68 | }, 69 | } 70 | 71 | parts := strings.Split(cn, ",o=") 72 | if len(parts) > 1 { 73 | parent.Subject.CommonName = parts[0] 74 | parent.Subject.Organization = parts[1:] 75 | } 76 | 77 | cert, err := x509.CreateCertificate(rand.Reader, &parent, caCert, signer.Public(), caKey) 78 | if err != nil { 79 | return nil, err 80 | } 81 | 82 | return x509.ParseCertificate(cert) 83 | } 84 | 85 | func NewSignedCert(signer crypto.Signer, caCert *x509.Certificate, caKey crypto.Signer, cn string, orgs []string, 86 | domains []string, ips []net.IP) (*x509.Certificate, error) { 87 | 88 | serialNumber, err := rand.Int(rand.Reader, new(big.Int).SetInt64(math.MaxInt64)) 89 | if err != nil { 90 | return nil, err 91 | } 92 | 93 | expirationDays := defaultNewSignedCertExpirationDays 94 | envExpirationDays := os.Getenv("CATTLE_NEW_SIGNED_CERT_EXPIRATION_DAYS") 95 | if envExpirationDays != "" { 96 | if envExpirationDaysInt, err := strconv.Atoi(envExpirationDays); err != nil { 97 | logrus.Infof("[NewSignedCert] expiration days from ENV (%s) could not be converted to int (falling back to default value: %d)", envExpirationDays, defaultNewSignedCertExpirationDays) 98 | } else { 99 | expirationDays = envExpirationDaysInt 100 | } 101 | } 102 | 103 | notBefore := cert.CalculateNotBefore(caCert) 104 | parent := x509.Certificate{ 105 | DNSNames: domains, 106 | ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, 107 | IPAddresses: ips, 108 | KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature, 109 | NotBefore: notBefore, 110 | NotAfter: notBefore.Add(time.Hour * 24 * time.Duration(expirationDays)), 111 | SerialNumber: serialNumber, 112 | Subject: pkix.Name{ 113 | CommonName: cn, 114 | Organization: orgs, 115 | }, 116 | } 117 | 118 | cert, err := x509.CreateCertificate(rand.Reader, &parent, caCert, signer.Public(), caKey) 119 | if err != nil { 120 | return nil, err 121 | } 122 | 123 | parsedCert, err := x509.ParseCertificate(cert) 124 | if err == nil { 125 | logrus.Infof("certificate %s signed by %s: notBefore=%s notAfter=%s", 126 | parsedCert.Subject, caCert.Subject, parsedCert.NotBefore, parsedCert.NotAfter) 127 | } 128 | return parsedCert, err 129 | } 130 | 131 | func ParseCertPEM(pemCerts []byte) (*x509.Certificate, error) { 132 | var pemBlock *pem.Block 133 | for { 134 | pemBlock, pemCerts = pem.Decode(pemCerts) 135 | if pemBlock == nil { 136 | break 137 | } 138 | 139 | if pemBlock.Type == CertificateBlockType { 140 | return x509.ParseCertificate(pemBlock.Bytes) 141 | } 142 | } 143 | 144 | return nil, fmt.Errorf("pem does not include a valid x509 cert") 145 | } 146 | -------------------------------------------------------------------------------- /server/server_test.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "fmt" 7 | "net" 8 | "net/http" 9 | "sync" 10 | "testing" 11 | "time" 12 | 13 | "github.com/sirupsen/logrus" 14 | assertPkg "github.com/stretchr/testify/assert" 15 | ) 16 | 17 | type alwaysPanicHandler struct { 18 | msg string 19 | } 20 | 21 | func (z *alwaysPanicHandler) ServeHTTP(_ http.ResponseWriter, _ *http.Request) { 22 | panic(z.msg) 23 | } 24 | 25 | // safeWriter is used to allow writing to a buffer-based log in a web server 26 | // and safely read from it in the client (i.e. this test code) 27 | type safeWriter struct { 28 | writer *bytes.Buffer 29 | mutex *sync.Mutex 30 | } 31 | 32 | func newSafeWriter(writer *bytes.Buffer, mutex *sync.Mutex) *safeWriter { 33 | return &safeWriter{writer: writer, mutex: mutex} 34 | } 35 | 36 | func (s *safeWriter) Write(p []byte) (n int, err error) { 37 | s.mutex.Lock() 38 | defer s.mutex.Unlock() 39 | return s.writer.Write(p) 40 | } 41 | 42 | func TestTLSHandshakeErrorWriter(t *testing.T) { 43 | tests := []struct { 44 | name string 45 | ignoreTLSHandshakeError bool 46 | message []byte 47 | expectedLevel logrus.Level 48 | }{ 49 | { 50 | name: "TLS handshake error is logged as debug", 51 | message: []byte("http: TLS handshake error: EOF"), 52 | expectedLevel: logrus.DebugLevel, 53 | }, 54 | { 55 | name: "other errors are logged as error", 56 | message: []byte("some other server error"), 57 | expectedLevel: logrus.ErrorLevel, 58 | }, 59 | } 60 | var baseLogLevel = logrus.GetLevel() 61 | for _, tt := range tests { 62 | t.Run(tt.name, func(t *testing.T) { 63 | assert := assertPkg.New(t) 64 | 65 | var buf bytes.Buffer 66 | logrus.SetOutput(&buf) 67 | logrus.SetLevel(logrus.DebugLevel) 68 | 69 | debugger := &TLSErrorDebugger{} 70 | n, err := debugger.Write(tt.message) 71 | 72 | assert.Nil(err) 73 | assert.Equal(len(tt.message), n) 74 | 75 | logOutput := buf.String() 76 | assert.Contains(logOutput, "level="+tt.expectedLevel.String()) 77 | assert.Contains(logOutput, string(tt.message)) 78 | }) 79 | } 80 | logrus.SetLevel(baseLogLevel) 81 | } 82 | 83 | func TestHttpServerLogWithLogrus(t *testing.T) { 84 | assert := assertPkg.New(t) 85 | message := "debug-level writer" 86 | msg := fmt.Sprintf("panicking context: %s", message) 87 | var buf bytes.Buffer 88 | var mutex sync.Mutex 89 | safeWriter := newSafeWriter(&buf, &mutex) 90 | err := doRequest(safeWriter, message, logrus.ErrorLevel) 91 | assert.Nil(err) 92 | 93 | mutex.Lock() 94 | s := buf.String() 95 | assert.Greater(len(s), 0) 96 | assert.Contains(s, msg) 97 | assert.Contains(s, "panic serving 127.0.0.1") 98 | mutex.Unlock() 99 | } 100 | 101 | func TestHttpNoServerLogsWithLogrus(t *testing.T) { 102 | assert := assertPkg.New(t) 103 | 104 | message := "error-level writer" 105 | var buf bytes.Buffer 106 | var mutex sync.Mutex 107 | safeWriter := newSafeWriter(&buf, &mutex) 108 | err := doRequest(safeWriter, message, logrus.DebugLevel) 109 | assert.Nil(err) 110 | 111 | mutex.Lock() 112 | s := buf.String() 113 | if len(s) > 0 { 114 | assert.NotContains(s, message) 115 | } 116 | mutex.Unlock() 117 | } 118 | 119 | func doRequest(safeWriter *safeWriter, message string, logLevel logrus.Level) error { 120 | ctx, cancel := context.WithCancel(context.Background()) 121 | defer cancel() 122 | host := "127.0.0.1" 123 | httpPort := 9012 124 | httpsPort := 0 125 | msg := fmt.Sprintf("panicking context: %s", message) 126 | handler := alwaysPanicHandler{msg: msg} 127 | listenOpts := &ListenOpts{ 128 | BindHost: host, 129 | DisplayServerLogs: logLevel == logrus.ErrorLevel, 130 | } 131 | 132 | logrus.StandardLogger().SetOutput(safeWriter) 133 | if err := ListenAndServe(ctx, httpsPort, httpPort, &handler, listenOpts); err != nil { 134 | return err 135 | } 136 | addr := fmt.Sprintf("%s:%d", host, httpPort) 137 | return makeTheHttpRequest(addr) 138 | } 139 | 140 | func makeTheHttpRequest(addr string) error { 141 | url := fmt.Sprintf("%s://%s/", "http", addr) 142 | 143 | waitTime := 10 * time.Millisecond 144 | totalTime := 0 * time.Millisecond 145 | const maxWaitTime = 10 * time.Second 146 | // Waiting for server to be ready..., max of maxWaitTime 147 | for { 148 | conn, err := net.Dial("tcp", addr) 149 | if err == nil { 150 | conn.Close() 151 | break 152 | } else if totalTime > maxWaitTime { 153 | return fmt.Errorf("timed out waiting for the server to start after %d msec", totalTime/1e6) 154 | } 155 | time.Sleep(waitTime) 156 | totalTime += waitTime 157 | waitTime += 10 * time.Millisecond 158 | } 159 | 160 | client := &http.Client{ 161 | Timeout: 30 * time.Second, 162 | } 163 | req, err := http.NewRequest("GET", url, nil) 164 | if err != nil { 165 | return fmt.Errorf("error creating request: %w", err) 166 | } 167 | resp, err := client.Do(req) 168 | if err == nil { 169 | return fmt.Errorf("server should have panicked on request") 170 | } 171 | if resp != nil { 172 | defer resp.Body.Close() 173 | } 174 | return nil 175 | } 176 | -------------------------------------------------------------------------------- /cert/io.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2014 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package cert 18 | 19 | import ( 20 | "crypto" 21 | "crypto/ecdsa" 22 | "crypto/rsa" 23 | "crypto/x509" 24 | "encoding/pem" 25 | "fmt" 26 | "io/ioutil" 27 | "os" 28 | "path/filepath" 29 | ) 30 | 31 | // CanReadCertAndKey returns true if the certificate and key files already exists, 32 | // otherwise returns false. If lost one of cert and key, returns error. 33 | func CanReadCertAndKey(certPath, keyPath string) (bool, error) { 34 | certReadable := canReadFile(certPath) 35 | keyReadable := canReadFile(keyPath) 36 | 37 | if !certReadable && !keyReadable { 38 | return false, nil 39 | } 40 | 41 | if !certReadable { 42 | return false, fmt.Errorf("error reading %s, certificate and key must be supplied as a pair", certPath) 43 | } 44 | 45 | if !keyReadable { 46 | return false, fmt.Errorf("error reading %s, certificate and key must be supplied as a pair", keyPath) 47 | } 48 | 49 | return true, nil 50 | } 51 | 52 | // If the file represented by path exists and 53 | // readable, returns true otherwise returns false. 54 | func canReadFile(path string) bool { 55 | f, err := os.Open(path) 56 | if err != nil { 57 | return false 58 | } 59 | 60 | defer f.Close() 61 | 62 | return true 63 | } 64 | 65 | // WriteCert writes the pem-encoded certificate data to certPath. 66 | // The certificate file will be created with file mode 0644. 67 | // If the certificate file already exists, it will be overwritten. 68 | // The parent directory of the certPath will be created as needed with file mode 0755. 69 | func WriteCert(certPath string, data []byte) error { 70 | if err := os.MkdirAll(filepath.Dir(certPath), os.FileMode(0755)); err != nil { 71 | return err 72 | } 73 | return ioutil.WriteFile(certPath, data, os.FileMode(0644)) 74 | } 75 | 76 | // WriteKey writes the pem-encoded key data to keyPath. 77 | // The key file will be created with file mode 0600. 78 | // If the key file already exists, it will be overwritten. 79 | // The parent directory of the keyPath will be created as needed with file mode 0755. 80 | func WriteKey(keyPath string, data []byte) error { 81 | if err := os.MkdirAll(filepath.Dir(keyPath), os.FileMode(0755)); err != nil { 82 | return err 83 | } 84 | return ioutil.WriteFile(keyPath, data, os.FileMode(0600)) 85 | } 86 | 87 | // LoadOrGenerateKeyFile looks for a key in the file at the given path. If it 88 | // can't find one, it will generate a new key and store it there. 89 | func LoadOrGenerateKeyFile(keyPath string, force bool) (data []byte, wasGenerated bool, err error) { 90 | if !force { 91 | loadedData, err := ioutil.ReadFile(keyPath) 92 | // Call verifyKeyData to ensure the file wasn't empty/corrupt. 93 | if err == nil && verifyKeyData(loadedData) { 94 | return loadedData, false, err 95 | } 96 | if !os.IsNotExist(err) { 97 | return nil, false, fmt.Errorf("error loading key from %s: %v", keyPath, err) 98 | } 99 | } 100 | 101 | generatedData, err := MakeEllipticPrivateKeyPEM() 102 | if err != nil { 103 | return nil, false, fmt.Errorf("error generating key: %v", err) 104 | } 105 | if err := WriteKey(keyPath, generatedData); err != nil { 106 | return nil, false, fmt.Errorf("error writing key to %s: %v", keyPath, err) 107 | } 108 | return generatedData, true, nil 109 | } 110 | 111 | // MarshalPrivateKeyToPEM converts a known private key type of RSA or ECDSA to 112 | // a PEM encoded block or returns an error. 113 | func MarshalPrivateKeyToPEM(privateKey crypto.PrivateKey) ([]byte, error) { 114 | switch t := privateKey.(type) { 115 | case *ecdsa.PrivateKey: 116 | derBytes, err := x509.MarshalECPrivateKey(t) 117 | if err != nil { 118 | return nil, err 119 | } 120 | privateKeyPemBlock := &pem.Block{ 121 | Type: ECPrivateKeyBlockType, 122 | Bytes: derBytes, 123 | } 124 | return pem.EncodeToMemory(privateKeyPemBlock), nil 125 | case *rsa.PrivateKey: 126 | return EncodePrivateKeyPEM(t), nil 127 | default: 128 | return nil, fmt.Errorf("private key is not a recognized type: %T", privateKey) 129 | } 130 | } 131 | 132 | // NewPool returns an x509.CertPool containing the certificates in the given PEM-encoded file. 133 | // Returns an error if the file could not be read, a certificate could not be parsed, or if the file does not contain any certificates 134 | func NewPool(filename string) (*x509.CertPool, error) { 135 | certs, err := CertsFromFile(filename) 136 | if err != nil { 137 | return nil, err 138 | } 139 | pool := x509.NewCertPool() 140 | for _, cert := range certs { 141 | pool.AddCert(cert) 142 | } 143 | return pool, nil 144 | } 145 | 146 | // CertsFromFile returns the x509.Certificates contained in the given PEM-encoded file. 147 | // Returns an error if the file could not be read, a certificate could not be parsed, or if the file does not contain any certificates 148 | func CertsFromFile(file string) ([]*x509.Certificate, error) { 149 | pemBlock, err := ioutil.ReadFile(file) 150 | if err != nil { 151 | return nil, err 152 | } 153 | certs, err := ParseCertsPEM(pemBlock) 154 | if err != nil { 155 | return nil, fmt.Errorf("error reading %s: %s", file, err) 156 | } 157 | return certs, nil 158 | } 159 | 160 | // PrivateKeyFromFile returns the private key in rsa.PrivateKey or ecdsa.PrivateKey format from a given PEM-encoded file. 161 | // Returns an error if the file could not be read or if the private key could not be parsed. 162 | func PrivateKeyFromFile(file string) (interface{}, error) { 163 | data, err := ioutil.ReadFile(file) 164 | if err != nil { 165 | return nil, err 166 | } 167 | key, err := ParsePrivateKeyPEM(data) 168 | if err != nil { 169 | return nil, fmt.Errorf("error reading private key file %s: %v", file, err) 170 | } 171 | return key, nil 172 | } 173 | 174 | // PublicKeysFromFile returns the public keys in rsa.PublicKey or ecdsa.PublicKey format from a given PEM-encoded file. 175 | // Reads public keys from both public and private key files. 176 | func PublicKeysFromFile(file string) ([]interface{}, error) { 177 | data, err := ioutil.ReadFile(file) 178 | if err != nil { 179 | return nil, err 180 | } 181 | keys, err := ParsePublicKeysPEM(data) 182 | if err != nil { 183 | return nil, fmt.Errorf("error reading public key file %s: %v", file, err) 184 | } 185 | return keys, nil 186 | } 187 | 188 | // verifyKeyData returns true if the provided data appears to be a valid private key. 189 | func verifyKeyData(data []byte) bool { 190 | if len(data) == 0 { 191 | return false 192 | } 193 | _, err := ParsePrivateKeyPEM(data) 194 | return err == nil 195 | } 196 | -------------------------------------------------------------------------------- /listener_test.go: -------------------------------------------------------------------------------- 1 | package dynamiclistener 2 | 3 | import ( 4 | "crypto/tls" 5 | "net" 6 | "testing" 7 | "time" 8 | 9 | "github.com/rancher/dynamiclistener/factory" 10 | "github.com/stretchr/testify/assert" 11 | v1 "k8s.io/api/core/v1" 12 | apiError "k8s.io/apimachinery/pkg/api/errors" 13 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 14 | "k8s.io/apimachinery/pkg/runtime/schema" 15 | ) 16 | 17 | func Test_getCertificate(t *testing.T) { 18 | beforeKey, beforeCert, err := newCertificate() 19 | assert.NoError(t, err, "Error when setting up test - unable to construct before key for test") 20 | beforeTLSCert, err := tls.X509KeyPair(beforeCert, beforeKey) 21 | assert.NoError(t, err, "Error when setting up test - unable to convert before to tls.Certificate") 22 | afterKey, afterCert, err := newCertificate() 23 | assert.NoError(t, err, "Error when setting up test - unable to construct after key for test") 24 | afterTLSCert, err := tls.X509KeyPair(afterCert, afterKey) 25 | assert.NoError(t, err, "Error when setting up test - unable to convert after to tls.Certificate") 26 | tests := []struct { 27 | // input test vars 28 | name string 29 | secret *v1.Secret 30 | secretErr error 31 | cachedCert *tls.Certificate 32 | cachedVersion string 33 | currentConn *closeWrapper 34 | otherConns map[int]*closeWrapper 35 | 36 | // output/result test vars 37 | closedConns []int 38 | expectedCert *tls.Certificate 39 | wantError bool 40 | }{ 41 | { 42 | name: "no secret found", 43 | secret: nil, 44 | secretErr: apiError.NewNotFound(schema.GroupResource{ 45 | Group: "", 46 | Resource: "Secret", 47 | }, "testSecret"), 48 | currentConn: &closeWrapper{id: 0}, 49 | otherConns: map[int]*closeWrapper{}, 50 | 51 | expectedCert: nil, 52 | wantError: true, 53 | }, 54 | { 55 | name: "secret found, and is up to date", 56 | secret: &v1.Secret{ 57 | ObjectMeta: metav1.ObjectMeta{ 58 | ResourceVersion: "1", 59 | Name: "testSecret", 60 | Namespace: "test", 61 | }, 62 | Data: map[string][]byte{ 63 | v1.TLSCertKey: beforeCert, 64 | v1.TLSPrivateKeyKey: beforeKey, 65 | }, 66 | }, 67 | cachedVersion: "1", 68 | cachedCert: &beforeTLSCert, 69 | currentConn: &closeWrapper{id: 0}, 70 | otherConns: map[int]*closeWrapper{}, 71 | 72 | expectedCert: &beforeTLSCert, 73 | wantError: false, 74 | }, 75 | { 76 | name: "secret found, is not up to date, but k8s secret is not valid", 77 | secret: &v1.Secret{ 78 | ObjectMeta: metav1.ObjectMeta{ 79 | ResourceVersion: "2", 80 | Name: "testSecret", 81 | Namespace: "test", 82 | }, 83 | Data: map[string][]byte{ 84 | v1.TLSPrivateKeyKey: []byte("strawberry"), 85 | }, 86 | }, 87 | cachedVersion: "1", 88 | cachedCert: &beforeTLSCert, 89 | currentConn: &closeWrapper{id: 0}, 90 | otherConns: map[int]*closeWrapper{}, 91 | 92 | expectedCert: &beforeTLSCert, 93 | wantError: false, 94 | }, 95 | { 96 | name: "secret found, but is not up to date", 97 | secret: &v1.Secret{ 98 | ObjectMeta: metav1.ObjectMeta{ 99 | ResourceVersion: "2", 100 | Name: "testSecret", 101 | Namespace: "test", 102 | }, 103 | Data: map[string][]byte{ 104 | v1.TLSCertKey: afterCert, 105 | v1.TLSPrivateKeyKey: afterKey, 106 | }, 107 | }, 108 | cachedVersion: "1", 109 | cachedCert: &beforeTLSCert, 110 | currentConn: &closeWrapper{id: 0}, 111 | otherConns: map[int]*closeWrapper{}, 112 | 113 | expectedCert: &afterTLSCert, 114 | wantError: false, 115 | }, 116 | { 117 | name: "secret found, is not up to date, and we have conns using current cert", 118 | secret: &v1.Secret{ 119 | ObjectMeta: metav1.ObjectMeta{ 120 | ResourceVersion: "2", 121 | Name: "testSecret", 122 | Namespace: "test", 123 | }, 124 | Data: map[string][]byte{ 125 | v1.TLSCertKey: afterCert, 126 | v1.TLSPrivateKeyKey: afterKey, 127 | }, 128 | }, 129 | cachedVersion: "1", 130 | cachedCert: &beforeTLSCert, 131 | currentConn: &closeWrapper{id: 0}, 132 | otherConns: map[int]*closeWrapper{ 133 | 1: { 134 | id: 1, 135 | ready: false, 136 | Conn: &fakeConn{}, 137 | }, 138 | 2: { 139 | id: 2, 140 | ready: true, 141 | Conn: &fakeConn{}, 142 | }, 143 | }, 144 | 145 | closedConns: []int{2}, 146 | expectedCert: &afterTLSCert, 147 | wantError: false, 148 | }, 149 | } 150 | for i := range tests { 151 | test := tests[i] 152 | t.Run(test.name, func(t *testing.T) { 153 | t.Parallel() 154 | testConns := test.otherConns 155 | if testConns != nil { 156 | testConns[test.currentConn.id] = test.currentConn 157 | // make sure our conn is listed as one of the current connections 158 | } 159 | l := listener{ 160 | cert: test.cachedCert, 161 | version: test.cachedVersion, 162 | storage: &MockTLSStorage{ 163 | Secret: test.secret, 164 | SecretErr: test.secretErr, 165 | }, 166 | conns: testConns, 167 | } 168 | for _, conn := range testConns { 169 | conn.l = &l 170 | } 171 | newCert, err := l.getCertificate(&tls.ClientHelloInfo{Conn: test.currentConn}) 172 | if test.wantError { 173 | assert.Errorf(t, err, "expected an error but none was provdied") 174 | } else { 175 | assert.NoError(t, err, "did not expect an error but got one") 176 | } 177 | assert.Equal(t, test.expectedCert, newCert, "expected cert did not match actual cert") 178 | if test.expectedCert != nil && test.wantError == false && test.currentConn != nil && test.otherConns != nil { 179 | assert.True(t, test.currentConn.ready, "expected connection to be ready but it was not") 180 | } else { 181 | if test.currentConn != nil { 182 | assert.False(t, test.currentConn.ready, "did not expect connection to be ready") 183 | } 184 | } 185 | for _, closedConn := range test.closedConns { 186 | _, ok := l.conns[closedConn] 187 | assert.False(t, ok, "closed conns should not be found") 188 | } 189 | }) 190 | } 191 | } 192 | 193 | func newCertificate() ([]byte, []byte, error) { 194 | cert, key, err := factory.GenCA() 195 | if err != nil { 196 | return nil, nil, err 197 | } 198 | 199 | return factory.MarshalChain(key, cert) 200 | } 201 | 202 | type MockTLSStorage struct { 203 | Secret *v1.Secret 204 | SecretErr error 205 | } 206 | 207 | func (m *MockTLSStorage) Get() (*v1.Secret, error) { 208 | return m.Secret, m.SecretErr 209 | } 210 | 211 | func (m *MockTLSStorage) Update(secret *v1.Secret) error { 212 | panic("Not implemented") 213 | } 214 | 215 | // adapted from k8s.io/apimachinery@v0.18.8/pkg/util.proxy/ugradeaware_test.go 216 | type fakeConn struct{} 217 | 218 | func (f *fakeConn) Read([]byte) (int, error) { return 0, nil } 219 | func (f *fakeConn) Write([]byte) (int, error) { return 0, nil } 220 | func (f *fakeConn) Close() error { return nil } 221 | func (fakeConn) LocalAddr() net.Addr { return nil } 222 | func (fakeConn) RemoteAddr() net.Addr { return nil } 223 | func (fakeConn) SetDeadline(t time.Time) error { return nil } 224 | func (fakeConn) SetReadDeadline(t time.Time) error { return nil } 225 | func (fakeConn) SetWriteDeadline(t time.Time) error { return nil } 226 | -------------------------------------------------------------------------------- /server/server.go: -------------------------------------------------------------------------------- 1 | package server 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "crypto" 7 | "crypto/tls" 8 | "crypto/x509" 9 | "fmt" 10 | "io" 11 | "log" 12 | "net" 13 | "net/http" 14 | 15 | "github.com/rancher/dynamiclistener" 16 | "github.com/rancher/dynamiclistener/factory" 17 | "github.com/rancher/dynamiclistener/storage/file" 18 | "github.com/rancher/dynamiclistener/storage/kubernetes" 19 | "github.com/rancher/dynamiclistener/storage/memory" 20 | v1 "github.com/rancher/wrangler/v3/pkg/generated/controllers/core/v1" 21 | "github.com/sirupsen/logrus" 22 | "golang.org/x/crypto/acme/autocert" 23 | ) 24 | 25 | type ListenOpts struct { 26 | CAChain []*x509.Certificate 27 | // Deprecated: Use CAChain instead 28 | CA *x509.Certificate 29 | CAKey crypto.Signer 30 | Storage dynamiclistener.TLSStorage 31 | Secrets v1.SecretController 32 | CertNamespace string 33 | CertName string 34 | CANamespace string 35 | CAName string 36 | CertBackup string 37 | AcmeDomains []string 38 | BindHost string 39 | NoRedirect bool 40 | TLSListenerConfig dynamiclistener.Config 41 | 42 | // Override legacy behavior where server logs written to the application's logrus object 43 | // were dropped unless logrus was set to debug-level (such as by launching steve with '--debug'). 44 | // Setting this to true results in server logs appearing at an ERROR level. 45 | DisplayServerLogs bool 46 | IgnoreTLSHandshakeError bool 47 | } 48 | 49 | var TLSHandshakeError = []byte("http: TLS handshake error") 50 | 51 | var _ io.Writer = &TLSErrorDebugger{} 52 | 53 | type TLSErrorDebugger struct{} 54 | 55 | func (t *TLSErrorDebugger) Write(p []byte) (n int, err error) { 56 | p = bytes.TrimSpace(p) 57 | if bytes.HasPrefix(p, TLSHandshakeError) { 58 | logrus.Debug(string(p)) 59 | } else { 60 | logrus.Error(string(p)) 61 | } 62 | return len(p), err 63 | } 64 | 65 | func ListenAndServe(ctx context.Context, httpsPort, httpPort int, handler http.Handler, opts *ListenOpts) error { 66 | logger := logrus.StandardLogger() 67 | writer := logger.WriterLevel(logrus.DebugLevel) 68 | if opts == nil { 69 | opts = &ListenOpts{} 70 | } 71 | if opts.DisplayServerLogs { 72 | writer = logger.WriterLevel(logrus.ErrorLevel) 73 | } 74 | 75 | var errorLog *log.Logger 76 | if opts.IgnoreTLSHandshakeError { 77 | debugWriter := &TLSErrorDebugger{} 78 | errorLog = log.New(debugWriter, "", 0) 79 | } else { 80 | // Otherwise preserve legacy behaviour of displaying server logs only in debug mode. 81 | errorLog = log.New(writer, "", 0) 82 | } 83 | 84 | if opts.TLSListenerConfig.TLSConfig == nil { 85 | opts.TLSListenerConfig.TLSConfig = &tls.Config{} 86 | } 87 | 88 | if httpsPort > 0 { 89 | tlsTCPListener, err := dynamiclistener.NewTCPListener(opts.BindHost, httpsPort) 90 | if err != nil { 91 | return err 92 | } 93 | 94 | tlsTCPListener, handler, err = getTLSListener(ctx, tlsTCPListener, handler, *opts) 95 | if err != nil { 96 | return err 97 | } 98 | 99 | if !opts.NoRedirect { 100 | handler = dynamiclistener.HTTPRedirect(handler) 101 | } 102 | 103 | tlsServer := http.Server{ 104 | Handler: handler, 105 | BaseContext: func(listener net.Listener) context.Context { 106 | return ctx 107 | }, 108 | ErrorLog: errorLog, 109 | } 110 | 111 | go func() { 112 | logrus.Infof("Listening on %s:%d", opts.BindHost, httpsPort) 113 | err := tlsServer.Serve(tlsTCPListener) 114 | if err != http.ErrServerClosed && err != nil { 115 | logrus.Fatalf("https server failed: %v", err) 116 | } 117 | }() 118 | go func() { 119 | <-ctx.Done() 120 | tlsServer.Shutdown(context.Background()) 121 | }() 122 | } 123 | 124 | if httpPort > 0 { 125 | httpServer := http.Server{ 126 | Addr: fmt.Sprintf("%s:%d", opts.BindHost, httpPort), 127 | Handler: handler, 128 | ErrorLog: errorLog, 129 | BaseContext: func(listener net.Listener) context.Context { 130 | return ctx 131 | }, 132 | } 133 | go func() { 134 | logrus.Infof("Listening on %s:%d", opts.BindHost, httpPort) 135 | err := httpServer.ListenAndServe() 136 | if err != http.ErrServerClosed && err != nil { 137 | logrus.Fatalf("http server failed: %v", err) 138 | } 139 | }() 140 | go func() { 141 | <-ctx.Done() 142 | httpServer.Shutdown(context.Background()) 143 | }() 144 | } 145 | 146 | return nil 147 | } 148 | 149 | func getTLSListener(ctx context.Context, tcp net.Listener, handler http.Handler, opts ListenOpts) (net.Listener, http.Handler, error) { 150 | if len(opts.TLSListenerConfig.TLSConfig.NextProtos) == 0 { 151 | opts.TLSListenerConfig.TLSConfig.NextProtos = []string{"h2", "http/1.1"} 152 | } 153 | 154 | if len(opts.TLSListenerConfig.TLSConfig.Certificates) > 0 { 155 | return tls.NewListener(tcp, opts.TLSListenerConfig.TLSConfig), handler, nil 156 | } 157 | 158 | if len(opts.AcmeDomains) > 0 { 159 | return acmeListener(tcp, handler, opts) 160 | } 161 | 162 | storage := opts.Storage 163 | if storage == nil { 164 | storage = newStorage(ctx, opts) 165 | } 166 | 167 | caCert, caKey, err := getCA(opts) 168 | if err != nil { 169 | return nil, nil, err 170 | } 171 | 172 | listener, dynHandler, err := dynamiclistener.NewListenerWithChain(tcp, storage, caCert, caKey, opts.TLSListenerConfig) 173 | if err != nil { 174 | return nil, nil, err 175 | } 176 | 177 | return listener, wrapHandler(dynHandler, handler), nil 178 | } 179 | 180 | func getCA(opts ListenOpts) ([]*x509.Certificate, crypto.Signer, error) { 181 | if opts.CAKey != nil { 182 | if opts.CAChain != nil { 183 | return opts.CAChain, opts.CAKey, nil 184 | } else if opts.CA != nil { 185 | return []*x509.Certificate{opts.CA}, opts.CAKey, nil 186 | } 187 | } 188 | 189 | if opts.Secrets == nil { 190 | return factory.LoadOrGenCAChain() 191 | } 192 | 193 | if opts.CAName == "" { 194 | opts.CAName = "serving-ca" 195 | } 196 | 197 | if opts.CANamespace == "" { 198 | opts.CANamespace = opts.CertNamespace 199 | } 200 | 201 | if opts.CANamespace == "" { 202 | opts.CANamespace = "kube-system" 203 | } 204 | 205 | return kubernetes.LoadOrGenCAChain(opts.Secrets, opts.CANamespace, opts.CAName) 206 | } 207 | 208 | func newStorage(ctx context.Context, opts ListenOpts) dynamiclistener.TLSStorage { 209 | var result dynamiclistener.TLSStorage 210 | if opts.CertBackup == "" { 211 | result = memory.New() 212 | } else { 213 | result = memory.NewBacked(file.New(opts.CertBackup)) 214 | } 215 | 216 | if opts.Secrets == nil { 217 | return result 218 | } 219 | 220 | if opts.CertName == "" { 221 | opts.CertName = "serving-cert" 222 | } 223 | 224 | if opts.CertNamespace == "" { 225 | opts.CertNamespace = "kube-system" 226 | } 227 | 228 | return kubernetes.Load(ctx, opts.Secrets, opts.CertNamespace, opts.CertName, result) 229 | } 230 | 231 | func wrapHandler(handler http.Handler, next http.Handler) http.Handler { 232 | return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { 233 | handler.ServeHTTP(rw, req) 234 | next.ServeHTTP(rw, req) 235 | }) 236 | } 237 | 238 | func acmeListener(tcp net.Listener, handler http.Handler, opts ListenOpts) (net.Listener, http.Handler, error) { 239 | hosts := map[string]bool{} 240 | for _, domain := range opts.AcmeDomains { 241 | hosts[domain] = true 242 | } 243 | 244 | manager := autocert.Manager{ 245 | Cache: autocert.DirCache("certs-cache"), 246 | Prompt: func(tosURL string) bool { 247 | return true 248 | }, 249 | HostPolicy: func(ctx context.Context, host string) error { 250 | if !hosts[host] { 251 | return fmt.Errorf("host %s is not configured", host) 252 | } 253 | return nil 254 | }, 255 | } 256 | 257 | opts.TLSListenerConfig.TLSConfig.GetCertificate = func(hello *tls.ClientHelloInfo) (*tls.Certificate, error) { 258 | if hello.ServerName == "localhost" || hello.ServerName == "" { 259 | newHello := *hello 260 | newHello.ServerName = opts.AcmeDomains[0] 261 | return manager.GetCertificate(&newHello) 262 | } 263 | return manager.GetCertificate(hello) 264 | } 265 | 266 | return tls.NewListener(tcp, opts.TLSListenerConfig.TLSConfig), manager.HTTPHandler(handler), nil 267 | } 268 | -------------------------------------------------------------------------------- /cert/pem.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2014 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package cert 18 | 19 | import ( 20 | "crypto/ecdsa" 21 | "crypto/rsa" 22 | "crypto/x509" 23 | "encoding/pem" 24 | "errors" 25 | "fmt" 26 | ) 27 | 28 | const ( 29 | // ECPrivateKeyBlockType is a possible value for pem.Block.Type. 30 | ECPrivateKeyBlockType = "EC PRIVATE KEY" 31 | // RSAPrivateKeyBlockType is a possible value for pem.Block.Type. 32 | RSAPrivateKeyBlockType = "RSA PRIVATE KEY" 33 | // PrivateKeyBlockType is a possible value for pem.Block.Type. 34 | PrivateKeyBlockType = "PRIVATE KEY" 35 | // PublicKeyBlockType is a possible value for pem.Block.Type. 36 | PublicKeyBlockType = "PUBLIC KEY" 37 | // CertificateBlockType is a possible value for pem.Block.Type. 38 | CertificateBlockType = "CERTIFICATE" 39 | // CertificateRequestBlockType is a possible value for pem.Block.Type. 40 | CertificateRequestBlockType = "CERTIFICATE REQUEST" 41 | ) 42 | 43 | // EncodePublicKeyPEM returns PEM-encoded public data 44 | func EncodePublicKeyPEM(key *rsa.PublicKey) ([]byte, error) { 45 | der, err := x509.MarshalPKIXPublicKey(key) 46 | if err != nil { 47 | return []byte{}, err 48 | } 49 | block := pem.Block{ 50 | Type: PublicKeyBlockType, 51 | Bytes: der, 52 | } 53 | return pem.EncodeToMemory(&block), nil 54 | } 55 | 56 | // EncodePrivateKeyPEM returns PEM-encoded private key data 57 | func EncodePrivateKeyPEM(key *rsa.PrivateKey) []byte { 58 | block := pem.Block{ 59 | Type: RSAPrivateKeyBlockType, 60 | Bytes: x509.MarshalPKCS1PrivateKey(key), 61 | } 62 | return pem.EncodeToMemory(&block) 63 | } 64 | 65 | // EncodeCertPEM returns PEM-endcoded certificate data 66 | func EncodeCertPEM(cert *x509.Certificate) []byte { 67 | block := pem.Block{ 68 | Type: CertificateBlockType, 69 | Bytes: cert.Raw, 70 | } 71 | return pem.EncodeToMemory(&block) 72 | } 73 | 74 | // ParsePrivateKeyPEM returns a private key parsed from a PEM block in the supplied data. 75 | // Recognizes PEM blocks for "EC PRIVATE KEY", "RSA PRIVATE KEY", or "PRIVATE KEY" 76 | func ParsePrivateKeyPEM(keyData []byte) (interface{}, error) { 77 | var privateKeyPemBlock *pem.Block 78 | for { 79 | privateKeyPemBlock, keyData = pem.Decode(keyData) 80 | if privateKeyPemBlock == nil { 81 | break 82 | } 83 | 84 | switch privateKeyPemBlock.Type { 85 | case ECPrivateKeyBlockType: 86 | // ECDSA Private Key in ASN.1 format 87 | if key, err := x509.ParseECPrivateKey(privateKeyPemBlock.Bytes); err == nil { 88 | return key, nil 89 | } 90 | case RSAPrivateKeyBlockType: 91 | // RSA Private Key in PKCS#1 format 92 | if key, err := x509.ParsePKCS1PrivateKey(privateKeyPemBlock.Bytes); err == nil { 93 | return key, nil 94 | } 95 | case PrivateKeyBlockType: 96 | // RSA or ECDSA Private Key in unencrypted PKCS#8 format 97 | if key, err := x509.ParsePKCS8PrivateKey(privateKeyPemBlock.Bytes); err == nil { 98 | return key, nil 99 | } 100 | } 101 | 102 | // tolerate non-key PEM blocks for compatibility with things like "EC PARAMETERS" blocks 103 | // originally, only the first PEM block was parsed and expected to be a key block 104 | } 105 | 106 | // we read all the PEM blocks and didn't recognize one 107 | return nil, fmt.Errorf("data does not contain a valid RSA or ECDSA private key") 108 | } 109 | 110 | // ParsePublicKeysPEM is a helper function for reading an array of rsa.PublicKey or ecdsa.PublicKey from a PEM-encoded byte array. 111 | // Reads public keys from both public and private key files. 112 | func ParsePublicKeysPEM(keyData []byte) ([]interface{}, error) { 113 | var block *pem.Block 114 | keys := []interface{}{} 115 | for { 116 | // read the next block 117 | block, keyData = pem.Decode(keyData) 118 | if block == nil { 119 | break 120 | } 121 | 122 | // test block against parsing functions 123 | if privateKey, err := parseRSAPrivateKey(block.Bytes); err == nil { 124 | keys = append(keys, &privateKey.PublicKey) 125 | continue 126 | } 127 | if publicKey, err := parseRSAPublicKey(block.Bytes); err == nil { 128 | keys = append(keys, publicKey) 129 | continue 130 | } 131 | if privateKey, err := parseECPrivateKey(block.Bytes); err == nil { 132 | keys = append(keys, &privateKey.PublicKey) 133 | continue 134 | } 135 | if publicKey, err := parseECPublicKey(block.Bytes); err == nil { 136 | keys = append(keys, publicKey) 137 | continue 138 | } 139 | 140 | // tolerate non-key PEM blocks for backwards compatibility 141 | // originally, only the first PEM block was parsed and expected to be a key block 142 | } 143 | 144 | if len(keys) == 0 { 145 | return nil, fmt.Errorf("data does not contain any valid RSA or ECDSA public keys") 146 | } 147 | return keys, nil 148 | } 149 | 150 | // ParseCertsPEM returns the x509.Certificates contained in the given PEM-encoded byte array 151 | // Returns an error if a certificate could not be parsed, or if the data does not contain any certificates 152 | func ParseCertsPEM(pemCerts []byte) ([]*x509.Certificate, error) { 153 | ok := false 154 | certs := []*x509.Certificate{} 155 | for len(pemCerts) > 0 { 156 | var block *pem.Block 157 | block, pemCerts = pem.Decode(pemCerts) 158 | if block == nil { 159 | break 160 | } 161 | // Only use PEM "CERTIFICATE" blocks without extra headers 162 | if block.Type != CertificateBlockType || len(block.Headers) != 0 { 163 | continue 164 | } 165 | 166 | cert, err := x509.ParseCertificate(block.Bytes) 167 | if err != nil { 168 | return certs, err 169 | } 170 | 171 | certs = append(certs, cert) 172 | ok = true 173 | } 174 | 175 | if !ok { 176 | return certs, errors.New("data does not contain any valid RSA or ECDSA certificates") 177 | } 178 | return certs, nil 179 | } 180 | 181 | // parseRSAPublicKey parses a single RSA public key from the provided data 182 | func parseRSAPublicKey(data []byte) (*rsa.PublicKey, error) { 183 | var err error 184 | 185 | // Parse the key 186 | var parsedKey interface{} 187 | if parsedKey, err = x509.ParsePKIXPublicKey(data); err != nil { 188 | if cert, err := x509.ParseCertificate(data); err == nil { 189 | parsedKey = cert.PublicKey 190 | } else { 191 | return nil, err 192 | } 193 | } 194 | 195 | // Test if parsed key is an RSA Public Key 196 | var pubKey *rsa.PublicKey 197 | var ok bool 198 | if pubKey, ok = parsedKey.(*rsa.PublicKey); !ok { 199 | return nil, fmt.Errorf("data doesn't contain valid RSA Public Key") 200 | } 201 | 202 | return pubKey, nil 203 | } 204 | 205 | // parseRSAPrivateKey parses a single RSA private key from the provided data 206 | func parseRSAPrivateKey(data []byte) (*rsa.PrivateKey, error) { 207 | var err error 208 | 209 | // Parse the key 210 | var parsedKey interface{} 211 | if parsedKey, err = x509.ParsePKCS1PrivateKey(data); err != nil { 212 | if parsedKey, err = x509.ParsePKCS8PrivateKey(data); err != nil { 213 | return nil, err 214 | } 215 | } 216 | 217 | // Test if parsed key is an RSA Private Key 218 | var privKey *rsa.PrivateKey 219 | var ok bool 220 | if privKey, ok = parsedKey.(*rsa.PrivateKey); !ok { 221 | return nil, fmt.Errorf("data doesn't contain valid RSA Private Key") 222 | } 223 | 224 | return privKey, nil 225 | } 226 | 227 | // parseECPublicKey parses a single ECDSA public key from the provided data 228 | func parseECPublicKey(data []byte) (*ecdsa.PublicKey, error) { 229 | var err error 230 | 231 | // Parse the key 232 | var parsedKey interface{} 233 | if parsedKey, err = x509.ParsePKIXPublicKey(data); err != nil { 234 | if cert, err := x509.ParseCertificate(data); err == nil { 235 | parsedKey = cert.PublicKey 236 | } else { 237 | return nil, err 238 | } 239 | } 240 | 241 | // Test if parsed key is an ECDSA Public Key 242 | var pubKey *ecdsa.PublicKey 243 | var ok bool 244 | if pubKey, ok = parsedKey.(*ecdsa.PublicKey); !ok { 245 | return nil, fmt.Errorf("data doesn't contain valid ECDSA Public Key") 246 | } 247 | 248 | return pubKey, nil 249 | } 250 | 251 | // parseECPrivateKey parses a single ECDSA private key from the provided data 252 | func parseECPrivateKey(data []byte) (*ecdsa.PrivateKey, error) { 253 | var err error 254 | 255 | // Parse the key 256 | var parsedKey interface{} 257 | if parsedKey, err = x509.ParseECPrivateKey(data); err != nil { 258 | return nil, err 259 | } 260 | 261 | // Test if parsed key is an ECDSA Private Key 262 | var privKey *ecdsa.PrivateKey 263 | var ok bool 264 | if privKey, ok = parsedKey.(*ecdsa.PrivateKey); !ok { 265 | return nil, fmt.Errorf("data doesn't contain valid ECDSA Private Key") 266 | } 267 | 268 | return privKey, nil 269 | } 270 | -------------------------------------------------------------------------------- /storage/kubernetes/controller.go: -------------------------------------------------------------------------------- 1 | package kubernetes 2 | 3 | import ( 4 | "context" 5 | "maps" 6 | "time" 7 | 8 | "github.com/rancher/dynamiclistener" 9 | "github.com/rancher/dynamiclistener/cert" 10 | "github.com/rancher/wrangler/v3/pkg/generated/controllers/core" 11 | v1controller "github.com/rancher/wrangler/v3/pkg/generated/controllers/core/v1" 12 | "github.com/sirupsen/logrus" 13 | v1 "k8s.io/api/core/v1" 14 | "k8s.io/apimachinery/pkg/api/errors" 15 | metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 16 | "k8s.io/apimachinery/pkg/fields" 17 | "k8s.io/apimachinery/pkg/runtime" 18 | "k8s.io/apimachinery/pkg/util/wait" 19 | "k8s.io/apimachinery/pkg/watch" 20 | "k8s.io/client-go/tools/cache" 21 | toolswatch "k8s.io/client-go/tools/watch" 22 | "k8s.io/client-go/util/retry" 23 | "k8s.io/client-go/util/workqueue" 24 | ) 25 | 26 | type CoreGetter func() *core.Factory 27 | 28 | type storage struct { 29 | namespace, name string 30 | storage dynamiclistener.TLSStorage 31 | secrets v1controller.SecretController 32 | tls dynamiclistener.TLSFactory 33 | queue workqueue.TypedInterface[string] 34 | queuedSecret *v1.Secret 35 | } 36 | 37 | func Load(ctx context.Context, secrets v1controller.SecretController, namespace, name string, backing dynamiclistener.TLSStorage) dynamiclistener.TLSStorage { 38 | storage := &storage{ 39 | name: name, 40 | namespace: namespace, 41 | storage: backing, 42 | queue: workqueue.NewTyped[string](), 43 | } 44 | storage.runQueue() 45 | storage.init(ctx, secrets) 46 | return storage 47 | } 48 | 49 | func New(ctx context.Context, core CoreGetter, namespace, name string, backing dynamiclistener.TLSStorage) dynamiclistener.TLSStorage { 50 | storage := &storage{ 51 | name: name, 52 | namespace: namespace, 53 | storage: backing, 54 | queue: workqueue.NewTyped[string](), 55 | } 56 | storage.runQueue() 57 | 58 | // lazy init 59 | go func() { 60 | wait.PollImmediateUntilWithContext(ctx, time.Second, func(cxt context.Context) (bool, error) { 61 | if coreFactory := core(); coreFactory != nil { 62 | storage.init(ctx, coreFactory.Core().V1().Secret()) 63 | return true, nil 64 | } 65 | return false, nil 66 | }) 67 | }() 68 | 69 | return storage 70 | } 71 | 72 | // always return secret from backing storage 73 | func (s *storage) Get() (*v1.Secret, error) { 74 | return s.storage.Get() 75 | } 76 | 77 | // sync secret to Kubernetes and backing storage via workqueue 78 | func (s *storage) Update(secret *v1.Secret) error { 79 | // Asynchronously update the Kubernetes secret, as doing so inline may block the listener from 80 | // accepting new connections if the apiserver becomes unavailable after the Secrets controller 81 | // has been initialized. 82 | s.queuedSecret = secret 83 | s.queue.Add(s.name) 84 | return nil 85 | } 86 | 87 | func (s *storage) SetFactory(tls dynamiclistener.TLSFactory) { 88 | s.tls = tls 89 | } 90 | 91 | func (s *storage) init(ctx context.Context, secrets v1controller.SecretController) { 92 | s.secrets = secrets 93 | 94 | // Watch just the target secret, instead of using a wrangler OnChange handler 95 | // which watches all secrets in all namespaces. Changes to the secret 96 | // will be sent through the workqueue. 97 | go func() { 98 | fieldSelector := fields.Set{"metadata.name": s.name}.String() 99 | lw := &cache.ListWatch{ 100 | ListFunc: func(options metav1.ListOptions) (object runtime.Object, e error) { 101 | options.FieldSelector = fieldSelector 102 | return secrets.List(s.namespace, options) 103 | }, 104 | WatchFunc: func(options metav1.ListOptions) (i watch.Interface, e error) { 105 | options.FieldSelector = fieldSelector 106 | return secrets.Watch(s.namespace, options) 107 | }, 108 | } 109 | _, _, watch, done := toolswatch.NewIndexerInformerWatcher(lw, &v1.Secret{}) 110 | 111 | defer func() { 112 | s.queue.ShutDown() 113 | watch.Stop() 114 | <-done 115 | }() 116 | 117 | for { 118 | select { 119 | case <-ctx.Done(): 120 | return 121 | case ev := <-watch.ResultChan(): 122 | if secret, ok := ev.Object.(*v1.Secret); ok { 123 | s.queuedSecret = secret 124 | s.queue.Add(secret.Name) 125 | } 126 | } 127 | } 128 | }() 129 | 130 | // enqueue initial sync of the backing secret 131 | s.queuedSecret, _ = s.Get() 132 | s.queue.Add(s.name) 133 | } 134 | 135 | // runQueue starts a goroutine to process secrets updates from the workqueue 136 | func (s *storage) runQueue() { 137 | go func() { 138 | for s.processQueue() { 139 | } 140 | }() 141 | } 142 | 143 | // processQueue processes the secret update queue. 144 | // The key doesn't actually matter, as we are only handling a single secret with a single worker. 145 | func (s *storage) processQueue() bool { 146 | key, shutdown := s.queue.Get() 147 | if shutdown { 148 | return false 149 | } 150 | 151 | defer s.queue.Done(key) 152 | if err := s.update(); err != nil { 153 | logrus.Errorf("Failed to update Secret %s/%s: %v", s.namespace, s.name, err) 154 | } 155 | 156 | return true 157 | } 158 | 159 | func (s *storage) targetSecret() (*v1.Secret, error) { 160 | existingSecret, err := s.secrets.Get(s.namespace, s.name, metav1.GetOptions{}) 161 | if errors.IsNotFound(err) { 162 | return &v1.Secret{ 163 | ObjectMeta: metav1.ObjectMeta{ 164 | Name: s.name, 165 | Namespace: s.namespace, 166 | }, 167 | Type: v1.SecretTypeTLS, 168 | }, nil 169 | } 170 | return existingSecret, err 171 | } 172 | 173 | // saveInK8s handles merging the provided secret with the kubernetes secret. 174 | // This includes calling the tls factory to sign a new certificate with the 175 | // merged SAN entries, if possible. Note that the provided secret could be 176 | // either from Kubernetes due to the secret being changed by another client, or 177 | // from the listener trying to add SANs or regenerate the cert. 178 | func (s *storage) saveInK8s(secret *v1.Secret) (*v1.Secret, error) { 179 | // secret controller not initialized yet, just return the current secret. 180 | // if there is an existing secret in Kubernetes, that will get synced by the 181 | // list/watch once the controller is initialized. 182 | if s.secrets == nil { 183 | return secret, nil 184 | } 185 | 186 | targetSecret, err := s.targetSecret() 187 | if err != nil { 188 | return nil, err 189 | } 190 | 191 | // if we don't have a TLS factory we can't create certs, so don't bother trying to merge anything, 192 | // in favor of just blindly replacing the fields on the Kubernetes secret. 193 | if s.tls != nil { 194 | // merge new secret with secret from backing storage, if one exists 195 | if existing, err := s.Get(); err == nil && cert.IsValidTLSSecret(existing) { 196 | if newSecret, updated, err := s.tls.Merge(existing, secret); err == nil && updated { 197 | secret = newSecret 198 | } 199 | } 200 | 201 | // merge new secret with existing secret from Kubernetes, if one exists 202 | if cert.IsValidTLSSecret(targetSecret) { 203 | if newSecret, updated, err := s.tls.Merge(targetSecret, secret); err != nil { 204 | return nil, err 205 | } else if !updated { 206 | return newSecret, nil 207 | } else { 208 | secret = newSecret 209 | } 210 | } 211 | } 212 | 213 | // ensure that the merged secret actually contains data before overwriting the existing fields 214 | if !cert.IsValidTLSSecret(secret) { 215 | logrus.Warnf("Skipping save of TLS secret for %s/%s due to missing certificate data", s.namespace, s.name) 216 | return targetSecret, nil 217 | } 218 | 219 | // Any changes to the cert will change the fingerprint annotation, so we can use that 220 | // for change detection, and skip updating an existing secret if it has not changed. 221 | changed := !maps.Equal(targetSecret.Annotations, secret.Annotations) 222 | 223 | targetSecret.Type = v1.SecretTypeTLS 224 | targetSecret.Annotations = secret.Annotations 225 | targetSecret.Data = secret.Data 226 | 227 | if targetSecret.UID == "" { 228 | logrus.Infof("Creating new TLS secret for %s/%s (count: %d): %v", targetSecret.Namespace, targetSecret.Name, len(targetSecret.Annotations)-1, targetSecret.Annotations) 229 | return s.secrets.Create(targetSecret) 230 | } else if changed { 231 | logrus.Infof("Updating TLS secret for %s/%s (count: %d): %v", targetSecret.Namespace, targetSecret.Name, len(targetSecret.Annotations)-1, targetSecret.Annotations) 232 | return s.secrets.Update(targetSecret) 233 | } 234 | return targetSecret, nil 235 | } 236 | 237 | func isConflictOrAlreadyExists(err error) bool { 238 | return errors.IsConflict(err) || errors.IsAlreadyExists(err) 239 | } 240 | 241 | // update wraps a conflict retry around saveInK8s, which handles merging the 242 | // queued secret with the Kubernetes secret. Only after successfully 243 | // updating the Kubernetes secret will the backing storage be updated. 244 | func (s *storage) update() (err error) { 245 | var newSecret *v1.Secret 246 | if err := retry.OnError(retry.DefaultRetry, isConflictOrAlreadyExists, func() error { 247 | newSecret, err = s.saveInK8s(s.queuedSecret) 248 | return err 249 | }); err != nil { 250 | return err 251 | } 252 | return s.storage.Update(newSecret) 253 | } 254 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | -------------------------------------------------------------------------------- /cert/cert.go: -------------------------------------------------------------------------------- 1 | /* 2 | Copyright 2014 The Kubernetes Authors. 3 | 4 | Licensed under the Apache License, Version 2.0 (the "License"); 5 | you may not use this file except in compliance with the License. 6 | You may obtain a copy of the License at 7 | 8 | http://www.apache.org/licenses/LICENSE-2.0 9 | 10 | Unless required by applicable law or agreed to in writing, software 11 | distributed under the License is distributed on an "AS IS" BASIS, 12 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | See the License for the specific language governing permissions and 14 | limitations under the License. 15 | */ 16 | 17 | package cert 18 | 19 | import ( 20 | "bytes" 21 | "crypto" 22 | "crypto/ecdsa" 23 | "crypto/elliptic" 24 | "crypto/rand" 25 | cryptorand "crypto/rand" 26 | "crypto/rsa" 27 | "crypto/x509" 28 | "crypto/x509/pkix" 29 | "encoding/pem" 30 | "errors" 31 | "fmt" 32 | "io/ioutil" 33 | "math" 34 | "math/big" 35 | "net" 36 | "os" 37 | "path/filepath" 38 | "strconv" 39 | "strings" 40 | "time" 41 | 42 | "github.com/sirupsen/logrus" 43 | ) 44 | 45 | const ( 46 | rsaKeySize = 2048 47 | duration365d = time.Hour * 24 * 365 48 | ) 49 | 50 | var ErrStaticCert = errors.New("cannot renew static certificate") 51 | 52 | // Config contains the basic fields required for creating a certificate. 53 | type Config struct { 54 | CommonName string 55 | Organization []string 56 | AltNames AltNames 57 | Usages []x509.ExtKeyUsage 58 | ExpiresAt time.Duration 59 | } 60 | 61 | // AltNames contains the domain names and IP addresses that will be added 62 | // to the API Server's x509 certificate SubAltNames field. The values will 63 | // be passed directly to the x509.Certificate object. 64 | type AltNames struct { 65 | DNSNames []string 66 | IPs []net.IP 67 | } 68 | 69 | // NewPrivateKey creates an RSA private key 70 | func NewPrivateKey() (*rsa.PrivateKey, error) { 71 | return rsa.GenerateKey(cryptorand.Reader, rsaKeySize) 72 | } 73 | 74 | // NewSelfSignedCACert creates a CA certificate 75 | func NewSelfSignedCACert(cfg Config, key crypto.Signer) (*x509.Certificate, error) { 76 | notBefore := CalculateNotBefore(nil) 77 | tmpl := x509.Certificate{ 78 | SerialNumber: new(big.Int).SetInt64(0), 79 | Subject: pkix.Name{ 80 | CommonName: cfg.CommonName, 81 | Organization: cfg.Organization, 82 | }, 83 | NotBefore: notBefore, 84 | NotAfter: notBefore.Add(duration365d * 10), 85 | KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign, 86 | BasicConstraintsValid: true, 87 | IsCA: true, 88 | } 89 | 90 | certDERBytes, err := x509.CreateCertificate(cryptorand.Reader, &tmpl, &tmpl, key.Public(), key) 91 | if err != nil { 92 | return nil, err 93 | } 94 | 95 | logrus.Infof("generated self-signed CA certificate %s: notBefore=%s notAfter=%s", 96 | tmpl.Subject, tmpl.NotBefore, tmpl.NotAfter) 97 | 98 | return x509.ParseCertificate(certDERBytes) 99 | } 100 | 101 | // NewSignedCert creates a signed certificate using the given CA certificate and key based 102 | // on the given configuration. 103 | func NewSignedCert(cfg Config, key crypto.Signer, caCert *x509.Certificate, caKey crypto.Signer) (*x509.Certificate, error) { 104 | serial, err := rand.Int(rand.Reader, new(big.Int).SetInt64(math.MaxInt64)) 105 | if err != nil { 106 | return nil, err 107 | } 108 | if len(cfg.CommonName) == 0 { 109 | return nil, errors.New("must specify a CommonName") 110 | } 111 | if len(cfg.Usages) == 0 { 112 | return nil, errors.New("must specify at least one ExtKeyUsage") 113 | } 114 | expiresAt := duration365d 115 | if cfg.ExpiresAt > 0 { 116 | expiresAt = time.Duration(cfg.ExpiresAt) 117 | } else { 118 | envExpirationDays := os.Getenv("CATTLE_NEW_SIGNED_CERT_EXPIRATION_DAYS") 119 | if envExpirationDays != "" { 120 | if envExpirationDaysInt, err := strconv.Atoi(envExpirationDays); err != nil { 121 | logrus.Infof("[NewSignedCert] expiration days from ENV (%s) could not be converted to int (falling back to default value: %d)", envExpirationDays, expiresAt) 122 | } else { 123 | expiresAt = time.Hour * 24 * time.Duration(envExpirationDaysInt) 124 | } 125 | } 126 | } 127 | 128 | notBefore := CalculateNotBefore(caCert) 129 | certTmpl := x509.Certificate{ 130 | Subject: pkix.Name{ 131 | CommonName: cfg.CommonName, 132 | Organization: cfg.Organization, 133 | }, 134 | DNSNames: cfg.AltNames.DNSNames, 135 | IPAddresses: cfg.AltNames.IPs, 136 | SerialNumber: serial, 137 | NotBefore: notBefore, 138 | NotAfter: notBefore.Add(expiresAt), 139 | KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature, 140 | ExtKeyUsage: cfg.Usages, 141 | } 142 | certDERBytes, err := x509.CreateCertificate(cryptorand.Reader, &certTmpl, caCert, key.Public(), caKey) 143 | if err != nil { 144 | return nil, err 145 | } 146 | 147 | parsedCert, err := x509.ParseCertificate(certDERBytes) 148 | if err == nil { 149 | logrus.Infof("certificate %s signed by %s: notBefore=%s notAfter=%s", 150 | parsedCert.Subject, caCert.Subject, parsedCert.NotBefore, parsedCert.NotAfter) 151 | } 152 | return parsedCert, err 153 | } 154 | 155 | // MakeEllipticPrivateKeyPEM creates an ECDSA private key 156 | func MakeEllipticPrivateKeyPEM() ([]byte, error) { 157 | privateKey, err := ecdsa.GenerateKey(elliptic.P256(), cryptorand.Reader) 158 | if err != nil { 159 | return nil, err 160 | } 161 | 162 | derBytes, err := x509.MarshalECPrivateKey(privateKey) 163 | if err != nil { 164 | return nil, err 165 | } 166 | 167 | privateKeyPemBlock := &pem.Block{ 168 | Type: ECPrivateKeyBlockType, 169 | Bytes: derBytes, 170 | } 171 | return pem.EncodeToMemory(privateKeyPemBlock), nil 172 | } 173 | 174 | // GenerateSelfSignedCertKey creates a self-signed certificate and key for the given host. 175 | // Host may be an IP or a DNS name 176 | // You may also specify additional subject alt names (either ip or dns names) for the certificate. 177 | func GenerateSelfSignedCertKey(host string, alternateIPs []net.IP, alternateDNS []string) ([]byte, []byte, error) { 178 | return GenerateSelfSignedCertKeyWithFixtures(host, alternateIPs, alternateDNS, "") 179 | } 180 | 181 | // GenerateSelfSignedCertKeyWithFixtures creates a self-signed certificate and key for the given host. 182 | // Host may be an IP or a DNS name. You may also specify additional subject alt names (either ip or dns names) 183 | // for the certificate. 184 | // 185 | // If fixtureDirectory is non-empty, it is a directory path which can contain pre-generated certs. The format is: 186 | // _-_-.crt 187 | // _-_-.key 188 | // Certs/keys not existing in that directory are created. 189 | func GenerateSelfSignedCertKeyWithFixtures(host string, alternateIPs []net.IP, alternateDNS []string, fixtureDirectory string) ([]byte, []byte, error) { 190 | notBefore := CalculateNotBefore(nil) 191 | maxAge := time.Hour * 24 * 365 // one year self-signed certs 192 | 193 | baseName := fmt.Sprintf("%s_%s_%s", host, strings.Join(ipsToStrings(alternateIPs), "-"), strings.Join(alternateDNS, "-")) 194 | certFixturePath := filepath.Join(fixtureDirectory, baseName+".crt") 195 | keyFixturePath := filepath.Join(fixtureDirectory, baseName+".key") 196 | if len(fixtureDirectory) > 0 { 197 | cert, err := ioutil.ReadFile(certFixturePath) 198 | if err == nil { 199 | key, err := ioutil.ReadFile(keyFixturePath) 200 | if err == nil { 201 | return cert, key, nil 202 | } 203 | return nil, nil, fmt.Errorf("cert %s can be read, but key %s cannot: %v", certFixturePath, keyFixturePath, err) 204 | } 205 | maxAge = 100 * time.Hour * 24 * 365 // 100 years fixtures 206 | } 207 | 208 | caKey, err := rsa.GenerateKey(cryptorand.Reader, 2048) 209 | if err != nil { 210 | return nil, nil, err 211 | } 212 | 213 | caTemplate := x509.Certificate{ 214 | SerialNumber: big.NewInt(1), 215 | Subject: pkix.Name{ 216 | CommonName: fmt.Sprintf("%s-ca@%d", host, time.Now().Unix()), 217 | }, 218 | NotBefore: notBefore, 219 | NotAfter: notBefore.Add(maxAge), 220 | 221 | KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature | x509.KeyUsageCertSign, 222 | BasicConstraintsValid: true, 223 | IsCA: true, 224 | } 225 | 226 | caDERBytes, err := x509.CreateCertificate(cryptorand.Reader, &caTemplate, &caTemplate, &caKey.PublicKey, caKey) 227 | if err != nil { 228 | return nil, nil, err 229 | } 230 | 231 | caCertificate, err := x509.ParseCertificate(caDERBytes) 232 | if err != nil { 233 | return nil, nil, err 234 | } 235 | 236 | priv, err := rsa.GenerateKey(cryptorand.Reader, 2048) 237 | if err != nil { 238 | return nil, nil, err 239 | } 240 | 241 | template := x509.Certificate{ 242 | SerialNumber: big.NewInt(2), 243 | Subject: pkix.Name{ 244 | CommonName: fmt.Sprintf("%s@%d", host, time.Now().Unix()), 245 | }, 246 | NotBefore: notBefore, 247 | NotAfter: notBefore.Add(maxAge), 248 | 249 | KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature, 250 | ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageServerAuth}, 251 | BasicConstraintsValid: true, 252 | } 253 | 254 | if ip := net.ParseIP(host); ip != nil { 255 | template.IPAddresses = append(template.IPAddresses, ip) 256 | } else { 257 | template.DNSNames = append(template.DNSNames, host) 258 | } 259 | 260 | template.IPAddresses = append(template.IPAddresses, alternateIPs...) 261 | template.DNSNames = append(template.DNSNames, alternateDNS...) 262 | 263 | derBytes, err := x509.CreateCertificate(cryptorand.Reader, &template, caCertificate, &priv.PublicKey, caKey) 264 | if err != nil { 265 | return nil, nil, err 266 | } 267 | 268 | // Generate cert, followed by ca 269 | certBuffer := bytes.Buffer{} 270 | if err := pem.Encode(&certBuffer, &pem.Block{Type: CertificateBlockType, Bytes: derBytes}); err != nil { 271 | return nil, nil, err 272 | } 273 | if err := pem.Encode(&certBuffer, &pem.Block{Type: CertificateBlockType, Bytes: caDERBytes}); err != nil { 274 | return nil, nil, err 275 | } 276 | 277 | // Generate key 278 | keyBuffer := bytes.Buffer{} 279 | if err := pem.Encode(&keyBuffer, &pem.Block{Type: RSAPrivateKeyBlockType, Bytes: x509.MarshalPKCS1PrivateKey(priv)}); err != nil { 280 | return nil, nil, err 281 | } 282 | 283 | if len(fixtureDirectory) > 0 { 284 | if err := ioutil.WriteFile(certFixturePath, certBuffer.Bytes(), 0644); err != nil { 285 | return nil, nil, fmt.Errorf("failed to write cert fixture to %s: %v", certFixturePath, err) 286 | } 287 | if err := ioutil.WriteFile(keyFixturePath, keyBuffer.Bytes(), 0644); err != nil { 288 | return nil, nil, fmt.Errorf("failed to write key fixture to %s: %v", certFixturePath, err) 289 | } 290 | } 291 | 292 | return certBuffer.Bytes(), keyBuffer.Bytes(), nil 293 | } 294 | 295 | func ipsToStrings(ips []net.IP) []string { 296 | ss := make([]string, 0, len(ips)) 297 | for _, ip := range ips { 298 | ss = append(ss, ip.String()) 299 | } 300 | return ss 301 | } 302 | 303 | // IsCertExpired checks if the certificate about to expire 304 | func IsCertExpired(cert *x509.Certificate, days int) bool { 305 | expirationDate := cert.NotAfter 306 | diffDays := time.Until(expirationDate).Hours() / 24.0 307 | if diffDays <= float64(days) { 308 | logrus.Infof("certificate %s will expire in %f days at %s", cert.Subject, diffDays, cert.NotAfter) 309 | return true 310 | } 311 | return false 312 | } 313 | -------------------------------------------------------------------------------- /factory/gen.go: -------------------------------------------------------------------------------- 1 | package factory 2 | 3 | import ( 4 | "crypto" 5 | "crypto/ecdsa" 6 | "crypto/elliptic" 7 | "crypto/rand" 8 | "crypto/sha1" 9 | "crypto/sha256" 10 | "crypto/x509" 11 | "encoding/hex" 12 | "encoding/pem" 13 | "fmt" 14 | "net" 15 | "regexp" 16 | "sort" 17 | "strings" 18 | "time" 19 | 20 | "github.com/rancher/dynamiclistener/cert" 21 | "github.com/sirupsen/logrus" 22 | v1 "k8s.io/api/core/v1" 23 | ) 24 | 25 | const ( 26 | cnPrefix = "listener.cattle.io/cn-" 27 | Static = "listener.cattle.io/static" 28 | Fingerprint = "listener.cattle.io/fingerprint" 29 | ) 30 | 31 | var ( 32 | cnRegexp = regexp.MustCompile("^([A-Za-z0-9:][-A-Za-z0-9_.:]*)?[A-Za-z0-9:]$") 33 | ) 34 | 35 | type TLS struct { 36 | CACert []*x509.Certificate 37 | CAKey crypto.Signer 38 | CN string 39 | Organization []string 40 | FilterCN func(...string) []string 41 | ExpirationDaysCheck int 42 | } 43 | 44 | func cns(secret *v1.Secret) (cns []string) { 45 | if secret == nil { 46 | return nil 47 | } 48 | for k, v := range secret.Annotations { 49 | if strings.HasPrefix(k, cnPrefix) { 50 | cns = append(cns, v) 51 | } 52 | } 53 | return 54 | } 55 | 56 | func collectCNs(secret *v1.Secret) (domains []string, ips []net.IP, err error) { 57 | var ( 58 | cns = cns(secret) 59 | ) 60 | 61 | sort.Strings(cns) 62 | 63 | for _, v := range cns { 64 | ip := net.ParseIP(v) 65 | if ip == nil { 66 | domains = append(domains, v) 67 | } else { 68 | ips = append(ips, ip) 69 | } 70 | } 71 | 72 | return 73 | } 74 | 75 | // Merge combines the SAN lists from the target and additional Secrets, and 76 | // returns a potentially modified Secret, along with a bool indicating if the 77 | // returned Secret is not the same as the target Secret. Secrets with expired 78 | // certificates will never be returned. 79 | // 80 | // If the merge would not add any CNs to the additional Secret, the additional 81 | // Secret is returned, to allow for certificate rotation/regeneration. 82 | // 83 | // If the merge would not add any CNs to the target Secret, the target Secret is 84 | // returned; no merging is necessary. 85 | // 86 | // If neither certificate is acceptable as-is, a new certificate containing 87 | // the union of the two lists is generated, using the private key from the 88 | // first Secret. The returned Secret will contain the updated cert. 89 | func (t *TLS) Merge(target, additional *v1.Secret) (*v1.Secret, bool, error) { 90 | // static secrets can't be altered, don't bother trying 91 | if IsStatic(target) { 92 | return target, false, nil 93 | } 94 | 95 | mergedCNs := append(cns(target), cns(additional)...) 96 | 97 | // if the additional secret already has all the CNs, use it in preference to the 98 | // current one. This behavior is required to allow for renewal or regeneration. 99 | if !NeedsUpdate(0, additional, mergedCNs...) && !t.IsExpired(additional) { 100 | return additional, true, nil 101 | } 102 | 103 | // if the target secret already has all the CNs, continue using it. The additional 104 | // cert had only a subset of the current CNs, so nothing needs to be added. 105 | if !NeedsUpdate(0, target, mergedCNs...) && !t.IsExpired(target) { 106 | return target, false, nil 107 | } 108 | 109 | // neither cert currently has all the necessary CNs or is unexpired; generate a new one. 110 | return t.generateCert(target, mergedCNs...) 111 | } 112 | 113 | // Renew returns a copy of the given certificate that has been re-signed 114 | // to extend the NotAfter date. It is an error to attempt to renew 115 | // a static (user-provided) certificate. 116 | func (t *TLS) Renew(secret *v1.Secret) (*v1.Secret, error) { 117 | if IsStatic(secret) { 118 | return secret, cert.ErrStaticCert 119 | } 120 | cns := cns(secret) 121 | secret = secret.DeepCopy() 122 | secret.Annotations = map[string]string{} 123 | secret, _, err := t.generateCert(secret, cns...) 124 | return secret, err 125 | } 126 | 127 | // Filter ensures that the CNs are all valid accorting to both internal logic, and any filter callbacks. 128 | // The returned list will contain only approved CN entries. 129 | func (t *TLS) Filter(cn ...string) []string { 130 | if len(cn) == 0 || t.FilterCN == nil { 131 | return cn 132 | } 133 | return t.FilterCN(cn...) 134 | } 135 | 136 | // AddCN attempts to add a list of CN strings to a given Secret, returning the potentially-modified 137 | // Secret along with a bool indicating whether or not it has been updated. The Secret will not be changed 138 | // if it has an attribute indicating that it is static (aka user-provided), or if no new CNs were added. 139 | func (t *TLS) AddCN(secret *v1.Secret, cn ...string) (*v1.Secret, bool, error) { 140 | cn = t.Filter(cn...) 141 | 142 | if IsStatic(secret) || !NeedsUpdate(0, secret, cn...) { 143 | return secret, false, nil 144 | } 145 | return t.generateCert(secret, cn...) 146 | } 147 | 148 | func (t *TLS) Regenerate(secret *v1.Secret) (*v1.Secret, error) { 149 | cns := cns(secret) 150 | secret, _, err := t.generateCert(nil, cns...) 151 | return secret, err 152 | } 153 | 154 | func (t *TLS) generateCert(secret *v1.Secret, cn ...string) (*v1.Secret, bool, error) { 155 | secret = secret.DeepCopy() 156 | if secret == nil { 157 | secret = &v1.Secret{} 158 | } 159 | 160 | if err := t.Verify(secret); err != nil { 161 | logrus.Warnf("unable to verify existing certificate: %v - signing operation may change certificate issuer", err) 162 | } 163 | 164 | secret = populateCN(secret, cn...) 165 | 166 | privateKey, err := getPrivateKey(secret) 167 | if err != nil { 168 | return nil, false, err 169 | } 170 | 171 | domains, ips, err := collectCNs(secret) 172 | if err != nil { 173 | return nil, false, err 174 | } 175 | 176 | newCert, err := t.newCert(domains, ips, privateKey) 177 | if err != nil { 178 | return nil, false, err 179 | } 180 | 181 | keyBytes, certBytes, err := MarshalChain(privateKey, append([]*x509.Certificate{newCert}, t.CACert...)...) 182 | if err != nil { 183 | return nil, false, err 184 | } 185 | 186 | if secret.Data == nil { 187 | secret.Data = map[string][]byte{} 188 | } 189 | secret.Type = v1.SecretTypeTLS 190 | secret.Data[v1.TLSCertKey] = certBytes 191 | secret.Data[v1.TLSPrivateKeyKey] = keyBytes 192 | secret.Annotations[Fingerprint] = fmt.Sprintf("SHA1=%X", sha1.Sum(newCert.Raw)) 193 | 194 | return secret, true, nil 195 | } 196 | 197 | func (t *TLS) IsExpired(secret *v1.Secret) bool { 198 | if secret == nil { 199 | return false 200 | } 201 | 202 | certsPem := secret.Data[v1.TLSCertKey] 203 | if len(certsPem) == 0 { 204 | return false 205 | } 206 | 207 | certificates, err := cert.ParseCertsPEM(certsPem) 208 | if err != nil || len(certificates) == 0 { 209 | return false 210 | } 211 | 212 | expirationDays := time.Duration(t.ExpirationDaysCheck) * time.Hour * 24 213 | return time.Now().Add(expirationDays).After(certificates[0].NotAfter) 214 | } 215 | 216 | func (t *TLS) Verify(secret *v1.Secret) error { 217 | certsPem := secret.Data[v1.TLSCertKey] 218 | if len(certsPem) == 0 { 219 | return nil 220 | } 221 | 222 | certificates, err := cert.ParseCertsPEM(certsPem) 223 | if err != nil || len(certificates) == 0 { 224 | return err 225 | } 226 | 227 | verifyOpts := x509.VerifyOptions{ 228 | Roots: x509.NewCertPool(), 229 | KeyUsages: []x509.ExtKeyUsage{ 230 | x509.ExtKeyUsageAny, 231 | }, 232 | } 233 | for _, c := range t.CACert { 234 | verifyOpts.Roots.AddCert(c) 235 | } 236 | 237 | _, err = certificates[0].Verify(verifyOpts) 238 | return err 239 | } 240 | 241 | func (t *TLS) newCert(domains []string, ips []net.IP, privateKey crypto.Signer) (*x509.Certificate, error) { 242 | return NewSignedCert(privateKey, t.CACert[0], t.CAKey, t.CN, t.Organization, domains, ips) 243 | } 244 | 245 | func populateCN(secret *v1.Secret, cn ...string) *v1.Secret { 246 | secret = secret.DeepCopy() 247 | if secret.Annotations == nil { 248 | secret.Annotations = map[string]string{} 249 | } 250 | for _, cn := range cn { 251 | if cnRegexp.MatchString(cn) { 252 | secret.Annotations[getAnnotationKey(cn)] = cn 253 | } else { 254 | logrus.Errorf("dropping invalid CN: %s", cn) 255 | } 256 | } 257 | return secret 258 | } 259 | 260 | // IsStatic returns true if the Secret has an attribute indicating that it contains 261 | // a static (aka user-provided) certificate, which should not be modified. 262 | func IsStatic(secret *v1.Secret) bool { 263 | if secret != nil && secret.Annotations != nil { 264 | return secret.Annotations[Static] == "true" 265 | } 266 | return false 267 | } 268 | 269 | // NeedsUpdate returns true if any of the CNs are not currently present on the 270 | // secret's Certificate, as recorded in the cnPrefix annotations. It will return 271 | // false if all requested CNs are already present, or if maxSANs is non-zero and has 272 | // been exceeded. 273 | func NeedsUpdate(maxSANs int, secret *v1.Secret, cn ...string) bool { 274 | if secret == nil { 275 | return true 276 | } 277 | 278 | for _, cn := range cn { 279 | if secret.Annotations[getAnnotationKey(cn)] == "" { 280 | if maxSANs > 0 && len(cns(secret)) >= maxSANs { 281 | return false 282 | } 283 | return true 284 | } 285 | } 286 | 287 | return false 288 | } 289 | 290 | func getPrivateKey(secret *v1.Secret) (crypto.Signer, error) { 291 | keyBytes := secret.Data[v1.TLSPrivateKeyKey] 292 | if len(keyBytes) == 0 { 293 | return NewPrivateKey() 294 | } 295 | 296 | privateKey, err := cert.ParsePrivateKeyPEM(keyBytes) 297 | if signer, ok := privateKey.(crypto.Signer); ok && err == nil { 298 | return signer, nil 299 | } 300 | 301 | return NewPrivateKey() 302 | } 303 | 304 | // MarshalChain returns given key and certificates as byte slices. 305 | func MarshalChain(privateKey crypto.Signer, certs ...*x509.Certificate) (keyBytes, certChainBytes []byte, err error) { 306 | keyBytes, err = cert.MarshalPrivateKeyToPEM(privateKey) 307 | if err != nil { 308 | return nil, nil, err 309 | } 310 | 311 | for _, cert := range certs { 312 | if cert != nil { 313 | certBlock := pem.Block{ 314 | Type: CertificateBlockType, 315 | Bytes: cert.Raw, 316 | } 317 | certChainBytes = append(certChainBytes, pem.EncodeToMemory(&certBlock)...) 318 | } 319 | } 320 | return keyBytes, certChainBytes, nil 321 | } 322 | 323 | // Marshal returns the given cert and key as byte slices. 324 | func Marshal(x509Cert *x509.Certificate, privateKey crypto.Signer) (certBytes, keyBytes []byte, err error) { 325 | certBlock := pem.Block{ 326 | Type: CertificateBlockType, 327 | Bytes: x509Cert.Raw, 328 | } 329 | 330 | keyBytes, err = cert.MarshalPrivateKeyToPEM(privateKey) 331 | if err != nil { 332 | return nil, nil, err 333 | } 334 | 335 | return pem.EncodeToMemory(&certBlock), keyBytes, nil 336 | } 337 | 338 | // NewPrivateKey returnes a new ECDSA key 339 | func NewPrivateKey() (crypto.Signer, error) { 340 | return ecdsa.GenerateKey(elliptic.P256(), rand.Reader) 341 | } 342 | 343 | // getAnnotationKey return the key to use for a given CN. IPv4 addresses and short hostnames 344 | // are safe to store as-is, but longer hostnames and IPv6 address must be truncated and/or undergo 345 | // character replacement in order to be used as an annotation key. If the CN requires modification, 346 | // a portion of the SHA256 sum of the original value is used as the suffix, to reduce the likelihood 347 | // of collisions in modified values. 348 | func getAnnotationKey(cn string) string { 349 | cn = cnPrefix + cn 350 | cnLen := len(cn) 351 | if cnLen < 64 && !strings.ContainsRune(cn, ':') { 352 | return cn 353 | } 354 | digest := sha256.Sum256([]byte(cn)) 355 | cn = strings.ReplaceAll(cn, ":", "_") 356 | if cnLen > 56 { 357 | cnLen = 56 358 | } 359 | return cn[0:cnLen] + "-" + hex.EncodeToString(digest[0:])[0:6] 360 | } 361 | -------------------------------------------------------------------------------- /listener.go: -------------------------------------------------------------------------------- 1 | package dynamiclistener 2 | 3 | import ( 4 | "context" 5 | "crypto" 6 | "crypto/tls" 7 | "crypto/x509" 8 | "net" 9 | "net/http" 10 | "strings" 11 | "sync" 12 | "time" 13 | 14 | "github.com/rancher/dynamiclistener/cert" 15 | "github.com/rancher/dynamiclistener/factory" 16 | "github.com/sirupsen/logrus" 17 | v1 "k8s.io/api/core/v1" 18 | ) 19 | 20 | type TLSStorage interface { 21 | Get() (*v1.Secret, error) 22 | Update(secret *v1.Secret) error 23 | } 24 | 25 | type TLSFactory interface { 26 | Renew(secret *v1.Secret) (*v1.Secret, error) 27 | AddCN(secret *v1.Secret, cn ...string) (*v1.Secret, bool, error) 28 | Merge(target *v1.Secret, additional *v1.Secret) (*v1.Secret, bool, error) 29 | Filter(cn ...string) []string 30 | Regenerate(secret *v1.Secret) (*v1.Secret, error) 31 | } 32 | 33 | type SetFactory interface { 34 | SetFactory(tls TLSFactory) 35 | } 36 | 37 | // Deprecated: Use NewListenerWithChain instead as it supports intermediate CAs 38 | func NewListener(l net.Listener, storage TLSStorage, caCert *x509.Certificate, caKey crypto.Signer, config Config) (net.Listener, http.Handler, error) { 39 | return NewListenerWithChain(l, storage, []*x509.Certificate{caCert}, caKey, config) 40 | } 41 | 42 | func NewListenerWithChain(l net.Listener, storage TLSStorage, caCert []*x509.Certificate, caKey crypto.Signer, config Config) (net.Listener, http.Handler, error) { 43 | if config.CN == "" { 44 | config.CN = "dynamic" 45 | } 46 | if len(config.Organization) == 0 { 47 | config.Organization = []string{"dynamic"} 48 | } 49 | if config.TLSConfig == nil { 50 | config.TLSConfig = &tls.Config{} 51 | } 52 | if config.ExpirationDaysCheck == 0 { 53 | config.ExpirationDaysCheck = 90 54 | } 55 | 56 | dynamicListener := &listener{ 57 | factory: &factory.TLS{ 58 | CACert: caCert, 59 | CAKey: caKey, 60 | CN: config.CN, 61 | Organization: config.Organization, 62 | FilterCN: allowDefaultSANs(config.SANs, config.FilterCN), 63 | ExpirationDaysCheck: config.ExpirationDaysCheck, 64 | }, 65 | Listener: l, 66 | certReady: make(chan struct{}), 67 | storage: &nonNil{storage: storage}, 68 | sans: config.SANs, 69 | maxSANs: config.MaxSANs, 70 | tlsConfig: config.TLSConfig, 71 | } 72 | if dynamicListener.tlsConfig == nil { 73 | dynamicListener.tlsConfig = &tls.Config{} 74 | } 75 | dynamicListener.tlsConfig.GetCertificate = dynamicListener.getCertificate 76 | 77 | if config.CloseConnOnCertChange { 78 | if len(dynamicListener.tlsConfig.Certificates) == 0 { 79 | dynamicListener.tlsConfig.NextProtos = []string{"http/1.1"} 80 | } 81 | dynamicListener.conns = map[int]*closeWrapper{} 82 | } 83 | 84 | if setter, ok := storage.(SetFactory); ok { 85 | setter.SetFactory(dynamicListener.factory) 86 | } 87 | 88 | if config.RegenerateCerts != nil && config.RegenerateCerts() { 89 | if err := dynamicListener.regenerateCerts(); err != nil { 90 | return nil, nil, err 91 | } 92 | } 93 | 94 | tlsListener := tls.NewListener(dynamicListener.WrapExpiration(config.ExpirationDaysCheck), dynamicListener.tlsConfig) 95 | 96 | return tlsListener, dynamicListener.cacheHandler(), nil 97 | } 98 | 99 | func allowDefaultSANs(sans []string, next func(...string) []string) func(...string) []string { 100 | if next == nil { 101 | return nil 102 | } else if len(sans) == 0 { 103 | return next 104 | } 105 | 106 | sanMap := map[string]bool{} 107 | for _, san := range sans { 108 | sanMap[san] = true 109 | } 110 | 111 | return func(s ...string) []string { 112 | var ( 113 | good []string 114 | unknown []string 115 | ) 116 | for _, s := range s { 117 | if sanMap[s] { 118 | good = append(good, s) 119 | } else { 120 | unknown = append(unknown, s) 121 | } 122 | } 123 | 124 | return append(good, next(unknown...)...) 125 | } 126 | } 127 | 128 | type cancelClose struct { 129 | cancel func() 130 | net.Listener 131 | } 132 | 133 | func (c *cancelClose) Close() error { 134 | c.cancel() 135 | return c.Listener.Close() 136 | } 137 | 138 | type Config struct { 139 | CN string 140 | Organization []string 141 | TLSConfig *tls.Config 142 | SANs []string 143 | MaxSANs int 144 | ExpirationDaysCheck int 145 | CloseConnOnCertChange bool 146 | RegenerateCerts func() bool 147 | FilterCN func(...string) []string 148 | } 149 | 150 | type listener struct { 151 | sync.RWMutex 152 | net.Listener 153 | 154 | conns map[int]*closeWrapper 155 | connID int 156 | connLock sync.Mutex 157 | 158 | factory TLSFactory 159 | storage TLSStorage 160 | version string 161 | tlsConfig *tls.Config 162 | cert *tls.Certificate 163 | certReady chan struct{} 164 | sans []string 165 | maxSANs int 166 | init sync.Once 167 | } 168 | 169 | func (l *listener) WrapExpiration(days int) net.Listener { 170 | ctx, cancel := context.WithCancel(context.Background()) 171 | go func() { 172 | 173 | // wait for cert to be set, this will unblock when the channel is closed 174 | select { 175 | case <-ctx.Done(): 176 | return 177 | case <-l.certReady: 178 | } 179 | 180 | for { 181 | wait := 6 * time.Hour 182 | if err := l.checkExpiration(days); err != nil { 183 | logrus.Errorf("dynamiclistener %s: failed to check and renew dynamic cert: %v", l.Addr(), err) 184 | // Don't go into short retry loop if we're using a static (user-provided) cert. 185 | // We will still check and print an error every six hours until the user updates the secret with 186 | // a cert that is not about to expire. Hopefully this will prompt them to take action. 187 | if err != cert.ErrStaticCert { 188 | wait = 5 * time.Minute 189 | } 190 | } 191 | select { 192 | case <-ctx.Done(): 193 | return 194 | case <-time.After(wait): 195 | } 196 | } 197 | }() 198 | 199 | return &cancelClose{ 200 | cancel: cancel, 201 | Listener: l, 202 | } 203 | } 204 | 205 | // regenerateCerts regenerates the used certificates and 206 | // updates the secret. 207 | func (l *listener) regenerateCerts() error { 208 | l.Lock() 209 | defer l.Unlock() 210 | 211 | secret, err := l.storage.Get() 212 | if err != nil { 213 | return err 214 | } 215 | 216 | newSecret, err := l.factory.Regenerate(secret) 217 | if err != nil { 218 | return err 219 | } 220 | if err := l.storage.Update(newSecret); err != nil { 221 | return err 222 | } 223 | // clear version to force cert reload 224 | l.version = "" 225 | 226 | return nil 227 | } 228 | 229 | func (l *listener) checkExpiration(days int) error { 230 | l.Lock() 231 | defer l.Unlock() 232 | 233 | if days == 0 { 234 | return nil 235 | } 236 | 237 | if l.cert == nil { 238 | return nil 239 | } 240 | 241 | secret, err := l.storage.Get() 242 | if err != nil { 243 | return err 244 | } 245 | 246 | keyPair, err := tls.X509KeyPair(secret.Data[v1.TLSCertKey], secret.Data[v1.TLSPrivateKeyKey]) 247 | if err != nil { 248 | return err 249 | } 250 | 251 | certParsed, err := x509.ParseCertificate(keyPair.Certificate[0]) 252 | if err != nil { 253 | return err 254 | } 255 | 256 | if cert.IsCertExpired(certParsed, days) { 257 | secret, err := l.factory.Renew(secret) 258 | if err != nil { 259 | return err 260 | } 261 | if err := l.storage.Update(secret); err != nil { 262 | return err 263 | } 264 | // clear version to force cert reload 265 | l.version = "" 266 | } 267 | 268 | return nil 269 | } 270 | 271 | func (l *listener) Accept() (net.Conn, error) { 272 | l.init.Do(func() { 273 | if len(l.sans) > 0 { 274 | if err := l.updateCert(l.sans...); err != nil { 275 | logrus.Errorf("dynamiclistener %s: failed to update cert with configured SANs: %v", l.Addr(), err) 276 | return 277 | } 278 | } 279 | if cert, err := l.loadCert(nil); err != nil { 280 | logrus.Errorf("dynamiclistener %s: failed to preload certificate: %v", l.Addr(), err) 281 | } else if cert == nil { 282 | // This should only occur on the first startup when no SANs are configured in the listener config, in which 283 | // case no certificate can be created, as dynamiclistener will not create certificates until at least one IP 284 | // or DNS SAN is set. It will also occur when using the Kubernetes storage without a local File cache. 285 | // For reliable serving of requests, callers should configure a local cache and/or a default set of SANs. 286 | logrus.Warnf("dynamiclistener %s: no cached certificate available for preload - deferring certificate load until storage initialization or first client request", l.Addr()) 287 | } 288 | }) 289 | 290 | conn, err := l.Listener.Accept() 291 | if err != nil { 292 | return conn, err 293 | } 294 | 295 | addr := conn.LocalAddr() 296 | if addr == nil { 297 | return conn, nil 298 | } 299 | 300 | host, _, err := net.SplitHostPort(addr.String()) 301 | if err != nil { 302 | logrus.Errorf("dynamiclistener %s: failed to parse connection local address %s: %v", l.Addr(), addr, err) 303 | return conn, nil 304 | } 305 | 306 | if err := l.updateCert(host); err != nil { 307 | logrus.Errorf("dynamiclistener %s: failed to update cert with connection local address: %v", l.Addr(), err) 308 | } 309 | 310 | if l.conns != nil { 311 | conn = l.wrap(conn) 312 | } 313 | 314 | return conn, nil 315 | } 316 | 317 | func (l *listener) wrap(conn net.Conn) net.Conn { 318 | l.connLock.Lock() 319 | defer l.connLock.Unlock() 320 | l.connID++ 321 | 322 | wrapper := &closeWrapper{ 323 | Conn: conn, 324 | id: l.connID, 325 | l: l, 326 | } 327 | l.conns[l.connID] = wrapper 328 | 329 | return wrapper 330 | } 331 | 332 | type closeWrapper struct { 333 | net.Conn 334 | id int 335 | l *listener 336 | ready bool 337 | } 338 | 339 | func (c *closeWrapper) close() error { 340 | delete(c.l.conns, c.id) 341 | return c.Conn.Close() 342 | } 343 | 344 | func (c *closeWrapper) Close() error { 345 | c.l.connLock.Lock() 346 | defer c.l.connLock.Unlock() 347 | return c.close() 348 | } 349 | 350 | func (l *listener) getCertificate(hello *tls.ClientHelloInfo) (*tls.Certificate, error) { 351 | newConn := hello.Conn 352 | if hello.ServerName != "" { 353 | if err := l.updateCert(hello.ServerName); err != nil { 354 | logrus.Errorf("dynamiclistener %s: failed to update cert with TLS ServerName: %v", l.Addr(), err) 355 | return nil, err 356 | } 357 | } 358 | connCert, err := l.loadCert(newConn) 359 | if connCert != nil && err == nil && newConn != nil && l.conns != nil { 360 | // if we were successfully able to load a cert and are closing connections on cert changes, mark newConn ready 361 | // this will allow us to close the connection if a future connection forces the cert to re-load 362 | wrapper, ok := newConn.(*closeWrapper) 363 | if !ok { 364 | logrus.Debugf("will not mark non-close wrapper connection from %s to %s as ready", newConn.RemoteAddr(), newConn.LocalAddr()) 365 | return connCert, err 366 | } 367 | l.connLock.Lock() 368 | l.conns[wrapper.id].ready = true 369 | l.connLock.Unlock() 370 | } 371 | return connCert, err 372 | } 373 | 374 | func (l *listener) updateCert(cn ...string) error { 375 | cn = l.factory.Filter(cn...) 376 | if len(cn) == 0 { 377 | return nil 378 | } 379 | 380 | l.RLock() 381 | defer l.RUnlock() 382 | 383 | secret, err := l.storage.Get() 384 | if err != nil { 385 | return err 386 | } 387 | 388 | if factory.IsStatic(secret) || !factory.NeedsUpdate(l.maxSANs, secret, cn...) { 389 | return nil 390 | } 391 | 392 | l.RUnlock() 393 | l.Lock() 394 | defer l.RLock() 395 | defer l.Unlock() 396 | 397 | secret, updated, err := l.factory.AddCN(secret, append(l.sans, cn...)...) 398 | if err != nil { 399 | return err 400 | } 401 | 402 | if updated { 403 | if err := l.storage.Update(secret); err != nil { 404 | return err 405 | } 406 | // Clear version to force cert reload next time loadCert is called by TLSConfig's 407 | // GetCertificate hook to provide a certificate for a new connection. Note that this 408 | // means the old certificate stays in l.cert until a new connection is made. 409 | l.version = "" 410 | } 411 | 412 | return nil 413 | } 414 | 415 | func (l *listener) loadCert(currentConn net.Conn) (*tls.Certificate, error) { 416 | l.RLock() 417 | defer l.RUnlock() 418 | 419 | secret, err := l.storage.Get() 420 | if err != nil { 421 | return nil, err 422 | } 423 | if l.cert != nil && l.version == secret.ResourceVersion && secret.ResourceVersion != "" { 424 | return l.cert, nil 425 | } 426 | 427 | defer l.RLock() 428 | l.RUnlock() 429 | l.Lock() 430 | defer l.Unlock() 431 | 432 | secret, err = l.storage.Get() 433 | if err != nil { 434 | return nil, err 435 | } 436 | if !cert.IsValidTLSSecret(secret) { 437 | return l.cert, nil 438 | } 439 | if l.cert != nil && l.version == secret.ResourceVersion && secret.ResourceVersion != "" { 440 | return l.cert, nil 441 | } 442 | 443 | cert, err := tls.X509KeyPair(secret.Data[v1.TLSCertKey], secret.Data[v1.TLSPrivateKeyKey]) 444 | if err != nil { 445 | return nil, err 446 | } 447 | 448 | // cert has changed, close closeWrapper wrapped connections if this isn't the first load 449 | if currentConn != nil && l.conns != nil && l.cert != nil { 450 | l.connLock.Lock() 451 | for _, conn := range l.conns { 452 | // Don't close a connection that's in the middle of completing a TLS handshake 453 | if !conn.ready { 454 | continue 455 | } 456 | _ = conn.close() 457 | } 458 | l.connLock.Unlock() 459 | } 460 | 461 | // we can only close the ready channel once when the cert is first assigned 462 | canClose := l.cert == nil 463 | l.cert = &cert 464 | if canClose { 465 | close(l.certReady) 466 | } 467 | l.version = secret.ResourceVersion 468 | return l.cert, nil 469 | } 470 | 471 | func (l *listener) cacheHandler() http.Handler { 472 | return http.HandlerFunc(func(resp http.ResponseWriter, req *http.Request) { 473 | h, _, err := net.SplitHostPort(req.Host) 474 | if err != nil { 475 | h = req.Host 476 | } 477 | 478 | ip := net.ParseIP(h) 479 | if len(ip) > 0 { 480 | for _, v := range req.Header["User-Agent"] { 481 | if strings.Contains(strings.ToLower(v), "mozilla") { 482 | return 483 | } 484 | } 485 | 486 | if err := l.updateCert(h); err != nil { 487 | logrus.Errorf("dynamiclistener %s: failed to update cert with HTTP request Host header: %v", l.Addr(), err) 488 | } 489 | } 490 | }) 491 | } 492 | 493 | type nonNil struct { 494 | sync.Mutex 495 | storage TLSStorage 496 | } 497 | 498 | func (n *nonNil) Get() (*v1.Secret, error) { 499 | n.Lock() 500 | defer n.Unlock() 501 | 502 | s, err := n.storage.Get() 503 | if err != nil || s == nil { 504 | return &v1.Secret{}, err 505 | } 506 | return s, nil 507 | } 508 | 509 | func (n *nonNil) Update(secret *v1.Secret) error { 510 | n.Lock() 511 | defer n.Unlock() 512 | 513 | return n.storage.Update(secret) 514 | } 515 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= 2 | github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= 3 | github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= 4 | github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 5 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 6 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 7 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= 8 | github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 9 | github.com/emicklei/go-restful/v3 v3.12.2 h1:DhwDP0vY3k8ZzE0RunuJy8GhNpPL6zqLkDf9B/a0/xU= 10 | github.com/emicklei/go-restful/v3 v3.12.2/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= 11 | github.com/evanphx/json-patch v5.9.11+incompatible h1:ixHHqfcGvxhWkniF1tWxBHA0yb4Z+d1UQi45df52xW8= 12 | github.com/evanphx/json-patch v5.9.11+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= 13 | github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM= 14 | github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= 15 | github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY= 16 | github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= 17 | github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ= 18 | github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY= 19 | github.com/go-openapi/jsonreference v0.21.0 h1:Rs+Y7hSXT83Jacb7kFyjn4ijOuVGSvOdF2+tg1TRrwQ= 20 | github.com/go-openapi/jsonreference v0.21.0/go.mod h1:LmZmgsrTkVg9LG4EaHeY8cBDslNPMo06cago5JNLkm4= 21 | github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE= 22 | github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= 23 | github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI= 24 | github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8= 25 | github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= 26 | github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= 27 | github.com/google/gnostic-models v0.7.0 h1:qwTtogB15McXDaNqTZdzPJRHvaVJlAl+HVQnLmJEJxo= 28 | github.com/google/gnostic-models v0.7.0/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ= 29 | github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= 30 | github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= 31 | github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= 32 | github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db h1:097atOisP2aRj7vFgYQBbFN4U4JNXUNYpxael3UzMyo= 33 | github.com/google/pprof v0.0.0-20241029153458-d1b30febd7db/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= 34 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 35 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 36 | github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= 37 | github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= 38 | github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= 39 | github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= 40 | github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= 41 | github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= 42 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 43 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 44 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 45 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 46 | github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= 47 | github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= 48 | github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= 49 | github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= 50 | github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 51 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= 52 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= 53 | github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= 54 | github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee h1:W5t00kpgFdJifH4BDsTlE89Zl93FEloxaWZfGcifgq8= 55 | github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= 56 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= 57 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= 58 | github.com/onsi/ginkgo/v2 v2.21.0 h1:7rg/4f3rB88pb5obDgNZrNHrQ4e6WpjonchcpuBRnZM= 59 | github.com/onsi/ginkgo/v2 v2.21.0/go.mod h1:7Du3c42kxCUegi0IImZ1wUQzMBVecgIHjR1C+NkhLQo= 60 | github.com/onsi/gomega v1.35.1 h1:Cwbd75ZBPxFSuZ6T+rN/WCb/gOc6YgFBXLlZLhC7Ds4= 61 | github.com/onsi/gomega v1.35.1/go.mod h1:PvZbdDc8J6XJEpDK4HCuRBm8a6Fzp9/DmhC9C7yFlog= 62 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 63 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 64 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 65 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= 66 | github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 67 | github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q= 68 | github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0= 69 | github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= 70 | github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= 71 | github.com/prometheus/common v0.62.0 h1:xasJaQlnWAeyHdUBeGjXmutelfJHWMRr+Fg4QszZ2Io= 72 | github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I= 73 | github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= 74 | github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= 75 | github.com/rancher/lasso v0.2.5-rc.1 h1:E88eG3FDYGmccJjGDjv7HrknSaYQz4WNGgJ/gGjmGFc= 76 | github.com/rancher/lasso v0.2.5-rc.1/go.mod h1:71rWfv+KkdSmSxZ9Ly5QYhxAu0nEUcaq9N2ByjcHqAM= 77 | github.com/rancher/wrangler/v3 v3.3.0-rc.2 h1:o/jNO0uQmcAUTxIwu7q3VBTkS82XDh7gb0Ofv0DpVoQ= 78 | github.com/rancher/wrangler/v3 v3.3.0-rc.2/go.mod h1:0RpxDgbQ4Lgzfuy7JPNk5jZfTKJQoCN6qUhlrDgNY9E= 79 | github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= 80 | github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= 81 | github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= 82 | github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= 83 | github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= 84 | github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 85 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 86 | github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= 87 | github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= 88 | github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 89 | github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 90 | github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= 91 | github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= 92 | github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM= 93 | github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg= 94 | github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 95 | github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 96 | go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= 97 | go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= 98 | go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y= 99 | go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU= 100 | go.yaml.in/yaml/v2 v2.4.2 h1:DzmwEr2rDGHl7lsFgAHxmNz/1NlQ7xLIrlN2h5d1eGI= 101 | go.yaml.in/yaml/v2 v2.4.2/go.mod h1:081UH+NErpNdqlCXm3TtEran0rJZGxAYx9hb/ELlsPU= 102 | go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= 103 | go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= 104 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 105 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 106 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 107 | golang.org/x/crypto v0.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI= 108 | golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8= 109 | golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 110 | golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 111 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 112 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 113 | golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 114 | golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 115 | golang.org/x/net v0.44.0 h1:evd8IRDyfNBMBTTY5XRF1vaZlD+EmWx6x8PkhR04H/I= 116 | golang.org/x/net v0.44.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY= 117 | golang.org/x/oauth2 v0.27.0 h1:da9Vo7/tDv5RH/7nZDz1eMGS/q1Vv1N/7FCrBhI9I3M= 118 | golang.org/x/oauth2 v0.27.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8= 119 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 120 | golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 121 | golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 122 | golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug= 123 | golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= 124 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 125 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 126 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 127 | golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 128 | golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k= 129 | golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= 130 | golang.org/x/term v0.35.0 h1:bZBVKBudEyhRcajGcNc3jIfWPqV4y/Kt2XcoigOWtDQ= 131 | golang.org/x/term v0.35.0/go.mod h1:TPGtkTLesOwf2DE8CgVYiZinHAOuy5AYUYT1lENIZnA= 132 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 133 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 134 | golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk= 135 | golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4= 136 | golang.org/x/time v0.9.0 h1:EsRrnYcQiGH+5FfbgvV4AP7qEZstoyrHB0DzarOQ4ZY= 137 | golang.org/x/time v0.9.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= 138 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 139 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 140 | golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= 141 | golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= 142 | golang.org/x/tools v0.37.0 h1:DVSRzp7FwePZW356yEAChSdNcQo6Nsp+fex1SUW09lE= 143 | golang.org/x/tools v0.37.0/go.mod h1:MBN5QPQtLMHVdvsbtarmTNukZDdgwdwlO5qGacAzF0w= 144 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 145 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 146 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 147 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 148 | google.golang.org/protobuf v1.36.8 h1:xHScyCOEuuwZEc6UtSOvPbAT4zRh0xcNRYekJwfqyMc= 149 | google.golang.org/protobuf v1.36.8/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU= 150 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 151 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= 152 | gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= 153 | gopkg.in/evanphx/json-patch.v4 v4.12.0 h1:n6jtcsulIzXPJaxegRbvFNNrZDjbij7ny3gmSPG+6V4= 154 | gopkg.in/evanphx/json-patch.v4 v4.12.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M= 155 | gopkg.in/inf.v0 v0.9.1 h1:73M5CoZyi3ZLMOyDlQh031Cx6N9NDJ2Vvfl76EDAgDc= 156 | gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw= 157 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 158 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 159 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 160 | k8s.io/api v0.34.1 h1:jC+153630BMdlFukegoEL8E/yT7aLyQkIVuwhmwDgJM= 161 | k8s.io/api v0.34.1/go.mod h1:SB80FxFtXn5/gwzCoN6QCtPD7Vbu5w2n1S0J5gFfTYk= 162 | k8s.io/apimachinery v0.34.1 h1:dTlxFls/eikpJxmAC7MVE8oOeP1zryV7iRyIjB0gky4= 163 | k8s.io/apimachinery v0.34.1/go.mod h1:/GwIlEcWuTX9zKIg2mbw0LRFIsXwrfoVxn+ef0X13lw= 164 | k8s.io/client-go v0.34.1 h1:ZUPJKgXsnKwVwmKKdPfw4tB58+7/Ik3CrjOEhsiZ7mY= 165 | k8s.io/client-go v0.34.1/go.mod h1:kA8v0FP+tk6sZA0yKLRG67LWjqufAoSHA2xVGKw9Of8= 166 | k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= 167 | k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= 168 | k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b h1:MloQ9/bdJyIu9lb1PzujOPolHyvO06MXG5TUIj2mNAA= 169 | k8s.io/kube-openapi v0.0.0-20250710124328-f3f2b991d03b/go.mod h1:UZ2yyWbFTpuhSbFhv24aGNOdoRdJZgsIObGBUaYVsts= 170 | k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 h1:SjGebBtkBqHFOli+05xYbK8YF1Dzkbzn+gDM4X9T4Ck= 171 | k8s.io/utils v0.0.0-20251002143259-bc988d571ff4/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= 172 | sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 h1:gBQPwqORJ8d8/YNZWEjoZs7npUVDpVXUUOFfW6CgAqE= 173 | sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= 174 | sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= 175 | sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= 176 | sigs.k8s.io/structured-merge-diff/v6 v6.3.0 h1:jTijUJbW353oVOd9oTlifJqOGEkUw2jB/fXCbTiQEco= 177 | sigs.k8s.io/structured-merge-diff/v6 v6.3.0/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE= 178 | sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs= 179 | sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4= 180 | --------------------------------------------------------------------------------