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 all 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 @@ -271,6 +271,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 @@ -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",
Geal marked this conversation as resolved.
Show resolved Hide resolved
));
}
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 @@ -4296,6 +4296,7 @@ expression: "&schema"
"tls": {
"description": "TLS related configuration options.",
"default": {
"supergraph": null,
"subgraph": {
"all": {
"certificate_authorities": null
Expand Down Expand Up @@ -4351,6 +4352,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