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_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_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 dns_min_ttl = parse(strings, ENV_DNS_MIN_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| {
|
||||
// There cannot be a default pod namespace, and the pod namespace is required.
|
||||
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.
|
||||
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?) {
|
||||
(Some(trust_anchors), Some(end_entity_cert), Some(private_key)) =>
|
||||
let namespaces = Namespaces {
|
||||
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 {
|
||||
trust_anchors,
|
||||
end_entity_cert,
|
||||
private_key,
|
||||
})),
|
||||
(_, None, None) => Ok(None), // No TLS in server role.
|
||||
(trust_anchors, end_entity_cert, private_key) => {
|
||||
service_identity,
|
||||
}))
|
||||
},
|
||||
(None, None, None, _) => Ok(None), // No TLS.
|
||||
(trust_anchors, end_entity_cert, private_key, pod_name) => {
|
||||
if trust_anchors.is_none() {
|
||||
error!("{} is not set; it is required when {} and {} are set.",
|
||||
ENV_TLS_TRUST_ANCHORS, ENV_TLS_CERT, ENV_TLS_PRIVATE_KEY);
|
||||
}
|
||||
if end_entity_cert.is_none() {
|
||||
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() {
|
||||
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)
|
||||
},
|
||||
}?;
|
||||
|
||||
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?
|
||||
|
@ -374,10 +386,7 @@ impl<'a> TryFrom<&'a Strings> for Config {
|
|||
|
||||
bind_timeout: bind_timeout?.unwrap_or(DEFAULT_BIND_TIMEOUT),
|
||||
|
||||
namespaces: Namespaces {
|
||||
pod: pod_namespace?,
|
||||
tls_controller: tls_controller_namespace,
|
||||
},
|
||||
namespaces,
|
||||
|
||||
dns_min_ttl: dns_min_ttl?,
|
||||
|
||||
|
|
|
@ -97,7 +97,7 @@ impl BoundPort {
|
|||
// TLS when needed.
|
||||
pub fn listen_and_fold<T, F, Fut>(
|
||||
self,
|
||||
tls_config: tls::ServerConfigWatch,
|
||||
tls: Option<(tls::Identity, tls::ServerConfigWatch)>,
|
||||
initial: T,
|
||||
f: F)
|
||||
-> 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
|
||||
// do it here.
|
||||
set_nodelay_or_warn(&socket);
|
||||
match tls_config.borrow().as_ref() {
|
||||
Some(tls_config) => {
|
||||
Either::A(
|
||||
tls::Connection::accept(socket, tls_config.clone())
|
||||
.map(move |tls| (Connection::new(Box::new(tls)), remote_addr)))
|
||||
},
|
||||
None => Either::B(future::ok((Connection::new(Box::new(socket)), remote_addr))),
|
||||
if let Some((_identity, config_watch)) = &tls {
|
||||
// TODO: use `identity` to differentiate between TLS
|
||||
// that the proxy should terminate vs. TLS that should
|
||||
// be passed through.
|
||||
if let Some(config) = &*config_watch.borrow() {
|
||||
return Either::A(
|
||||
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| {
|
||||
future::ok(match r {
|
||||
|
|
|
@ -582,7 +582,7 @@ fn pb_to_addr_meta(
|
|||
labels.sort_by(|(k0, _), (k1, _)| k0.cmp(k1));
|
||||
|
||||
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,
|
||||
Err(e) => {
|
||||
error!("Failed to parse TLS identity: {:?}", e);
|
||||
|
|
|
@ -245,9 +245,7 @@ where
|
|||
// to the managed application (private destination).
|
||||
let inbound = {
|
||||
let ctx = ctx::Proxy::inbound(&process_ctx);
|
||||
|
||||
let bind = bind.clone().with_ctx(ctx.clone());
|
||||
|
||||
let default_addr = config.private_forward.map(|a| a.into());
|
||||
|
||||
let router = Router::new(
|
||||
|
@ -257,7 +255,7 @@ where
|
|||
);
|
||||
serve(
|
||||
inbound_listener,
|
||||
tls_server_config,
|
||||
config.tls_settings.map(|settings| (settings.service_identity, tls_server_config)),
|
||||
router,
|
||||
config.private_connect_timeout,
|
||||
config.inbound_ports_disable_protocol_detection,
|
||||
|
@ -281,7 +279,7 @@ where
|
|||
);
|
||||
serve(
|
||||
outbound_listener,
|
||||
tls::ServerConfig::no_tls(), // No TLS between service & proxy.
|
||||
None, // No TLS between service & proxy.
|
||||
router,
|
||||
config.public_connect_timeout,
|
||||
config.outbound_ports_disable_protocol_detection,
|
||||
|
@ -348,7 +346,7 @@ where
|
|||
|
||||
fn serve<R, B, E, F, G>(
|
||||
bound_port: BoundPort,
|
||||
tls_config: tls::ServerConfigWatch,
|
||||
tls_config: Option<(tls::Identity, tls::ServerConfigWatch)>,
|
||||
router: Router<R>,
|
||||
tcp_connect_timeout: Duration,
|
||||
disable_protocol_detection_ports: IndexSet<u16>,
|
||||
|
@ -507,7 +505,7 @@ where
|
|||
let fut = {
|
||||
let log = log.clone();
|
||||
bound_port.listen_and_fold(
|
||||
tls::ServerConfig::no_tls(), // TODO: serve over TLS.
|
||||
None, // TODO: serve over TLS.
|
||||
server,
|
||||
move |server, (session, remote)| {
|
||||
let log = log.clone().with_remote(remote);
|
||||
|
|
|
@ -102,7 +102,7 @@ impl Control {
|
|||
let fut = {
|
||||
let log = log.clone();
|
||||
bound_port.listen_and_fold(
|
||||
::tls::ServerConfig::no_tls(), // TODO: Serve over TLS.
|
||||
None, // TODO: Serve over TLS.
|
||||
hyper::server::conn::Http::new(),
|
||||
move |hyper, (conn, remote)| {
|
||||
let service = service.clone();
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
use std::{
|
||||
fmt,
|
||||
sync::Arc,
|
||||
time::SystemTime,
|
||||
};
|
||||
|
||||
use super::{
|
||||
|
@ -16,6 +16,13 @@ pub struct CertResolver {
|
|||
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 {
|
||||
signer: Signer,
|
||||
}
|
||||
|
@ -29,33 +36,18 @@ impl CertResolver {
|
|||
/// Returns a new `CertResolver` that has a certificate (chain) verified to
|
||||
/// 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
|
||||
/// key.
|
||||
pub fn new(
|
||||
root_cert_store: &rustls::RootCertStore,
|
||||
_certificate_was_validated: (), // TODO: `rustls::ServerCertVerified`.
|
||||
cert_chain: Vec<rustls::Certificate>,
|
||||
private_key: untrusted::Input)
|
||||
-> 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)
|
||||
.map_err(|ring::error::Unspecified| config::Error::InvalidPrivateKey)?;
|
||||
|
||||
|
@ -139,4 +131,3 @@ const SIGNATURE_ALG_RUSTLS_SCHEME: rustls::SignatureScheme =
|
|||
rustls::SignatureScheme::ECDSA_NISTP256_SHA256;
|
||||
const SIGNATURE_ALG_RUSTLS_ALGORITHM: rustls::internal::msgs::enums::SignatureAlgorithm =
|
||||
rustls::internal::msgs::enums::SignatureAlgorithm::ECDSA;
|
||||
static SIGNATURE_ALG_WEBPKI: &webpki::SignatureAlgorithm = &webpki::ECDSA_P256_SHA256;
|
||||
|
|
|
@ -8,6 +8,7 @@ use std::{
|
|||
|
||||
use super::{
|
||||
cert_resolver::CertResolver,
|
||||
Identity,
|
||||
|
||||
rustls,
|
||||
untrusted,
|
||||
|
@ -39,10 +40,15 @@ pub struct CommonSettings {
|
|||
|
||||
/// 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.
|
||||
pub struct CommonConfig {
|
||||
#[derive(Debug)]
|
||||
struct CommonConfig {
|
||||
root_cert_store: rustls::RootCertStore,
|
||||
cert_resolver: Arc<CertResolver>,
|
||||
}
|
||||
|
@ -64,9 +70,8 @@ pub type ServerConfigWatch = Watch<Option<ServerConfig>>;
|
|||
pub enum Error {
|
||||
Io(PathBuf, io::Error),
|
||||
FailedToParseTrustAnchors(Option<webpki::Error>),
|
||||
EndEntityCertIsNotValid(webpki::Error),
|
||||
EndEntityCertIsNotValid(rustls::TLSError),
|
||||
InvalidPrivateKey,
|
||||
TimeConversionFailed,
|
||||
}
|
||||
|
||||
impl CommonSettings {
|
||||
|
@ -83,7 +88,7 @@ impl 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.
|
||||
pub fn stream_changes(self, interval: Duration)
|
||||
fn stream_changes(self, interval: Duration)
|
||||
-> impl Stream<Item = CommonConfig, Error = ()>
|
||||
{
|
||||
let paths = self.paths().iter()
|
||||
|
@ -143,8 +148,30 @@ impl CommonConfig {
|
|||
let private_key = load_file_contents(&settings.private_key)?;
|
||||
let private_key = untrusted::Input::from(&private_key);
|
||||
|
||||
// `CertResolver::new` is responsible for the consistency check.
|
||||
let cert_resolver = CertResolver::new(&root_cert_store, cert_chain, 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,
|
||||
|
@ -221,11 +248,6 @@ impl ServerConfig {
|
|||
config.cert_resolver = common.cert_resolver.clone();
|
||||
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> {
|
||||
|
@ -258,3 +280,100 @@ 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]
|
||||
}
|
||||
|
||||
|
||||
#[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
|
||||
/// allowed.
|
||||
#[derive(Clone, Debug, Eq, PartialEq, Hash)]
|
||||
pub struct DnsName(webpki::DNSName);
|
||||
pub struct DnsName(pub(super) webpki::DNSName);
|
||||
|
||||
impl fmt::Display for DnsName {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> Result<(), fmt::Error> {
|
||||
|
|
|
@ -1,11 +1,12 @@
|
|||
use conduit_proxy_controller_grpc;
|
||||
use convert::TryFrom;
|
||||
use super::{DnsName, InvalidDnsName};
|
||||
use super::{DnsName, InvalidDnsName, webpki};
|
||||
use std::sync::Arc;
|
||||
use config::Namespaces;
|
||||
|
||||
/// An endpoint's identity.
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct Identity(Arc<DnsName>);
|
||||
pub struct Identity(pub(super) Arc<DnsName>);
|
||||
|
||||
impl Identity {
|
||||
/// 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
|
||||
/// information is returned.
|
||||
pub fn maybe_from(
|
||||
pb: conduit_proxy_controller_grpc::destination::TlsIdentity,
|
||||
controller_namespace: Option<&str>)
|
||||
pub fn maybe_from_protobuf(
|
||||
controller_namespace: Option<&str>,
|
||||
pb: conduit_proxy_controller_grpc::destination::TlsIdentity)
|
||||
-> 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
|
||||
// join it using '.' to try to form a DNS name. The rest of the DNS
|
||||
// name rules will be enforced by `DnsName::try_from`.
|
||||
|
@ -31,42 +54,39 @@ impl Identity {
|
|||
}
|
||||
}
|
||||
|
||||
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(());
|
||||
}
|
||||
let controller_ns = if let Some(controller_ns) = &namespaces.tls_controller {
|
||||
controller_ns
|
||||
} else {
|
||||
error!("controller namespace not provided");
|
||||
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.
|
||||
// Log any/any per-component errors before returning.
|
||||
let controller_ns_check = check_single_label(controller_ns, "controller namespace");
|
||||
let pod_ns_check = check_single_label(&namespaces.pod, "pod namespace");
|
||||
let pod_name_check = check_single_label(pod_name, "pod name");
|
||||
if controller_ns_check.is_err() || pod_ns_check.is_err() || pod_name_check.is_err() {
|
||||
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 = 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