mirror of https://github.com/linkerd/linkerd2.git
Proxy: Make TLS server aware of its own identity. (#1148)
* Proxy: Make TLS server aware of its own identity. When validating the TLS configuration, make sure the certificate is valid for the current pod. Make the pod's identity available at that point in time so it can do so. Since the identity is available now, simplify the validation of our own certificate by using Rustls's API instead of dropping down to the lower-level webpli API. This is a step towards the server differentiating between TLS handshakes it is supposed to terminate vs. TLS handshakes it is supposed to pass through. This is also a step toward the client side (connect) of TLS, which will reuse much of the configuration logic. Signed-off-by: Brian Smith <brian@briansmith.org>
This commit is contained in:
parent
13716cd269
commit
f82d16f50e
|
@ -188,6 +188,7 @@ 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_TLS_PRIVATE_KEY: &str = "CONDUIT_PROXY_TLS_PRIVATE_KEY";
|
||||||
|
|
||||||
pub const ENV_CONTROLLER_NAMESPACE: &str = "CONDUIT_PROXY_CONTROLLER_NAMESPACE";
|
pub const ENV_CONTROLLER_NAMESPACE: &str = "CONDUIT_PROXY_CONTROLLER_NAMESPACE";
|
||||||
|
pub const ENV_POD_NAME: &str = "CONDUIT_PROXY_POD_NAME";
|
||||||
pub const ENV_POD_NAMESPACE: &str = "CONDUIT_PROXY_POD_NAMESPACE";
|
pub const ENV_POD_NAMESPACE: &str = "CONDUIT_PROXY_POD_NAMESPACE";
|
||||||
|
|
||||||
pub const ENV_CONTROL_URL: &str = "CONDUIT_PROXY_CONTROL_URL";
|
pub const ENV_CONTROL_URL: &str = "CONDUIT_PROXY_CONTROL_URL";
|
||||||
|
@ -275,6 +276,7 @@ impl<'a> TryFrom<&'a Strings> for Config {
|
||||||
let metrics_retain_idle = parse(strings, ENV_METRICS_RETAIN_IDLE, parse_duration);
|
let metrics_retain_idle = parse(strings, ENV_METRICS_RETAIN_IDLE, parse_duration);
|
||||||
let dns_min_ttl = parse(strings, ENV_DNS_MIN_TTL, parse_duration);
|
let dns_min_ttl = parse(strings, ENV_DNS_MIN_TTL, parse_duration);
|
||||||
let dns_max_ttl = parse(strings, ENV_DNS_MAX_TTL, parse_duration);
|
let dns_max_ttl = parse(strings, ENV_DNS_MAX_TTL, parse_duration);
|
||||||
|
let pod_name = strings.get(ENV_POD_NAME);
|
||||||
let pod_namespace = strings.get(ENV_POD_NAMESPACE).and_then(|maybe_value| {
|
let pod_namespace = strings.get(ENV_POD_NAMESPACE).and_then(|maybe_value| {
|
||||||
// There cannot be a default pod namespace, and the pod namespace is required.
|
// There cannot be a default pod namespace, and the pod namespace is required.
|
||||||
maybe_value.ok_or_else(|| {
|
maybe_value.ok_or_else(|| {
|
||||||
|
@ -288,41 +290,51 @@ impl<'a> TryFrom<&'a Strings> for Config {
|
||||||
// too easy to connect to the wrong controller, which would be dangerous.
|
// too easy to connect to the wrong controller, which would be dangerous.
|
||||||
let control_host_and_port = parse(strings, ENV_CONTROL_URL, parse_url);
|
let control_host_and_port = parse(strings, ENV_CONTROL_URL, parse_url);
|
||||||
|
|
||||||
let tls_settings = match (tls_trust_anchors?, tls_end_entity_cert?, tls_private_key?) {
|
let namespaces = Namespaces {
|
||||||
(Some(trust_anchors), Some(end_entity_cert), Some(private_key)) =>
|
pod: pod_namespace?,
|
||||||
|
tls_controller: controller_namespace?,
|
||||||
|
};
|
||||||
|
|
||||||
|
let tls_settings = match (tls_trust_anchors?,
|
||||||
|
tls_end_entity_cert?,
|
||||||
|
tls_private_key?,
|
||||||
|
pod_name?.as_ref())
|
||||||
|
{
|
||||||
|
(Some(trust_anchors),
|
||||||
|
Some(end_entity_cert),
|
||||||
|
Some(private_key),
|
||||||
|
Some(pod_name)) => {
|
||||||
|
let service_identity = tls::Identity::try_from_pod_name(&namespaces, pod_name)
|
||||||
|
.map_err(|_| Error::InvalidEnvVar)?; // Already logged.
|
||||||
Ok(Some(tls::CommonSettings {
|
Ok(Some(tls::CommonSettings {
|
||||||
trust_anchors,
|
trust_anchors,
|
||||||
end_entity_cert,
|
end_entity_cert,
|
||||||
private_key,
|
private_key,
|
||||||
})),
|
service_identity,
|
||||||
(_, None, None) => Ok(None), // No TLS in server role.
|
}))
|
||||||
(trust_anchors, end_entity_cert, private_key) => {
|
},
|
||||||
|
(None, None, None, _) => Ok(None), // No TLS.
|
||||||
|
(trust_anchors, end_entity_cert, private_key, pod_name) => {
|
||||||
if trust_anchors.is_none() {
|
if trust_anchors.is_none() {
|
||||||
error!("{} is not set; it is required when {} and {} are set.",
|
error!("{} is not set; it is required when {} and {} are set.",
|
||||||
ENV_TLS_TRUST_ANCHORS, ENV_TLS_CERT, ENV_TLS_PRIVATE_KEY);
|
ENV_TLS_TRUST_ANCHORS, ENV_TLS_CERT, ENV_TLS_PRIVATE_KEY);
|
||||||
}
|
}
|
||||||
if end_entity_cert.is_none() {
|
if end_entity_cert.is_none() {
|
||||||
error!("{} is not set; it is required when {} are set.",
|
error!("{} is not set; it is required when {} are set.",
|
||||||
ENV_TLS_CERT, ENV_TLS_PRIVATE_KEY);
|
ENV_TLS_CERT, ENV_TLS_TRUST_ANCHORS);
|
||||||
}
|
}
|
||||||
if private_key.is_none() {
|
if private_key.is_none() {
|
||||||
error!("{} is not set; it is required when {} are set.",
|
error!("{} is not set; it is required when {} are set.",
|
||||||
ENV_TLS_PRIVATE_KEY, ENV_TLS_CERT);
|
ENV_TLS_PRIVATE_KEY, ENV_TLS_TRUST_ANCHORS);
|
||||||
|
}
|
||||||
|
if pod_name.is_none() {
|
||||||
|
error!("{} is not set; it is required when {} are set.",
|
||||||
|
ENV_POD_NAME, ENV_TLS_CERT);
|
||||||
}
|
}
|
||||||
Err(Error::InvalidEnvVar)
|
Err(Error::InvalidEnvVar)
|
||||||
},
|
},
|
||||||
}?;
|
}?;
|
||||||
|
|
||||||
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 {
|
Ok(Config {
|
||||||
private_listener: Listener {
|
private_listener: Listener {
|
||||||
addr: private_listener_addr?
|
addr: private_listener_addr?
|
||||||
|
@ -374,10 +386,7 @@ impl<'a> TryFrom<&'a Strings> for Config {
|
||||||
|
|
||||||
bind_timeout: bind_timeout?.unwrap_or(DEFAULT_BIND_TIMEOUT),
|
bind_timeout: bind_timeout?.unwrap_or(DEFAULT_BIND_TIMEOUT),
|
||||||
|
|
||||||
namespaces: Namespaces {
|
namespaces,
|
||||||
pod: pod_namespace?,
|
|
||||||
tls_controller: tls_controller_namespace,
|
|
||||||
},
|
|
||||||
|
|
||||||
dns_min_ttl: dns_min_ttl?,
|
dns_min_ttl: dns_min_ttl?,
|
||||||
|
|
||||||
|
|
|
@ -97,7 +97,7 @@ impl BoundPort {
|
||||||
// TLS when needed.
|
// TLS when needed.
|
||||||
pub fn listen_and_fold<T, F, Fut>(
|
pub fn listen_and_fold<T, F, Fut>(
|
||||||
self,
|
self,
|
||||||
tls_config: tls::ServerConfigWatch,
|
tls: Option<(tls::Identity, tls::ServerConfigWatch)>,
|
||||||
initial: T,
|
initial: T,
|
||||||
f: F)
|
f: F)
|
||||||
-> impl Future<Item = (), Error = io::Error> + Send + 'static
|
-> impl Future<Item = (), Error = io::Error> + Send + 'static
|
||||||
|
@ -126,14 +126,18 @@ impl BoundPort {
|
||||||
// libraries don't have the necessary API for that, so just
|
// libraries don't have the necessary API for that, so just
|
||||||
// do it here.
|
// do it here.
|
||||||
set_nodelay_or_warn(&socket);
|
set_nodelay_or_warn(&socket);
|
||||||
match tls_config.borrow().as_ref() {
|
if let Some((_identity, config_watch)) = &tls {
|
||||||
Some(tls_config) => {
|
// TODO: use `identity` to differentiate between TLS
|
||||||
Either::A(
|
// that the proxy should terminate vs. TLS that should
|
||||||
tls::Connection::accept(socket, tls_config.clone())
|
// be passed through.
|
||||||
.map(move |tls| (Connection::new(Box::new(tls)), remote_addr)))
|
if let Some(config) = &*config_watch.borrow() {
|
||||||
},
|
return Either::A(
|
||||||
None => Either::B(future::ok((Connection::new(Box::new(socket)), remote_addr))),
|
tls::Connection::accept(socket, config.clone())
|
||||||
|
.map(move |tls| (Connection::new(Box::new(tls)), remote_addr)));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Either::B(future::ok((Connection::new(Box::new(socket)), remote_addr)))
|
||||||
})
|
})
|
||||||
.then(|r| {
|
.then(|r| {
|
||||||
future::ok(match r {
|
future::ok(match r {
|
||||||
|
|
|
@ -582,7 +582,7 @@ fn pb_to_addr_meta(
|
||||||
labels.sort_by(|(k0, _), (k1, _)| k0.cmp(k1));
|
labels.sort_by(|(k0, _), (k1, _)| k0.cmp(k1));
|
||||||
|
|
||||||
let tls_identity = pb.tls_identity.and_then(|pb| {
|
let tls_identity = pb.tls_identity.and_then(|pb| {
|
||||||
match tls::Identity::maybe_from(pb, tls_controller_namespace) {
|
match tls::Identity::maybe_from_protobuf(tls_controller_namespace, pb) {
|
||||||
Ok(maybe_tls) => maybe_tls,
|
Ok(maybe_tls) => maybe_tls,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
error!("Failed to parse TLS identity: {:?}", e);
|
error!("Failed to parse TLS identity: {:?}", e);
|
||||||
|
|
|
@ -245,9 +245,7 @@ where
|
||||||
// to the managed application (private destination).
|
// to the managed application (private destination).
|
||||||
let inbound = {
|
let inbound = {
|
||||||
let ctx = ctx::Proxy::inbound(&process_ctx);
|
let ctx = ctx::Proxy::inbound(&process_ctx);
|
||||||
|
|
||||||
let bind = bind.clone().with_ctx(ctx.clone());
|
let bind = bind.clone().with_ctx(ctx.clone());
|
||||||
|
|
||||||
let default_addr = config.private_forward.map(|a| a.into());
|
let default_addr = config.private_forward.map(|a| a.into());
|
||||||
|
|
||||||
let router = Router::new(
|
let router = Router::new(
|
||||||
|
@ -257,7 +255,7 @@ where
|
||||||
);
|
);
|
||||||
serve(
|
serve(
|
||||||
inbound_listener,
|
inbound_listener,
|
||||||
tls_server_config,
|
config.tls_settings.map(|settings| (settings.service_identity, tls_server_config)),
|
||||||
router,
|
router,
|
||||||
config.private_connect_timeout,
|
config.private_connect_timeout,
|
||||||
config.inbound_ports_disable_protocol_detection,
|
config.inbound_ports_disable_protocol_detection,
|
||||||
|
@ -281,7 +279,7 @@ where
|
||||||
);
|
);
|
||||||
serve(
|
serve(
|
||||||
outbound_listener,
|
outbound_listener,
|
||||||
tls::ServerConfig::no_tls(), // No TLS between service & proxy.
|
None, // No TLS between service & proxy.
|
||||||
router,
|
router,
|
||||||
config.public_connect_timeout,
|
config.public_connect_timeout,
|
||||||
config.outbound_ports_disable_protocol_detection,
|
config.outbound_ports_disable_protocol_detection,
|
||||||
|
@ -348,7 +346,7 @@ where
|
||||||
|
|
||||||
fn serve<R, B, E, F, G>(
|
fn serve<R, B, E, F, G>(
|
||||||
bound_port: BoundPort,
|
bound_port: BoundPort,
|
||||||
tls_config: tls::ServerConfigWatch,
|
tls_config: Option<(tls::Identity, tls::ServerConfigWatch)>,
|
||||||
router: Router<R>,
|
router: Router<R>,
|
||||||
tcp_connect_timeout: Duration,
|
tcp_connect_timeout: Duration,
|
||||||
disable_protocol_detection_ports: IndexSet<u16>,
|
disable_protocol_detection_ports: IndexSet<u16>,
|
||||||
|
@ -507,7 +505,7 @@ where
|
||||||
let fut = {
|
let fut = {
|
||||||
let log = log.clone();
|
let log = log.clone();
|
||||||
bound_port.listen_and_fold(
|
bound_port.listen_and_fold(
|
||||||
tls::ServerConfig::no_tls(), // TODO: serve over TLS.
|
None, // TODO: serve over TLS.
|
||||||
server,
|
server,
|
||||||
move |server, (session, remote)| {
|
move |server, (session, remote)| {
|
||||||
let log = log.clone().with_remote(remote);
|
let log = log.clone().with_remote(remote);
|
||||||
|
|
|
@ -102,7 +102,7 @@ impl Control {
|
||||||
let fut = {
|
let fut = {
|
||||||
let log = log.clone();
|
let log = log.clone();
|
||||||
bound_port.listen_and_fold(
|
bound_port.listen_and_fold(
|
||||||
::tls::ServerConfig::no_tls(), // TODO: Serve over TLS.
|
None, // TODO: Serve over TLS.
|
||||||
hyper::server::conn::Http::new(),
|
hyper::server::conn::Http::new(),
|
||||||
move |hyper, (conn, remote)| {
|
move |hyper, (conn, remote)| {
|
||||||
let service = service.clone();
|
let service = service.clone();
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
use std::{
|
use std::{
|
||||||
|
fmt,
|
||||||
sync::Arc,
|
sync::Arc,
|
||||||
time::SystemTime,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
|
@ -16,6 +16,13 @@ pub struct CertResolver {
|
||||||
certified_key: rustls::sign::CertifiedKey,
|
certified_key: rustls::sign::CertifiedKey,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl fmt::Debug for CertResolver {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
|
||||||
|
f.debug_struct("CertResolver")
|
||||||
|
.finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
struct SigningKey {
|
struct SigningKey {
|
||||||
signer: Signer,
|
signer: Signer,
|
||||||
}
|
}
|
||||||
|
@ -29,33 +36,18 @@ impl CertResolver {
|
||||||
/// Returns a new `CertResolver` that has a certificate (chain) verified to
|
/// Returns a new `CertResolver` that has a certificate (chain) verified to
|
||||||
/// have been issued by one of the given trust anchors.
|
/// have been issued by one of the given trust anchors.
|
||||||
///
|
///
|
||||||
|
/// TODO: Have the caller pass in a `rustls::ServerCertVerified` as evidence
|
||||||
|
/// that the certificate chain was validated, once Rustls's (safe) API
|
||||||
|
/// supports that.
|
||||||
|
///
|
||||||
/// TODO: Verify that the public key of the certificate matches the private
|
/// TODO: Verify that the public key of the certificate matches the private
|
||||||
/// key.
|
/// key.
|
||||||
pub fn new(
|
pub fn new(
|
||||||
root_cert_store: &rustls::RootCertStore,
|
_certificate_was_validated: (), // TODO: `rustls::ServerCertVerified`.
|
||||||
cert_chain: Vec<rustls::Certificate>,
|
cert_chain: Vec<rustls::Certificate>,
|
||||||
private_key: untrusted::Input)
|
private_key: untrusted::Input)
|
||||||
-> Result<Self, config::Error>
|
-> Result<Self, config::Error>
|
||||||
{
|
{
|
||||||
let now = webpki::Time::try_from(SystemTime::now())
|
|
||||||
.map_err(|ring::error::Unspecified| config::Error::TimeConversionFailed)?;
|
|
||||||
|
|
||||||
let trust_anchors = root_cert_store.roots.iter()
|
|
||||||
.map(|owned_trust_anchor| owned_trust_anchor.to_trust_anchor())
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
let trust_anchors = webpki::TLSServerTrustAnchors(&trust_anchors);
|
|
||||||
|
|
||||||
// Verify that we were given a valid TLS certificate that was issued by
|
|
||||||
// our CA.
|
|
||||||
parse_end_entity_cert(&cert_chain)
|
|
||||||
.and_then(|cert| {
|
|
||||||
cert.verify_is_valid_tls_server_cert(
|
|
||||||
&[SIGNATURE_ALG_WEBPKI],
|
|
||||||
&trust_anchors,
|
|
||||||
&[], // No intermediate certificates
|
|
||||||
now)
|
|
||||||
}).map_err(config::Error::EndEntityCertIsNotValid)?;
|
|
||||||
|
|
||||||
let private_key = signature::key_pair_from_pkcs8(SIGNATURE_ALG_RING_SIGNING, private_key)
|
let private_key = signature::key_pair_from_pkcs8(SIGNATURE_ALG_RING_SIGNING, private_key)
|
||||||
.map_err(|ring::error::Unspecified| config::Error::InvalidPrivateKey)?;
|
.map_err(|ring::error::Unspecified| config::Error::InvalidPrivateKey)?;
|
||||||
|
|
||||||
|
@ -139,4 +131,3 @@ const SIGNATURE_ALG_RUSTLS_SCHEME: rustls::SignatureScheme =
|
||||||
rustls::SignatureScheme::ECDSA_NISTP256_SHA256;
|
rustls::SignatureScheme::ECDSA_NISTP256_SHA256;
|
||||||
const SIGNATURE_ALG_RUSTLS_ALGORITHM: rustls::internal::msgs::enums::SignatureAlgorithm =
|
const SIGNATURE_ALG_RUSTLS_ALGORITHM: rustls::internal::msgs::enums::SignatureAlgorithm =
|
||||||
rustls::internal::msgs::enums::SignatureAlgorithm::ECDSA;
|
rustls::internal::msgs::enums::SignatureAlgorithm::ECDSA;
|
||||||
static SIGNATURE_ALG_WEBPKI: &webpki::SignatureAlgorithm = &webpki::ECDSA_P256_SHA256;
|
|
||||||
|
|
|
@ -8,6 +8,7 @@ use std::{
|
||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
cert_resolver::CertResolver,
|
cert_resolver::CertResolver,
|
||||||
|
Identity,
|
||||||
|
|
||||||
rustls,
|
rustls,
|
||||||
untrusted,
|
untrusted,
|
||||||
|
@ -39,10 +40,15 @@ pub struct CommonSettings {
|
||||||
|
|
||||||
/// The private key in DER-encoded PKCS#8 form.
|
/// The private key in DER-encoded PKCS#8 form.
|
||||||
pub private_key: PathBuf,
|
pub private_key: PathBuf,
|
||||||
|
|
||||||
|
/// The identity we use to identify the service being proxied (as opposed
|
||||||
|
/// to the psuedo-service exposed on the proxy's control port).
|
||||||
|
pub service_identity: Identity,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Validated configuration common between TLS clients and TLS servers.
|
/// Validated configuration common between TLS clients and TLS servers.
|
||||||
pub struct CommonConfig {
|
#[derive(Debug)]
|
||||||
|
struct CommonConfig {
|
||||||
root_cert_store: rustls::RootCertStore,
|
root_cert_store: rustls::RootCertStore,
|
||||||
cert_resolver: Arc<CertResolver>,
|
cert_resolver: Arc<CertResolver>,
|
||||||
}
|
}
|
||||||
|
@ -64,9 +70,8 @@ pub type ServerConfigWatch = Watch<Option<ServerConfig>>;
|
||||||
pub enum Error {
|
pub enum Error {
|
||||||
Io(PathBuf, io::Error),
|
Io(PathBuf, io::Error),
|
||||||
FailedToParseTrustAnchors(Option<webpki::Error>),
|
FailedToParseTrustAnchors(Option<webpki::Error>),
|
||||||
EndEntityCertIsNotValid(webpki::Error),
|
EndEntityCertIsNotValid(rustls::TLSError),
|
||||||
InvalidPrivateKey,
|
InvalidPrivateKey,
|
||||||
TimeConversionFailed,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl CommonSettings {
|
impl CommonSettings {
|
||||||
|
@ -83,7 +88,7 @@ impl CommonSettings {
|
||||||
/// The returned stream consists of each subsequent successfully loaded
|
/// The returned stream consists of each subsequent successfully loaded
|
||||||
/// `CommonSettings` after each change. If the settings could not be
|
/// `CommonSettings` after each change. If the settings could not be
|
||||||
/// reloaded (i.e., they were malformed), nothing is sent.
|
/// reloaded (i.e., they were malformed), nothing is sent.
|
||||||
pub fn stream_changes(self, interval: Duration)
|
fn stream_changes(self, interval: Duration)
|
||||||
-> impl Stream<Item = CommonConfig, Error = ()>
|
-> impl Stream<Item = CommonConfig, Error = ()>
|
||||||
{
|
{
|
||||||
let paths = self.paths().iter()
|
let paths = self.paths().iter()
|
||||||
|
@ -143,8 +148,30 @@ impl CommonConfig {
|
||||||
let private_key = load_file_contents(&settings.private_key)?;
|
let private_key = load_file_contents(&settings.private_key)?;
|
||||||
let private_key = untrusted::Input::from(&private_key);
|
let private_key = untrusted::Input::from(&private_key);
|
||||||
|
|
||||||
// `CertResolver::new` is responsible for the consistency check.
|
// Ensure the certificate is valid for the services we terminate for
|
||||||
let cert_resolver = CertResolver::new(&root_cert_store, cert_chain, private_key)?;
|
// TLS. This assumes that server cert validation does the same or
|
||||||
|
// more validation than client cert validation.
|
||||||
|
//
|
||||||
|
// XXX: Rustls currently only provides access to a
|
||||||
|
// `ServerCertVerifier` through
|
||||||
|
// `rustls::ClientConfig::get_verifier()`.
|
||||||
|
//
|
||||||
|
// XXX: Once `rustls::ServerCertVerified` is exposed in Rustls's
|
||||||
|
// safe API, remove the `map(|_| ())` below.
|
||||||
|
//
|
||||||
|
// TODO: Restrict accepted signatutre algorithms.
|
||||||
|
let certificate_was_validated =
|
||||||
|
rustls::ClientConfig::new().get_verifier().verify_server_cert(
|
||||||
|
&root_cert_store,
|
||||||
|
&cert_chain,
|
||||||
|
settings.service_identity.as_dns_name_ref(),
|
||||||
|
&[]) // No OCSP
|
||||||
|
.map(|_| ())
|
||||||
|
.map_err(Error::EndEntityCertIsNotValid)?;
|
||||||
|
|
||||||
|
// `CertResolver::new` is responsible for verifying that the
|
||||||
|
// private key is the right one for the certificate.
|
||||||
|
let cert_resolver = CertResolver::new(certificate_was_validated, cert_chain, private_key)?;
|
||||||
|
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
root_cert_store,
|
root_cert_store,
|
||||||
|
@ -221,11 +248,6 @@ impl ServerConfig {
|
||||||
config.cert_resolver = common.cert_resolver.clone();
|
config.cert_resolver = common.cert_resolver.clone();
|
||||||
ServerConfig(Arc::new(config))
|
ServerConfig(Arc::new(config))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn no_tls() -> ServerConfigWatch {
|
|
||||||
let (watch, _) = Watch::new(None);
|
|
||||||
watch
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn load_file_contents(path: &PathBuf) -> Result<Vec<u8>, Error> {
|
fn load_file_contents(path: &PathBuf) -> Result<Vec<u8>, Error> {
|
||||||
|
@ -258,3 +280,100 @@ fn set_common_settings(versions: &mut Vec<rustls::ProtocolVersion>) {
|
||||||
// Only enable TLS 1.2 until TLS 1.3 is stable.
|
// Only enable TLS 1.2 until TLS 1.3 is stable.
|
||||||
*versions = vec![rustls::ProtocolVersion::TLSv1_2]
|
*versions = vec![rustls::ProtocolVersion::TLSv1_2]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use tls::{CommonSettings, Identity, ServerConfig};
|
||||||
|
use super::{CommonConfig, Error};
|
||||||
|
use config::Namespaces;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
struct Strings {
|
||||||
|
pod_name: &'static str,
|
||||||
|
pod_ns: &'static str,
|
||||||
|
controller_ns: &'static str,
|
||||||
|
trust_anchors: &'static str,
|
||||||
|
end_entity_cert: &'static str,
|
||||||
|
private_key: &'static str,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn settings(s: &Strings) -> CommonSettings {
|
||||||
|
let dir = PathBuf::from("src/transport/tls/testdata");
|
||||||
|
let namespaces = Namespaces {
|
||||||
|
pod: s.pod_ns.into(),
|
||||||
|
tls_controller: Some(s.controller_ns.into()),
|
||||||
|
};
|
||||||
|
let service_identity = Identity::try_from_pod_name(&namespaces, s.pod_name).unwrap();
|
||||||
|
CommonSettings {
|
||||||
|
trust_anchors: dir.join(s.trust_anchors),
|
||||||
|
end_entity_cert: dir.join(s.end_entity_cert),
|
||||||
|
private_key: dir.join(s.private_key),
|
||||||
|
service_identity,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn can_construct_server_config_from_valid_settings() {
|
||||||
|
let settings = settings(&Strings {
|
||||||
|
pod_name: "foo",
|
||||||
|
pod_ns: "ns1",
|
||||||
|
controller_ns: "conduit",
|
||||||
|
trust_anchors: "ca1.pem",
|
||||||
|
end_entity_cert: "foo-ns1-ca1.crt",
|
||||||
|
private_key: "foo-ns1-ca1.p8",
|
||||||
|
});
|
||||||
|
let config = CommonConfig::load_from_disk(&settings).unwrap();
|
||||||
|
let _: ServerConfig = ServerConfig::from(&config); // Infallible.
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn recognize_ca_did_not_issue_cert() {
|
||||||
|
let settings = settings(&Strings {
|
||||||
|
pod_name: "foo",
|
||||||
|
pod_ns: "ns1",
|
||||||
|
controller_ns: "conduit",
|
||||||
|
trust_anchors: "ca2.pem", // Mismatch
|
||||||
|
end_entity_cert: "foo-ns1-ca1.crt",
|
||||||
|
private_key: "foo-ns1-ca1.p8",
|
||||||
|
});
|
||||||
|
match CommonConfig::load_from_disk(&settings) {
|
||||||
|
Err(Error::EndEntityCertIsNotValid(_)) => (),
|
||||||
|
r => unreachable!("CommonConfig::load_from_disk returned {:?}", r),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn recognize_cert_is_not_valid_for_identity() {
|
||||||
|
let settings = settings(&Strings {
|
||||||
|
pod_name: "foo", // Mismatch
|
||||||
|
pod_ns: "ns1",
|
||||||
|
controller_ns: "conduit",
|
||||||
|
trust_anchors: "ca1.pem",
|
||||||
|
end_entity_cert: "bar-ns1-ca1.crt",
|
||||||
|
private_key: "bar-ns1-ca1.p8",
|
||||||
|
});
|
||||||
|
match CommonConfig::load_from_disk(&settings) {
|
||||||
|
Err(Error::EndEntityCertIsNotValid(_)) => (),
|
||||||
|
r => unreachable!("CommonConfig::load_from_disk returned {:?}", r),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// XXX: The check that this tests hasn't been implemented yet.
|
||||||
|
#[test]
|
||||||
|
#[should_panic]
|
||||||
|
fn recognize_private_key_is_not_valid_for_cert() {
|
||||||
|
let settings = settings(&Strings {
|
||||||
|
pod_name: "foo",
|
||||||
|
pod_ns: "ns1",
|
||||||
|
controller_ns: "conduit",
|
||||||
|
trust_anchors: "ca1.pem",
|
||||||
|
end_entity_cert: "foo-ns1-ca1.crt",
|
||||||
|
private_key: "bar-ns1-ca1.p8", // Mismatch
|
||||||
|
});
|
||||||
|
match CommonConfig::load_from_disk(&settings) {
|
||||||
|
Err(_) => (), // // TODO: Err(Error::InvalidPrivateKey) > (),
|
||||||
|
r => unreachable!("CommonConfig::load_from_disk returned {:?}", r),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -6,7 +6,7 @@ use convert::TryFrom;
|
||||||
/// are specified in [RFC 5280 Section 7.2], except that underscores are also
|
/// are specified in [RFC 5280 Section 7.2], except that underscores are also
|
||||||
/// allowed.
|
/// allowed.
|
||||||
#[derive(Clone, Debug, Eq, PartialEq, Hash)]
|
#[derive(Clone, Debug, Eq, PartialEq, Hash)]
|
||||||
pub struct DnsName(webpki::DNSName);
|
pub struct DnsName(pub(super) webpki::DNSName);
|
||||||
|
|
||||||
impl fmt::Display for DnsName {
|
impl fmt::Display for DnsName {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
|
fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
|
||||||
|
|
|
@ -1,11 +1,12 @@
|
||||||
use conduit_proxy_controller_grpc;
|
use conduit_proxy_controller_grpc;
|
||||||
use convert::TryFrom;
|
use convert::TryFrom;
|
||||||
use super::{DnsName, InvalidDnsName};
|
use super::{DnsName, InvalidDnsName, webpki};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
use config::Namespaces;
|
||||||
|
|
||||||
/// An endpoint's identity.
|
/// An endpoint's identity.
|
||||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||||
pub struct Identity(Arc<DnsName>);
|
pub struct Identity(pub(super) Arc<DnsName>);
|
||||||
|
|
||||||
impl Identity {
|
impl Identity {
|
||||||
/// Parses the given TLS identity, if provided.
|
/// Parses the given TLS identity, if provided.
|
||||||
|
@ -14,11 +15,33 @@ impl Identity {
|
||||||
///
|
///
|
||||||
/// In the event of an error, the error is logged, so no detailed error
|
/// In the event of an error, the error is logged, so no detailed error
|
||||||
/// information is returned.
|
/// information is returned.
|
||||||
pub fn maybe_from(
|
pub fn maybe_from_protobuf(
|
||||||
pb: conduit_proxy_controller_grpc::destination::TlsIdentity,
|
controller_namespace: Option<&str>,
|
||||||
controller_namespace: Option<&str>)
|
pb: conduit_proxy_controller_grpc::destination::TlsIdentity)
|
||||||
-> Result<Option<Self>, ()>
|
-> Result<Option<Self>, ()>
|
||||||
{
|
{
|
||||||
|
use conduit_proxy_controller_grpc::destination::tls_identity::Strategy;
|
||||||
|
match pb.strategy {
|
||||||
|
Some(Strategy::K8sPodNamespace(i)) => {
|
||||||
|
// 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
let namespaces = Namespaces {
|
||||||
|
pod: i.pod_ns,
|
||||||
|
tls_controller: Some(i.controller_ns),
|
||||||
|
};
|
||||||
|
Self::try_from_pod_name(&namespaces, &i.pod_name).map(Some)
|
||||||
|
},
|
||||||
|
None => Ok(None), // No TLS.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn try_from_pod_name(namespaces: &Namespaces, pod_name: &str) -> Result<Self, ()> {
|
||||||
// Verifies that the string doesn't contain '.' so that it is safe to
|
// 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
|
// join it using '.' to try to form a DNS name. The rest of the DNS
|
||||||
// name rules will be enforced by `DnsName::try_from`.
|
// name rules will be enforced by `DnsName::try_from`.
|
||||||
|
@ -31,42 +54,39 @@ impl Identity {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
use conduit_proxy_controller_grpc::destination::tls_identity::Strategy;
|
let controller_ns = if let Some(controller_ns) = &namespaces.tls_controller {
|
||||||
match pb.strategy {
|
controller_ns
|
||||||
Some(Strategy::K8sPodNamespace(i)) => {
|
} else {
|
||||||
// Log any/any per-component errors before returning.
|
error!("controller namespace not provided");
|
||||||
let controller_ns_check = check_single_label(&i.controller_ns, "controller_ms");
|
return Err(());
|
||||||
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
|
// Log any/any per-component errors before returning.
|
||||||
// share the same controller then we won't be able to validate
|
let controller_ns_check = check_single_label(controller_ns, "controller namespace");
|
||||||
// the certificate yet. TODO: Support cross-controller
|
let pod_ns_check = check_single_label(&namespaces.pod, "pod namespace");
|
||||||
// certificate validation and lock this down.
|
let pod_name_check = check_single_label(pod_name, "pod name");
|
||||||
if controller_namespace != Some(i.controller_ns.as_ref()) {
|
if controller_ns_check.is_err() || pod_ns_check.is_err() || pod_name_check.is_err() {
|
||||||
return Ok(None)
|
return Err(());
|
||||||
}
|
|
||||||
|
|
||||||
// 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.
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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 = pod_name,
|
||||||
|
pod_ns = &namespaces.pod,
|
||||||
|
controller_ns = controller_ns,
|
||||||
|
);
|
||||||
|
|
||||||
|
DnsName::try_from(&name)
|
||||||
|
.map(|name| Identity(Arc::new(name)))
|
||||||
|
.map_err(|InvalidDnsName| {
|
||||||
|
error!("Invalid DNS name: {:?}", name);
|
||||||
|
()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn as_dns_name_ref(&self) -> webpki::DNSNameRef {
|
||||||
|
(self.0).0.as_ref()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Binary file not shown.
Binary file not shown.
|
@ -0,0 +1,10 @@
|
||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
MIIBYjCCAQigAwIBAgIUeNut9wWIX23pQjr5vp9CVjT5ckYwCgYIKoZIzj0EAwIw
|
||||||
|
DzENMAsGA1UECxMETm9uZTAeFw0xODA2MTgyMDQ3MDBaFw0yMzA2MTcyMDQ3MDBa
|
||||||
|
MA8xDTALBgNVBAsTBE5vbmUwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAATpX00P
|
||||||
|
uIzqsJOyfF8j5KQxZwl7z8gwFRUOKS5pGYToLZRHknDCAM6+8atts+rlOCbRx3Ip
|
||||||
|
BOE6/zl8mmgwDtHho0IwQDAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB
|
||||||
|
/zAdBgNVHQ4EFgQUd6BIvpxr8rUL9quQelM8GiTv2h0wCgYIKoZIzj0EAwIDSAAw
|
||||||
|
RQIhANh+WhsYerczLUi5fsZNOFVA+gTaYLaPfjkZ+xduaFnDAiBor7lB9XUt/1Xn
|
||||||
|
J+2gLEkKlZnmoDx7EWOlnfK5/zkVMg==
|
||||||
|
-----END CERTIFICATE-----
|
|
@ -0,0 +1,10 @@
|
||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
MIIBYTCCAQigAwIBAgIUeFHfUb7rtx5XqLkZCvGtBWl9dBwwCgYIKoZIzj0EAwIw
|
||||||
|
DzENMAsGA1UECxMETm9uZTAeFw0xODA2MTgyMDQ3MDBaFw0yMzA2MTcyMDQ3MDBa
|
||||||
|
MA8xDTALBgNVBAsTBE5vbmUwWTATBgcqhkjOPQIBBggqhkjOPQMBBwNCAASeliXf
|
||||||
|
Pezb5p/uCv2vHKyTcGY4fIJ/3EZNmImCoKsR2KBjNhOZY5hJNC0XIXscPFwuUKpe
|
||||||
|
IkYBxLaz1N72VmnGo0IwQDAOBgNVHQ8BAf8EBAMCAQYwDwYDVR0TAQH/BAUwAwEB
|
||||||
|
/zAdBgNVHQ4EFgQUY9VEJcxsPngQJsJ0VC7ZDhjbWAgwCgYIKoZIzj0EAwIDRwAw
|
||||||
|
RAIgS9lRS+ZSM5xCRx3V57zutoPrxaLqZjO3T7WxbgZXesQCIDpLonnDBl9lApMV
|
||||||
|
X6SzSOuotZ6t2dz2bHSxw2KYLPK/
|
||||||
|
-----END CERTIFICATE-----
|
Binary file not shown.
Binary file not shown.
|
@ -0,0 +1,35 @@
|
||||||
|
#!/bin/bash
|
||||||
|
set -euox pipefail
|
||||||
|
|
||||||
|
ca() {
|
||||||
|
filename=$1
|
||||||
|
name=$2
|
||||||
|
echo '{"names":[{"CN": "${name}","OU":"None"}]}' \
|
||||||
|
| cfssl genkey -initca - \
|
||||||
|
| cfssljson -bare ${name}
|
||||||
|
}
|
||||||
|
|
||||||
|
ee() {
|
||||||
|
ca_name=$1
|
||||||
|
ee_name=$2
|
||||||
|
hostname=$3
|
||||||
|
echo '{}' \
|
||||||
|
| cfssl gencert -ca ${ca_name}.pem -ca-key ${ca_name}-key.pem -hostname=${hostname} - \
|
||||||
|
| cfssljson -bare ${ee_name}
|
||||||
|
openssl pkcs8 -topk8 -nocrypt -inform pem -outform der \
|
||||||
|
-in ${ee_name}-key.pem \
|
||||||
|
-out ${ee_name}-${ca_name}.p8
|
||||||
|
openssl x509 -inform pem -outform der \
|
||||||
|
-in ${ee_name}.pem \
|
||||||
|
-out ${ee_name}-${ca_name}.crt
|
||||||
|
rm ${ee_name}.pem
|
||||||
|
}
|
||||||
|
|
||||||
|
ca "Cluster-local CA 1" ca1
|
||||||
|
ca "Cluster-local CA 1" ca2 # Same name, different key pair.
|
||||||
|
ee ca1 foo-ns1 foo.ns1.conduit-managed-pods.conduit.svc.cluster.local
|
||||||
|
ee ca2 foo-ns1 foo.ns1.conduit-managed-pods.conduit.svc.cluster.local # Same, but different CA
|
||||||
|
ee ca1 bar-ns1 bar.ns1.conduit-managed-pods.conduit.svc.cluster.local # Different service.
|
||||||
|
|
||||||
|
rm *-key.pem *.csr
|
||||||
|
|
Loading…
Reference in New Issue