Skip to content

Commit 9c6768b

Browse files
CopilotSteake
andcommitted
Address all code review comments from PR #3
Co-authored-by: Steake <530040+Steake@users.noreply.github.com>
1 parent 5b5f780 commit 9c6768b

12 files changed

Lines changed: 180 additions & 66 deletions

File tree

crates/bitcell-admin/README.md

Lines changed: 17 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -162,14 +162,23 @@ bitcell-admin/
162162

163163
## Security Considerations
164164

165-
⚠️ **IMPORTANT**: The admin console provides powerful administrative capabilities. In production:
166-
167-
1. **Enable authentication** before exposing to network
168-
2. **Use HTTPS/TLS** for encrypted communication
169-
3. **Restrict access** via firewall rules or VPN
170-
4. **Use strong passwords** and rotate regularly
171-
5. **Enable audit logging** for all administrative actions
172-
6. **Limit API rate limits** to prevent abuse
165+
⚠️ **CRITICAL SECURITY WARNING** ⚠️
166+
167+
**NO AUTHENTICATION IS CURRENTLY IMPLEMENTED**
168+
169+
The admin console currently allows **unrestricted access** to all endpoints. This is a **critical security vulnerability**.
170+
171+
**DO NOT expose this admin console to any network (including localhost) in production without implementing authentication first.**
172+
173+
For production deployments, you MUST:
174+
175+
1. **Implement authentication** before exposing to any network
176+
2. **Use HTTPS/TLS** for all communication (never HTTP in production)
177+
3. **Restrict network access** via firewall rules, VPN, or IP allowlisting
178+
4. **Use strong passwords** and rotate them regularly
179+
5. **Enable comprehensive audit logging** for all administrative actions
180+
6. **Implement API rate limiting** to prevent abuse
181+
7. **Run with least-privilege** user accounts (never as root)
173182

174183
## Development
175184

crates/bitcell-admin/src/api/metrics.rs

Lines changed: 10 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -78,13 +78,9 @@ pub async fn get_metrics(
7878
.await
7979
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, Json(e)))?;
8080

81-
// Calculate system metrics (placeholder - would normally come from node metrics or system stats)
82-
let uptime_seconds = if let Some(first_node) = aggregated.node_metrics.first() {
83-
let duration = chrono::Utc::now().signed_duration_since(first_node.last_updated);
84-
duration.num_seconds().max(0) as u64
85-
} else {
86-
0
87-
};
81+
// Calculate system metrics
82+
// TODO: Track actual node start times to compute real uptime
83+
let uptime_seconds = 0u64; // Placeholder - requires node start time tracking
8884

8985
let response = MetricsResponse {
9086
chain: ChainMetrics {
@@ -100,20 +96,20 @@ pub async fn get_metrics(
10096
total_peers: aggregated.total_nodes * 10, // Estimate
10197
bytes_sent: aggregated.bytes_sent,
10298
bytes_received: aggregated.bytes_received,
103-
messages_sent: 0, // TODO: Add to node metrics
104-
messages_received: 0, // TODO: Add to node metrics
99+
messages_sent: 0, // TODO: Requires adding message_sent to node metrics
100+
messages_received: 0, // TODO: Requires adding message_received to node metrics
105101
},
106102
ebsl: EbslMetrics {
107103
active_miners: aggregated.active_miners,
108104
banned_miners: aggregated.banned_miners,
109-
average_trust_score: 0.85, // TODO: Calculate from actual data
110-
total_slashing_events: 0, // TODO: Add to node metrics
105+
average_trust_score: 0.85, // TODO: Requires adding trust scores to node metrics
106+
total_slashing_events: 0, // TODO: Requires adding slashing events to node metrics
111107
},
112108
system: SystemMetrics {
113109
uptime_seconds,
114-
cpu_usage: 0.0, // TODO: Add system metrics collection
115-
memory_usage_mb: 0, // TODO: Add system metrics collection
116-
disk_usage_mb: 0, // TODO: Add system metrics collection
110+
cpu_usage: 0.0, // TODO: Requires system metrics collection (e.g., sysinfo crate)
111+
memory_usage_mb: 0, // TODO: Requires system metrics collection
112+
disk_usage_mb: 0, // TODO: Requires system metrics collection
117113
},
118114
};
119115

crates/bitcell-admin/src/api/nodes.rs

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,16 @@ pub async fn get_node(
4747
State(state): State<Arc<AppState>>,
4848
Path(id): Path<String>,
4949
) -> Result<Json<NodeResponse>, (StatusCode, Json<ErrorResponse>)> {
50+
// Validate node ID format (alphanumeric and hyphens only)
51+
if !id.chars().all(|c| c.is_alphanumeric() || c == '-' || c == '_') {
52+
return Err((
53+
StatusCode::BAD_REQUEST,
54+
Json(ErrorResponse {
55+
error: "Invalid node ID format".to_string(),
56+
}),
57+
));
58+
}
59+
5060
match state.process.get_node(&id) {
5161
Some(node) => Ok(Json(NodeResponse { node })),
5262
None => Err((
@@ -62,8 +72,28 @@ pub async fn get_node(
6272
pub async fn start_node(
6373
State(state): State<Arc<AppState>>,
6474
Path(id): Path<String>,
65-
Json(_req): Json<StartNodeRequest>,
75+
Json(req): Json<StartNodeRequest>,
6676
) -> Result<Json<NodeResponse>, (StatusCode, Json<ErrorResponse>)> {
77+
// Validate node ID format (alphanumeric and hyphens only)
78+
if !id.chars().all(|c| c.is_alphanumeric() || c == '-' || c == '_') {
79+
return Err((
80+
StatusCode::BAD_REQUEST,
81+
Json(ErrorResponse {
82+
error: "Invalid node ID format".to_string(),
83+
}),
84+
));
85+
}
86+
87+
// Config is not supported yet
88+
if req.config.is_some() {
89+
return Err((
90+
StatusCode::BAD_REQUEST,
91+
Json(ErrorResponse {
92+
error: "Custom config is not supported yet".to_string(),
93+
}),
94+
));
95+
}
96+
6797
match state.process.start_node(&id) {
6898
Ok(node) => {
6999
tracing::info!("Started node '{}' successfully", id);
@@ -83,6 +113,16 @@ pub async fn stop_node(
83113
State(state): State<Arc<AppState>>,
84114
Path(id): Path<String>,
85115
) -> Result<Json<NodeResponse>, (StatusCode, Json<ErrorResponse>)> {
116+
// Validate node ID format (alphanumeric and hyphens only)
117+
if !id.chars().all(|c| c.is_alphanumeric() || c == '-' || c == '_') {
118+
return Err((
119+
StatusCode::BAD_REQUEST,
120+
Json(ErrorResponse {
121+
error: "Invalid node ID format".to_string(),
122+
}),
123+
));
124+
}
125+
86126
match state.process.stop_node(&id) {
87127
Ok(node) => {
88128
tracing::info!("Stopped node '{}' successfully", id);

crates/bitcell-admin/src/api/setup.rs

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ use serde::{Deserialize, Serialize};
99
use std::sync::Arc;
1010

1111
use crate::AppState;
12-
use crate::setup::NodeEndpoint;
12+
use crate::setup::{NodeEndpoint, SETUP_FILE_PATH};
1313

1414
#[derive(Debug, Serialize)]
1515
pub struct SetupStatusResponse {
@@ -68,7 +68,7 @@ pub async fn add_node(
6868
state.setup.add_node(node.clone());
6969

7070
// Save setup state
71-
let setup_path = std::path::PathBuf::from(".bitcell/admin/setup.json");
71+
let setup_path = std::path::PathBuf::from(SETUP_FILE_PATH);
7272
state.setup.save_to_file(&setup_path)
7373
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, Json(e)))?;
7474

@@ -87,7 +87,7 @@ pub async fn set_config_path(
8787
state.setup.set_config_path(path.clone());
8888

8989
// Save setup state
90-
let setup_path = std::path::PathBuf::from(".bitcell/admin/setup.json");
90+
let setup_path = std::path::PathBuf::from(SETUP_FILE_PATH);
9191
state.setup.save_to_file(&setup_path)
9292
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, Json(e)))?;
9393

@@ -101,17 +101,30 @@ pub async fn set_data_dir(
101101
) -> Result<Json<String>, (StatusCode, Json<String>)> {
102102
let path = std::path::PathBuf::from(&req.path);
103103

104-
// Create directory if it doesn't exist
104+
// Create directory if it doesn't exist with restrictive permissions
105105
std::fs::create_dir_all(&path)
106106
.map_err(|e| (
107107
StatusCode::INTERNAL_SERVER_ERROR,
108108
Json(format!("Failed to create data directory: {}", e))
109109
))?;
110110

111+
// Set restrictive permissions on Unix systems (0700 - owner only)
112+
#[cfg(unix)]
113+
{
114+
use std::os::unix::fs::PermissionsExt;
115+
let permissions = std::fs::Permissions::from_mode(0o700);
116+
std::fs::set_permissions(&path, permissions)
117+
.map_err(|e| (
118+
StatusCode::INTERNAL_SERVER_ERROR,
119+
Json(format!("Failed to set directory permissions: {}", e))
120+
))?;
121+
tracing::info!("Set data directory permissions to 0700 (owner only)");
122+
}
123+
111124
state.setup.set_data_dir(path);
112125

113126
// Save setup state
114-
let setup_path = std::path::PathBuf::from(".bitcell/admin/setup.json");
127+
let setup_path = std::path::PathBuf::from(SETUP_FILE_PATH);
115128
state.setup.save_to_file(&setup_path)
116129
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, Json(e)))?;
117130

@@ -125,7 +138,7 @@ pub async fn complete_setup(
125138
state.setup.mark_initialized();
126139

127140
// Save setup state
128-
let setup_path = std::path::PathBuf::from(".bitcell/admin/setup.json");
141+
let setup_path = std::path::PathBuf::from(SETUP_FILE_PATH);
129142
state.setup.save_to_file(&setup_path)
130143
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, Json(e)))?;
131144

crates/bitcell-admin/src/api/test.rs

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -114,18 +114,16 @@ pub async fn run_battle_test(
114114

115115
let (outcome, energy_a, energy_b) = tokio::task::spawn_blocking(move || {
116116
// Simulate the battle
117-
let outcome = battle.simulate()
118-
.map_err(|e| format!("Battle simulation error: {:?}", e))?;
117+
let outcome = battle.simulate();
119118

120119
// Get final grid to measure energies
121120
let final_grid = battle.final_grid();
122121
let (energy_a, energy_b) = battle.measure_regional_energy(&final_grid);
123122

124-
Ok::<_, String>((outcome, energy_a, energy_b))
123+
(outcome, energy_a, energy_b)
125124
})
126125
.await
127-
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, Json(format!("Task join error: {}", e))))?
128-
.map_err(|e: String| (StatusCode::INTERNAL_SERVER_ERROR, Json(e)))?;
126+
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, Json(format!("Task join error: {}", e))))?;
129127

130128
let duration = start.elapsed();
131129

@@ -222,8 +220,7 @@ pub async fn run_battle_visualization(
222220
// Run simulation and capture frames
223221
let (outcome, frames) = tokio::task::spawn_blocking(move || {
224222
// Get outcome
225-
let outcome = battle.simulate()
226-
.map_err(|e| format!("Battle simulation error: {:?}", e))?;
223+
let outcome = battle.simulate();
227224

228225
// Get grid states at sample steps
229226
let grids = battle.grid_states(&sample_steps);
@@ -243,11 +240,10 @@ pub async fn run_battle_visualization(
243240
});
244241
}
245242

246-
Ok::<_, String>((outcome, frames))
243+
(outcome, frames)
247244
})
248245
.await
249-
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, Json(format!("Task join error: {}", e))))?
250-
.map_err(|e: String| (StatusCode::INTERNAL_SERVER_ERROR, Json(e)))?;
246+
.map_err(|e| (StatusCode::INTERNAL_SERVER_ERROR, Json(format!("Task join error: {}", e))))?;
251247

252248
let winner = match outcome {
253249
BattleOutcome::AWins => "glider_a".to_string(),

crates/bitcell-admin/src/lib.rs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ pub use api::AdminApi;
3030
pub use deployment::DeploymentManager;
3131
pub use config::ConfigManager;
3232
pub use process::ProcessManager;
33+
pub use setup::SETUP_FILE_PATH;
3334

3435
/// Administrative console server
3536
pub struct AdminConsole {
@@ -50,7 +51,7 @@ impl AdminConsole {
5051
let setup = Arc::new(setup::SetupManager::new());
5152

5253
// Try to load setup state from default location
53-
let setup_path = std::path::PathBuf::from(".bitcell/admin/setup.json");
54+
let setup_path = std::path::PathBuf::from(SETUP_FILE_PATH);
5455
if let Err(e) = setup.load_from_file(&setup_path) {
5556
tracing::warn!("Failed to load setup state: {}", e);
5657
}
@@ -112,7 +113,9 @@ impl AdminConsole {
112113
// Static files
113114
.nest_service("/static", ServeDir::new("static"))
114115

115-
// CORS
116+
// CORS - WARNING: Permissive CORS allows requests from any origin.
117+
// This is only suitable for local development. For production,
118+
// configure specific allowed origins to prevent CSRF attacks.
116119
.layer(CorsLayer::permissive())
117120

118121
// State

crates/bitcell-admin/src/metrics_client.rs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,9 +31,9 @@ impl MetricsClient {
3131
pub fn new() -> Self {
3232
Self {
3333
client: reqwest::Client::builder()
34-
.timeout(Duration::from_secs(5))
34+
.timeout(Duration::from_secs(10))
3535
.build()
36-
.unwrap(),
36+
.expect("Failed to build HTTP client for metrics"),
3737
}
3838
}
3939

@@ -58,6 +58,9 @@ impl MetricsClient {
5858
}
5959

6060
/// Parse Prometheus metrics format
61+
/// NOTE: This is a basic parser that only handles simple "metric_name value" format.
62+
/// It does NOT support metric labels (e.g., metric{label="value"}).
63+
/// For production use, consider using a proper Prometheus parsing library.
6164
fn parse_prometheus_metrics(&self, node_id: &str, endpoint: &str, text: &str) -> Result<NodeMetrics, String> {
6265
let mut metrics = HashMap::new();
6366

crates/bitcell-admin/src/process.rs

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,8 @@ impl ProcessManager {
6969
}
7070

7171
// Build command to start node
72+
// NOTE: Uses 'cargo run' which is suitable for development only.
73+
// For production deployments, use a compiled binary path instead.
7274
let mut cmd = Command::new("cargo");
7375
cmd.arg("run")
7476
.arg("-p")
@@ -93,7 +95,10 @@ impl ProcessManager {
9395

9496
// Spawn the process
9597
let child = cmd.spawn()
96-
.map_err(|e| format!("Failed to spawn process: {}", e))?;
98+
.map_err(|e| {
99+
tracing::error!("Failed to spawn process for node '{}': {:?}", id, e);
100+
"Failed to start node process".to_string()
101+
})?;
97102

98103
node.process = Some(child);
99104
node.info.status = NodeStatus::Running;
@@ -116,10 +121,20 @@ impl ProcessManager {
116121
// Try graceful shutdown first
117122
#[cfg(unix)]
118123
{
119-
use std::os::unix::process::CommandExt;
120124
let pid = process.id();
121-
unsafe {
122-
libc::kill(pid as i32, libc::SIGTERM);
125+
// SAFETY: We call libc::kill to send SIGTERM to the child process.
126+
// The PID is obtained from process.id(), which should be valid for a running child.
127+
// However, the process may have already exited, or permissions may be insufficient.
128+
// We check the return value for errors.
129+
let res = unsafe { libc::kill(pid as i32, libc::SIGTERM) };
130+
if res != 0 {
131+
let errno = std::io::Error::last_os_error();
132+
tracing::warn!(
133+
"Failed to send SIGTERM to process {} for node '{}': {}",
134+
pid,
135+
id,
136+
errno
137+
);
123138
}
124139

125140
// Wait up to 5 seconds for graceful shutdown

crates/bitcell-admin/src/setup.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@ use serde::{Deserialize, Serialize};
44
use std::path::PathBuf;
55
use std::sync::RwLock;
66

7+
/// Default setup file path
8+
pub const SETUP_FILE_PATH: &str = ".bitcell/admin/setup.json";
9+
710
#[derive(Debug, Clone, Serialize, Deserialize)]
811
pub struct SetupState {
912
pub initialized: bool,

0 commit comments

Comments
 (0)