difftreelog
fix legacy ssh store support
in: trunk
4 files changed
cmds/fleet/src/cmds/build_systems.rsdiffbeforeafterboth--- a/cmds/fleet/src/cmds/build_systems.rs
+++ b/cmds/fleet/src/cmds/build_systems.rs
@@ -106,6 +106,9 @@
if let Some(destination) = opts.action_attr::<String>(&host, "dest").await? {
host.set_session_destination(destination);
};
+ if let Some(legacy) = opts.action_attr::<bool>(&host, "legacy_ssh_store").await? {
+ host.set_legacy_ssh_store(legacy);
+ };
set.spawn_local(
(async move {
crates/fleet-base/src/host.rsdiffbeforeafterboth--- a/crates/fleet-base/src/host.rs
+++ b/crates/fleet-base/src/host.rs
@@ -13,7 +13,7 @@
use anyhow::{Context, Result, anyhow, bail, ensure};
use fleet_shared::SecretData;
use nix_eval::{Value, nix_go, nix_go_json, util::assert_warn};
-use openssh::SessionBuilder;
+use openssh::{ControlPersist, SessionBuilder};
use serde::de::DeserializeOwned;
use tabled::Tabled;
use tempfile::NamedTempFile;
@@ -99,6 +99,7 @@
// TODO: Both of those values are taken from host opts, there should be a cleaner way to specify it
deploy_kind: OnceCell<DeployKind>,
session_destination: OnceCell<String>,
+ legacy_ssh_store: OnceCell<bool>,
pub host_config: Option<Value>,
pub nixos_config: OnceCell<Value>,
@@ -219,6 +220,11 @@
.set(kind)
.expect("deploy kind is already set");
}
+ pub fn set_legacy_ssh_store(&self, legacy: bool) {
+ self.legacy_ssh_store
+ .set(legacy)
+ .expect("legacy ssh store is already set")
+ }
pub async fn deploy_kind(&self) -> Result<DeployKind> {
if let Some(kind) = self.deploy_kind.get() {
return Ok(*kind);
@@ -263,7 +269,8 @@
if let Some(session) = &self.session.get() {
return Ok((*session).clone());
};
- let session = SessionBuilder::default();
+ let mut session = SessionBuilder::default();
+ session.control_persist(ControlPersist::ClosedAfterInitialConnection);
let dest = self.session_destination.get().unwrap_or(&self.name);
let session = session
@@ -418,9 +425,15 @@
);
nix.arg("copy").arg("--substitute-on-destination");
+ let proto = if self.legacy_ssh_store.get().cloned().unwrap_or(false) {
+ "ssh"
+ } else {
+ "ssh-ng"
+ };
+
match self.deploy_kind().await? {
DeployKind::Fleet | DeployKind::UpgradeToFleet | DeployKind::NixosLustrate => {
- nix.comparg("--to", format!("ssh-ng://{}", self.name));
+ nix.comparg("--to", format!("{proto}://{}", self.name));
}
DeployKind::NixosInstall => {
nix
@@ -428,7 +441,7 @@
.arg("--no-check-sigs")
.comparg(
"--to",
- format!("ssh-ng://root@{}?remote-store=/mnt", self.name),
+ format!("{proto}://root@{}?remote-store=/mnt", self.name),
);
}
}
@@ -568,6 +581,7 @@
session: OnceLock::new(),
deploy_kind: OnceCell::new(),
session_destination: OnceCell::new(),
+ legacy_ssh_store: OnceCell::new(),
}
}
@@ -589,6 +603,7 @@
session: OnceLock::new(),
deploy_kind: OnceCell::new(),
session_destination: OnceCell::new(),
+ legacy_ssh_store: OnceCell::new(),
})
}
pub async fn list_hosts(&self) -> Result<Vec<ConfigHost>> {
crates/fleet-shared/src/encoding.rsdiffbeforeafterboth1use std::{2 fmt::{self, Display},3 str::FromStr,4};56use base64::engine::{Engine, general_purpose::STANDARD_NO_PAD};7use serde::{Deserialize, Deserializer, Serialize, de::Error};8use unicode_categories::UnicodeCategories;910#[derive(Debug, PartialEq, Clone)]11pub struct SecretData {12 pub data: Vec<u8>,13 pub encrypted: bool,14}1516const BASE64_ENCODED_PREFIX: &str = "<BASE64-ENCODED>\n";17const Z85_ENCODED_PREFIX: &str = "<Z85-ENCODED>\n";18// Multiline text in Nix can only end with \n, which is not cool for actual single-line strings.19const PLAINTEXT_NEWLINE_PREFIX: &str = "<PLAINTEXT-NL>\n";20const PLAINTEXT_PREFIX: &str = "<PLAINTEXT>";2122const SECRET_PREFIX: &str = "<ENCRYPTED>";2324impl<'de> Deserialize<'de> for SecretData {25 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>26 where27 D: Deserializer<'de>,28 {29 let string = String::deserialize(deserializer)?;30 string.parse().map_err(D::Error::custom)31 }32}3334impl Serialize for SecretData {35 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>36 where37 S: serde::Serializer,38 {39 self.to_string().serialize(serializer)40 }41}4243impl FromStr for SecretData {44 type Err = String;4546 fn from_str(string: &str) -> Result<Self, Self::Err> {47 let (encrypted, string) = if let Some(unprefixed) = string.strip_prefix(SECRET_PREFIX) {48 (true, unprefixed)49 } else {50 (false, string)51 };52 let data = if let Some(unprefixed) = string.strip_prefix(BASE64_ENCODED_PREFIX) {53 STANDARD_NO_PAD54 .decode(unprefixed.replace(['\n', '\t', ' '], ""))55 .map_err(|e| format!("base64-encoded failed: {e}"))?56 } else if let Some(unprefixed) = string.strip_prefix(Z85_ENCODED_PREFIX) {57 z85::decode(unprefixed.replace(['\n', '\t', ' '], ""))58 .map_err(|e| format!("z85-encoded failed: {e}"))?59 } else if let Some(unprefixed) = string.strip_prefix(PLAINTEXT_NEWLINE_PREFIX) {60 unprefixed.as_bytes().to_owned()61 } else if let Some(unprefixed) = string.strip_prefix(PLAINTEXT_PREFIX) {62 unprefixed.as_bytes().to_owned()63 } else {64 let secret_prefix = format!("{SECRET_PREFIX}{Z85_ENCODED_PREFIX}");65 return Err(format!(66 "unknown secret encoding. If you're migrating from old version of fleet, prefix public secret fields with {PLAINTEXT_PREFIX:?}, and encrypted data with {secret_prefix:?}: {string}"67 ));68 };69 Ok(Self { data, encrypted })70 }71}7273impl Display for SecretData {74 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {75 let mut readable = std::str::from_utf8(&self.data).ok();76 if self.encrypted {77 write!(f, "{SECRET_PREFIX}")?;78 // Always base64-encode encrypted fields.79 readable = None;80 }81 if Some(false) == readable.map(is_printable) {82 readable = None83 };84 // TODO: Check if text is readable, and has no unprintable characters?..85 if let Some(plaintext) = readable {86 if plaintext.ends_with('\n') {87 write!(f, "{PLAINTEXT_NEWLINE_PREFIX}")?;88 } else {89 write!(f, "{PLAINTEXT_PREFIX}")?;90 }91 write!(f, "{plaintext}")?;92 } else {93 write!(f, "{BASE64_ENCODED_PREFIX}")?;94 let encoded = STANDARD_NO_PAD.encode(&self.data);95 for ele in encoded.as_bytes().chunks(64) {96 let chunk = std::str::from_utf8(ele).expect(97 "any slice of base64-encoded text is utf-8 compatible, as it is ascii-based",98 );99 writeln!(f, "{chunk}")?;100 }101 };102 Ok(())103 }104}105106fn is_printable(text: &str) -> bool {107 text.chars().all(|c| {108 c.is_letter()109 || c.is_mark()110 || c.is_number()111 || c.is_punctuation()112 || c.is_separator()113 || c == '\n' || c == '\t'114 // Complete base64 alphabet115 || c == '/' || c == '+'116 || c == '='117 })118}119120#[test]121fn test() {122 fn check_roundtrip(data: SecretData, expected: &str) {123 let string = data.to_string();124 assert_eq!(string, expected, "unexpected encoding");125 let roundtrip: SecretData = string.parse().expect("roundtrip parse");126 assert_eq!(data, roundtrip, "roundtrip didn't match");127 }128 check_roundtrip(129 SecretData {130 data: vec![1, 2, 3, 4, 5, 6],131 encrypted: false,132 },133 "<BASE64-ENCODED>\nAQIDBAUG\n",134 );135 check_roundtrip(136 SecretData {137 data: vec![1, 2, 3, 4, 5, 6],138 encrypted: true,139 },140 "<ENCRYPTED><BASE64-ENCODED>\nAQIDBAUG\n",141 );142 check_roundtrip(143 SecretData {144 data: "Привет, мир!\n".to_owned().into(),145 encrypted: false,146 },147 "<PLAINTEXT-NL>\nПривет, мир!\n",148 );149 check_roundtrip(150 SecretData {151 data: "Привет, мир!".to_owned().into(),152 encrypted: false,153 },154 "<PLAINTEXT>Привет, мир!",155 );156}1use std::{2 collections::BTreeMap, fmt::{self, Display}, str::FromStr3};45use base64::engine::{Engine, general_purpose::STANDARD_NO_PAD};6use serde::{Deserialize, Deserializer, Serialize, de::Error};7use unicode_categories::UnicodeCategories;89#[derive(Debug, PartialEq, Clone)]10pub struct SecretData {11 pub data: Vec<u8>,12 pub encrypted: bool,13}1415const BASE64_ENCODED_PREFIX: &str = "<BASE64-ENCODED>\n";16const Z85_ENCODED_PREFIX: &str = "<Z85-ENCODED>\n";17// Multiline text in Nix can only end with \n, which is not cool for actual single-line strings.18const PLAINTEXT_NEWLINE_PREFIX: &str = "<PLAINTEXT-NL>\n";19const PLAINTEXT_PREFIX: &str = "<PLAINTEXT>";2021const SECRET_PREFIX: &str = "<ENCRYPTED>";2223impl<'de> Deserialize<'de> for SecretData {24 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>25 where26 D: Deserializer<'de>,27 {28 let string = String::deserialize(deserializer)?;29 string.parse().map_err(D::Error::custom)30 }31}3233impl Serialize for SecretData {34 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>35 where36 S: serde::Serializer,37 {38 self.to_string().serialize(serializer)39 }40}4142impl FromStr for SecretData {43 type Err = String;4445 fn from_str(string: &str) -> Result<Self, Self::Err> {46 let (encrypted, string) = if let Some(unprefixed) = string.strip_prefix(SECRET_PREFIX) {47 (true, unprefixed)48 } else {49 (false, string)50 };51 let data = if let Some(unprefixed) = string.strip_prefix(BASE64_ENCODED_PREFIX) {52 STANDARD_NO_PAD53 .decode(unprefixed.replace(['\n', '\t', ' '], ""))54 .map_err(|e| format!("base64-encoded failed: {e}"))?55 } else if let Some(unprefixed) = string.strip_prefix(Z85_ENCODED_PREFIX) {56 z85::decode(unprefixed.replace(['\n', '\t', ' '], ""))57 .map_err(|e| format!("z85-encoded failed: {e}"))?58 } else if let Some(unprefixed) = string.strip_prefix(PLAINTEXT_NEWLINE_PREFIX) {59 unprefixed.as_bytes().to_owned()60 } else if let Some(unprefixed) = string.strip_prefix(PLAINTEXT_PREFIX) {61 unprefixed.as_bytes().to_owned()62 } else {63 let secret_prefix = format!("{SECRET_PREFIX}{Z85_ENCODED_PREFIX}");64 return Err(format!(65 "unknown secret encoding. If you're migrating from old version of fleet, prefix public secret fields with {PLAINTEXT_PREFIX:?}, and encrypted data with {secret_prefix:?}: {string}"66 ));67 };68 Ok(Self { data, encrypted })69 }70}7172impl Display for SecretData {73 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {74 let mut readable = std::str::from_utf8(&self.data).ok();75 if self.encrypted {76 write!(f, "{SECRET_PREFIX}")?;77 // Always base64-encode encrypted fields.78 readable = None;79 }80 if Some(false) == readable.map(is_printable) {81 readable = None82 };83 // TODO: Check if text is readable, and has no unprintable characters?..84 if let Some(plaintext) = readable {85 if plaintext.ends_with('\n') {86 write!(f, "{PLAINTEXT_NEWLINE_PREFIX}")?;87 } else {88 write!(f, "{PLAINTEXT_PREFIX}")?;89 }90 write!(f, "{plaintext}")?;91 } else {92 write!(f, "{BASE64_ENCODED_PREFIX}")?;93 let encoded = STANDARD_NO_PAD.encode(&self.data);94 for ele in encoded.as_bytes().chunks(64) {95 let chunk = std::str::from_utf8(ele).expect(96 "any slice of base64-encoded text is utf-8 compatible, as it is ascii-based",97 );98 writeln!(f, "{chunk}")?;99 }100 };101 Ok(())102 }103}104105fn is_printable(text: &str) -> bool {106 text.chars().all(|c| {107 c.is_letter()108 || c.is_mark()109 || c.is_number()110 || c.is_punctuation()111 || c.is_separator()112 || c == '\n' || c == '\t'113 // Complete base64 alphabet114 || c == '/' || c == '+'115 || c == '='116 })117}118119#[test]120fn test() {121 fn check_roundtrip(data: SecretData, expected: &str) {122 let string = data.to_string();123 assert_eq!(string, expected, "unexpected encoding");124 let roundtrip: SecretData = string.parse().expect("roundtrip parse");125 assert_eq!(data, roundtrip, "roundtrip didn't match");126 }127 check_roundtrip(128 SecretData {129 data: vec![1, 2, 3, 4, 5, 6],130 encrypted: false,131 },132 "<BASE64-ENCODED>\nAQIDBAUG\n",133 );134 check_roundtrip(135 SecretData {136 data: vec![1, 2, 3, 4, 5, 6],137 encrypted: true,138 },139 "<ENCRYPTED><BASE64-ENCODED>\nAQIDBAUG\n",140 );141 check_roundtrip(142 SecretData {143 data: "Привет, мир!\n".to_owned().into(),144 encrypted: false,145 },146 "<PLAINTEXT-NL>\nПривет, мир!\n",147 );148 check_roundtrip(149 SecretData {150 data: "Привет, мир!".to_owned().into(),151 encrypted: false,152 },153 "<PLAINTEXT>Привет, мир!",154 );155}flake.nixdiffbeforeafterboth--- a/flake.nix
+++ b/flake.nix
@@ -168,12 +168,9 @@
cargo-fuzz
cargo-watch
cargo-outdated
- gdb
pkg-config
openssl
- bacon
- nil
rustPlatform.bindgenHook
inputs'.nix.packages.nix-expr-c
inputs'.nix.packages.nix-flake-c