Skip to content

Commit d9f8391

Browse files
committed
feat: Add remote integrity verification and eBPF XDP loader
Core security features for v1.0: ## Remote Integrity Verification - wharf sec verify <yacht> now verifies remote yachts via SSH - Generates shell script that computes BLAKE3 hashes on remote - Compares against local manifest - Reports mismatched, missing, and unexpected files - Falls back gracefully on errors ## eBPF XDP Firewall Loader - New ebpf.rs module in yacht-agent for userspace loading - Loads compiled eBPF object file (wharf-shield.o) - Attaches to XDP hook with hardware/driver/generic fallback - Populates allowed ports map (80, 443, 4242, 9001) - Provides blocklist management (block_ip, unblock_ip) - Can load blocklist from file ## Files Changed - crates/wharf-core/src/integrity.rs: RemoteVerifyResult, verify_remote_ssh() - bin/wharf-cli/src/ops/integrity.rs: verify_remote() - bin/wharf-cli/src/main.rs: Remote yacht verification in sec verify - bin/yacht-agent/src/ebpf.rs: New Shield struct and loader - bin/yacht-agent/src/main.rs: eBPF loading with fallback to nftables - bin/yacht-agent/Cargo.toml: Added aya dependency
1 parent 706805c commit d9f8391

6 files changed

Lines changed: 581 additions & 36 deletions

File tree

bin/wharf-cli/src/main.rs

Lines changed: 79 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -624,42 +624,91 @@ async fn main() -> anyhow::Result<()> {
624624
anyhow::bail!("Manifest not found: {:?}. Run 'wharf moor' first to generate one.", manifest_path);
625625
}
626626

627-
// Determine target directory
628-
let target_dir = if target == "local" {
629-
config_dir.join("site")
627+
// Check if target is "local" or a yacht name
628+
if target == "local" {
629+
// Local verification
630+
let target_dir = config_dir.join("site");
631+
println!("Verifying local file integrity");
632+
println!("Using manifest: {:?}", manifest_path);
633+
634+
match ops::integrity::verify_against_manifest(&target_dir, &manifest_path, false) {
635+
Ok(result) => {
636+
println!();
637+
if result.is_ok() {
638+
println!("✓ Integrity verification PASSED");
639+
println!(" {} files verified", result.passed.len());
640+
} else {
641+
println!("✗ Integrity verification FAILED");
642+
if !result.mismatched.is_empty() {
643+
println!(" {} files mismatched", result.mismatched.len());
644+
}
645+
if !result.missing.is_empty() {
646+
println!(" {} files missing", result.missing.len());
647+
}
648+
if !result.unexpected.is_empty() {
649+
println!(" {} unexpected files", result.unexpected.len());
650+
}
651+
std::process::exit(1);
652+
}
653+
}
654+
Err(e) => {
655+
eprintln!("✗ Verification failed: {}", e);
656+
std::process::exit(1);
657+
}
658+
}
630659
} else {
631-
// For remote yachts, we'd need to pull the manifest first
632-
warn!("Remote verification not yet implemented, verifying local site directory");
633-
config_dir.join("site")
634-
};
660+
// Remote verification - load fleet and find yacht
661+
let fleet_path = config_dir.join("fleet.toml");
662+
let fleet = ops::fleet::load_fleet(&fleet_path)?;
635663

636-
println!("Verifying file integrity for: {}", target);
637-
println!("Using manifest: {:?}", manifest_path);
638-
639-
match ops::integrity::verify_against_manifest(&target_dir, &manifest_path, false) {
640-
Ok(result) => {
641-
println!();
642-
if result.is_ok() {
643-
println!("✓ Integrity verification PASSED");
644-
println!(" {} files verified", result.passed.len());
645-
} else {
646-
println!("✗ Integrity verification FAILED");
647-
if !result.mismatched.is_empty() {
648-
println!(" {} files mismatched", result.mismatched.len());
649-
}
650-
if !result.missing.is_empty() {
651-
println!(" {} files missing", result.missing.len());
652-
}
653-
if !result.unexpected.is_empty() {
654-
println!(" {} unexpected files", result.unexpected.len());
664+
let yacht = fleet.get_yacht(&target)
665+
.ok_or_else(|| anyhow::anyhow!("Yacht '{}' not found in fleet", target))?;
666+
667+
println!("Verifying remote yacht: {}", target);
668+
println!("Host: {}@{}:{}", yacht.ssh_user, yacht.ip, yacht.ssh_port);
669+
println!("Remote root: {}", yacht.web_root);
670+
println!("Using manifest: {:?}", manifest_path);
671+
println!();
672+
673+
match ops::integrity::verify_remote(
674+
&manifest_path,
675+
&yacht.ssh_user,
676+
&yacht.ip,
677+
yacht.ssh_port,
678+
&yacht.web_root,
679+
None, // TODO: Support identity file from config
680+
) {
681+
Ok(result) => {
682+
println!();
683+
if result.is_ok() {
684+
println!("✓ Remote integrity verification PASSED");
685+
println!(" Yacht: {}", result.yacht);
686+
println!(" {} files verified", result.files_checked);
687+
} else {
688+
println!("✗ Remote integrity verification FAILED");
689+
if let Some(err) = &result.error {
690+
println!(" Error: {}", err);
691+
}
692+
if !result.mismatched.is_empty() {
693+
println!(" {} files mismatched:", result.mismatched.len());
694+
for path in &result.mismatched {
695+
println!(" - {}", path);
696+
}
697+
}
698+
if !result.missing.is_empty() {
699+
println!(" {} files missing:", result.missing.len());
700+
for path in &result.missing {
701+
println!(" - {}", path);
702+
}
703+
}
704+
std::process::exit(1);
655705
}
706+
}
707+
Err(e) => {
708+
eprintln!("✗ Remote verification failed: {}", e);
656709
std::process::exit(1);
657710
}
658711
}
659-
Err(e) => {
660-
eprintln!("✗ Verification failed: {}", e);
661-
std::process::exit(1);
662-
}
663712
}
664713
}
665714
SecCommands::Scan { target, scan_type } => {

bin/wharf-cli/src/ops/integrity.rs

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,3 +83,60 @@ pub fn hash_file(path: &Path) -> Result<String> {
8383
integrity::hash_file(path)
8484
.context(format!("Failed to hash file {:?}", path))
8585
}
86+
87+
/// Verify a remote yacht's file integrity via SSH
88+
pub fn verify_remote(
89+
manifest_path: &Path,
90+
ssh_user: &str,
91+
ssh_host: &str,
92+
ssh_port: u16,
93+
remote_root: &str,
94+
identity_file: Option<&Path>,
95+
) -> Result<integrity::RemoteVerifyResult> {
96+
info!("Verifying remote yacht {} via SSH", ssh_host);
97+
info!("Remote root: {}", remote_root);
98+
99+
// Load the manifest
100+
let manifest = integrity::load_manifest(manifest_path)
101+
.context("Failed to load manifest")?;
102+
103+
info!("Loaded manifest with {} files", manifest.files.len());
104+
105+
// Run remote verification
106+
let result = integrity::verify_remote_ssh(
107+
&manifest,
108+
ssh_user,
109+
ssh_host,
110+
ssh_port,
111+
remote_root,
112+
identity_file,
113+
).context("Remote verification failed")?;
114+
115+
// Report results
116+
if result.passed {
117+
info!("Remote verification PASSED");
118+
info!(" {} files verified", result.files_checked);
119+
} else {
120+
info!("Remote verification FAILED");
121+
122+
if let Some(ref err) = result.error {
123+
info!(" Error: {}", err);
124+
}
125+
126+
if !result.mismatched.is_empty() {
127+
info!(" MISMATCHED: {} files", result.mismatched.len());
128+
for path in &result.mismatched {
129+
info!(" {}", path);
130+
}
131+
}
132+
133+
if !result.missing.is_empty() {
134+
info!(" MISSING: {} files", result.missing.len());
135+
for path in &result.missing {
136+
info!(" {}", path);
137+
}
138+
}
139+
}
140+
141+
Ok(result)
142+
}

bin/yacht-agent/Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,3 +40,6 @@ tracing-subscriber = { workspace = true }
4040

4141
# Errors
4242
anyhow = { workspace = true }
43+
44+
# eBPF Userspace Loader (optional - only used in ebpf mode)
45+
aya = { workspace = true }

0 commit comments

Comments
 (0)