Skip to content

Commit

Permalink
TLS server support (#2614)
Browse files Browse the repository at this point in the history
This allows TLS for the GraphQL endpoint and any other endpoint using
the same listen address. For now, the configuration is limited to one
server certificate, TLS 1.2 and 1.3 (rustls does not support lower
versions), safe default ciphers and no client authentication.
  • Loading branch information
Geal authored Feb 22, 2023
1 parent 76aaa5e commit 7fdb0a5
Show file tree
Hide file tree
Showing 12 changed files with 388 additions and 22 deletions.
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 @@ -270,6 +270,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 @@ -202,6 +202,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 @@ -25,9 +25,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 @@ -176,21 +176,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
132 changes: 132 additions & 0 deletions apollo-router/src/configuration/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,22 +12,33 @@ mod yaml;

use std::collections::HashMap;
use std::fmt;
use std::io;
use std::io::BufReader;
use std::iter;
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::read_one;
use rustls_pemfile::Item;
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 @@ -41,6 +52,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 @@ -623,9 +635,129 @@ 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())
}

fn load_keys(data: &str) -> io::Result<PrivateKey> {
let mut reader = BufReader::new(data.as_bytes());
let mut key_iterator = iter::from_fn(|| read_one(&mut reader).transpose());

let private_key = match key_iterator.next() {
Some(Ok(Item::RSAKey(key))) => PrivateKey(key),
Some(Ok(Item::PKCS8Key(key))) => PrivateKey(key),
Some(Ok(Item::ECKey(key))) => PrivateKey(key),
Some(Err(e)) => {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
format!("could not parse the key: {e}"),
))
}
Some(_) => {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"expected a private key",
))
}
None => {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"could not find a private key",
))
}
};

if key_iterator.next().is_some() {
return Err(io::Error::new(
io::ErrorKind::InvalidInput,
"expected exactly one private key",
));
}
Ok(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 @@ -4291,6 +4291,7 @@ expression: "&schema"
"tls": {
"description": "TLS related configuration options.",
"default": {
"supergraph": null,
"subgraph": {
"all": {
"certificate_authorities": null
Expand Down Expand Up @@ -4346,6 +4347,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
34 changes: 34 additions & 0 deletions apollo-router/src/configuration/testdata/server.crt
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
-----BEGIN CERTIFICATE-----
MIIF9TCCA92gAwIBAgIUM+6LSYfTRzSalYzqdFfuPbcznyswDQYJKoZIhvcNAQEL
BQAwQDELMAkGA1UEBhMCRlIxCjAIBgNVBAgMASAxCjAIBgNVBAoMASAxGTAXBgNV
BAMMEGxvY2FsLmFwb2xsby5kZXYwHhcNMjIxMjE2MTQ1OTMzWhcNMjMwMTE1MTQ1
OTMzWjBAMQswCQYDVQQGEwJGUjEKMAgGA1UECAwBIDEKMAgGA1UECgwBIDEZMBcG
A1UEAwwQbG9jYWwuYXBvbGxvLmRldjCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCC
AgoCggIBALK52xtnhD1MJEuuXbLlEU3tcPO3MIWYFY2i+rTyYQYKxa5a4QG9vBjB
bQb18b2xXVxmCs57MYt9v5GQrU4Dc55qWXVzSFK3wLD8PvS+NaTkjh+TH1MbW8Rr
BVxTq1XD0HJAJfXdbTlg62VoKk6UXFk+YH/In+u1UAq0T1amC39B8hiTFNd2Yawg
SKn4i+6NmZluzIb88ZLzRb2xrnEd2FG4JAucPHpTjmNtwFzl3nmbgMNKntLA3Ac+
CdaIWuPqkbDEDzR5mP8tx2IzUSz3C08Z1Oo+8uS5aOyWg8l4MPBhyWONFA8ilvd3
+yjzPKwa/zFEozoUp5GWSWLl53Ff6anw54yUIND0qhD5X4ICtOk2F41Gwv/GKTSO
AnfwpxZiUji2OOZwXQ/Zs+lUXTgQvshvb6PXbJT6T3wxou+WpVJFDctELBNdMNbe
WldtYvPry7rngLWOUsLq6c/oQibvL19Pc98532LKsWFsYEMRVA7WNsyj040Y9FoO
zBgvZ/AyxgT/23/P9xJxg0RjqOkO8jPx5kpDOL9c8qkKds7CQ5z4d3DZuzLjfKUw
pT3u10an1nh5xmcSH9vLnZuDoJL9OzJdmQ8pEdKdQGTTP4KXM/OUh8+sSxFyLoYV
850SeydMTTm72GkWzwq2npp2KNo41F0mT2eyvQSNy0PIN6eSRgpnAgMBAAGjgeYw
geMwHQYDVR0OBBYEFDV0smlfWnSnE/PtxF65lwn5ewgrMHsGA1UdIwR0MHKAFDV0
smlfWnSnE/PtxF65lwn5ewgroUSkQjBAMQswCQYDVQQGEwJGUjEKMAgGA1UECAwB
IDEKMAgGA1UECgwBIDEZMBcGA1UEAwwQbG9jYWwuYXBvbGxvLmRldoIUM+6LSYfT
RzSalYzqdFfuPbcznyswCwYDVR0PBAQDAgL8MBsGA1UdEQQUMBKCEGxvY2FsLmFw
b2xsby5kZXYwGwYDVR0SBBQwEoIQbG9jYWwuYXBvbGxvLmRldjANBgkqhkiG9w0B
AQsFAAOCAgEAZtNEFzXO0phLUQm2yRZzOA5OPwsW97iMBUc2o5tP8jkkmWTNMWHe
1COAkPVBpPS+lbCAMdoMd+yrQ/tZdMmVvqXYMc087ZkIGeIG8NOHWJkBoAlV3mYP
feb8nbbZBHLzZUgj8p77sQeCR3gbodeUHoB3YEgb/btz39r6zYBdBcbrhU1D4Qki
+xpd1iYdo/qI9TwgnEavcIZ4Zpv7T6IvxPXQ6WjWofXlb3G8atm5lL88TxMszHv4
d2A3giMd4wv66usme9CN2kFKV568eQqnqAzY+bNGdAVlLX2ieWCKT9NmUhHc8b1M
oaS6E/qlcOT4c+F8/kDcW35TasPuzLEH8YBrn+e+rl0etv6DJL3gBqMciJNJ0DSj
YW0inRx6VaQCH0iqzeKjy7bas6Mj/emfkmMIuzL2UVFE2khfMqbpaR9Uat4jbIzH
Pfh5zF40bklOqA5axztJurKWv5deEof5PZ5jLx47VIU3VrwYmIUEUpOdEi426LwX
0TSEG0P8d82UqU+mh7Ibcd1KWTmmwA7pJ9hsN6n2VYhogojh1n1lwDH0g6ND6+mh
LOGdw2a3DeyWSZNl/HRezyq983gbK/1U2DeuoxzAC8axEJa4iRRBWMKX7XdBuuHD
wj3nI/0PXcNFsiaB7qPpIFCv7F9fw44tdh58mCdQSWC+JeJp43E9Wfg=
-----END CERTIFICATE-----
Loading

0 comments on commit 7fdb0a5

Please sign in to comment.