diff --git a/policy-controller/k8s/index/src/lib.rs b/policy-controller/k8s/index/src/lib.rs index b3d1f4520..dd46d0c3a 100644 --- a/policy-controller/k8s/index/src/lib.rs +++ b/policy-controller/k8s/index/src/lib.rs @@ -71,4 +71,11 @@ impl ClusterInfo { sa, ns, self.control_plane_ns, self.identity_domain ) } + + fn namespace_identity(&self, ns: &str) -> String { + format!( + "*.{}.serviceaccount.identity.{}.{}", + ns, self.control_plane_ns, self.identity_domain + ) + } } diff --git a/policy-controller/k8s/index/src/meshtls_authentication.rs b/policy-controller/k8s/index/src/meshtls_authentication.rs index 6dd26a38d..b3ab79495 100644 --- a/policy-controller/k8s/index/src/meshtls_authentication.rs +++ b/policy-controller/k8s/index/src/meshtls_authentication.rs @@ -2,7 +2,7 @@ use crate::ClusterInfo; use anyhow::Result; use linkerd_policy_controller_core::IdentityMatch; use linkerd_policy_controller_k8s_api::{ - policy::MeshTLSAuthentication, ResourceExt, ServiceAccount, + policy::MeshTLSAuthentication, Namespace, ResourceExt, ServiceAccount, }; #[derive(Debug, PartialEq)] @@ -29,6 +29,9 @@ impl Spec { let ns = tgt.namespace.as_deref().unwrap_or(&namespace); let id = cluster.service_account_identity(ns, &tgt.name); Ok(IdentityMatch::Exact(id)) + } else if tgt.targets_kind::() { + let id = cluster.namespace_identity(tgt.name.as_str()); + Ok(id.parse::()?) } else { anyhow::bail!("unsupported target type: {:?}", tgt.canonical_kind()) } diff --git a/policy-controller/src/admission.rs b/policy-controller/src/admission.rs index dfb8c950f..04cddf22d 100644 --- a/policy-controller/src/admission.rs +++ b/policy-controller/src/admission.rs @@ -11,6 +11,7 @@ use futures::future; use hyper::{body::Buf, http, Body, Request, Response}; use k8s_openapi::api::core::v1::ServiceAccount; use kube::{core::DynamicObject, Resource, ResourceExt}; +use linkerd_policy_controller_k8s_api::{policy::NamespacedTargetRef, Namespace}; use serde::de::DeserializeOwned; use std::task; use thiserror::Error; @@ -233,15 +234,27 @@ impl Validate for Admission { } } +fn validate_identity_ref(id: &NamespacedTargetRef) -> Result<()> { + if id.targets_kind::() { + return Ok(()); + } + + if id.targets_kind::() { + if id.namespace.is_some() { + bail!("Namespace identity_ref is cluster-scoped and cannot have a namespace"); + } + return Ok(()); + } + + bail!("invalid identity target kind: {}", id.canonical_kind()); +} + #[async_trait::async_trait] impl Validate for Admission { async fn validate(self, _ns: &str, _name: &str, spec: MeshTLSAuthenticationSpec) -> Result<()> { // The CRD validates identity strings, but does not validate identity references. - for id in spec.identity_refs.iter().flatten() { - if !id.targets_kind::() { - bail!("invalid identity target kind: {}", id.canonical_kind()); - } + validate_identity_ref(id)?; } Ok(()) diff --git a/policy-test/tests/admit_meshtls_authentication.rs b/policy-test/tests/admit_meshtls_authentication.rs index a5c057015..eeccb849a 100644 --- a/policy-test/tests/admit_meshtls_authentication.rs +++ b/policy-test/tests/admit_meshtls_authentication.rs @@ -25,6 +25,48 @@ async fn accepts_valid_ref() { .await; } +#[tokio::test(flavor = "current_thread")] +async fn accepts_ns_ref() { + admission::accepts(|ns| MeshTLSAuthentication { + metadata: api::ObjectMeta { + namespace: Some(ns), + name: Some("test".to_string()), + ..Default::default() + }, + spec: MeshTLSAuthenticationSpec { + identity_refs: Some(vec![NamespacedTargetRef { + group: None, + kind: "Namespace".to_string(), + name: "default".to_string(), + namespace: None, + }]), + ..Default::default() + }, + }) + .await; +} + +#[tokio::test(flavor = "current_thread")] +async fn rejects_namespaced_namespace() { + admission::rejects(|ns| MeshTLSAuthentication { + metadata: api::ObjectMeta { + namespace: Some(ns), + name: Some("test".to_string()), + ..Default::default() + }, + spec: MeshTLSAuthenticationSpec { + identity_refs: Some(vec![NamespacedTargetRef { + group: None, + kind: "Namespace".to_string(), + name: "default".to_string(), + namespace: Some("default".to_string()), + }]), + ..Default::default() + }, + }) + .await; +} + #[tokio::test(flavor = "current_thread")] async fn accepts_strings() { admission::accepts(|ns| MeshTLSAuthentication { diff --git a/policy-test/tests/e2e_authorization_policy.rs b/policy-test/tests/e2e_authorization_policy.rs index ed5e03185..91cb51587 100644 --- a/policy-test/tests/e2e_authorization_policy.rs +++ b/policy-test/tests/e2e_authorization_policy.rs @@ -39,11 +39,56 @@ async fn meshtls() { ); let (injected_status, uninjected_status) = tokio::join!(injected.exit_code(), uninjected.exit_code()); - assert_eq!( - injected_status, 0, + assert_eq!(injected_status, 0, "injected curl must contact nginx"); + assert_ne!( + uninjected_status, 0, + "uninjected curl must fail to contact nginx" + ); + }) + .await; +} + +#[tokio::test(flavor = "current_thread")] +async fn meshtls_namespace() { + with_temp_ns(|client, ns| async move { + // First create all of the policies we'll need so that the nginx pod + // starts up with the correct policy (to prevent races). + // + // The policy requires that all connections are authenticated with MeshTLS + // and come from service accounts in the given namespace. + let (srv, mtls_ns) = tokio::join!( + create(&client, nginx::server(&ns)), + create(&client, ns_authenticated(&ns)) + ); + create( + &client, + authz_policy( + &ns, + "nginx", + &srv, + Some(NamespacedTargetRef::from_resource(&mtls_ns)), + ), + ) + .await; + + // Create the nginx pod and wait for it to be ready. + tokio::join!( + create(&client, nginx::service(&ns)), + create_ready_pod(&client, nginx::pod(&ns)) + ); + + let curl = curl::Runner::init(&client, &ns).await; + let (injected, uninjected) = tokio::join!( + curl.run("curl-injected", "http://nginx", LinkerdInject::Enabled), + curl.run("curl-uninjected", "http://nginx", LinkerdInject::Disabled), + ); + let (injected_status, uninjected_status) = + tokio::join!(injected.exit_code(), uninjected.exit_code()); + assert_eq!(injected_status, 0, "injected curl must contact nginx"); + assert_ne!( + uninjected_status, 0, "uninjected curl must fail to contact nginx" ); - assert_ne!(uninjected_status, 0, "injected curl must contact nginx"); }) .await; } @@ -352,6 +397,25 @@ fn all_authenticated(ns: &str) -> k8s::policy::MeshTLSAuthentication { } } +fn ns_authenticated(ns: &str) -> k8s::policy::MeshTLSAuthentication { + k8s::policy::MeshTLSAuthentication { + metadata: k8s::ObjectMeta { + namespace: Some(ns.to_string()), + name: Some("all-authenticated".to_string()), + ..Default::default() + }, + spec: k8s::policy::MeshTLSAuthenticationSpec { + identity_refs: Some(vec![NamespacedTargetRef { + group: None, + kind: "Namespace".to_string(), + name: ns.to_string(), + namespace: None, + }]), + identities: None, + }, + } +} + fn allow_ips( ns: &str, ips: impl IntoIterator,