Skip to content

Commit 2f9fb56

Browse files
committed
Add admin JWT for admin endpoints
1 parent 6aed0d5 commit 2f9fb56

7 files changed

Lines changed: 69 additions & 14 deletions

File tree

crates/cli/src/docker_init.rs

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,11 @@ use std::{
77
use cb_common::{
88
config::{
99
load_optional_env_var, CommitBoostConfig, LogsSettings, ModuleKind, SignerConfig,
10-
SignerType, BUILDER_PORT_ENV, BUILDER_URLS_ENV, CHAIN_SPEC_ENV, CONFIG_DEFAULT, CONFIG_ENV,
11-
DIRK_CA_CERT_DEFAULT, DIRK_CA_CERT_ENV, DIRK_CERT_DEFAULT, DIRK_CERT_ENV,
12-
DIRK_DIR_SECRETS_DEFAULT, DIRK_DIR_SECRETS_ENV, DIRK_KEY_DEFAULT, DIRK_KEY_ENV, JWTS_ENV,
13-
LOGS_DIR_DEFAULT, LOGS_DIR_ENV, METRICS_PORT_ENV, MODULE_ID_ENV, MODULE_JWT_ENV,
14-
PBS_ENDPOINT_ENV, PBS_MODULE_NAME, PROXY_DIR_DEFAULT, PROXY_DIR_ENV,
10+
SignerType, ADMIN_JWT_ENV, BUILDER_PORT_ENV, BUILDER_URLS_ENV, CHAIN_SPEC_ENV,
11+
CONFIG_DEFAULT, CONFIG_ENV, DIRK_CA_CERT_DEFAULT, DIRK_CA_CERT_ENV, DIRK_CERT_DEFAULT,
12+
DIRK_CERT_ENV, DIRK_DIR_SECRETS_DEFAULT, DIRK_DIR_SECRETS_ENV, DIRK_KEY_DEFAULT,
13+
DIRK_KEY_ENV, JWTS_ENV, LOGS_DIR_DEFAULT, LOGS_DIR_ENV, METRICS_PORT_ENV, MODULE_ID_ENV,
14+
MODULE_JWT_ENV, PBS_ENDPOINT_ENV, PBS_MODULE_NAME, PROXY_DIR_DEFAULT, PROXY_DIR_ENV,
1515
PROXY_DIR_KEYS_DEFAULT, PROXY_DIR_KEYS_ENV, PROXY_DIR_SECRETS_DEFAULT,
1616
PROXY_DIR_SECRETS_ENV, SIGNER_DEFAULT, SIGNER_DIR_KEYS_DEFAULT, SIGNER_DIR_KEYS_ENV,
1717
SIGNER_DIR_SECRETS_DEFAULT, SIGNER_DIR_SECRETS_ENV, SIGNER_JWT_SECRET_ENV, SIGNER_KEYS_ENV,
@@ -334,6 +334,7 @@ pub async fn handle_docker_init(config_path: PathBuf, output_dir: PathBuf) -> Re
334334
let mut signer_envs = IndexMap::from([
335335
get_env_val(CONFIG_ENV, CONFIG_DEFAULT),
336336
get_env_same(JWTS_ENV),
337+
get_env_same(ADMIN_JWT_ENV),
337338
get_env_uval(SIGNER_PORT_ENV, signer_port as u64),
338339
]);
339340

@@ -360,6 +361,7 @@ pub async fn handle_docker_init(config_path: PathBuf, output_dir: PathBuf) -> Re
360361

361362
// write jwts to env
362363
envs.insert(JWTS_ENV.into(), format_comma_separated(&jwts));
364+
envs.insert(ADMIN_JWT_ENV.into(), random_jwt_secret());
363365

364366
// volumes
365367
let mut volumes = vec![config_volume.clone()];

crates/common/src/config/constants.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ pub const SIGNER_PORT_ENV: &str = "CB_SIGNER_PORT";
3737

3838
/// Comma separated list module_id=jwt_secret
3939
pub const JWTS_ENV: &str = "CB_JWTS";
40+
pub const ADMIN_JWT_ENV: &str = "CB_ADMIN_JWT";
4041
/// The JWT secret for the signer to validate the modules requests
4142
pub const SIGNER_JWT_SECRET_ENV: &str = "CB_SIGNER_JWT_SECRET";
4243

crates/common/src/config/signer.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -89,14 +89,15 @@ pub struct StartSignerConfig {
8989
pub store: Option<ProxyStore>,
9090
pub server_port: u16,
9191
pub jwts: HashMap<ModuleId, String>,
92+
pub admin_secret: String,
9293
pub dirk: Option<DirkConfig>,
9394
}
9495

9596
impl StartSignerConfig {
9697
pub fn load_from_env() -> Result<Self> {
9798
let config = CommitBoostConfig::from_env_path()?;
9899

99-
let jwts = load_jwt_secrets()?;
100+
let (admin_secret, jwts) = load_jwt_secrets()?;
100101
let server_port = load_env_var(SIGNER_PORT_ENV)?.parse()?;
101102

102103
let signer = config.signer.ok_or_eyre("Signer config is missing")?.inner;
@@ -107,6 +108,7 @@ impl StartSignerConfig {
107108
loader: Some(loader),
108109
server_port,
109110
jwts,
111+
admin_secret,
110112
store,
111113
dirk: None,
112114
}),
@@ -135,6 +137,7 @@ impl StartSignerConfig {
135137
chain: config.chain,
136138
server_port,
137139
jwts,
140+
admin_secret,
138141
loader: None,
139142
store,
140143
dirk: Some(DirkConfig {

crates/common/src/config/utils.rs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ use std::{collections::HashMap, path::Path};
33
use eyre::{bail, Context, Result};
44
use serde::de::DeserializeOwned;
55

6-
use super::JWTS_ENV;
6+
use super::{ADMIN_JWT_ENV, JWTS_ENV};
77
use crate::types::ModuleId;
88

99
pub fn load_env_var(env: &str) -> Result<String> {
@@ -25,9 +25,10 @@ pub fn load_file_from_env<T: DeserializeOwned>(env: &str) -> Result<T> {
2525
}
2626

2727
/// Loads a map of module id -> jwt secret from a json env
28-
pub fn load_jwt_secrets() -> Result<HashMap<ModuleId, String>> {
28+
pub fn load_jwt_secrets() -> Result<(String, HashMap<ModuleId, String>)> {
29+
let admin_jwt = std::env::var(ADMIN_JWT_ENV).wrap_err(format!("{ADMIN_JWT_ENV} is not set"))?;
2930
let jwt_secrets = std::env::var(JWTS_ENV).wrap_err(format!("{JWTS_ENV} is not set"))?;
30-
decode_string_to_map(&jwt_secrets)
31+
decode_string_to_map(&jwt_secrets).map(|secrets| (admin_jwt, secrets))
3132
}
3233

3334
fn decode_string_to_map(raw: &str) -> Result<HashMap<ModuleId, String>> {

crates/common/src/types.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,12 @@ pub struct JwtClaims {
2323
pub module: String,
2424
}
2525

26+
#[derive(Debug, Serialize, Deserialize)]
27+
pub struct JwtAdmin {
28+
pub exp: u64,
29+
pub admin: bool,
30+
}
31+
2632
#[derive(Clone, Copy, PartialEq, Eq)]
2733
pub enum Chain {
2834
Mainnet,

crates/common/src/utils.rs

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ use crate::{
2626
config::LogsSettings,
2727
constants::SIGNER_JWT_EXPIRATION,
2828
pbs::HEADER_VERSION_VALUE,
29-
types::{Chain, Jwt, JwtClaims, ModuleId},
29+
types::{Chain, Jwt, JwtAdmin, JwtClaims, ModuleId},
3030
};
3131

3232
const MILLIS_PER_SECOND: u64 = 1_000;
@@ -320,6 +320,24 @@ pub fn validate_jwt(jwt: Jwt, secret: &str) -> eyre::Result<()> {
320320
.map_err(From::from)
321321
}
322322

323+
/// Validate an admin JWT with the given secret
324+
pub fn validate_admin_jwt(jwt: Jwt, secret: &str) -> eyre::Result<()> {
325+
let mut validation = jsonwebtoken::Validation::default();
326+
validation.leeway = 10;
327+
328+
let token = jsonwebtoken::decode::<JwtAdmin>(
329+
jwt.as_str(),
330+
&jsonwebtoken::DecodingKey::from_secret(secret.as_ref()),
331+
&validation,
332+
)?;
333+
334+
if token.claims.admin {
335+
Ok(())
336+
} else {
337+
eyre::bail!("Token is not admin")
338+
}
339+
}
340+
323341
/// Generates a random string
324342
pub fn random_jwt_secret() -> String {
325343
rand::rng().sample_iter(&Alphanumeric).take(32).map(char::from).collect()

crates/signer/src/service.rs

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ use cb_common::{
2323
config::StartSignerConfig,
2424
constants::{COMMIT_BOOST_COMMIT, COMMIT_BOOST_VERSION},
2525
types::{Chain, Jwt, ModuleId},
26-
utils::{decode_jwt, validate_jwt},
26+
utils::{decode_jwt, validate_admin_jwt, validate_jwt},
2727
};
2828
use cb_metrics::provider::MetricsProvider;
2929
use eyre::Context;
@@ -48,6 +48,8 @@ struct SigningState {
4848
/// Map of modules ids to JWT secrets. This also acts as registry of all
4949
/// modules running
5050
jwts: Arc<RwLock<HashMap<ModuleId, String>>>,
51+
/// Secret for the admin JWT
52+
admin_secret: Arc<RwLock<String>>,
5153
}
5254

5355
impl SigningService {
@@ -62,6 +64,7 @@ impl SigningService {
6264
let state = SigningState {
6365
manager: Arc::new(RwLock::new(start_manager(config.clone()).await?)),
6466
jwts: Arc::new(RwLock::new(config.jwts)),
67+
admin_secret: Arc::new(RwLock::new(config.admin_secret)),
6568
};
6669

6770
let loaded_consensus = state.manager.read().await.available_consensus_signers();
@@ -71,21 +74,25 @@ impl SigningService {
7174

7275
SigningService::init_metrics(config.chain)?;
7376

74-
let app = axum::Router::new()
77+
let signer_app = axum::Router::new()
7578
.route(REQUEST_SIGNATURE_PATH, post(handle_request_signature))
7679
.route(GET_PUBKEYS_PATH, get(handle_get_pubkeys))
7780
.route(GENERATE_PROXY_KEY_PATH, post(handle_generate_proxy))
7881
.route_layer(middleware::from_fn_with_state(state.clone(), jwt_auth))
82+
.with_state(state.clone())
83+
.route_layer(middleware::from_fn(log_request));
84+
85+
let admin_app = axum::Router::new()
7986
.route(RELOAD_PATH, post(handle_reload))
8087
.route(REVOKE_JWT, post(handle_revoke_jwt))
88+
.route_layer(middleware::from_fn_with_state(state.clone(), admin_auth))
8189
.with_state(state.clone())
8290
.route_layer(middleware::from_fn(log_request))
8391
.route(STATUS_PATH, get(handle_status));
84-
8592
let address = SocketAddr::from(([0, 0, 0, 0], config.server_port));
8693
let listener = TcpListener::bind(address).await?;
8794

88-
axum::serve(listener, app).await.wrap_err("signer server exited")
95+
axum::serve(listener, signer_app.merge(admin_app)).await.wrap_err("signer server exited")
8996
}
9097

9198
fn init_metrics(network: Chain) -> eyre::Result<()> {
@@ -125,6 +132,22 @@ async fn jwt_auth(
125132
Ok(next.run(req).await)
126133
}
127134

135+
async fn admin_auth(
136+
State(state): State<SigningState>,
137+
TypedHeader(auth): TypedHeader<Authorization<Bearer>>,
138+
req: Request,
139+
next: Next,
140+
) -> Result<Response, SignerModuleError> {
141+
let jwt: Jwt = auth.token().to_string().into();
142+
143+
validate_admin_jwt(jwt, &state.admin_secret.read().await).map_err(|e| {
144+
error!("Unauthorized request. Invalid JWT: {e}");
145+
SignerModuleError::Unauthorized
146+
})?;
147+
148+
Ok(next.run(req).await)
149+
}
150+
128151
/// Requests logging middleware layer
129152
async fn log_request(req: Request, next: Next) -> Result<Response, SignerModuleError> {
130153
let url = &req.uri().clone();
@@ -273,6 +296,7 @@ async fn handle_reload(
273296
};
274297

275298
state.jwts = Arc::new(RwLock::new(config.jwts.clone()));
299+
state.admin_secret = Arc::new(RwLock::new(config.admin_secret.clone()));
276300

277301
let new_manager = match start_manager(config).await {
278302
Ok(manager) => manager,

0 commit comments

Comments
 (0)