Skip to content

ci: trigger production safety remediation #1

ci: trigger production safety remediation

ci: trigger production safety remediation #1

name: Apply production safety pass
on:
push:
branches:
- remediation/all-20260620
paths:
- REMEDIATION_TRIGGER
permissions:
contents: write
concurrency:
group: production-safety-pass
cancel-in-progress: false
jobs:
patch-test-commit:
runs-on: ubuntu-latest
timeout-minutes: 120
steps:
- uses: actions/checkout@v4
with:
ref: remediation/all-20260620
fetch-depth: 0
- uses: dtolnay/rust-toolchain@stable
- uses: Swatinem/rust-cache@v2
- name: Apply cross-crate remediation
shell: bash
run: |
python3 - <<'PY'
from pathlib import Path
from textwrap import dedent
def replace_once(path: str, old: str, new: str) -> None:
p = Path(path)
text = p.read_text()
count = text.count(old)
if count != 1:
raise SystemExit(f"{path}: expected one anchor, found {count}")
p.write_text(text.replace(old, new))
def append_once(path: str, marker: str, content: str) -> None:
p = Path(path)
text = p.read_text()
if marker not in text:
p.write_text(text.rstrip() + "\n" + content)
# Node startup: explicit opt-in for uncertified multi-validator mode;
# mainnet remains blocked until checkpoint certificates are implemented.
replace_once(
"crates/kanari-node/src/main.rs",
dedent('''\
/// Bootstrap peer multiaddr to connect to (can be specified multiple times)
#[arg(long, value_name = "MULTIADDR")]
bootstrap: Option<Vec<String>>,
'''),
dedent('''\
/// Bootstrap peer multiaddr to connect to (can be specified multiple times)
#[arg(long, value_name = "MULTIADDR")]
bootstrap: Option<Vec<String>>,
/// Explicitly opt in to the current uncertified multi-validator protocol.
/// This is intended only for controlled development and test environments.
#[arg(long, default_value = "false")]
unsafe_allow_uncertified_consensus: bool,
'''),
)
replace_once(
"crates/kanari-node/src/main.rs",
dedent('''\
consensus_public_keys,
bootstrap,
} => {
validate_start_authority_config(&authority_id, &authorities)?;
'''),
dedent('''\
consensus_public_keys,
bootstrap,
unsafe_allow_uncertified_consensus,
} => {
validate_start_authority_config(&authority_id, &authorities)?;
validate_network_safety(&network, unsafe_allow_uncertified_consensus)?;
'''),
)
replace_once(
"crates/kanari-node/src/main.rs",
"fn validate_consensus_private_key_file(path: &Path) -> Result<()> {\n",
dedent('''\
fn validate_network_safety(
network: &NetworkMode,
unsafe_allow_uncertified_consensus: bool,
) -> Result<()> {
if matches!(network, NetworkMode::Mainnet) {
anyhow::bail!(
"mainnet startup is disabled until authenticated checkpoint certificates and atomic execution are implemented"
);
}
if !unsafe_allow_uncertified_consensus {
anyhow::bail!(
"multi-validator startup requires --unsafe-allow-uncertified-consensus until certified finality is implemented"
);
}
Ok(())
}
fn validate_consensus_private_key_file(path: &Path) -> Result<()> {
'''),
)
replace_once(
"crates/kanari-node/src/main.rs",
" use super::{read_consensus_private_key, validate_start_authority_config};\n",
" use super::{\n NetworkMode, read_consensus_private_key, validate_network_safety,\n validate_start_authority_config,\n };\n",
)
replace_once(
"crates/kanari-node/src/main.rs",
dedent('''\
#[cfg(unix)]
#[test]
fn consensus_key_file_rejects_group_or_world_permissions() {
'''),
dedent('''\
#[test]
fn mainnet_is_always_blocked() {
assert!(validate_network_safety(&NetworkMode::Mainnet, true).is_err());
}
#[test]
fn testnet_requires_explicit_unsafe_opt_in() {
assert!(validate_network_safety(&NetworkMode::Testnet, false).is_err());
assert!(validate_network_safety(&NetworkMode::Testnet, true).is_ok());
}
#[cfg(unix)]
#[test]
fn consensus_key_file_rejects_group_or_world_permissions() {
'''),
)
# P2P compressed payload bounds.
replace_once(
"crates/kanari-node/src/p2p.rs",
"const LARGE_MESSAGE_COMPRESSION_THRESHOLD: usize = 100_000;\n",
dedent('''\
const LARGE_MESSAGE_COMPRESSION_THRESHOLD: usize = 100_000;
const MAX_COMPRESSED_P2P_PAYLOAD: usize = 1_000_000;
const MAX_DECOMPRESSED_P2P_PAYLOAD: usize = 8 * 1024 * 1024;
'''),
)
replace_once(
"crates/kanari-node/src/p2p.rs",
dedent('''\
/// Decompress a compressed UTF-8 P2P payload.
pub fn decompress_payload(compressed_data: Vec<u8>) -> Result<String> {
let mut decoder = GzDecoder::new(&compressed_data[..]);
let mut decompressed = String::new();
decoder.read_to_string(&mut decompressed)?;
Ok(decompressed)
}
'''),
dedent('''\
/// Decompress a compressed UTF-8 P2P payload with strict bounds.
pub fn decompress_payload(compressed_data: Vec<u8>) -> Result<String> {
if compressed_data.len() > MAX_COMPRESSED_P2P_PAYLOAD {
anyhow::bail!(
"compressed P2P payload exceeds {} bytes",
MAX_COMPRESSED_P2P_PAYLOAD
);
}
let decoder = GzDecoder::new(&compressed_data[..]);
let mut limited = decoder.take((MAX_DECOMPRESSED_P2P_PAYLOAD + 1) as u64);
let mut decompressed = Vec::new();
limited.read_to_end(&mut decompressed)?;
if decompressed.len() > MAX_DECOMPRESSED_P2P_PAYLOAD {
anyhow::bail!(
"decompressed P2P payload exceeds {} bytes",
MAX_DECOMPRESSED_P2P_PAYLOAD
);
}
String::from_utf8(decompressed)
.map_err(|e| anyhow::anyhow!("decompressed P2P payload is not UTF-8: {}", e))
}
'''),
)
append_once(
"crates/kanari-node/src/p2p.rs",
"mod bounded_decompression_tests",
dedent('''\
#[cfg(test)]
mod bounded_decompression_tests {
use super::*;
#[test]
fn compressed_payload_round_trips() {
let original = "checkpoint-payload".repeat(1024);
let compressed = gzip_string(&original).unwrap();
assert_eq!(decompress_payload(compressed).unwrap(), original);
}
#[test]
fn decompression_bomb_is_rejected() {
let oversized = "x".repeat(MAX_DECOMPRESSED_P2P_PAYLOAD + 1);
let compressed = gzip_string(&oversized).unwrap();
assert!(decompress_payload(compressed).is_err());
}
#[test]
fn oversized_compressed_input_is_rejected() {
assert!(
decompress_payload(vec![0u8; MAX_COMPRESSED_P2P_PAYLOAD + 1]).is_err()
);
}
}
'''),
)
# RPC immediate execution is simulation only; query sizes are bounded.
replace_once(
"crates/kanari-rpc-server/src/transaction/mod.rs",
dedent('''\
let tx_for_broadcast = signed_tx.clone();
if let Err(e) = state.engine.submit_transactions_batch(vec![signed_tx]) {
error!("Failed to submit executed transaction: {}", e);
return RpcResponse {
jsonrpc: "2.0".into(),
result: None,
error: Some(RpcError::transaction_error(format!(
"Simulation successful, but mempool submission failed: {}",
e
))),
id: request_id,
};
}
state.broadcast_submitted_transaction(tx_for_broadcast);
info!(
"{} executed immediately & submitted: {}",
action, tx_hash_hex
);
respond_with_serialize(
request_id,
serde_json::json!({
"hash": tx_hash_hex,
"status": "executed",
"action": action,
"changeset": cs_value
}),
)
'''),
dedent('''\
info!("{} simulated without submission: {}", action, tx_hash_hex);
respond_with_serialize(
request_id,
serde_json::json!({
"hash": tx_hash_hex,
"status": "simulated",
"submitted": false,
"action": action,
"changeset": cs_value
}),
)
'''),
)
replace_once(
"crates/kanari-rpc-server/src/transaction/mod.rs",
dedent('''\
let limit = request
.params
.get("limit")
.and_then(|v| v.as_u64())
.unwrap_or(50) as usize;
'''),
dedent('''\
const DEFAULT_LIMIT: u64 = 50;
const MAX_LIMIT: u64 = 200;
let requested_limit = request
.params
.get("limit")
.and_then(|v| v.as_u64())
.unwrap_or(DEFAULT_LIMIT);
if requested_limit == 0 || requested_limit > MAX_LIMIT {
return invalid_params_response(
request.id,
format!("limit must be between 1 and {}", MAX_LIMIT),
);
}
let limit = match usize::try_from(requested_limit) {
Ok(limit) => limit,
Err(_) => return invalid_params_response(request.id, "limit is too large"),
};
'''),
)
# Runtime storage must fail closed instead of silently creating divergent state.
runtime_path = "move-execution/v1/kanari-move-runtime-v1/src/move_runtime/mod.rs"
replace_once(
runtime_path,
dedent('''\
Err(e) => {
log::warn!("[RUNTIME] shared object store load failed: {}", e);
Arc::from(ObjectStorage::boxed_inmemory())
}
'''),
dedent('''\
Err(e) => {
return Err(anyhow::anyhow!(
"[RUNTIME] shared object store load failed: {}",
e
));
}
'''),
)
replace_once(
runtime_path,
dedent('''\
Err(e) => {
log::warn!("[RUNTIME] DB load failed. Fallback to in-memory: {}", e);
Arc::from(ObjectStorage::boxed_inmemory())
}
'''),
dedent('''\
Err(e) => {
return Err(anyhow::anyhow!("[RUNTIME] object DB load failed: {}", e));
}
'''),
)
replace_once(
runtime_path,
dedent('''\
Err(e) => {
log::warn!("[RUNTIME] isolated object store load failed: {}", e);
Arc::from(ObjectStorage::boxed_inmemory())
}
'''),
dedent('''\
Err(e) => {
return Err(anyhow::anyhow!(
"[RUNTIME] isolated object store load failed: {}",
e
));
}
'''),
)
replace_once(
runtime_path,
dedent('''\
let published_modules: HashSet<ModuleId> = state
.get_all_module_ids()
.unwrap_or_default()
.into_iter()
.collect();
'''),
dedent('''\
let published_modules: HashSet<ModuleId> =
state.get_all_module_ids()?.into_iter().collect();
'''),
)
# RPC resource limits and restrictive default CORS.
replace_once(
"Cargo.toml",
'tower-http = { version = "0.7.0", features = ["cors"] }\n',
'tower-http = { version = "0.7.0", features = ["cors", "limit", "timeout"] }\n'
'tower = { version = "0.5.3", features = ["limit"] }\n',
)
replace_once(
"crates/kanari-rpc-server/Cargo.toml",
"tower-http = { workspace = true }\n",
"tower-http = { workspace = true }\ntower.workspace = true\n",
)
replace_once(
"crates/kanari-rpc-server/Cargo.toml",
'tower = { version = "0.5.3", features = ["util"] }\n',
'tower = { workspace = true, features = ["util"] }\n',
)
rpc_lib = "crates/kanari-rpc-server/src/lib.rs"
replace_once(
rpc_lib,
" http::{StatusCode, header},\n",
" http::{Method, StatusCode, header},\n",
)
replace_once(
rpc_lib,
dedent('''\
use std::sync::Arc;
use tower_http::cors::{Any, CorsLayer};
use tracing::info;
'''),
dedent('''\
use std::{sync::Arc, time::Duration};
use tower::limit::ConcurrencyLimitLayer;
use tower_http::{
cors::CorsLayer,
limit::RequestBodyLimitLayer,
timeout::TimeoutLayer,
};
use tracing::info;
'''),
)
replace_once(
rpc_lib,
"type TransactionBroadcaster = Arc<dyn Fn(SignedTransaction) -> Result<()> + Send + Sync>;\n",
dedent('''\
const MAX_RPC_BODY_BYTES: usize = 2 * 1024 * 1024;
const MAX_RPC_CONCURRENCY: usize = 128;
const RPC_TIMEOUT_SECONDS: u64 = 30;
type TransactionBroadcaster = Arc<dyn Fn(SignedTransaction) -> Result<()> + Send + Sync>;
'''),
)
replace_once(
rpc_lib,
dedent('''\
pub fn create_router(state: RpcServerState) -> Router {
let cors = CorsLayer::new()
.allow_origin(Any)
.allow_methods(Any)
.allow_headers(Any);
Router::new()
.route("/", post(handle_rpc))
.route("/rpc", post(handle_rpc))
.route("/metrics", get(handle_metrics))
.layer(cors)
.with_state(state)
}
'''),
dedent('''\
pub fn create_router(state: RpcServerState) -> Router {
let cors = CorsLayer::new()
.allow_methods([Method::GET, Method::POST])
.allow_headers([header::CONTENT_TYPE]);
Router::new()
.route("/", post(handle_rpc))
.route("/rpc", post(handle_rpc))
.route("/metrics", get(handle_metrics))
.layer(RequestBodyLimitLayer::new(MAX_RPC_BODY_BYTES))
.layer(TimeoutLayer::new(Duration::from_secs(RPC_TIMEOUT_SECONDS)))
.layer(ConcurrencyLimitLayer::new(MAX_RPC_CONCURRENCY))
.layer(cors)
.with_state(state)
}
'''),
)
PY
- name: Format and validate
run: |
cargo fmt --all
git diff --check
cargo check -p kanari-auth -p kanari-node -p kanari-core -p kanari-rpc-server -p kanari-move-runtime-v1
cargo test -p kanari-auth --lib
cargo test -p kanari-node --lib
cargo test -p kanari-rpc-server --lib
- name: Commit validated remediation
shell: bash
run: |
git config user.name kanari-security-bot
git config user.email security-bot@users.noreply.github.com
git add -A
if ! git diff --cached --quiet; then
git commit -m "security: apply production safety pass"
git push origin HEAD:remediation/all-20260620
fi