difftreelog
feat add-shared --readd
in: trunk
1 file changed
cmds/fleet/src/cmds/secrets/mod.rsdiffbeforeafterboth1use crate::{2 fleetdata::{FleetSecret, FleetSharedSecret},3 host::Config,4};5use anyhow::{bail, ensure, Context, Result};6use chrono::Utc;7use clap::Parser;8use futures::{StreamExt, TryStreamExt};9use owo_colors::OwoColorize;10use std::{11 collections::HashSet,12 io::{self, Cursor, Read},13 path::PathBuf,14};15use tabled::{Table, Tabled};16use tokio::fs::read_to_string;17use tracing::{error, info, info_span, warn};1819#[derive(Parser)]20pub enum Secrets {21 /// Force load keys for all defined hosts22 ForceKeys,23 /// Add secret, data should be provided in stdin24 AddShared {25 /// Secret name26 name: String,27 /// Secret owners28 machines: Vec<String>,29 /// Override secret if already present30 #[clap(long)]31 force: bool,32 #[clap(long)]33 public: Option<String>,34 #[clap(long)]35 public_file: Option<PathBuf>,3637 /// Secret with this name already exists, override its value while keeping the same owners.38 #[clap(long)]39 readd: bool,40 },41 /// Add secret, data should be provided in stdin42 Add {43 /// Secret name44 name: String,45 /// Secret owners46 machine: String,47 /// Override secret if already present48 #[clap(long)]49 force: bool,50 #[clap(long)]51 public: Option<String>,52 #[clap(long)]53 public_file: Option<PathBuf>,54 },55 /// Read secret from remote host, requires sudo on said host56 Read {57 name: String,58 machine: String,59 #[clap(long)]60 plaintext: bool,61 },62 UpdateShared {63 name: String,6465 #[clap(long)]66 machines: Option<Vec<String>>,6768 #[clap(long)]69 add_machines: Vec<String>,70 #[clap(long)]71 remove_machines: Vec<String>,7273 /// Which host should we use to decrypt74 #[clap(long)]75 prefer_identities: Vec<String>,76 },77 Regenerate {78 /// Which host should we use to decrypt, in case if reencryption is required, without79 /// regeneration80 #[clap(long)]81 prefer_identities: Vec<String>,82 },83 List {},84}8586impl Secrets {87 pub async fn run(self, config: &Config) -> Result<()> {88 match self {89 Secrets::ForceKeys => {90 for host in config.list_hosts().await? {91 if config.should_skip(&host.name) {92 continue;93 }94 config.key(&host.name).await?;95 }96 }97 Secrets::AddShared {98 mut machines,99 name,100 force,101 public,102 public_file,103 readd,104 } => {105 let exists = config.has_shared(&name);106 if exists && !force && !readd {107 bail!("secret already defined");108 }109 if readd {110 // Fixme: use clap to limit this usage111 ensure!(!force, "--force and --readd are not compatible");112 ensure!(exists, "secret doesn't exists");113 ensure!(114 machines.is_empty(),115 "you can't use machines argument for --readd"116 );117 let shared = config.shared_secret(&name)?;118 machines = shared.owners;119 }120121 let recipients = futures::stream::iter(machines.iter())122 .then(|m| config.recipient(m))123 .try_collect::<Vec<_>>()124 .await?;125126 let secret = {127 let mut input = vec![];128 io::stdin().read_to_end(&mut input)?;129130 if input.is_empty() {131 input132 } else {133 let mut encrypted = vec![];134 let recipients = recipients135 .iter()136 .cloned()137 .map(|r| Box::new(r) as Box<dyn age::Recipient + Send>)138 .collect();139 let mut encryptor = age::Encryptor::with_recipients(recipients)140 .expect("recipients provided")141 .wrap_output(&mut encrypted)?;142 io::copy(&mut Cursor::new(input), &mut encryptor)?;143 encryptor.finish()?;144 encrypted145 }146 };147 config.replace_shared(148 name,149 FleetSharedSecret {150 owners: machines,151 secret: FleetSecret {152 created_at: Utc::now(),153 expires_at: None,154 secret,155 public: match (public, public_file) {156 (Some(v), None) => Some(v),157 (None, Some(v)) => Some(read_to_string(v).await?),158 (Some(_), Some(_)) => {159 bail!("only public or public_file should be set")160 }161 (None, None) => None,162 },163 },164 },165 );166 }167 Secrets::Add {168 machine,169 name,170 force,171 public,172 public_file,173 } => {174 let recipient = config.recipient(&machine).await?;175176 let secret = {177 let mut input = vec![];178 io::stdin().read_to_end(&mut input)?;179 if input.is_empty() {180 bail!("no data provided")181 }182183 let mut encrypted = vec![];184 let recipient = Box::new(recipient) as Box<dyn age::Recipient + Send>;185 let mut encryptor = age::Encryptor::with_recipients(vec![recipient])186 .expect("recipients provided")187 .wrap_output(&mut encrypted)?;188 io::copy(&mut Cursor::new(input), &mut encryptor)?;189 encryptor.finish()?;190 encrypted191 };192193 if config.has_secret(&machine, &name) && !force {194 bail!("secret already defined");195 }196 config.insert_secret(197 &machine,198 name,199 FleetSecret {200 created_at: Utc::now(),201 expires_at: None,202 secret,203 public: match (public, public_file) {204 (Some(v), None) => Some(v),205 (None, Some(v)) => Some(std::fs::read_to_string(v)?),206 (Some(_), Some(_)) => bail!("only public or public_file should be set"),207 (None, None) => None,208 },209 },210 );211 }212 // TODO: Instead of using sudo, decode secret on remote machine213 #[allow(clippy::await_holding_refcell_ref)]214 Secrets::Read {215 name,216 machine,217 plaintext,218 } => {219 let secret = config.host_secret(&machine, &name)?;220 if secret.secret.is_empty() {221 bail!("no secret {name}");222 }223 let data = config.decrypt_on_host(&machine, secret.secret).await?;224 if plaintext {225 let s = String::from_utf8(data).context("output is not utf8")?;226 print!("{s}");227 } else {228 println!("{}", z85::encode(&data));229 }230 }231 Secrets::UpdateShared {232 name,233 machines,234 mut add_machines,235 mut remove_machines,236 prefer_identities,237 } => {238 if machines.is_none() && add_machines.is_empty() && remove_machines.is_empty() {239 bail!("no operation");240 }241242 let mut secret = config.shared_secret(&name)?;243 if secret.secret.secret.is_empty() {244 bail!("no secret");245 }246247 let initial_machines = secret.owners.clone();248 let mut target_machines = secret.owners.clone();249 info!("Currently encrypted for {initial_machines:?}");250251 // ensure!(machines.is_some() || !add_machines.is_empty() || )252 if let Some(machines) = machines {253 ensure!(254 add_machines.is_empty() && remove_machines.is_empty(),255 "can't combine --machines and --add-machines/--remove-machines"256 );257 let target = initial_machines.iter().collect::<HashSet<_>>();258 let source = machines.iter().collect::<HashSet<_>>();259 for removed in target.difference(&source) {260 remove_machines.push((*removed).clone());261 }262 for added in source.difference(&target) {263 add_machines.push((*added).clone());264 }265 }266267 for machine in &remove_machines {268 let mut removed = false;269 while let Some(pos) = target_machines.iter().position(|m| m == machine) {270 target_machines.swap_remove(pos);271 removed = true;272 }273 if !removed {274 warn!("secret is not enabled for {machine}");275 }276 }277 for machine in &add_machines {278 if target_machines.iter().any(|m| m == machine) {279 warn!("secret is already added to {machine}");280 } else {281 target_machines.push(machine.to_owned());282 }283 }284 if !remove_machines.is_empty() {285 warn!("secret will not be regenerated for removed machines, and until host rebuild, they will still possess the ability to decode secret");286 }287288 if target_machines.is_empty() {289 info!("no machines left for secret, removing it");290 config.remove_shared(&name);291 return Ok(());292 }293294 if target_machines == initial_machines {295 warn!("secret owners are already correct");296 return Ok(());297 }298299 let identity_holder = if !prefer_identities.is_empty() {300 prefer_identities301 .iter()302 .find(|i| initial_machines.iter().any(|s| s == *i))303 } else {304 secret.owners.first()305 };306 let Some(identity_holder) = identity_holder else {307 bail!("no available holder found");308 };309 let target_recipients = futures::stream::iter(&target_machines)310 .then(|m| async { config.key(m).await })311 .collect::<Vec<_>>()312 .await;313 let target_recipients =314 target_recipients.into_iter().collect::<Result<Vec<_>>>()?;315316 let encrypted = config317 .reencrypt_on_host(identity_holder, secret.secret.secret, target_recipients)318 .await?;319320 secret.owners = target_machines;321 secret.secret.secret = encrypted;322 config.replace_shared(name, secret);323 }324 Secrets::Regenerate { prefer_identities } => {325 {326 let expected_shared_set = config327 .list_configured_shared()328 .await?329 .into_iter()330 .collect::<HashSet<_>>();331 let shared_set = config.list_shared().into_iter().collect::<HashSet<_>>();332 for removed in expected_shared_set.difference(&shared_set) {333 error!("secret needs to be generated: {removed}")334 }335 }336 let mut to_remove = Vec::new();337 for name in &config.list_shared() {338 info!("updating secret: {name}");339 let mut data = config.shared_secret(name)?;340 let expected_owners: Vec<String> = config341 .config_field342 .get_json_deep(["sharedSecrets", name, "expectedOwners"])343 .await?;344 if expected_owners.is_empty() {345 warn!("secret was removed from fleet config: {name}, removing from data");346 to_remove.push(name.to_string());347 continue;348 }349 let set = data.owners.iter().collect::<HashSet<_>>();350 let expected_set = expected_owners.iter().collect::<HashSet<_>>();351 let should_remove = set.difference(&expected_set).next().is_some();352 if set != expected_set {353 let owner_dependent: bool = config354 .config_field355 .get_json_deep(["sharedSecrets", name, "ownerDependent"])356 .await?;357 if !owner_dependent {358 warn!("reencrypting secret '{name}' for new owner set");359 // TODO: force regeneration360 if should_remove {361 warn!("secret will not be regenerated for removed machines, and until host rebuild, they will still possess the ability to decode secret");362 }363364 let identity_holder = if !prefer_identities.is_empty() {365 prefer_identities366 .iter()367 .find(|i| data.owners.iter().any(|s| s == *i))368 } else {369 data.owners.first()370 };371 let Some(identity_holder) = identity_holder else {372 bail!("no available holder found");373 };374375 let target_recipients = futures::stream::iter(&expected_owners)376 .then(|m| async { config.key(m).await })377 .collect::<Vec<_>>()378 .await;379 let target_recipients =380 target_recipients.into_iter().collect::<Result<Vec<_>>>()?;381382 let encrypted = config383 .reencrypt_on_host(384 identity_holder,385 data.secret.secret,386 target_recipients,387 )388 .await?;389390 data.secret.secret = encrypted;391 data.owners = expected_owners;392 config.replace_shared(name.to_owned(), data);393 } else {394 error!("secret '{name}' should be regenerated manually");395 }396 } else {397 info!("secret data is ok")398 }399 }400 for k in to_remove {401 config.remove_shared(&k);402 }403 }404 Secrets::List {} => {405 let _span = info_span!("loading secrets").entered();406 let configured = config.list_configured_shared().await?;407 #[derive(Tabled)]408 struct SecretDisplay {409 #[tabled(rename = "Name")]410 name: String,411 #[tabled(rename = "Owners")]412 owners: String,413 }414 let mut table = vec![];415 for name in configured.iter().cloned() {416 let config = config.clone();417 let expected_owners = config.shared_secret_expected_owners(&name).await?;418 let data = config.shared_secret(&name)?;419 let owners = data420 .owners421 .iter()422 .map(|o| {423 if expected_owners.contains(o) {424 o.green().to_string()425 } else {426 o.red().to_string()427 }428 })429 .collect::<Vec<_>>();430 table.push(SecretDisplay {431 owners: owners.join(", "),432 name,433 })434 }435 info!("loaded\n{}", Table::new(table).to_string())436 }437 }438 Ok(())439 }440}