Add TLS capability to the beacon node HTTP API (#2668)

Currently, the beacon node has no ability to serve the HTTP API over TLS.
Adding this functionality would be helpful for certain use cases, such as when you need a validator client to connect to a backup beacon node which is outside your local network, and the use of an SSH tunnel or reverse proxy would be inappropriate.

## Proposed Changes

- Add three new CLI flags to the beacon node
  - `--http-enable-tls`: enables TLS
  - `--http-tls-cert`: to specify the path to the certificate file
  - `--http-tls-key`: to specify the path to the key file
- Update the HTTP API to optionally use `warp`'s [`TlsServer`](https://docs.rs/warp/0.3.1/warp/struct.TlsServer.html) depending on the presence of the `--http-enable-tls` flag
- Update tests and docs
- Use a custom branch for `warp` to ensure proper error handling

## Additional Info

Serving the API over TLS should currently be considered experimental. The reason for this is that it uses code from an [unmerged PR](https://github.com/seanmonstar/warp/pull/717). This commit provides the `try_bind_with_graceful_shutdown` method to `warp`, which is helpful for controlling error flow when the TLS configuration is invalid (cert/key files don't exist, incorrect permissions, etc). 
I've implemented the same code in my [branch here](https://github.com/macladson/warp/tree/tls).

Once the code has been reviewed and merged upstream into `warp`, we can remove the dependency on my branch and the feature can be considered more stable.

Currently, the private key file must not be password-protected in order to be read into Lighthouse.
This commit is contained in:
Mac L 2021-10-12 03:35:49 +00:00
parent 0aee7ec873
commit a73d698e30
12 changed files with 191 additions and 17 deletions

15
Cargo.lock generated
View File

@ -489,6 +489,7 @@ dependencies = [
"futures", "futures",
"genesis", "genesis",
"hex", "hex",
"http_api",
"hyper", "hyper",
"lighthouse_version", "lighthouse_version",
"monitoring_api", "monitoring_api",
@ -6208,6 +6209,17 @@ dependencies = [
"tokio", "tokio",
] ]
[[package]]
name = "tokio-rustls"
version = "0.22.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bc6844de72e57df1980054b38be3a9f4702aba4858be64dd700181a8a6d0e1b6"
dependencies = [
"rustls",
"tokio",
"webpki",
]
[[package]] [[package]]
name = "tokio-stream" name = "tokio-stream"
version = "0.1.7" version = "0.1.7"
@ -6799,7 +6811,7 @@ dependencies = [
[[package]] [[package]]
name = "warp" name = "warp"
version = "0.3.0" version = "0.3.0"
source = "git+https://github.com/paulhauner/warp?branch=cors-wildcard#1f7daf462e6286fe5fd1743f7b788227efd3fa5c" source = "git+https://github.com/macladson/warp?rev=dfa259e#dfa259e19b7490e6bc4bf247e8b76f671d29a0eb"
dependencies = [ dependencies = [
"bytes 1.1.0", "bytes 1.1.0",
"futures", "futures",
@ -6817,6 +6829,7 @@ dependencies = [
"serde_json", "serde_json",
"serde_urlencoded", "serde_urlencoded",
"tokio", "tokio",
"tokio-rustls",
"tokio-stream", "tokio-stream",
"tokio-tungstenite", "tokio-tungstenite",
"tokio-util", "tokio-util",

View File

@ -38,3 +38,4 @@ hex = "0.4.2"
slasher = { path = "../slasher" } slasher = { path = "../slasher" }
monitoring_api = { path = "../common/monitoring_api" } monitoring_api = { path = "../common/monitoring_api" }
sensitive_url = { path = "../common/sensitive_url" } sensitive_url = { path = "../common/sensitive_url" }
http_api = { path = "http_api" }

View File

@ -6,7 +6,7 @@ edition = "2018"
autotests = false # using a single test binary compiles faster autotests = false # using a single test binary compiles faster
[dependencies] [dependencies]
warp = { git = "https://github.com/paulhauner/warp ", branch = "cors-wildcard" } warp = { git = "https://github.com/macladson/warp", rev ="dfa259e", features = ["tls"] }
serde = { version = "1.0.116", features = ["derive"] } serde = { version = "1.0.116", features = ["derive"] }
tokio = { version = "1.10.0", features = ["macros","sync"] } tokio = { version = "1.10.0", features = ["macros","sync"] }
tokio-stream = { version = "0.1.3", features = ["sync"] } tokio-stream = { version = "0.1.3", features = ["sync"] }

View File

@ -36,6 +36,8 @@ use std::borrow::Cow;
use std::convert::TryInto; use std::convert::TryInto;
use std::future::Future; use std::future::Future;
use std::net::{Ipv4Addr, SocketAddr, SocketAddrV4}; use std::net::{Ipv4Addr, SocketAddr, SocketAddrV4};
use std::path::PathBuf;
use std::pin::Pin;
use std::sync::Arc; use std::sync::Arc;
use tokio::sync::mpsc::UnboundedSender; use tokio::sync::mpsc::UnboundedSender;
use tokio_stream::{wrappers::BroadcastStream, StreamExt}; use tokio_stream::{wrappers::BroadcastStream, StreamExt};
@ -61,6 +63,16 @@ const API_PREFIX: &str = "eth";
/// finalized head. /// finalized head.
const SYNC_TOLERANCE_EPOCHS: u64 = 8; const SYNC_TOLERANCE_EPOCHS: u64 = 8;
/// A custom type which allows for both unsecured and TLS-enabled HTTP servers.
type HttpServer = (SocketAddr, Pin<Box<dyn Future<Output = ()> + Send>>);
/// Configuration used when serving the HTTP server over TLS.
#[derive(PartialEq, Debug, Clone, Serialize, Deserialize)]
pub struct TlsConfig {
pub cert: PathBuf,
pub key: PathBuf,
}
/// A wrapper around all the items required to spawn the HTTP server. /// A wrapper around all the items required to spawn the HTTP server.
/// ///
/// The server will gracefully handle the case where any fields are `None`. /// The server will gracefully handle the case where any fields are `None`.
@ -81,6 +93,7 @@ pub struct Config {
pub listen_port: u16, pub listen_port: u16,
pub allow_origin: Option<String>, pub allow_origin: Option<String>,
pub serve_legacy_spec: bool, pub serve_legacy_spec: bool,
pub tls_config: Option<TlsConfig>,
} }
impl Default for Config { impl Default for Config {
@ -91,6 +104,7 @@ impl Default for Config {
listen_port: 5052, listen_port: 5052,
allow_origin: None, allow_origin: None,
serve_legacy_spec: true, serve_legacy_spec: true,
tls_config: None,
} }
} }
} }
@ -218,7 +232,7 @@ pub fn prometheus_metrics() -> warp::filters::log::Log<impl Fn(warp::filters::lo
pub fn serve<T: BeaconChainTypes>( pub fn serve<T: BeaconChainTypes>(
ctx: Arc<Context<T>>, ctx: Arc<Context<T>>,
shutdown: impl Future<Output = ()> + Send + Sync + 'static, shutdown: impl Future<Output = ()> + Send + Sync + 'static,
) -> Result<(SocketAddr, impl Future<Output = ()>), Error> { ) -> Result<HttpServer, Error> {
let config = ctx.config.clone(); let config = ctx.config.clone();
let log = ctx.log.clone(); let log = ctx.log.clone();
@ -2587,22 +2601,37 @@ pub fn serve<T: BeaconChainTypes>(
.map(|reply| warp::reply::with_header(reply, "Server", &version_with_platform())) .map(|reply| warp::reply::with_header(reply, "Server", &version_with_platform()))
.with(cors_builder.build()); .with(cors_builder.build());
let (listening_socket, server) = { let http_socket: SocketAddrV4 = SocketAddrV4::new(config.listen_addr, config.listen_port);
warp::serve(routes).try_bind_with_graceful_shutdown( let http_server: HttpServer = match config.tls_config {
SocketAddrV4::new(config.listen_addr, config.listen_port), Some(tls_config) => {
async { let (socket, server) = warp::serve(routes)
shutdown.await; .tls()
}, .cert_path(tls_config.cert)
)? .key_path(tls_config.key)
.try_bind_with_graceful_shutdown(http_socket, async {
shutdown.await;
})?;
info!(log, "HTTP API is being served over TLS";);
(socket, Box::pin(server))
}
None => {
let (socket, server) =
warp::serve(routes).try_bind_with_graceful_shutdown(http_socket, async {
shutdown.await;
})?;
(socket, Box::pin(server))
}
}; };
info!( info!(
log, log,
"HTTP API started"; "HTTP API started";
"listen_address" => listening_socket.to_string(), "listen_address" => %http_server.0,
); );
Ok((listening_socket, server)) Ok(http_server)
} }
/// Publish a message to the libp2p pubsub network. /// Publish a message to the libp2p pubsub network.

View File

@ -131,6 +131,7 @@ pub async fn create_api_server<T: BeaconChainTypes>(
listen_port: 0, listen_port: 0,
allow_origin: None, allow_origin: None,
serve_legacy_spec: true, serve_legacy_spec: true,
tls_config: None,
}, },
chain: Some(chain.clone()), chain: Some(chain.clone()),
network_tx: Some(network_tx), network_tx: Some(network_tx),

View File

@ -7,7 +7,7 @@ edition = "2018"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies] [dependencies]
warp = { git = "https://github.com/paulhauner/warp ", branch = "cors-wildcard" } warp = { git = "https://github.com/macladson/warp", rev ="dfa259e" }
serde = { version = "1.0.116", features = ["derive"] } serde = { version = "1.0.116", features = ["derive"] }
slog = "2.5.2" slog = "2.5.2"
beacon_chain = { path = "../beacon_chain" } beacon_chain = { path = "../beacon_chain" }

View File

@ -217,6 +217,29 @@ pub fn cli_app<'a, 'b>() -> App<'a, 'b> {
.help("Disable serving of legacy data on the /config/spec endpoint. May be \ .help("Disable serving of legacy data on the /config/spec endpoint. May be \
disabled by default in a future release.") disabled by default in a future release.")
) )
.arg(
Arg::with_name("http-enable-tls")
.long("http-enable-tls")
.help("Serves the RESTful HTTP API server over TLS. This feature is currently \
experimental.")
.takes_value(false)
.requires("http-tls-cert")
.requires("http-tls-key")
)
.arg(
Arg::with_name("http-tls-cert")
.long("http-tls-cert")
.help("The path of the certificate to be used when serving the HTTP API server \
over TLS.")
.takes_value(true)
)
.arg(
Arg::with_name("http-tls-key")
.long("http-tls-key")
.help("The path of the private key to be used when serving the HTTP API server \
over TLS. Must not be password-protected.")
.takes_value(true)
)
/* Prometheus metrics HTTP server related arguments */ /* Prometheus metrics HTTP server related arguments */
.arg( .arg(
Arg::with_name("metrics") Arg::with_name("metrics")

View File

@ -4,6 +4,7 @@ use client::{ClientConfig, ClientGenesis};
use directory::{DEFAULT_BEACON_NODE_DIR, DEFAULT_NETWORK_DIR, DEFAULT_ROOT_DIR}; use directory::{DEFAULT_BEACON_NODE_DIR, DEFAULT_NETWORK_DIR, DEFAULT_ROOT_DIR};
use eth2_libp2p::{multiaddr::Protocol, Enr, Multiaddr, NetworkConfig, PeerIdSerialized}; use eth2_libp2p::{multiaddr::Protocol, Enr, Multiaddr, NetworkConfig, PeerIdSerialized};
use eth2_network_config::{Eth2NetworkConfig, DEFAULT_HARDCODED_NETWORK}; use eth2_network_config::{Eth2NetworkConfig, DEFAULT_HARDCODED_NETWORK};
use http_api::TlsConfig;
use sensitive_url::SensitiveUrl; use sensitive_url::SensitiveUrl;
use slog::{info, warn, Logger}; use slog::{info, warn, Logger};
use std::cmp; use std::cmp;
@ -111,6 +112,21 @@ pub fn get_config<E: EthSpec>(
client_config.http_api.serve_legacy_spec = false; client_config.http_api.serve_legacy_spec = false;
} }
if cli_args.is_present("http-enable-tls") {
client_config.http_api.tls_config = Some(TlsConfig {
cert: cli_args
.value_of("http-tls-cert")
.ok_or("--http-tls-cert was not provided.")?
.parse::<PathBuf>()
.map_err(|_| "http-tls-cert is not a valid path name.")?,
key: cli_args
.value_of("http-tls-key")
.ok_or("--http-tls-key was not provided.")?
.parse::<PathBuf>()
.map_err(|_| "http-tls-key is not a valid path name.")?,
});
}
/* /*
* Prometheus metrics HTTP server * Prometheus metrics HTTP server
*/ */

View File

@ -13,9 +13,14 @@ The following CLI flags control the HTTP server:
provided). provided).
- `--http-port`: specify the listen port of the server. - `--http-port`: specify the listen port of the server.
- `--http-address`: specify the listen address of the server. It is _not_ recommended to listen - `--http-address`: specify the listen address of the server. It is _not_ recommended to listen
on `0.0.0.0`, please see [Security](#security) below. on `0.0.0.0`, please see [Security](#security) below.
- `--http-allow-origin`: specify the value of the `Access-Control-Allow-Origin` - `--http-allow-origin`: specify the value of the `Access-Control-Allow-Origin`
header. The default is to not supply a header. header. The default is to not supply a header.
- `--http-enable-tls`: serve the HTTP server over TLS. Must be used with `--http-tls-cert`
and `http-tls-key`. This feature is currently experimental, please see
[Serving the HTTP API over TLS](#serving-the-http-api-over-tls) below.
- `--http-tls-cert`: specify the path to the certificate file for Lighthouse to use.
- `--http-tls-key`: specify the path to the private key file for Lighthouse to use.
The schema of the API aligns with the standard Eth2 Beacon Node API as defined The schema of the API aligns with the standard Eth2 Beacon Node API as defined
at [github.com/ethereum/beacon-APIs](https://github.com/ethereum/beacon-APIs). at [github.com/ethereum/beacon-APIs](https://github.com/ethereum/beacon-APIs).
@ -118,6 +123,68 @@ curl -X GET "http://localhost:5052/eth/v1/beacon/states/head/validators/1" -H "
} }
``` ```
## Serving the HTTP API over TLS
> **Warning**: This feature is currently experimental.
The HTTP server can be served over TLS by using the `--http-enable-tls`,
`http-tls-cert` and `http-tls-key` flags.
This allows the API to be accessed via HTTPS, encrypting traffic to
and from the server.
This is particularly useful when connecting validator clients to
beacon nodes on different machines or remote servers.
However, even when serving the HTTP API server over TLS, it should
not be exposed publicly without one of the security measures suggested
in the [Security](#security) section.
Below is an simple example serving the HTTP API over TLS using a
self-signed certificate on Linux:
### Enabling TLS on a beacon node
Generate a self-signed certificate using `openssl`:
```bash
openssl req -x509 -nodes -newkey rsa:4096 -keyout key.pem -out cert.pem -days 365 -subj "/CN=localhost"
```
Note that currently Lighthouse only accepts keys that are not password protected.
This means we need to run with the `-nodes` flag (short for 'no DES').
Once generated, we can run Lighthouse:
```bash
lighthouse bn --http --http-enable-tls --http-tls-cert cert.pem --http-tls-key key.pem
```
Note that the user running Lighthouse must have permission to read the
certificate and key.
The API is now being served at `https://localhost:5052`.
To test connectivity, you can run the following:
```bash
curl -X GET "https://localhost:5052/eth/v1/node/version" -H "accept: application/json" --cacert cert.pem
```
### Connecting a validator client
In order to connect a validator client to a beacon node over TLS, we need to
add the certificate to the trust store of our operating system.
The process for this will vary depending on your operating system.
Below are the instructions for Ubuntu and Arch Linux:
```bash
# Ubuntu
sudo cp cert.pem /usr/local/share/ca-certificates/beacon.crt
sudo update-ca-certificates
```
```bash
# Arch
sudo cp cert.pem /etc/ca-certificates/trust-source/anchors/beacon.crt
sudo trust extract-compat
```
Now the validator client can be connected to the beacon node by running:
```bash
lighthouse vc --beacon-nodes https://localhost:5052
```
## Troubleshooting ## Troubleshooting
### HTTP API is unavailable or refusing connections ### HTTP API is unavailable or refusing connections

View File

@ -7,7 +7,7 @@ edition = "2018"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies] [dependencies]
warp = { git = "https://github.com/paulhauner/warp ", branch = "cors-wildcard" } warp = { git = "https://github.com/macladson/warp", rev ="dfa259e" }
eth2 = { path = "../eth2" } eth2 = { path = "../eth2" }
types = { path = "../../consensus/types" } types = { path = "../../consensus/types" }
beacon_chain = { path = "../../beacon_node/beacon_chain" } beacon_chain = { path = "../../beacon_node/beacon_chain" }

View File

@ -573,6 +573,30 @@ fn http_allow_origin_all_flag() {
.run() .run()
.with_config(|config| assert_eq!(config.http_api.allow_origin, Some("*".to_string()))); .with_config(|config| assert_eq!(config.http_api.allow_origin, Some("*".to_string())));
} }
#[test]
fn http_tls_flags() {
let dir = TempDir::new().expect("Unable to create temporary directory");
CommandLineTest::new()
.flag("http-enable-tls", None)
.flag(
"http-tls-cert",
dir.path().join("certificate.crt").as_os_str().to_str(),
)
.flag(
"http-tls-key",
dir.path().join("private.key").as_os_str().to_str(),
)
.run()
.with_config(|config| {
let tls_config = config
.http_api
.tls_config
.as_ref()
.expect("tls_config was empty.");
assert_eq!(tls_config.cert, dir.path().join("certificate.crt"));
assert_eq!(tls_config.key, dir.path().join("private.key"));
});
}
// Tests for Metrics flags. // Tests for Metrics flags.
#[test] #[test]

View File

@ -43,7 +43,7 @@ eth2_keystore = { path = "../crypto/eth2_keystore" }
account_utils = { path = "../common/account_utils" } account_utils = { path = "../common/account_utils" }
lighthouse_version = { path = "../common/lighthouse_version" } lighthouse_version = { path = "../common/lighthouse_version" }
warp_utils = { path = "../common/warp_utils" } warp_utils = { path = "../common/warp_utils" }
warp = { git = "https://github.com/paulhauner/warp ", branch = "cors-wildcard" } warp = { git = "https://github.com/macladson/warp", rev ="dfa259e" }
hyper = "0.14.4" hyper = "0.14.4"
eth2_serde_utils = "0.1.0" eth2_serde_utils = "0.1.0"
libsecp256k1 = "0.6.0" libsecp256k1 = "0.6.0"