1 /*
2 * Copyright 2025-2026 the Pacemaker project contributors
3 *
4 * The version control history for this file may have further details.
5 *
6 * This source code is licensed under the GNU General Public License version 2
7 * or later (GPLv2+) WITHOUT ANY WARRANTY.
8 */
9
10 #include <crm_internal.h>
11
12 #include <errno.h> // EINVAL, ENODEV, ENOENT, ENOTCONN
13 #include <locale.h> // setlocale
14 #include <stdbool.h>
15 #include <stddef.h>
16 #include <stdlib.h> // setenv, unsetenv
17 #include <syslog.h> // LOG_DEBUG
18 #include <sys/stat.h> // umask, S_IRGRP, S_IROTH, ...
19 #include <sys/wait.h> // WEXITSTATUS
20 #include <unistd.h> // geteuid
21
22 #include <glib.h>
23 #include <libxml/tree.h> // xmlNode, xmlNodeGetContent
24 #include <libxml/xmlmemory.h> // xmlFree
25 #include <libxml/xmlstring.h> // xmlChar
26
27 #include <crm/cib/internal.h> // cib__clean_up_connection, cib__signon_query
28 #include <crm/common/internal.h>
29 #include <crm/common/results.h>
30 #include <crm/common/xml.h> // crm_element_value, PCMK_XA_*
31
32 #include <pacemaker-internal.h> // pcmk__query_node_name
33
34 #define SUMMARY "cibsecret - manage sensitive information in Pacemaker CIB"
35
36 #define LRM_MAGIC "lrm://"
37 #define SSH_OPTS "-o StrictHostKeyChecking=no"
38
39 static gchar **remainder = NULL;
40 static gboolean no_cib = FALSE;
41
42 static GOptionEntry entries[] = {
43 { "no-cib", 'C', G_OPTION_FLAG_NONE, G_OPTION_ARG_NONE, &no_cib,
44 "Don't read or write the CIB",
45 NULL },
46
47 { G_OPTION_REMAINING, 0, G_OPTION_FLAG_NONE, G_OPTION_ARG_STRING_ARRAY, &remainder,
48 NULL,
49 NULL },
50
51 { NULL }
52 };
53
54 /*!
55 * \internal
56 * \brief A function for running a command on remote hosts
57 *
58 * \param[in,out] out Output object
59 * \param[in] nodes A list of remote hosts
60 * \param[in] argv The list of command line arguments to run
61 *
62 * \return Standard Pacemaker return code
63 *
64 * \note On error, \p out->err() will be called to record stderr of the process
65 */
66 typedef int (*rsh_fn_t)(pcmk__output_t *out, char **nodes, char **argv);
67
68 /*!
69 * \internal
70 * \brief A function for copying a file to remote hosts
71 *
72 * \param[in,out] out Output object
73 * \param[in] nodes A list of remote hosts
74 * \param[in] to The destination path on the remote host
75 * \param[in] from The local file (or directory) to copy
76 *
77 * \return Standard Pacemaker return code
78 *
79 * \note \p from can either be a single file or a directory. It cannot be
80 * be multiple files in a space-separated string. If multiple files need
81 * to be copied, either copy the entire directory at once or call this
82 * function multiple times.
83 *
84 * \note On error, \p out->err() will be called to record stderr of the process
85 */
86 typedef int (*rcp_fn_t)(pcmk__output_t *out, char **nodes, const char *to,
87 const char *from);
88
89 struct subcommand_entry {
90 const char *name;
91 int args;
92 const char *usage;
93 bool requires_cib;
94 int (*handler)(pcmk__output_t *out, rsh_fn_t rsh_fn, rcp_fn_t rcp_fn);
95 };
96
97 /*!
98 * \internal
99 * \brief Run a command line process
100 *
101 * \param[in,out] out Output object
102 * \param[in] argv The list of command line arguments to run
103 * \param[out] standard_out If not NULL, where to save stdout of the process
104 *
105 * \return Standard Pacemaker return code
106 *
107 * \note On error, \p out->err() will be called to record stderr of the process
108 */
109 static int
110 run_cmdline(pcmk__output_t *out, char **argv, char **standard_out)
111 {
112 int rc = pcmk_rc_ok;
113 gboolean success = FALSE;
114 GError *error = NULL;
115 gchar *sout = NULL;
116 gchar *serr = NULL;
117 int status;
118
119 /* A failure here is a failure starting the program (for example, it doesn't
120 * exist on the $PATH), not that it ran but exited with an error code.
121 */
122 success = g_spawn_sync(NULL, argv, NULL, G_SPAWN_SEARCH_PATH, NULL, NULL,
123 &sout, &serr, &status, &error);
124 if (!success) {
125 out->err(out, "%s", error->message);
126 rc = pcmk_rc_error;
127 goto done;
128 }
129
130 /* A failure here indicates that the program exited with a non-zero exit
131 * code or due to a fatal signal.
132 */
133 /* @FIXME @COMPAT g_spawn_check_exit_status is deprecated as of glib 2.70
134 * and is replaced with g_spawn_check_wait_status.
135 */
136 success = g_spawn_check_exit_status(status, &error);
137
138 if (!success) {
139 out->err(out, "%s", error->message);
140 out->subprocess_output(out, WEXITSTATUS(status), sout, serr);
141 rc = pcmk_rc_error;
142 }
143
144 done:
145 pcmk__str_update(standard_out, sout);
146
147 g_free(sout);
148 g_free(serr);
149 g_clear_error(&error);
150
151 return rc;
152 }
153
154 static int
155 pssh(pcmk__output_t *out, char **nodes, char **cmd)
156 {
157 int rc = pcmk_rc_ok;
158 gchar *hosts = g_strjoinv(" ", nodes);
159
160 int cmd_len = g_strv_length(cmd);
161 // Length of our args, plus the length of cmd, plus 1 for terminating NULL
162 int total_len = 7 + cmd_len + 1;
163 gchar **argv = g_new0(gchar *, total_len);
164
165 argv[0] = g_strdup("pssh");
166 argv[1] = g_strdup("-i");
167 argv[2] = g_strdup("-H");
168 argv[3] = hosts;
169 argv[4] = g_strdup("-O");
170 argv[5] = g_strdup("StrictHostKeyChecking=no");
171 argv[6] = g_strdup("--");
172
173 // Append passed in args to argv
174 for (int i = 0; i < cmd_len; i++) {
175 argv[i+7] = g_strdup(cmd[i]);
176 }
177
178 argv[total_len-1] = NULL;
179
180 rc = run_cmdline(out, argv, NULL);
181 g_strfreev(argv);
182 return rc;
183 }
184
185 static int
186 pdsh(pcmk__output_t *out, char **nodes, char **cmd)
187 {
188 int rc = pcmk_rc_ok;
189 gchar *hosts = g_strjoinv(",", nodes);
190
191 int cmd_len = g_strv_length(cmd);
192 int total_len = 4 + cmd_len + 1;
193 gchar **argv = g_new0(gchar *, total_len);
194
195 argv[0] = g_strdup("pdsh");
196 argv[1] = g_strdup("-w");
197 argv[2] = hosts;
198 argv[3] = g_strdup("--");
199
200 for (int i = 0; i < cmd_len; i++) {
201 argv[i+4] = g_strdup(cmd[i]);
202 }
203
204 argv[total_len-1] = NULL;
205
206 setenv("PDSH_SSH_ARGS_APPEND", SSH_OPTS, 1);
207 rc = run_cmdline(out, argv, NULL);
208 unsetenv("PDSH_SSH_ARGS_APPEND");
209
210 g_strfreev(argv);
211 return rc;
212 }
213
214 static int
215 ssh(pcmk__output_t *out, char **nodes, char **cmd)
216 {
217 int cmd_len = g_strv_length(cmd);
218 int total_len = 5 + cmd_len + 1;
219 int rc = pcmk_rc_ok;
220
221 for (char **node = nodes; *node != NULL; node++) {
222 gchar **argv = g_new0(gchar *, total_len);
223
224 argv[0] = g_strdup("ssh");
225 argv[1] = g_strdup("-o");
226 argv[2] = g_strdup("StrictHostKeyChecking=no");
227 argv[3] = g_strdup(*node);
228 argv[4] = g_strdup("--");
229
230 for (int i = 0; i < cmd_len; i++) {
231 argv[i+5] = g_strdup(cmd[i]);
232 }
233
234 argv[total_len-1] = NULL;
235
236 rc = run_cmdline(out, argv, NULL);
237 g_strfreev(argv);
238
239 if (rc != pcmk_rc_ok) {
240 return rc;
241 }
242 }
243
244 return rc;
245 }
246
247 static int
248 pscp(pcmk__output_t *out, char **nodes, const char *to, const char *from)
249 {
250 int rc = pcmk_rc_ok;
251 gchar **argv = g_new0(gchar *, 11);
252 gchar *hosts = g_strjoinv(" ", nodes);
253
254 argv[0] = g_strdup("pscp.pssh");
255 argv[1] = g_strdup("-H");
256 argv[2] = hosts;
257 argv[3] = g_strdup("-x");
258 argv[4] = g_strdup("-pr");
259 argv[5] = g_strdup("-O");
260 argv[6] = g_strdup("StrictHostKeyChecking=no");
261 argv[7] = g_strdup("--");
262 argv[8] = g_strdup(from);
263 argv[9] = g_strdup(to);
264 argv[10] = NULL;
265
266 rc = run_cmdline(out, argv, NULL);
267
268 g_strfreev(argv);
269 return rc;
270 }
271
272 static int
273 pdcp(pcmk__output_t *out, char **nodes, const char *to, const char *from)
274 {
275 int rc = pcmk_rc_ok;
276 gchar **argv = g_new0(gchar *, 8);
277 gchar *hosts = g_strjoinv(",", nodes);
278
279 argv[0] = g_strdup("pdcp");
280 argv[1] = g_strdup("-pr");
281 argv[2] = g_strdup("-w");
282 argv[3] = hosts;
283 argv[4] = g_strdup("--");
284 argv[5] = g_strdup(from);
285 argv[6] = g_strdup(to);
286 argv[7] = NULL;
287
288 setenv("PDSH_SSH_ARGS_APPEND", SSH_OPTS, 1);
289 rc = run_cmdline(out, argv, NULL);
290 unsetenv("PDSH_SSH_ARGS_APPEND");
291
292 g_strfreev(argv);
293 return rc;
294 }
295
296 static int
297 scp(pcmk__output_t *out, char **nodes, const char *to, const char *from)
298 {
299 int rc = pcmk_rc_ok;
300
301 for (char **node = nodes; *node != NULL; node++) {
302 gchar **argv = g_new0(gchar *, 7);
303
304 argv[0] = g_strdup("scp");
305 argv[1] = g_strdup("-pqr");
306 argv[2] = g_strdup("-o");
307 argv[3] = g_strdup("StrictHostKeyChecking=no");
308 argv[4] = g_strdup(from);
309 argv[5] = g_strdup_printf("%s:%s", *node, to);
310 argv[6] = NULL;
311
312 rc = run_cmdline(out, argv, NULL);
313 g_strfreev(argv);
314
315 if (rc != pcmk_rc_ok) {
316 return rc;
317 }
318 }
319
320 return rc;
321 }
322
323 static gchar **
324 reachable_hosts(pcmk__output_t *out, GList *all)
325 {
326 GPtrArray *reachable = NULL;
327 gchar *path = NULL;
328
329 path = g_find_program_in_path("fping");
330
331 reachable = g_ptr_array_new();
332
333 if ((path == NULL) || (geteuid() != 0)) {
334 for (GList *host = all; host != NULL; host = host->next) {
335 int rc = pcmk_rc_ok;
336 gchar **argv = g_new0(gchar *, 6);
337
338 argv[0] = g_strdup("ping");
339 argv[1] = g_strdup("-c");
340 argv[2] = g_strdup("2");
341 argv[3] = g_strdup("-q");
342 argv[4] = g_strdup(host->data);
343 argv[5] = NULL;
344
345 rc = run_cmdline(out, argv, NULL);
346 g_strfreev(argv);
347
348 if (rc == pcmk_rc_ok) {
349 g_ptr_array_add(reachable, g_strdup(host->data));
350 }
351 }
352
353 } else {
354 GString *all_str = g_string_sized_new(64);
355 gchar **parts = NULL;
356 gchar **argv = g_new0(gchar *, 5);
357 char *standard_out = NULL;
358
359 for (GList *host = all; host != NULL; host = host->next) {
360 pcmk__add_word(&all_str, 64, host->data);
361 }
362
363 argv[0] = g_strdup("fping");
364 argv[1] = g_strdup("-a");
365 argv[2] = g_strdup("-q");
366 argv[3] = g_strdup(all_str->str);
367 argv[4] = NULL;
368
369 run_cmdline(out, argv, &standard_out);
370 g_strfreev(argv);
371
372 parts = g_strsplit(standard_out, "\n", 0);
373 for (gchar **p = parts; *p != NULL; p++) {
374 if (pcmk__str_empty(*p)) {
375 continue;
376 }
377
378 g_ptr_array_add(reachable, g_strdup(*p));
379 }
380
381 free(standard_out);
382 g_string_free(all_str, TRUE);
383 g_strfreev(parts);
384 }
385
386 g_free(path);
387 g_ptr_array_add(reachable, NULL);
388 return (char **) g_ptr_array_free(reachable, FALSE);
389 }
390
391 struct node_data {
392 pcmk__output_t *out;
393 char *local_node;
394 const char *field;
395 GList *all_nodes;
396 };
397
398 static void
399 node_iter_helper(xmlNode *result, void *user_data)
400 {
401 struct node_data *data = user_data;
402 const char *uname = pcmk__xe_get(result, PCMK_XA_UNAME);
403 const char *id = pcmk__xe_get(result, data->field);
404 const char *name = pcmk__s(uname, id);
405
406 /* Filter out the local node */
407 if (pcmk__str_eq(name, data->local_node, pcmk__str_null_matches)) {
408 return;
409 }
410
411 data->all_nodes = g_list_append(data->all_nodes, g_strdup(name));
412 }
413
414 static gchar **
415 get_live_peers(pcmk__output_t *out)
416 {
417 int rc = pcmk_rc_ok;
418 xmlNode *xml_node = NULL;
419 gchar **reachable = NULL;
420 cib_t *cib = NULL;
421
422 struct node_data nd = {
423 .out = out,
424 .local_node = NULL,
425 .all_nodes = NULL
426 };
427
428 rc = cib__signon_query(out, &cib, &xml_node);
429 if (rc != pcmk_rc_ok) {
430 out->err(out, "Could not get list of cluster nodes");
431 goto done;
432 }
433
434 /* Get the local node name if possible. */
435 if (cib->variant != cib_file) {
436 rc = pcmk__query_node_name(out, 0, &(nd.local_node), 0);
437 if (rc != pcmk_rc_ok) {
438 out->err(out, "Could not get local node name");
439 goto done;
440 }
441 }
442
443 /* Filter out the local node from the list of all node names. If we don't
444 * have a local node (for instance, because CIB_file is set) then we'll
445 * just use the list of all node names instead.
446 */
447 nd.field = PCMK_XA_ID;
448 pcmk__xpath_foreach_result(xml_node->doc, PCMK__XP_MEMBER_NODE_CONFIG,
449 node_iter_helper, &nd);
450 nd.field = PCMK_XA_VALUE;
451 pcmk__xpath_foreach_result(xml_node->doc, PCMK__XP_GUEST_NODE_CONFIG,
452 node_iter_helper, &nd);
453 nd.field = PCMK_XA_ID;
454 pcmk__xpath_foreach_result(xml_node->doc, PCMK__XP_REMOTE_NODE_CONFIG,
455 node_iter_helper, &nd);
456
457 if (nd.all_nodes == NULL) {
458 goto done;
459 }
460
461 /* Get a list of all nodes that respond to pings */
462 reachable = reachable_hosts(out, nd.all_nodes);
463
464 /* Warn the user about any that didn't respond to pings */
465 for (const GList *iter = nd.all_nodes; iter != NULL; iter = iter->next) {
466 const char *node_name = iter->data;
467
468 if (!pcmk__g_strv_contains(reachable, node_name)) {
469 out->info(out, "Node %s is down - you'll need to update it "
470 "with `cibsecret sync` later", node_name);
471 }
472 }
473
474 done:
475 cib__clean_up_connection(&cib);
476
477 free(nd.local_node);
478 free(xml_node);
479 g_list_free_full(nd.all_nodes, g_free);
480
481 return reachable;
482 }
483
484 static int
485 sync_one_file(pcmk__output_t *out, rsh_fn_t rsh_fn, rcp_fn_t rcp_fn,
486 const char *path)
487 {
488 int rc = pcmk_rc_ok;
489 gchar *dirname = NULL;
490 gchar **peers = get_live_peers(out);
491 gchar *peer_str = NULL;
492 gchar **argv = g_new0(gchar *, 4);
493
494 if (peers == NULL) {
495 g_strfreev(argv);
496 return pcmk_rc_ok;
497 }
498
499 peer_str = g_strjoinv(" ", peers);
500
501 if (pcmk__str_eq(remainder[0], "delete", pcmk__str_none)) {
502 out->info(out, "Deleting %s from %s ...", path, peer_str);
503 } else {
504 out->info(out, "Syncing %s to %s ...", path, peer_str);
505 }
506
507 dirname = g_path_get_dirname(path);
508
509 argv[0] = g_strdup("mkdir");
510 argv[1] = g_strdup("-p");
511 argv[2] = g_strdup(dirname);
512 argv[3] = NULL;
513
514 rc = rsh_fn(out, peers, argv);
515
516 if (rc != pcmk_rc_ok) {
517 goto done;
518 }
519
520 if (g_file_test(path, G_FILE_TEST_EXISTS | G_FILE_TEST_IS_REGULAR)) {
521 char *sign_path = NULL;
522
523 rc = rcp_fn(out, peers, dirname, path);
524 if (rc != pcmk_rc_ok) {
525 goto done;
526 }
527
528 sign_path = pcmk__assert_asprintf("%s.sign", path);
529 rc = rcp_fn(out, peers, dirname, sign_path);
530 free(sign_path);
531
532 } else {
533 g_strfreev(argv);
534 argv = g_new0(gchar *, 5);
535
536 argv[0] = g_strdup("rm");
537 argv[1] = g_strdup("-f");
538 argv[2] = g_strdup(path);
539 argv[3] = g_strdup_printf("%s.sign", path);
540 argv[4] = NULL;
541
542 rc = rsh_fn(out, peers, argv);
543 }
544
545 done:
546 g_free(dirname);
547 g_strfreev(argv);
548 g_strfreev(peers);
549 g_free(peer_str);
550 return rc;
551 }
552
553 static int
554 check_cib_rsc(pcmk__output_t *out, const char *rsc)
555 {
556 int rc = pcmk_rc_ok;
557 gchar **argv = g_new0(gchar *, 5);
558
559 if (no_cib) {
560 goto done;
561 }
562
563 argv[0] = g_strdup("crm_resource");
564 argv[1] = g_strdup("-r");
565 argv[2] = g_strdup(rsc);
566 argv[3] = g_strdup("-W");
567 argv[4] = NULL;
568
569 rc = run_cmdline(out, argv, NULL);
570
571 done:
572 g_strfreev(argv);
573 return rc;
574 }
575
576 static bool
577 is_secret(const char *s)
578 {
579 if (no_cib) {
580 /* Assume that the secret is in the CIB if we can't connect */
581 return true;
582 }
583
584 return pcmk__str_eq(s, LRM_MAGIC, pcmk__str_none);
585 }
586
587 static char *
588 get_cib_param(pcmk__output_t *out, const char *rsc, const char *param)
589 {
590 int rc = pcmk_rc_ok;
591 gchar **argv = g_new0(gchar *, 7);
592 char *standard_out = NULL;
593 char *retval = NULL;
594 char *xpath = NULL;
595 xmlNode *xml = NULL;
596 xmlNode *node = NULL;
597 xmlChar *content = NULL;
598
599 if (no_cib) {
600 g_strfreev(argv);
601 return NULL;
602 }
603
604 argv[0] = g_strdup("crm_resource");
605 argv[1] = g_strdup("-r");
606 argv[2] = g_strdup(rsc);
607 argv[3] = g_strdup("-g");
608 argv[4] = g_strdup(param);
609 argv[5] = g_strdup("--output-as=xml");
610 argv[6] = NULL;
611
612 rc = run_cmdline(out, argv, &standard_out);
613 g_strfreev(argv);
614
615 if (rc != pcmk_rc_ok) {
616 goto done;
617 }
618
619 xml = pcmk__xml_parse(standard_out);
620
621 if (xml == NULL) {
622 goto done;
623 }
624
625 xpath = pcmk__assert_asprintf("//" PCMK_XE_ITEM "[@" PCMK_XA_NAME "='%s']",
626 param);
627 node = pcmk__xpath_find_one(xml->doc, xpath, LOG_DEBUG);
628
629 if (node == NULL) {
630 goto done;
631 }
632
633 content = xmlNodeGetContent(node);
634
635 if (content != NULL) {
636 retval = pcmk__str_copy((char *) content);
637 xmlFree(content);
638 }
639
640 done:
641 free(standard_out);
642 free(xpath);
643 pcmk__xml_free(xml);
644 return retval;
645 }
646
647 static int
648 remove_cib_param(pcmk__output_t *out, const char *rsc, const char *param)
649 {
650 int rc = pcmk_rc_ok;
651 gchar **argv = g_new0(gchar *, 6);
652
653 if (no_cib) {
654 goto done;
655 }
656
657 argv[0] = g_strdup("crm_resource");
658 argv[1] = g_strdup("-r");
659 argv[2] = g_strdup(rsc);
660 argv[3] = g_strdup("-d");
661 argv[4] = g_strdup(param);
662 argv[5] = NULL;
663
664 rc = run_cmdline(out, argv, NULL);
665
666 done:
667 g_strfreev(argv);
668 return rc;
669 }
670
671 static int
672 set_cib_param(pcmk__output_t *out, const char *rsc, const char *param,
673 const char *value)
674 {
675 int rc = pcmk_rc_ok;
676 gchar **argv = g_new0(gchar *, 8);
677
678 if (no_cib) {
679 goto done;
680 }
681
682 argv[0] = g_strdup("crm_resource");
683 argv[1] = g_strdup("-r");
684 argv[2] = g_strdup(rsc);
685 argv[3] = g_strdup("-p");
686 argv[4] = g_strdup(param);
687 argv[5] = g_strdup("-v");
688 argv[6] = g_strdup(value);
689 argv[7] = NULL;
690
691 rc = run_cmdline(out, argv, NULL);
692
693 done:
694 g_strfreev(argv);
695 return rc;
696 }
697
698 static char *
699 local_files_get(const char *rsc, const char *param)
700 {
701 char *retval = NULL;
702 char *lf_file = NULL;
703 gchar *contents = NULL;
704
705 lf_file = pcmk__assert_asprintf(PCMK__CIB_SECRETS_DIR "/%s/%s", rsc, param);
706 if (g_file_get_contents(lf_file, &contents, NULL, NULL)) {
707 contents = g_strchomp(contents);
708 retval = pcmk__str_copy(contents);
709 g_free(contents);
710 }
711
712 free(lf_file);
713 return retval;
714 }
715
716 static char *
717 local_files_getsum(const char *rsc, const char *param)
718 {
719 char *retval = NULL;
720 char *lf_file = NULL;
721 gchar *contents = NULL;
722
723 lf_file = pcmk__assert_asprintf(PCMK__CIB_SECRETS_DIR "/%s/%s.sign", rsc,
724 param);
725 if (g_file_get_contents(lf_file, &contents, NULL, NULL)) {
726 contents = g_strchomp(contents);
727 retval = pcmk__str_copy(contents);
728 g_free(contents);
729 }
730
731 free(lf_file);
732 return retval;
733 }
734
735 static int
736 local_files_remove(pcmk__output_t *out, rsh_fn_t rsh_fn, rcp_fn_t rcp_fn,
737 const char *rsc, const char *param)
738 {
739 int rc = pcmk_rc_ok;
740 char *lf_file = NULL;
741 gchar **argv = g_new0(gchar *, 5);
742
743 lf_file = pcmk__assert_asprintf(PCMK__CIB_SECRETS_DIR "/%s/%s", rsc, param);
744
745 argv[0] = g_strdup("rm");
746 argv[1] = g_strdup("-f");
747 argv[2] = g_strdup(lf_file);
748 argv[3] = g_strdup_printf("%s.sign", lf_file);
749 argv[4] = NULL;
750
751 rc = run_cmdline(out, argv, NULL);
752
753 if (rc == pcmk_rc_ok) {
754 rc = sync_one_file(out, rsh_fn, rcp_fn, lf_file);
755 }
756
757 free(lf_file);
758 g_strfreev(argv);
759 return rc;
760 }
761
762 static int
763 local_files_set(pcmk__output_t *out, rsh_fn_t rsh_fn, rcp_fn_t rcp_fn,
764 const char *rsc, const char *param, const char *value)
765 {
766 char *contents = NULL;
767 char *lf_dir = NULL;
768 char *lf_file = NULL;
769 char *sign_file = NULL;
770 char *calc_sum = NULL;
771 int rc = pcmk_rc_ok;
772
773 lf_dir = pcmk__assert_asprintf(PCMK__CIB_SECRETS_DIR "/%s", rsc);
774
775 if (g_mkdir_with_parents(lf_dir, 0700) != 0) {
776 rc = errno;
777 out->err(out, "Could not create directory %s: %s", lf_dir,
778 pcmk_rc_str(rc));
779 goto done;
780 }
781
782 lf_file = pcmk__assert_asprintf("%s/%s", lf_dir, param);
783 contents = pcmk__assert_asprintf("%s\n", value);
784 if (!g_file_set_contents(lf_file, contents, -1, NULL)) {
785 rc = EIO;
786 out->err(out, "Could not create file %s: %s", lf_file,
787 pcmk_rc_str(rc));
788 goto done;
789 }
790
791 free(contents);
792
793 sign_file = pcmk__assert_asprintf("%s/%s.sign", lf_dir, param);
794 calc_sum = pcmk__md5sum(value);
795 contents = pcmk__assert_asprintf("%s\n", calc_sum);
796
797 if (!g_file_set_contents(sign_file, contents, -1, NULL)) {
798 rc = EIO;
799 out->err(out, "Could not create file %s: %s", sign_file,
800 pcmk_rc_str(rc));
801 goto done;
802 }
803
804 rc = sync_one_file(out, rsh_fn, rcp_fn, lf_file);
805
806 done:
807 free(contents);
808 free(calc_sum);
809 free(sign_file);
810 free(lf_dir);
811 free(lf_file);
812 return rc;
813 }
814
815 static int
816 subcommand_check(pcmk__output_t *out, rsh_fn_t rsh_fn, rcp_fn_t rcp_fn)
817 {
818 int rc = pcmk_rc_ok;
819 const char *rsc = remainder[1];
820 const char *param = remainder[2];
821 char *value = NULL;
822 char *calc_sum = NULL;
823 char *local_sum = NULL;
824 char *local_value = NULL;
825
826 if (check_cib_rsc(out, rsc) != pcmk_rc_ok) {
827 rc = ENODEV;
828 goto done;
829 }
830
831 value = get_cib_param(out, rsc, param);
832 if ((value == NULL) || !is_secret(value)) {
833 out->err(out, "Resource %s parameter %s not set as secret, nothing to check",
834 rsc, param);
835
836 /* I don't like this error code, but (1) it maps to CRM_EX_CONFIG which
837 * is what the old cibsecret.in would return in this case, and (2) we
838 * return it all over the place for a variety of CIB checking errors.
839 */
840 rc = pcmk_rc_unpack_error;
841 goto done;
842 }
843
844 local_sum = local_files_getsum(rsc, param);
845 if (local_sum == NULL) {
846 out->err(out, "No checksum for resource %s parameter %s", rsc, param);
847 rc = ENOENT;
848 goto done;
849 }
850
851 local_value = local_files_get(rsc, param);
852 if (local_value != NULL) {
853 calc_sum = pcmk__md5sum(local_value);
854 }
855
856 if ((local_value == NULL) || !pcmk__str_eq(calc_sum, local_sum, pcmk__str_none)) {
857 out->err(out, "Checksum mismatch for resource %s parameter %s", rsc, param);
858 rc = pcmk_rc_digest_mismatch;
859 }
860
861 done:
862 free(local_sum);
863 free(local_value);
864 free(calc_sum);
865 free(value);
866 return rc;
867 }
868
869 static int
870 subcommand_delete(pcmk__output_t *out, rsh_fn_t rsh_fn, rcp_fn_t rcp_fn)
871 {
872 int rc = pcmk_rc_ok;
873 const char *rsc = remainder[1];
874 const char *param = remainder[2];
875
876 if (check_cib_rsc(out, rsc) != pcmk_rc_ok) {
877 return ENODEV;
878 }
879
880 rc = local_files_remove(out, rsh_fn, rcp_fn, rsc, param);
881 if (rc != pcmk_rc_ok) {
882 return rc;
883 }
884
885 return remove_cib_param(out, rsc, param);
886 }
887
888 static int
889 subcommand_get(pcmk__output_t *out, rsh_fn_t rsh_fn, rcp_fn_t rcp_fn)
890 {
891 int rc = subcommand_check(out, rsh_fn, rcp_fn);
892 char *value = NULL;
893 const char *rsc = remainder[1];
894 const char *param = remainder[2];
895
896 if (rc != pcmk_rc_ok) {
897 return rc;
898 }
899
900 value = local_files_get(rsc, param);
901 pcmk__assert(value != NULL);
902 out->info(out, "%s", value);
903
904 free(value);
905 return pcmk_rc_ok;
906 }
907
908 /* The previous shell implementation of cibsecret allowed passing the value
909 * to set (what would be remainder[3] here) via stdin, which we do not support
910 * here at the moment.
911 */
912 static int
913 subcommand_set(pcmk__output_t *out, rsh_fn_t rsh_fn, rcp_fn_t rcp_fn)
914 {
915 int rc = pcmk_rc_ok;
916 const char *rsc = remainder[1];
917 const char *param = remainder[2];
918 const char *value = remainder[3];
919 char *current = NULL;
920
921 if (check_cib_rsc(out, rsc) != pcmk_rc_ok) {
922 rc = ENODEV;
923 goto done;
924 }
925
926 current = get_cib_param(out, rsc, param);
927 if ((current != NULL) && !pcmk__str_any_of(current, LRM_MAGIC, value, NULL)) {
928 out->err(out, "CIB value <%s> different for %s rsc parameter %s; please "
929 "delete it first", current, rsc, param);
930
931 /* I don't like this error code, but (1) it maps to CRM_EX_CONFIG which
932 * is what the old cibsecret.in would return in this case, and (2) we
933 * return it all over the place for a variety of CIB checking errors.
934 */
935 rc = pcmk_rc_unpack_error;
936 goto done;
937 }
938
939 rc = local_files_set(out, rsh_fn, rcp_fn, rsc, param, value);
940 if (rc != pcmk_rc_ok) {
941 goto done;
942 }
943
944 rc = set_cib_param(out, rsc, param, LRM_MAGIC);
945
946 done:
947 free(current);
948 return rc;
949 }
950
951 static int
952 subcommand_stash(pcmk__output_t *out, rsh_fn_t rsh_fn, rcp_fn_t rcp_fn)
953 {
954 int rc = pcmk_rc_ok;
955 const char *rsc = remainder[1];
956 const char *param = remainder[2];
957 char *value = NULL;
958
959 if (check_cib_rsc(out, rsc) != pcmk_rc_ok) {
960 rc = ENODEV;
961 goto done;
962 }
963
964 value = get_cib_param(out, rsc, param);
965 if ((value == NULL) || is_secret(value)) {
966 if (value == NULL) {
967 out->err(out, "Nothing to stash for resource %s parameter %s", rsc,
968 param);
969 rc = ENOENT;
970 } else {
971 out->err(out, "Resource %s parameter %s already set as secret", rsc,
972 param);
973 rc = EEXIST;
974 }
975
976 goto done;
977 }
978
979 remainder = g_realloc(remainder, sizeof(gchar *) * 5);
980 remainder[3] = g_strdup(value);
981 remainder[4] = NULL;
982
983 rc = subcommand_set(out, rsh_fn, rcp_fn);
984
985 done:
986 free(value);
987 return rc;
988 }
989
990 static int
991 subcommand_sync(pcmk__output_t *out, rsh_fn_t rsh_fn, rcp_fn_t rcp_fn)
992 {
993 int rc = pcmk_rc_ok;
994 gchar *dirname = NULL;
|
(1) Event path: |
Condition "__s == 1", taking false branch. |
|
(2) Event path: |
Condition "0", taking false branch. |
|
(3) Event alloc_fn: |
Storage is returned from allocation function "g_malloc0_n". |
|
(4) Event var_assign: |
Assigning: "__p" = storage returned from "g_malloc0_n(__n, __s)". |
|
(5) Event out_of_scope: |
Variable "__p" goes out of scope. |
|
(6) Event var_assign: |
Assigning: "argv" = "({...; __p;})". |
| Also see events: |
[leaked_storage] |
995 gchar **argv = g_new0(gchar *, 4);
996 gchar **peers = get_live_peers(out);
997 gchar *peer_str = NULL;
998
|
(7) Event path: |
Condition "peers == NULL", taking true branch. |
999 if (peers == NULL) {
|
CID (unavailable; MK=97acb324a3cf6de34bd37751adc5a3e6) (#1 of 1): Resource leak (RESOURCE_LEAK): |
1000 return pcmk_rc_ok;
1001 }
1002
1003 peer_str = g_strjoinv(" ", peers);
1004
1005 out->info(out, "Syncing %s to %s ...", PCMK__CIB_SECRETS_DIR, peer_str);
1006 g_free(peer_str);
1007
1008 dirname = g_path_get_dirname(PCMK__CIB_SECRETS_DIR);
1009
1010 argv[0] = g_strdup("rm");
1011 argv[1] = g_strdup("-rf");
1012 argv[2] = g_strdup(PCMK__CIB_SECRETS_DIR);
1013 argv[3] = NULL;
1014
1015 rc = rsh_fn(out, peers, argv);
1016 if (rc != pcmk_rc_ok) {
1017 goto done;
1018 }
1019
1020 g_strfreev(argv);
1021 argv = g_new0(gchar *, 4);
1022
1023 argv[0] = g_strdup("mkdir");
1024 argv[1] = g_strdup("-p");
1025 argv[2] = g_strdup(dirname);
1026 argv[3] = NULL;
1027
1028 rc = rsh_fn(out, peers, argv);
1029 if (rc != pcmk_rc_ok) {
1030 goto done;
1031 }
1032
1033 rc = rcp_fn(out, peers, dirname, PCMK__CIB_SECRETS_DIR);
1034
1035 done:
1036 g_strfreev(argv);
1037 g_strfreev(peers);
1038 g_free(dirname);
1039 return rc;
1040 }
1041
1042 static int
1043 subcommand_unstash(pcmk__output_t *out, rsh_fn_t rsh_fn, rcp_fn_t rcp_fn)
1044 {
1045 int rc = pcmk_rc_ok;
1046 const char *rsc = remainder[1];
1047 const char *param = remainder[2];
1048 char *local_value = NULL;
1049 char *cib_value = NULL;
1050
1051 local_value = local_files_get(rsc, param);
1052 if (local_value == NULL) {
1053 out->err(out, "Nothing to unstash for resource %s parameter %s",
1054 rsc, param);
1055 rc = ENOENT;
1056 goto done;
1057 }
1058
1059 if (check_cib_rsc(out, rsc) != pcmk_rc_ok) {
1060 rc = ENODEV;
1061 goto done;
1062 }
1063
1064 cib_value = get_cib_param(out, rsc, param);
1065 if (!is_secret(cib_value)) {
1066 out->info(out, "Resource %s parameter %s is not set as secret, but we "
1067 "have a local value so proceeding anyway", rsc, param);
1068 }
1069
1070 rc = local_files_remove(out, rsh_fn, rcp_fn, rsc, param);
1071 if (rc != pcmk_rc_ok) {
1072 goto done;
1073 }
1074
1075 rc = set_cib_param(out, rsc, param, local_value);
1076
1077 done:
1078 free(cib_value);
1079 free(local_value);
1080 return rc;
1081 }
1082
1083 static struct subcommand_entry subcommand_table[] = {
1084 { "check", 2, "check <resource-id> <resource-parameter>", false,
1085 subcommand_check },
1086 { "delete", 2, "delete <resource-id> <resource-parameter>", false,
1087 subcommand_delete },
1088 { "get", 2, "get <resource-id> <resource-parameter>", false,
1089 subcommand_get },
1090 { "set", 3, "set <resource-id> <resource-parameter> <value>", false,
1091 subcommand_set },
1092 { "stash", 2, "stash <resource-id> <resource-parameter>", true,
1093 subcommand_stash },
1094 { "sync", 0, "sync", false, subcommand_sync },
1095 { "unstash", 2, "unstash <resource-id> <resource-parameter>", true,
1096 subcommand_unstash },
1097 { NULL },
1098 };
1099
1100 static bool
1101 tools_installed(pcmk__output_t *out, rsh_fn_t *rsh_fn, rcp_fn_t *rcp_fn,
1102 GError **error)
1103 {
1104 gchar *path = NULL;
1105
1106 path = g_find_program_in_path("pssh");
1107 if (path != NULL) {
1108 g_free(path);
1109 *rsh_fn = pssh;
1110 *rcp_fn = pscp;
1111 return true;
1112 }
1113
1114 path = g_find_program_in_path("pdsh");
1115 if (path != NULL) {
1116 g_free(path);
1117 *rsh_fn = pdsh;
1118 *rcp_fn = pdcp;
1119 return true;
1120 }
1121
1122 path = g_find_program_in_path("ssh");
1123 if (path != NULL) {
1124 g_free(path);
1125 *rsh_fn = ssh;
1126 *rcp_fn = scp;
1127 return true;
1128 }
1129
1130 out->err(out, "Please install one of pssh, pdsh, or ssh");
1131 return false;
1132 }
1133
1134 static pcmk__supported_format_t formats[] = {
1135 PCMK__SUPPORTED_FORMAT_NONE,
1136 PCMK__SUPPORTED_FORMAT_TEXT,
1137 PCMK__SUPPORTED_FORMAT_XML,
1138 { NULL, NULL, NULL }
1139 };
1140
1141 static GOptionContext *
1142 build_arg_context(pcmk__common_args_t *args, GOptionGroup **group) {
1143 const char *desc = NULL;
1144 GOptionContext *context = NULL;
1145
1146 desc = "This command manages sensitive resource parameter values that should not be\n"
1147 "stored directly in Pacemaker's Cluster Information Base (CIB). Such values\n"
1148 "are handled by storing a special string directly in the CIB that tells\n"
1149 "Pacemaker to look in a separate, protected file for the actual value.\n\n"
1150
1151 "The secret files are not encrypted, but protected by file system permissions\n"
1152 "such that only root can read or modify them.\n\n"
1153
1154 "Since the secret files are stored locally, they must be synchronized across all\n"
1155 "cluster nodes. This command handles the synchronization using (in order of\n"
1156 "preference) pssh, pdsh, or ssh, so one of those must be installed. Before\n"
1157 "synchronizing, this command will ping the cluster nodes to determine which are\n"
1158 "alive, using fping if it is installed, otherwise the ping command. Installing\n"
1159 "fping is strongly recommended for better performance.\n\n"
1160
1161 "Commands and their parameters:\n\n"
1162 "check <resource-id> <resource-parameter>\n"
1163 "\tVerify that the locally stored value of a sensitive resource parameter\n"
1164 "\tmatches its locally stored MD5 hash.\n\n"
1165 "delete <resource-id> <resource-parameter>\n"
1166 "\tRemove a sensitive resource parameter value.\n\n"
1167 "get <resource-id> <resource-parameter>\n"
1168 "\tDisplay the locally stored value of a sensitive resource parameter.\n\n"
1169 "set <resource-id> <resource-parameter> <value>\n"
1170 "\tSet the value of a sensitive resource parameter.\n\n"
1171 "stash <resource-id> <resource-parameter>\n"
1172 "\tMake a non-sensitive resource parameter that is already in the CIB\n"
1173 "\tsensitive (move its value to a locally stored and protected file).\n"
1174 "\tThis may not be used with -C.\n\n"
1175 "sync\n"
1176 "\tCopy all locally stored secrets to all other nodes.\n\n"
1177 "unstash <resource-id> <resource-parameter>\n"
1178 "\tMake a sensitive resource parameter that is already in the CIB\n"
1179 "\tnon-sensitive (move its value from the locally stored file to the CIB).\n"
1180 "\tThis may not be used with -C.\n\n\n"
1181
1182 "Known limitations:\n\n"
1183
1184 "This command can only be run from full cluster nodes (not Pacemaker Remote\n"
1185 "nodes).\n\n"
1186
1187 "Changes are not atomic, so the cluster may use different values while a\n"
1188 "change is in progress. To avoid problems, it is recommended to put the\n"
1189 "cluster in maintenance mode when making changes with this command.\n\n"
1190
1191 "Changes in secret values do not trigger an agent reload or restart of the\n"
1192 "affected resource, since they do not change the CIB. If a response is\n"
1193 "desired before the next cluster recheck interval, any CIB change (such as\n"
1194 "setting a node attribute) will trigger it.\n\n"
1195
1196 "If any node is down when changes to secrets are made, or a new node is\n"
1197 "later added to the cluster, it may have different values when it joins the\n"
1198 "cluster, before 'cibsecret sync' is run. To avoid this, it is recommended to\n"
1199 "run the sync command (from another node) before starting Pacemaker on the\n"
1200 "node.\n\n"
1201
1202 "Examples:\n\n"
1203
1204 "# cibsecret set ipmi_node1 passwd SecreT_PASS\n\n"
1205 "# cibsecret get ipmi_node1 passwd\n\n"
1206 "# cibsecret check ipmi_node1 passwd\n\n"
1207 "# cibsecret stash ipmi_node2 passwd\n\n"
1208 "# cibsecret sync\n";
1209
1210 context = pcmk__build_arg_context(args, "text (default), xml", group,
1211 "<subcommand> [options]");
1212 g_option_context_set_description(context, desc);
1213 pcmk__add_main_args(context, entries);
1214 return context;
1215 }
1216
1217 int
1218 main(int argc, char **argv)
1219 {
1220 crm_exit_t exit_code = CRM_EX_OK;
1221 int rc = pcmk_rc_ok;
1222
1223 pcmk__output_t *out = NULL;
1224
1225 GError *error = NULL;
1226
1227 GOptionGroup *output_group = NULL;
1228 pcmk__common_args_t *args = pcmk__new_common_args(SUMMARY);
1229 gchar **processed_args = pcmk__cmdline_preproc(argv, NULL);
1230 GOptionContext *context = build_arg_context(args, &output_group);
1231
1232 struct subcommand_entry cmd;
1233 rsh_fn_t rsh_fn;
1234 rcp_fn_t rcp_fn;
1235
1236 // Load locale information for the local host from the environment
1237 setlocale(LC_ALL, "");
1238
1239 pcmk__register_formats(output_group, formats);
1240 if (!g_option_context_parse_strv(context, &processed_args, &error)) {
1241 exit_code = CRM_EX_USAGE;
1242 goto done;
1243 }
1244
1245 pcmk__cli_init_logging("cibsecret", args->verbosity);
1246
1247 rc = pcmk__output_new(&out, args->output_ty, args->output_dest, argv);
1248 if (rc != pcmk_rc_ok) {
1249 exit_code = CRM_EX_ERROR;
1250 g_set_error(&error, PCMK__EXITC_ERROR, exit_code,
1251 "Error creating output format %s: %s", args->output_ty,
1252 pcmk_rc_str(rc));
1253 goto done;
1254 }
1255
1256 if (args->version) {
1257 out->version(out);
1258 goto done;
1259 }
1260
1261 /* No subcommand was given */
1262 if ((remainder == NULL) || (g_strv_length(remainder) == 0)) {
1263 gchar *help = g_option_context_get_help(context, TRUE, NULL);
1264
1265 exit_code = CRM_EX_USAGE;
1266 g_set_error(&error, PCMK__EXITC_ERROR, exit_code,
1267 "Must specify a command option\n\n%s", help);
1268 g_free(help);
1269 goto done;
1270 }
1271
1272 /* Traverse the subcommand table looking for a match. */
1273 for (int i = 0; i < PCMK__NELEM(subcommand_table); i++) {
1274 cmd = subcommand_table[i];
1275
1276 if (!pcmk__str_eq(remainder[0], cmd.name, pcmk__str_none)) {
1277 continue;
1278 }
1279
1280 /* We found a match. Check that enough arguments were given and
1281 * display a usage message if not. The "+ 1" is because the table
1282 * entry lists how many arguments the subcommand takes, which does not
1283 * include the subcommand itself.
1284 */
1285 if (g_strv_length(remainder) != cmd.args + 1) {
1286 exit_code = CRM_EX_USAGE;
1287 g_set_error(&error, PCMK__EXITC_ERROR, exit_code, "Usage: %s",
1288 cmd.usage);
1289 goto done;
1290 }
1291
1292 /* We've found the subcommand handler and it's used correctly. */
1293 break;
1294 }
1295
1296 /* If we didn't find a match, a valid subcommand wasn't given. */
1297 if (cmd.name == NULL) {
1298 exit_code = CRM_EX_USAGE;
1299 g_set_error(&error, PCMK__EXITC_ERROR, exit_code,
1300 "Invalid subcommand given; valid subcommands: "
1301 "check, delete, get, set, stash, sync, unstash");
1302 goto done;
1303 }
1304
1305 /* Check that we have the tools necessary to manage secrets */
1306 if (!tools_installed(out, &rsh_fn, &rcp_fn, &error)) {
1307 exit_code = CRM_EX_NOT_INSTALLED;
1308 goto done;
1309 }
1310
1311 /* Set a default umask so files we create are only accessible by the
1312 * cluster user.
1313 */
1314 umask(S_IRGRP | S_IWGRP | S_IXGRP | S_IROTH | S_IWOTH | S_IXOTH);
1315
1316 /* Call the subcommand handler. If the handler fails, it will have already
1317 * set exit_code to the reason why so there's no need to worry with
1318 * additional error checking here at the moment.
1319 */
1320 if (cmd.requires_cib && no_cib) {
1321 out->err(out, "No access to Pacemaker, %s not supported", cmd.name);
1322 exit_code = CRM_EX_USAGE;
1323 goto done;
1324 }
1325
1326 rc = cmd.handler(out, rsh_fn, rcp_fn);
1327 exit_code = pcmk_rc2exitc(rc);
1328
1329 done:
1330 g_strfreev(processed_args);
1331 g_strfreev(remainder);
1332 pcmk__free_arg_context(context);
1333
1334 pcmk__output_and_clear_error(&error, out);
1335
1336 if (out != NULL) {
1337 out->finish(out, exit_code, true, NULL);
1338 pcmk__output_free(out);
1339 }
1340 pcmk__unregister_formats();
1341 crm_exit(exit_code);
1342 }
1343