Skip to content

CBST2-06: Implement rate limiting for JWT auth failures #310

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

Open
wants to merge 45 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
c68125d
bump version
ltitanb Apr 2, 2025
d9979a2
Successful cross-compilation, but runtime has memory allocation issues
jclapis May 5, 2025
97ef653
Working with OpenSSL static-linked
jclapis May 6, 2025
91eefe2
Got dynamic linking working, added a feature flag to toggle dynamic v…
jclapis May 6, 2025
de09415
Fixed the vendored build arg
jclapis May 6, 2025
3aee63d
Reintroduced the cargo chef setup
jclapis May 6, 2025
c07c717
Ported the cross-compilation stuff into PBS
jclapis May 6, 2025
699b7ec
Split the dockerfiles into separate builder / image definitions
jclapis May 7, 2025
7165f12
Added a build guide
jclapis May 7, 2025
9438dae
Refactored the Github release action to use the Docker builder
jclapis May 13, 2025
12c020a
Fixed the Docker image binary filenames
jclapis May 13, 2025
53cafc0
Cleaned up the Darwin artifact step
jclapis May 13, 2025
58c6117
Made the CI workflow and justfile use the same toolchain as the source
jclapis May 14, 2025
45e581b
Revert "Made the CI workflow and justfile use the same toolchain as t…
jclapis May 14, 2025
24a10c5
Testing removal of OpenSSL vendored option
jclapis May 14, 2025
e36da54
Updating just in the CI workflow
jclapis May 14, 2025
843b110
Merge branch 'main' into cross-compile
jclapis May 28, 2025
e7c6d19
Refactored the signer to support host and port config settings
jclapis May 21, 2025
6117219
Updated docs
jclapis May 21, 2025
c0f591d
Fixing Clippy in CI workflow
jclapis May 21, 2025
adbd34a
Removed obviated CI setup
jclapis May 28, 2025
e3488b3
Minor dedup of RwLock guard acquisition
jclapis May 20, 2025
c3d7ec4
Added rate limiting for signer clients with repeated JWT auth failures
jclapis May 22, 2025
9ddad64
Added Signer config validation
jclapis May 22, 2025
c62185e
Started unit test setup for the Signer
jclapis May 22, 2025
dc73c62
Finished a basic signer module unit test
jclapis May 28, 2025
6c3d967
Added a JWT failure unit test
jclapis May 28, 2025
6464638
Added a rate limit test and cleaned up a bit
jclapis May 28, 2025
0313f18
Added unique ports to unit tests for parallel execution
jclapis May 28, 2025
346eea4
Cleaned up the build Dockerfile and removed an extra dependency layer
jclapis May 28, 2025
7b20d2f
Ported the build script over to the justfile
jclapis May 29, 2025
cf3f0b1
Merge branch 'main' into cross-compile
jclapis May 29, 2025
ca9f4a1
Added a justfile recipe for installing protoc
jclapis May 29, 2025
3eed526
Merge branch 'cross-compile' into add-ip-bind-to-signer
jclapis May 29, 2025
aa6ad96
Merge branch 'add-ip-bind-to-signer' into rate-limit-jwt
jclapis May 29, 2025
fc872ac
Merge branch 'main' into add-ip-bind-to-signer
jclapis Jun 3, 2025
ca0c6e8
Merge branch 'add-ip-bind-to-signer' into rate-limit-jwt
jclapis Jun 3, 2025
40d34aa
Merge branch 'main' into add-ip-bind-to-signer
jclapis Jun 9, 2025
d537288
Update crates/cli/src/docker_init.rs
jclapis Jun 9, 2025
7afb763
Added example signer config params
jclapis Jun 9, 2025
09ac821
Cleaned up signer config loading from feedback
jclapis Jun 9, 2025
cf39d86
Merge remote-tracking branch 'origin/add-ip-bind-to-signer' into add-…
jclapis Jun 9, 2025
2431937
Merge branch 'add-ip-bind-to-signer' into rate-limit-jwt
jclapis Jun 9, 2025
2e1198b
Merge branch 'main' into rate-limit-jwt
jclapis Jun 9, 2025
ccaf97d
Added JWT auth fields to the example config
jclapis Jun 10, 2025
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
17 changes: 15 additions & 2 deletions Cargo.lock

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

2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ color-eyre = "0.6.3"
ctr = "0.9.2"
derive_more = { version = "2.0.1", features = ["deref", "display", "from", "into"] }
docker-compose-types = "0.16.0"
docker-image = "0.2.1"
eth2_keystore = { git = "https://github.com/sigp/lighthouse", rev = "8d058e4040b765a96aa4968f4167af7571292be2" }
ethereum_serde_utils = "0.7.0"
ethereum_ssz = "0.8"
Expand All @@ -57,6 +58,7 @@ serde_json = "1.0.117"
serde_yaml = "0.9.33"
sha2 = "0.10.8"
ssz_types = "0.10"
tempfile = "3.20.0"
thiserror = "2.0.12"
tokio = { version = "1.37.0", features = ["full"] }
toml = "0.8.13"
Expand Down
6 changes: 6 additions & 0 deletions config.example.toml
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,12 @@ host = "127.0.0.1"
# Port to listen for Signer API calls on
# OPTIONAL, DEFAULT: 20000
port = 20000
# Number of JWT authentication attempts a client can fail before blocking that client temporarily from Signer access
# OPTIONAL, DEFAULT: 3
jwt_auth_fail_limit: 3
# How long to block a client from Signer access, in seconds, if it failed JWT authentication too many times
# OPTIONAL, DEFAULT: 300
jwt_auth_fail_timeout_seconds: 300

# For Remote signer:
# [signer.remote]
Expand Down
1 change: 1 addition & 0 deletions crates/common/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ blst.workspace = true
cipher.workspace = true
ctr.workspace = true
derive_more.workspace = true
docker-image.workspace = true
eth2_keystore.workspace = true
ethereum_serde_utils.workspace = true
ethereum_ssz.workspace = true
Expand Down
5 changes: 5 additions & 0 deletions crates/common/src/config/constants.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,11 @@ pub const SIGNER_MODULE_NAME: &str = "signer";
/// Where the signer module should open the server
pub const SIGNER_ENDPOINT_ENV: &str = "CB_SIGNER_ENDPOINT";

// JWT authentication settings
pub const SIGNER_JWT_AUTH_FAIL_LIMIT_ENV: &str = "CB_SIGNER_JWT_AUTH_FAIL_LIMIT";
pub const SIGNER_JWT_AUTH_FAIL_TIMEOUT_SECONDS_ENV: &str =
"CB_SIGNER_JWT_AUTH_FAIL_TIMEOUT_SECONDS";

/// Comma separated list module_id=jwt_secret
pub const JWTS_ENV: &str = "CB_JWTS";

Expand Down
3 changes: 3 additions & 0 deletions crates/common/src/config/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,9 @@ impl CommitBoostConfig {
/// Validate config
pub async fn validate(&self) -> Result<()> {
self.pbs.pbs_config.validate(self.chain).await?;
if let Some(signer) = &self.signer {
signer.validate().await?;
}
Ok(())
}

Expand Down
64 changes: 60 additions & 4 deletions crates/common/src/config/signer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,25 @@ use std::{
path::PathBuf,
};

use eyre::{bail, OptionExt, Result};
use docker_image::DockerImage;
use eyre::{bail, ensure, OptionExt, Result};
use serde::{Deserialize, Serialize};
use tonic::transport::{Certificate, Identity};
use url::Url;

use super::{
load_jwt_secrets, load_optional_env_var, utils::load_env_var, CommitBoostConfig,
SIGNER_ENDPOINT_ENV, SIGNER_IMAGE_DEFAULT,
SIGNER_ENDPOINT_ENV, SIGNER_IMAGE_DEFAULT, SIGNER_JWT_AUTH_FAIL_LIMIT_ENV,
SIGNER_JWT_AUTH_FAIL_TIMEOUT_SECONDS_ENV,
};
use crate::{
config::{DIRK_CA_CERT_ENV, DIRK_CERT_ENV, DIRK_DIR_SECRETS_ENV, DIRK_KEY_ENV},
signer::{ProxyStore, SignerLoader, DEFAULT_SIGNER_PORT},
signer::{
ProxyStore, SignerLoader, DEFAULT_JWT_AUTH_FAIL_LIMIT,
DEFAULT_JWT_AUTH_FAIL_TIMEOUT_SECONDS, DEFAULT_SIGNER_PORT,
},
types::{Chain, ModuleId},
utils::{default_host, default_u16},
utils::{default_host, default_u16, default_u32},
};

#[derive(Debug, Serialize, Deserialize, Clone)]
Expand All @@ -32,11 +37,39 @@ pub struct SignerConfig {
/// Docker image of the module
#[serde(default = "default_signer")]
pub docker_image: String,

/// Number of JWT auth failures before rate limiting an endpoint
/// If set to 0, no rate limiting will be applied
#[serde(default = "default_u32::<DEFAULT_JWT_AUTH_FAIL_LIMIT>")]
pub jwt_auth_fail_limit: u32,

/// Duration in seconds to rate limit an endpoint after the JWT auth failure
/// limit has been reached
#[serde(default = "default_u32::<DEFAULT_JWT_AUTH_FAIL_TIMEOUT_SECONDS>")]
pub jwt_auth_fail_timeout_seconds: u32,

/// Inner type-specific configuration
#[serde(flatten)]
pub inner: SignerType,
}

impl SignerConfig {
/// Validate the signer config
pub async fn validate(&self) -> Result<()> {
// Port must be positive
ensure!(self.port > 0, "Port must be positive");

// The Docker tag must parse
ensure!(!self.docker_image.is_empty(), "Docker image is empty");
ensure!(
DockerImage::parse(&self.docker_image).is_ok(),
format!("Invalid Docker image: {}", self.docker_image)
);

Ok(())
}
}

fn default_signer() -> String {
SIGNER_IMAGE_DEFAULT.to_string()
}
Expand Down Expand Up @@ -100,6 +133,8 @@ pub struct StartSignerConfig {
pub store: Option<ProxyStore>,
pub endpoint: SocketAddr,
pub jwts: HashMap<ModuleId, String>,
pub jwt_auth_fail_limit: u32,
pub jwt_auth_fail_timeout_seconds: u32,
pub dirk: Option<DirkConfig>,
}

Expand All @@ -119,12 +154,31 @@ impl StartSignerConfig {
SocketAddr::from((signer_config.host, signer_config.port))
};

// Load the JWT auth fail limit the same way
let jwt_auth_fail_limit =
if let Some(limit) = load_optional_env_var(SIGNER_JWT_AUTH_FAIL_LIMIT_ENV) {
limit.parse()?
} else {
signer_config.jwt_auth_fail_limit
};

// Load the JWT auth fail timeout the same way
let jwt_auth_fail_timeout_seconds = if let Some(timeout) =
load_optional_env_var(SIGNER_JWT_AUTH_FAIL_TIMEOUT_SECONDS_ENV)
{
timeout.parse()?
} else {
signer_config.jwt_auth_fail_timeout_seconds
};

match signer_config.inner {
SignerType::Local { loader, store, .. } => Ok(StartSignerConfig {
chain: config.chain,
loader: Some(loader),
endpoint,
jwts,
jwt_auth_fail_limit,
jwt_auth_fail_timeout_seconds,
store,
dirk: None,
}),
Expand Down Expand Up @@ -153,6 +207,8 @@ impl StartSignerConfig {
chain: config.chain,
endpoint,
jwts,
jwt_auth_fail_limit,
jwt_auth_fail_timeout_seconds,
loader: None,
store,
dirk: Some(DirkConfig {
Expand Down
5 changes: 5 additions & 0 deletions crates/common/src/signer/constants.rs
Original file line number Diff line number Diff line change
@@ -1 +1,6 @@
pub const DEFAULT_SIGNER_PORT: u16 = 20000;

// Rate limit signer API requests for 5 minutes after the endpoint has 3 JWT
// auth failures
pub const DEFAULT_JWT_AUTH_FAIL_LIMIT: u32 = 3;
pub const DEFAULT_JWT_AUTH_FAIL_TIMEOUT_SECONDS: u32 = 5 * 60;
4 changes: 4 additions & 0 deletions crates/common/src/utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,10 @@ pub const fn default_u64<const U: u64>() -> u64 {
U
}

pub const fn default_u32<const U: u32>() -> u32 {
U
}

pub const fn default_u16<const U: u16>() -> u16 {
U
}
Expand Down
6 changes: 6 additions & 0 deletions crates/signer/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@ pub enum SignerModuleError {

#[error("internal error: {0}")]
Internal(String),

#[error("rate limited for {0} more seconds")]
RateLimited(f64),
}

impl IntoResponse for SignerModuleError {
Expand All @@ -45,6 +48,9 @@ impl IntoResponse for SignerModuleError {
(StatusCode::INTERNAL_SERVER_ERROR, "internal error".to_string())
}
SignerModuleError::SignerError(err) => (StatusCode::BAD_REQUEST, err.to_string()),
SignerModuleError::RateLimited(duration) => {
(StatusCode::TOO_MANY_REQUESTS, format!("rate limited for {duration:?}"))
}
}
.into_response()
}
Expand Down
Loading
Loading