Add interactive vault put - prompt for secrets instead of CLI args
All checks were successful
BotServer CI / build (push) Successful in 10m50s

This commit is contained in:
Rodrigo Rodriguez (Pragmatismo) 2026-03-16 22:27:06 -03:00
parent c0b619b58f
commit 35b793d29c
4 changed files with 88 additions and 27 deletions

1
.gitignore vendored
View file

@ -14,3 +14,4 @@ docs/book
*.rdb *.rdb
botserver-installers botserver-installers
.git-rewrite .git-rewrite
vault-unseal-keys

View file

@ -252,8 +252,9 @@ pub async fn run() -> Result<()> {
vault_migrate(env_file).await?; vault_migrate(env_file).await?;
} }
"put" => { "put" => {
if args.len() < 5 { if args.len() < 4 {
eprintln!("Usage: botserver vault put <path> <key=value> [key=value...]"); eprintln!("Usage: botserver vault put <path> [key=value] [key=value...]");
eprintln!(" botserver vault put <path> (interactive mode - prompts for keys)");
return Ok(()); return Ok(());
} }
let path = &args[3]; let path = &args[3];
@ -610,6 +611,44 @@ async fn vault_put(path: &str, kvs: &[&str]) -> Result<()> {
} }
let mut data: HashMap<String, String> = HashMap::new(); let mut data: HashMap<String, String> = HashMap::new();
// If no key=value provided, enter interactive mode
if kvs.is_empty() {
println!("\n=== Interactive Vault Store ===");
println!("Path: {}", path);
println!("Enter values (press Enter with empty key to finish):\n");
loop {
print!("Key: ");
std::io::Write::flush(&mut std::io::stdout())?;
let mut key = String::new();
std::io::stdin().read_line(&mut key)?;
let key = key.trim().to_string();
if key.is_empty() {
break;
}
print!("Value: ");
std::io::Write::flush(&mut std::io::stdout())?;
let mut value = String::new();
std::io::stdin().read_line(&mut value)?;
let value = value.trim().to_string();
if value.is_empty() {
eprintln!("Value cannot be empty, skipping '{}'", key);
continue;
}
data.insert(key.clone(), value);
println!("* Saved '{}'\n", key);
}
if data.is_empty() {
return Err(anyhow::anyhow!("No values provided"));
}
} else {
// Command line mode
for kv in kvs { for kv in kvs {
if let Some((k, v)) = kv.split_once('=') { if let Some((k, v)) = kv.split_once('=') {
data.insert(k.to_string(), v.to_string()); data.insert(k.to_string(), v.to_string());
@ -621,9 +660,10 @@ async fn vault_put(path: &str, kvs: &[&str]) -> Result<()> {
if data.is_empty() { if data.is_empty() {
return Err(anyhow::anyhow!("No valid key=value pairs provided")); return Err(anyhow::anyhow!("No valid key=value pairs provided"));
} }
}
manager.put_secret(path, data).await?; manager.put_secret(path, data.clone()).await?;
println!("* Stored {} key(s) at {}", kvs.len(), path); println!("\n✓ Stored {} key(s) at {}", data.len(), path);
Ok(()) Ok(())
} }

View file

@ -409,27 +409,52 @@ impl PackageManager {
} }
fn assign_static_ip(container_name: &str) -> Result<String> { fn assign_static_ip(container_name: &str) -> Result<String> {
// Pick a deterministic IP from the last 2 bytes of the MAC address to avoid collisions. // Discover the host bridge gateway and subnet dynamically from the container's default route.
// The container already has IPv6 + a link on eth0, so we can read the bridge from the host.
let bridge_info = std::process::Command::new("ip")
.args(["-4", "addr", "show", "lxdbr0"])
.output()
.ok()
.and_then(|o| {
let out = String::from_utf8_lossy(&o.stdout).to_string();
// Extract "10.x.x.x/prefix" from "inet 10.x.x.x/24 ..."
out.lines()
.find(|l| l.contains("inet "))
.and_then(|l| l.split_whitespace().nth(1))
.map(|s| s.to_string())
});
let (gateway, prefix) = match bridge_info.as_deref().and_then(|cidr| {
let (ip, pfx_str) = cidr.split_once('/')?;
let parts: Vec<&str> = ip.split('.').collect();
let base = format!("{}.{}.{}.", parts[0], parts[1], parts[2]);
let pfx = pfx_str.parse::<u8>().ok()?;
Some((ip.to_string(), format!("{base}{{octet}}/{pfx}")))
}) {
Some(pair) => pair,
None => return Err(anyhow::anyhow!("Cannot determine lxdbr0 subnet")),
};
// Derive last octet from MAC to avoid collisions (range 100-249).
let mac_out = safe_lxc(&["exec", container_name, "--", "cat", "/sys/class/net/eth0/address"]); let mac_out = safe_lxc(&["exec", container_name, "--", "cat", "/sys/class/net/eth0/address"]);
let last_octet = mac_out let octet = mac_out
.and_then(|o| o.status.success().then(|| String::from_utf8_lossy(&o.stdout).trim().to_string())) .and_then(|o| o.status.success().then(|| String::from_utf8_lossy(&o.stdout).trim().to_string()))
.and_then(|mac| { .and_then(|mac| {
let parts: Vec<&str> = mac.split(':').collect(); let parts: Vec<&str> = mac.split(':').collect();
let a = u8::from_str_radix(parts.get(4)?, 16).ok()?; let a = u8::from_str_radix(parts.get(4)?, 16).ok()?;
let b = u8::from_str_radix(parts.get(5)?, 16).ok()?; let b = u8::from_str_radix(parts.get(5)?, 16).ok()?;
// Map into 100-250 range to avoid gateway (.1) and broadcast (.255) Some(100u16 + (u16::from(a) * 256 + u16::from(b)) % 150)
Some(100u16 + ((u16::from(a) * 256 + u16::from(b)) % 150) as u16)
}) })
.unwrap_or(100); .unwrap_or(100);
let ip = format!("10.43.228.{last_octet}"); let cidr = prefix.replace("{octet}", &octet.to_string());
let cidr = format!("{ip}/24"); let ip = cidr.split('/').next().unwrap_or("").to_string();
safe_lxc(&["exec", container_name, "--", "ip", "addr", "add", &cidr, "dev", "eth0"]); safe_lxc(&["exec", container_name, "--", "ip", "addr", "add", &cidr, "dev", "eth0"]);
safe_lxc(&["exec", container_name, "--", "ip", "route", "add", "default", "via", "10.43.228.1"]); safe_lxc(&["exec", container_name, "--", "ip", "route", "add", "default", "via", &gateway]);
safe_lxc(&["exec", container_name, "--", "bash", "-c", safe_lxc(&["exec", container_name, "--", "bash", "-c",
&format!("printf 'nameserver 8.8.8.8\\nnameserver 8.8.4.4\\n' > /etc/resolv.conf && \ &format!("printf 'nameserver 8.8.8.8\\nnameserver 8.8.4.4\\n' > /etc/resolv.conf && \
printf '#!/bin/sh\\nip addr add {cidr} dev eth0 2>/dev/null||true\\nip route add default via 10.43.228.1 2>/dev/null||true\\nexit 0\\n' > /etc/rc.local && \ printf '#!/bin/sh\\nip addr add {cidr} dev eth0 2>/dev/null||true\\nip route add default via {gateway} 2>/dev/null||true\\nexit 0\\n' > /etc/rc.local && \
chmod +x /etc/rc.local && systemctl enable rc-local 2>/dev/null||true")]); chmod +x /etc/rc.local && systemctl enable rc-local 2>/dev/null||true")]);
info!("Assigned static IP {} to container '{}'", ip, container_name); info!("Assigned static IP {} to container '{}'", ip, container_name);

View file

@ -1,5 +0,0 @@
Unseal Key 1: KNMpbHTx0zYKG+P8oKoFNe7KKK7gEFCJ/IeoyXmHJpcb
Unseal Key 2: XoTPcUpbE4j5u6AeIjVhzWd1ku4U5NCANlPLf8pJMA3y
Unseal Key 3: sGEbMnQxDmS5itqsdNacia+glPKBIzLk2/9jntEkAIwo
Unseal Key 4: 9jPawRkzt0nmuR4V+KeecMns2in3pj+fQFqLfsfyimN1
Unseal Key 5: JO0Bi3UXibXdBMTcCPNmmghXhLNcV14035KkZhc3kU1j