Skip to content

Commit

Permalink
feat: add restriction for installing modules with forbidden mounted b…
Browse files Browse the repository at this point in the history
…inaries [NET-428] (#1535)

* add restriction for installing modules with forbidden mounted binaries
  • Loading branch information
kmd-fl authored Mar 29, 2023
1 parent dc69146 commit 481dbd4
Show file tree
Hide file tree
Showing 13 changed files with 203 additions and 61 deletions.
6 changes: 4 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ test:
cargo test --release

server:
RUST_LOG="info,tide=off,run-console=debug" \
RUST_LOG="info,tide=off,tracing=off,avm_server=off,run-console=debug" \
cargo run --release -p particle-node

server-debug:
Expand All @@ -33,7 +33,9 @@ server-debug:
wasmer_interface_types_fl=info,\
async_std=info,\
async_io=info,\
polling=info" \
polling=info, \
avm_server=off,\
tracing=off"\
cargo run --release -p particle-node -- -c ./deploy/Config.default.toml

.PHONY: server server-debug test release build deploy
3 changes: 3 additions & 0 deletions crates/created-swarm/src/swarm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,7 @@ pub struct SwarmConfig {
pub builtins_dir: Option<PathBuf>,
pub spell_base_dir: Option<PathBuf>,
pub timer_resolution: Duration,
pub allowed_binaries: Vec<String>,
}

impl SwarmConfig {
Expand All @@ -261,6 +262,7 @@ impl SwarmConfig {
builtins_dir: None,
spell_base_dir: None,
timer_resolution: default_script_storage_timer_resolution(),
allowed_binaries: vec![],
}
}
}
Expand Down Expand Up @@ -351,6 +353,7 @@ pub fn create_swarm_with_runtime<RT: AquaRuntime>(
resolved.node_config.particle_execution_timeout = EXECUTION_TIMEOUT;

resolved.node_config.script_storage_timer_resolution = config.timer_resolution;
resolved.node_config.allowed_binaries = config.allowed_binaries.clone();

let management_kp = fluence_keypair::KeyPair::generate_ed25519();
let management_peer_id = libp2p::identity::Keypair::from(management_kp.clone())
Expand Down
121 changes: 121 additions & 0 deletions crates/particle-node-tests/tests/modules.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
/*
* Copyright 2023 Fluence Labs Limited
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

use base64::{engine::general_purpose::STANDARD as base64, Engine};
use connected_client::ConnectedClient;
use created_swarm::{make_swarms, make_swarms_with_cfg};
use maplit::hashmap;
use serde_json::json;
use service_modules::load_module;

#[tokio::test]
async fn test_add_module_mounted_binaries() {
let swarms = make_swarms_with_cfg(1, |mut cfg| {
cfg.allowed_binaries = vec!["/usr/bin/curl".to_string()];
cfg
})
.await;

let mut client = ConnectedClient::connect_to(swarms[0].multiaddr.clone())
.await
.expect("connect client");
let module = load_module("tests/tetraplets/artifacts", "tetraplets").expect("load module");

let config = json!(
{
"name": "tetraplets",
"mem_pages_count": 100,
"logger_enabled": true,
"wasi": {
"envs": json!({}),
"preopened_files": vec!["/tmp"],
"mapped_dirs": json!({}),
},
"mounted_binaries": json!({"cmd": "/usr/bin/curl"})
});

let script = r#"
(xor
(seq
(call node ("dist" "add_module") [module_bytes module_config])
(call client ("return" "") ["ok"])
)
(call client ("return" "") [%last_error%.$.message])
)
"#;

let data = hashmap! {
"client" => json!(client.peer_id.to_string()),
"node" => json!(client.node.to_string()),
"module_bytes" => json!(base64.encode(&module)),
"module_config" => config,
};

let response = client.execute_particle(script, data).await.unwrap();
if let Some(result) = response[0].as_str() {
assert_eq!("ok", result);
} else {
panic!("can't receive response from node");
}
}

#[tokio::test]
async fn test_add_module_mounted_binaries_forbidden() {
let swarms = make_swarms(1).await;

let mut client = ConnectedClient::connect_to(swarms[0].multiaddr.clone())
.await
.expect("connect client");
let module = load_module("tests/tetraplets/artifacts", "tetraplets").expect("load module");

let config = json!(
{
"name": "tetraplets",
"mem_pages_count": 100,
"logger_enabled": true,
"wasi": {
"envs": json!({}),
"preopened_files": vec!["/tmp"],
"mapped_dirs": json!({}),
},
"mounted_binaries": json!({"cmd": "/usr/bin/behbehbeh"})
});

let script = r#"
(xor
(seq
(call node ("dist" "add_module") [module_bytes module_config])
(call client ("return" "") ["ok"])
)
(call client ("return" "") [%last_error%.$.message])
)
"#;

let data = hashmap! {
"client" => json!(client.peer_id.to_string()),
"node" => json!(client.node.to_string()),
"module_bytes" => json!(base64.encode(&module)),
"module_config" => config,
};

let response = client.execute_particle(script, data).await.unwrap();
if let Some(result) = response[0].as_str() {
let expected = "Local service error, ret_code is 1, error message is '\"Error: Config error: requested mounted binary /usr/bin/behbehbeh is forbidden on this host\\nForbiddenMountedBinary { forbidden_path: \\\"/usr/bin/behbehbeh\\\" }\"'";
assert_eq!(expected, result);
} else {
panic!("can't receive response from node");
}
}
49 changes: 0 additions & 49 deletions crates/particle-node-tests/tests/script_storage.rs
Original file line number Diff line number Diff line change
Expand Up @@ -617,55 +617,6 @@ async fn add_script_delay_oneshot() {
assert_eq!(list, vec![serde_json::Value::Array(vec![])]);
}

#[tokio::test]
async fn add_script_random_delay() {
let swarms = make_swarms(1).await;

let interval = 3u64;

let mut client = ConnectedClient::connect_to(swarms[0].multiaddr.clone())
.await
.wrap_err("connect client")
.unwrap();

let script = f!(r#"
(seq
(call "{client.node}" ("peer" "timestamp_sec") [] result)
(call "{client.peer_id}" ("op" "return") [result])
)
"#);

let mut res = client
.execute_particle(
f!(r#"
(seq
(call relay ("peer" "timestamp_sec") [] now)
(seq
(call relay ("script" "add") [script "{interval}"])
(call %init_peer_id% ("op" "return") [now])
)
)
"#),
hashmap! {
"relay" => json!(client.node.to_string()),
"script" => json!(script),
},
)
.await
.unwrap();

let res = res.pop().unwrap();
let now = res.as_u64().unwrap();

let res = client.receive_args().await.wrap_err("receive").unwrap();
let res = res.into_iter().next().unwrap().as_u64().unwrap();
let eps = 2u64;
let expected = now + interval + eps;
log::info!("res {}", res);
log::info!("expected {}", expected);
assert!((now..=expected).contains(&res));
}

async fn create_file_share(client: &mut ConnectedClient) -> CreatedService {
create_service(
client,
Expand Down
12 changes: 8 additions & 4 deletions crates/particle-node-tests/tests/services.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,16 @@ use serde_json::Value as JValue;

use base64::{engine::general_purpose::STANDARD as base64, Engine};
use connected_client::ConnectedClient;
use created_swarm::make_swarms;
use created_swarm::{make_swarms, make_swarms_with_cfg};
use service_modules::load_module;

#[tokio::test]
async fn create_service_from_config() {
let swarms = make_swarms(1).await;
let swarms = make_swarms_with_cfg(1, |mut cfg| {
cfg.allowed_binaries = vec!["/does/not/exist".to_string()];
cfg
})
.await;

let mut client = ConnectedClient::connect_to(swarms[0].multiaddr.clone())
.await
Expand Down Expand Up @@ -64,11 +68,11 @@ async fn create_service_from_config() {
[
[
"abc",
"/tmp"
"/does/not/exist"
],
[
"2222",
"/tmp"
"/does/not/exist"
]
]
]
Expand Down
4 changes: 4 additions & 0 deletions crates/server-config/src/defaults.rs
Original file line number Diff line number Diff line change
Expand Up @@ -204,3 +204,7 @@ pub fn default_module_max_heap_size() -> bytesize::ByteSize {
pub fn default_max_builtin_metrics_storage_size() -> usize {
5
}

pub fn default_allowed_binaries() -> Vec<String> {
vec!["/usr/bin/curl".to_string(), "/usr/bin/ipfs".to_string()]
}
3 changes: 3 additions & 0 deletions crates/server-config/src/node_config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,9 @@ pub struct NodeConfig {
#[serde(with = "peerid_serializer")]
#[serde(default = "default_management_peer_id")]
pub management_peer_id: PeerId,

#[serde(default = "default_allowed_binaries")]
pub allowed_binaries: Vec<String>,
}

#[derive(Clone, Deserialize, Derivative, Copy)]
Expand Down
21 changes: 19 additions & 2 deletions crates/server-config/src/services_config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@ use fs_utils::{create_dirs, set_write_only, to_abs_path};

use bytesize::ByteSize;
use libp2p::PeerId;
use std::collections::HashMap;
use std::path::PathBuf;
use std::collections::{HashMap, HashSet};
use std::path::{Path, PathBuf};

#[derive(Debug, Clone)]
pub struct ServicesConfig {
Expand Down Expand Up @@ -47,6 +47,8 @@ pub struct ServicesConfig {
pub max_heap_size: ByteSize,
/// Default heap size in bytes available for the module unless otherwise specified.
pub default_heap_size: Option<ByteSize>,
/// List of allowed binaries paths
pub allowed_binaries: HashSet<PathBuf>,
}

impl ServicesConfig {
Expand All @@ -60,9 +62,23 @@ impl ServicesConfig {
builtins_management_peer_id: PeerId,
max_heap_size: ByteSize,
default_heap_size: Option<ByteSize>,
allowed_binaries: Vec<String>,
) -> Result<Self, std::io::Error> {
let base_dir = to_abs_path(base_dir);

let allowed_binaries = allowed_binaries
.into_iter()
.map(|path_str| {
let path = Path::new(&path_str);
match path.try_exists() {
Err(err) => log::warn!("cannot check binary `{path_str}`: {err}"),
Ok(false) => log::warn!("binary `{path_str}` does not exist"),
_ => {}
};
path.to_path_buf()
})
.collect::<_>();

let this = Self {
local_peer_id,
blueprint_dir: config_utils::blueprint_dir(&base_dir),
Expand All @@ -75,6 +91,7 @@ impl ServicesConfig {
builtins_management_peer_id,
max_heap_size,
default_heap_size,
allowed_binaries,
};

create_dirs(&[
Expand Down
1 change: 1 addition & 0 deletions particle-builtins/src/builtins.rs
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@ where
vault_dir,
config.max_heap_size,
config.default_heap_size,
config.allowed_binaries.clone(),
);
let particles_vault_dir = vault_dir.to_path_buf();
let management_peer_id = config.management_peer_id;
Expand Down
2 changes: 2 additions & 0 deletions particle-modules/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,8 @@ pub enum ModuleError {
max_heap_size_wanted: u64,
max_heap_size_allowed: u64,
},
#[error("Config error: requested mounted binary {forbidden_path} is forbidden on this host")]
ForbiddenMountedBinary { forbidden_path: String },
}

impl From<ModuleError> for JValue {
Expand Down
Loading

0 comments on commit 481dbd4

Please sign in to comment.