Add support for MeshTLSAuthentication to target namespaces (#8356)

The `identity_refs` section of the `MeshTLSAuthentication` resource currently
only supports ServiceAccount resources.  We add support for Namespace resources
as well.  When a `MeshTLSAuthentication` has a Namespace identity_ref, this
means that all ServiceAccounts in that Namespace are authenticated.

Fixes #8298

Signed-off-by: Alex Leong <alex@buoyant.io>
This commit is contained in:
Alex Leong 2022-04-30 16:11:33 -07:00 committed by GitHub
parent fc9ed47d64
commit 5c50d253af
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 137 additions and 8 deletions

View File

@ -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
)
}
}

View File

@ -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::<Namespace>() {
let id = cluster.namespace_identity(tgt.name.as_str());
Ok(id.parse::<IdentityMatch>()?)
} else {
anyhow::bail!("unsupported target type: {:?}", tgt.canonical_kind())
}

View File

@ -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<AuthorizationPolicySpec> for Admission {
}
}
fn validate_identity_ref(id: &NamespacedTargetRef) -> Result<()> {
if id.targets_kind::<ServiceAccount>() {
return Ok(());
}
if id.targets_kind::<Namespace>() {
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<MeshTLSAuthenticationSpec> 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::<ServiceAccount>() {
bail!("invalid identity target kind: {}", id.canonical_kind());
}
validate_identity_ref(id)?;
}
Ok(())

View File

@ -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 {

View File

@ -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<Item = std::net::IpAddr>,