mirror of https://github.com/linkerd/linkerd2.git
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:
parent
fc9ed47d64
commit
5c50d253af
|
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(())
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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>,
|
||||
|
|
|
|||
Loading…
Reference in New Issue