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