1import hashlib
2from base64 import b64decode
3from typing import Generator, List, Tuple, Union
4
5from abstract_tests import HTTP, AmbassadorTest, Node, ServiceType
6from kat.harness import EDGE_STACK, Query
7from tests.integration.manifests import namespace_manifest
8from tests.selfsigned import TLSCerts
9from tests.utils import create_crl_pem_b64
10
11bug_404_routes = (
12 True # Do we erroneously send 404 responses directly instead of redirect-to-tls first?
13)
14
15
16class TLSContextsTest(AmbassadorTest):
17 """
18 This test makes sure that TLS is not turned on when it's not intended to. For example, when an 'upstream'
19 TLS configuration is passed, the port is not supposed to switch to 443
20 """
21
22 def init(self):
23 self.target = HTTP()
24
25 if EDGE_STACK:
26 self.xfail = "Not yet supported in Edge Stack"
27
28 self.xfail = "FIXME: IHA"
29
30 def manifests(self) -> str:
31 return (
32 f"""
33---
34apiVersion: v1
35metadata:
36 name: test-tlscontexts-secret
37 labels:
38 kat-ambassador-id: tlscontextstest
39data:
40 tls.crt: {TLSCerts["master.datawire.io"].k8s_crt}
41kind: Secret
42type: Opaque
43"""
44 + super().manifests()
45 )
46
47 def config(self) -> Generator[Union[str, Tuple[Node, str]], None, None]:
48 yield self, self.format(
49 """
50---
51apiVersion: getambassador.io/v3alpha1
52kind: Module
53name: tls
54ambassador_id: [{self.ambassador_id}]
55config:
56 upstream:
57 enabled: True
58 secret: test-tlscontexts-secret
59"""
60 )
61
62 yield self, self.format(
63 """
64---
65apiVersion: getambassador.io/v3alpha1
66kind: Mapping
67name: {self.target.path.k8s}
68prefix: /{self.name}/
69service: {self.target.path.fqdn}
70"""
71 )
72
73 def scheme(self) -> str:
74 return "https"
75
76 def queries(self):
77 yield Query(
78 self.url(self.name + "/"),
79 error=["connection refused", "connection reset by peer", "EOF", "request canceled"],
80 )
81
82 def requirements(self):
83 yield from (
84 r for r in super().requirements() if r[0] == "url" and r[1].url.startswith("http://")
85 )
86
87
88class ClientCertificateAuthentication(AmbassadorTest):
89 def init(self):
90 self.xfail = "FIXME: IHA"
91 self.target = HTTP()
92
93 def manifests(self) -> str:
94 return (
95 f"""
96---
97apiVersion: v1
98metadata:
99 name: test-clientcert-client-secret
100 labels:
101 kat-ambassador-id: clientcertificateauthentication
102data:
103 tls.crt: {TLSCerts["master.datawire.io"].k8s_crt}
104kind: Secret
105type: Opaque
106---
107apiVersion: v1
108kind: Secret
109metadata:
110 name: test-clientcert-server-secret
111 labels:
112 kat-ambassador-id: clientcertificateauthentication
113type: kubernetes.io/tls
114data:
115 tls.crt: {TLSCerts["ambassador.example.com"].k8s_crt}
116 tls.key: {TLSCerts["ambassador.example.com"].k8s_key}
117"""
118 + super().manifests()
119 )
120
121 def config(self) -> Generator[Union[str, Tuple[Node, str]], None, None]:
122 yield self, self.format(
123 """
124---
125apiVersion: getambassador.io/v3alpha1
126kind: Module
127name: ambassador
128config:
129 forward_client_cert_details: SANITIZE_SET
130 set_current_client_cert_details:
131 subject: true
132---
133apiVersion: getambassador.io/v3alpha1
134kind: Module
135ambassador_id: [{self.ambassador_id}]
136name: tls
137config:
138 server:
139 enabled: True
140 secret: test-clientcert-server-secret
141 client:
142 enabled: True
143 secret: test-clientcert-client-secret
144 cert_required: True
145"""
146 )
147
148 yield self, self.format(
149 """
150---
151apiVersion: getambassador.io/v3alpha1
152kind: Mapping
153name: {self.target.path.k8s}
154prefix: /{self.name}/
155service: {self.target.path.fqdn}
156add_request_headers:
157 x-cert-start: { value: "%DOWNSTREAM_PEER_CERT_V_START%" }
158 x-cert-end: { value: "%DOWNSTREAM_PEER_CERT_V_END%" }
159 x-cert-start-custom: { value: "%DOWNSTREAM_PEER_CERT_V_START(%b %e %H:%M:%S %Y %Z)%" }
160 x-cert-end-custom: { value: "%DOWNSTREAM_PEER_CERT_V_END(%b %e %H:%M:%S %Y %Z)%" }
161"""
162 )
163
164 def scheme(self) -> str:
165 return "https"
166
167 def queries(self):
168 yield Query(
169 self.url(self.name + "/"),
170 insecure=True,
171 client_crt=TLSCerts["presto.example.com"].pubcert,
172 client_key=TLSCerts["presto.example.com"].privkey,
173 client_cert_required=True,
174 ca_cert=TLSCerts["master.datawire.io"].pubcert,
175 )
176
177 # In TLS < 1.3, there's not a dedicated alert code for "the client forgot to include a certificate",
178 # so we get a generic alert=40 ("handshake_failure"). We also include "write: connection reset by peer"
179 # because we've seen cases where Envoy and the client library don't play nicely, so the error report doesn't
180 # get back before the connection closes.
181 yield Query(
182 self.url(self.name + "/"),
183 insecure=True,
184 maxTLSv="v1.2",
185 error=["tls: handshake failure", "write: connection reset by peer"],
186 )
187
188 # TLS 1.3 added a dedicated alert=116 ("certificate_required") for that scenario. See above for why
189 # "write: connection reset by peer " is also accepted.
190 yield Query(
191 self.url(self.name + "/"),
192 insecure=True,
193 minTLSv="v1.3",
194 error=["tls: certificate required", "write: connection reset by peer"],
195 )
196
197 def check(self):
198 cert = TLSCerts["presto.example.com"].pubcert
199 # base64-decode the cert data after removing the "---BEGIN CERTIFICATE---" / "---END CERTIFICATE---" lines.
200 certraw = b64decode("\n".join(l for l in cert.split("\n") if not l.startswith("-")))
201 # take the sha256 sum aof that.
202 certhash = hashlib.sha256(certraw).hexdigest()
203
204 assert self.results[0].backend
205 assert self.results[0].backend.request
206 assert self.results[0].backend.request.headers["x-forwarded-client-cert"] == [
207 f'Hash={certhash};Subject="CN=presto.example.com,OU=Engineering,O=Ambassador Labs,L=Boston,ST=MA,C=US"'
208 ], (
209 "unexpected x-forwarded-client-cert value: %s"
210 % self.results[0].backend.request.headers["x-forwarded-client-cert"]
211 )
212 assert self.results[0].backend.request.headers["x-cert-start"] == [
213 "2021-11-10T13:12:00.000Z"
214 ], (
215 "unexpected x-cert-start value: %s"
216 % self.results[0].backend.request.headers["x-cert-start"]
217 )
218 assert self.results[0].backend.request.headers["x-cert-end"] == [
219 "2099-11-10T13:12:00.000Z"
220 ], (
221 "unexpected x-cert-end value: %s"
222 % self.results[0].backend.request.headers["x-cert-end"]
223 )
224 assert self.results[1].backend
225 assert self.results[1].backend.request
226 assert self.results[0].backend.request.headers["x-cert-start-custom"] == [
227 "Nov 10 13:12:00 2021 UTC"
228 ], (
229 "unexpected x-cert-start-custom value: %s"
230 % self.results[1].backend.request.headers["x-cert-start-custom"]
231 )
232 assert self.results[0].backend.request.headers["x-cert-end-custom"] == [
233 "Nov 10 13:12:00 2099 UTC"
234 ], (
235 "unexpected x-cert-end-custom value: %s"
236 % self.results[0].backend.request.headers["x-cert-end-custom"]
237 )
238
239 def requirements(self):
240 for r in super().requirements():
241 query = r[1]
242 query.insecure = True
243 query.client_cert = TLSCerts["presto.example.com"].pubcert
244 query.client_key = TLSCerts["presto.example.com"].privkey
245 query.client_cert_required = True
246 query.ca_cert = TLSCerts["master.datawire.io"].pubcert
247 yield (r[0], query)
248
249
250class ClientCertificateAuthenticationContext(AmbassadorTest):
251 def init(self):
252 self.xfail = "FIXME: IHA"
253 self.target = HTTP()
254
255 def manifests(self) -> str:
256 return (
257 self.format(
258 f"""
259---
260apiVersion: v1
261metadata:
262 name: ccauthctx-client-secret
263 labels:
264 kat-ambassador-id: {self.ambassador_id}
265data:
266 tls.crt: {TLSCerts["master.datawire.io"].k8s_crt}
267kind: Secret
268type: Opaque
269---
270apiVersion: v1
271kind: Secret
272metadata:
273 name: ccauthctx-server-secret
274 labels:
275 kat-ambassador-id: {self.ambassador_id}
276type: kubernetes.io/tls
277data:
278 tls.crt: {TLSCerts["ambassador.example.com"].k8s_crt}
279 tls.key: {TLSCerts["ambassador.example.com"].k8s_key}
280---
281apiVersion: getambassador.io/v3alpha1
282kind: TLSContext
283metadata:
284 name: ccauthctx-tls
285 labels:
286 kat-ambassador-id: {self.ambassador_id}
287spec:
288 ambassador_id: [{self.ambassador_id}]
289 hosts: [ "*" ]
290 secret: ccauthctx-server-secret
291 ca_secret: ccauthctx-client-secret
292 cert_required: True
293"""
294 )
295 + super().manifests()
296 )
297
298 def config(self) -> Generator[Union[str, Tuple[Node, str]], None, None]:
299 yield self, self.format(
300 """
301---
302apiVersion: getambassador.io/v3alpha1
303kind: Mapping
304name: {self.target.path.k8s}
305prefix: /{self.name}/
306service: {self.target.path.fqdn}
307"""
308 )
309
310 def scheme(self) -> str:
311 return "https"
312
313 def queries(self):
314 yield Query(
315 self.url(self.name + "/"),
316 insecure=True,
317 client_crt=TLSCerts["presto.example.com"].pubcert,
318 client_key=TLSCerts["presto.example.com"].privkey,
319 client_cert_required=True,
320 ca_cert=TLSCerts["master.datawire.io"].pubcert,
321 )
322
323 # In TLS < 1.3, there's not a dedicated alert code for "the client forgot to include a certificate",
324 # so we get a generic alert=40 ("handshake_failure"). We also include "write: connection reset by peer"
325 # because we've seen cases where Envoy and the client library don't play nicely, so the error report doesn't
326 # get back before the connection closes.
327 yield Query(
328 self.url(self.name + "/"),
329 insecure=True,
330 maxTLSv="v1.2",
331 error=["tls: handshake failure", "write: connection reset by peer"],
332 )
333
334 # TLS 1.3 added a dedicated alert=116 ("certificate_required") for that scenario. See above for why
335 # "write: connection reset by peer" is also accepted.
336 yield Query(
337 self.url(self.name + "/"),
338 insecure=True,
339 minTLSv="v1.3",
340 error=["tls: certificate required", "write: connection reset by peer"],
341 )
342
343 def requirements(self):
344 for r in super().requirements():
345 query = r[1]
346 query.insecure = True
347 query.client_cert = TLSCerts["presto.example.com"].pubcert
348 query.client_key = TLSCerts["presto.example.com"].privkey
349 query.client_cert_required = True
350 query.ca_cert = TLSCerts["master.datawire.io"].pubcert
351 yield (r[0], query)
352
353
354class ClientCertificateAuthenticationContextCRL(AmbassadorTest):
355 def init(self):
356 self.xfail = "FIXME: IHA" # This test should cover TLSContext with a crl_secret
357 self.target = HTTP()
358
359 def manifests(self) -> str:
360 return (
361 self.format(
362 f"""
363---
364apiVersion: v1
365metadata:
366 name: ccauthctxcrl-client-secret
367 labels:
368 kat-ambassador-id: {self.ambassador_id}
369data:
370 tls.crt: {TLSCerts["master.datawire.io"].k8s_crt}
371kind: Secret
372type: Opaque
373---
374apiVersion: v1
375kind: Secret
376metadata:
377 name: ccauthctxcrl-server-secret
378 labels:
379 kat-ambassador-id: {self.ambassador_id}
380type: kubernetes.io/tls
381data:
382 tls.crt: {TLSCerts["ambassador.example.com"].k8s_crt}
383 tls.key: {TLSCerts["ambassador.example.com"].k8s_key}
384---
385apiVersion: v1
386kind: Secret
387metadata:
388 name: ccauthctxcrl-crl-secret
389 labels:
390 kat-ambassador-id: {self.ambassador_id}
391type: Opaque
392data:
393 crl.pem: {create_crl_pem_b64(TLSCerts["master.datawire.io"].pubcert, TLSCerts["master.datawire.io"].privkey, [TLSCerts["presto.example.com"].pubcert])}
394---
395apiVersion: getambassador.io/v3alpha1
396kind: TLSContext
397metadata:
398 name: ccauthctxcrl-tls
399 labels:
400 kat-ambassador-id: {self.ambassador_id}
401spec:
402 ambassador_id: [{self.ambassador_id}]
403 hosts: [ "*" ]
404 secret: ccauthctxcrl-server-secret
405 ca_secret: ccauthctxcrl-client-secret
406 crl_secret: ccauthctxcrl-crl-secret
407 cert_required: True
408"""
409 )
410 + super().manifests()
411 )
412
413 def config(self) -> Generator[Union[str, Tuple[Node, str]], None, None]:
414 yield self, self.format(
415 """
416---
417apiVersion: getambassador.io/v3alpha1
418kind: Mapping
419name: {self.target.path.k8s}
420prefix: /
421service: {self.target.path.fqdn}
422hostname: "*"
423"""
424 )
425
426 def scheme(self) -> str:
427 return "https"
428
429 def queries(self):
430 yield Query(
431 self.url(self.name + "/"),
432 insecure=True,
433 client_crt=TLSCerts["presto.example.com"].pubcert,
434 client_key=TLSCerts["presto.example.com"].privkey,
435 client_cert_required=True,
436 ca_cert=TLSCerts["master.datawire.io"].pubcert,
437 error=["tls: revoked certificate"],
438 )
439
440 def requirements(self):
441 yield ("pod", self.path.k8s)
442
443
444class TLSOriginationSecret(AmbassadorTest):
445 def init(self):
446 self.xfail = "FIXME: IHA"
447 self.target = HTTP()
448
449 def manifests(self) -> str:
450 return (
451 f"""
452---
453apiVersion: v1
454kind: Secret
455metadata:
456 name: test-origination-secret
457 labels:
458 kat-ambassador-id: tlsoriginationsecret
459type: kubernetes.io/tls
460data:
461 tls.crt: {TLSCerts["localhost"].k8s_crt}
462 tls.key: {TLSCerts["localhost"].k8s_key}
463"""
464 + super().manifests()
465 )
466
467 def config(self) -> Generator[Union[str, Tuple[Node, str]], None, None]:
468 fingerprint = (
469 hashlib.sha1(
470 (
471 TLSCerts["localhost"].pubcert + "\n" + TLSCerts["localhost"].privkey + "\n"
472 ).encode("utf-8")
473 )
474 .hexdigest()
475 .upper()
476 )
477
478 yield self, f"""
479---
480apiVersion: getambassador.io/v3alpha1
481kind: Module
482ambassador_id: [{self.ambassador_id}]
483name: tls
484config:
485 upstream:
486 secret: test-origination-secret
487 upstream-files:
488 cert_chain_file: /tmp/ambassador/snapshots/default/secrets-decoded/test-origination-secret/{fingerprint}.crt
489 private_key_file: /tmp/ambassador/snapshots/default/secrets-decoded/test-origination-secret/{fingerprint}.key
490"""
491
492 yield self, self.format(
493 """
494---
495apiVersion: getambassador.io/v3alpha1
496kind: Mapping
497name: {self.target.path.k8s}
498prefix: /{self.name}/
499service: {self.target.path.fqdn}
500tls: upstream
501"""
502 )
503
504 yield self, self.format(
505 """
506---
507apiVersion: getambassador.io/v3alpha1
508kind: Mapping
509name: {self.target.path.k8s}-files
510prefix: /{self.name}-files/
511service: {self.target.path.fqdn}
512tls: upstream-files
513"""
514 )
515
516 def queries(self):
517 yield Query(self.url(self.name + "/"))
518 yield Query(self.url(self.name + "-files/"))
519
520 def check(self):
521 for r in self.results:
522 assert r.backend
523 assert r.backend.request
524 assert r.backend.request.tls.enabled
525
526
527class TLS(AmbassadorTest):
528
529 target: ServiceType
530
531 def init(self):
532 self.xfail = "FIXME: IHA"
533 self.target = HTTP()
534
535 def manifests(self) -> str:
536 return (
537 f"""
538---
539apiVersion: v1
540kind: Secret
541metadata:
542 name: test-tls-secret
543 labels:
544 kat-ambassador-id: tls
545type: kubernetes.io/tls
546data:
547 tls.crt: {TLSCerts["localhost"].k8s_crt}
548 tls.key: {TLSCerts["localhost"].k8s_key}
549---
550apiVersion: v1
551kind: Secret
552metadata:
553 name: ambassador-certs
554 labels:
555 kat-ambassador-id: tls
556type: kubernetes.io/tls
557data:
558 tls.crt: {TLSCerts["localhost"].k8s_crt}
559 tls.key: {TLSCerts["localhost"].k8s_key}
560---
561apiVersion: getambassador.io/v3alpha1
562kind: Host
563metadata:
564 name: tls-host
565 labels:
566 kat-ambassador-id: tls
567spec:
568 ambassador_id: [tls]
569 tlsSecret:
570 name: test-tls-secret
571 requestPolicy:
572 insecure:
573 action: Reject
574"""
575 + super().manifests()
576 )
577
578 def config(self) -> Generator[Union[str, Tuple[Node, str]], None, None]:
579 # # Use self here, not self.target, because we want the TLS module to
580 # # be annotated on the Ambassador itself.
581 # yield self, self.format("""
582 # ---
583 # apiVersion: getambassador.io/v3alpha1
584 # kind: Module
585 # name: tls
586 # ambassador_id: [{self.ambassador_id}]
587 # config:
588 # server:
589 # enabled: True
590 # secret: test-tls-secret
591 # """)
592
593 # Use self.target _here_, because we want the mapping to be annotated
594 # on the service, not the Ambassador. Also, you don't need to include
595 # the ambassador_id unless you need some special ambassador_id that
596 # isn't something that kat already knows about.
597 #
598 # If the test were more complex, we'd probably need to do some sort
599 # of mangling for the mapping name and prefix. For this simple test,
600 # it's not necessary.
601 yield self.target, self.format(
602 """
603---
604apiVersion: getambassador.io/v3alpha1
605kind: Mapping
606name: tls_target_mapping
607prefix: /tls-target/
608service: {self.target.path.fqdn}
609"""
610 )
611
612 def scheme(self) -> str:
613 return "https"
614
615 def queries(self):
616 yield Query(self.url("tls-target/"), insecure=True)
617
618
619class TLSInvalidSecret(AmbassadorTest):
620
621 target: ServiceType
622
623 def init(self):
624 self.xfail = "FIXME: IHA"
625 self.target = HTTP()
626
627 def config(self) -> Generator[Union[str, Tuple[Node, str]], None, None]:
628 yield self, self.format(
629 """
630---
631apiVersion: getambassador.io/v3alpha1
632kind: Module
633name: tls
634ambassador_id: [{self.ambassador_id}]
635config:
636 server:
637 enabled: True
638 secret: test-certs-secret-invalid
639 missing-secret-key:
640 cert_chain_file: /nonesuch
641 bad-path-info:
642 cert_chain_file: /nonesuch
643 private_key_file: /nonesuch
644 validation-without-termination:
645 enabled: True
646 secret: test-certs-secret-invalid
647 ca_secret: ambassador-certs
648"""
649 )
650
651 yield self.target, self.format(
652 """
653---
654apiVersion: getambassador.io/v3alpha1
655kind: Mapping
656name: tls_target_mapping
657prefix: /tls-target/
658service: {self.target.path.fqdn}
659"""
660 )
661
662 def scheme(self) -> str:
663 return "http"
664
665 def queries(self):
666 yield Query(self.url("ambassador/v0/diag/?json=true&filter=errors"), phase=2)
667
668 def check(self):
669 assert self.results[0].backend
670 errors = self.results[0].backend.response
671
672 expected = set(
673 {
674 "TLSContext server found no certificate in secret test-certs-secret-invalid in namespace default, ignoring...",
675 "TLSContext bad-path-info found no cert_chain_file '/nonesuch'",
676 "TLSContext bad-path-info found no private_key_file '/nonesuch'",
677 "TLSContext validation-without-termination found no certificate in secret test-certs-secret-invalid in namespace default, ignoring...",
678 "TLSContext missing-secret-key: 'cert_chain_file' requires 'private_key_file' as well",
679 }
680 )
681
682 current = set({})
683 for errsvc, errtext in errors:
684 current.add(errtext)
685
686 diff = expected - current
687
688 assert len(diff) == 0, f"expected {len(expected)} errors, got {len(errors)}: Missing {diff}"
689
690
691class TLSContextTest(AmbassadorTest):
692 # debug = True
693
694 def init(self):
695 self.xfail = "FIXME: IHA"
696 self.target = HTTP()
697
698 if EDGE_STACK:
699 self.xfail = "XFailing for now"
700
701 def manifests(self) -> str:
702 return (
703 namespace_manifest("secret-namespace")
704 + f"""
705---
706apiVersion: v1
707data:
708 tls.crt: {TLSCerts["localhost"].k8s_crt}
709 tls.key: {TLSCerts["localhost"].k8s_key}
710kind: Secret
711metadata:
712 name: test-tlscontext-secret-0
713 labels:
714 kat-ambassador-id: tlscontexttest
715type: kubernetes.io/tls
716---
717apiVersion: v1
718data:
719 tls.crt: {TLSCerts["tls-context-host-1"].k8s_crt}
720 tls.key: {TLSCerts["tls-context-host-1"].k8s_key}
721kind: Secret
722metadata:
723 name: test-tlscontext-secret-1
724 namespace: secret-namespace
725 labels:
726 kat-ambassador-id: tlscontexttest
727type: kubernetes.io/tls
728---
729apiVersion: v1
730data:
731 tls.crt: {TLSCerts["tls-context-host-2"].k8s_crt}
732 tls.key: {TLSCerts["tls-context-host-2"].k8s_key}
733kind: Secret
734metadata:
735 name: test-tlscontext-secret-2
736 labels:
737 kat-ambassador-id: tlscontexttest
738type: kubernetes.io/tls
739"""
740 + super().manifests()
741 )
742
743 def config(self) -> Generator[Union[str, Tuple[Node, str]], None, None]:
744 yield self, self.format(
745 """
746---
747apiVersion: getambassador.io/v3alpha1
748kind: Mapping
749name: {self.name}-same-prefix-1
750prefix: /tls-context-same/
751service: http://{self.target.path.fqdn}
752host: tls-context-host-1
753"""
754 )
755 yield self, self.format(
756 """
757---
758apiVersion: getambassador.io/v3alpha1
759kind: TLSContext
760name: {self.name}-same-context-1
761hosts:
762- tls-context-host-1
763secret: test-tlscontext-secret-1.secret-namespace
764min_tls_version: v1.0
765max_tls_version: v1.3
766redirect_cleartext_from: 8080
767"""
768 )
769 yield self, self.format(
770 """
771---
772apiVersion: getambassador.io/v3alpha1
773kind: Mapping
774name: {self.name}-same-prefix-2
775prefix: /tls-context-same/
776service: http://{self.target.path.fqdn}
777host: tls-context-host-2
778"""
779 )
780 yield self, self.format(
781 """
782---
783apiVersion: getambassador.io/v3alpha1
784kind: TLSContext
785name: {self.name}-same-context-2
786hosts:
787- tls-context-host-2
788secret: test-tlscontext-secret-2
789alpn_protocols: h2,http/1.1
790redirect_cleartext_from: 8080
791"""
792 )
793 yield self, self.format(
794 """
795---
796apiVersion: getambassador.io/v3alpha1
797kind: Module
798name: tls
799config:
800 server:
801 enabled: True
802 secret: test-tlscontext-secret-0
803"""
804 )
805 yield self, self.format(
806 """
807---
808apiVersion: getambassador.io/v3alpha1
809kind: Mapping
810name: {self.name}-other-mapping
811prefix: /{self.name}/
812service: https://{self.target.path.fqdn}
813"""
814 )
815 # Ambassador should not return an error when hostname is not present.
816 yield self, self.format(
817 """
818---
819apiVersion: getambassador.io/v3alpha1
820kind: TLSContext
821name: {self.name}-no-secret
822min_tls_version: v1.0
823max_tls_version: v1.3
824redirect_cleartext_from: 8080
825"""
826 )
827 # Ambassador should return an error for this configuration.
828 yield self, self.format(
829 """
830---
831apiVersion: getambassador.io/v3alpha1
832kind: TLSContext
833name: {self.name}-same-context-error
834hosts:
835- tls-context-host-1
836redirect_cleartext_from: 8080
837"""
838 )
839 # Ambassador should return an error for this configuration.
840 yield self, self.format(
841 """
842---
843apiVersion: getambassador.io/v3alpha1
844kind: TLSContext
845name: {self.name}-rcf-error
846hosts:
847- tls-context-host-1
848redirect_cleartext_from: 8081
849"""
850 )
851
852 def scheme(self) -> str:
853 return "https"
854
855 @staticmethod
856 def _go_close_connection_error(url):
857 """
858 :param url: url passed to the query
859 :return: error message string that Go's net/http package throws when server closes connection
860 """
861 return "Get {}: EOF".format(url)
862
863 def queries(self):
864 # 0
865 yield Query(
866 self.url("ambassador/v0/diag/?json=true&filter=errors"),
867 headers={"Host": "tls-context-host-2"},
868 insecure=True,
869 sni=True,
870 )
871
872 # 1 - Correct host #1
873 yield Query(
874 self.url("tls-context-same/"),
875 headers={"Host": "tls-context-host-1"},
876 expected=200,
877 insecure=True,
878 sni=True,
879 )
880 # 2 - Correct host #2
881 yield Query(
882 self.url("tls-context-same/"),
883 headers={"Host": "tls-context-host-2"},
884 expected=200,
885 insecure=True,
886 sni=True,
887 )
888
889 # 3 - Incorrect host
890 yield Query(
891 self.url("tls-context-same/"),
892 headers={"Host": "tls-context-host-3"},
893 # error=self._go_close_connection_error(self.url("tls-context-same/")),
894 expected=404,
895 insecure=True,
896 )
897
898 # 4 - Incorrect path, correct host
899 yield Query(
900 self.url("tls-context-different/"),
901 headers={"Host": "tls-context-host-1"},
902 expected=404,
903 insecure=True,
904 sni=True,
905 )
906
907 # Other mappings with no host will respond with the fallbock cert.
908 # 5 - no Host header, fallback cert from the TLS module
909 yield Query(
910 self.url(self.name + "/"),
911 # error=self._go_close_connection_error(self.url(self.name + "/")),
912 insecure=True,
913 )
914
915 # 6 - explicit Host header, fallback cert
916 yield Query(
917 self.url(self.name + "/"),
918 # error=self._go_close_connection_error(self.url(self.name + "/")),
919 # sni=True,
920 headers={"Host": "tls-context-host-3"},
921 insecure=True,
922 )
923
924 # 7 - explicit Host header 1 wins, we'll get the SNI cert for this overlapping path
925 yield Query(
926 self.url(self.name + "/"),
927 headers={"Host": "tls-context-host-1"},
928 expected=200,
929 insecure=True,
930 sni=True,
931 )
932
933 # 8 - explicit Host header 2 wins, we'll get the SNI cert for this overlapping path
934 yield Query(
935 self.url(self.name + "/"),
936 headers={"Host": "tls-context-host-2"},
937 expected=200,
938 insecure=True,
939 sni=True,
940 )
941
942 # 9 - Redirect cleartext from actually redirects.
943 yield Query(
944 self.url("tls-context-same/", scheme="http"),
945 headers={"Host": "tls-context-host-1"},
946 expected=301,
947 insecure=True,
948 sni=True,
949 )
950
951 def check(self):
952 # XXX Ew. If self.results[0].json is empty, the harness won't convert it to a response.
953 errors = self.results[0].json
954 num_errors = len(errors)
955 assert num_errors == 5, "expected 5 errors, got {} -\n{}".format(num_errors, errors)
956
957 errors_that_should_be_found = {
958 "TLSContext TLSContextTest-no-secret has no certificate information at all?": False,
959 "TLSContext TLSContextTest-same-context-error has no certificate information at all?": False,
960 "TLSContext TLSContextTest-same-context-error is missing cert_chain_file": False,
961 "TLSContext TLSContextTest-same-context-error is missing private_key_file": False,
962 "TLSContext: TLSContextTest-rcf-error; configured conflicting redirect_from port: 8081": False,
963 }
964
965 unknown_errors: List[str] = []
966 for err in errors:
967 text = err[1]
968
969 if text in errors_that_should_be_found:
970 errors_that_should_be_found[text] = True
971 else:
972 unknown_errors.append(f"Unexpected error {text}")
973
974 for err, found in errors_that_should_be_found.items():
975 if not found:
976 unknown_errors.append(f"Missing error {err}")
977
978 assert not unknown_errors, f"Problems with errors: {unknown_errors}"
979
980 idx = 0
981
982 for result in self.results:
983 if result.status == 200 and result.query.headers:
984 host_header = result.query.headers["Host"]
985 tls_common_name = result.tls[0]["Issuer"]["CommonName"]
986
987 # XXX Weirdness with the fallback cert here! You see, if we use host
988 # tls-context-host-3 (or, really, anything except -1 or -2), then the
989 # fallback cert actually has CN 'localhost'. We should replace this with
990 # a real fallback cert, but for now, just hack the host_header.
991 #
992 # Ew.
993
994 if host_header == "tls-context-host-3":
995 host_header = "localhost"
996
997 assert host_header == tls_common_name, "test %d wanted CN %s, but got %s" % (
998 idx,
999 host_header,
1000 tls_common_name,
1001 )
1002
1003 idx += 1
1004
1005 def requirements(self):
1006 # We're replacing super()'s requirements deliberately here. Without a Host header they can't work.
1007 yield (
1008 "url",
1009 Query(
1010 self.url("ambassador/v0/check_ready"),
1011 headers={"Host": "tls-context-host-1"},
1012 insecure=True,
1013 sni=True,
1014 ),
1015 )
1016 yield (
1017 "url",
1018 Query(
1019 self.url("ambassador/v0/check_alive"),
1020 headers={"Host": "tls-context-host-1"},
1021 insecure=True,
1022 sni=True,
1023 ),
1024 )
1025 yield (
1026 "url",
1027 Query(
1028 self.url("ambassador/v0/check_ready"),
1029 headers={"Host": "tls-context-host-2"},
1030 insecure=True,
1031 sni=True,
1032 ),
1033 )
1034 yield (
1035 "url",
1036 Query(
1037 self.url("ambassador/v0/check_alive"),
1038 headers={"Host": "tls-context-host-2"},
1039 insecure=True,
1040 sni=True,
1041 ),
1042 )
1043
1044
1045class TLSIngressTest(AmbassadorTest):
1046 def init(self):
1047 self.xfail = "FIXME: IHA"
1048 self.target = HTTP()
1049
1050 def manifests(self) -> str:
1051 self.manifest_envs = """
1052 - name: AMBASSADOR_DEBUG
1053 value: "diagd"
1054"""
1055
1056 return (
1057 namespace_manifest("secret-namespace-ingress")
1058 + f"""
1059---
1060apiVersion: v1
1061data:
1062 tls.crt: {TLSCerts["localhost"].k8s_crt}
1063 tls.key: {TLSCerts["localhost"].k8s_key}
1064kind: Secret
1065metadata:
1066 name: test-tlscontext-secret-ingress-0
1067 labels:
1068 kat-ambassador-id: tlsingresstest
1069type: kubernetes.io/tls
1070---
1071apiVersion: v1
1072data:
1073 tls.crt: {TLSCerts["tls-context-host-1"].k8s_crt}
1074 tls.key: {TLSCerts["tls-context-host-1"].k8s_key}
1075kind: Secret
1076metadata:
1077 name: test-tlscontext-secret-ingress-1
1078 namespace: secret-namespace-ingress
1079 labels:
1080 kat-ambassador-id: tlsingresstest
1081type: kubernetes.io/tls
1082---
1083apiVersion: v1
1084data:
1085 tls.crt: {TLSCerts["tls-context-host-2"].k8s_crt}
1086 tls.key: {TLSCerts["tls-context-host-2"].k8s_key}
1087kind: Secret
1088metadata:
1089 name: test-tlscontext-secret-ingress-2
1090 labels:
1091 kat-ambassador-id: tlsingresstest
1092type: kubernetes.io/tls
1093---
1094apiVersion: networking.k8s.io/v1
1095kind: Ingress
1096metadata:
1097 annotations:
1098 kubernetes.io/ingress.class: ambassador
1099 getambassador.io/ambassador-id: tlsingresstest
1100 name: {self.name.lower()}-1
1101spec:
1102 tls:
1103 - secretName: test-tlscontext-secret-ingress-1.secret-namespace-ingress
1104 hosts:
1105 - tls-context-host-1
1106 rules:
1107 - host: tls-context-host-1
1108 http:
1109 paths:
1110 - backend:
1111 service:
1112 name: {self.target.path.k8s}
1113 port:
1114 number: 80
1115 path: /tls-context-same/
1116 pathType: Prefix
1117---
1118apiVersion: networking.k8s.io/v1
1119kind: Ingress
1120metadata:
1121 annotations:
1122 kubernetes.io/ingress.class: ambassador
1123 getambassador.io/ambassador-id: tlsingresstest
1124 name: {self.name.lower()}-2
1125spec:
1126 tls:
1127 - secretName: test-tlscontext-secret-ingress-2
1128 hosts:
1129 - tls-context-host-2
1130 rules:
1131 - host: tls-context-host-2
1132 http:
1133 paths:
1134 - backend:
1135 service:
1136 name: {self.target.path.k8s}
1137 port:
1138 number: 80
1139 path: /tls-context-same/
1140 pathType: Prefix
1141"""
1142 + super().manifests()
1143 )
1144
1145 def config(self) -> Generator[Union[str, Tuple[Node, str]], None, None]:
1146 yield self, self.format(
1147 """
1148---
1149apiVersion: getambassador.io/v3alpha1
1150kind: Module
1151name: tls
1152config:
1153 server:
1154 enabled: True
1155 secret: test-tlscontext-secret-ingress-0
1156"""
1157 )
1158
1159 yield self, self.format(
1160 """
1161---
1162apiVersion: getambassador.io/v3alpha1
1163kind: Mapping
1164hostname: "*"
1165name: {self.name}-other-mapping
1166prefix: /{self.name}/
1167service: https://{self.target.path.fqdn}
1168"""
1169 )
1170
1171 def scheme(self) -> str:
1172 return "https"
1173
1174 @staticmethod
1175 def _go_close_connection_error(url):
1176 """
1177 :param url: url passed to the query
1178 :return: error message string that Go's net/http package throws when server closes connection
1179 """
1180 return "Get {}: EOF".format(url)
1181
1182 def queries(self):
1183 # 0
1184 yield Query(
1185 self.url("ambassador/v0/diag/?json=true&filter=errors"),
1186 headers={"Host": "tls-context-host-2"},
1187 insecure=True,
1188 sni=True,
1189 )
1190
1191 # 1 - Correct host #1
1192 yield Query(
1193 self.url("tls-context-same/"),
1194 headers={"Host": "tls-context-host-1"},
1195 expected=200,
1196 insecure=True,
1197 sni=True,
1198 )
1199 # 2 - Correct host #2
1200 yield Query(
1201 self.url("tls-context-same/"),
1202 headers={"Host": "tls-context-host-2"},
1203 expected=200,
1204 insecure=True,
1205 sni=True,
1206 )
1207
1208 # 3 - Incorrect host
1209 yield Query(
1210 self.url("tls-context-same/"),
1211 headers={"Host": "tls-context-host-3"},
1212 # error=self._go_close_connection_error(self.url("tls-context-same/")),
1213 expected=404,
1214 insecure=True,
1215 )
1216
1217 # 4 - Incorrect path, correct host
1218 yield Query(
1219 self.url("tls-context-different/"),
1220 headers={"Host": "tls-context-host-1"},
1221 expected=404,
1222 insecure=True,
1223 sni=True,
1224 )
1225
1226 # Other mappings with no host will respond with the fallbock cert.
1227 # 5 - no Host header, fallback cert from the TLS module
1228 yield Query(
1229 self.url(self.name + "/"),
1230 # error=self._go_close_connection_error(self.url(self.name + "/")),
1231 insecure=True,
1232 )
1233
1234 # 6 - explicit Host header, fallback cert
1235 yield Query(
1236 self.url(self.name + "/"),
1237 # error=self._go_close_connection_error(self.url(self.name + "/")),
1238 # sni=True,
1239 headers={"Host": "tls-context-host-3"},
1240 insecure=True,
1241 )
1242
1243 # 7 - explicit Host header 1 wins, we'll get the SNI cert for this overlapping path
1244 yield Query(
1245 self.url(self.name + "/"),
1246 headers={"Host": "tls-context-host-1"},
1247 expected=200,
1248 insecure=True,
1249 sni=True,
1250 )
1251
1252 # 7 - explicit Host header 2 wins, we'll get the SNI cert for this overlapping path
1253 yield Query(
1254 self.url(self.name + "/"),
1255 headers={"Host": "tls-context-host-2"},
1256 expected=200,
1257 insecure=True,
1258 sni=True,
1259 )
1260
1261 def check(self):
1262 # XXX Ew. If self.results[0].json is empty, the harness won't convert it to a response.
1263 errors = self.results[0].json
1264 num_errors = len(errors)
1265 assert num_errors == 0, "expected 0 errors, got {} -\n{}".format(num_errors, errors)
1266
1267 idx = 0
1268
1269 for result in self.results:
1270 if result.status == 200 and result.query.headers:
1271 host_header = result.query.headers["Host"]
1272 tls_common_name = result.tls[0]["Issuer"]["CommonName"]
1273
1274 # XXX Weirdness with the fallback cert here! You see, if we use host
1275 # tls-context-host-3 (or, really, anything except -1 or -2), then the
1276 # fallback cert actually has CN 'localhost'. We should replace this with
1277 # a real fallback cert, but for now, just hack the host_header.
1278 #
1279 # Ew.
1280
1281 if host_header == "tls-context-host-3":
1282 host_header = "localhost"
1283
1284 # Yep, that's expected. Since the TLS secret for 'tls-context-host-1' is
1285 # not namespaced it should only resolve to the Ingress' own
1286 # namespace, and can't use the 'secret.namespace' Ambassador syntax
1287 if host_header == "tls-context-host-1":
1288 host_header = "localhost"
1289
1290 assert host_header == tls_common_name, "test %d wanted CN %s, but got %s" % (
1291 idx,
1292 host_header,
1293 tls_common_name,
1294 )
1295
1296 idx += 1
1297
1298 def requirements(self):
1299 yield (
1300 "url",
1301 Query(
1302 self.url("ambassador/v0/check_ready"),
1303 headers={"Host": "tls-context-host-1"},
1304 insecure=True,
1305 sni=True,
1306 ),
1307 )
1308 yield (
1309 "url",
1310 Query(
1311 self.url("ambassador/v0/check_alive"),
1312 headers={"Host": "tls-context-host-1"},
1313 insecure=True,
1314 sni=True,
1315 ),
1316 )
1317 yield (
1318 "url",
1319 Query(
1320 self.url("ambassador/v0/check_ready"),
1321 headers={"Host": "tls-context-host-2"},
1322 insecure=True,
1323 sni=True,
1324 ),
1325 )
1326 yield (
1327 "url",
1328 Query(
1329 self.url("ambassador/v0/check_alive"),
1330 headers={"Host": "tls-context-host-2"},
1331 insecure=True,
1332 sni=True,
1333 ),
1334 )
1335
1336
1337class TLSContextProtocolMaxVersion(AmbassadorTest):
1338 # Here we're testing that the client can't exceed the maximum TLS version
1339 # configured.
1340 #
1341 # XXX 2019-09-11: vet that the test client's support for TLS v1.3 is up-to-date.
1342 # It appears not to be.
1343
1344 # debug = True
1345
1346 def init(self):
1347 self.target = HTTP()
1348
1349 if EDGE_STACK:
1350 self.xfail = "Not yet supported in Edge Stack"
1351
1352 self.xfail = "FIXME: IHA"
1353
1354 def manifests(self) -> str:
1355 return (
1356 f"""
1357---
1358apiVersion: v1
1359data:
1360 tls.crt: {TLSCerts["tls-context-host-1"].k8s_crt}
1361 tls.key: {TLSCerts["tls-context-host-1"].k8s_key}
1362kind: Secret
1363metadata:
1364 name: secret.max-version
1365 labels:
1366 kat-ambassador-id: tlscontextprotocolmaxversion
1367type: kubernetes.io/tls
1368"""
1369 + super().manifests()
1370 )
1371
1372 def config(self) -> Generator[Union[str, Tuple[Node, str]], None, None]:
1373 yield self, self.format(
1374 """
1375---
1376apiVersion: getambassador.io/v3alpha1
1377kind: Module
1378name: ambassador
1379config:
1380 defaults:
1381 tls_secret_namespacing: False
1382---
1383apiVersion: getambassador.io/v3alpha1
1384kind: Mapping
1385name: {self.name}-same-prefix-1
1386prefix: /tls-context-same/
1387service: http://{self.target.path.fqdn}
1388host: tls-context-host-1
1389---
1390apiVersion: getambassador.io/v3alpha1
1391kind: TLSContext
1392name: {self.name}-same-context-1
1393hosts:
1394- tls-context-host-1
1395secret: secret.max-version
1396min_tls_version: v1.1
1397max_tls_version: v1.2
1398"""
1399 )
1400
1401 def scheme(self) -> str:
1402 return "https"
1403
1404 @staticmethod
1405 def _go_close_connection_error(url):
1406 """
1407 :param url: url passed to the query
1408 :return: error message string that Go's net/http package throws when server closes connection
1409 """
1410 return "Get {}: EOF".format(url)
1411
1412 def queries(self):
1413 # ----
1414 # XXX 2019-09-11
1415 # These aren't actually reporting the negotiated version, alhough correct
1416 # behavior can be verified with a custom log format. What, does the silly thing just not
1417 # report the negotiated version if it's the max you've requested??
1418 #
1419 # For now, we're checking for the None result, but, ew.
1420 # ----
1421
1422 yield Query(
1423 self.url("tls-context-same/"),
1424 headers={"Host": "tls-context-host-1"},
1425 expected=200,
1426 insecure=True,
1427 sni=True,
1428 minTLSv="v1.2",
1429 maxTLSv="v1.2",
1430 )
1431
1432 # This should give us TLS v1.1
1433 yield Query(
1434 self.url("tls-context-same/"),
1435 headers={"Host": "tls-context-host-1"},
1436 expected=200,
1437 insecure=True,
1438 sni=True,
1439 minTLSv="v1.0",
1440 maxTLSv="v1.1",
1441 )
1442
1443 # This should be an error.
1444 yield Query(
1445 self.url("tls-context-same/"),
1446 headers={"Host": "tls-context-host-1"},
1447 expected=200,
1448 insecure=True,
1449 sni=True,
1450 minTLSv="v1.3",
1451 maxTLSv="v1.3",
1452 error=[
1453 "tls: server selected unsupported protocol version 303",
1454 "tls: no supported versions satisfy MinVersion and MaxVersion",
1455 "tls: protocol version not supported",
1456 "read: connection reset by peer",
1457 ],
1458 ) # The TLS inspector just closes the connection. Wow.
1459
1460 def check(self):
1461 assert self.results[0].backend
1462 assert self.results[0].backend.request
1463 tls_0_version = self.results[0].backend.request.tls.negotiated_protocol_version
1464 assert self.results[1].backend
1465 assert self.results[1].backend.request
1466 tls_1_version = self.results[1].backend.request.tls.negotiated_protocol_version
1467
1468 # See comment in queries for why these are None. They should be v1.2 and v1.1 respectively.
1469 assert tls_0_version == None, f"requesting TLS v1.2 got TLS {tls_0_version}"
1470 assert tls_1_version == None, f"requesting TLS v1.0-v1.1 got TLS {tls_1_version}"
1471
1472 def requirements(self):
1473 # We're replacing super()'s requirements deliberately here. Without a Host header they can't work.
1474 yield (
1475 "url",
1476 Query(
1477 self.url("ambassador/v0/check_ready"),
1478 headers={"Host": "tls-context-host-1"},
1479 insecure=True,
1480 sni=True,
1481 minTLSv="v1.2",
1482 ),
1483 )
1484 yield (
1485 "url",
1486 Query(
1487 self.url("ambassador/v0/check_alive"),
1488 headers={"Host": "tls-context-host-1"},
1489 insecure=True,
1490 sni=True,
1491 minTLSv="v1.2",
1492 ),
1493 )
1494
1495
1496class TLSContextProtocolMinVersion(AmbassadorTest):
1497 # Here we're testing that the client can't drop below the minimum TLS version
1498 # configured.
1499 #
1500 # XXX 2019-09-11: vet that the test client's support for TLS v1.3 is up-to-date.
1501 # It appears not to be.
1502
1503 # debug = True
1504
1505 def init(self):
1506 self.xfail = "FIXME: IHA"
1507 self.target = HTTP()
1508
1509 def manifests(self) -> str:
1510 return (
1511 f"""
1512---
1513apiVersion: v1
1514data:
1515 tls.crt: {TLSCerts["tls-context-host-1"].k8s_crt}
1516 tls.key: {TLSCerts["tls-context-host-1"].k8s_key}
1517kind: Secret
1518metadata:
1519 name: secret.min-version
1520 labels:
1521 kat-ambassador-id: tlscontextprotocolminversion
1522type: kubernetes.io/tls
1523"""
1524 + super().manifests()
1525 )
1526
1527 def config(self) -> Generator[Union[str, Tuple[Node, str]], None, None]:
1528 yield self, self.format(
1529 """
1530---
1531apiVersion: getambassador.io/v3alpha1
1532kind: Mapping
1533name: {self.name}-same-prefix-1
1534prefix: /tls-context-same/
1535service: https://{self.target.path.fqdn}
1536host: tls-context-host-1
1537---
1538apiVersion: getambassador.io/v3alpha1
1539kind: TLSContext
1540name: {self.name}-same-context-1
1541hosts:
1542- tls-context-host-1
1543secret: secret.min-version
1544secret_namespacing: False
1545min_tls_version: v1.2
1546max_tls_version: v1.3
1547"""
1548 )
1549
1550 def scheme(self) -> str:
1551 return "https"
1552
1553 @staticmethod
1554 def _go_close_connection_error(url):
1555 """
1556 :param url: url passed to the query
1557 :return: error message string that Go's net/http package throws when server closes connection
1558 """
1559 return "Get {}: EOF".format(url)
1560
1561 def queries(self):
1562 # This should give v1.3, but it currently seems to give 1.2.
1563 yield Query(
1564 self.url("tls-context-same/"),
1565 headers={"Host": "tls-context-host-1"},
1566 expected=200,
1567 insecure=True,
1568 sni=True,
1569 minTLSv="v1.2",
1570 maxTLSv="v1.3",
1571 )
1572
1573 # This should give v1.2
1574 yield Query(
1575 self.url("tls-context-same/"),
1576 headers={"Host": "tls-context-host-1"},
1577 expected=200,
1578 insecure=True,
1579 sni=True,
1580 minTLSv="v1.1",
1581 maxTLSv="v1.2",
1582 )
1583
1584 # This should be an error.
1585 yield Query(
1586 self.url("tls-context-same/"),
1587 headers={"Host": "tls-context-host-1"},
1588 expected=200,
1589 insecure=True,
1590 sni=True,
1591 minTLSv="v1.0",
1592 maxTLSv="v1.0",
1593 error=[
1594 "tls: server selected unsupported protocol version 303",
1595 "tls: no supported versions satisfy MinVersion and MaxVersion",
1596 "tls: protocol version not supported",
1597 ],
1598 )
1599
1600 def check(self):
1601 assert self.results[0].backend
1602 assert self.results[0].backend.request
1603 tls_0_version = self.results[0].backend.request.tls.negotiated_protocol_version
1604 assert self.results[1].backend
1605 assert self.results[1].backend.request
1606 tls_1_version = self.results[1].backend.request.tls.negotiated_protocol_version
1607
1608 # Hmmm. Why does Envoy prefer 1.2 to 1.3 here?? This may be a client thing -- have to
1609 # rebuild with Go 1.13.
1610 assert tls_0_version == "v1.2", f"requesting TLS v1.2-v1.3 got TLS {tls_0_version}"
1611 assert tls_1_version == "v1.2", f"requesting TLS v1.1-v1.2 got TLS {tls_1_version}"
1612
1613 def requirements(self):
1614 # We're replacing super()'s requirements deliberately here. Without a Host header they can't work.
1615 yield (
1616 "url",
1617 Query(
1618 self.url("ambassador/v0/check_ready"),
1619 headers={"Host": "tls-context-host-1"},
1620 insecure=True,
1621 sni=True,
1622 ),
1623 )
1624 yield (
1625 "url",
1626 Query(
1627 self.url("ambassador/v0/check_alive"),
1628 headers={"Host": "tls-context-host-1"},
1629 insecure=True,
1630 sni=True,
1631 ),
1632 )
1633
1634
1635class TLSContextCipherSuites(AmbassadorTest):
1636 # debug = True
1637
1638 def init(self):
1639 self.xfail = "FIXME: IHA"
1640 self.target = HTTP()
1641
1642 def manifests(self) -> str:
1643 return (
1644 f"""
1645---
1646apiVersion: v1
1647data:
1648 tls.crt: {TLSCerts["tls-context-host-1"].k8s_crt}
1649 tls.key: {TLSCerts["tls-context-host-1"].k8s_key}
1650kind: Secret
1651metadata:
1652 name: secret.cipher-suites
1653 labels:
1654 kat-ambassador-id: tlscontextciphersuites
1655type: kubernetes.io/tls
1656"""
1657 + super().manifests()
1658 )
1659
1660 def config(self) -> Generator[Union[str, Tuple[Node, str]], None, None]:
1661 yield self, self.format(
1662 """
1663---
1664apiVersion: getambassador.io/v3alpha1
1665kind: Mapping
1666name: {self.name}-same-prefix-1
1667prefix: /tls-context-same/
1668service: https://{self.target.path.fqdn}
1669host: tls-context-host-1
1670"""
1671 )
1672 yield self, self.format(
1673 """
1674---
1675apiVersion: getambassador.io/v3alpha1
1676kind: TLSContext
1677name: {self.name}-same-context-1
1678hosts:
1679- tls-context-host-1
1680secret: secret.cipher-suites
1681secret_namespacing: False
1682max_tls_version: v1.2
1683cipher_suites:
1684- ECDHE-RSA-AES128-GCM-SHA256
1685ecdh_curves:
1686- P-256
1687"""
1688 )
1689
1690 def scheme(self) -> str:
1691 return "https"
1692
1693 @staticmethod
1694 def _go_close_connection_error(url):
1695 """
1696 :param url: url passed to the query
1697 :return: error message string that Go's net/http package throws when server closes connection
1698 """
1699 return "Get {}: EOF".format(url)
1700
1701 def queries(self):
1702 yield Query(
1703 self.url("tls-context-same/"),
1704 headers={"Host": "tls-context-host-1"},
1705 expected=200,
1706 insecure=True,
1707 sni=True,
1708 cipherSuites=["TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256"],
1709 maxTLSv="v1.2",
1710 )
1711
1712 yield Query(
1713 self.url("tls-context-same/"),
1714 headers={"Host": "tls-context-host-1"},
1715 expected=200,
1716 insecure=True,
1717 sni=True,
1718 cipherSuites=["TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256"],
1719 maxTLSv="v1.2",
1720 error="tls: handshake failure",
1721 )
1722
1723 yield Query(
1724 self.url("tls-context-same/"),
1725 headers={"Host": "tls-context-host-1"},
1726 expected=200,
1727 insecure=True,
1728 sni=True,
1729 cipherSuites=["TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256"],
1730 ecdhCurves=["X25519"],
1731 maxTLSv="v1.2",
1732 error="tls: handshake failure",
1733 )
1734
1735 def check(self):
1736 assert self.results[0].backend
1737 assert self.results[0].backend.request
1738 tls_0_version = self.results[0].backend.request.tls.negotiated_protocol_version
1739
1740 assert tls_0_version == "v1.2", f"requesting TLS v1.2 got TLS {tls_0_version}"
1741
1742 def requirements(self):
1743 yield (
1744 "url",
1745 Query(
1746 self.url("ambassador/v0/check_ready"),
1747 headers={"Host": "tls-context-host-1"},
1748 insecure=True,
1749 sni=True,
1750 ),
1751 )
1752 yield (
1753 "url",
1754 Query(
1755 self.url("ambassador/v0/check_alive"),
1756 headers={"Host": "tls-context-host-1"},
1757 insecure=True,
1758 sni=True,
1759 ),
1760 )
1761
1762
1763class TLSContextIstioSecretTest(AmbassadorTest):
1764 # debug = True
1765
1766 def init(self):
1767 self.target = HTTP()
1768
1769 if EDGE_STACK:
1770 self.xfail = "XFailing for now"
1771
1772 def manifests(self) -> str:
1773 return (
1774 namespace_manifest("secret-namespace")
1775 + """
1776---
1777apiVersion: v1
1778data:
1779 cert-chain.pem: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSURJVENDQWdtZ0F3SUJBZ0lSQU8wbFh1OVhOYkNrejJJTEhiYlVBbDh3RFFZSktvWklodmNOQVFFTEJRQXcKR0RFV01CUUdBMVVFQ2hNTlkyeDFjM1JsY2k1c2IyTmhiREFlRncweU1EQXhNakF4TmpReE5EbGFGdzB5TURBMApNVGt4TmpReE5EbGFNQUF3Z2dFaU1BMEdDU3FHU0liM0RRRUJBUVVBQTRJQkR3QXdnZ0VLQW9JQkFRQ3h2RWxuCmd6SldTejR6RGM5TE5od0xCZm1nTStlY3k0T096UEFtSGhnZER2RFhLVE40Qll0bS8veTFRT2tGNG9JeHVMVnAKYW5ULzdHdUJHNzlrbUg1TkpkcWhzV0c1b1h0TWpiZnZnZFJ6dW50UVg1OFI5d0pWT2YwNlo4dHFUYmE4VVI3YQpYZFY1c2VSbGtINU1VWmhVNXkxNzA1ZVNycVBROGVBd1hiazdOejNlTUd4Ujc1NjZOK3g2UDIrcEZmTDF1dEJ3CnRhSVVpYlVNR0liODcwYmtxVmlzSHQ1aC95blkrV3FlclJLREhTLzVRQlZiMytZSXd4N3o1b3FPbDBvZ05YODkKVnlzNFM0NzdXNDBPWGRZaStHeGwwKzFVT2F3NEw2a0tTaWhjVTZJUm1YbWhiUXpRb0VvazN6TDNaR2hWS3FhbwpUaFdqTVhrMkZxS1pNSnBCQWdNQkFBR2pmakI4TUE0R0ExVWREd0VCL3dRRUF3SUZvREFkQmdOVkhTVUVGakFVCkJnZ3JCZ0VGQlFjREFRWUlLd1lCQlFVSEF3SXdEQVlEVlIwVEFRSC9CQUl3QURBOUJnTlZIUkVCQWY4RU16QXgKaGk5emNHbG1abVU2THk5amJIVnpkR1Z5TG14dlkyRnNMMjV6TDJGdFltRnpjMkZrYjNJdmMyRXZaR1ZtWVhWcwpkREFOQmdrcWhraUc5dzBCQVFzRkFBT0NBUUVBaHQ3c1dSOHEzeFNaM1BsTGFnS0REc1c2UlYyRUJCRkNhR08rCjlJb2lrQXZTdTV2b3VKS3EzVHE0WU9LRzJnbEpvVSs1c2lmL25DYzFva1ZTakNJSnh1UVFhdzd5QkV0WWJaZkYKSXI2WEkzbUtCVC9kWHpOM00yL1g4Q3RBNHI5SFQ4VmxmMitJMHNqb01hVE80WHdPNVQ5eXdoREJXdzdrdThVRApnMjdzTFlHVy9UNzIvT0JGUEcxa2VlRUpva3BhSXZQOVliWS9qSlRWZVVIYk1FODVOckJFMWNndUVnSlVod1VKCkhiam4xcEFKMHZsUWZrVW9mT3VRZkFtZGpHWjc2N2phOE5ldHZBdk9tRExPV2dzQWM4KzRsRXBKVURwcmhlVEoKazBrSFh6cUMyTzN4a250U0QxM2FFa2VUMXJocjM3MXc1OTVJUjgvR1llSis3a3JqRkE9PQotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0tCg==
1780 key.pem: LS0tLS1CRUdJTiBSU0EgUFJJVkFURSBLRVktLS0tLQpNSUlFb2dJQkFBS0NBUUVBc2J4Slo0TXlWa3MrTXczUFN6WWNDd1g1b0RQbm5NdURqc3p3Smg0WUhRN3cxeWt6CmVBV0xadi84dFVEcEJlS0NNYmkxYVdwMC8reHJnUnUvWkpoK1RTWGFvYkZodWFGN1RJMjM3NEhVYzdwN1VGK2YKRWZjQ1ZUbjlPbWZMYWsyMnZGRWUybDNWZWJIa1paQitURkdZVk9jdGU5T1hrcTZqMFBIZ01GMjVPemM5M2pCcwpVZStldWpmc2VqOXZxUlh5OWJyUWNMV2lGSW0xREJpRy9POUc1S2xZckI3ZVlmOHAyUGxxbnEwU2d4MHYrVUFWClc5L21DTU1lOCthS2pwZEtJRFYvUFZjck9FdU8rMXVORGwzV0l2aHNaZFB0VkRtc09DK3BDa29vWEZPaUVabDUKb1cwTTBLQktKTjh5OTJSb1ZTcW1xRTRWb3pGNU5oYWltVENhUVFJREFRQUJBb0lCQUI1bXdIK09OMnYvVHRKWQp5RjVyRVB6cHRyc3FaYkd5TmZ5VkhYYkhxd1E5YkFEQnNXWVVQTFlQajJCSmpCSlBua2wyK01EaFRzWC80SnVpCjdXZjlsWTBJcm83OTBtTjROYWp3ak1mUkExQVFVOHQ1cjdIWStITXZpaHNWYWZ2eTh4RGZKMUhldndjajRKZG0KMGRPb0dWQmNnckV0amoydTFhS0YzUDBvNnVndno2SmtSWld2SjZ4SGlya0NETk5MWlpzbHB5UzFHRjZmYm9aTwp1SmFTLzc2S25JS1FQT3hCaE83ME80WHF6am5wMVk1UzduTjRoM1Z2RmVPREcvQ2pWaGhOcE4xV0NadFNvSXBwCk9XOVdONVRvUnZhVDhnelljcG9TOEMzYXVqSzVvV1FiVzdRZys2NXRoWGNqcFpRM0VFSnNaLzNsTWRsbGE3TFcKT2k3Vkhpa0NnWUVBeHBUQjZodnBRcnNXUUhjcDhRdG94RitNUThVL2l5WjZ6dU5BNHZyWFdwUlFDVVg4d1ZiRwowTFNZN1lSVGhuOGtUZ09vWlNWMU9VcThUTjlnOG91UUh6bS9ta1FpV0p0bnNXWGJtNjF3SFozaWNlQ1FxWDU4CmoyUjM2eXBONGpuUENPREVwcDVKWExZLzNFTnZnYTBxSm9ZVWp4UUpHZDgyWUxKRmJrMHZmTzhDZ1lFQTVTQ0MKcHJTR0NBL0dUVkY4MjRmaW1YTkNMcllOVmV1TStqZFdqQUFBZkQzWHpUK1JWeFZsTENTVUluQUdtYjh2djZlcApreHYrdWlBZTg2TDBhUVVDTENSRFF2SjR3MnNPRWkwWWMwTGlKUGdBN1JLeFhwVGUrQ09vS1VmcTZyVi96TTdNCmhCbWtDT2ZoUnRDT3NENGNBcWQ0MzluQjZBVm01K21VV0FqNHU4OENnWUJFTXBSQi9TSG5xKzZoWndzOVgraTAKQUFoZ3dkM253T2hPSXRlRzNCU1hZL1gwcVZkN1luelc4aDdPK3pIZ0w4dmRDdjZLOWdsRENycU9QK3pBZjFPWQpsYkdLbmptWmFvMTY2L3MyaEtMTFdReUtoVS9KRmNwYlNHcXlsWTIzMHBpYWVPNndOZzRGekFVMGRPaFhoWXZEClBTclVWRkluMDNPT1U4cnFiWkdRZXdLQmdFMVJPaVZNOTRtUzRTVElJYXptM3NWUFNuNyt1ZU5MZUNnYk1sNU4KeGR3bTlrSnhkL2I5NWtVT0Z0ckVHTVlhNk43d2tkMXRiZmlhekRjRXZ4c05NSjE2b3lQZE5Ia2xEL3Q4TWlyNgozOXIvd1RnK3ZaR2dCTm1SRnJiUGFPdEkwZFpuMWtXaGJXUC84MW4xR0tGS1pDTlZKZ25Mcm80Ly9HaTN2bkl5Cm5OU3JBb0dBVGVidmRLamtENk5XQmJMWSsrVUVQN1ZLd0pOWlR1VWUvT0FBdlJIKzZaWEV4SkhtM3pjV280TVkKMG8vL2dyNzhBdDM4NEk5QVBwMnQwV3lmTmlaTStWUFh4a1lKTU5IU01mcXdGcVRVSmE3NGttNVUrYnB4Mm1ueAovUlR6aElHMDE4SXN3NHBGeUZ4ekpTSVdCK2VpVEF6NFZsMEw2ZU0yNUp5R3lyU2x0Q2M9Ci0tLS0tRU5EIFJTQSBQUklWQVRFIEtFWS0tLS0tCg==
1781 root-cert.pem: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUMzVENDQWNXZ0F3SUJBZ0lRVGRHUmJPampxWjBEZDUxOVJqdXlzREFOQmdrcWhraUc5dzBCQVFzRkFEQVkKTVJZd0ZBWURWUVFLRXcxamJIVnpkR1Z5TG14dlkyRnNNQjRYRFRJd01ERXlNREUyTkRBek5Gb1hEVE13TURFeApOekUyTkRBek5Gb3dHREVXTUJRR0ExVUVDaE1OWTJ4MWMzUmxjaTVzYjJOaGJEQ0NBU0l3RFFZSktvWklodmNOCkFRRUJCUUFEZ2dFUEFEQ0NBUW9DZ2dFQkFMNzhadlRtQ2hxYUM5Z0lFUFlWSWYrVkFsU0tJR2JsdktvUUJNNmwKWlNBTmxNQXg3elJQTjFQdVMrV2I5M1hxMXNzN1hEUEY4UmlIL2dCWE05aGZsNUpGTDErbmlLYWR3RHh5UUdXQQpPMUFBQXNmZlpud3NkWDhDOGdCcE5zUkVZYVo5SzExdDI5NmV5WUc1d3ozMW9rZVFYSTVrSU0vdWgxL2wwN3pKClU3eG8zSmVZbHpMZnJSVWhNRnc1Vk5ETkNCY3JldEoyOWgvZzRpS1plM2JDS3laVmJRUkN3VjR5ck12YTA4Z3kKYzRhSGJud1VtRThKT0JvcE5abW1uOHc0bFcwQjFsS1Q3aFhBRldJdW55WVhIOWFabUJJd1pPVk9kV0N4SmZnTQpKSWY1UVJSY0s5MVZGMjYvcUp2RHlwaVpxcHFJcEdQWHJHbHF2dGtTSmwxdHhYMENBd0VBQWFNak1DRXdEZ1lEClZSMFBBUUgvQkFRREFnSUVNQThHQTFVZEV3RUIvd1FGTUFNQkFmOHdEUVlKS29aSWh2Y05BUUVMQlFBRGdnRUIKQUpjWXl3WkoxeUZpQzRpT0xNbXY4MTZYZEhUSWdRTGlLaXNBdGRqb21TdXhLc0o1eXZ3M2lGdkROSklseEQ4SgoyVVROR2JJTFN2d29qQ1JzQVcyMlJtelpjZG95SXkvcFVIR25EVUpiMk14T0svaEVWU0x4cnN6RHlEK2YwR1liCjdhL1Q2ZmJFbUdYK0JHTnBKZ2lTKytwUm5JMzE3THN6aldtTUlmbVF3T1NtZXNvKzhMSXAxZS9STGVKcThoM0cKREZzcVA4c1BLaHNEM1M1RWNGYU5vSVg4OThVK3UvUWlKd3BoS2lDK3RRRzExeGJZanMxaURNcFJpUGsvSi9NRwpiaTZnQm8zZGdjZ1RWWFdOY2YzeHRiQWErMmkzK3k1V25ydHoyK1d4ZG96cEhpN3FLL1BEbGpwVG5JdkY2Nm0wCjBFYVA0T3ZOY29hNk12MUpoYkFVK0w0PQotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0tCg==
1782kind: Secret
1783metadata:
1784 name: istio.test-tlscontext-istio-secret-1
1785 namespace: secret-namespace
1786 labels:
1787 kat-ambassador-id: tlscontextistiosecret
1788type: istio.io/key-and-cert
1789"""
1790 + super().manifests()
1791 )
1792
1793 def config(self) -> Generator[Union[str, Tuple[Node, str]], None, None]:
1794 yield self, self.format(
1795 """
1796---
1797apiVersion: getambassador.io/v3alpha1
1798kind: Mapping
1799name: {self.name}-istio-prefix-1
1800prefix: /tls-context-istio/
1801service: https://{self.target.path.fqdn}
1802tls: {self.name}-istio-context-1
1803"""
1804 )
1805 yield self, self.format(
1806 """
1807---
1808apiVersion: getambassador.io/v3alpha1
1809kind: TLSContext
1810name: {self.name}-istio-context-1
1811secret: istio.test-tlscontext-istio-secret-1
1812namespace: secret-namespace
1813secret_namespacing: False
1814"""
1815 )
1816
1817 def queries(self):
1818 yield Query(self.url("ambassador/v0/diag/?json=true&filter=errors"), phase=2)
1819
1820 def check(self):
1821 assert (
1822 self.results[0].backend is None
1823 ), f"expected 0 errors, got {len(self.results[0].backend.response)}: received {self.results[0].backend.response}"
1824
1825
1826class TLSCoalescing(AmbassadorTest):
1827 def init(self):
1828 self.target = HTTP()
1829
1830 if EDGE_STACK:
1831 self.xfail = "Not yet supported in Edge Stack"
1832
1833 self.xfail = "FIXME: IHA"
1834
1835 def manifests(self) -> str:
1836 return (
1837 f"""
1838---
1839apiVersion: v1
1840metadata:
1841 name: tlscoalescing-certs
1842 labels:
1843 kat-ambassador-id: tlscoalescing
1844data:
1845 tls.crt: {TLSCerts["*.domain.com"].k8s_crt}
1846 tls.key: {TLSCerts["*.domain.com"].k8s_key}
1847kind: Secret
1848type: kubernetes.io/tls
1849"""
1850 + super().manifests()
1851 )
1852
1853 def config(self) -> Generator[Union[str, Tuple[Node, str]], None, None]:
1854 yield self, self.format(
1855 """
1856apiVersion: getambassador.io/v3alpha1
1857kind: TLSContext
1858name: tlscoalescing-context
1859secret: tlscoalescing-certs
1860alpn_protocols: h2, http/1.1
1861hosts:
1862- domain.com
1863- a.domain.com
1864- b.domain.com
1865"""
1866 )
1867
1868 def scheme(self) -> str:
1869 return "https"
1870
1871 @staticmethod
1872 def _go_close_connection_error(url):
1873 """
1874 :param url: url passed to the query
1875 :return: error message string that Go's net/http package throws when server closes connection
1876 """
1877 return "Get {}: EOF".format(url)
1878
1879 def queries(self):
1880 yield Query(
1881 self.url("ambassador/v0/diag/"),
1882 headers={"Host": "a.domain.com"},
1883 insecure=True,
1884 sni=True,
1885 )
1886 yield Query(
1887 self.url("ambassador/v0/diag/"),
1888 headers={"Host": "b.domain.com"},
1889 insecure=True,
1890 sni=True,
1891 )
1892
1893 def requirements(self):
1894 yield ("url", Query(self.url("ambassador/v0/check_ready"), insecure=True, sni=True))
1895
1896
1897class TLSInheritFromModule(AmbassadorTest):
1898 target: ServiceType
1899
1900 def init(self):
1901 self.xfail = "FIXME: IHA"
1902 self.edge_stack_cleartext_host = False
1903 self.target = HTTP()
1904
1905 def config(self) -> Generator[Union[str, Tuple[Node, str]], None, None]:
1906 # These are annotations instead of resources because the name matters.
1907 yield self, self.format(
1908 """
1909---
1910apiVersion: getambassador.io/v3alpha1
1911kind: Module
1912name: tls
1913ambassador_id: [{self.ambassador_id}]
1914config:
1915 server:
1916 enabled: True
1917 redirect_cleartext_from: 8080
1918"""
1919 )
1920
1921 def manifests(self) -> str:
1922 return (
1923 self.format(
1924 """
1925---
1926apiVersion: getambassador.io/v3alpha1
1927kind: TLSContext
1928metadata:
1929 name: {self.path.k8s}
1930spec:
1931 ambassador_id: [ {self.ambassador_id} ]
1932 alpn_protocols: "h2,http/1.1"
1933 hosts:
1934 - a.domain.com
1935 secret: {self.path.k8s}
1936---
1937apiVersion: v1
1938kind: Secret
1939metadata:
1940 name: {self.path.k8s}
1941 labels:
1942 kat-ambassador-id: {self.ambassador_id}
1943type: kubernetes.io/tls
1944data:
1945 tls.crt: """
1946 + TLSCerts["a.domain.com"].k8s_crt
1947 + """
1948 tls.key: """
1949 + TLSCerts["a.domain.com"].k8s_key
1950 + """
1951---
1952apiVersion: getambassador.io/v3alpha1
1953kind: Mapping
1954metadata:
1955 name: {self.path.k8s}-target-mapping
1956spec:
1957 ambassador_id: [ {self.ambassador_id} ]
1958 prefix: /foo
1959 service: {self.target.path.fqdn}
1960"""
1961 )
1962 + super().manifests()
1963 )
1964
1965 def scheme(self) -> str:
1966 return "https"
1967
1968 def queries(self):
1969 yield Query(self.url("foo", scheme="http"), headers={"Host": "a.domain.com"}, expected=301)
1970 yield Query(
1971 self.url("bar", scheme="http"),
1972 headers={"Host": "a.domain.com"},
1973 expected=(404 if bug_404_routes else 301),
1974 )
1975 yield Query(
1976 self.url("foo", scheme="https"),
1977 headers={"Host": "a.domain.com"},
1978 ca_cert=TLSCerts["a.domain.com"].pubcert,
1979 sni=True,
1980 expected=200,
1981 )
1982 yield Query(
1983 self.url("bar", scheme="https"),
1984 headers={"Host": "a.domain.com"},
1985 ca_cert=TLSCerts["a.domain.com"].pubcert,
1986 sni=True,
1987 expected=404,
1988 )
1989
1990 def requirements(self):
1991 for r in super().requirements():
1992 query = r[1]
1993 query.headers = {"Host": "a.domain.com"}
1994 query.sni = (
1995 True # Use query.headers["Host"] instead of urlparse(query.url).hostname for SNI
1996 )
1997 query.ca_cert = TLSCerts["a.domain.com"].pubcert
1998 yield (r[0], query)
View as plain text