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:
Brian Smith 2018-06-07 11:14:57 -10:00 committed by GitHub
parent f17b09898a
commit 19d92e9ad2
12 changed files with 150 additions and 66 deletions

View File

@ -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,
);

View File

@ -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?,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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,
};