Skip to content

Commit

Permalink
Add TLS capability to the beacon node HTTP API (#2668)
Browse files Browse the repository at this point in the history
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](seanmonstar/warp#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.
  • Loading branch information
macladson committed Oct 12, 2021
1 parent 0aee7ec commit a73d698
Show file tree
Hide file tree
Showing 12 changed files with 191 additions and 17 deletions.
15 changes: 14 additions & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions beacon_node/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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" }
2 changes: 1 addition & 1 deletion beacon_node/http_api/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"] }
Expand Down
49 changes: 39 additions & 10 deletions beacon_node/http_api/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand All @@ -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<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.
///
/// The server will gracefully handle the case where any fields are `None`.
Expand All @@ -81,6 +93,7 @@ pub struct Config {
pub listen_port: u16,
pub allow_origin: Option<String>,
pub serve_legacy_spec: bool,
pub tls_config: Option<TlsConfig>,
}

impl Default for Config {
Expand All @@ -91,6 +104,7 @@ impl Default for Config {
listen_port: 5052,
allow_origin: None,
serve_legacy_spec: true,
tls_config: None,
}
}
}
Expand Down Expand Up @@ -218,7 +232,7 @@ pub fn prometheus_metrics() -> warp::filters::log::Log<impl Fn(warp::filters::lo
pub fn serve<T: BeaconChainTypes>(
ctx: Arc<Context<T>>,
shutdown: impl Future<Output = ()> + Send + Sync + 'static,
) -> Result<(SocketAddr, impl Future<Output = ()>), Error> {
) -> Result<HttpServer, Error> {
let config = ctx.config.clone();
let log = ctx.log.clone();

Expand Down Expand Up @@ -2587,22 +2601,37 @@ pub fn serve<T: BeaconChainTypes>(
.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.
Expand Down
1 change: 1 addition & 0 deletions beacon_node/http_api/tests/common.rs
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,7 @@ pub async fn create_api_server<T: BeaconChainTypes>(
listen_port: 0,
allow_origin: None,
serve_legacy_spec: true,
tls_config: None,
},
chain: Some(chain.clone()),
network_tx: Some(network_tx),
Expand Down
2 changes: 1 addition & 1 deletion beacon_node/http_metrics/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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" }
Expand Down
23 changes: 23 additions & 0 deletions beacon_node/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
16 changes: 16 additions & 0 deletions beacon_node/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -111,6 +112,21 @@ pub fn get_config<E: EthSpec>(
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
*/
Expand Down
71 changes: 69 additions & 2 deletions book/src/api-bn.md
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand Down Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion common/warp_utils/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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" }
Expand Down
24 changes: 24 additions & 0 deletions lighthouse/tests/beacon_node.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
2 changes: 1 addition & 1 deletion validator_client/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down

0 comments on commit a73d698

Please sign in to comment.