mirror of https://github.com/linkerd/linkerd2.git
479 lines
17 KiB
Rust
479 lines
17 KiB
Rust
use std::{
|
|
self,
|
|
fs::File,
|
|
io::{self, Cursor, Read},
|
|
path::PathBuf,
|
|
sync::Arc,
|
|
time::Duration,
|
|
};
|
|
|
|
use super::{
|
|
cert_resolver::CertResolver,
|
|
Identity,
|
|
|
|
rustls,
|
|
untrusted,
|
|
webpki,
|
|
};
|
|
use conditional::Conditional;
|
|
use futures::{future, stream, Future, Stream};
|
|
use futures_watch::Watch;
|
|
use ring::signature;
|
|
|
|
/// Not-yet-validated settings that are used for both TLS clients and TLS
|
|
/// servers.
|
|
///
|
|
/// The trust anchors are stored in PEM format because, in Kubernetes, they are
|
|
/// stored in a ConfigMap, and until very recently Kubernetes cannot store
|
|
/// binary data in ConfigMaps. Also, PEM is the most interoperable way to
|
|
/// distribute trust anchors, especially if it is desired to support multiple
|
|
/// trust anchors at once.
|
|
///
|
|
/// The end-entity certificate and private key are in DER format because they
|
|
/// are stored in the secret store where space utilization is a concern, and
|
|
/// because PEM doesn't offer any advantages.
|
|
#[derive(Clone, Debug)]
|
|
pub struct CommonSettings {
|
|
/// The trust anchors as concatenated PEM-encoded X.509 certificates.
|
|
pub trust_anchors: PathBuf,
|
|
|
|
/// The end-entity certificate as a DER-encoded X.509 certificate.
|
|
pub end_entity_cert: PathBuf,
|
|
|
|
/// The private key in DER-encoded PKCS#8 form.
|
|
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.
|
|
#[derive(Debug)]
|
|
struct CommonConfig {
|
|
root_cert_store: rustls::RootCertStore,
|
|
cert_resolver: Arc<CertResolver>,
|
|
}
|
|
|
|
|
|
/// Validated configuration for TLS servers.
|
|
#[derive(Clone)]
|
|
pub struct ClientConfig(pub(super) Arc<rustls::ClientConfig>);
|
|
|
|
/// XXX: `rustls::ClientConfig` doesn't implement `Debug` yet.
|
|
impl std::fmt::Debug for ClientConfig {
|
|
fn fmt(&self, f: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> {
|
|
f.debug_struct("ClientConfig")
|
|
.finish()
|
|
}
|
|
}
|
|
|
|
/// Validated configuration for TLS servers.
|
|
#[derive(Clone)]
|
|
pub struct ServerConfig(pub(super) Arc<rustls::ServerConfig>);
|
|
|
|
pub type ClientConfigWatch = Watch<Option<ClientConfig>>;
|
|
pub type ServerConfigWatch = Watch<Option<ServerConfig>>;
|
|
|
|
/// The configuration in effect for a client (`ClientConfig`) or server
|
|
/// (`ServerConfig`) TLS connection.
|
|
#[derive(Clone, Debug)]
|
|
pub struct ConnectionConfig<C> where C: Clone + std::fmt::Debug {
|
|
pub identity: Identity,
|
|
pub config: C,
|
|
}
|
|
|
|
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
|
|
pub enum ReasonForNoTls {
|
|
/// TLS is disabled.
|
|
Disabled,
|
|
|
|
/// TLS was enabled but the configuration isn't available (yet).
|
|
NoConfig,
|
|
|
|
/// TLS isn't implemented for the connection between the proxy and the
|
|
/// control plane yet.
|
|
NotImplementedForControlPlane,
|
|
|
|
/// TLS is only enabled for HTTP (HTTPS) right now.
|
|
NotImplementedForNonHttp,
|
|
|
|
}
|
|
|
|
pub type ConditionalConnectionConfig<C> = Conditional<ConnectionConfig<C>, ReasonForNoTls>;
|
|
|
|
#[derive(Debug)]
|
|
pub enum Error {
|
|
Io(PathBuf, io::Error),
|
|
FailedToParseTrustAnchors(Option<webpki::Error>),
|
|
EndEntityCertIsNotValid(rustls::TLSError),
|
|
InvalidPrivateKey,
|
|
}
|
|
|
|
impl CommonSettings {
|
|
fn paths(&self) -> [&PathBuf; 3] {
|
|
[
|
|
&self.trust_anchors,
|
|
&self.end_entity_cert,
|
|
&self.private_key,
|
|
]
|
|
}
|
|
|
|
/// Stream changes to the files described by this `CommonSettings`.
|
|
///
|
|
/// The returned stream consists of each subsequent successfully loaded
|
|
/// `CommonSettings` after each change. If the settings could not be
|
|
/// reloaded (i.e., they were malformed), nothing is sent.
|
|
fn stream_changes(self, interval: Duration)
|
|
-> impl Stream<Item = CommonConfig, Error = ()>
|
|
{
|
|
let paths = self.paths().iter()
|
|
.map(|&p| p.clone())
|
|
.collect::<Vec<_>>();
|
|
// Generate one "change" immediately before starting to watch
|
|
// the files, so that we'll try to load them now if they exist.
|
|
stream::once(Ok(()))
|
|
.chain(::fs_watch::stream_changes(paths, interval))
|
|
.filter_map(move |_| {
|
|
CommonConfig::load_from_disk(&self)
|
|
.map_err(|e| warn!("error reloading TLS config: {:?}, falling back", e))
|
|
.ok()
|
|
})
|
|
}
|
|
|
|
}
|
|
|
|
impl CommonConfig {
|
|
/// Loads a configuration from the given files and validates it. If an
|
|
/// error is returned then the caller should try again after the files are
|
|
/// updated.
|
|
///
|
|
/// In a valid configuration, all the files need to be in sync with each
|
|
/// other. For example, the private key file must contain the private
|
|
/// key for the end-entity certificate, and the end-entity certificate
|
|
/// must be issued by the CA represented by a certificate in the
|
|
/// trust anchors file. Since filesystem operations are not atomic, we
|
|
/// need to check for this consistency.
|
|
fn load_from_disk(settings: &CommonSettings) -> Result<Self, Error> {
|
|
let root_cert_store = load_file_contents(&settings.trust_anchors)
|
|
.and_then(|file_contents| {
|
|
let mut root_cert_store = rustls::RootCertStore::empty();
|
|
let (added, skipped) = root_cert_store.add_pem_file(&mut Cursor::new(file_contents))
|
|
.map_err(|err| {
|
|
error!("error parsing trust anchors file: {:?}", err);
|
|
Error::FailedToParseTrustAnchors(None)
|
|
})?;
|
|
if skipped != 0 {
|
|
warn!("skipped {} trust anchors in trust anchors file", skipped);
|
|
}
|
|
if added > 0 {
|
|
Ok(root_cert_store)
|
|
} else {
|
|
error!("no valid trust anchors in trust anchors file");
|
|
Err(Error::FailedToParseTrustAnchors(None))
|
|
}
|
|
})?;
|
|
|
|
let end_entity_cert = load_file_contents(&settings.end_entity_cert)?;
|
|
|
|
// XXX: Assume there are no intermediates since there is no way to load
|
|
// them yet.
|
|
let cert_chain = vec![rustls::Certificate(end_entity_cert)];
|
|
|
|
// Load the private key after we've validated the certificate.
|
|
let private_key = load_file_contents(&settings.private_key)?;
|
|
let private_key = untrusted::Input::from(&private_key);
|
|
|
|
// Ensure the certificate is valid for the services we terminate for
|
|
// 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 {
|
|
root_cert_store,
|
|
cert_resolver: Arc::new(cert_resolver),
|
|
})
|
|
}
|
|
|
|
}
|
|
|
|
pub fn watch_for_config_changes(settings: Option<&CommonSettings>)
|
|
-> (ClientConfigWatch, ServerConfigWatch, Box<Future<Item = (), Error = ()> + Send>)
|
|
{
|
|
let settings = if let Some(settings) = settings {
|
|
settings.clone()
|
|
} else {
|
|
let (client_watch, _) = Watch::new(None);
|
|
let (server_watch, _) = Watch::new(None);
|
|
let no_future = future::ok(());
|
|
return (client_watch, server_watch, Box::new(no_future));
|
|
};
|
|
|
|
let changes = settings.stream_changes(Duration::from_secs(1));
|
|
let (client_watch, client_store) = Watch::new(None);
|
|
let (server_watch, server_store) = Watch::new(None);
|
|
|
|
// `Store::store` will return an error iff all watchers have been dropped,
|
|
// so we'll use `fold` to cancel the forwarding future. Eventually, we can
|
|
// also use the fold to continue tracking previous states if we need to do
|
|
// that.
|
|
let f = changes
|
|
.fold(
|
|
(client_store, server_store),
|
|
|(mut client_store, mut server_store), ref config| {
|
|
client_store
|
|
.store(Some(ClientConfig::from(config)))
|
|
.map_err(|_| trace!("all client config watchers dropped"))?;
|
|
server_store
|
|
.store(Some(ServerConfig::from(config)))
|
|
.map_err(|_| trace!("all server config watchers dropped"))?;
|
|
Ok((client_store, server_store))
|
|
})
|
|
.then(|_| {
|
|
trace!("forwarding to server config watch finished.");
|
|
Ok(())
|
|
});
|
|
|
|
// This function and `ServerConfig::no_tls` return `Box<Future<...>>`
|
|
// rather than `impl Future<...>` so that they can have the _same_ return
|
|
// types (impl Traits are not the same type unless the original
|
|
// non-anonymized type was the same).
|
|
(client_watch, server_watch, Box::new(f))
|
|
}
|
|
|
|
impl ClientConfig {
|
|
fn from(common: &CommonConfig) -> Self {
|
|
let mut config = rustls::ClientConfig::new();
|
|
set_common_settings(&mut config.versions);
|
|
|
|
// XXX: Rustls's built-in verifiers don't let us tweak things as fully
|
|
// as we'd like (e.g. controlling the set of trusted signature
|
|
// algorithms), but they provide good enough defaults for now.
|
|
// TODO: lock down the verification further.
|
|
// TODO: Change Rustls's API to Avoid needing to clone `root_cert_store`.
|
|
config.root_store = common.root_cert_store.clone();
|
|
|
|
// Disable session resumption for the time-being until resumption is
|
|
// more tested.
|
|
config.enable_tickets = false;
|
|
|
|
// Enable client authentication if and only if we were configured for
|
|
// it.
|
|
config.client_auth_cert_resolver = common.cert_resolver.clone();
|
|
|
|
ClientConfig(Arc::new(config))
|
|
}
|
|
|
|
/// Some tests aren't set up to do TLS yet, but we require a
|
|
/// `ClientConfigWatch`. We can't use `#[cfg(test)]` here because the
|
|
/// benchmarks use this.
|
|
pub fn no_tls() -> ClientConfigWatch {
|
|
let (watch, _) = Watch::new(None);
|
|
watch
|
|
}
|
|
}
|
|
|
|
impl ServerConfig {
|
|
fn from(common: &CommonConfig) -> Self {
|
|
// Ask TLS clients for a certificate and accept any certificate issued
|
|
// by our trusted CA(s).
|
|
//
|
|
// Initially, also allow TLS clients that don't have a client
|
|
// certificate too, to minimize risk. TODO: Use
|
|
// `AllowAnyAuthenticatedClient` to require a valid certificate.
|
|
//
|
|
// XXX: Rustls's built-in verifiers don't let us tweak things as fully
|
|
// as we'd like (e.g. controlling the set of trusted signature
|
|
// algorithms), but they provide good enough defaults for now.
|
|
// TODO: lock down the verification further.
|
|
//
|
|
// TODO: Change Rustls's API to Avoid needing to clone `root_cert_store`.
|
|
let client_cert_verifier =
|
|
rustls::AllowAnyAnonymousOrAuthenticatedClient::new(common.root_cert_store.clone());
|
|
|
|
let mut config = rustls::ServerConfig::new(client_cert_verifier);
|
|
set_common_settings(&mut config.versions);
|
|
config.cert_resolver = common.cert_resolver.clone();
|
|
ServerConfig(Arc::new(config))
|
|
}
|
|
}
|
|
|
|
pub fn current_connection_config<C>(identity: Option<&Identity>, watch: &Watch<Option<C>>)
|
|
-> ConditionalConnectionConfig<C> where C: Clone + std::fmt::Debug
|
|
{
|
|
match identity {
|
|
Some(identity) => {
|
|
match *watch.borrow() {
|
|
Some(ref config) =>
|
|
Conditional::Some(ConnectionConfig {
|
|
identity: identity.clone(),
|
|
config: config.clone()
|
|
}),
|
|
None => Conditional::None(ReasonForNoTls::NoConfig),
|
|
}
|
|
},
|
|
None => Conditional::None(ReasonForNoTls::Disabled),
|
|
}
|
|
}
|
|
|
|
fn load_file_contents(path: &PathBuf) -> Result<Vec<u8>, Error> {
|
|
fn load_file(path: &PathBuf) -> Result<Vec<u8>, io::Error> {
|
|
let mut result = Vec::new();
|
|
let mut file = File::open(path)?;
|
|
loop {
|
|
match file.read_to_end(&mut result) {
|
|
Ok(_) => {
|
|
return Ok(result);
|
|
},
|
|
Err(e) => {
|
|
if e.kind() != io::ErrorKind::Interrupted {
|
|
return Err(e);
|
|
}
|
|
},
|
|
}
|
|
}
|
|
}
|
|
|
|
load_file(path)
|
|
.map(|contents| {
|
|
trace!("loaded file {:?}", path);
|
|
contents
|
|
})
|
|
.map_err(|e| Error::Io(path.clone(), e))
|
|
}
|
|
|
|
fn set_common_settings(versions: &mut Vec<rustls::ProtocolVersion>) {
|
|
// Only enable TLS 1.2 until TLS 1.3 is stable.
|
|
*versions = vec![rustls::ProtocolVersion::TLSv1_2]
|
|
|
|
// XXX: Rustls doesn't provide a good way to customize the cipher suite
|
|
// support, so just use its defaults, which are still pretty good.
|
|
// TODO: Expand Rustls's API to allow us to clearly whitelist the cipher
|
|
// suites we want to enable.
|
|
}
|
|
|
|
// Keep these in sync.
|
|
pub(super) static SIGNATURE_ALG_RING_SIGNING: &signature::SigningAlgorithm =
|
|
&signature::ECDSA_P256_SHA256_ASN1_SIGNING;
|
|
pub(super) const SIGNATURE_ALG_RUSTLS_SCHEME: rustls::SignatureScheme =
|
|
rustls::SignatureScheme::ECDSA_NISTP256_SHA256;
|
|
pub(super) const SIGNATURE_ALG_RUSTLS_ALGORITHM: rustls::internal::msgs::enums::SignatureAlgorithm =
|
|
rustls::internal::msgs::enums::SignatureAlgorithm::ECDSA;
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use tls::{ClientConfig, 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_client_and_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 _: ClientConfig = ClientConfig::from(&config); // Infallible.
|
|
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),
|
|
}
|
|
}
|
|
}
|