difftreelog
feat reencrypt secret on remote server
in: trunk
8 files changed
cmds/fleet/src/cmds/info.rsdiffbeforeafterboth1use std::{collections::BTreeSet, time::Duration};1use std::collections::BTreeSet;223use crate::{command::CommandExt, host::Config};3use crate::host::Config;4use anyhow::{bail, ensure, Result};4use anyhow::{ensure, Result};5use clap::Parser;5use clap::Parser;6use nixlike::format_nix;7use serde_json::{json, Value};8use tokio::{9 fs::{self, File},10 io::AsyncWriteExt,11 process::Command,12};13614#[derive(Parser)]7#[derive(Parser)]15pub struct Info {8pub struct Info {cmds/fleet/src/cmds/secrets/mod.rsdiffbeforeafterboth12 iter,12 iter,13 path::PathBuf,13 path::PathBuf,14};14};15use tokio::fs::read_to_string;15use tracing::{info, warn};16use tracing::{info, warn};161717#[derive(Parser)]18#[derive(Parser)]50 Read {51 Read {51 name: String,52 name: String,52 machine: String,53 machine: String,54 #[clap(long)]55 plaintext: bool,53 },56 },54 UpdateShared {57 UpdateShared {55 name: String,58 name: String,565960 #[clap(long)]57 machines: Option<Vec<String>>,61 machines: Option<Vec<String>>,586263 #[clap(long)]59 add_machines: Vec<String>,64 add_machines: Vec<String>,65 #[clap(long)]60 remove_machines: Vec<String>,66 remove_machines: Vec<String>,616762 /// Which host should we use to decrypt68 /// Which host should we use to decrypt69 #[clap(long)]63 prefer_identities: Vec<String>,70 prefer_identities: Vec<String>,64 },71 },65 Regenerate,72 Regenerate {73 /// Which host should we use to decrypt, in case if reencryption is required, without74 /// regeneration75 #[clap(long)]76 prefer_identities: Vec<String>,77 },66}78}677968impl Secrets {80impl Secrets {110 }122 }111 };123 };112124113 let mut data = config.data_mut();125 if config.has_shared(&name) && !force {114 if data.shared_secrets.contains_key(&name) && !force {115 bail!("secret already defined");126 bail!("secret already defined");116 }127 }117 data.shared_secrets.insert(128 config.replace_shared(118 name,129 name,119 FleetSharedSecret {130 FleetSharedSecret {120 owners: machines,131 owners: machines,123 secret,134 secret,124 public: match (public, public_file) {135 public: match (public, public_file) {125 (Some(v), None) => Some(v),136 (Some(v), None) => Some(v),126 (None, Some(v)) => Some(std::fs::read_to_string(v)?),137 (None, Some(v)) => Some(read_to_string(v).await?),127 (Some(_), Some(_)) => {138 (Some(_), Some(_)) => {128 bail!("only public or public_file should be set")139 bail!("only public or public_file should be set")129 }140 }159 encrypted170 encrypted160 };171 };161172162 let mut data = config.data_mut();173 if config.has_secret(&machine, &name) && !force {163 let host_secrets = data.host_secrets.entry(machine).or_default();164 if host_secrets.contains_key(&name) && !force {165 bail!("secret already defined");174 bail!("secret already defined");166 }175 }167 host_secrets.insert(176 config.insert_secret(177 &machine,168 name,178 name,169 FleetSecret {179 FleetSecret {170 expire_at: None,180 expire_at: None,180 }190 }181 // TODO: Instead of using sudo, decode secret on remote machine191 // TODO: Instead of using sudo, decode secret on remote machine182 #[allow(clippy::await_holding_refcell_ref)]192 #[allow(clippy::await_holding_refcell_ref)]183 Secrets::Read { name, machine } => {193 Secrets::Read {194 name,184 let data = config.data();195 machine,185186 let Some(host_secrets) = data.host_secrets.get(&machine) else {196 plaintext,187 bail!("no secrets for machine {machine}");197 } => {188 };189 let Some(secret) = host_secrets.get(&name) else {198 let secret = config.host_secret(&machine, &name)?;190 bail!("machine {machine} has no secret {name}");191 };192 if secret.secret.is_empty() {199 if secret.secret.is_empty() {193 bail!("no secret {name}");200 bail!("no secret {name}");194 }201 }195 let identity = config.identity(&machine).await?;202 let data = config.decrypt_on_host(&machine, secret.secret).await?;196 let decryptor = Decryptor::new(Cursor::new(&secret.secret))?;197 let decryptor = match decryptor {203 if plaintext {198 Decryptor::Recipients(r) => r,204 let s = String::from_utf8(data).context("output is not utf8")?;199 Decryptor::Passphrase(_) => bail!("should be recipients"),200 };201 let mut decryptor = decryptor202 .decrypt(iter::once(&identity as &dyn age::Identity))203 .context("failed to decrypt, wrong key?")?;204205 let mut decrypted = Vec::new();205 print!("{s}");206 decryptor206 } else {207 .read_to_end(&mut decrypted)208 .context("failed to decrypt")?;207 println!("{}", z85::encode(&data));209 // secret.secret208 }210 std::io::stdout().lock().write_all(&decrypted)?;211 }209 }212 Secrets::UpdateShared {210 Secrets::UpdateShared {213 name,211 name,216 mut remove_machines,214 mut remove_machines,217 prefer_identities,215 prefer_identities,218 } => {216 } => {219 let mut data = config.data_mut();220 if machines.is_none() && add_machines.is_empty() && remove_machines.is_empty() {217 if machines.is_none() && add_machines.is_empty() && remove_machines.is_empty() {221 bail!("no operation");218 bail!("no operation");222 }219 }223220224 let Some(mut secret) = data.shared_secrets.get_mut(&name) else {221 let mut secret = config.shared_secret(&name)?;225 bail!("no shared secret {name}");226 };227 if secret.secret.secret.is_empty() {222 if secret.secret.secret.is_empty() {228 bail!("no secret");223 bail!("no secret");229 }224 }230225231 let initial_machines = secret.owners.clone();226 let initial_machines = secret.owners.clone();232 let mut target_machines = secret.owners.clone();227 let mut target_machines = secret.owners.clone();228 info!("Currently encrypted for {initial_machines:?}");233229234 // ensure!(machines.is_some() || !add_machines.is_empty() || )230 // ensure!(machines.is_some() || !add_machines.is_empty() || )235 if let Some(machines) = machines {231 if let Some(machines) = machines {254 removed = true;250 removed = true;255 }251 }256 if !removed {252 if !removed {257 bail!("secret is not enabled for {machine}");253 warn!("secret is not enabled for {machine}");258 }254 }259 }255 }260 for machine in &add_machines {256 for machine in &add_machines {261 if target_machines.iter().any(|m| m == machine) {257 if target_machines.iter().any(|m| m == machine) {262 warn!("secret is already added to {machine}");258 warn!("secret is already added to {machine}");259 } else {260 target_machines.push(machine.to_owned());263 }261 }264 }262 }265 if remove_machines.is_empty() {263 if !remove_machines.is_empty() {266 warn!("secret will not be regenerated for removed machines, and until host rebuild, they will still possess the ability to decode secret");264 warn!("secret will not be regenerated for removed machines, and until host rebuild, they will still possess the ability to decode secret");267 }265 }266268 if target_machines.is_empty() {267 if target_machines.is_empty() {269 info!("no machines left for secret, removing it");268 info!("no machines left for secret, removing it");270 data.shared_secrets.remove(&name);269 config.remove_shared(&name);271 return Ok(());270 return Ok(());272 }271 }273272273 if target_machines == initial_machines {274 warn!("secret owners are already correct");275 return Ok(());276 }277274 let identity_holder = if !prefer_identities.is_empty() {278 let identity_holder = if !prefer_identities.is_empty() {275 prefer_identities279 prefer_identities276 .iter()280 .iter()282 bail!("no available holder found");286 bail!("no available holder found");283 };287 };284 let target_recipients = futures::stream::iter(&target_machines)288 let target_recipients = futures::stream::iter(&target_machines)285 .flat_map(|m| futures::stream::once(config.recipient(m)))289 .then(|m| async { config.key(m).await })286 .collect::<Vec<_>>()290 .collect::<Vec<_>>()287 .await291 .await;288 .into_iter()292 let target_recipients =289 .map(|v| v.map(|v| Box::new(v) as Box<dyn age::Recipient + Send>))293 target_recipients.into_iter().collect::<Result<Vec<_>>>()?;290 .collect::<Result<Vec<_>>>()?;291294292 let identity = config.identity(identity_holder).await?;295 let encrypted = config293 let decryptor = Decryptor::new(Cursor::new(&secret.secret.secret))?;296 .reencrypt_on_host(&identity_holder, secret.secret.secret, target_recipients)294 let decryptor = match decryptor {295 Decryptor::Recipients(r) => r,296 Decryptor::Passphrase(_) => bail!("should be recipients"),297 };297 .await?;298 let mut decryptor = decryptor299 .decrypt(iter::once(&identity as &dyn age::Identity))300 .context("failed to decrypt, wrong key?")?;301298302 let mut decrypted = Vec::new();299 secret.owners = target_machines;303 decryptor304 .read_to_end(&mut decrypted)305 .context("failed to decrypt")?;306307 let mut encrypted = vec![];308 let mut encryptor = Encryptor::with_recipients(target_recipients)309 .expect("recipients provided")310 .wrap_output(&mut encrypted)?;311 io::copy(&mut Cursor::new(decrypted), &mut encryptor)?;312 encryptor.finish()?;313314 secret.secret.secret = encrypted;300 secret.secret.secret = encrypted;301 config.replace_shared(name, secret);315 }302 }316 Secrets::Regenerate => {303 Secrets::Regenerate { prefer_identities } => {317 // config.data_mut().shared_secrets318 {304 {319 let expected_shared_set =305 let expected_shared_set =320 config.shared_config_attr_names("sharedSecrets").await?;306 config.shared_config_attr_names("sharedSecrets").await?;321 let expected_shared_set = expected_shared_set.iter().collect::<HashSet<_>>();307 let expected_shared_set = expected_shared_set.iter().collect::<HashSet<_>>();322 let shared_set = config.data();308 let shared_set = config.list_shared();323 let shared_set = shared_set.shared_secrets.keys().collect::<HashSet<_>>();309 let shared_set = shared_set.iter().collect::<HashSet<_>>();324 for removed in expected_shared_set.difference(&shared_set) {310 for removed in expected_shared_set.difference(&shared_set) {325 warn!("secret needs to be generated: {removed}")311 warn!("secret needs to be generated: {removed}")326 }312 }327 }313 }328 let mut to_remove = Vec::new();314 let mut to_remove = Vec::new();329 for (name, data) in &config.data().shared_secrets {315 for name in &config.list_shared() {316 let mut data = config.shared_secret(name)?;330 let expected_owners: Vec<String> = config317 let expected_owners: Vec<String> = config331 .shared_config_attr(&format!("sharedSecrets.\"{name}\".expectedOwners"))318 .shared_config_attr(&format!("sharedSecrets.\"{name}\".expectedOwners"))332 .await?;319 .await?;337 }324 }338 let set = data.owners.iter().collect::<HashSet<_>>();325 let set = data.owners.iter().collect::<HashSet<_>>();339 let expected_set = expected_owners.iter().collect::<HashSet<_>>();326 let expected_set = expected_owners.iter().collect::<HashSet<_>>();327 let should_remove = set.difference(&expected_set).next().is_some();340 if set != expected_set {328 if set != expected_set {341 warn!("reconfiguring owners for {name}");329 warn!("reconfiguring owners for {name}");330 let generator: Option<String> = config331 .shared_config_attr(&format!("sharedSecrets.\"{name}\".generator"))332 .await?;333 // TODO: if !.owner_dependent334 if let Some(str) = generator {335 todo!("regenerate")336 } else {337 if should_remove {338 warn!("secret will not be regenerated for removed machines, and until host rebuild, they will still possess the ability to decode secret");339 }340341 let identity_holder = if !prefer_identities.is_empty() {342 prefer_identities343 .iter()344 .find(|i| data.owners.iter().any(|s| s == *i))345 } else {346 data.owners.first()347 };348 let Some(identity_holder) = identity_holder else {349 bail!("no available holder found");350 };351352 let target_recipients = futures::stream::iter(&expected_owners)353 .then(|m| async { config.key(m).await })354 .collect::<Vec<_>>()355 .await;356 let target_recipients =357 target_recipients.into_iter().collect::<Result<Vec<_>>>()?;358359 let encrypted = config360 .reencrypt_on_host(361 &identity_holder,362 data.secret.secret,363 target_recipients,364 )365 .await?;366367 data.secret.secret = encrypted;368 data.owners = expected_owners;369 config.replace_shared(name.to_owned(), data);370 }342 }371 }343 }372 }344 for k in to_remove {373 for k in to_remove {345 config.data_mut().shared_secrets.remove(&k);374 config.remove_shared(&k);346 }375 }347 }376 }348 }377 }cmds/fleet/src/fleetdata.rsdiffbeforeafterboth1use anyhow::{bail, Result};1use anyhow::Result;2use chrono::{DateTime, Utc};2use chrono::{DateTime, Utc};3use nixlike::format_nix;3use nixlike::format_nix;4use serde::{Deserialize, Deserializer, Serialize, Serializer};4use serde::{Deserialize, Deserializer, Serialize, Serializer};5use serde_json::{json, Value};6use std::collections::BTreeMap;5use std::collections::BTreeMap;7use tempfile::TempDir;6use tempfile::TempDir;8use tokio::{7use tokio::{11 process::Command,10 process::Command,12};11};1314use crate::command::CommandExt;151216#[derive(Serialize, Deserialize, Default)]13#[derive(Serialize, Deserialize, Default)]17#[serde(rename_all = "camelCase")]14#[serde(rename_all = "camelCase")]34 pub host_secrets: BTreeMap<String, BTreeMap<String, FleetSecret>>,31 pub host_secrets: BTreeMap<String, BTreeMap<String, FleetSecret>>,35}32}363337#[derive(Serialize, Deserialize)]34#[derive(Serialize, Deserialize, Clone)]38#[serde(rename_all = "camelCase")]35#[serde(rename_all = "camelCase")]36#[must_use]39pub struct FleetSharedSecret {37pub struct FleetSharedSecret {40 pub owners: Vec<String>,38 pub owners: Vec<String>,41 #[serde(flatten)]39 #[serde(flatten)]42 pub secret: FleetSecret,40 pub secret: FleetSecret,43}41}444245#[derive(Serialize, Deserialize)]43#[derive(Serialize, Deserialize, Clone)]46#[serde(rename_all = "camelCase")]44#[serde(rename_all = "camelCase")]45#[must_use]47pub struct FleetSecret {46pub struct FleetSecret {48 #[serde(default)]47 #[serde(default)]49 #[serde(skip_serializing_if = "Option::is_none")]48 #[serde(skip_serializing_if = "Option::is_none")]75 .and_then(|string| z85::decode(string).map_err(|err| Error::custom(err.to_string())))74 .and_then(|string| z85::decode(string).map_err(|err| Error::custom(err.to_string())))76}75}777677/// Isn't used yet78#[allow(dead_code)]78pub async fn dummy_flake() -> Result<TempDir> {79pub async fn dummy_flake() -> Result<TempDir> {79 let data_str = fs::read_to_string("fleet.nix").await?;80 let data_str = fs::read_to_string("fleet.nix").await?;8081cmds/fleet/src/host.rsdiffbeforeafterboth8 sync::Arc,8 sync::Arc,9};9};101011use anyhow::Result;11use anyhow::{Result, bail, Context};12use clap::{ArgGroup, Parser};12use clap::{ArgGroup, Parser};13use serde::de::DeserializeOwned;13use serde::de::DeserializeOwned;14use tempfile::{NamedTempFile, TempDir};14use tempfile::NamedTempFile;15use tokio::process::Command;15use tokio::process::Command;161617use crate::{17use crate::{18 command::CommandExt,18 command::CommandExt,19 fleetdata::{dummy_flake, FleetData},19 fleetdata::{FleetData, FleetSecret, FleetSharedSecret},20};20};212122pub struct FleetConfigInternals {22pub struct FleetConfigInternals {125 .await125 .await126 }126 }127127128 pub fn data(&self) -> Ref<FleetData> {128 pub(super) fn data(&self) -> Ref<FleetData> {129 self.data.borrow()129 self.data.borrow()130 }130 }131 pub fn data_mut(&self) -> RefMut<FleetData> {131 pub(super) fn data_mut(&self) -> RefMut<FleetData> {132 self.data.borrow_mut()132 self.data.borrow_mut()133 }133 }134135 pub fn list_shared(&self) -> Vec<String> {136 let data = self.data();137 data.shared_secrets.keys().cloned().collect()138 }139 pub fn has_shared(&self, name: &str) -> bool {140 let data = self.data();141 data.shared_secrets.contains_key(name)142 }143 pub fn replace_shared(&self, name: String, shared: FleetSharedSecret) {144 let mut data = self.data_mut();145 data.shared_secrets.insert(name.to_owned(), shared);146 }147 pub fn remove_shared(&self, secret: &str) {148 let mut data = self.data_mut();149 data.shared_secrets.remove(secret);150 }151152 pub fn list_secrets(&self, host: &str) -> Vec<String> {153 let data = self.data();154 let Some(host_secrets) = data.host_secrets.get(host) else {155 return Vec::new(); 156 };157 host_secrets.keys().cloned().collect()158 }159 pub fn has_secret(&self, host: &str, secret: &str) -> bool {160 let data = self.data();161 let Some(host_secrets) = data.host_secrets.get(host) else {162 return false; 163 };164 host_secrets.contains_key(secret)165 }166 pub fn insert_secret(&self, host: &str, secret: String, value: FleetSecret) {167 let mut data = self.data_mut();168 let host_secrets = data.host_secrets.entry(host.to_owned()).or_default();169 host_secrets.insert(secret, value);170 }171172 pub async fn decrypt_on_host(&self, host: &str, data: Vec<u8>) -> Result<Vec<u8>>{173 let data = z85::encode(&data);174 let encoded = self.command_on(host, "fleet-install-secrets", true)175 .arg("decrypt")176 .arg("--secret")177 .arg(data).run_string().await.context("failed to call remote host for decrypt")?.trim().to_owned();178 Ok(z85::decode(encoded).context("bad encoded data? outdated host?")?)179 }180 pub async fn reencrypt_on_host(&self, host: &str, data: Vec<u8>, targets: Vec<String>) -> Result<Vec<u8>>{181 let data = z85::encode(&data);182 let mut recmd = self.command_on(host, "fleet-install-secrets", true);183 recmd184 .arg("reencrypt")185 .arg("--secret")186 .arg(format!("\"{}\"", data.replace('$', "\\$")));187 for target in targets {188 recmd.arg("--targets");189 recmd.arg(format!("\"{target}\""));190 }191 let encoded = recmd.run_string().await.context("failed to call remote host for decrypt")?.trim().to_owned();192 Ok(z85::decode(encoded).context("bad encoded data? outdated host?")?)193 }194195 #[must_use]196 pub fn host_secret(&self, host: &str, secret: &str) -> Result<FleetSecret> {197 let data = self.data();198 let Some(host_secrets) = data.host_secrets.get(host) else {199 bail!("no secrets for machine {host}");200 };201 let Some(secret) = host_secrets.get(secret) else {202 bail!("machine {host} has no secret {secret}");203 };204 Ok(secret.clone())205 }206 #[must_use]207 pub fn shared_secret(&self, secret: &str) -> Result<FleetSharedSecret> {208 let data = self.data();209 let Some(secret) = data.shared_secrets.get(secret) else {210 bail!("no shared secret {secret}");211 };212 Ok(secret.clone())213 }134214135 pub fn save(&self) -> Result<()> {215 pub fn save(&self) -> Result<()> {136 let mut tempfile = NamedTempFile::new_in(self.directory.clone())?;216 let mut tempfile = NamedTempFile::new_in(self.directory.clone())?;cmds/fleet/src/keys.rsdiffbeforeafterboth36 }36 }37 }37 }38 /// Insecure, requires root38 /// Insecure, requires root39 pub async fn identity(&self, host: &str) -> anyhow::Result<age::ssh::Identity> {39 pub async fn recipient(&self, host: &str) -> anyhow::Result<age::ssh::Recipient> {40 warn!("Loading private key for {host}");41 let key = self42 .command_on(host, "cat", true)43 .arg("/etc/ssh/ssh_host_ed25519_key")44 .run_string()45 .await?;46 Ok(age::ssh::Identity::from_buffer(key.as_bytes(), None)?)47 }48 pub async fn recipient(&self, host: &str) -> anyhow::Result<age::ssh::Recipient> {49 let key = self.key(host).await?;40 let key = self.key(host).await?;50 age::ssh::Recipient::from_str(&key).map_err(|e| anyhow!("parse recipient error: {:?}", e))41 age::ssh::Recipient::from_str(&key).map_err(|e| anyhow!("parse recipient error: {:?}", e))51 }42 }cmds/install-secrets/src/main.rsdiffbeforeafterboth1use age::Decryptor;1use age::{ssh::Identity as SshIdentity, ssh::Recipient as SshRecipient, Decryptor};2use age::{Encryptor, Identity, Recipient};2use anyhow::{anyhow, bail, Context, Result};3use anyhow::{anyhow, bail, Context, Result};3use clap::Parser;4use clap::Parser;4use log::{error, info, warn};5use log::{error, info, warn};5use nix::sys::stat::Mode;6use nix::sys::stat::Mode;6use nix::unistd::{chown, Group, User};7use nix::unistd::{chown, Group, User};7use serde::{Deserialize, Deserializer};8use serde::{Deserialize, Deserializer};9use std::fmt::{self, Display};8use std::fs::{self, File};10use std::fs::{self, File};9use std::io::{self, Cursor, Read, Write};11use std::io::{self, Cursor, Read, Write};10use std::iter;12use std::iter;11use std::os::unix::prelude::PermissionsExt;13use std::os::unix::prelude::PermissionsExt;14use std::path::Path;12use std::str::from_utf8;15use std::str::{from_utf8, FromStr};13use std::{collections::HashMap, path::PathBuf};16use std::{collections::HashMap, path::PathBuf};1718#[derive(Clone, Debug)]19struct SecretWrapper(Vec<u8>);20impl Display for SecretWrapper {21 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {22 let encoded = z85::encode(&self.0);23 write!(f, "{encoded}")24 }25}26impl FromStr for SecretWrapper {27 type Err = z85::DecodeError;2829 fn from_str(s: &str) -> Result<Self, Self::Err> {30 z85::decode(s).map(Self)31 }32}33impl<'de> Deserialize<'de> for SecretWrapper {34 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>35 where36 D: Deserializer<'de>,37 {38 let v = String::deserialize(deserializer)?;39 let de = z85::decode(v).map_err(|err| serde::de::Error::custom(err.to_string()))?;40 Ok(Self(de))41 }42}144315#[derive(Parser)]44#[derive(Parser)]16#[clap(author)]45#[clap(author)]17struct Opts {46enum Opts {47 /// Install secrets from json specification18 data: PathBuf,48 Install { data: PathBuf },19}49 /// Reencrypt secret using host key, outputting in z85 encoded string50 Reencrypt {51 #[clap(long)]52 secret: SecretWrapper,53 #[clap(long)]54 targets: Vec<String>,55 },56 /// Decrypt secret using host key, outputting in z85 encoded string57 Decrypt {58 #[clap(long)]59 secret: SecretWrapper,60 /// Shoult decoded output be printed as plaintext, instead of z85?61 #[clap(long)]62 plaintext: bool,63 },64}206521#[derive(Deserialize)]66#[derive(Deserialize)]25 mode: String,70 mode: String,26 owner: String,71 owner: String,277228 #[serde(deserialize_with = "from_z85")]29 secret: Option<Vec<u8>>,73 secret: Option<SecretWrapper>,30 public: Option<String>,74 public: Option<String>,317532 public_path: PathBuf,76 public_path: PathBuf,36 stable_secret_path: PathBuf,80 stable_secret_path: PathBuf,37}81}3839fn from_z85<'de, D>(deserializer: D) -> Result<Option<Vec<u8>>, D::Error>40where41 D: Deserializer<'de>,42{43 use serde::de::Error;44 if let Some(v) = <Option<String>>::deserialize(deserializer)? {45 Ok(Some(46 z85::decode(v).map_err(|err| Error::custom(err.to_string()))?,47 ))48 } else {49 Ok(None)50 }51}528253type Data = HashMap<String, DataItem>;83type Data = HashMap<String, DataItem>;8485fn decrypt(input: &SecretWrapper, identity: &dyn Identity) -> Result<Vec<u8>> {86 let mut input = Cursor::new(&input.0);87 let decryptor = Decryptor::new(&mut input).context("failed to init decryptor")?;88 let decryptor = match decryptor {89 Decryptor::Recipients(r) => r,90 Decryptor::Passphrase(_) => bail!("should be recipients"),91 };92 let mut decryptor = decryptor93 .decrypt(iter::once(identity as &dyn age::Identity))94 .context("failed to decrypt, wrong key?")?;9596 let mut decrypted = Vec::new();97 decryptor98 .read_to_end(&mut decrypted)99 .context("failed to decrypt")?;100 Ok(decrypted)101}102fn encrypt(input: &[u8], targets: Vec<String>) -> Result<SecretWrapper> {103 let recipients = targets104 .into_iter()105 .map(|t| {106 SshRecipient::from_str(&t).map_err(|e| anyhow!("failed to parse recipient: {e:?}"))107 })108 .collect::<Result<Vec<SshRecipient>>>()?;109 let recipients = recipients110 .into_iter()111 .map(|v| Box::new(v) as Box<dyn Recipient + Send>)112 .collect::<Vec<_>>();113 let mut encrypted = vec![];114 let mut encryptor = Encryptor::with_recipients(recipients)115 .expect("recipients provided")116 .wrap_output(&mut encrypted)117 .expect("constructor should not fail");118 io::copy(&mut Cursor::new(input), &mut encryptor).expect("copy should not fail");119 encryptor.finish().context("failed to finish encryption")?;120 Ok(SecretWrapper(encrypted))121}5412255fn init_secret(identity: &age::ssh::Identity, value: DataItem) -> Result<()> {123fn init_secret(identity: &age::ssh::Identity, value: DataItem) -> Result<()> {56 if let Some(public) = &value.public {124 if let Some(public) = &value.public {93 let mut hashed = File::create(&value.secret_path)?;161 let mut hashed = File::create(&value.secret_path)?;9416295 // File is owned by root, and only root can modify it163 // File is owned by root, and only root can modify it96 let decrypted = {164 let decrypted = decrypt(&secret, identity)?;97 let mut input = Cursor::new(&secret);98 let decryptor = Decryptor::new(&mut input).context("failed to init decryptor")?;99 let decryptor = match decryptor {100 Decryptor::Recipients(r) => r,101 Decryptor::Passphrase(_) => bail!("should be recipients"),102 };103 let mut decryptor = decryptor104 .decrypt(iter::once(identity as &dyn age::Identity))105 .context("failed to decrypt, wrong key?")?;106107 let mut decrypted = Vec::new();108 decryptor109 .read_to_end(&mut decrypted)110 .context("failed to decrypt")?;111 decrypted112 };113 if decrypted.is_empty() {165 if decrypted.is_empty() {114 warn!("secret is decoded as empty, something is broken?");166 warn!("secret is decoded as empty, something is broken?");115 }167 }132 Ok(())184 Ok(())133}185}186187fn host_identity() -> anyhow::Result<SshIdentity> {188 let identity = SshIdentity::from_buffer(189 &mut Cursor::new(190 fs::read("/etc/ssh/ssh_host_ed25519_key").context("failed to read host private key")?,191 ),192 None,193 )194 .context("failed to parse identity")?;195 Ok(identity)196}134197135fn main() -> anyhow::Result<()> {198fn install(data: &Path) -> anyhow::Result<()> {136 env_logger::Builder::new()137 .filter_level(log::LevelFilter::Info)138 .init();139140 let opts = Opts::parse();141 let data = fs::read(&opts.data).context("failed to read secrets data")?;199 let data = fs::read(data).context("failed to read secrets data")?;142 let data_str = from_utf8(&data).context("failed to read data to string")?;200 let data_str = from_utf8(&data).context("failed to read data to string")?;143 let data: Data = serde_json::from_str(data_str).context("failed to parse data")?;201 let data: Data = serde_json::from_str(data_str).context("failed to parse data")?;144202149 fs::create_dir("/run/secrets").context("failed to create secrets directory")?;207 fs::create_dir("/run/secrets").context("failed to create secrets directory")?;150 }208 }151209152 let identity = age::ssh::Identity::from_buffer(210 let identity = host_identity()?;153 &mut Cursor::new(154 fs::read("/etc/ssh/ssh_host_ed25519_key").context("failed to read host private key")?,155 ),156 None,157 )158 .context("failed to parse identity")?;159211160 let mut failed = false;212 let mut failed = false;161 for (name, value) in data {213 for (name, value) in data {175 Ok(())227 Ok(())176}228}229230fn main() -> anyhow::Result<()> {231 env_logger::Builder::new()232 .filter_level(log::LevelFilter::Info)233 .init();234235 let opts = Opts::parse();236237 match opts {238 Opts::Install { data } => install(&data),239 Opts::Reencrypt { secret, targets } => {240 let identity = host_identity()?;241 let decrypted = decrypt(&secret, &identity).context("during decryption")?;242 let encrypted = encrypt(&decrypted, targets).context("during re-encryption")?;243244 println!("{encrypted}");245 Ok(())246 }247 Opts::Decrypt { secret, plaintext } => {248 let identity = host_identity()?;249 let decrypted = decrypt(&secret, &identity).context("during decryption")?;250251 if plaintext {252 let s = String::from_utf8(decrypted).context("output is not utf8")?;253 print!("{}", s);254 } else {255 println!("{}", SecretWrapper(decrypted));256 }257 Ok(())258 }259 }260}177261modules/fleet/secrets.nixdiffbeforeafterboth20 '';20 '';21 default = [ ];21 default = [ ];22 };22 };23 ownerDependent = mkOption {24 type = bool;25 description = "Is this secret owner-dependent, and needs to be regenerated on ownership set change, or it may be just reencrypted";26 };23 generator = mkOption {27 generator = mkOption {24 type = package;28 type = nullOr package;25 description = "Derivation to execute for secret generation";29 description = ''30 Derivation to execute for secret generation3132 If null - may only be created manually33 '';34 default = null;26 };35 };27 expireIn = mkOption {36 expireIn = mkOption {28 type = nullOr int;37 type = nullOr int;nixos/secrets.nixdiffbeforeafterboth89 };89 };90 };90 };91 config = {91 config = {92 environment.systemPackages = with pkgs; [pkgs.fleet-install-secrets];92 system.activationScripts.decryptSecrets = stringAfter [ "users" "groups" "specialfs" ] ''93 system.activationScripts.decryptSecrets = stringAfter [ "users" "groups" "specialfs" ] ''93 1>&2 echo "setting up secrets"94 1>&2 echo "setting up secrets"94 ${pkgs.fleet-install-secrets}/bin/fleet-install-secrets ${secretsFile}95 ${pkgs.fleet-install-secrets}/bin/fleet-install-secrets install ${secretsFile}95 '';96 '';96 };97 };97}98}9899