Proxy: Map Kubernetes Pod Namespace/Name to TLS identity. (#1074)
* Proxy: Map Kubernetes Pod Namespace/Name to TLS identity. Map the Kubernetes identity into a DNS name that can be used to validate the peer's certificate. The final mapping is TBD; the important thing for now is that the mapped name doesn't collide with any real DNS name. Encapsulate the mapping logic within the TLS submodule. Minimize `Arc`ing and `Clone`ing of TLS identities. This has no effect in default configurations since the settings that enable the functionality are not set by default. Signed-off-by: Brian Smith <brian@briansmith.org>
This commit is contained in:
parent
f17b09898a
commit
19d92e9ad2
|
@ -213,7 +213,7 @@ where
|
|||
|
||||
// Map a socket address to a connection.
|
||||
let connect = self.sensors.connect(
|
||||
transport::Connect::new(addr, ep.tls_identity()),
|
||||
transport::Connect::new(addr, ep.tls_identity().cloned()),
|
||||
&client_ctx,
|
||||
);
|
||||
|
||||
|
|
|
@ -70,7 +70,7 @@ pub struct Config {
|
|||
/// Timeout after which to cancel binding a request.
|
||||
pub bind_timeout: Duration,
|
||||
|
||||
pub pod_namespace: String,
|
||||
pub namespaces: Namespaces,
|
||||
|
||||
/// Optional minimum TTL for DNS lookups.
|
||||
pub dns_min_ttl: Option<Duration>,
|
||||
|
@ -79,6 +79,14 @@ pub struct Config {
|
|||
pub dns_max_ttl: Option<Duration>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Namespaces {
|
||||
pub pod: String,
|
||||
|
||||
/// `None` if TLS is disabled; otherwise must be `Some`.
|
||||
pub tls_controller: Option<String>,
|
||||
}
|
||||
|
||||
/// Configuration settings for binding a listener.
|
||||
///
|
||||
/// TODO: Rename this to be more inline with the actual types.
|
||||
|
@ -172,6 +180,7 @@ pub const ENV_TLS_TRUST_ANCHORS: &str = "CONDUIT_PROXY_TLS_TRUST_ANCHORS";
|
|||
pub const ENV_TLS_CERT: &str = "CONDUIT_PROXY_TLS_CERT";
|
||||
pub const ENV_TLS_PRIVATE_KEY: &str = "CONDUIT_PROXY_TLS_PRIVATE_KEY";
|
||||
|
||||
pub const ENV_CONTROLLER_NAMESPACE: &str = "CONDUIT_PROXY_CONTROLLER_NAMESPACE";
|
||||
pub const ENV_POD_NAMESPACE: &str = "CONDUIT_PROXY_POD_NAMESPACE";
|
||||
|
||||
pub const ENV_CONTROL_URL: &str = "CONDUIT_PROXY_CONTROL_URL";
|
||||
|
@ -266,6 +275,7 @@ impl<'a> TryFrom<&'a Strings> for Config {
|
|||
Error::InvalidEnvVar
|
||||
})
|
||||
});
|
||||
let controller_namespace = strings.get(ENV_CONTROLLER_NAMESPACE);
|
||||
|
||||
// There is no default controller URL because a default would make it
|
||||
// too easy to connect to the wrong controller, which would be dangerous.
|
||||
|
@ -303,6 +313,16 @@ impl<'a> TryFrom<&'a Strings> for Config {
|
|||
},
|
||||
}?;
|
||||
|
||||
let tls_controller_namespace = match (&tls_settings, controller_namespace?) {
|
||||
(Some(_), Some(ns)) => Some(ns),
|
||||
(Some(_), None) => {
|
||||
error!("{} is not set; it is required when {} are set.",
|
||||
ENV_CONTROLLER_NAMESPACE, ENV_TLS_TRUST_ANCHORS);
|
||||
return Err(Error::InvalidEnvVar);
|
||||
},
|
||||
_ => None,
|
||||
};
|
||||
|
||||
Ok(Config {
|
||||
private_listener: Listener {
|
||||
addr: private_listener_addr?
|
||||
|
@ -354,7 +374,10 @@ impl<'a> TryFrom<&'a Strings> for Config {
|
|||
|
||||
bind_timeout: bind_timeout?.unwrap_or(DEFAULT_BIND_TIMEOUT),
|
||||
|
||||
pod_namespace: pod_namespace?,
|
||||
namespaces: Namespaces {
|
||||
pod: pod_namespace?,
|
||||
tls_controller: tls_controller_namespace,
|
||||
},
|
||||
|
||||
dns_min_ttl: dns_min_ttl?,
|
||||
|
||||
|
|
|
@ -24,11 +24,11 @@ use conduit_proxy_controller_grpc::destination::client::Destination as Destinati
|
|||
use conduit_proxy_controller_grpc::destination::update::Update as PbUpdate2;
|
||||
use conduit_proxy_controller_grpc::destination::{
|
||||
Update as PbUpdate,
|
||||
TlsIdentity as TlsIdentityPb,
|
||||
WeightedAddr,
|
||||
};
|
||||
|
||||
use super::{TlsIdentity, Metadata, ResolveRequest, Responder, Update};
|
||||
use super::{Metadata, ResolveRequest, Responder, Update};
|
||||
use config::Namespaces;
|
||||
use control::{
|
||||
cache::{Cache, CacheChange, Exists},
|
||||
fully_qualified_authority::FullyQualifiedAuthority,
|
||||
|
@ -39,6 +39,7 @@ use dns::{self, IpAddrListFuture};
|
|||
use telemetry::metrics::DstLabels;
|
||||
use transport::{DnsNameAndPort, HostAndPort, LookupAddressAndConnect};
|
||||
use timeout::Timeout;
|
||||
use transport::tls;
|
||||
|
||||
type DestinationServiceQuery<T> = Remote<PbUpdate, T>;
|
||||
type UpdateRx<T> = Receiver<PbUpdate, T>;
|
||||
|
@ -51,7 +52,7 @@ type UpdateRx<T> = Receiver<PbUpdate, T>;
|
|||
/// propagated to all requesters.
|
||||
struct Background<T: HttpService<ResponseBody = RecvBody>> {
|
||||
dns_resolver: dns::Resolver,
|
||||
default_destination_namespace: String,
|
||||
namespaces: Namespaces,
|
||||
destinations: HashMap<DnsNameAndPort, DestinationSet<T>>,
|
||||
/// A queue of authorities that need to be reconnected.
|
||||
reconnects: VecDeque<DnsNameAndPort>,
|
||||
|
@ -75,7 +76,7 @@ struct DestinationSet<T: HttpService<ResponseBody = RecvBody>> {
|
|||
pub(super) fn task(
|
||||
request_rx: mpsc::UnboundedReceiver<ResolveRequest>,
|
||||
dns_resolver: dns::Resolver,
|
||||
default_destination_namespace: String,
|
||||
namespaces: Namespaces,
|
||||
host_and_port: HostAndPort,
|
||||
) -> impl Future<Item = (), Error = ()>
|
||||
{
|
||||
|
@ -105,7 +106,7 @@ pub(super) fn task(
|
|||
let mut disco = Background::new(
|
||||
request_rx,
|
||||
dns_resolver,
|
||||
default_destination_namespace,
|
||||
namespaces,
|
||||
);
|
||||
|
||||
future::poll_fn(move || {
|
||||
|
@ -125,11 +126,11 @@ where
|
|||
fn new(
|
||||
request_rx: mpsc::UnboundedReceiver<ResolveRequest>,
|
||||
dns_resolver: dns::Resolver,
|
||||
default_destination_namespace: String,
|
||||
namespaces: Namespaces,
|
||||
) -> Self {
|
||||
Self {
|
||||
dns_resolver,
|
||||
default_destination_namespace,
|
||||
namespaces,
|
||||
destinations: HashMap::new(),
|
||||
reconnects: VecDeque::new(),
|
||||
rpc_ready: false,
|
||||
|
@ -197,7 +198,7 @@ where
|
|||
},
|
||||
Entry::Vacant(vac) => {
|
||||
let query = Self::query_destination_service_if_relevant(
|
||||
&self.default_destination_namespace,
|
||||
&self.namespaces.pod,
|
||||
client,
|
||||
vac.key(),
|
||||
"connect",
|
||||
|
@ -239,7 +240,7 @@ where
|
|||
while let Some(auth) = self.reconnects.pop_front() {
|
||||
if let Some(set) = self.destinations.get_mut(&auth) {
|
||||
set.query = Self::query_destination_service_if_relevant(
|
||||
&self.default_destination_namespace,
|
||||
&self.namespaces.pod,
|
||||
client,
|
||||
&auth,
|
||||
"reconnect",
|
||||
|
@ -268,7 +269,8 @@ where
|
|||
let (new_query, found_by_destination_service) = match set.query.take() {
|
||||
Some(Remote::ConnectedOrConnecting { rx }) => {
|
||||
let (new_query, found_by_destination_service) =
|
||||
set.poll_destination_service(auth, rx);
|
||||
set.poll_destination_service(
|
||||
auth, rx, self.namespaces.tls_controller.as_ref().map(|s| s.as_ref()));
|
||||
if let Remote::NeedsReconnect = new_query {
|
||||
set.reset_on_next_modification();
|
||||
self.reconnects.push_back(auth.clone());
|
||||
|
@ -363,6 +365,7 @@ where
|
|||
&mut self,
|
||||
auth: &DnsNameAndPort,
|
||||
mut rx: UpdateRx<T>,
|
||||
tls_controller_namespace: Option<&str>,
|
||||
) -> (DestinationServiceQuery<T>, Exists<()>) {
|
||||
let mut exists = Exists::Unknown;
|
||||
|
||||
|
@ -374,7 +377,8 @@ where
|
|||
let addrs = a_set
|
||||
.addrs
|
||||
.into_iter()
|
||||
.filter_map(|pb| pb_to_addr_meta(pb, &set_labels));
|
||||
.filter_map(|pb|
|
||||
pb_to_addr_meta(pb, &set_labels, tls_controller_namespace));
|
||||
self.add(auth, addrs)
|
||||
},
|
||||
Some(PbUpdate2::Remove(r_set)) => {
|
||||
|
@ -562,6 +566,7 @@ impl<T: HttpService<ResponseBody = RecvBody>> DestinationSet<T> {
|
|||
fn pb_to_addr_meta(
|
||||
pb: WeightedAddr,
|
||||
set_labels: &HashMap<String, String>,
|
||||
tls_controller_namespace: Option<&str>,
|
||||
) -> Option<(SocketAddr, Metadata)> {
|
||||
let addr = pb.addr.and_then(pb_to_sock_addr)?;
|
||||
|
||||
|
@ -570,27 +575,23 @@ fn pb_to_addr_meta(
|
|||
.collect::<Vec<_>>();
|
||||
labels.sort_by(|(k0, _), (k1, _)| k0.cmp(k1));
|
||||
|
||||
let tls = pb.tls_identity.and_then(TlsIdentity::from_pb);
|
||||
let tls_identity = pb.tls_identity.and_then(|pb| {
|
||||
match tls::Identity::maybe_from(pb, tls_controller_namespace) {
|
||||
Ok(maybe_tls) => maybe_tls,
|
||||
Err(e) => {
|
||||
error!("Failed to parse TLS identity: {:?}", e);
|
||||
// XXX: Wallpaper over the error and keep going without TLS.
|
||||
// TODO: Hard fail here once the TLS infrastructure has been
|
||||
// validated.
|
||||
None
|
||||
},
|
||||
}
|
||||
});
|
||||
|
||||
let meta = Metadata::new(DstLabels::new(labels.into_iter()), tls);
|
||||
let meta = Metadata::new(DstLabels::new(labels.into_iter()), tls_identity);
|
||||
Some((addr, meta))
|
||||
}
|
||||
|
||||
impl TlsIdentity {
|
||||
pub fn from_pb(pb: TlsIdentityPb) -> Option<Self> {
|
||||
use conduit_proxy_controller_grpc::destination::tls_identity::Strategy;
|
||||
pb.strategy.map(|strategy| match strategy {
|
||||
Strategy::K8sPodNamespace(i) =>
|
||||
TlsIdentity::K8sPodNamespace {
|
||||
controller_ns: i.controller_ns,
|
||||
pod_ns: i.pod_ns,
|
||||
pod_name: i.pod_name,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fn pb_to_sock_addr(pb: TcpAddress) -> Option<SocketAddr> {
|
||||
use conduit_proxy_controller_grpc::common::ip_address::Ip;
|
||||
use std::net::{Ipv4Addr, Ipv6Addr};
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
use std::net::SocketAddr;
|
||||
|
||||
use telemetry::metrics::DstLabels;
|
||||
use super::{Metadata, TlsIdentity};
|
||||
use super::Metadata;
|
||||
use tls;
|
||||
|
||||
/// An individual traffic target.
|
||||
///
|
||||
|
@ -34,7 +35,7 @@ impl Endpoint {
|
|||
self.metadata.dst_labels()
|
||||
}
|
||||
|
||||
pub fn tls_identity(&self) -> Option<&TlsIdentity> {
|
||||
pub fn tls_identity(&self) -> Option<&tls::Identity> {
|
||||
self.metadata.tls_identity()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -26,6 +26,7 @@
|
|||
|
||||
use std::net::SocketAddr;
|
||||
use std::sync::{Arc, Weak};
|
||||
use tls;
|
||||
|
||||
use futures::{
|
||||
sync::mpsc,
|
||||
|
@ -46,6 +47,7 @@ pub mod background;
|
|||
mod endpoint;
|
||||
|
||||
pub use self::endpoint::Endpoint;
|
||||
use config::Namespaces;
|
||||
|
||||
/// A handle to request resolutions from the background discovery task.
|
||||
#[derive(Clone, Debug)]
|
||||
|
@ -93,21 +95,7 @@ pub struct Metadata {
|
|||
dst_labels: Option<DstLabels>,
|
||||
|
||||
/// How to verify TLS for the endpoint.
|
||||
tls_identity: Option<Arc<TlsIdentity>>,
|
||||
}
|
||||
|
||||
/// How to verify TLS for an endpoint.
|
||||
///
|
||||
/// XXX: This currently derives `PartialEq and `Eq`, which is not entirely
|
||||
/// correct, as domain name equality ought to be case insensitive. However,
|
||||
/// `Metadata` must be `Eq`.
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub enum TlsIdentity {
|
||||
K8sPodNamespace {
|
||||
controller_ns: String,
|
||||
pod_ns: String,
|
||||
pod_name: String,
|
||||
},
|
||||
tls_identity: Option<tls::Identity>,
|
||||
}
|
||||
|
||||
|
||||
|
@ -153,7 +141,7 @@ pub trait Bind {
|
|||
/// to drive the background task.
|
||||
pub fn new(
|
||||
dns_resolver: dns::Resolver,
|
||||
default_destination_namespace: String,
|
||||
namespaces: Namespaces,
|
||||
host_and_port: HostAndPort,
|
||||
) -> (Resolver, impl Future<Item = (), Error = ()>) {
|
||||
let (request_tx, rx) = mpsc::unbounded();
|
||||
|
@ -161,7 +149,7 @@ pub fn new(
|
|||
let bg = background::task(
|
||||
rx,
|
||||
dns_resolver,
|
||||
default_destination_namespace,
|
||||
namespaces,
|
||||
host_and_port,
|
||||
);
|
||||
(disco, bg)
|
||||
|
@ -258,11 +246,11 @@ impl Metadata {
|
|||
|
||||
pub fn new(
|
||||
dst_labels: Option<DstLabels>,
|
||||
tls_identity: Option<TlsIdentity>
|
||||
tls_identity: Option<tls::Identity>
|
||||
) -> Self {
|
||||
Metadata {
|
||||
dst_labels,
|
||||
tls_identity: tls_identity.map(Arc::new),
|
||||
tls_identity,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -271,7 +259,7 @@ impl Metadata {
|
|||
self.dst_labels.as_ref()
|
||||
}
|
||||
|
||||
pub fn tls_identity(&self) -> Option<&TlsIdentity> {
|
||||
self.tls_identity.as_ref().map(Arc::as_ref)
|
||||
pub fn tls_identity(&self) -> Option<&tls::Identity> {
|
||||
self.tls_identity.as_ref()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,9 +2,9 @@ use http;
|
|||
use std::sync::{Arc, atomic::AtomicUsize};
|
||||
|
||||
use ctx;
|
||||
use control::destination;
|
||||
use telemetry::metrics::DstLabels;
|
||||
use std::sync::atomic::Ordering;
|
||||
use transport::tls;
|
||||
|
||||
|
||||
/// A `RequestId` can be mapped to a `u64`. No `RequestId`s will map to the
|
||||
|
@ -76,7 +76,7 @@ impl Request {
|
|||
Arc::new(r)
|
||||
}
|
||||
|
||||
pub fn tls_identity(&self) -> Option<&destination::TlsIdentity> {
|
||||
pub fn tls_identity(&self) -> Option<&tls::Identity> {
|
||||
self.client.tls_identity()
|
||||
}
|
||||
|
||||
|
@ -95,10 +95,6 @@ impl Response {
|
|||
Arc::new(r)
|
||||
}
|
||||
|
||||
pub fn tls_identity(&self) -> Option<&destination::TlsIdentity> {
|
||||
self.request.tls_identity()
|
||||
}
|
||||
|
||||
pub fn dst_labels(&self) -> Option<&DstLabels> {
|
||||
self.request.dst_labels()
|
||||
}
|
||||
|
|
|
@ -48,7 +48,7 @@ impl Process {
|
|||
pub fn new(config: &config::Config) -> Arc<Self> {
|
||||
let start_time = SystemTime::now();
|
||||
Arc::new(Self {
|
||||
scheduled_namespace: config.pod_namespace.clone(),
|
||||
scheduled_namespace: config.namespaces.pod.clone(),
|
||||
start_time,
|
||||
})
|
||||
}
|
||||
|
|
|
@ -4,6 +4,7 @@ use std::sync::Arc;
|
|||
use ctx;
|
||||
use control::destination;
|
||||
use telemetry::metrics::DstLabels;
|
||||
use transport::tls;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum Ctx {
|
||||
|
@ -93,7 +94,7 @@ impl Client {
|
|||
Arc::new(c)
|
||||
}
|
||||
|
||||
pub fn tls_identity(&self) -> Option<&destination::TlsIdentity> {
|
||||
pub fn tls_identity(&self) -> Option<&tls::Identity> {
|
||||
self.metadata.tls_identity()
|
||||
}
|
||||
|
||||
|
|
|
@ -222,7 +222,7 @@ where
|
|||
|
||||
let (resolver, resolver_bg) = control::destination::new(
|
||||
dns_resolver.clone(),
|
||||
config.pod_namespace.clone(),
|
||||
config.namespaces.clone(),
|
||||
control_host_and_port
|
||||
);
|
||||
|
||||
|
|
|
@ -8,9 +8,9 @@ use std::str::FromStr;
|
|||
use http;
|
||||
|
||||
use connection;
|
||||
use control::destination;
|
||||
use convert::TryFrom;
|
||||
use dns;
|
||||
use transport::tls;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Connect {
|
||||
|
@ -102,7 +102,7 @@ impl Connect {
|
|||
/// Returns a `Connect` to `addr`.
|
||||
pub fn new(
|
||||
addr: SocketAddr,
|
||||
tls_identity: Option<&destination::TlsIdentity>,
|
||||
tls_identity: Option<tls::Identity>,
|
||||
) -> Self {
|
||||
// TODO: this is currently unused.
|
||||
let _ = tls_identity;
|
||||
|
|
|
@ -0,0 +1,72 @@
|
|||
use conduit_proxy_controller_grpc;
|
||||
use convert::TryFrom;
|
||||
use super::{DnsName, InvalidDnsName};
|
||||
use std::sync::Arc;
|
||||
|
||||
/// An endpoint's identity.
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct Identity(Arc<DnsName>);
|
||||
|
||||
impl Identity {
|
||||
/// Parses the given TLS identity, if provided.
|
||||
///
|
||||
/// `controller_namespace` will be None if TLS is disabled.
|
||||
///
|
||||
/// In the event of an error, the error is logged, so no detailed error
|
||||
/// information is returned.
|
||||
pub fn maybe_from(
|
||||
pb: conduit_proxy_controller_grpc::destination::TlsIdentity,
|
||||
controller_namespace: Option<&str>)
|
||||
-> Result<Option<Self>, ()>
|
||||
{
|
||||
// Verifies that the string doesn't contain '.' so that it is safe to
|
||||
// join it using '.' to try to form a DNS name. The rest of the DNS
|
||||
// name rules will be enforced by `DnsName::try_from`.
|
||||
fn check_single_label(value: &str, name: &str) -> Result<(), ()> {
|
||||
if !value.contains('.') {
|
||||
Ok(())
|
||||
} else {
|
||||
error!("Invalid {}: {:?}", name, value);
|
||||
Err(())
|
||||
}
|
||||
}
|
||||
|
||||
use conduit_proxy_controller_grpc::destination::tls_identity::Strategy;
|
||||
match pb.strategy {
|
||||
Some(Strategy::K8sPodNamespace(i)) => {
|
||||
// Log any/any per-component errors before returning.
|
||||
let controller_ns_check = check_single_label(&i.controller_ns, "controller_ms");
|
||||
let pod_ns_check = check_single_label(&i.pod_ns, "pod_ns");
|
||||
let pod_name_check = check_single_label(&i.pod_name, "pod_name");
|
||||
if controller_ns_check.is_err() || pod_ns_check.is_err() || pod_name_check.is_err() {
|
||||
return Err(());
|
||||
}
|
||||
|
||||
// XXX: If we don't know the controller's namespace or we don't
|
||||
// share the same controller then we won't be able to validate
|
||||
// the certificate yet. TODO: Support cross-controller
|
||||
// certificate validation and lock this down.
|
||||
if controller_namespace != Some(i.controller_ns.as_ref()) {
|
||||
return Ok(None)
|
||||
}
|
||||
|
||||
// We reserve all names under a fake "managed-pods" service in
|
||||
// our namespace for identifying pods by name.
|
||||
let name = format!(
|
||||
"{pod}.{pod_ns}.conduit-managed-pods.{controller_ns}.svc.cluster.local.",
|
||||
pod = i.pod_name,
|
||||
pod_ns = i.pod_ns,
|
||||
controller_ns = i.controller_ns,
|
||||
);
|
||||
|
||||
DnsName::try_from(&name)
|
||||
.map(|name| Some(Identity(Arc::new(name))))
|
||||
.map_err(|InvalidDnsName| {
|
||||
error!("Invalid DNS name: {:?}", name);
|
||||
()
|
||||
})
|
||||
},
|
||||
None => Ok(None), // No TLS.
|
||||
}
|
||||
}
|
||||
}
|
|
@ -9,9 +9,11 @@ mod config;
|
|||
mod cert_resolver;
|
||||
mod connection;
|
||||
mod dns_name;
|
||||
mod identity;
|
||||
|
||||
pub use self::{
|
||||
config::{CommonSettings, CommonConfig, Error, ServerConfig, ServerConfigWatch},
|
||||
connection::Connection,
|
||||
dns_name::DnsName,
|
||||
dns_name::{DnsName, InvalidDnsName},
|
||||
identity::Identity,
|
||||
};
|
||||
|
|
Loading…
Reference in New Issue