k8s.io/kubernetes@v1.31.0-alpha.0.0.20240520171757-56147500dadc/test/integration/apiserver/podlogs/podlogs_test.go (about) 1 /* 2 Copyright 2018 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 podlogs 18 19 import ( 20 "context" 21 "crypto/ecdsa" 22 "crypto/elliptic" 23 "crypto/rand" 24 "crypto/tls" 25 "crypto/x509" 26 "crypto/x509/pkix" 27 "encoding/pem" 28 "fmt" 29 "math" 30 "math/big" 31 "net" 32 "net/http" 33 "net/http/httptest" 34 "net/url" 35 "os" 36 "strconv" 37 "strings" 38 "testing" 39 "time" 40 41 corev1 "k8s.io/api/core/v1" 42 "k8s.io/apimachinery/pkg/api/errors" 43 metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" 44 "k8s.io/apimachinery/pkg/util/wait" 45 "k8s.io/apiserver/pkg/authentication/authenticatorfactory" 46 "k8s.io/apiserver/pkg/endpoints/filters" 47 "k8s.io/apiserver/pkg/server/dynamiccertificates" 48 "k8s.io/client-go/kubernetes" 49 "k8s.io/client-go/transport" 50 certutil "k8s.io/client-go/util/cert" 51 "k8s.io/client-go/util/keyutil" 52 "k8s.io/kubernetes/cmd/kube-apiserver/app/options" 53 "k8s.io/kubernetes/test/integration/framework" 54 "k8s.io/kubernetes/test/utils/ktesting" 55 ) 56 57 func TestInsecurePodLogs(t *testing.T) { 58 badCA := writeDataToTempFile(t, []byte(` 59 -----BEGIN CERTIFICATE----- 60 MIIDMDCCAhigAwIBAgIIHNPD7sig7YIwDQYJKoZIhvcNAQELBQAwNjESMBAGA1UE 61 CxMJb3BlbnNoaWZ0MSAwHgYDVQQDExdhZG1pbi1rdWJlY29uZmlnLXNpZ25lcjAe 62 Fw0xOTA1MzAxNTA3MzlaFw0yOTA1MjcxNTA3MzlaMDYxEjAQBgNVBAsTCW9wZW5z 63 aGlmdDEgMB4GA1UEAxMXYWRtaW4ta3ViZWNvbmZpZy1zaWduZXIwggEiMA0GCSqG 64 SIb3DQEBAQUAA4IBDwAwggEKAoIBAQD0dHk23lHRcuq06FzYDOl9J9+s8pnGxqA3 65 IPcARI6ag/98aYe3ENwAB5e1i7AU2F2WiDZgj444w374XLdVgIK8zgQEm9yoqrlc 66 +/ayO7ceKklrKHOMwh63LvGLEOqzhol2nFmBhXAZt+HyIoZHXN0IqlA92196+Dml 67 0WOn1F4ce6JbAtEceFHPgLeI7KFmVaPz2796pBXh23ii6r7WvV1Rn9MKlMSBJQR4 68 0LZzu9/j+GdnFXewdLAAMfgPzwEqv6h3PzvtUCjgdraHEm8Rs7s15S3PUmLK4RQS 69 PsThx5BhJEGd/W6EzQ3BKoQfochhu3mnAQtW1J07CullySQ5Gg9fAgMBAAGjQjBA 70 MA4GA1UdDwEB/wQEAwICpDAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBQkTaaw 71 YJSZ5k2Wd+OsM4GFMTGdqzANBgkqhkiG9w0BAQsFAAOCAQEAHK7+zBZPLqK+f9DT 72 UEnpwRmZ0aeGS4YgbGIkqpjxJymVOwkRd5A1wslvVfGZ6yOQthF6KlCmqnPyJJMR 73 I7FHw8j0h2ci90fEQ6IS90Y/ZJXkcgiK9Ncwa35GFGs8QrBxN4leGhtm84BnnBHN 74 cTWpa4zcBwru0CRG7iHc66VX16X8jHB1iFeZ5W/FgY4MsE+G1Vze4mCXSPVI4BZ2 75 /qlAgogjBivvSwQ9SFuCszg7IPjvT2ksm+Cf+8eT4YBqW41F85vBGR+FYK14yIla 76 Bgqc+dJN9xS9Ah5gLiGQJ6C4niUA11piCpvMsy+j/LQ1Erx47KMar5fuMXYk7iPq 77 1vqIwg== 78 -----END CERTIFICATE----- 79 `)) 80 81 tCtx := ktesting.Init(t) 82 clientSet, _, tearDownFn := framework.StartTestServer(tCtx, t, framework.TestServerSetup{ 83 ModifyServerRunOptions: func(opts *options.ServerRunOptions) { 84 opts.GenericServerRunOptions.MaxRequestBodyBytes = 1024 * 1024 85 // I have no idea what this cert is, but it doesn't matter, we just want something that always fails validation 86 opts.KubeletConfig.TLSClientConfig.CAFile = badCA 87 }, 88 }) 89 defer tearDownFn() 90 91 fakeKubeletServer := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { 92 w.Write([]byte("fake-log")) 93 w.WriteHeader(http.StatusOK) 94 })) 95 defer fakeKubeletServer.Close() 96 97 pod := prepareFakeNodeAndPod(tCtx, t, clientSet, fakeKubeletServer) 98 99 insecureResult := clientSet.CoreV1().Pods("ns").GetLogs(pod.Name, &corev1.PodLogOptions{InsecureSkipTLSVerifyBackend: true}).Do(context.TODO()) 100 if err := insecureResult.Error(); err != nil { 101 t.Fatal(err) 102 } 103 insecureStatusCode := 0 104 insecureResult.StatusCode(&insecureStatusCode) 105 if insecureStatusCode != http.StatusOK { 106 t.Fatal(insecureStatusCode) 107 } 108 109 secureResult := clientSet.CoreV1().Pods("ns").GetLogs(pod.Name, &corev1.PodLogOptions{}).Do(tCtx) 110 if err := secureResult.Error(); err == nil || !strings.Contains(err.Error(), "x509: certificate signed by unknown authority") { 111 t.Fatal(err) 112 } 113 secureStatusCode := 0 114 secureResult.StatusCode(&secureStatusCode) 115 if secureStatusCode == http.StatusOK { 116 raw, rawErr := secureResult.Raw() 117 if rawErr != nil { 118 t.Log(rawErr) 119 } 120 t.Log(string(raw)) 121 t.Fatal(secureStatusCode) 122 } 123 } 124 125 func prepareFakeNodeAndPod(ctx context.Context, t *testing.T, clientSet kubernetes.Interface, fakeKubeletServer *httptest.Server) *corev1.Pod { 126 t.Helper() 127 128 fakeKubeletURL, err := url.Parse(fakeKubeletServer.URL) 129 if err != nil { 130 t.Fatal(err) 131 } 132 fakeKubeletHost, fakeKubeletPortStr, err := net.SplitHostPort(fakeKubeletURL.Host) 133 if err != nil { 134 t.Fatal(err) 135 } 136 fakeKubeletPort, err := strconv.ParseUint(fakeKubeletPortStr, 10, 32) 137 if err != nil { 138 t.Fatal(err) 139 } 140 141 node, err := clientSet.CoreV1().Nodes().Create(ctx, &corev1.Node{ 142 ObjectMeta: metav1.ObjectMeta{Name: "fake"}, 143 }, metav1.CreateOptions{}) 144 if err != nil { 145 t.Fatal(err) 146 } 147 node.Status = corev1.NodeStatus{ 148 Addresses: []corev1.NodeAddress{ 149 { 150 Type: corev1.NodeExternalIP, 151 Address: fakeKubeletHost, 152 }, 153 }, 154 DaemonEndpoints: corev1.NodeDaemonEndpoints{ 155 KubeletEndpoint: corev1.DaemonEndpoint{ 156 Port: int32(fakeKubeletPort), 157 }, 158 }, 159 } 160 node, err = clientSet.CoreV1().Nodes().UpdateStatus(ctx, node, metav1.UpdateOptions{}) 161 if err != nil { 162 t.Fatal(err) 163 } 164 165 _, err = clientSet.CoreV1().Namespaces().Create(ctx, &corev1.Namespace{ 166 ObjectMeta: metav1.ObjectMeta{Name: "ns"}, 167 }, metav1.CreateOptions{}) 168 if err != nil { 169 t.Fatal(err) 170 } 171 172 _, err = clientSet.CoreV1().ServiceAccounts("ns").Create(ctx, &corev1.ServiceAccount{ 173 ObjectMeta: metav1.ObjectMeta{Name: "default", Namespace: "ns"}, 174 }, metav1.CreateOptions{}) 175 if err != nil { 176 t.Fatal(err) 177 } 178 179 falseRef := false 180 pod, err := clientSet.CoreV1().Pods("ns").Create(ctx, &corev1.Pod{ 181 ObjectMeta: metav1.ObjectMeta{Name: "test-pod", Namespace: "ns"}, 182 Spec: corev1.PodSpec{ 183 Containers: []corev1.Container{ 184 { 185 Name: "foo", 186 Image: "some/image:latest", 187 }, 188 }, 189 NodeName: node.Name, 190 AutomountServiceAccountToken: &falseRef, 191 }, 192 }, metav1.CreateOptions{}) 193 if err != nil { 194 t.Fatal(err) 195 } 196 197 return pod 198 } 199 200 func TestPodLogsKubeletClientCertReload(t *testing.T) { 201 ctx, cancel := context.WithTimeout(context.Background(), 3*time.Minute) 202 t.Cleanup(cancel) 203 204 origCertCallbackRefreshDuration := transport.CertCallbackRefreshDuration 205 origDialerStopCh := transport.DialerStopCh 206 transport.CertCallbackRefreshDuration = time.Second // make client cert reloading fast 207 transport.DialerStopCh = ctx.Done() 208 t.Cleanup(func() { 209 transport.CertCallbackRefreshDuration = origCertCallbackRefreshDuration 210 transport.DialerStopCh = origDialerStopCh 211 }) 212 213 // create a CA to sign the API server's kubelet client cert 214 startingCerts := generateClientCert(t) 215 216 dynamicCAContentFromFile, err := dynamiccertificates.NewDynamicCAContentFromFile("client-ca-bundle", startingCerts.caFile) 217 if err != nil { 218 t.Fatal(err) 219 } 220 if err := dynamicCAContentFromFile.RunOnce(ctx); err != nil { 221 t.Fatal(err) 222 } 223 go dynamicCAContentFromFile.Run(ctx, 1) 224 authenticatorConfig := authenticatorfactory.DelegatingAuthenticatorConfig{ 225 ClientCertificateCAContentProvider: dynamicCAContentFromFile, 226 } 227 authenticator, _, err := authenticatorConfig.New() 228 if err != nil { 229 t.Fatal(err) 230 } 231 232 // this fake kubelet will perform per request authentication using the configured CA (which is dynamically reloaded) 233 fakeKubeletServer := httptest.NewUnstartedServer( 234 filters.WithAuthentication( 235 http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { 236 _, _ = w.Write([]byte("pod-logs-here")) 237 }), 238 authenticator, 239 http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { 240 w.WriteHeader(http.StatusUnauthorized) 241 }), 242 nil, 243 nil, 244 ), 245 ) 246 fakeKubeletServer.TLS = &tls.Config{ClientAuth: tls.RequestClientCert} 247 fakeKubeletServer.StartTLS() 248 t.Cleanup(fakeKubeletServer.Close) 249 250 kubeletCA := writeDataToTempFile(t, pem.EncodeToMemory(&pem.Block{ 251 Type: "CERTIFICATE", 252 Bytes: fakeKubeletServer.Certificate().Raw, 253 })) 254 255 clientSet, _, tearDownFn := framework.StartTestServer(ctx, t, framework.TestServerSetup{ 256 ModifyServerRunOptions: func(opts *options.ServerRunOptions) { 257 opts.GenericServerRunOptions.MaxRequestBodyBytes = 1024 * 1024 258 opts.KubeletConfig.TLSClientConfig.CAFile = kubeletCA 259 opts.KubeletConfig.TLSClientConfig.CertFile = startingCerts.clientCertFile 260 opts.KubeletConfig.TLSClientConfig.KeyFile = startingCerts.clientCertKeyFile 261 }, 262 }) 263 t.Cleanup(tearDownFn) 264 265 pod := prepareFakeNodeAndPod(ctx, t, clientSet, fakeKubeletServer) 266 267 // verify that the starting state works as expected 268 podLogs, err := clientSet.CoreV1().Pods("ns").GetLogs(pod.Name, &corev1.PodLogOptions{}).DoRaw(ctx) 269 if err != nil { 270 t.Fatal(err) 271 } 272 if l := string(podLogs); l != "pod-logs-here" { 273 t.Fatalf("unexpected pod logs: %s", l) 274 } 275 276 // generate a new CA and overwrite the existing CA that the kubelet is using for request authentication 277 newCerts := generateClientCert(t) 278 if err := os.Rename(newCerts.caFile, startingCerts.caFile); err != nil { 279 t.Fatal(err) 280 } 281 282 // wait until the kubelet observes the new CA 283 if err := wait.PollUntilContextCancel(ctx, time.Second, true, func(ctx context.Context) (bool, error) { 284 _, errLog := clientSet.CoreV1().Pods("ns").GetLogs(pod.Name, &corev1.PodLogOptions{}).DoRaw(ctx) 285 if errors.IsUnauthorized(errLog) { 286 return true, nil 287 } 288 return false, errLog 289 }); err != nil { 290 t.Fatal(err) 291 } 292 293 // now update the API server's kubelet client cert to use the new cert 294 if err := os.Rename(newCerts.clientCertFile, startingCerts.clientCertFile); err != nil { 295 t.Fatal(err) 296 } 297 if err := os.Rename(newCerts.clientCertKeyFile, startingCerts.clientCertKeyFile); err != nil { 298 t.Fatal(err) 299 } 300 301 // confirm that the API server observes the new client cert and closes existing connections to use it 302 if err := wait.PollUntilContextCancel(ctx, time.Second, true, func(ctx context.Context) (bool, error) { 303 fixedPodLogs, errLog := clientSet.CoreV1().Pods("ns").GetLogs(pod.Name, &corev1.PodLogOptions{}).DoRaw(ctx) 304 if errors.IsUnauthorized(errLog) { 305 t.Log("api server has not observed new client cert") 306 return false, nil 307 } 308 if errLog != nil { 309 return false, errLog 310 } 311 if l := string(fixedPodLogs); l != "pod-logs-here" { 312 return false, fmt.Errorf("unexpected pod logs: %s", l) 313 } 314 return true, nil 315 }); err != nil { 316 t.Fatal(err) 317 } 318 } 319 320 type testCerts struct { 321 caFile, clientCertFile, clientCertKeyFile string 322 } 323 324 func generateClientCert(t *testing.T) testCerts { 325 t.Helper() 326 327 caPrivateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) 328 if err != nil { 329 t.Fatal(err) 330 } 331 caCert, err := certutil.NewSelfSignedCACert(certutil.Config{CommonName: "test-ca"}, caPrivateKey) 332 if err != nil { 333 t.Fatal(err) 334 } 335 336 clientCertKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) 337 if err != nil { 338 t.Fatal(err) 339 } 340 341 clientCertKeyBytes, err := keyutil.MarshalPrivateKeyToPEM(clientCertKey) 342 if err != nil { 343 t.Fatal(err) 344 } 345 346 // returns a uniform random value in [0, max-1), then add 1 to serial to make it a uniform random value in [1, max). 347 serial, err := rand.Int(rand.Reader, new(big.Int).SetInt64(math.MaxInt64-1)) 348 if err != nil { 349 t.Fatal(err) 350 } 351 serial = new(big.Int).Add(serial, big.NewInt(1)) 352 certTmpl := x509.Certificate{ 353 Subject: pkix.Name{ 354 CommonName: "the-api-server-user", 355 }, 356 NotBefore: caCert.NotBefore, 357 SerialNumber: serial, 358 NotAfter: time.Now().Add(time.Hour).UTC(), 359 KeyUsage: x509.KeyUsageKeyEncipherment | x509.KeyUsageDigitalSignature, 360 ExtKeyUsage: []x509.ExtKeyUsage{x509.ExtKeyUsageClientAuth}, 361 } 362 clientCertDERBytes, err := x509.CreateCertificate(rand.Reader, &certTmpl, caCert, clientCertKey.Public(), caPrivateKey) 363 if err != nil { 364 t.Fatal(err) 365 } 366 367 clientCert, err := x509.ParseCertificate(clientCertDERBytes) 368 if err != nil { 369 t.Fatal(err) 370 } 371 372 return testCerts{ 373 caFile: writeDataToTempFile(t, pem.EncodeToMemory(&pem.Block{ 374 Type: "CERTIFICATE", 375 Bytes: caCert.Raw, 376 })), 377 clientCertFile: writeDataToTempFile(t, pem.EncodeToMemory(&pem.Block{ 378 Type: "CERTIFICATE", 379 Bytes: clientCert.Raw, 380 })), 381 clientCertKeyFile: writeDataToTempFile(t, clientCertKeyBytes), 382 } 383 } 384 385 func writeDataToTempFile(t *testing.T, data []byte) string { 386 t.Helper() 387 388 file, err := os.CreateTemp("", "pod-logs-test-") 389 if err != nil { 390 t.Fatal(err) 391 } 392 if _, err := file.Write(data); err != nil { 393 t.Fatal(err) 394 } 395 t.Cleanup(func() { 396 _ = os.Remove(file.Name()) 397 }) 398 return file.Name() 399 }