|
1 | 1 | package apiserver |
2 | 2 |
|
3 | 3 | import ( |
| 4 | + "context" |
4 | 5 | "crypto/tls" |
| 6 | + "fmt" |
| 7 | + "io" |
| 8 | + "math/rand" |
| 9 | + "os/exec" |
| 10 | + "strings" |
| 11 | + "time" |
5 | 12 |
|
6 | 13 | g "github.com/onsi/ginkgo/v2" |
| 14 | + o "github.com/onsi/gomega" |
| 15 | + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" |
| 16 | + e2e "k8s.io/kubernetes/test/e2e/framework" |
7 | 17 |
|
| 18 | + configv1 "github.com/openshift/api/config/v1" |
8 | 19 | "github.com/openshift/library-go/pkg/crypto" |
9 | 20 | exutil "github.com/openshift/origin/test/extended/util" |
10 | 21 | ) |
11 | 22 |
|
| 23 | +const ( |
| 24 | + namespace = "apiserver-tls-test" |
| 25 | +) |
| 26 | + |
| 27 | +// This test only checks whether components are serving the proper TLS version based |
| 28 | +// on the expected version set in the TLS profile config. It is a part of the |
| 29 | +// openshift/conformance/parallel test suite, and it is expected that there are jobs |
| 30 | +// which run that entire conformance suite against clusters running any TLS profiles |
| 31 | +// that there is a desire to test. |
12 | 32 | var _ = g.Describe("[sig-api-machinery][Feature:APIServer]", func() { |
13 | 33 | defer g.GinkgoRecover() |
14 | 34 |
|
15 | | - oc := exutil.NewCLI("apiserver") |
| 35 | + var oc = exutil.NewCLI(namespace) |
| 36 | + var ctx = context.Background() |
16 | 37 |
|
17 | | - g.It("TestTLSDefaults", func() { |
18 | | - g.Skip("skipping because it was broken in master") |
| 38 | + g.BeforeEach(func() { |
| 39 | + isMicroShift, err := exutil.IsMicroShiftCluster(oc.AdminKubeClient()) |
| 40 | + o.Expect(err).NotTo(o.HaveOccurred()) |
19 | 41 |
|
20 | | - t := g.GinkgoT() |
21 | | - // Verify we fail with TLS versions less than the default, and work with TLS versions >= the default |
22 | | - for _, tlsVersionName := range crypto.ValidTLSVersions() { |
23 | | - tlsVersion := crypto.TLSVersionOrDie(tlsVersionName) |
24 | | - expectSuccess := tlsVersion >= crypto.DefaultTLSVersion() |
25 | | - config := &tls.Config{MinVersion: tlsVersion, MaxVersion: tlsVersion, InsecureSkipVerify: true} |
26 | | - |
27 | | - { |
28 | | - conn, err := tls.Dial("tcp4", oc.AdminConfig().Host, config) |
29 | | - if err == nil { |
30 | | - conn.Close() |
| 42 | + isHyperShift, err := exutil.IsHypershift(ctx, oc.AdminConfigClient()) |
| 43 | + o.Expect(err).NotTo(o.HaveOccurred()) |
| 44 | + |
| 45 | + if isMicroShift || isHyperShift { |
| 46 | + g.Skip("TLS configuration for the apiserver resource is not applicable to MicroShift or HyperShift clusters - skipping") |
| 47 | + } |
| 48 | + }) |
| 49 | + |
| 50 | + g.It("TestTLSMinimumVersions", func() { |
| 51 | + |
| 52 | + g.By("Getting the APIServer configuration") |
| 53 | + config, err := oc.AdminConfigClient().ConfigV1().APIServers().Get(ctx, "cluster", metav1.GetOptions{}) |
| 54 | + o.Expect(err).NotTo(o.HaveOccurred()) |
| 55 | + |
| 56 | + g.By("Determining expected TLS behavior based on the cluster's TLS profile") |
| 57 | + var tlsShouldWork, tlsShouldNotWork *tls.Config |
| 58 | + switch { |
| 59 | + case config.Spec.TLSSecurityProfile == nil, |
| 60 | + config.Spec.TLSSecurityProfile.Type == configv1.TLSProfileIntermediateType: |
| 61 | + tlsShouldWork = &tls.Config{MinVersion: tls.VersionTLS12, MaxVersion: tls.VersionTLS13, InsecureSkipVerify: true} |
| 62 | + tlsShouldNotWork = &tls.Config{MinVersion: tls.VersionTLS11, MaxVersion: tls.VersionTLS11, InsecureSkipVerify: true} |
| 63 | + g.By("Using intermediate TLS profile: connections with TLS ≥1.2 should work, <1.2 should fail") |
| 64 | + case config.Spec.TLSSecurityProfile.Type == configv1.TLSProfileModernType: |
| 65 | + tlsShouldWork = &tls.Config{MinVersion: tls.VersionTLS13, MaxVersion: tls.VersionTLS13, InsecureSkipVerify: true} |
| 66 | + tlsShouldNotWork = &tls.Config{MinVersion: tls.VersionTLS12, MaxVersion: tls.VersionTLS12, InsecureSkipVerify: true} |
| 67 | + g.By("Using modern TLS profile: only TLS 1.3 connections should succeed") |
| 68 | + default: |
| 69 | + g.Skip("Only intermediate or modern profiles are tested") |
| 70 | + } |
| 71 | + |
| 72 | + targets := []struct { |
| 73 | + name, namespace, port string |
| 74 | + }{ |
| 75 | + {"apiserver", "openshift-kube-apiserver", "443"}, |
| 76 | + {"oauth-openshift", "openshift-authentication", "443"}, |
| 77 | + {"kube-controller-manager", "openshift-kube-controller-manager", "443"}, |
| 78 | + {"scheduler", "openshift-kube-scheduler", "443"}, |
| 79 | + {"api", "openshift-apiserver", "443"}, |
| 80 | + {"api", "openshift-oauth-apiserver", "443"}, |
| 81 | + {"machine-config-controller", "openshift-machine-config-operator", "9001"}, |
| 82 | + } |
| 83 | + |
| 84 | + g.By("Verifying TLS behavior for core control plane components") |
| 85 | + for _, target := range targets { |
| 86 | + g.By(fmt.Sprintf("Checking %s/%s on port %s", target.namespace, target.name, target.port)) |
| 87 | + err = forwardPortAndExecute(target.name, target.namespace, target.port, |
| 88 | + func(port int) error { return checkTLSConnection(port, tlsShouldWork, tlsShouldNotWork) }) |
| 89 | + o.Expect(err).NotTo(o.HaveOccurred()) |
| 90 | + } |
| 91 | + |
| 92 | + g.By("Checking etcd's TLS behavior") |
| 93 | + err = forwardPortAndExecute("etcd", "openshift-etcd", "2379", func(port int) error { |
| 94 | + conn, err := tls.Dial("tcp", fmt.Sprintf("localhost:%d", port), tlsShouldWork) |
| 95 | + if err != nil { |
| 96 | + if !strings.Contains(err.Error(), "remote error: tls: bad certificate") { |
| 97 | + return fmt.Errorf("should work: %w", err) |
31 | 98 | } |
32 | | - if success := err == nil; success != expectSuccess { |
33 | | - t.Errorf("Expected success %v, got %v with TLS version %s dialing master", expectSuccess, success, tlsVersionName) |
| 99 | + } else { |
| 100 | + err = conn.Close() |
| 101 | + if err != nil { |
| 102 | + return fmt.Errorf("failed to close connection: %w", err) |
34 | 103 | } |
35 | 104 | } |
36 | | - } |
| 105 | + conn, err = tls.Dial("tcp", fmt.Sprintf("localhost:%d", port), tlsShouldNotWork) |
| 106 | + if err == nil { |
| 107 | + return fmt.Errorf("should not work: connection unexpectedly succeeded, closing conn status: %v", conn.Close()) |
| 108 | + } |
| 109 | + return nil |
| 110 | + }) |
| 111 | + o.Expect(err).NotTo(o.HaveOccurred()) |
| 112 | + }) |
| 113 | + |
| 114 | + g.It("TestTLSDefaults", func() { |
| 115 | + t := g.GinkgoT() |
| 116 | + |
| 117 | + _, err := e2e.LoadClientset(true) |
| 118 | + o.Expect(err).NotTo(o.HaveOccurred()) |
37 | 119 |
|
38 | | - // Verify the only ciphers we work with are in the default set. |
39 | | - // Not all default ciphers will succeed because they depend on the serving cert type. |
40 | | - defaultCiphers := map[uint16]bool{} |
41 | | - for _, defaultCipher := range crypto.DefaultCiphers() { |
42 | | - defaultCiphers[defaultCipher] = true |
| 120 | + g.By("Getting the APIServer config") |
| 121 | + config, err := oc.AdminConfigClient().ConfigV1().APIServers().Get(ctx, "cluster", metav1.GetOptions{}) |
| 122 | + o.Expect(err).NotTo(o.HaveOccurred()) |
| 123 | + |
| 124 | + if config.Spec.TLSSecurityProfile != nil && |
| 125 | + config.Spec.TLSSecurityProfile.Type != configv1.TLSProfileIntermediateType { |
| 126 | + g.Skip("Cluster TLS profile is not default (intermediate), skipping cipher defaults check") |
43 | 127 | } |
44 | | - for _, cipherName := range crypto.ValidCipherSuites() { |
45 | | - cipher, err := crypto.CipherSuite(cipherName) |
46 | | - if err != nil { |
47 | | - t.Fatal(err) |
| 128 | + |
| 129 | + g.By("Verifying TLS version and cipher behavior via port-forward to apiserver") |
| 130 | + err = forwardPortAndExecute("apiserver", "openshift-kube-apiserver", "443", func(port int) error { |
| 131 | + host := fmt.Sprintf("localhost:%d", port) |
| 132 | + t.Logf("Testing TLS versions and ciphers against %s", host) |
| 133 | + |
| 134 | + // Test TLS versions |
| 135 | + for _, tlsVersionName := range crypto.ValidTLSVersions() { |
| 136 | + tlsVersion := crypto.TLSVersionOrDie(tlsVersionName) |
| 137 | + expectSuccess := tlsVersion >= crypto.DefaultTLSVersion() |
| 138 | + cfg := &tls.Config{MinVersion: tlsVersion, MaxVersion: tlsVersion, InsecureSkipVerify: true} |
| 139 | + |
| 140 | + t.Logf("Testing TLS version %s (0x%04x), expectSuccess=%v", tlsVersionName, tlsVersion, expectSuccess) |
| 141 | + conn, dialErr := tls.Dial("tcp", host, cfg) |
| 142 | + if dialErr == nil { |
| 143 | + t.Logf("TLS %s succeeded, negotiated version: 0x%04x", tlsVersionName, conn.ConnectionState().Version) |
| 144 | + closeErr := conn.Close() |
| 145 | + if closeErr != nil { |
| 146 | + return fmt.Errorf("failed to close connection: %v", closeErr) |
| 147 | + } |
| 148 | + } else { |
| 149 | + t.Logf("TLS %s failed with error: %v", tlsVersionName, dialErr) |
| 150 | + } |
| 151 | + if success := dialErr == nil; success != expectSuccess { |
| 152 | + return fmt.Errorf("expected success %v, got %v with TLS version %s", expectSuccess, success, tlsVersionName) |
| 153 | + } |
| 154 | + } |
| 155 | + |
| 156 | + // Test cipher suites |
| 157 | + defaultCiphers := map[uint16]bool{} |
| 158 | + for _, c := range crypto.DefaultCiphers() { |
| 159 | + defaultCiphers[c] = true |
48 | 160 | } |
49 | | - expectFailure := !defaultCiphers[cipher] |
50 | | - config := &tls.Config{CipherSuites: []uint16{cipher}, InsecureSkipVerify: true} |
51 | 161 |
|
52 | | - { |
53 | | - conn, err := tls.Dial("tcp4", oc.AdminConfig().Host, config) |
54 | | - if err == nil { |
55 | | - conn.Close() |
| 162 | + for _, cipherName := range crypto.ValidCipherSuites() { |
| 163 | + cipher, err := crypto.CipherSuite(cipherName) |
| 164 | + if err != nil { |
| 165 | + return err |
| 166 | + } |
| 167 | + expectFailure := !defaultCiphers[cipher] |
| 168 | + // Constrain to TLS 1.2 because the intermediate profile allows both TLS 1.2 and TLS 1.3. |
| 169 | + // If MaxVersion is unspecified, the client negotiates TLS 1.3 when the server supports it. |
| 170 | + // TLS 1.3 does not support configuring cipher suites (predetermined by the spec), so |
| 171 | + // specifying any cipher suite (RC4 or otherwise) has no effect with TLS 1.3. |
| 172 | + // By forcing TLS 1.2, we can actually test the cipher suite restrictions. |
| 173 | + cfg := &tls.Config{ |
| 174 | + CipherSuites: []uint16{cipher}, |
| 175 | + MinVersion: tls.VersionTLS12, |
| 176 | + MaxVersion: tls.VersionTLS12, |
| 177 | + InsecureSkipVerify: true, |
| 178 | + } |
| 179 | + |
| 180 | + conn, dialErr := tls.Dial("tcp", host, cfg) |
| 181 | + if dialErr == nil { |
| 182 | + closeErr := conn.Close() |
56 | 183 | if expectFailure { |
57 | | - t.Errorf("Expected failure on cipher %s, got success dialing master", cipherName) |
| 184 | + return fmt.Errorf("expected failure on cipher %s, got success. Closing conn: %v", cipherName, closeErr) |
58 | 185 | } |
59 | 186 | } |
60 | 187 | } |
61 | | - } |
62 | 188 |
|
| 189 | + return nil |
| 190 | + }) |
| 191 | + o.Expect(err).NotTo(o.HaveOccurred()) |
63 | 192 | }) |
64 | 193 | }) |
| 194 | + |
| 195 | +func forwardPortAndExecute(serviceName, namespace, remotePort string, toExecute func(localPort int) error) error { |
| 196 | + var err error |
| 197 | + for i := 0; i < 3; i++ { |
| 198 | + if err = func() error { |
| 199 | + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) |
| 200 | + defer cancel() |
| 201 | + localPort := rand.Intn(65534-1025) + 1025 |
| 202 | + args := []string{"port-forward", fmt.Sprintf("svc/%s", serviceName), fmt.Sprintf("%d:%s", localPort, remotePort), "-n", namespace} |
| 203 | + |
| 204 | + cmd := exec.CommandContext(ctx, "oc", args...) |
| 205 | + stdout, stderr, err := e2e.StartCmdAndStreamOutput(cmd) |
| 206 | + if err != nil { |
| 207 | + return err |
| 208 | + } |
| 209 | + defer stdout.Close() |
| 210 | + defer stderr.Close() |
| 211 | + defer e2e.TryKill(cmd) |
| 212 | + |
| 213 | + e2e.Logf("oc port-forward output: %s", readPartialFrom(stdout, 1024)) |
| 214 | + return toExecute(localPort) |
| 215 | + }(); err == nil { |
| 216 | + return nil |
| 217 | + } else { |
| 218 | + e2e.Logf("failed to start oc port-forward command or test: %v", err) |
| 219 | + time.Sleep(2 * time.Second) |
| 220 | + } |
| 221 | + } |
| 222 | + return err |
| 223 | +} |
| 224 | + |
| 225 | +func readPartialFrom(r io.Reader, maxBytes int) string { |
| 226 | + buf := make([]byte, maxBytes) |
| 227 | + n, err := r.Read(buf) |
| 228 | + if err != nil && err != io.EOF { |
| 229 | + return fmt.Sprintf("error reading: %v", err) |
| 230 | + } |
| 231 | + return string(buf[:n]) |
| 232 | +} |
| 233 | + |
| 234 | +func checkTLSConnection(port int, tlsShouldWork, tlsShouldNotWork *tls.Config) error { |
| 235 | + conn, err := tls.Dial("tcp", fmt.Sprintf("localhost:%d", port), tlsShouldWork) |
| 236 | + if err != nil { |
| 237 | + return fmt.Errorf("should work: %w", err) |
| 238 | + } |
| 239 | + err = conn.Close() |
| 240 | + if err != nil { |
| 241 | + return fmt.Errorf("failed to close connection: %w", err) |
| 242 | + } |
| 243 | + |
| 244 | + conn, err = tls.Dial("tcp", fmt.Sprintf("localhost:%d", port), tlsShouldNotWork) |
| 245 | + if err == nil { |
| 246 | + return fmt.Errorf("should not work: connection unexpectedly succeeded, closing conn status: %v", conn.Close()) |
| 247 | + } |
| 248 | + if !strings.Contains(err.Error(), "protocol version") && |
| 249 | + !strings.Contains(err.Error(), "no supported versions satisfy") && |
| 250 | + !strings.Contains(err.Error(), "handshake failure") { |
| 251 | + return fmt.Errorf("should not work: got error, but not a TLS version mismatch: %w", err) |
| 252 | + } |
| 253 | + return nil |
| 254 | +} |
0 commit comments