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>,36 },37 /// Add secret, data should be provided in stdin38 Add {39 /// Secret name40 name: String,41 /// Secret owners42 machine: String,43 /// Override secret if already present44 #[clap(long)]45 force: bool,46 #[clap(long)]47 public: Option<String>,48 #[clap(long)]49 public_file: Option<PathBuf>,50 },51 /// Read secret from remote host, requires sudo on said host52 Read {53 name: String,54 machine: String,55 #[clap(long)]56 plaintext: bool,57 },58 UpdateShared {59 name: String,6061 #[clap(long)]62 machines: Option<Vec<String>>,6364 #[clap(long)]65 add_machines: Vec<String>,66 #[clap(long)]67 remove_machines: Vec<String>,6869 /// Which host should we use to decrypt70 #[clap(long)]71 prefer_identities: Vec<String>,72 },73 Regenerate {74 /// Which host should we use to decrypt, in case if reencryption is required, without75 /// regeneration76 #[clap(long)]77 prefer_identities: Vec<String>,78 },79 List {},80}8182impl Secrets {83 pub async fn run(self, config: &Config) -> Result<()> {84 match self {85 Secrets::ForceKeys => {86 for host in config.list_hosts().await? {87 if config.should_skip(&host.name) {88 continue;89 }90 config.key(&host.name).await?;91 }92 }93 Secrets::AddShared {94 machines,95 name,96 force,97 public,98 public_file,99 } => {100 let recipients = futures::stream::iter(machines.iter())101 .then(|m| config.recipient(m))102 .try_collect::<Vec<_>>()103 .await?;104105 let secret = {106 let mut input = vec![];107 io::stdin().read_to_end(&mut input)?;108109 if input.is_empty() {110 input111 } else {112 let mut encrypted = vec![];113 let recipients = recipients114 .iter()115 .cloned()116 .map(|r| Box::new(r) as Box<dyn age::Recipient + Send>)117 .collect();118 let mut encryptor = age::Encryptor::with_recipients(recipients)119 .expect("recipients provided")120 .wrap_output(&mut encrypted)?;121 io::copy(&mut Cursor::new(input), &mut encryptor)?;122 encryptor.finish()?;123 encrypted124 }125 };126127 if config.has_shared(&name) && !force {128 bail!("secret already defined");129 }130 config.replace_shared(131 name,132 FleetSharedSecret {133 owners: machines,134 secret: FleetSecret {135 created_at: Utc::now(),136 expires_at: None,137 secret,138 public: match (public, public_file) {139 (Some(v), None) => Some(v),140 (None, Some(v)) => Some(read_to_string(v).await?),141 (Some(_), Some(_)) => {142 bail!("only public or public_file should be set")143 }144 (None, None) => None,145 },146 },147 },148 );149 }150 Secrets::Add {151 machine,152 name,153 force,154 public,155 public_file,156 } => {157 let recipient = config.recipient(&machine).await?;158159 let secret = {160 let mut input = vec![];161 io::stdin().read_to_end(&mut input)?;162 if input.is_empty() {163 bail!("no data provided")164 }165166 let mut encrypted = vec![];167 let recipient = Box::new(recipient) as Box<dyn age::Recipient + Send>;168 let mut encryptor = age::Encryptor::with_recipients(vec![recipient])169 .expect("recipients provided")170 .wrap_output(&mut encrypted)?;171 io::copy(&mut Cursor::new(input), &mut encryptor)?;172 encryptor.finish()?;173 encrypted174 };175176 if config.has_secret(&machine, &name) && !force {177 bail!("secret already defined");178 }179 config.insert_secret(180 &machine,181 name,182 FleetSecret {183 created_at: Utc::now(),184 expires_at: None,185 secret,186 public: match (public, public_file) {187 (Some(v), None) => Some(v),188 (None, Some(v)) => Some(std::fs::read_to_string(v)?),189 (Some(_), Some(_)) => bail!("only public or public_file should be set"),190 (None, None) => None,191 },192 },193 );194 }195 // TODO: Instead of using sudo, decode secret on remote machine196 #[allow(clippy::await_holding_refcell_ref)]197 Secrets::Read {198 name,199 machine,200 plaintext,201 } => {202 let secret = config.host_secret(&machine, &name)?;203 if secret.secret.is_empty() {204 bail!("no secret {name}");205 }206 let data = config.decrypt_on_host(&machine, secret.secret).await?;207 if plaintext {208 let s = String::from_utf8(data).context("output is not utf8")?;209 print!("{s}");210 } else {211 println!("{}", z85::encode(&data));212 }213 }214 Secrets::UpdateShared {215 name,216 machines,217 mut add_machines,218 mut remove_machines,219 prefer_identities,220 } => {221 if machines.is_none() && add_machines.is_empty() && remove_machines.is_empty() {222 bail!("no operation");223 }224225 let mut secret = config.shared_secret(&name)?;226 if secret.secret.secret.is_empty() {227 bail!("no secret");228 }229230 let initial_machines = secret.owners.clone();231 let mut target_machines = secret.owners.clone();232 info!("Currently encrypted for {initial_machines:?}");233234 // ensure!(machines.is_some() || !add_machines.is_empty() || )235 if let Some(machines) = machines {236 ensure!(237 add_machines.is_empty() && remove_machines.is_empty(),238 "can't combine --machines and --add-machines/--remove-machines"239 );240 let target = initial_machines.iter().collect::<HashSet<_>>();241 let source = machines.iter().collect::<HashSet<_>>();242 for removed in target.difference(&source) {243 remove_machines.push((*removed).clone());244 }245 for added in source.difference(&target) {246 add_machines.push((*added).clone());247 }248 }249250 for machine in &remove_machines {251 let mut removed = false;252 while let Some(pos) = target_machines.iter().position(|m| m == machine) {253 target_machines.swap_remove(pos);254 removed = true;255 }256 if !removed {257 warn!("secret is not enabled for {machine}");258 }259 }260 for machine in &add_machines {261 if target_machines.iter().any(|m| m == machine) {262 warn!("secret is already added to {machine}");263 } else {264 target_machines.push(machine.to_owned());265 }266 }267 if !remove_machines.is_empty() {268 warn!("secret will not be regenerated for removed machines, and until host rebuild, they will still possess the ability to decode secret");269 }270271 if target_machines.is_empty() {272 info!("no machines left for secret, removing it");273 config.remove_shared(&name);274 return Ok(());275 }276277 if target_machines == initial_machines {278 warn!("secret owners are already correct");279 return Ok(());280 }281282 let identity_holder = if !prefer_identities.is_empty() {283 prefer_identities284 .iter()285 .find(|i| initial_machines.iter().any(|s| s == *i))286 } else {287 secret.owners.first()288 };289 let Some(identity_holder) = identity_holder else {290 bail!("no available holder found");291 };292 let target_recipients = futures::stream::iter(&target_machines)293 .then(|m| async { config.key(m).await })294 .collect::<Vec<_>>()295 .await;296 let target_recipients =297 target_recipients.into_iter().collect::<Result<Vec<_>>>()?;298299 let encrypted = config300 .reencrypt_on_host(identity_holder, secret.secret.secret, target_recipients)301 .await?;302303 secret.owners = target_machines;304 secret.secret.secret = encrypted;305 config.replace_shared(name, secret);306 }307 Secrets::Regenerate { prefer_identities } => {308 {309 let expected_shared_set = config310 .list_configured_shared()311 .await?312 .into_iter()313 .collect::<HashSet<_>>();314 let shared_set = config.list_shared().into_iter().collect::<HashSet<_>>();315 for removed in expected_shared_set.difference(&shared_set) {316 error!("secret needs to be generated: {removed}")317 }318 }319 let mut to_remove = Vec::new();320 for name in &config.list_shared() {321 info!("updating secret: {name}");322 let mut data = config.shared_secret(name)?;323 let expected_owners: Vec<String> = config324 .config_field325 .get_json_deep(["sharedSecrets", name, "expectedOwners"])326 .await?;327 if expected_owners.is_empty() {328 warn!("secret was removed from fleet config: {name}, removing from data");329 to_remove.push(name.to_string());330 continue;331 }332 let set = data.owners.iter().collect::<HashSet<_>>();333 let expected_set = expected_owners.iter().collect::<HashSet<_>>();334 let should_remove = set.difference(&expected_set).next().is_some();335 if set != expected_set {336 let owner_dependent: bool = config337 .config_field338 .get_json_deep(["sharedSecrets", name, "ownerDependent"])339 .await?;340 if !owner_dependent {341 warn!("reencrypting secret '{name}' for new owner set");342 // TODO: force regeneration343 if should_remove {344 warn!("secret will not be regenerated for removed machines, and until host rebuild, they will still possess the ability to decode secret");345 }346347 let identity_holder = if !prefer_identities.is_empty() {348 prefer_identities349 .iter()350 .find(|i| data.owners.iter().any(|s| s == *i))351 } else {352 data.owners.first()353 };354 let Some(identity_holder) = identity_holder else {355 bail!("no available holder found");356 };357358 let target_recipients = futures::stream::iter(&expected_owners)359 .then(|m| async { config.key(m).await })360 .collect::<Vec<_>>()361 .await;362 let target_recipients =363 target_recipients.into_iter().collect::<Result<Vec<_>>>()?;364365 let encrypted = config366 .reencrypt_on_host(367 identity_holder,368 data.secret.secret,369 target_recipients,370 )371 .await?;372373 data.secret.secret = encrypted;374 data.owners = expected_owners;375 config.replace_shared(name.to_owned(), data);376 } else {377 error!("secret '{name}' should be regenerated manually");378 }379 } else {380 info!("secret data is ok")381 }382 }383 for k in to_remove {384 config.remove_shared(&k);385 }386 }387 Secrets::List {} => {388 let _span = info_span!("loading secrets").entered();389 let configured = config.list_configured_shared().await?;390 #[derive(Tabled)]391 struct SecretDisplay {392 #[tabled(rename = "Name")]393 name: String,394 #[tabled(rename = "Owners")]395 owners: String,396 }397 let mut table = vec![];398 for name in configured.iter().cloned() {399 let config = config.clone();400 let expected_owners = config.shared_secret_expected_owners(&name).await?;401 let data = config.shared_secret(&name)?;402 let owners = data403 .owners404 .iter()405 .map(|o| {406 if expected_owners.contains(o) {407 o.green().to_string()408 } else {409 o.red().to_string()410 }411 })412 .collect::<Vec<_>>();413 table.push(SecretDisplay {414 owners: owners.join(", "),415 name,416 })417 }418 info!("loaded\n{}", Table::new(table).to_string())419 }420 }421 Ok(())422 }423}