1from typing import Generator, Tuple, Union
2
3from abstract_tests import HTTP, AmbassadorTest, Node, ServiceType
4from kat.harness import Query
5
6################
7# NOTE: The IPAllow and IPDeny tests are not entirely straightforward. In
8# particular:
9#
10# 1. They currently use an annotation for their Ambassador modules to keep
11# them distinct between the two tests. If you don't like annotations for
12# this, you'll have to set up separate namespaces.
13#
14# 2. Our Listener _must_ set l7Depth == 1 in order for the tests to work:
15#
16# - When we hit /target/ with XFF "99.99.0.1", Envoy receives exactly that.
17# Since l7Depth is 1, Envoy accepts that as the valid address
18# of the remote end of the connection, RBAC accepts that as matching the
19# 99.99.0.0/16 CIDR block, and the request is allowed or denied as
20# appropriate. Great. But when it's accepted, the rules for XFF are that
21# Envoy must append the peer address to the XFF list before forwarding, so
22# the upstream sees XFF "99.99.0.1,$katIP". In the /target/ case, the
23# upstream is a KAT backend HTTP service -- it doesn't care about XFF, and
24# just responds OK.
25#
26# - When we hit /localhost/ with XFF "99.99.0.1", though, _Ambassador is the
27# upstream_. So everything up to rewriting XFF as "99.99.0.1,$katIP" is the
28# same, but Envoy hands that upstream to... itself. Since l7Depth is still 1,
29# Envoy throws away the 99.99.0.1 part and believes that the connection is
30# coming from $katIP, which does _not_ match the 99.99.0.0/16 CIDR block --
31# but the raw peer address _is_ in fact 127.0.0.1, so _that_ matches the
32# peer: 127.0.0.1 principal.
33
34
35class IPAllow(AmbassadorTest):
36 target: ServiceType
37
38 def init(self):
39 self.target = HTTP()
40 self.add_default_http_listener = False
41 self.add_default_https_listener = False
42
43 def manifests(self) -> str:
44 return (
45 self.format(
46 """
47---
48apiVersion: getambassador.io/v3alpha1
49kind: Listener
50metadata:
51 name: {self.path.k8s}-listener
52 labels:
53 kat-ambassador-id: {self.ambassador_id}
54spec:
55 ambassador_id: [ {self.ambassador_id} ]
56 port: 8080
57 protocol: HTTP
58 securityModel: XFP
59 hostBinding:
60 namespace:
61 from: ALL
62 # Allow one trusted hop, so that KAT can fake addresses with XFF (see NOTE above).
63 l7Depth: 1
64---
65apiVersion: getambassador.io/v3alpha1
66kind: Mapping
67metadata:
68 name: {self.path.k8s}-target-mapping
69spec:
70 ambassador_id: [{self.ambassador_id}]
71 hostname: "*"
72 prefix: /target/
73 service: {self.target.path.fqdn}
74---
75apiVersion: getambassador.io/v3alpha1
76kind: Mapping
77metadata:
78 name: {self.path.k8s}-localhost-mapping
79spec:
80 ambassador_id: [{self.ambassador_id}]
81 hostname: "*"
82 prefix: /localhost/
83 rewrite: /target/ # See NOTE above
84 service: 127.0.0.1:8080 # See NOTE above
85"""
86 )
87 + super().manifests()
88 )
89
90 def config(self) -> Generator[Union[str, Tuple[Node, str]], None, None]:
91 yield self, self.format(
92 """
93---
94apiVersion: getambassador.io/v3alpha1
95kind: Module
96name: ambassador
97ambassador_id: [{self.ambassador_id}]
98config:
99 ip_allow:
100 - peer: 127.0.0.1 # peer address must be localhost
101 - remote: 99.99.0.0/16 # honors PROXY and XFF
102"""
103 )
104
105 def queries(self):
106 # 0. Straightforward: hit /target/ and /localhost/ with nothing special, get 403s.
107 yield Query(self.url("target/00"), expected=403)
108 yield Query(self.url("localhost/01"), expected=403)
109
110 # 1. Hit /target/ and /localhost/ with X-Forwarded-For specifying something good, get 200s.
111 yield Query(self.url("target/10"), headers={"X-Forwarded-For": "99.99.0.1"})
112 yield Query(self.url("localhost/11"), headers={"X-Forwarded-For": "99.99.0.1"})
113
114 # 2. Hit /target/ and /localhost/ with X-Forwarded-For specifying something bad, get a 403.
115 yield Query(self.url("target/20"), headers={"X-Forwarded-For": "99.98.0.1"}, expected=403)
116 yield Query(
117 self.url("localhost/21"), headers={"X-Forwarded-For": "99.98.0.1"}, expected=403
118 )
119
120 # Done. Note that the /localhost/ endpoint is wrapping around to make a localhost call back
121 # to Ambassador to check the peer: principal -- see the NOTE above.
122
123 def requirements(self):
124 # We're replacing super()'s requirements deliberately here. Without X-Forwarded-For they can't work.
125 yield (
126 "url",
127 Query(self.url("ambassador/v0/check_ready"), headers={"X-Forwarded-For": "99.99.0.1"}),
128 )
129 yield (
130 "url",
131 Query(self.url("ambassador/v0/check_alive"), headers={"X-Forwarded-For": "99.99.0.1"}),
132 )
133
134
135class IPDeny(AmbassadorTest):
136 target: ServiceType
137
138 def init(self):
139 self.target = HTTP()
140 self.add_default_http_listener = False
141 self.add_default_https_listener = False
142
143 def manifests(self) -> str:
144 return (
145 self.format(
146 """
147---
148apiVersion: getambassador.io/v3alpha1
149kind: Listener
150metadata:
151 name: {self.path.k8s}-listener
152 labels:
153 kat-ambassador-id: {self.ambassador_id}
154spec:
155 ambassador_id: [ {self.ambassador_id} ]
156 port: 8080
157 protocol: HTTP
158 securityModel: XFP
159 hostBinding:
160 namespace:
161 from: ALL
162 # Allow one trusted hop, so that KAT can fake addresses with XFF (see NOTE above).
163 l7Depth: 1
164---
165apiVersion: getambassador.io/v3alpha1
166kind: Mapping
167metadata:
168 name: {self.path.k8s}-target-mapping
169spec:
170 ambassador_id: [{self.ambassador_id}]
171 hostname: "*"
172 prefix: /target/
173 service: {self.target.path.fqdn}
174---
175apiVersion: getambassador.io/v3alpha1
176kind: Mapping
177metadata:
178 name: {self.path.k8s}-localhost-mapping
179spec:
180 ambassador_id: [{self.ambassador_id}]
181 hostname: "*"
182 prefix: /localhost/
183 rewrite: /target/ # See NOTE above
184 service: 127.0.0.1:8080 # See NOTE above
185"""
186 )
187 + super().manifests()
188 )
189
190 def config(self) -> Generator[Union[str, Tuple[Node, str]], None, None]:
191 yield self, self.format(
192 """
193---
194apiVersion: getambassador.io/v3alpha1
195kind: Module
196name: ambassador
197ambassador_id: [{self.ambassador_id}]
198config:
199 ip_deny:
200 - peer: 127.0.0.1 # peer address cannot be localhost (weird, huh?)
201 - remote: 99.98.0.0/16 # honors PROXY and XFF
202"""
203 )
204
205 def queries(self):
206 # 0. Straightforward: hit /target/ and /localhost/ with nothing special, get 403s.
207 yield Query(self.url("target/00"), expected=200)
208 yield Query(self.url("localhost/01"), expected=403) # This should _never_ work.
209
210 # 1. Hit /target/ and /localhost/ with X-Forwarded-For specifying something bad, get 403s.
211 yield Query(self.url("target/10"), headers={"X-Forwarded-For": "99.98.0.1"}, expected=403)
212 yield Query(
213 self.url("localhost/11"), headers={"X-Forwarded-For": "99.98.0.1"}, expected=403
214 )
215
216 # 2. Hit /target/ with X-Forwarded-For specifying something not so bad, get a 200. /localhost/
217 # will _still_ get a 403 though.
218 yield Query(self.url("target/20"), headers={"X-Forwarded-For": "99.99.0.1"}, expected=200)
219 yield Query(
220 self.url("localhost/21"), headers={"X-Forwarded-For": "99.99.0.1"}, expected=403
221 )
222
223 # Done. Note that the /localhost/ endpoint is wrapping around to make a localhost call back
224 # to Ambassador to check the peer: principal -- see the NOTE above.
225
226 def requirements(self):
227 # We're replacing super()'s requirements deliberately here. Without X-Forwarded-For they can't work.
228 yield (
229 "url",
230 Query(self.url("ambassador/v0/check_ready"), headers={"X-Forwarded-For": "99.99.0.1"}),
231 )
232 yield (
233 "url",
234 Query(self.url("ambassador/v0/check_alive"), headers={"X-Forwarded-For": "99.99.0.1"}),
235 )
View as plain text