10 files changed
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1045,7 +1045,10 @@
"ed25519-dalek",
"fleet-shared",
"hex",
+ "hmac",
+ "pbkdf2",
"rand 0.10.1",
+ "sha2",
"x25519-dalek",
]
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -31,6 +31,7 @@
futures = "0.3.31"
futures-util = { version = "0.3.31", features = ["sink"] }
hex = "0.4.3"
+hmac = "0.12"
hostname = "0.4.1"
human-repr = "1.1"
hyper = "1.8.1"
@@ -40,11 +41,12 @@
linked-hash-map = "0.5.6"
nix = { version = "0.31.2", features = ["fs", "user"] }
nom = "8.0.0"
+openssh = "0.11.5"
opentelemetry = "0.31.0"
+opentelemetry-appender-tracing = "0.31.1"
opentelemetry-otlp = { version = "0.31.0", features = ["grpc-tonic", "gzip-tonic", "http-json", "reqwest-rustls"] }
opentelemetry_sdk = "0.31.0"
-opentelemetry-appender-tracing = "0.31.1"
-openssh = "0.11.5"
+pbkdf2 = "0.12"
peg = "0.8.5"
pkg-config = "0.3.30"
rand = "0.10.0"
@@ -52,6 +54,7 @@
serde = { version = "1.0", features = ["derive"] }
serde-transcode = "1.1.1"
serde_json = "1.0"
+sha2 = "0.10"
shlex = "1.3"
tabled = "0.20.0"
tempfile = "3.20"
--- a/cmds/fleet/Cargo.toml
+++ b/cmds/fleet/Cargo.toml
@@ -44,12 +44,12 @@
indicatif = { workspace = true, optional = true }
nom.workspace = true
opentelemetry.workspace = true
+opentelemetry-appender-tracing.workspace = true
+opentelemetry-exporter-env.workspace = true
opentelemetry_sdk.workspace = true
thiserror.workspace = true
tracing-indicatif = { workspace = true, optional = true }
tracing-opentelemetry.workspace = true
-opentelemetry-exporter-env.workspace = true
-opentelemetry-appender-tracing.workspace = true
[features]
default = ["indicatif"]
--- a/cmds/generator-helper/Cargo.toml
+++ b/cmds/generator-helper/Cargo.toml
@@ -13,5 +13,8 @@
base64.workspace = true
ed25519-dalek.workspace = true
hex.workspace = true
+hmac.workspace = true
+pbkdf2.workspace = true
rand.workspace = true
+sha2.workspace = true
x25519-dalek.workspace = true
--- a/cmds/generator-helper/src/main.rs
+++ b/cmds/generator-helper/src/main.rs
@@ -10,14 +10,30 @@
ssh::{ParseRecipientKeyError, Recipient as SshRecipient},
};
use anyhow::{Context, Result, anyhow, bail, ensure};
+use base64::{Engine as _, engine::general_purpose::STANDARD, write::EncoderWriter};
use clap::{Parser, ValueEnum};
use ed25519_dalek::SecretKey;
use fleet_shared::SecretData;
+use hmac::Mac as _;
use rand::{
Rng as _,
distr::{Alphanumeric, Distribution, SampleString, Uniform},
rng,
};
+use sha2::Digest as _;
+
+fn gen_password(rng: &mut impl rand::Rng, size: usize, no_symbols: bool) -> String {
+ if no_symbols {
+ Alphanumeric.sample_string(rng, size)
+ } else {
+ const GEN_ASCII_SYMBOLS: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-!\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~";
+ let uniform = Uniform::new(0, GEN_ASCII_SYMBOLS.len()).expect("range is valid");
+ (0..size)
+ .map(|_| uniform.sample(rng))
+ .map(|i| GEN_ASCII_SYMBOLS[i] as char)
+ .collect::<String>()
+ }
+}
fn write_output_file(out: &str) -> Result<File> {
let file = OpenOptions::new()
@@ -105,8 +121,6 @@
match encoding {
OutputEncoding::Raw => coerce(w),
OutputEncoding::Base64 => {
- use base64::{engine::general_purpose::STANDARD, write::EncoderWriter};
-
let writer = EncoderWriter::new(w, &STANDARD);
coerce(writer)
}
@@ -177,6 +191,18 @@
#[arg(long, short = 'e', value_enum, default_value_t)]
encoding: OutputEncoding,
},
+ PostgresPassword {
+ #[arg(long, short = 's')]
+ secret: String,
+ #[arg(long, short = 'H')]
+ hash: String,
+ #[arg(long, default_value_t = 24)]
+ size: usize,
+ #[arg(long, default_value_t = 4096)]
+ iterations: u32,
+ #[arg(long, short = 'n')]
+ no_symbols: bool,
+ },
Bytes {
#[arg(long, short = 'o')]
output: String,
@@ -286,20 +312,62 @@
"misconfiguration? password is shorter than 6 chars"
);
let recipients = load_identities()?;
- let out = if no_symbols {
- Alphanumeric.sample_string(&mut rng, size)
- } else {
- // Alphabet of Alphanumberic + symbols
- const GEN_ASCII_SYMBOLS: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-!\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~";
- let uniform =
- Uniform::new(0, GEN_ASCII_SYMBOLS.len()).expect("range is valid");
- (0..size)
- .map(|_| uniform.sample(&mut rng))
- .map(|i| GEN_ASCII_SYMBOLS[i] as char)
- .collect::<String>()
- };
+ let out = gen_password(&mut rng, size, no_symbols);
write_private(&recipients, &output, out.as_bytes(), encoding)?;
}
+ Generate::PostgresPassword {
+ secret,
+ hash,
+ size,
+ iterations,
+ no_symbols,
+ } => {
+ ensure!(
+ size >= 6,
+ "misconfiguration? password is shorter than 6 chars"
+ );
+ let recipients = load_identities()?;
+ let password = gen_password(&mut rng, size, no_symbols);
+
+ let mut salt = [0u8; 16];
+ rng.fill_bytes(&mut salt);
+ let salted = pbkdf2::pbkdf2_hmac_array::<sha2::Sha256, 32>(
+ password.as_bytes(),
+ &salt,
+ iterations,
+ );
+
+ type HmacSha256 = hmac::Hmac<sha2::Sha256>;
+ let mut mac = <HmacSha256 as hmac::Mac>::new_from_slice(&salted)
+ .expect("HMAC accepts any key length");
+ mac.update(b"Client Key");
+ let client_key = mac.finalize().into_bytes();
+
+ let mut hasher = sha2::Sha256::new();
+ hasher.update(client_key);
+ let stored_key = hasher.finalize();
+
+ let mut mac = <HmacSha256 as hmac::Mac>::new_from_slice(&salted)
+ .expect("HMAC accepts any key length");
+ mac.update(b"Server Key");
+ let server_key = mac.finalize().into_bytes();
+
+ let hash_str = format!(
+ "SCRAM-SHA-256${}:{}${}:{}",
+ iterations,
+ STANDARD.encode(salt),
+ STANDARD.encode(stored_key),
+ STANDARD.encode(server_key),
+ );
+
+ write_private(
+ &recipients,
+ &secret,
+ password.as_bytes(),
+ OutputEncoding::Raw,
+ )?;
+ write_public(&hash, hash_str.as_bytes(), OutputEncoding::Raw)?;
+ }
Generate::Bytes {
output,
count,
--- a/crates/nix-eval/src/lib.rs
+++ b/crates/nix-eval/src/lib.rs
@@ -26,9 +26,8 @@
clear_err, copy_value, err_NIX_ERR_KEY, err_NIX_ERR_NIX_ERROR, err_NIX_ERR_OVERFLOW,
err_NIX_ERR_UNKNOWN, err_code, err_info_msg, err_msg, eval_state_build,
eval_state_builder_load, eval_state_builder_new, eval_state_builder_set_eval_setting,
- expr_eval_from_string, fetchers_settings,
- fetchers_settings_free, fetchers_settings_new, flake_lock, flake_lock_flags,
- flake_lock_flags_free, flake_lock_flags_new, flake_reference,
+ expr_eval_from_string, fetchers_settings, fetchers_settings_free, fetchers_settings_new,
+ flake_lock, flake_lock_flags, flake_lock_flags_free, flake_lock_flags_new, flake_reference,
flake_reference_and_fragment_from_string, flake_reference_parse_flags,
flake_reference_parse_flags_free, flake_reference_parse_flags_new,
flake_reference_parse_flags_set_base_directory, flake_settings, flake_settings_free,
@@ -323,7 +322,9 @@
thread_local! {
static THREAD_STATE: RefCell<ThreadState> = RefCell::new(ThreadState::new().expect("thread state init shouldn't fail"));
}
-pub(crate) fn with_default_context<T>(f: impl FnOnce(*mut c_context, *mut c_eval_state) -> T) -> Result<T> {
+pub(crate) fn with_default_context<T>(
+ f: impl FnOnce(*mut c_context, *mut c_eval_state) -> T,
+) -> Result<T> {
let global = &GLOBAL_STATE.state;
let (ctx, state) = THREAD_STATE.with_borrow_mut(|w| (w.ctx.0, global.0));
let mut ctx = NixContext(ctx);
@@ -439,7 +440,11 @@
}
}
-pub(crate) unsafe extern "C" fn copy_nix_str(start: *const c_char, n: c_uint, user_data: *mut c_void) {
+pub(crate) unsafe extern "C" fn copy_nix_str(
+ start: *const c_char,
+ n: c_uint,
+ user_data: *mut c_void,
+) {
let s = unsafe { slice::from_raw_parts(start.cast::<u8>(), n as usize) };
let s = std::str::from_utf8(s).expect("c string has invalid utf-8");
unsafe { *user_data.cast::<String>() = s.to_owned() };
@@ -1128,8 +1133,8 @@
let test_result: String = nix_go_json!(builtins.uppercaseSuffix2("test")("suffix"));
assert_eq!(test_result, "TESTsuffix");
- let drv_path = nix_go!(attrs.packages["x86_64-linux"]["fleet-install-secrets"].drvPath)
- .to_string()?;
+ let drv_path =
+ nix_go!(attrs.packages["x86_64-linux"]["fleet-install-secrets"].drvPath).to_string()?;
let graph = drv::DrvGraph::resolve(&drv_path)?;
eprintln!(
"fleet-install-secrets dependency graph: {} nodes",
--- a/crates/nix-eval/src/logging.rs
+++ b/crates/nix-eval/src/logging.rs
@@ -351,12 +351,15 @@
if let Some(entry) = drv_graph.get_mut(dep_path) {
entry.refcount += 1;
} else {
- drv_graph.insert(dep_path.clone(), DrvGraphEntry {
- name: dep_node.name.clone(),
- parent: Some(path.clone()),
- span: None,
- refcount: 1,
- });
+ drv_graph.insert(
+ dep_path.clone(),
+ DrvGraphEntry {
+ name: dep_node.name.clone(),
+ parent: Some(path.clone()),
+ span: None,
+ refcount: 1,
+ },
+ );
}
paths.push(dep_path.clone());
queue.push_back(dep_path.clone());
--- a/crates/opentelemetry-exporter-env/src/lib.rs
+++ b/crates/opentelemetry-exporter-env/src/lib.rs
@@ -11,10 +11,7 @@
#[derive(thiserror::Error, Debug)]
pub enum Error {
#[error("environment variable {env} contains invalid UTF-8: {value:?}")]
- InvalidUtf8 {
- env: &'static str,
- value: OsString,
- },
+ InvalidUtf8 { env: &'static str, value: OsString },
#[error("environment variable {env}={value:?}: {error}")]
EnvParse {
env: &'static str,
@@ -103,13 +100,30 @@
#[cfg_attr(feature = "clap", derive(clap::Parser))]
pub struct SignalExporterSettings {
/// Traces exporter to be used.
- #[cfg_attr(feature = "clap", arg(long = "otel-traces-exporter", env = "OTEL_TRACES_EXPORTER", value_enum))]
+ #[cfg_attr(
+ feature = "clap",
+ arg(
+ long = "otel-traces-exporter",
+ env = "OTEL_TRACES_EXPORTER",
+ value_enum
+ )
+ )]
pub traces: Option<ExporterKind>,
/// Metrics exporter to be used.
- #[cfg_attr(feature = "clap", arg(long = "otel-metrics-exporter", env = "OTEL_METRICS_EXPORTER", value_enum))]
+ #[cfg_attr(
+ feature = "clap",
+ arg(
+ long = "otel-metrics-exporter",
+ env = "OTEL_METRICS_EXPORTER",
+ value_enum
+ )
+ )]
pub metrics: Option<ExporterKind>,
/// Logs exporter to be used.
- #[cfg_attr(feature = "clap", arg(long = "otel-logs-exporter", env = "OTEL_LOGS_EXPORTER", value_enum))]
+ #[cfg_attr(
+ feature = "clap",
+ arg(long = "otel-logs-exporter", env = "OTEL_LOGS_EXPORTER", value_enum)
+ )]
pub logs: Option<ExporterKind>,
}
--- a/crates/opentelemetry-exporter-env/src/otlp.rs
+++ b/crates/opentelemetry-exporter-env/src/otlp.rs
@@ -55,15 +55,13 @@
}
builder.build()
}
- OtlpProtocol::HttpProtobuf | OtlpProtocol::HttpJson => {
- <$exporter>::builder()
- .with_http()
- .with_endpoint(&s.endpoint)
- .with_headers(to_hashmap(s.headers.as_deref()))
- .with_protocol(s.protocol.into())
- .with_timeout(s.timeout)
- .build()
- }
+ OtlpProtocol::HttpProtobuf | OtlpProtocol::HttpJson => <$exporter>::builder()
+ .with_http()
+ .with_endpoint(&s.endpoint)
+ .with_headers(to_hashmap(s.headers.as_deref()))
+ .with_protocol(s.protocol.into())
+ .with_timeout(s.timeout)
+ .build(),
}
}};
}
12{ lib }:3let4 inherit (lib.trivial) isFunction functionArgs;5 inherit (lib.options) mkOption mergeOneOption;6 inherit (lib.modules) mkOverride;7 inherit (lib.types)8 listOf9 submodule10 attrsOf11 mkOptionType12 ;13 inherit (lib.strings) optionalString hasPrefix removePrefix;14in15rec {16 types = {17 overlay = mkOptionType {18 name = "nixpkgs-overlay";19 description = "nixpkgs overlay";20 check = {21 __functor = _self: isFunction;22 isV2MergeCoherent = true;23 };24 merge = mergeOneOption;25 };26 listOfOverlay = listOf types.overlay;2728 mkHostsType = module: attrsOf (submodule module);29 mkDataType = module: submodule module;30 };3132 options = {33 mkHostsOption =34 module:35 mkOption {36 type = types.mkHostsType module;37 };38 mkDataOption =39 module:40 mkOption {41 type = types.mkDataType module;42 };43 };4445 inherit (options) mkHostsOption;4647 modules = {48 495051 mkFleetDefault = mkOverride 999;52 535455 mkFleetGeneratorDefault = mkOverride 1001;56 };5758 inherit (modules) mkFleetDefault mkFleetGeneratorDefault;5960 secrets = {6162 6364656667686970717273 mkPassword =74 {75 size ? 32,76 }:77 (78 {79 coreutils,80 mkSecretGenerator,81 }:82 mkSecretGenerator {83 script = ''84 mkdir $out85 gh generate password -o $out/secret --size ${toString size}86 '';87 parts.secret.encrypted = true;88 }89 );9091 9293949596979899100101102103104 105 106 mkPostgresPassword =107 {108 size ? 32,109 iterations ? 4096,110 noSymbols ? false,111 }:112 (113 { mkSecretGenerator }:114 mkSecretGenerator {115 script = ''116 mkdir $out117 gh generate postgres-password \118 -s $out/secret \119 -H $out/hash \120 --size ${toString size} \121 --iterations ${toString iterations} \122 ${optionalString noSymbols "--no-symbols"}123 '';124 parts.secret.encrypted = true;125 parts.hash.encrypted = false;126 }127 );128129 130131132133134135136137138139140141142 mkEd25519 =143 {144 noEmbedPublic ? false,145 encoding ? null,146 }:147 (148 { mkSecretGenerator }:149 mkSecretGenerator {150 script = ''151 mkdir $out152 gh generate ed25519 -p $out/public -s $out/secret \153 ${optionalString noEmbedPublic "--no-embed-public"} \154 ${optionalString (encoding != null) "--encoding=${encoding}"}155 '';156 parts.secret.encrypted = true;157 parts.public.encrypted = false;158 }159 );160161 162163164165166167168169170171172 mkX25519 =173 {174 encoding ? null,175 }:176 (177 { mkSecretGenerator }:178 mkSecretGenerator {179 script = ''180 mkdir $out181 gh generate x25519 -p $out/public -s $out/secret \182 ${optionalString (encoding != null) "--encoding=${encoding}"}183 '';184185 parts.secret.encrypted = true;186 parts.public.encrypted = false;187 }188 );189190 mkAskPass =191 {192 prompt ? "Secret value",193 part ? "secret",194 }:195 (196 {197 kdePackages,198 mkImpureSecretGenerator,199 }:200 mkImpureSecretGenerator {201 script = ''202 mkdir $out203 ${kdePackages.kdialog}/bin/kdialog --inputbox "${prompt}" | gh private -o $out/${part}204 '';205206 parts.${part}.encrypted = true;207 }208 );209210 mkAskFile =211 {212 header ? "",213 part ? "secret",214 }:215 (216 {217 kdePackages,218 coreutils,219 mkImpureSecretGenerator,220 }:221 mkImpureSecretGenerator {222 script = ''223 mkdir $out224 tmpfile=$(${coreutils}/bin/mktemp)225 trap "${coreutils}/bin/rm -f $tmpfile" EXIT226 cat > "$tmpfile" <<'HEADER'227 ${header}228 HEADER229 ${kdePackages.kate}/bin/kate --startanon --new --block "$tmpfile"230 gh private -o $out/${part} < "$tmpfile"231 '';232233 parts.${part}.encrypted = true;234 }235 );236237 mkAskEnv =238 {239 header ? "",240 variables ? [ ],241 part ? "secret",242 }:243 mkAskFile {244 inherit part;245 header = builtins.concatStringsSep "\n" (246 (map (l: "# ${l}") (lib.splitString "\n" header)) ++ (map (v: "${v}=") variables)247 );248 };249250 251252253254255256257258259260 mkRsa =261 {262 size ? 4096,263 }:264 (265 {266 openssl,267 mkSecretGenerator,268 }:269 mkSecretGenerator {270 script = ''271 mkdir $out272273 ${openssl}/bin/openssl genrsa -out rsa_private.key ${toString size}274 ${openssl}/bin/openssl rsa -in rsa_private.key -pubout -out rsa_public.key275276 cat rsa_private.key | gh private -o $out/secret277 cat rsa_public.key | gh public -o $out/public278 '';279280 parts.secret.encrypted = true;281 parts.public.encrypted = false;282 }283 );284285 286287288289290291292293294295296297298299 mkBytes =300 {301 count ? 32,302 encoding,303 noNuls ? false,304 }:305 (306 { mkSecretGenerator }:307 mkSecretGenerator {308 script = ''309 mkdir $out310 gh generate bytes --count=${toString count} --encoding=${encoding} -o $out/secret \311 ${optionalString noNuls "--no-nuls"}312 '';313 parts.secret.encrypted = true;314 }315 );316 317318319 mkHexBytes =320 {321 count ? 32,322 }:323 mkBytes {324 inherit count;325 encoding = "hex";326 };327 328329330 mkBase64Bytes =331 {332 count ? 32,333 }:334 mkBytes {335 inherit count;336 encoding = "base64";337 };338339 340 341 342 };343344 inherit (secrets)345 mkPassword346 mkPostgresPassword347 mkEd25519348 mkX25519349 mkRsa350 mkBytes351 mkHexBytes352 mkBase64Bytes353 mkAskPass354 mkAskFile355 mkAskEnv356 ;357358 strings =359 let360 plaintextPrefix = "<PLAINTEXT>";361 plaintextNewlinePrefix = "<PLAINTEXT-NL>";362 in363 {364 365366367 decodeRawSecret =368 raw:369 if hasPrefix plaintextPrefix raw then370 removePrefix plaintextPrefix raw371 else if hasPrefix plaintextNewlinePrefix raw then372 removePrefix plaintextNewlinePrefix raw373 else374 throw "decodeRawSecret only works with plaintext-encoded secret public parts, got ${raw}";375 };376377 inherit (strings) decodeRawSecret;378}