From a73d698e30f54fb6830eec63e08faeae6c23911f Mon Sep 17 00:00:00 2001 From: Mac L Date: Tue, 12 Oct 2021 03:35:49 +0000 Subject: [PATCH] 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. --- Cargo.lock | 15 +++++- beacon_node/Cargo.toml | 1 + beacon_node/http_api/Cargo.toml | 2 +- beacon_node/http_api/src/lib.rs | 49 +++++++++++++++---- beacon_node/http_api/tests/common.rs | 1 + beacon_node/http_metrics/Cargo.toml | 2 +- beacon_node/src/cli.rs | 23 +++++++++ beacon_node/src/config.rs | 16 +++++++ book/src/api-bn.md | 71 +++++++++++++++++++++++++++- common/warp_utils/Cargo.toml | 2 +- lighthouse/tests/beacon_node.rs | 24 ++++++++++ validator_client/Cargo.toml | 2 +- 12 files changed, 191 insertions(+), 17 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 5b4a76fb61..05cb90eedc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -489,6 +489,7 @@ dependencies = [ "futures", "genesis", "hex", + "http_api", "hyper", "lighthouse_version", "monitoring_api", @@ -6208,6 +6209,17 @@ dependencies = [ "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]] name = "tokio-stream" version = "0.1.7" @@ -6799,7 +6811,7 @@ dependencies = [ [[package]] name = "warp" 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 = [ "bytes 1.1.0", "futures", @@ -6817,6 +6829,7 @@ dependencies = [ "serde_json", "serde_urlencoded", "tokio", + "tokio-rustls", "tokio-stream", "tokio-tungstenite", "tokio-util", diff --git a/beacon_node/Cargo.toml b/beacon_node/Cargo.toml index 1e852a7e11..3f4d5728e8 100644 --- a/beacon_node/Cargo.toml +++ b/beacon_node/Cargo.toml @@ -38,3 +38,4 @@ hex = "0.4.2" slasher = { path = "../slasher" } monitoring_api = { path = "../common/monitoring_api" } sensitive_url = { path = "../common/sensitive_url" } +http_api = { path = "http_api" } diff --git a/beacon_node/http_api/Cargo.toml b/beacon_node/http_api/Cargo.toml index 6939b02c4e..c5fa86e0eb 100644 --- a/beacon_node/http_api/Cargo.toml +++ b/beacon_node/http_api/Cargo.toml @@ -6,7 +6,7 @@ edition = "2018" autotests = false # using a single test binary compiles faster [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"] } tokio = { version = "1.10.0", features = ["macros","sync"] } tokio-stream = { version = "0.1.3", features = ["sync"] } diff --git a/beacon_node/http_api/src/lib.rs b/beacon_node/http_api/src/lib.rs index 2412b1f545..3e6939146c 100644 --- a/beacon_node/http_api/src/lib.rs +++ b/beacon_node/http_api/src/lib.rs @@ -36,6 +36,8 @@ use std::borrow::Cow; use std::convert::TryInto; use std::future::Future; use std::net::{Ipv4Addr, SocketAddr, SocketAddrV4}; +use std::path::PathBuf; +use std::pin::Pin; use std::sync::Arc; use tokio::sync::mpsc::UnboundedSender; use tokio_stream::{wrappers::BroadcastStream, StreamExt}; @@ -61,6 +63,16 @@ const API_PREFIX: &str = "eth"; /// finalized head. const SYNC_TOLERANCE_EPOCHS: u64 = 8; +/// A custom type which allows for both unsecured and TLS-enabled HTTP servers. +type HttpServer = (SocketAddr, Pin + 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. /// /// The server will gracefully handle the case where any fields are `None`. @@ -81,6 +93,7 @@ pub struct Config { pub listen_port: u16, pub allow_origin: Option, pub serve_legacy_spec: bool, + pub tls_config: Option, } impl Default for Config { @@ -91,6 +104,7 @@ impl Default for Config { listen_port: 5052, allow_origin: None, serve_legacy_spec: true, + tls_config: None, } } } @@ -218,7 +232,7 @@ pub fn prometheus_metrics() -> warp::filters::log::Log( ctx: Arc>, shutdown: impl Future + Send + Sync + 'static, -) -> Result<(SocketAddr, impl Future), Error> { +) -> Result { let config = ctx.config.clone(); let log = ctx.log.clone(); @@ -2587,22 +2601,37 @@ pub fn serve( .map(|reply| warp::reply::with_header(reply, "Server", &version_with_platform())) .with(cors_builder.build()); - let (listening_socket, server) = { - warp::serve(routes).try_bind_with_graceful_shutdown( - SocketAddrV4::new(config.listen_addr, config.listen_port), - async { - shutdown.await; - }, - )? + let http_socket: SocketAddrV4 = SocketAddrV4::new(config.listen_addr, config.listen_port); + let http_server: HttpServer = match config.tls_config { + Some(tls_config) => { + let (socket, server) = warp::serve(routes) + .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!( log, "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. diff --git a/beacon_node/http_api/tests/common.rs b/beacon_node/http_api/tests/common.rs index 69d67424a1..04224b8471 100644 --- a/beacon_node/http_api/tests/common.rs +++ b/beacon_node/http_api/tests/common.rs @@ -131,6 +131,7 @@ pub async fn create_api_server( listen_port: 0, allow_origin: None, serve_legacy_spec: true, + tls_config: None, }, chain: Some(chain.clone()), network_tx: Some(network_tx), diff --git a/beacon_node/http_metrics/Cargo.toml b/beacon_node/http_metrics/Cargo.toml index 8338fbb045..30291ef9b0 100644 --- a/beacon_node/http_metrics/Cargo.toml +++ b/beacon_node/http_metrics/Cargo.toml @@ -7,7 +7,7 @@ edition = "2018" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [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"] } slog = "2.5.2" beacon_chain = { path = "../beacon_chain" } diff --git a/beacon_node/src/cli.rs b/beacon_node/src/cli.rs index f27d430b74..f95b94b0c3 100644 --- a/beacon_node/src/cli.rs +++ b/beacon_node/src/cli.rs @@ -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 \ 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 */ .arg( Arg::with_name("metrics") diff --git a/beacon_node/src/config.rs b/beacon_node/src/config.rs index b0fd687f48..f22011da0d 100644 --- a/beacon_node/src/config.rs +++ b/beacon_node/src/config.rs @@ -4,6 +4,7 @@ use client::{ClientConfig, ClientGenesis}; use directory::{DEFAULT_BEACON_NODE_DIR, DEFAULT_NETWORK_DIR, DEFAULT_ROOT_DIR}; use eth2_libp2p::{multiaddr::Protocol, Enr, Multiaddr, NetworkConfig, PeerIdSerialized}; use eth2_network_config::{Eth2NetworkConfig, DEFAULT_HARDCODED_NETWORK}; +use http_api::TlsConfig; use sensitive_url::SensitiveUrl; use slog::{info, warn, Logger}; use std::cmp; @@ -111,6 +112,21 @@ pub fn get_config( 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::() + .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::() + .map_err(|_| "http-tls-key is not a valid path name.")?, + }); + } + /* * Prometheus metrics HTTP server */ diff --git a/book/src/api-bn.md b/book/src/api-bn.md index a376b8ba79..bd1132f8ec 100644 --- a/book/src/api-bn.md +++ b/book/src/api-bn.md @@ -13,9 +13,14 @@ The following CLI flags control the HTTP server: provided). - `--http-port`: specify the listen port of the server. - `--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` - 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 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 ### HTTP API is unavailable or refusing connections diff --git a/common/warp_utils/Cargo.toml b/common/warp_utils/Cargo.toml index ea8fe7d4ee..dcd6bebc67 100644 --- a/common/warp_utils/Cargo.toml +++ b/common/warp_utils/Cargo.toml @@ -7,7 +7,7 @@ edition = "2018" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -warp = { git = "https://github.com/paulhauner/warp ", branch = "cors-wildcard" } +warp = { git = "https://github.com/macladson/warp", rev ="dfa259e" } eth2 = { path = "../eth2" } types = { path = "../../consensus/types" } beacon_chain = { path = "../../beacon_node/beacon_chain" } diff --git a/lighthouse/tests/beacon_node.rs b/lighthouse/tests/beacon_node.rs index 170f8dae20..a454ad63e2 100644 --- a/lighthouse/tests/beacon_node.rs +++ b/lighthouse/tests/beacon_node.rs @@ -573,6 +573,30 @@ fn http_allow_origin_all_flag() { .run() .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. #[test] diff --git a/validator_client/Cargo.toml b/validator_client/Cargo.toml index 83158e9ec1..d1b212cc73 100644 --- a/validator_client/Cargo.toml +++ b/validator_client/Cargo.toml @@ -43,7 +43,7 @@ eth2_keystore = { path = "../crypto/eth2_keystore" } account_utils = { path = "../common/account_utils" } lighthouse_version = { path = "../common/lighthouse_version" } 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" eth2_serde_utils = "0.1.0" libsecp256k1 = "0.6.0"