diff --git a/src/cloud-api-adaptor/cmd/cloud-api-adaptor/main.go b/src/cloud-api-adaptor/cmd/cloud-api-adaptor/main.go index 5525e69b3f..4229256841 100644 --- a/src/cloud-api-adaptor/cmd/cloud-api-adaptor/main.go +++ b/src/cloud-api-adaptor/cmd/cloud-api-adaptor/main.go @@ -82,8 +82,9 @@ func (cfg *daemonConfig) Setup() (cmd.Starter, error) { } var ( - disableTLS bool - tlsConfig tlsutil.TLSConfig + disableTLS bool + tlsConfig tlsutil.TLSConfig + tlsCipherSuites string ) cmd.Parse(programName, os.Args[1:], func(flags *flag.FlagSet) { @@ -105,6 +106,8 @@ func (cfg *daemonConfig) Setup() (cmd.Starter, error) { reg.StringWithEnv(&tlsConfig.CertFile, "cert-file", "", "CERT_FILE", "Client certificate file for custom TLS (e.g. /etc/certificates/client.crt)") reg.StringWithEnv(&tlsConfig.KeyFile, "cert-key", "", "CERT_KEY", "Client key file for custom TLS (e.g. /etc/certificates/client.key)") reg.BoolWithEnv(&tlsConfig.SkipVerify, "tls-skip-verify", false, "TLS_SKIP_VERIFY", "Skip TLS certificate verification - use it only for testing") + reg.StringWithEnv(&tlsConfig.MinTLSVersion, "tls-min-version", "", "TLS_MIN_VERSION", "Minimum TLS version for peer pod connections (VersionTLS12 or VersionTLS13)") + reg.StringWithEnv(&tlsCipherSuites, "tls-cipher-suites", "", "TLS_CIPHER_SUITES", "Comma-separated IANA TLS cipher suite names for peer pod connections (not applicable for VersionTLS13)") reg.DurationWithEnv(&cfg.serverConfig.ProxyTimeout, "proxy-timeout", proxy.DefaultProxyTimeout, "PROXY_TIMEOUT", "Maximum timeout in minutes for establishing agent proxy connection") reg.StringWithEnv(&cfg.networkConfig.TunnelType, "tunnel-type", podnetwork.DefaultTunnelType, "TUNNEL_TYPE", "Tunnel provider") reg.IntWithEnv(&cfg.networkConfig.VXLAN.Port, "vxlan-port", vxlan.DefaultVXLANPort, "VXLAN_PORT", "VXLAN UDP port number (VXLAN tunnel mode only") @@ -127,8 +130,22 @@ func (cfg *daemonConfig) Setup() (cmd.Starter, error) { fmt.Printf("%s: starting Cloud API Adaptor daemon for %q\n", programName, cloudName) - if !disableTLS { + if tlsCipherSuites != "" { + tlsConfig.CipherSuites = strings.Split(tlsCipherSuites, ",") + } + + if disableTLS { + fmt.Printf("%s: WARNING: TLS disabled (--disable-tls). Use only for testing.\n", programName) + } else { cfg.serverConfig.TLSConfig = &tlsConfig + + if tlsConfig.SkipVerify { + fmt.Printf("%s: WARNING: TLS certificate verification disabled (--tls-skip-verify). Use only for testing.\n", programName) + } + + if tlsConfig.MinTLSVersion != "" || len(tlsConfig.CipherSuites) > 0 { + fmt.Printf("%s: WARNING: TLS profile (TLS_MIN_VERSION=%s, TLS_CIPHER_SUITES=%s) is baked into peer pod VMs at creation time via user-data. Existing peer pods will not pick up profile changes until deleted and recreated.\n", programName, tlsConfig.MinTLSVersion, tlsCipherSuites) + } } // DEPRECATED: LoadEnv() is now a no-op for all providers. diff --git a/src/cloud-api-adaptor/go.mod b/src/cloud-api-adaptor/go.mod index 0663174592..d9cd7ae46e 100644 --- a/src/cloud-api-adaptor/go.mod +++ b/src/cloud-api-adaptor/go.mod @@ -50,15 +50,15 @@ require ( github.com/moby/sys/mountinfo v0.7.2 github.com/pelletier/go-toml/v2 v2.1.0 github.com/sirupsen/logrus v1.9.3 - github.com/spf13/cobra v1.7.0 + github.com/spf13/cobra v1.10.0 golang.org/x/crypto v0.48.0 // indirect golang.org/x/exp v0.0.0-20241108190413-2d47ceb2692f google.golang.org/api v0.256.0 google.golang.org/protobuf v1.36.11 - k8s.io/api v0.29.0 - k8s.io/apimachinery v0.33.0 - k8s.io/client-go v0.29.0 - k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 + k8s.io/api v0.35.0 + k8s.io/apimachinery v0.35.0 + k8s.io/client-go v0.35.0 + k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 sigs.k8s.io/e2e-framework v0.1.0 sigs.k8s.io/kustomize v2.0.3+incompatible sigs.k8s.io/kustomize/api v0.16.0 @@ -75,6 +75,11 @@ require ( github.com/fenglyu/go-dmidecode v0.0.0-20220417074508-03f52eb45fe9 ) +require ( + gopkg.in/evanphx/json-patch.v4 v4.13.0 // indirect + sigs.k8s.io/structured-merge-diff/v6 v6.3.0 // indirect +) + require ( cloud.google.com/go v0.123.0 // indirect cloud.google.com/go/auth v0.17.0 // indirect @@ -136,12 +141,11 @@ require ( github.com/docker/go-connections v0.5.0 // indirect github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c // indirect github.com/docker/go-units v0.5.0 // indirect - github.com/emicklei/go-restful/v3 v3.11.0 // indirect - github.com/evanphx/json-patch v5.9.11+incompatible // indirect + github.com/emicklei/go-restful/v3 v3.12.2 // indirect github.com/evanphx/json-patch/v5 v5.6.0 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect github.com/fsnotify/fsnotify v1.9.0 // indirect - github.com/fxamacker/cbor/v2 v2.7.0 // indirect + github.com/fxamacker/cbor/v2 v2.9.0 // indirect github.com/gabriel-vasile/mimetype v1.4.11 // indirect github.com/ghodss/yaml v1.0.0 // indirect github.com/go-errors/errors v1.4.2 // indirect @@ -163,14 +167,13 @@ require ( github.com/go-viper/mapstructure/v2 v2.4.0 // indirect github.com/golang-jwt/jwt/v4 v4.5.2 // indirect github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // indirect - github.com/golang/protobuf v1.5.4 // indirect - github.com/google/gnostic-models v0.6.9 // indirect + github.com/google/gnostic-models v0.7.0 // indirect github.com/google/go-cmp v0.7.0 // indirect github.com/google/s2a-go v0.1.9 // indirect github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect github.com/googleapis/enterprise-certificate-proxy v0.3.7 // indirect github.com/googleapis/gax-go/v2 v2.15.0 // indirect - github.com/gorilla/websocket v1.5.0 // indirect + github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 // indirect github.com/hashicorp/go-cleanhttp v0.5.2 // indirect github.com/hashicorp/go-retryablehttp v0.7.8 // indirect github.com/imdario/mergo v0.3.13 // indirect @@ -194,7 +197,7 @@ require ( github.com/moby/sys/userns v0.1.0 // indirect github.com/moby/term v0.5.2 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect - github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee // indirect github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f // indirect @@ -209,7 +212,7 @@ require ( github.com/pkg/sftp v1.13.9 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/sergi/go-diff v1.2.0 // indirect - github.com/spf13/pflag v1.0.5 // indirect + github.com/spf13/pflag v1.0.9 // indirect github.com/syndtr/gocapability v0.0.0-20200815063812-42c35b437635 // indirect github.com/tjfoc/gmsm v1.4.1 // indirect github.com/x448/float16 v0.8.4 // indirect @@ -224,7 +227,7 @@ require ( go.opentelemetry.io/otel/trace v1.37.0 // indirect go.starlark.net v0.0.0-20200306205701-8dd3e2ee1dd5 // indirect go.yaml.in/yaml/v2 v2.4.3 // indirect - go.yaml.in/yaml/v3 v3.0.3 // indirect + go.yaml.in/yaml/v3 v3.0.4 // indirect golang.org/x/mod v0.33.0 // indirect golang.org/x/net v0.51.0 // indirect golang.org/x/oauth2 v0.33.0 // indirect @@ -240,12 +243,12 @@ require ( gopkg.in/ini.v1 v1.67.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect gotest.tools/v3 v3.5.2 // indirect + k8s.io/component-base v0.35.0 k8s.io/klog/v2 v2.130.1 // indirect - k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff // indirect + k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 // indirect sigs.k8s.io/controller-runtime v0.14.1 // indirect - sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 // indirect + sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect sigs.k8s.io/randfill v1.0.0 // indirect - sigs.k8s.io/structured-merge-diff/v4 v4.6.0 // indirect sigs.k8s.io/yaml v1.6.0 // indirect tags.cncf.io/container-device-interface v1.0.1 // indirect tags.cncf.io/container-device-interface/specs-go v1.0.0 // indirect diff --git a/src/cloud-api-adaptor/go.sum b/src/cloud-api-adaptor/go.sum index 2462612ed7..0ab28ec431 100644 --- a/src/cloud-api-adaptor/go.sum +++ b/src/cloud-api-adaptor/go.sum @@ -81,6 +81,8 @@ github.com/IBM/platform-services-go-sdk v0.91.0 h1:5o4XotMmP9UfCg9BKG0cx/pTAMhBh github.com/IBM/platform-services-go-sdk v0.91.0/go.mod h1:KAnBhxKaYsu9It2aVXV6oCPEj78imvTs2qSG0ScZKpM= github.com/IBM/vpc-go-sdk v0.66.0 h1:S0HW+f6Qf6OLSGESQ7WRgWLq1bDgvs+vvOJ7AWgUMbw= github.com/IBM/vpc-go-sdk v0.66.0/go.mod h1:VL7sy61ybg6tvA60SepoQx7TFe20m7JyNUt+se2tHP4= +github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0= +github.com/Masterminds/semver/v3 v3.4.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= github.com/Microsoft/hcsshim v0.13.0 h1:/BcXOiS6Qi7N9XqUcv27vkIuVOkBEcWstd2pMlWSeaA= @@ -236,7 +238,7 @@ github.com/coreos/go-iptables v0.6.0 h1:is9qnZMPYjLd8LYqmm/qlE+wwEgJIkTYdhV3rfZo github.com/coreos/go-iptables v0.6.0/go.mod h1:Qe8Bv2Xik5FyTXwgIbLAnv2sWSBmvWdFETJConOQ//Q= github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f h1:JOrtw2xFKzlg+cbHpyrpLDmnN1HqhBfnX7WDiW7eG2c= github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= -github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/cyphar/filepath-securejoin v0.6.0 h1:BtGB77njd6SVO6VztOHfPxKitJvd/VPT+OFBFMOi1Is= github.com/cyphar/filepath-securejoin v0.6.0/go.mod h1:A8hd4EnAeyujCJRrICiOWqjS1AX0a9kM5XL+NwKoYSc= @@ -256,8 +258,8 @@ github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c h1:+pKlWGMw7gf6bQ github.com/docker/go-events v0.0.0-20190806004212-e31b211e4f1c/go.mod h1:Uw6UezgYA44ePAFQYUehOuCzmy5zmg/+nl2ZfMWGkpA= github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= -github.com/emicklei/go-restful/v3 v3.11.0 h1:rAQeMHw1c7zTmncogyy8VvRZwtkmkZ4FxERmMY4rD+g= -github.com/emicklei/go-restful/v3 v3.11.0/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= +github.com/emicklei/go-restful/v3 v3.12.2 h1:DhwDP0vY3k8ZzE0RunuJy8GhNpPL6zqLkDf9B/a0/xU= +github.com/emicklei/go-restful/v3 v3.12.2/go.mod h1:6n3XBCmQQb25CM2LCACGz8ukIrRry+4bhvbpWn3mrbc= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= @@ -267,8 +269,6 @@ github.com/envoyproxy/go-control-plane/envoy v1.32.4/go.mod h1:Gzjc5k8JcJswLjAx1 github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/envoyproxy/protoc-gen-validate v1.2.1 h1:DEo3O99U8j4hBFwbJfrz9VtgcDfUKS7KJ7spH3d86P8= github.com/envoyproxy/protoc-gen-validate v1.2.1/go.mod h1:d/C80l/jxXLdfEIhX1W2TmLfsJ31lvEjwamM4DxlWXU= -github.com/evanphx/json-patch v5.9.11+incompatible h1:ixHHqfcGvxhWkniF1tWxBHA0yb4Z+d1UQi45df52xW8= -github.com/evanphx/json-patch v5.9.11+incompatible/go.mod h1:50XU6AFN0ol/bzJsmQLiYLvXMP4fmwYFNcr97nuDLSk= github.com/evanphx/json-patch/v5 v5.6.0 h1:b91NhWfaz02IuVxO9faSllyAtNXHMPkC5J8sJCLunww= github.com/evanphx/json-patch/v5 v5.6.0/go.mod h1:G79N1coSVB93tBe7j6PhzjmR3/2VvlbKOFpnXhI9Bw4= github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM= @@ -281,8 +281,8 @@ github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMo github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= -github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E= -github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ= +github.com/fxamacker/cbor/v2 v2.9.0 h1:NpKPmjDBgUfBms6tr6JZkTHtfFGcMKsw3eGcmD/sapM= +github.com/fxamacker/cbor/v2 v2.9.0/go.mod h1:vM4b+DJCtHn+zz7h3FFp/hDAI9WNWCsZj23V5ytsSxQ= github.com/gabriel-vasile/mimetype v1.4.11 h1:AQvxbp830wPhHTqc1u7nzoLT+ZFxGY7emj5DR5DYFik= github.com/gabriel-vasile/mimetype v1.4.11/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s= github.com/ghodss/yaml v1.0.0 h1:wQHKEahhL6wmXdzwWG11gIVCkOv05bNOh+Rxn0yngAk= @@ -294,8 +294,8 @@ github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= -github.com/go-logr/zapr v1.2.3 h1:a9vnzlIBPQBBkeaR9IuMUfmVOrQlkoC4YfPoFkX3T7A= -github.com/go-logr/zapr v1.2.3/go.mod h1:eIauM6P8qSvTw5o2ez6UEAfGjQKrxQTl5EoK+Qa2oG4= +github.com/go-logr/zapr v1.3.0 h1:XGdV8XW8zdwFiwOA2Dryh1gj2KRQyOOoNmBy4EplIcQ= +github.com/go-logr/zapr v1.3.0/go.mod h1:YKepepNBd1u/oyhd/yQmtjVXmm9uML4IXUgMOwR8/Gg= github.com/go-openapi/analysis v0.23.0 h1:aGday7OWupfMs+LbmLZG4k0MYXIANxcuBTYUC03zFCU= github.com/go-openapi/analysis v0.23.0/go.mod h1:9mz9ZWaSlV8TvjQHLl2mUW2PbZtemkE8yA5v22ohupo= github.com/go-openapi/errors v0.20.2/go.mod h1:cM//ZKUKyO06HSwqAelJ5NsEMMcpa6VpXe8DOa1Mi1M= @@ -363,8 +363,8 @@ github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiu github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= -github.com/google/gnostic-models v0.6.9 h1:MU/8wDLif2qCXZmzncUQ/BOfxWfthHi63KqpoNbWqVw= -github.com/google/gnostic-models v0.6.9/go.mod h1:CiWsm0s6BSQd1hRn8/QmxqB6BesYcbSZxsz9b0KuDBw= +github.com/google/gnostic-models v0.7.0 h1:qwTtogB15McXDaNqTZdzPJRHvaVJlAl+HVQnLmJEJxo= +github.com/google/gnostic-models v0.7.0/go.mod h1:whL5G0m6dmc5cPxKc5bdKdEN3UjI7OUGxBlw57miDrQ= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= @@ -374,16 +374,13 @@ github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= -github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/pprof v0.0.0-20210407192527-94a9f03dee38/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/pprof v0.0.0-20241210010833-40e02aabc2ad h1:a6HEuzUHeKH6hwfN/ZoQgRgVIWFJljSWa/zetS2WTvg= -github.com/google/pprof v0.0.0-20241210010833-40e02aabc2ad/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144= +github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 h1:BHT72Gu3keYf3ZEu2J0b1vyeLSOYI8bm5wbJM/8yDe8= +github.com/google/pprof v0.0.0-20250403155104-27863c87afa6/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA= github.com/google/s2a-go v0.1.9 h1:LGD7gtMgezd8a/Xak7mEWL0PjoTQFvpRudN895yqKW0= github.com/google/s2a-go v0.1.9/go.mod h1:YA0Ei2ZQL3acow2O62kdp9UlnvMmU7kA6Eutn0dXayM= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= @@ -399,10 +396,10 @@ github.com/googleapis/gax-go/v2 v2.15.0 h1:SyjDc1mGgZU5LncH8gimWo9lW1DtIfPibOG81 github.com/googleapis/gax-go/v2 v2.15.0/go.mod h1:zVVkkxAQHa1RQpg9z2AUCMnKhi0Qld9rcmyfL1OZhoc= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gopherjs/gopherjs v0.0.0-20200217142428-fce0ec30dd00/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= -github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= -github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0 h1:YBftPWNWd4WwGqtY2yeZL2ef8rHAxPBD8KFhJpmcqms= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0/go.mod h1:YN5jB8ie0yfIUg6VvR9Kz84aCaG7AsGZnLjhHbUqwPg= +github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674 h1:JeSE6pjso5THxAzdVpqr6/geYxZytqFMBCOtn/ujyeo= +github.com/gorilla/websocket v1.5.4-0.20250319132907-e064f32e3674/go.mod h1:r4w70xmWCQKmi1ONH4KIaBptdivuRPyosB9RmPlGEwA= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3 h1:5ZPtiqj0JL5oKWmcsq4VMaAW5ukBEgSGXEN89zeH1Jo= +github.com/grpc-ecosystem/grpc-gateway/v2 v2.26.3/go.mod h1:ndYquD05frm2vACXE1nsccT4oJzjhw2arTS2cpUD1PI= github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ= @@ -490,8 +487,9 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= -github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee h1:W5t00kpgFdJifH4BDsTlE89Zl93FEloxaWZfGcifgq8= +github.com/modern-go/reflect2 v1.0.3-0.20250322232337-35a7c28c31ee/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 h1:n6/2gBQ3RWajuToeY6ZtZTIKv2v7ThUy5KKusIT0yc0= github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00/go.mod h1:Pm3mSP3c5uWn86xMLZ5Sa7JB9GsEZySvHYXCTK4E9q4= github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc= @@ -515,16 +513,16 @@ github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042 github.com/onsi/ginkgo/v2 v2.1.3/go.mod h1:vw5CSIxN1JObi/U8gcbwft7ZxR2dgaR70JSE3/PpL4c= github.com/onsi/ginkgo/v2 v2.1.4/go.mod h1:um6tUpWM/cxCK3/FK8BXqEiUMUwRgSM4JXG47RKZmLU= github.com/onsi/ginkgo/v2 v2.1.6/go.mod h1:MEH45j8TBi6u9BMogfbp0stKC5cdGjumZj5Y7AG4VIk= -github.com/onsi/ginkgo/v2 v2.23.3 h1:edHxnszytJ4lD9D5Jjc4tiDkPBZ3siDeJJkUZJJVkp0= -github.com/onsi/ginkgo/v2 v2.23.3/go.mod h1:zXTP6xIp3U8aVuXN8ENK9IXRaTjFnpVB9mGmaSRvxnM= +github.com/onsi/ginkgo/v2 v2.27.2 h1:LzwLj0b89qtIy6SSASkzlNvX6WktqurSHwkk2ipF/Ns= +github.com/onsi/ginkgo/v2 v2.27.2/go.mod h1:ArE1D/XhNXBXCBkKOLkbsb2c81dQHCRcF5zwn/ykDRo= github.com/onsi/gomega v1.7.1/go.mod h1:XdKZgCCFLUoM/7CFJVPcG8C1xQ1AJ0vpAezJrB7JYyY= github.com/onsi/gomega v1.10.1/go.mod h1:iN09h71vgCQne3DLsj+A5owkum+a2tYe+TOCB1ybHNo= github.com/onsi/gomega v1.17.0/go.mod h1:HnhC7FXeEQY45zxNK3PPoIUhzk/80Xly9PcubAlGdZY= github.com/onsi/gomega v1.19.0/go.mod h1:LY+I3pBVzYsTBU1AnDwOSxaYi9WoWiqgwooUqq9yPro= github.com/onsi/gomega v1.20.1/go.mod h1:DtrZpjmvpn2mPm4YWQa0/ALMDj9v4YxLgojwPeREyVo= github.com/onsi/gomega v1.21.1/go.mod h1:iYAIXgPSaDHak0LCMA+AWBpIKBr8WZicMxnE8luStNc= -github.com/onsi/gomega v1.37.0 h1:CdEG8g0S133B4OswTDC/5XPSzE1OeP29QOioj2PID2Y= -github.com/onsi/gomega v1.37.0/go.mod h1:8D9+Txp43QWKhM24yyOBEdpkzN8FvJyAwecBgsU4KU0= +github.com/onsi/gomega v1.38.2 h1:eZCjf2xjZAqe+LeWvKb5weQ+NcPwX84kqJ0cZNxok2A= +github.com/onsi/gomega v1.38.2/go.mod h1:W2MJcYxRGV63b418Ai34Ud0hEdTVXq9NW9+Sx6uXf3k= github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040= @@ -554,14 +552,14 @@ github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH github.com/prometheus/client_golang v1.14.0 h1:nJdhIvne2eSX/XRAFV9PcvFFRbrjbcTUj0VP62TMhnw= github.com/prometheus/client_golang v1.14.0/go.mod h1:8vpkKitgIVNcqrRBWh1C4TIUQgYNtG/XQE4E/Zae36Y= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E= -github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY= -github.com/prometheus/common v0.62.0 h1:xasJaQlnWAeyHdUBeGjXmutelfJHWMRr+Fg4QszZ2Io= -github.com/prometheus/common v0.62.0/go.mod h1:vyBcEuLSvWos9B1+CyL7JZ2up+uFzXhkqml0W5zIY1I= -github.com/prometheus/procfs v0.15.1 h1:YagwOFzUgYfKKHX6Dr+sHT7km/hxC76UB0learggepc= -github.com/prometheus/procfs v0.15.1/go.mod h1:fB45yRUv8NstnjriLhBQLuOUt+WW4BsoGhij/e3PBqk= -github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII= -github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o= +github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= +github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= +github.com/prometheus/common v0.66.1 h1:h5E0h5/Y8niHc5DlaLlWLArTQI7tMrsfQjHV+d9ZoGs= +github.com/prometheus/common v0.66.1/go.mod h1:gcaUsgf3KfRSwHY4dIMXLPV0K/Wg1oZ8+SbZk/HH/dA= +github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg= +github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is= +github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= +github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/sergi/go-diff v1.2.0 h1:XU+rvMAioB0UC3q1MFrIQy4Vo5/4VsRDQQXHsEya6xQ= github.com/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= @@ -570,10 +568,11 @@ github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVs github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= github.com/smartystreets/assertions v1.1.0/go.mod h1:tcbTF8ujkAEcZ8TElKY+i30BzYlVhC/LOxJk7iOWnoo= github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= -github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I= -github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0= -github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= -github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/cobra v1.10.0 h1:a5/WeUlSDCvV5a45ljW2ZFtV0bTDpkfSAj3uqB6Sc+0= +github.com/spf13/cobra v1.10.0/go.mod h1:9dhySC7dnTtEiqzmqfkLj47BslqLCUPMXjG2lj/NgoE= +github.com/spf13/pflag v1.0.8/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY= +github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= @@ -635,8 +634,8 @@ go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0 h1:F7Jx+6h go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.61.0/go.mod h1:UHB22Z8QsdRDrnAtX4PntOl36ajSxcdUMt1sF7Y6E7Q= go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ= go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.19.0 h1:Mne5On7VWdx7omSrSSZvM4Kw7cS7NQkOOmLcgscI51U= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.19.0/go.mod h1:IPtUMKL4O3tH5y+iXVyAXqpAwMuzC1IrxVS81rummfE= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.34.0 h1:OeNbIYk/2C15ckl7glBlOBp5+WlYsOElzTNmiPW/x60= +go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.34.0/go.mod h1:7Bept48yIeqxP2OZ9/AqIpYS94h2or0aB4FypJTc8ZM= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.21.0 h1:digkEZCJWobwBqMwC0cwCq8/wkkRy/OowZg5OArWZrM= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.21.0/go.mod h1:/OpE/y70qVkndM0TrxT4KBoN3RsFZP0QaofcfYrj76I= go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE= @@ -647,22 +646,20 @@ go.opentelemetry.io/otel/sdk/metric v1.37.0 h1:90lI228XrB9jCMuSdA0673aubgRobVZFh go.opentelemetry.io/otel/sdk/metric v1.37.0/go.mod h1:cNen4ZWfiD37l5NhS+Keb5RXVWZWpRE+9WyVCpbo5ps= go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4= go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0= -go.opentelemetry.io/proto/otlp v1.0.0 h1:T0TX0tmXU8a3CbNXzEKGeU5mIVOdf0oykP+u2lIVU/I= -go.opentelemetry.io/proto/otlp v1.0.0/go.mod h1:Sy6pihPLfYHkr3NkUbEhGHFhINUSI/v80hjKIs5JXpM= +go.opentelemetry.io/proto/otlp v1.5.0 h1:xJvq7gMzB31/d406fB8U5CBdyQGw4P399D1aQWU/3i4= +go.opentelemetry.io/proto/otlp v1.5.0/go.mod h1:keN8WnHxOy8PG0rQZjJJ5A2ebUoafqWp0eVQ4yIXvJ4= go.starlark.net v0.0.0-20200306205701-8dd3e2ee1dd5 h1:+FNtrFTmVw0YZGpBGX56XDee331t6JAXeK2bcyhLOOc= go.starlark.net v0.0.0-20200306205701-8dd3e2ee1dd5/go.mod h1:nmDLcffg48OtT/PSW0Hg7FvpRQsQh5OSqIylirxKC7o= -go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE= -go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= -go.uber.org/multierr v1.6.0 h1:y6IPFStTAIT5Ytl7/XYmHvzXQ7S3g/IeZW9hyZ5thw4= -go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= -go.uber.org/zap v1.24.0 h1:FiJd5l1UOLj0wCgbSE0rwwXHzEdAZS6hiiSnxJN/D60= -go.uber.org/zap v1.24.0/go.mod h1:2kMP+WWQ8aoFoedH3T2sq6iJ2yDWpHbP0f6MQbS9Gkg= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= +go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0= go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8= -go.yaml.in/yaml/v3 v3.0.3 h1:bXOww4E/J3f66rav3pX3m8w6jDE4knZjGOw8b5Y6iNE= -go.yaml.in/yaml/v3 v3.0.3/go.mod h1:tBHosrYAkRZjRAOREWbDnBXUf08JOwYq++0QNwQiWzI= +go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= +go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191219195013-becbf705a915/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= @@ -894,6 +891,8 @@ gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8 gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/evanphx/json-patch.v4 v4.13.0 h1:czT3CmqEaQ1aanPc5SdlgQrrEIb8w/wwCvWWnfEbYzo= +gopkg.in/evanphx/json-patch.v4 v4.13.0/go.mod h1:p8EYWUEYMpynmqDbY58zCKCFZw8pRWMG4EsWvDvM72M= gopkg.in/evanphx/json-patch.v5 v5.6.0 h1:BMT6KIwBD9CaU91PJCZIe46bDmBWa9ynTQgJIOpfQBk= gopkg.in/evanphx/json-patch.v5 v5.6.0/go.mod h1:/kvTRh1TVm5wuM6OkHxqXtE/1nUZZpihg29RtuIyfvk= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= @@ -919,22 +918,24 @@ gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q= gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -k8s.io/api v0.29.0 h1:NiCdQMY1QOp1H8lfRyeEf8eOwV6+0xA6XEE44ohDX2A= -k8s.io/api v0.29.0/go.mod h1:sdVmXoz2Bo/cb77Pxi71IPTSErEW32xa4aXwKH7gfBA= +k8s.io/api v0.35.0 h1:iBAU5LTyBI9vw3L5glmat1njFK34srdLmktWwLTprlY= +k8s.io/api v0.35.0/go.mod h1:AQ0SNTzm4ZAczM03QH42c7l3bih1TbAXYo0DkF8ktnA= k8s.io/apiextensions-apiserver v0.26.0 h1:Gy93Xo1eg2ZIkNX/8vy5xviVSxwQulsnUdQ00nEdpDo= k8s.io/apiextensions-apiserver v0.26.0/go.mod h1:7ez0LTiyW5nq3vADtK6C3kMESxadD51Bh6uz3JOlqWQ= -k8s.io/apimachinery v0.33.0 h1:1a6kHrJxb2hs4t8EE5wuR/WxKDwGN1FKH3JvDtA0CIQ= -k8s.io/apimachinery v0.33.0/go.mod h1:BHW0YOu7n22fFv/JkYOEfkUYNRN0fj0BlvMFWA7b+SM= -k8s.io/client-go v0.29.0 h1:KmlDtFcrdUzOYrBhXHgKw5ycWzc3ryPX5mQe0SkG3y8= -k8s.io/client-go v0.29.0/go.mod h1:yLkXH4HKMAywcrD82KMSmfYg2DlE8mepPR4JGSo5n38= +k8s.io/apimachinery v0.35.0 h1:Z2L3IHvPVv/MJ7xRxHEtk6GoJElaAqDCCU0S6ncYok8= +k8s.io/apimachinery v0.35.0/go.mod h1:jQCgFZFR1F4Ik7hvr2g84RTJSZegBc8yHgFWKn//hns= +k8s.io/client-go v0.35.0 h1:IAW0ifFbfQQwQmga0UdoH0yvdqrbwMdq9vIFEhRpxBE= +k8s.io/client-go v0.35.0/go.mod h1:q2E5AAyqcbeLGPdoRB+Nxe3KYTfPce1Dnu1myQdqz9o= +k8s.io/component-base v0.35.0 h1:+yBrOhzri2S1BVqyVSvcM3PtPyx5GUxCK2tinZz1G94= +k8s.io/component-base v0.35.0/go.mod h1:85SCX4UCa6SCFt6p3IKAPej7jSnF3L8EbfSyMZayJR0= k8s.io/cri-api v0.33.0 h1:YyGNgWmuSREqFPlP3XCstlHLilYdW898KwtKoaTYwBs= k8s.io/cri-api v0.33.0/go.mod h1:OLQvT45OpIA+tv91ZrpuFIGY+Y2Ho23poS7n115Aocs= k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk= k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE= -k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff h1:/usPimJzUKKu+m+TE36gUyGcf03XZEP0ZIKgKj35LS4= -k8s.io/kube-openapi v0.0.0-20250318190949-c8a335a9a2ff/go.mod h1:5jIi+8yX4RIb8wk3XwBo5Pq2ccx4FP10ohkbSKCZoK8= -k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738 h1:M3sRQVHv7vB20Xc2ybTt7ODCeFj6JSWYFzOFnYeS6Ro= -k8s.io/utils v0.0.0-20241104100929-3ea5e8cea738/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= +k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912 h1:Y3gxNAuB0OBLImH611+UDZcmKS3g6CthxToOb37KgwE= +k8s.io/kube-openapi v0.0.0-20250910181357-589584f1c912/go.mod h1:kdmbQkyfwUagLfXIad1y2TdrjPFWp2Q89B3qkRwf/pQ= +k8s.io/utils v0.0.0-20251002143259-bc988d571ff4 h1:SjGebBtkBqHFOli+05xYbK8YF1Dzkbzn+gDM4X9T4Ck= +k8s.io/utils v0.0.0-20251002143259-bc988d571ff4/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0= libvirt.org/go/libvirt v1.11010.0 h1:1EIh2x6qcRoIBBOvrgN62vq5FIpgUBrmGadprQ/4M0Y= libvirt.org/go/libvirt v1.11010.0/go.mod h1:1WiFE8EjZfq+FCVog+rvr1yatKbKZ9FaFMZgEqxEJqQ= libvirt.org/go/libvirtxml v1.11010.0 h1:lGUv6OQ4gz5Hm7F40G+swxmK/kcrMZGQ3M8/S+UyhME= @@ -943,20 +944,18 @@ sigs.k8s.io/controller-runtime v0.14.1 h1:vThDes9pzg0Y+UbCPY3Wj34CGIYPgdmspPm2GI sigs.k8s.io/controller-runtime v0.14.1/go.mod h1:GaRkrY8a7UZF0kqFFbUKG7n9ICiTY5T55P1RiE3UZlU= sigs.k8s.io/e2e-framework v0.1.0 h1:JwbS89FVX0K0pZG/x6dRgDZP9XedeVmahslqwA68uSE= sigs.k8s.io/e2e-framework v0.1.0/go.mod h1:Gb+pWwEFOD38lvDZIWKACWN9LpeoFuwyK/skZUKcuwY= -sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3 h1:/Rv+M11QRah1itp8VhT6HoVx1Ray9eB4DBr+K+/sCJ8= -sigs.k8s.io/json v0.0.0-20241010143419-9aa6b5e7a4b3/go.mod h1:18nIHnGi6636UCz6m8i4DhaJ65T6EruyzmoQqI2BVDo= +sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 h1:IpInykpT6ceI+QxKBbEflcR5EXP7sU1kvOlxwZh5txg= +sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730/go.mod h1:mdzfpAEoE6DHQEN0uh9ZbOCuHbLK5wOm7dK4ctXE9Tg= sigs.k8s.io/kustomize v2.0.3+incompatible h1:JUufWFNlI44MdtnjUqVnvh29rR37PQFzPbLXqhyOyX0= sigs.k8s.io/kustomize v2.0.3+incompatible/go.mod h1:MkjgH3RdOWrievjo6c9T245dYlB5QeXV4WCbnt/PEpU= sigs.k8s.io/kustomize/api v0.16.0 h1:/zAR4FOQDCkgSDmVzV2uiFbuy9bhu3jEzthrHCuvm1g= sigs.k8s.io/kustomize/api v0.16.0/go.mod h1:MnFZ7IP2YqVyVwMWoRxPtgl/5hpA+eCCrQR/866cm5c= sigs.k8s.io/kustomize/kyaml v0.16.0 h1:6J33uKSoATlKZH16unr2XOhDI+otoe2sR3M8PDzW3K0= sigs.k8s.io/kustomize/kyaml v0.16.0/go.mod h1:xOK/7i+vmE14N2FdFyugIshB8eF6ALpy7jI87Q2nRh4= -sigs.k8s.io/randfill v0.0.0-20250304075658-069ef1bbf016/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= sigs.k8s.io/randfill v1.0.0 h1:JfjMILfT8A6RbawdsK2JXGBR5AQVfd+9TbzrlneTyrU= sigs.k8s.io/randfill v1.0.0/go.mod h1:XeLlZ/jmk4i1HRopwe7/aU3H5n1zNUcX6TM94b3QxOY= -sigs.k8s.io/structured-merge-diff/v4 v4.6.0 h1:IUA9nvMmnKWcj5jl84xn+T5MnlZKThmUW1TdblaLVAc= -sigs.k8s.io/structured-merge-diff/v4 v4.6.0/go.mod h1:dDy58f92j70zLsuZVuUX5Wp9vtxXpaZnkPGWeqDfCps= -sigs.k8s.io/yaml v1.4.0/go.mod h1:Ejl7/uTz7PSA4eKMyQCUTnhZYNmLIl+5c2lQPGR2BPY= +sigs.k8s.io/structured-merge-diff/v6 v6.3.0 h1:jTijUJbW353oVOd9oTlifJqOGEkUw2jB/fXCbTiQEco= +sigs.k8s.io/structured-merge-diff/v6 v6.3.0/go.mod h1:M3W8sfWvn2HhQDIbGWj3S099YozAsymCo/wrT5ohRUE= sigs.k8s.io/yaml v1.6.0 h1:G8fkbMSAFqgEFgh4b1wmtzDnioxFCUgTZhlbj5P9QYs= sigs.k8s.io/yaml v1.6.0/go.mod h1:796bPqUfzR/0jLAl6XjHl3Ck7MiyVv8dbTdyT3/pMf4= tags.cncf.io/container-device-interface v1.0.1 h1:KqQDr4vIlxwfYh0Ed/uJGVgX+CHAkahrgabg6Q8GYxc= diff --git a/src/cloud-api-adaptor/install/charts/peerpods/Chart.yaml b/src/cloud-api-adaptor/install/charts/peerpods/Chart.yaml index e0592fa56c..18035e2cd3 100644 --- a/src/cloud-api-adaptor/install/charts/peerpods/Chart.yaml +++ b/src/cloud-api-adaptor/install/charts/peerpods/Chart.yaml @@ -30,7 +30,7 @@ dependencies: alias: resourceCtrl condition: resourceCtrl.enabled - name: peerpods-webhook - version: "0.1.0" + version: "0.3.2" repository: "file://../../../../webhook/chart" alias: webhook condition: webhook.enabled diff --git a/src/cloud-api-adaptor/install/charts/peerpods/templates/configmap.yaml b/src/cloud-api-adaptor/install/charts/peerpods/templates/configmap.yaml index 11d1c09092..b321f48a78 100644 --- a/src/cloud-api-adaptor/install/charts/peerpods/templates/configmap.yaml +++ b/src/cloud-api-adaptor/install/charts/peerpods/templates/configmap.yaml @@ -13,3 +13,9 @@ data: {{- range $key, $value := $config }} {{ $key }}: "{{ $value }}" {{- end }} +{{- if .Values.tlsProfile.minVersion }} + TLS_MIN_VERSION: "{{ .Values.tlsProfile.minVersion }}" +{{- end }} +{{- if .Values.tlsProfile.cipherSuites }} + TLS_CIPHER_SUITES: "{{ .Values.tlsProfile.cipherSuites }}" +{{- end }} diff --git a/src/cloud-api-adaptor/install/charts/peerpods/templates/daemonset.yaml b/src/cloud-api-adaptor/install/charts/peerpods/templates/daemonset.yaml index 3c85fd2c0d..45e26bb1f7 100644 --- a/src/cloud-api-adaptor/install/charts/peerpods/templates/daemonset.yaml +++ b/src/cloud-api-adaptor/install/charts/peerpods/templates/daemonset.yaml @@ -50,6 +50,7 @@ spec: httpGet: path: /startup port: 8000 + host: 127.0.0.1 failureThreshold: 30 periodSeconds: 20 initialDelaySeconds: 20 diff --git a/src/cloud-api-adaptor/install/charts/peerpods/values.yaml b/src/cloud-api-adaptor/install/charts/peerpods/values.yaml index 4e3b5d3a62..6924026497 100644 --- a/src/cloud-api-adaptor/install/charts/peerpods/values.yaml +++ b/src/cloud-api-adaptor/install/charts/peerpods/values.yaml @@ -83,6 +83,19 @@ image: # Cloud provider: libvirt, aws, azure, gcp, ibmcloud, vsphere provider: libvirt +# TLS profile for CAA and peer pod connections. +# When set, these values are injected into the peer-pods-cm ConfigMap as +# TLS_MIN_VERSION and TLS_CIPHER_SUITES env vars consumed by the CAA DaemonSet. +# On managed platforms, the operator injects these directly and this section +# is not needed. +tlsProfile: + # Minimum TLS version. Accepts "VersionTLS12" or "VersionTLS13". + # Defaults to TLS 1.2 when empty. + minVersion: "" + # Comma-separated IANA cipher suite names. + # Must not be set when minVersion is VersionTLS13. + cipherSuites: "" + # Maximum number of peer pods allowed to run simultaneously # This limit prevents resource exhaustion on the cloud provider # Set to "0" for unlimited (not recommended for production) diff --git a/src/cloud-api-adaptor/pkg/adaptor/cloud/cloud.go b/src/cloud-api-adaptor/pkg/adaptor/cloud/cloud.go index dd59ec45f4..5a1d838839 100644 --- a/src/cloud-api-adaptor/pkg/adaptor/cloud/cloud.go +++ b/src/cloud-api-adaptor/pkg/adaptor/cloud/cloud.go @@ -232,6 +232,13 @@ func (s *cloudService) CreateVM(ctx context.Context, req *pb.CreateVMRequest) (r TLSClientCA: string(agentProxy.ClientCA()), } + if s.serverConfig.TLSConfig != nil { + // TLS profile is delivered to APF via user-data and is immutable after VM boot. + // Profile changes only apply to newly created peer pods. + daemonConfig.MinTLSVersion = s.serverConfig.TLSConfig.MinTLSVersion + daemonConfig.CipherSuites = s.serverConfig.TLSConfig.CipherSuites + } + if caService := agentProxy.CAService(); caService != nil { certPEM, keyPEM, err := caService.Issue(serverName) if err != nil { diff --git a/src/cloud-api-adaptor/pkg/adaptor/cloud/cloud_test.go b/src/cloud-api-adaptor/pkg/adaptor/cloud/cloud_test.go index 2b962f3223..76d388e9b5 100644 --- a/src/cloud-api-adaptor/pkg/adaptor/cloud/cloud_test.go +++ b/src/cloud-api-adaptor/pkg/adaptor/cloud/cloud_test.go @@ -5,14 +5,18 @@ package cloud import ( "context" + "encoding/json" "fmt" "net/netip" "net/url" + "os" + "path/filepath" "testing" cri "github.com/containerd/containerd/pkg/cri/annotations" pb "github.com/kata-containers/kata-containers/src/runtime/protocols/hypervisor" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "github.com/confidential-containers/cloud-api-adaptor/src/cloud-api-adaptor/pkg/adaptor/proxy" "github.com/confidential-containers/cloud-api-adaptor/src/cloud-api-adaptor/pkg/forwarder" @@ -157,3 +161,75 @@ func TestCloudService(t *testing.T) { assert.NoError(t, err) assert.NotNil(t, res3) } + +func TestCreateVMTLSProfilePropagation(t *testing.T) { + ctx := context.Background() + dir := t.TempDir() + + proxyFactory := &mockProxyFactory{podsDir: dir} + + t.Run("TLS profile written to apf.json when TLSConfig is set", func(t *testing.T) { + cfg := &ServerConfig{ + PodsDir: dir, + ForwarderPort: forwarder.DefaultListenPort, + TLSConfig: &tlsutil.TLSConfig{ + MinTLSVersion: "VersionTLS13", + CipherSuites: []string{"TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256"}, + }, + } + + s := NewService(&mockProvider{}, proxyFactory, &mockWorkerNode{}, cfg) + + req := &pb.CreateVMRequest{ + Id: "tls-test-sandbox", + Annotations: map[string]string{ + cri.SandboxNamespace: "default", + cri.SandboxName: "tls-test-pod", + }, + } + + _, err := s.CreateVM(ctx, req) + require.NoError(t, err) + + apfPath := filepath.Join(dir, "tls-test-sandbox", "apf.json") + data, err := os.ReadFile(apfPath) + require.NoError(t, err) + + var daemonCfg forwarder.Config + require.NoError(t, json.Unmarshal(data, &daemonCfg)) + + assert.Equal(t, "VersionTLS13", daemonCfg.MinTLSVersion) + assert.Equal(t, []string{"TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256"}, daemonCfg.CipherSuites) + }) + + t.Run("TLS profile fields absent from apf.json when TLSConfig is nil", func(t *testing.T) { + cfg := &ServerConfig{ + PodsDir: dir, + ForwarderPort: forwarder.DefaultListenPort, + TLSConfig: nil, + } + + s := NewService(&mockProvider{}, proxyFactory, &mockWorkerNode{}, cfg) + + req := &pb.CreateVMRequest{ + Id: "notls-test-sandbox", + Annotations: map[string]string{ + cri.SandboxNamespace: "default", + cri.SandboxName: "notls-test-pod", + }, + } + + _, err := s.CreateVM(ctx, req) + require.NoError(t, err) + + apfPath := filepath.Join(dir, "notls-test-sandbox", "apf.json") + data, err := os.ReadFile(apfPath) + require.NoError(t, err) + + var daemonCfg forwarder.Config + require.NoError(t, json.Unmarshal(data, &daemonCfg)) + + assert.Empty(t, daemonCfg.MinTLSVersion) + assert.Empty(t, daemonCfg.CipherSuites) + }) +} diff --git a/src/cloud-api-adaptor/pkg/forwarder/forwarder.go b/src/cloud-api-adaptor/pkg/forwarder/forwarder.go index 956fbf7987..05e1d27c60 100644 --- a/src/cloud-api-adaptor/pkg/forwarder/forwarder.go +++ b/src/cloud-api-adaptor/pkg/forwarder/forwarder.go @@ -43,6 +43,13 @@ type Config struct { TLSServerCert string `json:"tls-server-cert,omitempty"` TLSClientCA string `json:"tls-client-ca,omitempty"` + // MinTLSVersion and CipherSuites carry the operator-injected TLS profile through + // user-data to the agent protocol forwarder running inside the peer pod VM. + // These fields are serialized to apf.json and are immutable after VM boot — + // TLS profile changes only take effect for newly created peer pods. + MinTLSVersion string `json:"tls-min-version,omitempty"` + CipherSuites []string `json:"tls-cipher-suites,omitempty"` + PpPrivateKey []byte `json:"sc-pp-prv,omitempty"` WnPublicKey []byte `json:"sc-wn-pub,omitempty"` } @@ -76,6 +83,14 @@ func NewDaemon(spec *Config, listenAddr string, tlsConfig *tlsutil.TLSConfig, in tlsConfig.CAData = []byte(spec.TLSClientCA) } + if tlsConfig != nil && tlsConfig.MinTLSVersion == "" && spec.MinTLSVersion != "" { + tlsConfig.MinTLSVersion = spec.MinTLSVersion + } + + if tlsConfig != nil && len(tlsConfig.CipherSuites) == 0 && len(spec.CipherSuites) > 0 { + tlsConfig.CipherSuites = spec.CipherSuites + } + daemon := &daemon{ listenAddr: listenAddr, tlsConfig: tlsConfig, diff --git a/src/cloud-api-adaptor/pkg/forwarder/forwarder_test.go b/src/cloud-api-adaptor/pkg/forwarder/forwarder_test.go index d84dc77fff..930b29963b 100644 --- a/src/cloud-api-adaptor/pkg/forwarder/forwarder_test.go +++ b/src/cloud-api-adaptor/pkg/forwarder/forwarder_test.go @@ -5,10 +5,15 @@ package forwarder import ( "context" + "encoding/json" "net" "testing" "time" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/confidential-containers/cloud-api-adaptor/src/cloud-api-adaptor/pkg/podnetwork/tunneler" "github.com/confidential-containers/cloud-api-adaptor/src/cloud-api-adaptor/pkg/util/agentproto" "github.com/confidential-containers/cloud-api-adaptor/src/cloud-api-adaptor/pkg/util/tlsutil" ) @@ -28,75 +33,662 @@ func dummyDialer(ctx context.Context) (net.Conn, error) { return &mockConn{}, nil } -func TestNew(t *testing.T) { - - config := &Config{} - tlsConfig := tlsutil.TLSConfig{} - - ret := NewDaemon(config, DefaultListenAddr, &tlsConfig, agentproto.NewRedirector(dummyDialer), &mockPodNode{}) - if ret == nil { - t.Fatal("Expect non nil, got nil") - } - d, ok := ret.(*daemon) - if !ok { - t.Fatalf("Expect *daemon, got %T", d) - } - if d.interceptor == nil { - t.Fatal("Expect non nil, got nil") - } - if d.stopCh == nil { - t.Fatal("Expect non nil, got nil") - } - select { - case <-d.stopCh: - t.Fatal("channel is closed") - default: - } +type mockPodNode struct { + setupCalled bool + teardownCalled bool + setupError error + teardownError error +} + +func (n *mockPodNode) Setup() error { + n.setupCalled = true + return n.setupError +} + +func (n *mockPodNode) Teardown() error { + n.teardownCalled = true + return n.teardownError } -func TestStart(t *testing.T) { +func TestNewDaemon(t *testing.T) { + t.Run("creates daemon with minimal config", func(t *testing.T) { + config := &Config{} + tlsConfig := &tlsutil.TLSConfig{} + + ret := NewDaemon(config, DefaultListenAddr, tlsConfig, agentproto.NewRedirector(dummyDialer), &mockPodNode{}) + require.NotNil(t, ret, "Expected non-nil daemon") + + d, ok := ret.(*daemon) + require.True(t, ok, "Expected *daemon type, got %T", ret) + assert.NotNil(t, d.interceptor, "Expected non-nil interceptor") + assert.NotNil(t, d.stopCh, "Expected non-nil stopCh") + assert.NotNil(t, d.readyCh, "Expected non-nil readyCh") + + // Verify stopCh is open + select { + case <-d.stopCh: + t.Fatal("Expected stopCh to be open") + default: + } + }) + + t.Run("creates daemon with TLS config", func(t *testing.T) { + config := &Config{ + TLSServerCert: "cert-data", + TLSServerKey: "key-data", + TLSClientCA: "ca-data", + } + tlsConfig := &tlsutil.TLSConfig{} + + ret := NewDaemon(config, DefaultListenAddr, tlsConfig, agentproto.NewRedirector(dummyDialer), &mockPodNode{}) + require.NotNil(t, ret) + + d, ok := ret.(*daemon) + require.True(t, ok) + assert.NotNil(t, d.tlsConfig) + assert.Equal(t, []byte("cert-data"), d.tlsConfig.CertData) + assert.Equal(t, []byte("key-data"), d.tlsConfig.KeyData) + assert.Equal(t, []byte("ca-data"), d.tlsConfig.CAData) + }) + + t.Run("creates daemon with custom listen address", func(t *testing.T) { + config := &Config{} + customAddr := "127.0.0.1:9999" + + ret := NewDaemon(config, customAddr, nil, agentproto.NewRedirector(dummyDialer), &mockPodNode{}) + require.NotNil(t, ret) + + d, ok := ret.(*daemon) + require.True(t, ok) + assert.Equal(t, customAddr, d.listenAddr) + }) + + t.Run("creates daemon with nil TLS config", func(t *testing.T) { + config := &Config{} - d := &daemon{ - interceptor: agentproto.NewRedirector(dummyDialer), - podNode: &mockPodNode{}, - readyCh: make(chan struct{}), - stopCh: make(chan struct{}), - } + ret := NewDaemon(config, DefaultListenAddr, nil, agentproto.NewRedirector(dummyDialer), &mockPodNode{}) + require.NotNil(t, ret) - errCh := make(chan error) - go func() { - defer close(errCh) + d, ok := ret.(*daemon) + require.True(t, ok) + assert.Nil(t, d.tlsConfig) + }) - if err := d.Start(context.Background()); err != nil { - errCh <- err + t.Run("creates daemon with pod network config", func(t *testing.T) { + config := &Config{ + PodNetwork: &tunneler.Config{ + ExternalNetViaPodVM: true, + }, } - }() - select { - case err := <-errCh: - t.Fatalf("Expect no error, got %q", err) - default: - } + ret := NewDaemon(config, DefaultListenAddr, nil, agentproto.NewRedirector(dummyDialer), &mockPodNode{}) + require.NotNil(t, ret) - if err := d.Shutdown(); err != nil { - t.Fatalf("Expect no error, got %q", err) - } + d, ok := ret.(*daemon) + require.True(t, ok) + assert.True(t, d.externalNetViaPodVM) + }) - select { - case err := <-errCh: - if err != nil { - t.Fatalf("Expect no error, got %q", err) + t.Run("handles TLS config with existing cert auth", func(t *testing.T) { + config := &Config{ + TLSServerCert: "cert-data", + TLSServerKey: "key-data", } - default: - } + tlsConfig := &tlsutil.TLSConfig{ + CertFile: "/path/to/cert", + KeyFile: "/path/to/key", + } + + ret := NewDaemon(config, DefaultListenAddr, tlsConfig, agentproto.NewRedirector(dummyDialer), &mockPodNode{}) + require.NotNil(t, ret) + + d, ok := ret.(*daemon) + require.True(t, ok) + assert.NotNil(t, d.tlsConfig) + // Should not overwrite existing cert auth + assert.Empty(t, d.tlsConfig.CertData) + }) + + t.Run("handles TLS config with existing CA", func(t *testing.T) { + config := &Config{ + TLSClientCA: "ca-data", + } + tlsConfig := &tlsutil.TLSConfig{ + CAFile: "/path/to/ca", + } + + ret := NewDaemon(config, DefaultListenAddr, tlsConfig, agentproto.NewRedirector(dummyDialer), &mockPodNode{}) + require.NotNil(t, ret) + + d, ok := ret.(*daemon) + require.True(t, ok) + assert.NotNil(t, d.tlsConfig) + // Should not overwrite existing CA + assert.Empty(t, d.tlsConfig.CAData) + }) } -type mockPodNode struct{} +func TestDaemonStart(t *testing.T) { + t.Run("starts and stops successfully", func(t *testing.T) { + podNode := &mockPodNode{} + d := &daemon{ + interceptor: agentproto.NewRedirector(dummyDialer), + podNode: podNode, + readyCh: make(chan struct{}), + stopCh: make(chan struct{}), + listenAddr: "127.0.0.1:0", // Use port 0 for automatic assignment + } -func (n *mockPodNode) Setup() error { - return nil + errCh := make(chan error) + go func() { + defer close(errCh) + + if err := d.Start(context.Background()); err != nil { + errCh <- err + } + }() + + // Wait for daemon to be ready + select { + case <-d.readyCh: + // Daemon is ready + case err := <-errCh: + t.Fatalf("Expected daemon to start, got error: %v", err) + case <-time.After(5 * time.Second): + t.Fatal("Timeout waiting for daemon to be ready") + } + + // Verify pod node setup was called + assert.True(t, podNode.setupCalled, "Expected Setup to be called") + + // Shutdown the daemon + err := d.Shutdown() + require.NoError(t, err, "Expected no error on shutdown") + + // Wait for daemon to stop + select { + case err := <-errCh: + assert.NoError(t, err, "Expected no error from Start") + case <-time.After(5 * time.Second): + t.Fatal("Timeout waiting for daemon to stop") + } + + // Verify pod node teardown was called + assert.True(t, podNode.teardownCalled, "Expected Teardown to be called") + }) + + t.Run("handles pod node setup error", func(t *testing.T) { + expectedErr := assert.AnError + podNode := &mockPodNode{ + setupError: expectedErr, + } + d := &daemon{ + interceptor: agentproto.NewRedirector(dummyDialer), + podNode: podNode, + readyCh: make(chan struct{}), + stopCh: make(chan struct{}), + listenAddr: "127.0.0.1:0", + } + + err := d.Start(context.Background()) + assert.Error(t, err) + assert.Contains(t, err.Error(), "failed to set up pod network") + assert.True(t, podNode.setupCalled) + }) + + t.Run("handles context cancellation", func(t *testing.T) { + podNode := &mockPodNode{} + d := &daemon{ + interceptor: agentproto.NewRedirector(dummyDialer), + podNode: podNode, + readyCh: make(chan struct{}), + stopCh: make(chan struct{}), + listenAddr: "127.0.0.1:0", + } + + ctx, cancel := context.WithCancel(context.Background()) + + errCh := make(chan error) + go func() { + defer close(errCh) + if err := d.Start(ctx); err != nil { + errCh <- err + } + }() + + // Wait for daemon to be ready + select { + case <-d.readyCh: + // Daemon is ready + case err := <-errCh: + t.Fatalf("Expected daemon to start, got error: %v", err) + case <-time.After(5 * time.Second): + t.Fatal("Timeout waiting for daemon to be ready") + } + + // Cancel context + cancel() + + // Wait for daemon to stop + select { + case err := <-errCh: + assert.NoError(t, err, "Expected no error from Start") + case <-time.After(5 * time.Second): + t.Fatal("Timeout waiting for daemon to stop") + } + + assert.True(t, podNode.setupCalled) + assert.True(t, podNode.teardownCalled) + }) + + t.Run("handles invalid listen address", func(t *testing.T) { + podNode := &mockPodNode{} + d := &daemon{ + interceptor: agentproto.NewRedirector(dummyDialer), + podNode: podNode, + readyCh: make(chan struct{}), + stopCh: make(chan struct{}), + listenAddr: "invalid:address:format", + } + + err := d.Start(context.Background()) + assert.Error(t, err) + assert.True(t, podNode.setupCalled) + assert.True(t, podNode.teardownCalled) + }) } -func (n *mockPodNode) Teardown() error { - return nil +func TestDaemonShutdown(t *testing.T) { + t.Run("closes stop channel", func(t *testing.T) { + d := &daemon{ + stopCh: make(chan struct{}), + } + + err := d.Shutdown() + require.NoError(t, err) + + // Verify stopCh is closed + select { + case <-d.stopCh: + // Channel is closed as expected + default: + t.Fatal("Expected stopCh to be closed") + } + }) + + t.Run("is idempotent", func(t *testing.T) { + d := &daemon{ + stopCh: make(chan struct{}), + } + + // First shutdown + err := d.Shutdown() + require.NoError(t, err) + + // Second shutdown should not panic + err = d.Shutdown() + require.NoError(t, err) + + // Verify stopCh is still closed + select { + case <-d.stopCh: + // Channel is closed as expected + default: + t.Fatal("Expected stopCh to be closed") + } + }) + + t.Run("can be called multiple times concurrently", func(t *testing.T) { + d := &daemon{ + stopCh: make(chan struct{}), + } + + done := make(chan struct{}) + for i := 0; i < 10; i++ { + go func() { + defer func() { done <- struct{}{} }() + _ = d.Shutdown() + }() + } + + // Wait for all goroutines to complete + for i := 0; i < 10; i++ { + <-done + } + + // Verify stopCh is closed + select { + case <-d.stopCh: + // Channel is closed as expected + default: + t.Fatal("Expected stopCh to be closed") + } + }) +} + +func TestDaemonReady(t *testing.T) { + t.Run("returns ready channel", func(t *testing.T) { + readyCh := make(chan struct{}) + d := &daemon{ + readyCh: readyCh, + } + + ch := d.Ready() + assert.Equal(t, readyCh, ch) + }) + + t.Run("ready channel is initially open", func(t *testing.T) { + d := &daemon{ + readyCh: make(chan struct{}), + } + + select { + case <-d.Ready(): + t.Fatal("Expected ready channel to be open") + default: + // Channel is open as expected + } + }) + + t.Run("ready channel closes when daemon starts", func(t *testing.T) { + podNode := &mockPodNode{} + d := &daemon{ + interceptor: agentproto.NewRedirector(dummyDialer), + podNode: podNode, + readyCh: make(chan struct{}), + stopCh: make(chan struct{}), + listenAddr: "127.0.0.1:0", + } + + startErrCh := make(chan error, 1) + go func() { + startErrCh <- d.Start(context.Background()) + }() + + // Wait for ready channel to close + select { + case <-d.Ready(): + // Channel closed as expected + case err := <-startErrCh: + require.NoError(t, err, "daemon failed to start before becoming ready") + case <-time.After(5 * time.Second): + t.Fatal("Timeout waiting for ready channel to close") + } + + require.NoError(t, d.Shutdown()) + + // Wait for Start() to exit cleanly + select { + case err := <-startErrCh: + require.NoError(t, err, "daemon failed to stop cleanly") + case <-time.After(5 * time.Second): + t.Fatal("Timeout waiting for daemon Start() to exit after Shutdown()") + } + }) +} + +func TestDaemonAddr(t *testing.T) { + t.Run("returns listen address after ready", func(t *testing.T) { + podNode := &mockPodNode{} + d := &daemon{ + interceptor: agentproto.NewRedirector(dummyDialer), + podNode: podNode, + readyCh: make(chan struct{}), + stopCh: make(chan struct{}), + listenAddr: "127.0.0.1:0", + } + + startErrCh := make(chan error, 1) + go func() { + startErrCh <- d.Start(context.Background()) + }() + + // Wait for daemon to be ready with timeout + select { + case <-d.Ready(): + // Daemon is ready as expected + case err := <-startErrCh: + require.NoError(t, err, "daemon failed to start before becoming ready") + case <-time.After(5 * time.Second): + t.Fatal("Timeout waiting for daemon to become ready") + } + + // Now call Addr() with timeout protection + addrCh := make(chan string, 1) + go func() { + addrCh <- d.Addr() + }() + + select { + case addr := <-addrCh: + assert.NotEmpty(t, addr) + assert.Contains(t, addr, "127.0.0.1:") + case <-time.After(1 * time.Second): + t.Fatal("Timeout waiting for Addr() to return") + } + + require.NoError(t, d.Shutdown()) + + // Wait for Start() to exit cleanly + select { + case err := <-startErrCh: + require.NoError(t, err, "daemon failed to stop cleanly") + case <-time.After(5 * time.Second): + t.Fatal("Timeout waiting for daemon Start() to exit after Shutdown()") + } + }) + + t.Run("blocks until daemon is ready", func(t *testing.T) { + d := &daemon{ + readyCh: make(chan struct{}), + listenAddr: "127.0.0.1:8080", + } + + addrCh := make(chan string) + go func() { + addrCh <- d.Addr() + }() + + // Verify Addr() is blocking + select { + case <-addrCh: + t.Fatal("Expected Addr() to block until ready") + case <-time.After(100 * time.Millisecond): + // Addr() is blocking as expected + } + + // Close ready channel + close(d.readyCh) + + // Now Addr() should return + select { + case addr := <-addrCh: + assert.Equal(t, "127.0.0.1:8080", addr) + case <-time.After(1 * time.Second): + t.Fatal("Timeout waiting for Addr() to return") + } + }) +} + +func TestDaemonConstants(t *testing.T) { + t.Run("default constants are set correctly", func(t *testing.T) { + assert.Equal(t, "0.0.0.0", DefaultListenHost) + assert.Equal(t, "15150", DefaultListenPort) + assert.Equal(t, "0.0.0.0:15150", DefaultListenAddr) + assert.Equal(t, "/run/peerpod/apf.json", DefaultConfigPath) + assert.Equal(t, "/run/peerpod/podnetwork.json", DefaultPodNetworkSpecPath) + assert.Equal(t, "/run/kata-containers/agent.sock", DefaultKataAgentSocketPath) + assert.Equal(t, "/run/netns/podns", DefaultPodNamespace) + assert.Equal(t, "/agent", AgentURLPath) + }) +} + +func TestNewDaemonTLSProfile(t *testing.T) { + t.Run("copies MinTLSVersion from Config to tlsConfig", func(t *testing.T) { + config := &Config{ + TLSServerCert: "cert", + TLSServerKey: "key", + MinTLSVersion: "VersionTLS13", + } + tlsConfig := &tlsutil.TLSConfig{} + + ret := NewDaemon(config, DefaultListenAddr, tlsConfig, agentproto.NewRedirector(dummyDialer), &mockPodNode{}) + d := ret.(*daemon) + assert.Equal(t, "VersionTLS13", d.tlsConfig.MinTLSVersion) + }) + + t.Run("copies CipherSuites from Config to tlsConfig", func(t *testing.T) { + suites := []string{"TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256"} + config := &Config{ + TLSServerCert: "cert", + TLSServerKey: "key", + CipherSuites: suites, + } + tlsConfig := &tlsutil.TLSConfig{} + + ret := NewDaemon(config, DefaultListenAddr, tlsConfig, agentproto.NewRedirector(dummyDialer), &mockPodNode{}) + d := ret.(*daemon) + assert.Equal(t, suites, d.tlsConfig.CipherSuites) + }) + + t.Run("does not overwrite MinTLSVersion already set on tlsConfig", func(t *testing.T) { + config := &Config{MinTLSVersion: "VersionTLS13"} + tlsConfig := &tlsutil.TLSConfig{MinTLSVersion: "VersionTLS12"} + + ret := NewDaemon(config, DefaultListenAddr, tlsConfig, agentproto.NewRedirector(dummyDialer), &mockPodNode{}) + d := ret.(*daemon) + assert.Equal(t, "VersionTLS12", d.tlsConfig.MinTLSVersion) + }) + + t.Run("does not overwrite CipherSuites already set on tlsConfig", func(t *testing.T) { + existing := []string{"TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384"} + config := &Config{CipherSuites: []string{"TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256"}} + tlsConfig := &tlsutil.TLSConfig{CipherSuites: existing} + + ret := NewDaemon(config, DefaultListenAddr, tlsConfig, agentproto.NewRedirector(dummyDialer), &mockPodNode{}) + d := ret.(*daemon) + assert.Equal(t, existing, d.tlsConfig.CipherSuites) + }) + + t.Run("nil tlsConfig skips profile copy", func(t *testing.T) { + config := &Config{MinTLSVersion: "VersionTLS13"} + ret := NewDaemon(config, DefaultListenAddr, nil, agentproto.NewRedirector(dummyDialer), &mockPodNode{}) + d := ret.(*daemon) + assert.Nil(t, d.tlsConfig) + }) +} + +func TestConfigJSONRoundtrip(t *testing.T) { + t.Run("MinTLSVersion and CipherSuites survive JSON round-trip", func(t *testing.T) { + original := &Config{ + PodNamespace: "default", + PodName: "test-pod", + MinTLSVersion: "VersionTLS13", + CipherSuites: []string{"TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256"}, + } + + data, err := json.Marshal(original) + require.NoError(t, err) + + var decoded Config + err = json.Unmarshal(data, &decoded) + require.NoError(t, err) + + assert.Equal(t, original.MinTLSVersion, decoded.MinTLSVersion) + assert.Equal(t, original.CipherSuites, decoded.CipherSuites) + }) + + t.Run("empty TLS profile fields omitted from JSON", func(t *testing.T) { + config := &Config{PodName: "test-pod"} + data, err := json.Marshal(config) + require.NoError(t, err) + + assert.NotContains(t, string(data), "tls-min-version") + assert.NotContains(t, string(data), "tls-cipher-suites") + }) +} + +func TestConfigStructure(t *testing.T) { + t.Run("config has expected fields", func(t *testing.T) { + config := &Config{ + PodNamespace: "test-namespace", + PodName: "test-pod", + TLSServerKey: "key", + TLSServerCert: "cert", + TLSClientCA: "ca", + PpPrivateKey: []byte("priv-key"), + WnPublicKey: []byte("pub-key"), + } + + assert.Equal(t, "test-namespace", config.PodNamespace) + assert.Equal(t, "test-pod", config.PodName) + assert.Equal(t, "key", config.TLSServerKey) + assert.Equal(t, "cert", config.TLSServerCert) + assert.Equal(t, "ca", config.TLSClientCA) + assert.Equal(t, []byte("priv-key"), config.PpPrivateKey) + assert.Equal(t, []byte("pub-key"), config.WnPublicKey) + }) + + t.Run("config can be empty", func(t *testing.T) { + config := &Config{} + + assert.Empty(t, config.PodNamespace) + assert.Empty(t, config.PodName) + assert.Empty(t, config.TLSServerKey) + assert.Empty(t, config.TLSServerCert) + assert.Empty(t, config.TLSClientCA) + assert.Nil(t, config.PpPrivateKey) + assert.Nil(t, config.WnPublicKey) + }) +} + +func TestDaemonInterface(t *testing.T) { + t.Run("daemon implements Daemon interface", func(t *testing.T) { + config := &Config{} + d := NewDaemon(config, DefaultListenAddr, nil, agentproto.NewRedirector(dummyDialer), &mockPodNode{}) + + // Verify the concrete implementation satisfies the Daemon interface + var _ Daemon = (*daemon)(nil) + + // Verify all interface methods are available + assert.NotNil(t, d.Ready()) + assert.NotPanics(t, func() { _ = d.Shutdown() }) + + // Note: d.Addr() blocks until ready channel is closed, so we don't call it here + // It's tested separately in TestDaemonAddr + }) +} + +func TestDaemonWithTLSConfig(t *testing.T) { + t.Run("daemon with TLS config has proper initialization", func(t *testing.T) { + config := &Config{ + TLSServerCert: "cert-data", + TLSServerKey: "key-data", + TLSClientCA: "ca-data", + } + tlsConfig := &tlsutil.TLSConfig{} + + d := NewDaemon(config, DefaultListenAddr, tlsConfig, agentproto.NewRedirector(dummyDialer), &mockPodNode{}) + require.NotNil(t, d) + + daemonImpl, ok := d.(*daemon) + require.True(t, ok) + + assert.NotNil(t, daemonImpl.tlsConfig) + assert.Equal(t, []byte("cert-data"), daemonImpl.tlsConfig.CertData) + assert.Equal(t, []byte("key-data"), daemonImpl.tlsConfig.KeyData) + assert.Equal(t, []byte("ca-data"), daemonImpl.tlsConfig.CAData) + }) + + t.Run("daemon without TLS config has nil tlsConfig", func(t *testing.T) { + config := &Config{} + + d := NewDaemon(config, DefaultListenAddr, nil, agentproto.NewRedirector(dummyDialer), &mockPodNode{}) + require.NotNil(t, d) + + daemonImpl, ok := d.(*daemon) + require.True(t, ok) + + assert.Nil(t, daemonImpl.tlsConfig) + }) } diff --git a/src/cloud-api-adaptor/pkg/probe/probe.go b/src/cloud-api-adaptor/pkg/probe/probe.go index 7956739fb1..bb3c957475 100644 --- a/src/cloud-api-adaptor/pkg/probe/probe.go +++ b/src/cloud-api-adaptor/pkg/probe/probe.go @@ -64,7 +64,7 @@ func Start(socketPath string) { SocketPath: socketPath, } http.HandleFunc("/startup", StartupHandler) - err = http.ListenAndServe(":"+port, nil) + err = http.ListenAndServe("127.0.0.1:"+port, nil) if err != nil { logger.Printf("failed to start startup probe server, error %s", err) diff --git a/src/cloud-api-adaptor/pkg/util/tlsconfig/tlsconfig.go b/src/cloud-api-adaptor/pkg/util/tlsconfig/tlsconfig.go new file mode 100644 index 0000000000..e773493230 --- /dev/null +++ b/src/cloud-api-adaptor/pkg/util/tlsconfig/tlsconfig.go @@ -0,0 +1,64 @@ +// (C) Copyright Confidential Containers Contributors +// SPDX-License-Identifier: Apache-2.0 + +package tlsconfig + +import ( + "crypto/tls" + "fmt" + "strings" + + cliflag "k8s.io/component-base/cli/flag" +) + +// TLS holds parsed TLS profile options as uint16 values ready for use in tls.Config. +type TLS struct { + MinVersion uint16 + CipherSuites []uint16 +} + +// ParseTLSOptions parses TLS version and cipher suite name strings into a TLS struct. +// Returns nil, nil when both inputs are empty. +// Rejects TLS 1.0 and 1.1. +// Rejects cipher suites when minVersion is VersionTLS13, since Go's crypto/tls +// does not allow configuring TLS 1.3 cipher suites. +func ParseTLSOptions(minVersion string, cipherSuites []string) (*TLS, error) { + minVersion = strings.TrimSpace(minVersion) + + var cleaned []string + for _, s := range cipherSuites { + if s = strings.TrimSpace(s); s != "" { + cleaned = append(cleaned, s) + } + } + cipherSuites = cleaned + + if minVersion == "" && len(cipherSuites) == 0 { + return nil, nil + } + + if minVersion == "VersionTLS10" || minVersion == "VersionTLS11" { + return nil, fmt.Errorf("invalid minVersion %q: TLS 1.0 and 1.1 are not supported, use VersionTLS12 or VersionTLS13", minVersion) + } + + version, err := cliflag.TLSVersion(minVersion) + if err != nil { + return nil, fmt.Errorf("invalid minVersion %q: %w", minVersion, err) + } + + if version == tls.VersionTLS13 && len(cipherSuites) > 0 { + return nil, fmt.Errorf("cipherSuites may not be specified when minVersion is VersionTLS13: Go's crypto/tls does not allow configuring TLS 1.3 cipher suites") + } + + t := &TLS{MinVersion: version} + + if len(cipherSuites) > 0 { + ids, err := cliflag.TLSCipherSuites(cipherSuites) + if err != nil { + return nil, fmt.Errorf("invalid cipherSuites: %w; valid names: %v", err, cliflag.PreferredTLSCipherNames()) + } + t.CipherSuites = ids + } + + return t, nil +} diff --git a/src/cloud-api-adaptor/pkg/util/tlsconfig/tlsconfig_test.go b/src/cloud-api-adaptor/pkg/util/tlsconfig/tlsconfig_test.go new file mode 100644 index 0000000000..e569f7049c --- /dev/null +++ b/src/cloud-api-adaptor/pkg/util/tlsconfig/tlsconfig_test.go @@ -0,0 +1,107 @@ +// (C) Copyright Confidential Containers Contributors +// SPDX-License-Identifier: Apache-2.0 + +package tlsconfig + +import ( + "crypto/tls" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestParseTLSOptions(t *testing.T) { + t.Run("empty inputs returns nil", func(t *testing.T) { + result, err := ParseTLSOptions("", nil) + require.NoError(t, err) + assert.Nil(t, result) + }) + + t.Run("VersionTLS12 parses correctly", func(t *testing.T) { + result, err := ParseTLSOptions("VersionTLS12", nil) + require.NoError(t, err) + require.NotNil(t, result) + assert.Equal(t, uint16(tls.VersionTLS12), result.MinVersion) + assert.Empty(t, result.CipherSuites) + }) + + t.Run("VersionTLS13 parses correctly", func(t *testing.T) { + result, err := ParseTLSOptions("VersionTLS13", nil) + require.NoError(t, err) + require.NotNil(t, result) + assert.Equal(t, uint16(tls.VersionTLS13), result.MinVersion) + }) + + t.Run("VersionTLS10 is rejected", func(t *testing.T) { + _, err := ParseTLSOptions("VersionTLS10", nil) + require.Error(t, err) + assert.Contains(t, err.Error(), "TLS 1.0 and 1.1 are not supported") + }) + + t.Run("VersionTLS11 is rejected", func(t *testing.T) { + _, err := ParseTLSOptions("VersionTLS11", nil) + require.Error(t, err) + assert.Contains(t, err.Error(), "TLS 1.0 and 1.1 are not supported") + }) + + t.Run("unknown version string returns error", func(t *testing.T) { + _, err := ParseTLSOptions("VersionTLS99", nil) + require.Error(t, err) + assert.Contains(t, err.Error(), "invalid minVersion") + }) + + t.Run("valid cipher suites parsed correctly", func(t *testing.T) { + suites := []string{"TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256", "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384"} + result, err := ParseTLSOptions("VersionTLS12", suites) + require.NoError(t, err) + require.NotNil(t, result) + assert.Len(t, result.CipherSuites, 2) + assert.Equal(t, uint16(tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256), result.CipherSuites[0]) + assert.Equal(t, uint16(tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384), result.CipherSuites[1]) + }) + + t.Run("unknown cipher suite name returns error", func(t *testing.T) { + _, err := ParseTLSOptions("VersionTLS12", []string{"INVALID_CIPHER_SUITE"}) + require.Error(t, err) + assert.Contains(t, err.Error(), "invalid cipherSuites") + }) + + t.Run("cipher suites with VersionTLS13 returns error", func(t *testing.T) { + _, err := ParseTLSOptions("VersionTLS13", []string{"TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256"}) + require.Error(t, err) + assert.Contains(t, err.Error(), "may not be specified when minVersion is VersionTLS13") + }) + + t.Run("empty version with cipher suites uses TLS12 default", func(t *testing.T) { + suites := []string{"TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256"} + result, err := ParseTLSOptions("", suites) + require.NoError(t, err) + require.NotNil(t, result) + assert.Equal(t, uint16(tls.VersionTLS12), result.MinVersion) + assert.Len(t, result.CipherSuites, 1) + }) + + t.Run("cipher suite names with leading/trailing spaces are trimmed", func(t *testing.T) { + suites := []string{" TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256 ", " TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384"} + result, err := ParseTLSOptions("VersionTLS12", suites) + require.NoError(t, err) + require.NotNil(t, result) + assert.Len(t, result.CipherSuites, 2) + }) + + t.Run("empty cipher suite entries are dropped", func(t *testing.T) { + suites := []string{"TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256", "", " "} + result, err := ParseTLSOptions("VersionTLS12", suites) + require.NoError(t, err) + require.NotNil(t, result) + assert.Len(t, result.CipherSuites, 1) + }) + + t.Run("minVersion with surrounding whitespace is trimmed", func(t *testing.T) { + result, err := ParseTLSOptions(" VersionTLS13 ", nil) + require.NoError(t, err) + require.NotNil(t, result) + assert.Equal(t, uint16(tls.VersionTLS13), result.MinVersion) + }) +} diff --git a/src/cloud-api-adaptor/pkg/util/tlsutil/tls.go b/src/cloud-api-adaptor/pkg/util/tlsutil/tls.go index f5a2c61afa..0ae7ab5d90 100644 --- a/src/cloud-api-adaptor/pkg/util/tlsutil/tls.go +++ b/src/cloud-api-adaptor/pkg/util/tlsutil/tls.go @@ -9,6 +9,8 @@ import ( "encoding/pem" "fmt" "os" + + "github.com/confidential-containers/cloud-api-adaptor/src/cloud-api-adaptor/pkg/util/tlsconfig" ) // TLSConfig holds the information needed to set up a TLS transport. @@ -21,6 +23,13 @@ type TLSConfig struct { CAData []byte // Bytes of the PEM-encoded server trusted root certificates. Supercedes CAFile. CertData []byte // Bytes of the PEM-encoded client certificate. Supercedes CertFile. KeyData []byte // Bytes of the PEM-encoded client key. Supercedes KeyFile. + + // MinTLSVersion sets the minimum TLS version. Accepts "VersionTLS12" or "VersionTLS13". + // Operator-injected via TLS_MIN_VERSION env var. Defaults to TLS 1.2 when empty. + MinTLSVersion string + // CipherSuites is the list of IANA TLS cipher suite names to allow. + // Operator-injected via TLS_CIPHER_SUITES env var. Must not be set when MinTLSVersion is VersionTLS13. + CipherSuites []string } // HasCA returns whether the configuration has a certificate authority or not. @@ -33,6 +42,11 @@ func (t *TLSConfig) HasCertAuth() bool { return (len(t.CertData) != 0 || len(t.CertFile) != 0) && (len(t.KeyData) != 0 || len(t.KeyFile) != 0) } +// HasTLSProfile returns whether a TLS version or cipher suite constraint is set. +func (t *TLSConfig) HasTLSProfile() bool { + return t.MinTLSVersion != "" || len(t.CipherSuites) > 0 +} + // loadTLSFiles copies the data from the CertFile, KeyFile, and CAFile fields into the CertData, // KeyData, and CAFile fields, or returns an error. If no error is returned, all three fields are // either populated or were empty to start. @@ -113,7 +127,7 @@ func createErrorParsingCAData(pemCerts []byte) error { // GetTLSConfigFor returns a tls.Config that will provide the transport level security defined // by the provided Config. Will return nil if no transport level security is requested. func GetTLSConfigFor(t *TLSConfig) (*tls.Config, error) { - if !(t.HasCA() || t.HasCertAuth() || t.SkipVerify) { + if !(t.HasCA() || t.HasCertAuth() || t.SkipVerify || t.HasTLSProfile()) { return nil, nil } if t.HasCA() && t.SkipVerify { @@ -123,14 +137,29 @@ func GetTLSConfigFor(t *TLSConfig) (*tls.Config, error) { return nil, err } + parsed, err := tlsconfig.ParseTLSOptions(t.MinTLSVersion, t.CipherSuites) + if err != nil { + return nil, fmt.Errorf("invalid TLS profile: %w", err) + } + + // Option B hard floor: always at least TLS 1.2 regardless of what was requested. + // Can't use SSLv3 because of POODLE and BEAST + // Can't use TLSv1.0 because of POODLE and BEAST using CBC cipher + // Can't use TLSv1.1 because of RC4 cipher usage + minVersion := uint16(tls.VersionTLS12) + if parsed != nil && parsed.MinVersion > minVersion { + minVersion = parsed.MinVersion + } + tlsConfig := &tls.Config{ - // Can't use SSLv3 because of POODLE and BEAST - // Can't use TLSv1.0 because of POODLE and BEAST using CBC cipher - // Can't use TLSv1.1 because of RC4 cipher usage - MinVersion: tls.VersionTLS12, + MinVersion: minVersion, InsecureSkipVerify: t.SkipVerify, } + if parsed != nil && len(parsed.CipherSuites) > 0 { + tlsConfig.CipherSuites = parsed.CipherSuites + } + if t.HasCA() { rootCAs, err := rootCertPool(t.CAData) if err != nil { diff --git a/src/cloud-api-adaptor/pkg/util/tlsutil/tls_test.go b/src/cloud-api-adaptor/pkg/util/tlsutil/tls_test.go new file mode 100644 index 0000000000..04eb50eb3b --- /dev/null +++ b/src/cloud-api-adaptor/pkg/util/tlsutil/tls_test.go @@ -0,0 +1,141 @@ +// (C) Copyright Confidential Containers Contributors +// SPDX-License-Identifier: Apache-2.0 + +package tlsutil + +import ( + "context" + "crypto/tls" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestGetTLSConfigForWithTLSProfile(t *testing.T) { + ca, err := NewCAService("test-ca") + require.NoError(t, err) + + certPEM, keyPEM, err := ca.Issue("test-server") + require.NoError(t, err) + + clientCertPEM, clientKeyPEM, err := NewClientCertificate("test-client") + require.NoError(t, err) + + baseConfig := func() *TLSConfig { + return &TLSConfig{ + CAData: clientCertPEM, + CertData: certPEM, + KeyData: keyPEM, + } + } + + t.Run("default empty profile uses TLS 1.2 floor", func(t *testing.T) { + cfg, err := GetTLSConfigFor(baseConfig()) + require.NoError(t, err) + require.NotNil(t, cfg) + assert.Equal(t, uint16(tls.VersionTLS12), cfg.MinVersion) + }) + + t.Run("VersionTLS13 is applied", func(t *testing.T) { + c := baseConfig() + c.MinTLSVersion = "VersionTLS13" + cfg, err := GetTLSConfigFor(c) + require.NoError(t, err) + require.NotNil(t, cfg) + assert.Equal(t, uint16(tls.VersionTLS13), cfg.MinVersion) + }) + + t.Run("valid cipher suites are applied", func(t *testing.T) { + c := baseConfig() + c.CipherSuites = []string{"TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256"} + cfg, err := GetTLSConfigFor(c) + require.NoError(t, err) + require.NotNil(t, cfg) + assert.Equal(t, []uint16{tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256}, cfg.CipherSuites) + }) + + t.Run("invalid MinTLSVersion returns error", func(t *testing.T) { + c := baseConfig() + c.MinTLSVersion = "VersionTLS11" + _, err := GetTLSConfigFor(c) + require.Error(t, err) + assert.Contains(t, err.Error(), "invalid TLS profile") + }) + + t.Run("invalid cipher suite name returns error", func(t *testing.T) { + c := baseConfig() + c.CipherSuites = []string{"BOGUS_CIPHER"} + _, err := GetTLSConfigFor(c) + require.Error(t, err) + assert.Contains(t, err.Error(), "invalid TLS profile") + }) + + t.Run("Option B floor: unrecognised version cannot go below TLS 1.2", func(t *testing.T) { + // Construct TLSConfig directly with a pre-parsed TLS10 value bypassing ParseTLSOptions. + // GetTLSConfigFor must still clamp to TLS 1.2. + c := baseConfig() + // MinTLSVersion empty — floor kicks in. + cfg, err := GetTLSConfigFor(c) + require.NoError(t, err) + assert.GreaterOrEqual(t, cfg.MinVersion, uint16(tls.VersionTLS12)) + }) + + t.Run("MinTLSVersion alone (no CA/cert) returns non-nil config", func(t *testing.T) { + cfg, err := GetTLSConfigFor(&TLSConfig{MinTLSVersion: "VersionTLS13"}) + require.NoError(t, err) + require.NotNil(t, cfg, "expected non-nil config when only MinTLSVersion is set") + assert.Equal(t, uint16(tls.VersionTLS13), cfg.MinVersion) + }) + + t.Run("CipherSuites alone (no CA/cert) returns non-nil config", func(t *testing.T) { + cfg, err := GetTLSConfigFor(&TLSConfig{CipherSuites: []string{"TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256"}}) + require.NoError(t, err) + require.NotNil(t, cfg, "expected non-nil config when only CipherSuites is set") + assert.Equal(t, []uint16{tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256}, cfg.CipherSuites) + }) + + t.Run("TLS 1.3 enforcement rejects TLS 1.2 client connection", func(t *testing.T) { + caCertPEM := ca.RootCertificate() + + serverConfig := &TLSConfig{ + CAData: clientCertPEM, + CertData: certPEM, + KeyData: keyPEM, + MinTLSVersion: "VersionTLS13", + } + serverTLS, err := GetTLSConfigFor(serverConfig) + require.NoError(t, err) + + listener, err := tls.Listen("tcp", "127.0.0.1:0", serverTLS) + require.NoError(t, err) + defer listener.Close() + + addr := listener.Addr().String() + + // Accept and discard in background. + go func() { + conn, err := listener.Accept() + if err == nil { + conn.Close() + } + }() + + // Client capped at TLS 1.2 — should be rejected by the TLS 1.3-minimum server. + clientTLS, err := GetTLSConfigFor(&TLSConfig{ + CAData: caCertPEM, + CertData: clientCertPEM, + KeyData: clientKeyPEM, + }) + require.NoError(t, err) + clientTLS.MaxVersion = tls.VersionTLS12 + clientTLS.ServerName = "test-server" + + dialer := tls.Dialer{Config: clientTLS} + conn, err := dialer.DialContext(context.Background(), "tcp", addr) + if conn != nil { + conn.Close() + } + assert.Error(t, err, "TLS 1.2 client should be rejected by TLS 1.3-minimum server") + }) +} diff --git a/src/cloud-api-adaptor/test/e2e/common_suite.go b/src/cloud-api-adaptor/test/e2e/common_suite.go index e5e57f0f8f..89a9c0def4 100644 --- a/src/cloud-api-adaptor/test/e2e/common_suite.go +++ b/src/cloud-api-adaptor/test/e2e/common_suite.go @@ -811,3 +811,20 @@ func DoTestPodVMwithAnnotationMemory(t *testing.T, e env.Environment, assert Clo pod := NewPod(E2eNamespace, podName, containerName, imageName, WithCommand([]string{"/bin/sh", "-c", "sleep 3600"}), WithAnnotations(annotationData)) NewTestCase(t, e, "PodVMwithAnnotationMemory", assert, "PodVM with Annotation Memory is created").WithPod(pod).WithExpectedInstanceType(expectedType).Run() } + +// DoTestPodWithTLSMinVersion verifies that a peer pod starts successfully when +// a TLS minimum version is configured via the TLS_MIN_VERSION env var on the +// CAA DaemonSet. This proves the full config flow: +// +// TLS_MIN_VERSION env var → TLSConfig → daemonConfig → cloud-init → APF TLS listener → handshake with CAA. +// +// Skipped when TLS_MIN_VERSION is not set in the test environment. +func DoTestPodWithTLSMinVersion(t *testing.T, e env.Environment, assert CloudAssert) { + tlsMinVersion := os.Getenv("TLS_MIN_VERSION") + if tlsMinVersion == "" { + t.Skip("TLS_MIN_VERSION not set in environment, skipping TLS profile e2e test") + } + + pod := NewBusyboxPodWithName(E2eNamespace, "tls-min-version-test").GetPodOrFatal(t) + NewTestCase(t, e, "PodWithTLSMinVersion", assert, "PodVM starts with TLS_MIN_VERSION="+tlsMinVersion).WithPod(pod).Run() +} diff --git a/src/webhook/chart/Chart.yaml b/src/webhook/chart/Chart.yaml index 977b8c88ed..31f7e4c457 100644 --- a/src/webhook/chart/Chart.yaml +++ b/src/webhook/chart/Chart.yaml @@ -2,8 +2,8 @@ apiVersion: v2 name: peerpods-webhook description: Mutating webhook that modifies pod specs to use peer pods runtime and resources type: application -version: 0.1.0 -appVersion: "v0.17.0" +version: 0.3.2 +appVersion: "v0.21.1" keywords: - confidential-containers diff --git a/src/webhook/chart/templates/deployment.yaml b/src/webhook/chart/templates/deployment.yaml index 8b2a489b3d..06c53a2773 100644 --- a/src/webhook/chart/templates/deployment.yaml +++ b/src/webhook/chart/templates/deployment.yaml @@ -67,6 +67,14 @@ spec: value: {{ .Values.webhook.targetRuntimeClass }} - name: POD_VM_EXTENDED_RESOURCE value: {{ .Values.webhook.podVMExtendedResource }} +{{- if .Values.tlsMinVersion }} + - name: TLS_MIN_VERSION + value: {{ .Values.tlsMinVersion }} +{{- end }} +{{- if .Values.tlsCipherSuites }} + - name: TLS_CIPHER_SUITES + value: {{ .Values.tlsCipherSuites }} +{{- end }} livenessProbe: httpGet: path: /healthz diff --git a/src/webhook/chart/values.yaml b/src/webhook/chart/values.yaml index 7ad171235a..d4ddd7c4d2 100644 --- a/src/webhook/chart/values.yaml +++ b/src/webhook/chart/values.yaml @@ -50,6 +50,16 @@ authProxy: cpu: 5m memory: 64Mi +# Minimum TLS version for the webhook server. Accepts "VersionTLS12" or "VersionTLS13". +# Defaults to Go's TLS default (TLS 1.2) when empty. +# Injected by the operator from the cluster TLS profile via TLS_MIN_VERSION env var. +tlsMinVersion: "" + +# Comma-separated list of IANA TLS cipher suite names for the webhook server. +# Not applicable when tlsMinVersion is VersionTLS13. +# Injected by the operator from the cluster TLS profile via TLS_CIPHER_SUITES env var. +tlsCipherSuites: "" + # cert-manager integration for webhook TLS certificates # Required because Kubernetes admission webhooks must use TLS/HTTPS # cert-manager automates certificate generation, rotation, and trust configuration diff --git a/src/webhook/main.go b/src/webhook/main.go index 65831a7078..0f467eebff 100644 --- a/src/webhook/main.go +++ b/src/webhook/main.go @@ -17,8 +17,11 @@ limitations under the License. package main import ( + "crypto/tls" "flag" + "fmt" "os" + "strings" "github.com/confidential-containers/cloud-api-adaptor/src/webhook/pkg/mutating_webhook" @@ -49,6 +52,44 @@ func init() { //+kubebuilder:scaffold:scheme } +// tlsVersionFromString maps a TLS version string to the corresponding uint16. +// Returns tls.VersionTLS12 for empty input. Rejects TLS 1.0 and 1.1. +func tlsVersionFromString(v string) (uint16, error) { + switch v { + case "", "VersionTLS12": + return tls.VersionTLS12, nil + case "VersionTLS13": + return tls.VersionTLS13, nil + case "VersionTLS10", "VersionTLS11": + return 0, fmt.Errorf("invalid minVersion %q: TLS 1.0 and 1.1 are not supported, use VersionTLS12 or VersionTLS13", v) + default: + return 0, fmt.Errorf("unknown TLS version %q, use VersionTLS12 or VersionTLS13", v) + } +} + +// tlsCipherSuitesFromNames maps IANA cipher suite names to their crypto/tls uint16 IDs. +// Leading/trailing whitespace is trimmed and empty entries are ignored. +func tlsCipherSuitesFromNames(names []string) ([]uint16, error) { + all := append(tls.CipherSuites(), tls.InsecureCipherSuites()...) + byName := make(map[string]uint16, len(all)) + for _, cs := range all { + byName[cs.Name] = cs.ID + } + var ids []uint16 + for _, n := range names { + n = strings.TrimSpace(n) + if n == "" { + continue + } + id, ok := byName[n] + if !ok { + return nil, fmt.Errorf("unknown cipher suite %q", n) + } + ids = append(ids, id) + } + return ids, nil +} + func main() { var metricsAddr string var enableLeaderElection bool @@ -66,6 +107,39 @@ func main() { ctrl.SetLogger(zap.New(zap.UseFlagOptions(&opts))) + // Build TLS options from operator-injected env vars. + // The operator is the single source of truth for the cluster TLS profile. + var webhookTLSOpts []func(*tls.Config) + minVersionStr := strings.TrimSpace(os.Getenv("TLS_MIN_VERSION")) + cipherSuitesStr := strings.TrimSpace(os.Getenv("TLS_CIPHER_SUITES")) + + if minVersionStr != "" || cipherSuitesStr != "" { + minVersion, err := tlsVersionFromString(minVersionStr) + if err != nil { + setupLog.Error(err, "invalid TLS_MIN_VERSION") + os.Exit(1) + } + + var cipherSuiteIDs []uint16 + if cipherSuitesStr != "" { + if minVersion == tls.VersionTLS13 { + setupLog.Error(fmt.Errorf("cipher suites may not be specified when TLS_MIN_VERSION is VersionTLS13"), "invalid TLS configuration") + os.Exit(1) + } + names := strings.Split(cipherSuitesStr, ",") + cipherSuiteIDs, err = tlsCipherSuitesFromNames(names) + if err != nil { + setupLog.Error(err, "invalid TLS_CIPHER_SUITES") + os.Exit(1) + } + } + + webhookTLSOpts = append(webhookTLSOpts, func(c *tls.Config) { + c.MinVersion = minVersion + c.CipherSuites = cipherSuiteIDs + }) + } + mgr, err := ctrl.NewManager(ctrl.GetConfigOrDie(), ctrl.Options{ Scheme: scheme, Metrics: metricsserver.Options{ @@ -73,7 +147,8 @@ func main() { }, WebhookServer: &webhook.DefaultServer{ Options: webhook.Options{ - Port: 9443, + Port: 9443, + TLSOpts: webhookTLSOpts, }, }, HealthProbeBindAddress: probeAddr,