Skip to content

Commit bed165c

Browse files
Improve TLS support
* Make sure that the 'tls' settings in the config file is not unintentionally drowned out by its respective CLI flag's default * Support CA certificate, client certificate and client private key settings in rabbitmqadmin.conf References #72.
1 parent 9e47650 commit bed165c

File tree

5 files changed

+136
-32
lines changed

5 files changed

+136
-32
lines changed

CHANGELOG.md

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,33 @@
22

33
## v2.7.0 (in development)
44

5+
### Enhancements
6+
7+
* `rabbitmqadmin.conf` now supports more TLS-related settings: `ca_certificate_bundle_path` (corresponds to `--tls-ca-cert-file` on the command line),
8+
`client_certificate_file_path` (corresponds to `--tls-cert-file`), and `client_private_key_file_path` (corresponds to `--tls-key-file`).
9+
10+
As the names suggest, they are used to configure the CA certificate bundle file path, the client certificate file path,
11+
and the client private key file path, respectively:
12+
13+
```toml
14+
[production]
15+
hostname = "(redacted)"
16+
port = 15671
17+
username = "user-efe1f4d763f6"
18+
password = "(redacted)"
19+
tls = true
20+
ca_certificate_bundle_path = "/path/to/ca_certificate.pem"
21+
client_certificate_file_path = "/path/to/client_certificate.pem"
22+
client_private_key_file_path = "/path/to/client_key.pem"
23+
```
24+
25+
To learn more, see [RabbitMQ's TLS guide](https://www.rabbitmq.com/docs/ssl).
26+
527
### Bug Fixes
628

729
* Tool version was unintentionally missing from `-h` output (but present in its long counterpart, `--help`)
8-
30+
* The `tls` setting in `rabbitmqadmin.conf`, a `--use-tls` equivalent, was not respected when connecting to a node
31+
in certain cases
932

1033
## v2.6.0 (Jul 12, 2025)
1134

README.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1004,8 +1004,14 @@ password = "staging-1d20cfbd9d"
10041004
[production]
10051005
hostname = "(redacted)"
10061006
port = 15671
1007+
10071008
username = "user-efe1f4d763f6"
10081009
password = "(redacted)"
1010+
1011+
tls = true
1012+
ca_certificate_bundle_path = "/path/to/ca_certificate.pem"
1013+
client_certificate_file_path = "/path/to/client_certificate.pem"
1014+
client_private_key_file_path = "/path/to/client_key.pem"
10091015
```
10101016

10111017

src/cli.rs

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -462,32 +462,31 @@ pub fn parser(pre_flight_settings: PreFlightSettings) -> Command {
462462
.help("use TLS (HTTPS) for HTTP API requests ")
463463
.env("RABBITMQADMIN_USE_TLS")
464464
.value_parser(value_parser!(bool))
465-
.action(ArgAction::SetTrue)
466-
.requires("tls-ca-cert-file"),
465+
.action(ArgAction::SetTrue),
467466
)
468467
// --tls-ca-cert-file
469468
.arg(
470-
Arg::new("tls-ca-cert-file")
469+
Arg::new("ca_certificate_bundle_path")
471470
.long("tls-ca-cert-file")
472471
.required(false)
473472
.help("Local path to a CA certificate file in the PEM format")
474473
.value_parser(value_parser!(PathBuf)),
475474
)
476475
// --tls-cert-file
477476
.arg(
478-
Arg::new("tls-cert-file")
477+
Arg::new("client_certificate_file_path")
479478
.long("tls-cert-file")
480479
.required(false)
481-
.requires("tls-key-file")
480+
.requires("tls")
482481
.help("Local path to a client certificate file in the PEM format")
483482
.value_parser(value_parser!(PathBuf)),
484483
)
485484
// --tls-key-file
486485
.arg(
487-
Arg::new("tls-key-file")
486+
Arg::new("client_private_key_file_path")
488487
.long("tls-key-file")
489488
.required(false)
490-
.requires("tls-cert-file")
489+
.requires("tls")
491490
.help("Local path to a client private key file in the PEM format")
492491
.value_parser(value_parser!(PathBuf)),
493492
)

src/config.rs

Lines changed: 78 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ pub enum ConfigFileError {
6363
MissingConfigSection(String),
6464
#[error(transparent)]
6565
IoError(#[from] std::io::Error),
66-
#[error("failed to deserialize config file. Make sure it is valid TOML")]
66+
#[error("failed to deserialize config file. Make sure it is valid TOML. Details: {0}")]
6767
DeserializationError(#[from] toml::de::Error),
6868
}
6969

@@ -103,6 +103,13 @@ pub struct SharedSettings {
103103

104104
#[serde(skip_serializing_if = "Option::is_none")]
105105
pub table_style: Option<TableStyle>,
106+
107+
#[serde(skip_serializing_if = "Option::is_none")]
108+
pub ca_certificate_bundle_path: Option<PathBuf>,
109+
#[serde(skip_serializing_if = "Option::is_none")]
110+
pub client_certificate_file_path: Option<PathBuf>,
111+
#[serde(skip_serializing_if = "Option::is_none")]
112+
pub client_private_key_file_path: Option<PathBuf>,
106113
}
107114

108115
impl SharedSettings {
@@ -148,10 +155,9 @@ impl SharedSettings {
148155

149156
pub fn new_with_defaults(cli_args: &ArgMatches, config_file_defaults: &Self) -> Self {
150157
let default_hostname = DEFAULT_HOST.to_string();
151-
let should_use_tls = cli_args
152-
.get_one::<bool>("tls")
153-
.cloned()
154-
.unwrap_or(config_file_defaults.tls);
158+
let should_use_tls =
159+
cli_args.get_one::<bool>("tls").cloned().unwrap_or(false) || config_file_defaults.tls;
160+
155161
let non_interactive = cli_args
156162
.get_one::<bool>("non_interactive")
157163
.cloned()
@@ -207,8 +213,27 @@ impl SharedSettings {
207213
.or(Some(TableStyle::default()))
208214
.unwrap_or_default();
209215

216+
let ca_certificate_bundle_path = cli_args
217+
.get_one::<PathBuf>("ca_certificate_bundle_path")
218+
.cloned()
219+
.or(config_file_defaults.ca_certificate_bundle_path.clone());
220+
221+
let client_certificate_file_path = cli_args
222+
.get_one::<PathBuf>("client_certificate_file_path")
223+
.cloned()
224+
.or(config_file_defaults.client_certificate_file_path.clone());
225+
226+
let client_private_key_file_path = cli_args
227+
.get_one::<PathBuf>("client_private_key_file_path")
228+
.cloned()
229+
.or(config_file_defaults.client_private_key_file_path.clone());
230+
210231
Self {
211232
tls: should_use_tls,
233+
ca_certificate_bundle_path,
234+
client_certificate_file_path,
235+
client_private_key_file_path,
236+
212237
non_interactive,
213238
quiet,
214239
base_uri: None,
@@ -274,8 +299,24 @@ impl SharedSettings {
274299
.or(Some(TableStyle::default()))
275300
.unwrap_or_default();
276301

302+
let ca_certificate_bundle_path = cli_args
303+
.get_one::<PathBuf>("ca_certificate_bundle_path")
304+
.cloned();
305+
306+
let client_certificate_file_path = cli_args
307+
.get_one::<PathBuf>("client_certificate_file_path")
308+
.cloned();
309+
310+
let client_private_key_file_path = cli_args
311+
.get_one::<PathBuf>("client_private_key_file_path")
312+
.cloned();
313+
277314
Self {
278315
tls: should_use_tls,
316+
ca_certificate_bundle_path,
317+
client_certificate_file_path,
318+
client_private_key_file_path,
319+
279320
non_interactive,
280321
quiet,
281322
base_uri: None,
@@ -348,8 +389,24 @@ impl SharedSettings {
348389
.or(Some(TableStyle::default()))
349390
.unwrap_or_default();
350391

392+
let ca_certificate_bundle_path = cli_args
393+
.get_one::<PathBuf>("ca_certificate_bundle_path")
394+
.cloned();
395+
396+
let client_certificate_file_path = cli_args
397+
.get_one::<PathBuf>("client_certificate_file_path")
398+
.cloned();
399+
400+
let client_private_key_file_path = cli_args
401+
.get_one::<PathBuf>("client_private_key_file_path")
402+
.cloned();
403+
351404
Self {
352405
tls: should_use_tls,
406+
ca_certificate_bundle_path,
407+
client_certificate_file_path,
408+
client_private_key_file_path,
409+
353410
non_interactive,
354411
quiet,
355412
base_uri: Some(url.to_string()),
@@ -414,8 +471,24 @@ impl SharedSettings {
414471
.or(Some(TableStyle::default()))
415472
.unwrap_or_default();
416473

474+
let ca_certificate_bundle_path = cli_args
475+
.get_one::<PathBuf>("ca_certificate_bundle_path")
476+
.cloned();
477+
478+
let client_certificate_file_path = cli_args
479+
.get_one::<PathBuf>("client_certificate_file_path")
480+
.cloned();
481+
482+
let client_private_key_file_path = cli_args
483+
.get_one::<PathBuf>("client_private_key_file_path")
484+
.cloned();
485+
417486
Self {
418487
tls: should_use_tls,
488+
ca_certificate_bundle_path,
489+
client_certificate_file_path,
490+
client_private_key_file_path,
491+
419492
non_interactive,
420493
quiet,
421494
base_uri: Some(url.to_string()),

src/main.rs

Lines changed: 22 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ fn main() {
6666

6767
match configure_http_api_client(&cli, &common_settings, &endpoint.clone()) {
6868
Ok(client) => {
69-
let exit_code = dispatch_command(&cli, client, &common_settings, endpoint);
69+
let exit_code = dispatch_command(&cli, client, &common_settings);
7070
process::exit(exit_code.into())
7171
}
7272
Err(err) => {
@@ -116,13 +116,13 @@ fn resolve_run_configuration(cli: &ArgMatches) -> (SharedSettings, String) {
116116

117117
fn configure_http_api_client<'a>(
118118
cli: &'a ArgMatches,
119-
common_settings: &'a SharedSettings,
119+
merged_settings: &'a SharedSettings,
120120
endpoint: &'a str,
121121
) -> Result<APIClient, CommandRunError> {
122-
let httpc = build_http_client(cli, common_settings)?;
122+
let httpc = build_http_client(cli, merged_settings)?;
123123
// Due to how SharedSettings are computed, these should safe to unwrap()
124-
let username = common_settings.username.clone().unwrap();
125-
let password = common_settings.password.clone().unwrap();
124+
let username = merged_settings.username.clone().unwrap();
125+
let password = merged_settings.password.clone().unwrap();
126126
let client = build_rabbitmq_http_api_client(
127127
httpc,
128128
endpoint.to_owned(),
@@ -135,16 +135,15 @@ fn configure_http_api_client<'a>(
135135
fn dispatch_command(
136136
cli: &ArgMatches,
137137
client: APIClient,
138-
common_settings: &SharedSettings,
139-
endpoint: String,
138+
merged_settings: &SharedSettings,
140139
) -> ExitCode {
141140
if let Some((first_level, first_level_args)) = cli.subcommand() {
142141
if let Some((second_level, second_level_args)) = first_level_args.subcommand() {
143142
// this is a Tanzu RabbitMQ-specific command, these are grouped under "tanzu"
144143
if first_level == TANZU_COMMAND_PREFIX {
145144
if let Some((third_level, third_level_args)) = second_level_args.subcommand() {
146145
let pair = (second_level, third_level);
147-
let mut res_handler = ResultHandler::new(common_settings, second_level_args);
146+
let mut res_handler = ResultHandler::new(merged_settings, second_level_args);
148147
return dispatch_tanzu_subcommand(
149148
pair,
150149
third_level_args,
@@ -155,13 +154,13 @@ fn dispatch_command(
155154
} else {
156155
// this is a common (OSS and Tanzu) command
157156
let pair = (first_level, second_level);
158-
let vhost = virtual_host(common_settings, second_level_args);
159-
let mut res_handler = ResultHandler::new(common_settings, second_level_args);
157+
let vhost = virtual_host(merged_settings, second_level_args);
158+
let mut res_handler = ResultHandler::new(merged_settings, second_level_args);
160159
return dispatch_common_subcommand(
161160
pair,
162161
second_level_args,
163162
client,
164-
endpoint,
163+
merged_settings.endpoint(),
165164
vhost,
166165
&mut res_handler,
167166
);
@@ -192,12 +191,12 @@ fn build_http_client(
192191
if should_use_tls(common_settings) {
193192
let _ = CryptoProvider::install_default(rustls::crypto::aws_lc_rs::default_provider());
194193

195-
let ca_cert_pem_file = cli.get_one::<PathBuf>("tls-ca-cert-file");
196-
197-
let maybe_client_cert_pem_file = cli.get_one::<PathBuf>("tls-cert-file");
198-
let maybe_client_key_pem_file = cli.get_one::<PathBuf>("tls-key-file");
194+
let ca_cert_pem_file = common_settings.ca_certificate_bundle_path.clone();
195+
let maybe_client_cert_pem_file = common_settings.client_certificate_file_path.clone();
196+
let maybe_client_key_pem_file = common_settings.client_private_key_file_path.clone();
199197

200198
let ca_certs = ca_cert_pem_file
199+
.clone()
201200
.map(|path| load_certs(&path.to_string_lossy()))
202201
.unwrap()?;
203202

@@ -209,15 +208,18 @@ fn build_http_client(
209208
.tls_info(true)
210209
.tls_sni(true)
211210
.min_tls_version(reqwest::tls::Version::TLS_1_2)
211+
.tls_built_in_native_certs(true)
212212
.tls_built_in_root_certs(true)
213213
.danger_accept_invalid_certs(disable_peer_verification)
214214
.danger_accept_invalid_hostnames(disable_peer_verification);
215215

216-
// --tls-ca-cert-file
216+
// local certificate store
217217
let mut store = rustls::RootCertStore::empty();
218+
218219
for c in ca_certs {
219220
store.add(c).map_err(|err| {
220-
let readable_path = maybe_client_cert_pem_file
221+
let readable_path = ca_cert_pem_file
222+
.clone()
221223
.unwrap()
222224
.to_string_lossy()
223225
.to_string();
@@ -230,15 +232,16 @@ fn build_http_client(
230232

231233
// --tls-cert-file, --tls-key-file
232234
if maybe_client_cert_pem_file.is_some() && maybe_client_key_pem_file.is_some() {
233-
let client_cert_pem_file = maybe_client_cert_pem_file.unwrap();
234-
let client_key_pem_file = maybe_client_key_pem_file.unwrap();
235+
let client_cert_pem_file = maybe_client_cert_pem_file.clone().unwrap();
236+
let client_key_pem_file = maybe_client_key_pem_file.clone().unwrap();
235237

236238
let client_cert = fs::read(client_cert_pem_file)?;
237239
let client_key = fs::read(client_key_pem_file)?;
238240

239241
let concatenated = [&client_cert[..], &client_key[..]].concat();
240242
let client_id = Identity::from_pem(&concatenated).map_err(|err| {
241243
let readable_path = maybe_client_key_pem_file
244+
.clone()
242245
.unwrap()
243246
.to_string_lossy()
244247
.to_string();

0 commit comments

Comments
 (0)