1use kube::ResourceExt;
2use linkerd_policy_controller_k8s_api::{
3 self as k8s,
4 policy::{LocalTargetRef, NamespacedTargetRef},
5};
6use linkerd_policy_test::{
7 await_condition, create, create_ready_pod, curl, endpoints_ready, web, with_temp_ns,
8 LinkerdInject,
9};
10
11#[tokio::test(flavor = "current_thread")]
12async fn meshtls() {
13 with_temp_ns(|client, ns| async move {
14 // First create all of the policies we'll need so that the web pod
15 // starts up with the correct policy (to prevent races).
16 //
17 // The policy requires that all connections are authenticated with MeshTLS.
18 let (srv, all_mtls) = tokio::join!(
19 create(&client, web::server(&ns)),
20 create(&client, all_authenticated(&ns))
21 );
22 create(
23 &client,
24 authz_policy(
25 &ns,
26 "web",
27 LocalTargetRef::from_resource(&srv),
28 Some(NamespacedTargetRef::from_resource(&all_mtls)),
29 ),
30 )
31 .await;
32
33 // Create the web pod and wait for it to be ready.
34 tokio::join!(
35 create(&client, web::service(&ns)),
36 create_ready_pod(&client, web::pod(&ns))
37 );
38
39 let curl = curl::Runner::init(&client, &ns).await;
40 let (injected, uninjected) = tokio::join!(
41 curl.run("curl-injected", "http://web", LinkerdInject::Enabled),
42 curl.run("curl-uninjected", "http://web", LinkerdInject::Disabled),
43 );
44 let (injected_status, uninjected_status) =
45 tokio::join!(injected.exit_code(), uninjected.exit_code());
46 assert_eq!(
47 injected_status, 0,
48 "uninjected curl must fail to contact web"
49 );
50 assert_ne!(uninjected_status, 0, "injected curl must contact web");
51 })
52 .await;
53}
54
55#[tokio::test(flavor = "current_thread")]
56async fn targets_route() {
57 with_temp_ns(|client, ns| async move {
58 // First create all of the policies we'll need so that the web pod
59 // starts up with the correct policy (to prevent races).
60 //
61 // The policy requires that all connections are authenticated with MeshTLS.
62 let (srv, all_mtls) = tokio::join!(
63 create(&client, web::server(&ns)),
64 create(&client, all_authenticated(&ns)),
65 );
66 // Create a route which matches the /allowed path.
67 let (root_route, _roux_route) = tokio::join!(
68 create(&client, http_route("root", &ns, &srv.name_unchecked(), "/"),),
69 create(
70 &client,
71 http_route("roux", &ns, &srv.name_unchecked(), "/roux")
72 )
73 );
74 // Create a policy which allows all authenticated clients
75 create(
76 &client,
77 authz_policy(
78 &ns,
79 "root-authz",
80 LocalTargetRef::from_resource(&root_route),
81 Some(NamespacedTargetRef::from_resource(&all_mtls)),
82 ),
83 )
84 .await;
85
86 // Create the web pod and wait for it to be ready.
87 tokio::join!(
88 create(&client, web::service(&ns)),
89 create_ready_pod(&client, web::pod(&ns))
90 );
91
92 let curl = curl::Runner::init(&client, &ns).await;
93
94 let (allowed, no_route, unauth, no_authz) = tokio::join!(
95 curl.run("curl-allowed", "http://web/", LinkerdInject::Enabled),
96 curl.run(
97 "curl-no-route",
98 "http://web/noroute",
99 LinkerdInject::Enabled
100 ),
101 curl.run("curl-unauth", "http://web/", LinkerdInject::Disabled),
102 curl.run(
103 "curl-route-without-authz",
104 "http://web/roux",
105 LinkerdInject::Enabled
106 ),
107 );
108 let (allowed_status, no_route_status, unauth_status, no_authz_status) = tokio::join!(
109 allowed.http_status_code(),
110 no_route.http_status_code(),
111 unauth.http_status_code(),
112 no_authz.http_status_code(),
113 );
114 assert!(
115 allowed_status.is_success(),
116 "curling allowed route must contact web"
117 );
118 assert_eq!(
119 no_route_status,
120 hyper::StatusCode::NOT_FOUND,
121 "curl which does not match route must not contact web"
122 );
123 assert_eq!(
124 unauth_status,
125 hyper::StatusCode::FORBIDDEN,
126 "curl which is not authenticated must not contact web"
127 );
128 assert_eq!(
129 no_authz_status,
130 hyper::StatusCode::FORBIDDEN,
131 "curl to route with no authorizations must not contact web"
132 );
133
134 // Create a policy which allows all authenticated clients to access the server.
135 create(
136 &client,
137 authz_policy(
138 &ns,
139 "server-authz",
140 LocalTargetRef::from_resource(&srv),
141 Some(NamespacedTargetRef::from_resource(&all_mtls)),
142 ),
143 )
144 .await;
145
146 // Curl a route which doesn't have any authz, but its server does have
147 // an authz.
148 let route_with_server_authz_status = curl
149 .run(
150 "curl-route-with-server-authz",
151 "http://web/roux",
152 LinkerdInject::Enabled,
153 )
154 .await
155 .http_status_code()
156 .await;
157
158 assert!(
159 route_with_server_authz_status.is_success(),
160 "curl to route with no authorizations on server with authorizations must contact web"
161 );
162 })
163 .await;
164}
165
166#[tokio::test(flavor = "current_thread")]
167async fn targets_namespace() {
168 with_temp_ns(|client, ns| async move {
169 // First create all of the policies we'll need so that the web pod
170 // starts up with the correct policy (to prevent races).
171 //
172 // The policy requires that all connections are authenticated with MeshTLS.
173 let (_srv, all_mtls) = tokio::join!(
174 create(&client, web::server(&ns)),
175 create(&client, all_authenticated(&ns))
176 );
177 create(
178 &client,
179 authz_policy(
180 &ns,
181 "web",
182 LocalTargetRef {
183 group: None,
184 kind: "Namespace".to_string(),
185 name: ns.clone(),
186 },
187 Some(NamespacedTargetRef::from_resource(&all_mtls)),
188 ),
189 )
190 .await;
191
192 // Create the web pod and wait for it to be ready.
193 tokio::join!(
194 create(&client, web::service(&ns)),
195 create_ready_pod(&client, web::pod(&ns))
196 );
197
198 let curl = curl::Runner::init(&client, &ns).await;
199 let (injected, uninjected) = tokio::join!(
200 curl.run("curl-injected", "http://web", LinkerdInject::Enabled),
201 curl.run("curl-uninjected", "http://web", LinkerdInject::Disabled),
202 );
203 let (injected_status, uninjected_status) =
204 tokio::join!(injected.exit_code(), uninjected.exit_code());
205 assert_eq!(injected_status, 0, "injected curl must contact web");
206 assert_ne!(
207 uninjected_status, 0,
208 "uninjected curl must fail to contact web"
209 );
210 })
211 .await;
212}
213
214#[tokio::test(flavor = "current_thread")]
215async fn meshtls_namespace() {
216 with_temp_ns(|client, ns| async move {
217 // First create all of the policies we'll need so that the web pod
218 // starts up with the correct policy (to prevent races).
219 //
220 // The policy requires that all connections are authenticated with MeshTLS
221 // and come from service accounts in the given namespace.
222 let (srv, mtls_ns) = tokio::join!(
223 create(&client, web::server(&ns)),
224 create(&client, ns_authenticated(&ns))
225 );
226 create(
227 &client,
228 authz_policy(
229 &ns,
230 "web",
231 LocalTargetRef::from_resource(&srv),
232 Some(NamespacedTargetRef::from_resource(&mtls_ns)),
233 ),
234 )
235 .await;
236
237 // Create the web pod and wait for it to be ready.
238 tokio::join!(
239 create(&client, web::service(&ns)),
240 create_ready_pod(&client, web::pod(&ns))
241 );
242
243 let curl = curl::Runner::init(&client, &ns).await;
244 let (injected, uninjected) = tokio::join!(
245 curl.run("curl-injected", "http://web", LinkerdInject::Enabled),
246 curl.run("curl-uninjected", "http://web", LinkerdInject::Disabled),
247 );
248 let (injected_status, uninjected_status) =
249 tokio::join!(injected.exit_code(), uninjected.exit_code());
250 assert_eq!(injected_status, 0, "injected curl must contact web");
251 assert_ne!(
252 uninjected_status, 0,
253 "uninjected curl must fail to contact web"
254 );
255 })
256 .await;
257}
258
259#[tokio::test(flavor = "current_thread")]
260async fn network() {
261 // In order to test the network policy, we need to create the client pod
262 // before creating the authorization policy. To avoid races, we do this by
263 // creating a `curl-lock` configmap that prevents curl from actually being
264 // executed. Once web is running with the correct policy, the configmap is
265 // deleted to unblock the curl pods.
266 with_temp_ns(|client, ns| async move {
267 let curl = curl::Runner::init(&client, &ns).await;
268 curl.create_lock().await;
269
270 // Create a curl pod and wait for it to get an IP.
271 let blessed = curl
272 .run("curl-blessed", "http://web", LinkerdInject::Disabled)
273 .await;
274 let blessed_ip = blessed.ip().await;
275 tracing::debug!(curl.blessed.ip = %blessed_ip);
276
277 // Once we know the IP of the (blocked) pod, create an web
278 // authorization policy that permits connections from this pod.
279 let (srv, allow_ips) = tokio::join!(
280 create(&client, web::server(&ns)),
281 create(&client, allow_ips(&ns, Some(blessed_ip)))
282 );
283 create(
284 &client,
285 authz_policy(
286 &ns,
287 "web",
288 LocalTargetRef::from_resource(&srv),
289 Some(NamespacedTargetRef::from_resource(&allow_ips)),
290 ),
291 )
292 .await;
293
294 // Start web with the policy.
295 tokio::join!(
296 create(&client, web::service(&ns)),
297 create_ready_pod(&client, web::pod(&ns))
298 );
299
300 await_condition(&client, &ns, "web", endpoints_ready).await;
301
302 // Once the web pod is ready, delete the `curl-lock` configmap to
303 // unblock curl from running.
304 curl.delete_lock().await;
305 tracing::info!("unblocked curl");
306
307 // The blessed pod should be able to connect to the web pod.
308 let status = blessed.exit_code().await;
309 assert_eq!(status, 0, "blessed curl pod must succeed");
310
311 // Create another curl pod that is not included in the authorization. It
312 // should fail to connect to the web pod.
313 let status = curl
314 .run("curl-cursed", "http://web", LinkerdInject::Disabled)
315 .await
316 .exit_code()
317 .await;
318 assert_ne!(status, 0, "cursed curl pod must fail");
319 })
320 .await;
321}
322
323#[tokio::test(flavor = "current_thread")]
324async fn both() {
325 // In order to test the network policy, we need to create the client pod
326 // before creating the authorization policy. To avoid races, we do this by
327 // creating a `curl-lock` configmap that prevents curl from actually being
328 // executed. Once web is running with the correct policy, the configmap is
329 // deleted to unblock the curl pods.
330 with_temp_ns(|client, ns| async move {
331 let curl = curl::Runner::init(&client, &ns).await;
332 curl.create_lock().await;
333
334 let (blessed_injected, blessed_uninjected) = tokio::join!(
335 curl.run(
336 "curl-blessed-injected",
337 "http://web",
338 LinkerdInject::Enabled,
339 ),
340 curl.run(
341 "curl-blessed-uninjected",
342 "http://web",
343 LinkerdInject::Disabled,
344 )
345 );
346 let (blessed_injected_ip, blessed_uninjected_ip) =
347 tokio::join!(blessed_injected.ip(), blessed_uninjected.ip(),);
348 tracing::debug!(curl.blessed.injected.ip = ?blessed_injected_ip);
349 tracing::debug!(curl.blessed.uninjected.ip = ?blessed_uninjected_ip);
350
351 // Once we know the IP of the (blocked) pod, create an web
352 // authorization policy that permits connections from this pod.
353 let (srv, allow_ips, all_mtls) = tokio::join!(
354 create(&client, web::server(&ns)),
355 create(
356 &client,
357 allow_ips(&ns, vec![blessed_injected_ip, blessed_uninjected_ip]),
358 ),
359 create(&client, all_authenticated(&ns))
360 );
361 create(
362 &client,
363 authz_policy(
364 &ns,
365 "web",
366 LocalTargetRef::from_resource(&srv),
367 vec![
368 NamespacedTargetRef::from_resource(&allow_ips),
369 NamespacedTargetRef::from_resource(&all_mtls),
370 ],
371 ),
372 )
373 .await;
374
375 // Start web with the policy.
376 tokio::join!(
377 create(&client, web::service(&ns)),
378 create_ready_pod(&client, web::pod(&ns))
379 );
380
381 await_condition(&client, &ns, "web", endpoints_ready).await;
382
383 // Once the web pod is ready, delete the `curl-lock` configmap to
384 // unblock curl from running.
385 curl.delete_lock().await;
386 tracing::info!("unblocked curl");
387
388 let (blessed_injected_status, blessed_uninjected_status) =
389 tokio::join!(blessed_injected.exit_code(), blessed_uninjected.exit_code());
390 // The blessed and injected pod should be able to connect to the web pod.
391 assert_eq!(
392 blessed_injected_status, 0,
393 "blessed injected curl pod must succeed"
394 );
395 // The blessed and uninjected pod should NOT be able to connect to the web pod.
396 assert_ne!(
397 blessed_uninjected_status, 0,
398 "blessed uninjected curl pod must NOT succeed"
399 );
400
401 let (cursed_injected, cursed_uninjected) = tokio::join!(
402 curl.run("curl-cursed-injected", "http://web", LinkerdInject::Enabled,),
403 curl.run(
404 "curl-cursed-uninjected",
405 "http://web",
406 LinkerdInject::Disabled,
407 )
408 );
409 let (cursed_injected_status, cursed_uninjected_status) =
410 tokio::join!(cursed_injected.exit_code(), cursed_uninjected.exit_code(),);
411 assert_ne!(
412 cursed_injected_status, 0,
413 "cursed injected curl pod must fail"
414 );
415 assert_ne!(
416 cursed_uninjected_status, 0,
417 "cursed uninjected curl pod must fail"
418 );
419 })
420 .await;
421}
422
423#[tokio::test(flavor = "current_thread")]
424async fn either() {
425 // In order to test the network policy, we need to create the client pod
426 // before creating the authorization policy. To avoid races, we do this by
427 // creating a `curl-lock` configmap that prevents curl from actually being
428 // executed. Once web is running with the correct policy, the configmap is
429 // deleted to unblock the curl pods.
430 with_temp_ns(|client, ns| async move {
431 let curl = curl::Runner::init(&client, &ns).await;
432 curl.create_lock().await;
433
434 let (blessed_injected, blessed_uninjected) = tokio::join!(
435 curl.run(
436 "curl-blessed-injected",
437 "http://web",
438 LinkerdInject::Enabled,
439 ),
440 curl.run(
441 "curl-blessed-uninjected",
442 "http://web",
443 LinkerdInject::Disabled,
444 )
445 );
446 let (blessed_injected_ip, blessed_uninjected_ip) =
447 tokio::join!(blessed_injected.ip(), blessed_uninjected.ip());
448 tracing::debug!(curl.blessed.injected.ip = ?blessed_injected_ip);
449 tracing::debug!(curl.blessed.uninjected.ip = ?blessed_uninjected_ip);
450
451 // Once we know the IP of the (blocked) pod, create an web
452 // authorization policy that permits connections from this pod.
453 let (srv, allow_ips, all_mtls) = tokio::join!(
454 create(&client, web::server(&ns)),
455 create(&client, allow_ips(&ns, vec![blessed_uninjected_ip])),
456 create(&client, all_authenticated(&ns))
457 );
458 tokio::join!(
459 create(
460 &client,
461 authz_policy(
462 &ns,
463 "web-from-ip",
464 LocalTargetRef::from_resource(&srv),
465 vec![NamespacedTargetRef::from_resource(&allow_ips)],
466 ),
467 ),
468 create(
469 &client,
470 authz_policy(
471 &ns,
472 "web-from-id",
473 LocalTargetRef::from_resource(&srv),
474 vec![NamespacedTargetRef::from_resource(&all_mtls)],
475 ),
476 )
477 );
478
479 // Start web with the policy.
480 tokio::join!(
481 create(&client, web::service(&ns)),
482 create_ready_pod(&client, web::pod(&ns)),
483 );
484
485 await_condition(&client, &ns, "web", endpoints_ready).await;
486
487 // Once the web pod is ready, delete the `curl-lock` configmap to
488 // unblock curl from running.
489 curl.delete_lock().await;
490 tracing::info!("unblocked curl");
491
492 let (blessed_injected_status, blessed_uninjected_status) =
493 tokio::join!(blessed_injected.exit_code(), blessed_uninjected.exit_code());
494 // The blessed and injected pod should be able to connect to the web pod.
495 assert_eq!(
496 blessed_injected_status, 0,
497 "blessed injected curl pod must succeed"
498 );
499 // The blessed and uninjected pod should NOT be able to connect to the web pod.
500 assert_eq!(
501 blessed_uninjected_status, 0,
502 "blessed uninjected curl pod must succeed"
503 );
504
505 let (cursed_injected, cursed_uninjected) = tokio::join!(
506 curl.run("curl-cursed-injected", "http://web", LinkerdInject::Enabled,),
507 curl.run(
508 "curl-cursed-uninjected",
509 "http://web",
510 LinkerdInject::Disabled,
511 ),
512 );
513 let (cursed_injected_status, cursed_uninjected_status) =
514 tokio::join!(cursed_injected.exit_code(), cursed_uninjected.exit_code());
515 assert_eq!(
516 cursed_injected_status, 0,
517 "cursed injected curl pod must succeed"
518 );
519 assert_ne!(
520 cursed_uninjected_status, 0,
521 "cursed uninjected curl pod must fail"
522 );
523 })
524 .await;
525}
526
527#[tokio::test(flavor = "current_thread")]
528async fn empty_authentications() {
529 with_temp_ns(|client, ns| async move {
530 // Create a policy that does not require any authentications.
531 let srv = create(&client, web::server(&ns)).await;
532 create(
533 &client,
534 authz_policy(&ns, "web", LocalTargetRef::from_resource(&srv), None),
535 )
536 .await;
537
538 // Create the web pod and wait for it to be ready.
539 tokio::join!(
540 create(&client, web::service(&ns)),
541 create_ready_pod(&client, web::pod(&ns))
542 );
543
544 // All requests should work.
545 let curl = curl::Runner::init(&client, &ns).await;
546 let (injected, uninjected) = tokio::join!(
547 curl.run("curl-injected", "http://web", LinkerdInject::Enabled),
548 curl.run("curl-uninjected", "http://web", LinkerdInject::Disabled),
549 );
550 let (injected_status, uninjected_status) =
551 tokio::join!(injected.exit_code(), uninjected.exit_code());
552 assert_eq!(injected_status, 0, "injected curl must contact web");
553 assert_eq!(uninjected_status, 0, "uninjected curl must contact web");
554 })
555 .await;
556}
557
558// === helpers ===
559
560fn authz_policy(
561 ns: &str,
562 name: &str,
563 target: LocalTargetRef,
564 authns: impl IntoIterator<Item = NamespacedTargetRef>,
565) -> k8s::policy::AuthorizationPolicy {
566 k8s::policy::AuthorizationPolicy {
567 metadata: k8s::ObjectMeta {
568 namespace: Some(ns.to_string()),
569 name: Some(name.to_string()),
570 ..Default::default()
571 },
572 spec: k8s::policy::AuthorizationPolicySpec {
573 target_ref: target,
574 required_authentication_refs: authns.into_iter().collect(),
575 },
576 }
577}
578
579fn all_authenticated(ns: &str) -> k8s::policy::MeshTLSAuthentication {
580 k8s::policy::MeshTLSAuthentication {
581 metadata: k8s::ObjectMeta {
582 namespace: Some(ns.to_string()),
583 name: Some("all-authenticated".to_string()),
584 ..Default::default()
585 },
586 spec: k8s::policy::MeshTLSAuthenticationSpec {
587 identity_refs: None,
588 identities: Some(vec!["*".to_string()]),
589 },
590 }
591}
592
593fn ns_authenticated(ns: &str) -> k8s::policy::MeshTLSAuthentication {
594 k8s::policy::MeshTLSAuthentication {
595 metadata: k8s::ObjectMeta {
596 namespace: Some(ns.to_string()),
597 name: Some("all-authenticated".to_string()),
598 ..Default::default()
599 },
600 spec: k8s::policy::MeshTLSAuthenticationSpec {
601 identity_refs: Some(vec![NamespacedTargetRef {
602 group: None,
603 kind: "Namespace".to_string(),
604 name: ns.to_string(),
605 namespace: None,
606 }]),
607 identities: None,
608 },
609 }
610}
611
612fn allow_ips(
613 ns: &str,
614 ips: impl IntoIterator<Item = std::net::IpAddr>,
615) -> k8s::policy::NetworkAuthentication {
616 k8s::policy::NetworkAuthentication {
617 metadata: k8s::ObjectMeta {
618 namespace: Some(ns.to_string()),
619 name: Some("allow-pod".to_string()),
620 ..Default::default()
621 },
622 spec: k8s::policy::NetworkAuthenticationSpec {
623 networks: ips
624 .into_iter()
625 .map(|ip| k8s::policy::Network {
626 cidr: ip.into(),
627 except: None,
628 })
629 .collect(),
630 },
631 }
632}
633
634fn http_route(name: &str, ns: &str, server_name: &str, path: &str) -> k8s::policy::HttpRoute {
635 k8s::policy::HttpRoute {
636 metadata: k8s::ObjectMeta {
637 namespace: Some(ns.to_string()),
638 name: Some(name.to_string()),
639 ..Default::default()
640 },
641 spec: k8s::policy::HttpRouteSpec {
642 inner: k8s::policy::httproute::CommonRouteSpec {
643 parent_refs: Some(vec![k8s_gateway_api::ParentReference {
644 group: Some("policy.linkerd.io".to_string()),
645 kind: Some("Server".to_string()),
646 namespace: Some(ns.to_string()),
647 name: server_name.to_string(),
648 section_name: None,
649 port: None,
650 }]),
651 },
652 hostnames: None,
653 rules: Some(vec![k8s::policy::httproute::HttpRouteRule {
654 matches: Some(vec![k8s::policy::httproute::HttpRouteMatch {
655 path: Some(k8s::policy::httproute::HttpPathMatch::Exact {
656 value: path.to_string(),
657 }),
658 ..Default::default()
659 }]),
660 filters: None,
661 backend_refs: None,
662 timeouts: None,
663 }]),
664 },
665 status: None,
666 }
667}
View as plain text