Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

TLS server support #2614

Merged
merged 12 commits into from
Feb 22, 2023
Merged
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changesets/feat_geal_tls_server.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
### TLS server support ([Issue #2615](https://github.com/apollographql/router/issues/2615))

The Router has to provide a TLS server to support HTTP/2 on the client side. This uses the rustls implementation (no TLS versions below 1.2), limited to one server certificate and safe default ciphers.

By [@Geal](https://github.com/Geal) in https://github.com/apollographql/router/pull/2614
1 change: 1 addition & 0 deletions Cargo.lock
Original file line number Diff line number Diff line change
Expand Up @@ -272,6 +272,7 @@ dependencies = [
"test-span",
"thiserror",
"tokio",
"tokio-rustls",
"tokio-stream",
"tokio-util",
"tonic",
Expand Down
1 change: 1 addition & 0 deletions apollo-router/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,7 @@ urlencoding = "2.1.2"
uuid = { version = "1.2.2", features = ["serde", "v4"] }
yaml-rust = "0.4.5"
wsl = "0.1.0"
tokio-rustls = "0.23.4"

[target.'cfg(macos)'.dependencies]
uname = "0.1.1"
Expand Down
37 changes: 23 additions & 14 deletions apollo-router/src/axum_factory/axum_http_server_factory.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,9 @@ use hyper::Body;
use itertools::Itertools;
use multimap::MultiMap;
use serde::Serialize;
use tokio::net::TcpListener;
#[cfg(unix)]
use tokio::net::UnixListener;
use tokio_rustls::TlsAcceptor;
use tower::service_fn;
use tower::BoxError;
use tower::ServiceExt;
Expand Down Expand Up @@ -158,21 +158,30 @@ impl HttpServerFactory for AxumHttpServerFactory {
// if we received a TCP listener, reuse it, otherwise create a new one
let main_listener = match all_routers.main.0.clone() {
ListenAddr::SocketAddr(addr) => {
match main_listener.take().and_then(|listener| {
listener.local_addr().ok().and_then(|l| {
if l == ListenAddr::SocketAddr(addr) {
Some(listener)
let tls_config = configuration
.tls
.supergraph
.as_ref()
.map(|tls| tls.tls_config())
.transpose()?;
let tls_acceptor = tls_config.clone().map(TlsAcceptor::from);

match main_listener.take() {
Some(Listener::Tcp(listener)) => {
if listener.local_addr().ok() == Some(addr) {
Listener::new_from_listener(listener, tls_acceptor)
} else {
None
Listener::new_from_socket_addr(addr, tls_acceptor).await?
}
})
}) {
Some(listener) => listener,
None => Listener::Tcp(
TcpListener::bind(addr)
.await
.map_err(ApolloRouterError::ServerCreationError)?,
),
}
Some(Listener::Tls { listener, .. }) => {
if listener.local_addr().ok() == Some(addr) {
Listener::new_from_listener(listener, tls_acceptor)
} else {
Listener::new_from_socket_addr(addr, tls_acceptor).await?
}
}
_ => Listener::new_from_socket_addr(addr, tls_acceptor).await?,
}
}
#[cfg(unix)]
Expand Down
41 changes: 34 additions & 7 deletions apollo-router/src/axum_factory/listeners.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,17 +12,16 @@ use futures::channel::oneshot;
use futures::prelude::*;
use hyper::server::conn::Http;
use multimap::MultiMap;
use tokio::net::TcpListener;
#[cfg(unix)]
use tokio::net::UnixListener;
use tokio::sync::Notify;

use crate::configuration::Configuration;
use crate::configuration::ListenAddr;
use crate::http_server_factory::Listener;
use crate::http_server_factory::NetworkStream;
use crate::router::ApolloRouterError;
use crate::router_factory::Endpoint;
use crate::ListenAddr;

#[derive(Clone, Debug)]
pub(crate) struct ListenAddrAndRouter(pub(crate) ListenAddr, pub(crate) Router);
Expand Down Expand Up @@ -163,11 +162,7 @@ pub(super) async fn get_extra_listeners(
// if we received a TCP listener, reuse it, otherwise create a new one
#[cfg_attr(not(unix), allow(unused_mut))]
let listener = match listen_addr.clone() {
ListenAddr::SocketAddr(addr) => Listener::Tcp(
TcpListener::bind(addr)
.await
.map_err(ApolloRouterError::ServerCreationError)?,
),
ListenAddr::SocketAddr(addr) => Listener::new_from_socket_addr(addr, None).await?,
#[cfg(unix)]
ListenAddr::UnixSocket(path) => Listener::Unix(
UnixListener::bind(path).map_err(ApolloRouterError::ServerCreationError)?,
Expand Down Expand Up @@ -260,6 +255,38 @@ pub(super) fn serve_router_on_listen_addr(
.http1_keep_alive(true)
.serve_connection(stream, app);

tokio::pin!(connection);
tokio::select! {
// the connection finished first
_res = &mut connection => {
}
// the shutdown receiver was triggered first,
// so we tell the connection to do a graceful shutdown
// on the next request, then we wait for it to finish
_ = connection_shutdown.notified() => {
let c = connection.as_mut();
c.graceful_shutdown();

let _= connection.await;
}
}
},
NetworkStream::Tls(stream) => {
stream.get_ref().0
.set_nodelay(true)
.expect(
"this should not fail unless the socket is invalid",
);

let protocol = stream.get_ref().1.alpn_protocol();
let http2 = protocol == Some(&b"h2"[..]);

let connection = Http::new()
.http1_keep_alive(true)
.http1_header_read_timeout(Duration::from_secs(10))
.http2_only(http2)
.serve_connection(stream, app);

tokio::pin!(connection);
tokio::select! {
// the connection finished first
Expand Down
141 changes: 141 additions & 0 deletions apollo-router/src/configuration/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,22 +11,31 @@ mod yaml;

use std::collections::HashMap;
use std::fmt;
use std::io;
use std::io::BufReader;
use std::net::IpAddr;
use std::net::SocketAddr;
use std::num::NonZeroUsize;
use std::str::FromStr;
use std::sync::Arc;

use derivative::Derivative;
use displaydoc::Display;
use itertools::Itertools;
use once_cell::sync::Lazy;
use regex::Regex;
use rustls::Certificate;
use rustls::PrivateKey;
use rustls::ServerConfig;
use rustls_pemfile::certs;
use rustls_pemfile::rsa_private_keys;
use schemars::gen::SchemaGenerator;
use schemars::schema::ObjectValidation;
use schemars::schema::Schema;
use schemars::schema::SchemaObject;
use schemars::JsonSchema;
use serde::Deserialize;
use serde::Deserializer;
use serde::Serialize;
use serde_json::Map;
use serde_json::Value;
Expand All @@ -40,6 +49,7 @@ pub(crate) use self::schema::generate_upgrade;
use crate::cache::DEFAULT_CACHE_CAPACITY;
use crate::configuration::schema::Mode;
use crate::plugin::plugins;
use crate::ApolloRouterError;

static SUPERGRAPH_ENDPOINT_REGEX: Lazy<Regex> = Lazy::new(|| {
Regex::new(r"(?P<first_path>.*/)(?P<sub_path>.+)\*$")
Expand Down Expand Up @@ -622,9 +632,140 @@ pub(crate) struct RedisCache {
#[serde(deny_unknown_fields)]
#[serde(default)]
pub(crate) struct Tls {
/// TLS server configuration
///
/// this will affect the GraphQL endpoint and any other endpoint targeting the same listen address
pub(crate) supergraph: Option<TlsSupergraph>,
pub(crate) subgraph: TlsSubgraphWrapper,
}

/// Configuration options pertaining to the supergraph server component.
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)]
#[serde(deny_unknown_fields)]
pub(crate) struct TlsSupergraph {
/// server certificate in PEM format
#[serde(deserialize_with = "deserialize_certificate", skip_serializing)]
#[schemars(with = "String")]
pub(crate) certificate: Certificate,
/// server key in PEM format
#[serde(deserialize_with = "deserialize_key", skip_serializing)]
#[schemars(with = "String")]
pub(crate) key: PrivateKey,
/// list of certificate authorities in PEM format
#[serde(deserialize_with = "deserialize_certificate_chain", skip_serializing)]
#[schemars(with = "String")]
pub(crate) certificate_chain: Vec<Certificate>,
}

impl TlsSupergraph {
pub(crate) fn tls_config(&self) -> Result<Arc<rustls::ServerConfig>, ApolloRouterError> {
let mut certificates = vec![self.certificate.clone()];
certificates.extend(self.certificate_chain.iter().cloned());

let mut config = ServerConfig::builder()
.with_safe_defaults()
.with_no_client_auth()
.with_single_cert(certificates, self.key.clone())
.map_err(ApolloRouterError::Rustls)?;
config.alpn_protocols = vec![b"h2".to_vec(), b"http/1.1".to_vec()];

Ok(Arc::new(config))
}
}

fn deserialize_certificate<'de, D>(deserializer: D) -> Result<Certificate, D::Error>
where
D: Deserializer<'de>,
{
let data = String::deserialize(deserializer)?;

load_certs(&data)
.map_err(serde::de::Error::custom)
.and_then(|mut certs| {
if certs.len() > 1 {
Err(serde::de::Error::custom(
"expected exactly one server certificate",
))
} else {
certs.pop().ok_or(serde::de::Error::custom(
"expected exactly one server certificate",
))
}
})
}

fn deserialize_certificate_chain<'de, D>(deserializer: D) -> Result<Vec<Certificate>, D::Error>
where
D: Deserializer<'de>,
{
let data = String::deserialize(deserializer)?;

load_certs(&data).map_err(serde::de::Error::custom)
}

fn deserialize_key<'de, D>(deserializer: D) -> Result<PrivateKey, D::Error>
where
D: Deserializer<'de>,
{
let data = String::deserialize(deserializer)?;

load_keys(&data).map_err(serde::de::Error::custom)
}

fn load_certs(data: &str) -> io::Result<Vec<Certificate>> {
certs(&mut BufReader::new(data.as_bytes()))
.map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "invalid cert"))
.map(|mut certs| certs.drain(..).map(Certificate).collect())
}

//FIXME: handle ECDSA keys too
Geal marked this conversation as resolved.
Show resolved Hide resolved
fn load_keys(data: &str) -> io::Result<PrivateKey> {
/*rsa_private_keys(&mut BufReader::new(data.as_bytes()))
.map_err(|_| serde::de::Error::custom(io::Error::new(io::ErrorKind::InvalidInput, "invalid key")))
.map(|mut keys| keys.drain(..).map(PrivateKey).collect())*/
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we delete this ?


let mut keys = rsa_private_keys(&mut BufReader::new(data.as_bytes()))
.map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "invalid key"))?;
Geal marked this conversation as resolved.
Show resolved Hide resolved

if keys.len() > 1 {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"expected exactly one private key",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
"expected exactly one private key",
"expected exactly one private rsa key",

));
} else if let Some(key) = keys.pop() {
return Ok(PrivateKey(key));
}

let mut keys = rustls_pemfile::pkcs8_private_keys(&mut BufReader::new(data.as_bytes()))
.map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "invalid key"))?;
Geal marked this conversation as resolved.
Show resolved Hide resolved

if keys.len() > 1 {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"expected exactly one private key",
Geal marked this conversation as resolved.
Show resolved Hide resolved
));
} else if let Some(key) = keys.pop() {
return Ok(PrivateKey(key));
}

let mut keys = rustls_pemfile::ec_private_keys(&mut BufReader::new(data.as_bytes()))
.map_err(|_| io::Error::new(io::ErrorKind::InvalidInput, "invalid key"))?;
Geal marked this conversation as resolved.
Show resolved Hide resolved

if keys.len() > 1 {
Err(io::Error::new(
io::ErrorKind::InvalidInput,
"expected exactly one private key",
Geal marked this conversation as resolved.
Show resolved Hide resolved
))
} else if let Some(key) = keys.pop() {
Ok(PrivateKey(key))
} else {
Err(io::Error::new(
io::ErrorKind::InvalidInput,
"expected exactly one private key",
))
}
}

/// Configuration options pertaining to the subgraph server component.
#[derive(Debug, Clone, Deserialize, Serialize, JsonSchema)]
#[serde(deny_unknown_fields)]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3135,6 +3135,7 @@ expression: "&schema"
"tls": {
"description": "TLS related configuration options.",
"default": {
"supergraph": null,
"subgraph": {
"all": {
"certificate_authorities": null
Expand Down Expand Up @@ -3190,6 +3191,35 @@ expression: "&schema"
}
},
"additionalProperties": false
},
"supergraph": {
"description": "TLS server configuration\n\nthis will affect the GraphQL endpoint and any other endpoint targeting the same listen address",
"default": null,
"type": "object",
"required": [
"certificate",
"certificate_chain",
"key"
],
"properties": {
"certificate": {
"description": "server certificate in PEM format",
"writeOnly": true,
"type": "string"
},
"certificate_chain": {
"description": "list of certificate authorities in PEM format",
"writeOnly": true,
"type": "string"
},
"key": {
"description": "server key in PEM format",
"writeOnly": true,
"type": "string"
}
},
"additionalProperties": false,
"nullable": true
}
},
"additionalProperties": false
Expand Down
Loading