Skip to content

Commit a3666ef

Browse files
Merge pull request #27 from fastrevmd-lab/security-remediation
security: remediate RNC-SEC-001..006 + CI hardening
2 parents edf560f + 7e59a63 commit a3666ef

45 files changed

Lines changed: 1753 additions & 653 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.cargo/audit.toml

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
# cargo-audit configuration.
2+
#
3+
# This file lists RustSec advisories that are tracked and explicitly
4+
# risk-accepted (or pending upstream fixes). Each ignore entry must
5+
# include a rationale and a review date so it does not become invisible.
6+
7+
[advisories]
8+
ignore = [
9+
# RUSTSEC-2023-0071 — Marvin Attack timing side-channel in `rsa`.
10+
#
11+
# Reachability: `rsa 0.10.0-rc.16` is pulled in through
12+
# russh -> internal-russh-forked-ssh-key -> rsa.
13+
# The dependency exists unconditionally inside the forked ssh-key
14+
# crate; russh has no opt-out feature for it. Upstream `cargo audit`
15+
# reports "No fixed upgrade is available!"
16+
#
17+
# Exposure: only matters when an RSA SSH key is used. The library
18+
# default and CLI templates do not generate or use RSA keys, and
19+
# rustnetconf supports ed25519/ECDSA paths through SSH agent and
20+
# key_file auth.
21+
#
22+
# Mitigation: README/security documentation recommends Ed25519 or
23+
# ECDSA SSH keys until a fixed upstream release lands.
24+
#
25+
# Review date: 2026-08-01 (re-check russh + ssh-key for a fixed rsa
26+
# release; remove this ignore once upstream resolves it).
27+
"RUSTSEC-2023-0071",
28+
]

.github/workflows/ci.yml

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
name: CI
2+
3+
on:
4+
push:
5+
branches: [main]
6+
pull_request:
7+
branches: [main]
8+
9+
env:
10+
CARGO_TERM_COLOR: always
11+
RUSTFLAGS: -D warnings
12+
13+
jobs:
14+
test:
15+
name: Test (workspace, all features)
16+
runs-on: ubuntu-latest
17+
steps:
18+
- uses: actions/checkout@v4
19+
20+
- name: Install system dependencies
21+
run: |
22+
sudo apt-get update
23+
sudo apt-get install -y cmake
24+
25+
- name: Install Rust toolchain
26+
uses: dtolnay/rust-toolchain@stable
27+
28+
- name: Cache cargo registry and target
29+
uses: Swatinem/rust-cache@v2
30+
31+
- name: Build
32+
run: cargo build --workspace --all-features --verbose
33+
34+
- name: Test
35+
run: cargo test --workspace --all-features --verbose
36+
env:
37+
SKIP_INTEGRATION: "1"
38+
39+
clippy:
40+
name: Clippy (workspace, all features)
41+
runs-on: ubuntu-latest
42+
steps:
43+
- uses: actions/checkout@v4
44+
45+
- name: Install system dependencies
46+
run: |
47+
sudo apt-get update
48+
sudo apt-get install -y cmake
49+
50+
- name: Install Rust toolchain
51+
uses: dtolnay/rust-toolchain@stable
52+
with:
53+
components: clippy
54+
55+
- name: Cache cargo registry and target
56+
uses: Swatinem/rust-cache@v2
57+
58+
- name: Clippy
59+
run: cargo clippy --workspace --all-targets --all-features -- -D warnings
60+
61+
fmt:
62+
name: Rustfmt
63+
runs-on: ubuntu-latest
64+
steps:
65+
- uses: actions/checkout@v4
66+
67+
- name: Install Rust toolchain
68+
uses: dtolnay/rust-toolchain@stable
69+
with:
70+
components: rustfmt
71+
72+
- name: Check formatting
73+
run: cargo fmt --all -- --check
74+
75+
audit:
76+
name: cargo audit
77+
runs-on: ubuntu-latest
78+
steps:
79+
- uses: actions/checkout@v4
80+
81+
- name: Install Rust toolchain
82+
uses: dtolnay/rust-toolchain@stable
83+
84+
- name: Install cargo-audit
85+
run: cargo install --locked cargo-audit
86+
87+
- name: Run cargo audit
88+
run: cargo audit

Cargo.lock

Lines changed: 11 additions & 9 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

README.md

Lines changed: 38 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,12 @@ key_file = "~/.ssh/id_ed25519"
9898
# vendor auto-detected from device hello
9999
```
100100

101+
**Secrets:** `inventory.toml` may contain plaintext passwords. Prefer
102+
`key_file` or SSH-agent auth where possible. If you must use inline
103+
passwords, protect the file with `chmod 600 inventory.toml` and add it
104+
to `.gitignore`. Passwords are stored in zeroizing memory and redacted
105+
from `Debug` output, but the on-disk file itself is plaintext.
106+
101107
## Library — Quick Start
102108

103109
```toml
@@ -391,8 +397,25 @@ match result {
391397
- **Mock transport tests** — session state machine, CommitUnknown detection, lock recovery
392398
- **Integration tests** — 32 tests against a live Juniper vSRX including full edit-config round trips, vendor auto-detection, connection pooling, and concurrent sessions
393399

400+
### Prerequisites
401+
402+
The `rustnetconf-yang` subcrate builds `libyang2` from source via `yang2`'s `bundled` feature, which requires `cmake`. Install it before running workspace-wide tests or clippy:
403+
394404
```bash
395-
cargo test --workspace # Run all tests
405+
# Debian/Ubuntu
406+
sudo apt-get install cmake
407+
408+
# macOS
409+
brew install cmake
410+
411+
# Fedora/RHEL
412+
sudo dnf install cmake
413+
```
414+
415+
The core `rustnetconf` and `rustnetconf-cli` crates do not require `cmake`; `cargo test -p rustnetconf` works without it.
416+
417+
```bash
418+
cargo test --workspace # Run all tests (requires cmake)
396419
cargo test --test integration_vsrx # Run vSRX integration tests only
397420
SKIP_INTEGRATION=1 cargo test # Skip tests requiring a device
398421
```
@@ -408,7 +431,7 @@ SKIP_INTEGRATION=1 cargo test # Skip tests requiring a device
408431
### Security Features
409432

410433
- **Credential zeroization** — Passwords and key passphrases use `Zeroizing<String>` (via the `zeroize` crate) and are securely erased from memory on drop.
411-
- **SSH host key verification**`HostKeyVerification` must be set explicitly (no `Default` impl). Use `Fingerprint("SHA256:...")` to pin host keys in production. `AcceptAll` is available for lab use but emits a `tracing::warn!`.
434+
- **SSH host key verification**`HostKeyVerification` must be set explicitly. The `ClientBuilder` default is `RejectAll` (fail closed): the SSH handshake fails until the caller pins a fingerprint via `Fingerprint("SHA256:...")` or explicitly opts in to `AcceptAll` for lab use (logs a `tracing::warn!`). `ProxyJump` hops parsed from `~/.ssh/config` likewise default to `RejectAll` and must be individually configured. In the CLI, set `host_key_fingerprint` per device in `inventory.toml`, or pass `--insecure-accept-host-key` for lab use only.
412435
- **Shell-escaped ProxyCommand**`%h` and `%p` substitutions are shell-escaped to prevent command injection via malicious hostnames.
413436
- **XML fragment validation** — All user-provided RPC content is validated for well-formedness before insertion, preventing XML injection.
414437
- **XML attribute escaping** — All message-id values are escaped to prevent XML attribute injection.
@@ -419,10 +442,21 @@ SKIP_INTEGRATION=1 cargo test # Skip tests requiring a device
419442
- **Typed error hierarchy** — Structured error types (`ChannelClosed`, `SessionExpired`, `MessageIdMismatch`) enable precise error handling without string matching.
420443
- **No unsafe code** — The entire codebase uses safe Rust.
421444

445+
### Known advisories
446+
447+
- **RUSTSEC-2023-0071** (Marvin Attack, `rsa` crate timing side-channel)
448+
is present in the dependency graph via
449+
`russh → internal-russh-forked-ssh-key → rsa 0.10.0-rc.16`. No fixed
450+
upstream release is available yet. The advisory is risk-accepted with
451+
rationale in `.cargo/audit.toml` and CI re-checks on every run. It only
452+
matters when an **RSA** SSH key is used for authentication — Ed25519
453+
and ECDSA paths are unaffected. Use the mitigation in the next section.
454+
422455
### Security Best Practices
423456

424-
- Use Ed25519 SSH keys (not RSA) for device authentication
425-
- Set `host_key_verification(HostKeyVerification::Fingerprint(...))` in production — `HostKeyVerification` has no default, so you must choose explicitly
457+
- Use Ed25519 SSH keys (not RSA) for device authentication (also mitigates
458+
RUSTSEC-2023-0071 above)
459+
- Set `host_key_verification(HostKeyVerification::Fingerprint(...))` in production — the default is `RejectAll` (fail closed), so the connection will refuse to complete until you choose a policy. For the CLI, set `host_key_fingerprint = "SHA256:..."` per device in `inventory.toml`.
426460
- Set `.rpc_timeout(Duration::from_secs(30))` to prevent hanging on unresponsive devices
427461
- Prefer SSH agent auth over inline passwords
428462
- Store credentials in inventory.toml with restricted file permissions (`chmod 600`)

examples/edit_config.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,9 @@ async fn main() {
8484
if let Some(caps) = client.capabilities() {
8585
eprintln!(
8686
"Connected (session-id: {})",
87-
caps.session_id().map(|id| id.to_string()).unwrap_or_else(|| "unknown".into()),
87+
caps.session_id()
88+
.map(|id| id.to_string())
89+
.unwrap_or_else(|| "unknown".into()),
8890
);
8991
}
9092

examples/get_config.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,7 +75,9 @@ async fn main() {
7575
if let Some(caps) = client.capabilities() {
7676
eprintln!(
7777
"Connected (session-id: {}, capabilities: {})",
78-
caps.session_id().map(|id| id.to_string()).unwrap_or_else(|| "unknown".into()),
78+
caps.session_id()
79+
.map(|id| id.to_string())
80+
.unwrap_or_else(|| "unknown".into()),
7981
caps.all_uris().len()
8082
);
8183
}

examples/multi_device.rs

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,10 @@ async fn main() {
6161
process::exit(1);
6262
}
6363

64-
eprintln!("Fetching config from {} devices concurrently...", hosts.len());
64+
eprintln!(
65+
"Fetching config from {} devices concurrently...",
66+
hosts.len()
67+
);
6568
let start = Instant::now();
6669

6770
// Spawn concurrent tasks for each device
@@ -90,7 +93,11 @@ async fn main() {
9093
let result = match client.get_config(Datastore::Running).await {
9194
Ok(config) => {
9295
let elapsed = device_start.elapsed();
93-
Ok(format!("{} bytes in {:.1}s", config.len(), elapsed.as_secs_f64()))
96+
Ok(format!(
97+
"{} bytes in {:.1}s",
98+
config.len(),
99+
elapsed.as_secs_f64()
100+
))
94101
}
95102
Err(e) => Err(format!("get-config failed: {e}")),
96103
};

rustnetconf-cli/Cargo.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,3 +22,7 @@ serde_json = "1"
2222
quick-xml = "0.37"
2323
tokio = { version = "1", features = ["full"] }
2424
dialoguer = "0.11"
25+
zeroize = "1"
26+
27+
[dev-dependencies]
28+
tempfile = "3"

0 commit comments

Comments
 (0)