git.delta.rocks / fleet / refs/commits / 992c7b63e98f

difftreelog

feat postgres secret generator

xmvqpnskYaroslav Bolyukin2026-05-20parent: #7fa1963.patch.diff

10 files changed

modifiedCargo.lockdiffbeforeafterboth
1045 "ed25519-dalek",1045 "ed25519-dalek",
1046 "fleet-shared",1046 "fleet-shared",
1047 "hex",1047 "hex",
1048 "hmac",
1049 "pbkdf2",
1048 "rand 0.10.1",1050 "rand 0.10.1",
1051 "sha2",
1049 "x25519-dalek",1052 "x25519-dalek",
1050]1053]
10511054
modifiedCargo.tomldiffbeforeafterboth
31futures = "0.3.31"31futures = "0.3.31"
32futures-util = { version = "0.3.31", features = ["sink"] }32futures-util = { version = "0.3.31", features = ["sink"] }
33hex = "0.4.3"33hex = "0.4.3"
34hmac = "0.12"
34hostname = "0.4.1"35hostname = "0.4.1"
35human-repr = "1.1"36human-repr = "1.1"
36hyper = "1.8.1"37hyper = "1.8.1"
40linked-hash-map = "0.5.6"41linked-hash-map = "0.5.6"
41nix = { version = "0.31.2", features = ["fs", "user"] }42nix = { version = "0.31.2", features = ["fs", "user"] }
42nom = "8.0.0"43nom = "8.0.0"
44openssh = "0.11.5"
43opentelemetry = "0.31.0"45opentelemetry = "0.31.0"
46opentelemetry-appender-tracing = "0.31.1"
44opentelemetry-otlp = { version = "0.31.0", features = ["grpc-tonic", "gzip-tonic", "http-json", "reqwest-rustls"] }47opentelemetry-otlp = { version = "0.31.0", features = ["grpc-tonic", "gzip-tonic", "http-json", "reqwest-rustls"] }
45opentelemetry_sdk = "0.31.0"48opentelemetry_sdk = "0.31.0"
46opentelemetry-appender-tracing = "0.31.1"
47openssh = "0.11.5"49pbkdf2 = "0.12"
48peg = "0.8.5"50peg = "0.8.5"
49pkg-config = "0.3.30"51pkg-config = "0.3.30"
50rand = "0.10.0"52rand = "0.10.0"
51regex = "1.11"53regex = "1.11"
52serde = { version = "1.0", features = ["derive"] }54serde = { version = "1.0", features = ["derive"] }
53serde-transcode = "1.1.1"55serde-transcode = "1.1.1"
54serde_json = "1.0"56serde_json = "1.0"
57sha2 = "0.10"
55shlex = "1.3"58shlex = "1.3"
56tabled = "0.20.0"59tabled = "0.20.0"
57tempfile = "3.20"60tempfile = "3.20"
modifiedcmds/fleet/Cargo.tomldiffbeforeafterboth
44indicatif = { workspace = true, optional = true }44indicatif = { workspace = true, optional = true }
45nom.workspace = true45nom.workspace = true
46opentelemetry.workspace = true46opentelemetry.workspace = true
47opentelemetry-appender-tracing.workspace = true
48opentelemetry-exporter-env.workspace = true
47opentelemetry_sdk.workspace = true49opentelemetry_sdk.workspace = true
48thiserror.workspace = true50thiserror.workspace = true
49tracing-indicatif = { workspace = true, optional = true }51tracing-indicatif = { workspace = true, optional = true }
50tracing-opentelemetry.workspace = true52tracing-opentelemetry.workspace = true
51opentelemetry-exporter-env.workspace = true
52opentelemetry-appender-tracing.workspace = true
5353
54[features]54[features]
55default = ["indicatif"]55default = ["indicatif"]
modifiedcmds/generator-helper/Cargo.tomldiffbeforeafterboth
13base64.workspace = true13base64.workspace = true
14ed25519-dalek.workspace = true14ed25519-dalek.workspace = true
15hex.workspace = true15hex.workspace = true
16hmac.workspace = true
17pbkdf2.workspace = true
16rand.workspace = true18rand.workspace = true
19sha2.workspace = true
17x25519-dalek.workspace = true20x25519-dalek.workspace = true
1821
modifiedcmds/generator-helper/src/main.rsdiffbeforeafterboth
10 ssh::{ParseRecipientKeyError, Recipient as SshRecipient},10 ssh::{ParseRecipientKeyError, Recipient as SshRecipient},
11};11};
12use anyhow::{Context, Result, anyhow, bail, ensure};12use anyhow::{Context, Result, anyhow, bail, ensure};
13use base64::{Engine as _, engine::general_purpose::STANDARD, write::EncoderWriter};
13use clap::{Parser, ValueEnum};14use clap::{Parser, ValueEnum};
14use ed25519_dalek::SecretKey;15use ed25519_dalek::SecretKey;
15use fleet_shared::SecretData;16use fleet_shared::SecretData;
17use hmac::Mac as _;
16use rand::{18use rand::{
17 Rng as _,19 Rng as _,
18 distr::{Alphanumeric, Distribution, SampleString, Uniform},20 distr::{Alphanumeric, Distribution, SampleString, Uniform},
19 rng,21 rng,
20};22};
23use sha2::Digest as _;
24
25fn gen_password(rng: &mut impl rand::Rng, size: usize, no_symbols: bool) -> String {
26 if no_symbols {
27 Alphanumeric.sample_string(rng, size)
28 } else {
29 const GEN_ASCII_SYMBOLS: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-!\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~";
30 let uniform = Uniform::new(0, GEN_ASCII_SYMBOLS.len()).expect("range is valid");
31 (0..size)
32 .map(|_| uniform.sample(rng))
33 .map(|i| GEN_ASCII_SYMBOLS[i] as char)
34 .collect::<String>()
35 }
36}
2137
22fn write_output_file(out: &str) -> Result<File> {38fn write_output_file(out: &str) -> Result<File> {
23 let file = OpenOptions::new()39 let file = OpenOptions::new()
105 match encoding {121 match encoding {
106 OutputEncoding::Raw => coerce(w),122 OutputEncoding::Raw => coerce(w),
107 OutputEncoding::Base64 => {123 OutputEncoding::Base64 => {
108 use base64::{engine::general_purpose::STANDARD, write::EncoderWriter};
109
110 let writer = EncoderWriter::new(w, &STANDARD);124 let writer = EncoderWriter::new(w, &STANDARD);
111 coerce(writer)125 coerce(writer)
177 #[arg(long, short = 'e', value_enum, default_value_t)]191 #[arg(long, short = 'e', value_enum, default_value_t)]
178 encoding: OutputEncoding,192 encoding: OutputEncoding,
179 },193 },
194 PostgresPassword {
195 #[arg(long, short = 's')]
196 secret: String,
197 #[arg(long, short = 'H')]
198 hash: String,
199 #[arg(long, default_value_t = 24)]
200 size: usize,
201 #[arg(long, default_value_t = 4096)]
202 iterations: u32,
203 #[arg(long, short = 'n')]
204 no_symbols: bool,
205 },
180 Bytes {206 Bytes {
181 #[arg(long, short = 'o')]207 #[arg(long, short = 'o')]
182 output: String,208 output: String,
280 no_symbols,306 no_symbols,
281 output,307 output,
282 encoding,308 encoding,
283 } => {309 } => {
310 ensure!(
311 size >= 6,
312 "misconfiguration? password is shorter than 6 chars"
313 );
314 let recipients = load_identities()?;
315 let out = gen_password(&mut rng, size, no_symbols);
316 write_private(&recipients, &output, out.as_bytes(), encoding)?;
317 }
318 Generate::PostgresPassword {
319 secret,
320 hash,
321 size,
322 iterations,
323 no_symbols,
324 } => {
284 ensure!(325 ensure!(
285 size >= 6,326 size >= 6,
286 "misconfiguration? password is shorter than 6 chars"327 "misconfiguration? password is shorter than 6 chars"
287 );328 );
288 let recipients = load_identities()?;329 let recipients = load_identities()?;
289 let out = if no_symbols {330 let password = gen_password(&mut rng, size, no_symbols);
290 Alphanumeric.sample_string(&mut rng, size)331
291 } else {332 let mut salt = [0u8; 16];
292 // Alphabet of Alphanumberic + symbols333 rng.fill_bytes(&mut salt);
293 const GEN_ASCII_SYMBOLS: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-!\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~";334 let salted = pbkdf2::pbkdf2_hmac_array::<sha2::Sha256, 32>(
335 password.as_bytes(),
336 &salt,
337 iterations,
338 );
339
340 type HmacSha256 = hmac::Hmac<sha2::Sha256>;
294 let uniform =341 let mut mac = <HmacSha256 as hmac::Mac>::new_from_slice(&salted)
295 Uniform::new(0, GEN_ASCII_SYMBOLS.len()).expect("range is valid");342 .expect("HMAC accepts any key length");
343 mac.update(b"Client Key");
344 let client_key = mac.finalize().into_bytes();
345
296 (0..size)346 let mut hasher = sha2::Sha256::new();
297 .map(|_| uniform.sample(&mut rng))347 hasher.update(client_key);
298 .map(|i| GEN_ASCII_SYMBOLS[i] as char)348 let stored_key = hasher.finalize();
349
350 let mut mac = <HmacSha256 as hmac::Mac>::new_from_slice(&salted)
351 .expect("HMAC accepts any key length");
352 mac.update(b"Server Key");
299 .collect::<String>()353 let server_key = mac.finalize().into_bytes();
300 };354
355 let hash_str = format!(
356 "SCRAM-SHA-256${}:{}${}:{}",
357 iterations,
358 STANDARD.encode(salt),
359 STANDARD.encode(stored_key),
360 STANDARD.encode(server_key),
361 );
362
301 write_private(&recipients, &output, out.as_bytes(), encoding)?;363 write_private(
364 &recipients,
365 &secret,
366 password.as_bytes(),
367 OutputEncoding::Raw,
368 )?;
369 write_public(&hash, hash_str.as_bytes(), OutputEncoding::Raw)?;
302 }370 }
303 Generate::Bytes {371 Generate::Bytes {
304 output,372 output,
modifiedcrates/nix-eval/src/lib.rsdiffbeforeafterboth

no syntactic changes

modifiedcrates/nix-eval/src/logging.rsdiffbeforeafterboth

no syntactic changes

modifiedcrates/opentelemetry-exporter-env/src/lib.rsdiffbeforeafterboth

no syntactic changes

modifiedcrates/opentelemetry-exporter-env/src/otlp.rsdiffbeforeafterboth
55 }55 }
56 builder.build()56 builder.build()
57 }57 }
58 OtlpProtocol::HttpProtobuf | OtlpProtocol::HttpJson => {58 OtlpProtocol::HttpProtobuf | OtlpProtocol::HttpJson => <$exporter>::builder()
59 <$exporter>::builder()
60 .with_http()59 .with_http()
61 .with_endpoint(&s.endpoint)60 .with_endpoint(&s.endpoint)
62 .with_headers(to_hashmap(s.headers.as_deref()))61 .with_headers(to_hashmap(s.headers.as_deref()))
63 .with_protocol(s.protocol.into())62 .with_protocol(s.protocol.into())
64 .with_timeout(s.timeout)63 .with_timeout(s.timeout)
65 .build()64 .build(),
66 }
67 }65 }
68 }};66 }};
69}67}
modifiedlib/default.nixdiffbeforeafterboth
88 }88 }
89 );89 );
90
91 /**
92 Generate a random password suitable for PostgreSQL role authentication.
93
94 Options:
95 size: generated password length in ascii characters (bytes).
96 iterations: PBKDF2 iteration count (PG default: 4096).
97 noSymbols: by default, character set includes various special characters ($ , ! + * : ~), and might
98 not be accepted in some contexts, this option switches charset to just [A-Za-z0-9].
99
100 Output:
101 secret: encrypted password.
102 hash: SCRAM-SHA-256 hash of the password to be used by postgres.
103 */
104 # It is not possible to extract it into a scram-sha-256 generic generator because there is no estabilished format
105 # for it, and mongodb for example encodes it differently.
106 mkPostgresPassword =
107 {
108 size ? 32,
109 iterations ? 4096,
110 noSymbols ? false,
111 }:
112 (
113 { mkSecretGenerator }:
114 mkSecretGenerator {
115 script = ''
116 mkdir $out
117 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 );
90128
91 /**129 /**
92 Generate a random ed25519 keypair130 Generate a random ed25519 keypair
306343
307 inherit (secrets)344 inherit (secrets)
308 mkPassword345 mkPassword
346 mkPostgresPassword
309 mkEd25519347 mkEd25519
310 mkX25519348 mkX25519
311 mkRsa349 mkRsa