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
|
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 anyhow::Result;
|
||||||
use linkerd_policy_controller_core::IdentityMatch;
|
use linkerd_policy_controller_core::IdentityMatch;
|
||||||
use linkerd_policy_controller_k8s_api::{
|
use linkerd_policy_controller_k8s_api::{
|
||||||
policy::MeshTLSAuthentication, ResourceExt, ServiceAccount,
|
policy::MeshTLSAuthentication, Namespace, ResourceExt, ServiceAccount,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[derive(Debug, PartialEq)]
|
#[derive(Debug, PartialEq)]
|
||||||
|
|
@ -29,6 +29,9 @@ impl Spec {
|
||||||
let ns = tgt.namespace.as_deref().unwrap_or(&namespace);
|
let ns = tgt.namespace.as_deref().unwrap_or(&namespace);
|
||||||
let id = cluster.service_account_identity(ns, &tgt.name);
|
let id = cluster.service_account_identity(ns, &tgt.name);
|
||||||
Ok(IdentityMatch::Exact(id))
|
Ok(IdentityMatch::Exact(id))
|
||||||
|
} else if tgt.targets_kind::<Namespace>() {
|
||||||
|
let id = cluster.namespace_identity(tgt.name.as_str());
|
||||||
|
Ok(id.parse::<IdentityMatch>()?)
|
||||||
} else {
|
} else {
|
||||||
anyhow::bail!("unsupported target type: {:?}", tgt.canonical_kind())
|
anyhow::bail!("unsupported target type: {:?}", tgt.canonical_kind())
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,7 @@ use futures::future;
|
||||||
use hyper::{body::Buf, http, Body, Request, Response};
|
use hyper::{body::Buf, http, Body, Request, Response};
|
||||||
use k8s_openapi::api::core::v1::ServiceAccount;
|
use k8s_openapi::api::core::v1::ServiceAccount;
|
||||||
use kube::{core::DynamicObject, Resource, ResourceExt};
|
use kube::{core::DynamicObject, Resource, ResourceExt};
|
||||||
|
use linkerd_policy_controller_k8s_api::{policy::NamespacedTargetRef, Namespace};
|
||||||
use serde::de::DeserializeOwned;
|
use serde::de::DeserializeOwned;
|
||||||
use std::task;
|
use std::task;
|
||||||
use thiserror::Error;
|
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]
|
#[async_trait::async_trait]
|
||||||
impl Validate<MeshTLSAuthenticationSpec> for Admission {
|
impl Validate<MeshTLSAuthenticationSpec> for Admission {
|
||||||
async fn validate(self, _ns: &str, _name: &str, spec: MeshTLSAuthenticationSpec) -> Result<()> {
|
async fn validate(self, _ns: &str, _name: &str, spec: MeshTLSAuthenticationSpec) -> Result<()> {
|
||||||
// The CRD validates identity strings, but does not validate identity references.
|
// The CRD validates identity strings, but does not validate identity references.
|
||||||
|
|
||||||
for id in spec.identity_refs.iter().flatten() {
|
for id in spec.identity_refs.iter().flatten() {
|
||||||
if !id.targets_kind::<ServiceAccount>() {
|
validate_identity_ref(id)?;
|
||||||
bail!("invalid identity target kind: {}", id.canonical_kind());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|
|
||||||
|
|
@ -25,6 +25,48 @@ async fn accepts_valid_ref() {
|
||||||
.await;
|
.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")]
|
#[tokio::test(flavor = "current_thread")]
|
||||||
async fn accepts_strings() {
|
async fn accepts_strings() {
|
||||||
admission::accepts(|ns| MeshTLSAuthentication {
|
admission::accepts(|ns| MeshTLSAuthentication {
|
||||||
|
|
|
||||||
|
|
@ -39,11 +39,56 @@ async fn meshtls() {
|
||||||
);
|
);
|
||||||
let (injected_status, uninjected_status) =
|
let (injected_status, uninjected_status) =
|
||||||
tokio::join!(injected.exit_code(), uninjected.exit_code());
|
tokio::join!(injected.exit_code(), uninjected.exit_code());
|
||||||
assert_eq!(
|
assert_eq!(injected_status, 0, "injected curl must contact nginx");
|
||||||
injected_status, 0,
|
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"
|
"uninjected curl must fail to contact nginx"
|
||||||
);
|
);
|
||||||
assert_ne!(uninjected_status, 0, "injected curl must contact nginx");
|
|
||||||
})
|
})
|
||||||
.await;
|
.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(
|
fn allow_ips(
|
||||||
ns: &str,
|
ns: &str,
|
||||||
ips: impl IntoIterator<Item = std::net::IpAddr>,
|
ips: impl IntoIterator<Item = std::net::IpAddr>,
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue