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 return pcmk_rc_ok;
496 }
497
498 peer_str = g_strjoinv(" ", peers);
499
500 if (pcmk__str_eq(remainder[0], "delete", pcmk__str_none)) {
501 out->info(out, "Deleting %s from %s ...", path, peer_str);
502 } else {
503 out->info(out, "Syncing %s to %s ...", path, peer_str);
504 }
505
506 dirname = g_path_get_dirname(path);
507
508 argv[0] = g_strdup("mkdir");
509 argv[1] = g_strdup("-p");
510 argv[2] = g_strdup(dirname);
511 argv[3] = NULL;
512
513 rc = rsh_fn(out, peers, argv);
514
515 if (rc != pcmk_rc_ok) {
516 goto done;
517 }
518
519 if (g_file_test(path, G_FILE_TEST_EXISTS | G_FILE_TEST_IS_REGULAR)) {
520 char *sign_path = NULL;
521
522 rc = rcp_fn(out, peers, dirname, path);
523 if (rc != pcmk_rc_ok) {
524 goto done;
525 }
526
527 sign_path = pcmk__assert_asprintf("%s.sign", path);
528 rc = rcp_fn(out, peers, dirname, sign_path);
529 free(sign_path);
530
531 } else {
532 g_strfreev(argv);
533 argv = g_new0(gchar *, 5);
534
535 argv[0] = g_strdup("rm");
536 argv[1] = g_strdup("-f");
537 argv[2] = g_strdup(path);
538 argv[3] = g_strdup_printf("%s.sign", path);
539 argv[4] = NULL;
540
541 rc = rsh_fn(out, peers, argv);
542 }
543
544 done:
545 g_free(dirname);
546 g_strfreev(argv);
547 g_strfreev(peers);
548 g_free(peer_str);
549 return rc;
550 }
551
552 static int
553 check_cib_rsc(pcmk__output_t *out, const char *rsc)
554 {
555 int rc = pcmk_rc_ok;
556 gchar **argv = g_new0(gchar *, 5);
557
558 if (no_cib) {
559 goto done;
560 }
561
562 argv[0] = g_strdup("crm_resource");
563 argv[1] = g_strdup("-r");
564 argv[2] = g_strdup(rsc);
565 argv[3] = g_strdup("-W");
566 argv[4] = NULL;
567
568 rc = run_cmdline(out, argv, NULL);
569
570 done:
571 g_strfreev(argv);
572 return rc;
573 }
574
575 static bool
576 is_secret(const char *s)
577 {
578 if (no_cib) {
579 /* Assume that the secret is in the CIB if we can't connect */
580 return true;
581 }
582
583 return pcmk__str_eq(s, LRM_MAGIC, pcmk__str_none);
584 }
585
586 static char *
587 get_cib_param(pcmk__output_t *out, const char *rsc, const char *param)
588 {
589 int rc = pcmk_rc_ok;
590 gchar **argv = g_new0(gchar *, 7);
591 char *standard_out = NULL;
592 char *retval = NULL;
593 char *xpath = NULL;
594 xmlNode *xml = NULL;
595 xmlNode *node = NULL;
596 xmlChar *content = NULL;
597
598 if (no_cib) {
599 g_strfreev(argv);
600 return NULL;
601 }
602
603 argv[0] = g_strdup("crm_resource");
604 argv[1] = g_strdup("-r");
605 argv[2] = g_strdup(rsc);
606 argv[3] = g_strdup("-g");
607 argv[4] = g_strdup(param);
608 argv[5] = g_strdup("--output-as=xml");
609 argv[6] = NULL;
610
611 rc = run_cmdline(out, argv, &standard_out);
612 g_strfreev(argv);
613
614 if (rc != pcmk_rc_ok) {
615 goto done;
616 }
617
618 xml = pcmk__xml_parse(standard_out);
619
620 if (xml == NULL) {
621 goto done;
622 }
623
624 xpath = pcmk__assert_asprintf("//" PCMK_XE_ITEM "[@" PCMK_XA_NAME "='%s']",
625 param);
626 node = pcmk__xpath_find_one(xml->doc, xpath, LOG_DEBUG);
627
628 if (node == NULL) {
629 goto done;
630 }
631
632 content = xmlNodeGetContent(node);
633
634 if (content != NULL) {
635 retval = pcmk__str_copy((char *) content);
636 xmlFree(content);
637 }
638
639 done:
640 free(standard_out);
641 free(xpath);
642 pcmk__xml_free(xml);
643 return retval;
644 }
645
646 static int
647 remove_cib_param(pcmk__output_t *out, const char *rsc, const char *param)
648 {
649 int rc = pcmk_rc_ok;
650 gchar **argv = g_new0(gchar *, 6);
651
652 if (no_cib) {
653 goto done;
654 }
655
656 argv[0] = g_strdup("crm_resource");
657 argv[1] = g_strdup("-r");
658 argv[2] = g_strdup(rsc);
659 argv[3] = g_strdup("-d");
660 argv[4] = g_strdup(param);
661 argv[5] = NULL;
662
663 rc = run_cmdline(out, argv, NULL);
664
665 done:
666 g_strfreev(argv);
667 return rc;
668 }
669
670 static int
671 set_cib_param(pcmk__output_t *out, const char *rsc, const char *param,
672 const char *value)
673 {
674 int rc = pcmk_rc_ok;
675 gchar **argv = g_new0(gchar *, 8);
676
677 if (no_cib) {
678 goto done;
679 }
680
681 argv[0] = g_strdup("crm_resource");
682 argv[1] = g_strdup("-r");
683 argv[2] = g_strdup(rsc);
684 argv[3] = g_strdup("-p");
685 argv[4] = g_strdup(param);
686 argv[5] = g_strdup("-v");
687 argv[6] = g_strdup(value);
688 argv[7] = NULL;
689
690 rc = run_cmdline(out, argv, NULL);
691
692 done:
693 g_strfreev(argv);
694 return rc;
695 }
696
697 static char *
698 local_files_get(const char *rsc, const char *param)
699 {
700 char *retval = NULL;
701 char *lf_file = NULL;
702 gchar *contents = NULL;
703
704 lf_file = pcmk__assert_asprintf(PCMK__CIB_SECRETS_DIR "/%s/%s", rsc, param);
705 if (g_file_get_contents(lf_file, &contents, NULL, NULL)) {
706 contents = g_strchomp(contents);
707 retval = pcmk__str_copy(contents);
708 g_free(contents);
709 }
710
711 free(lf_file);
712 return retval;
713 }
714
715 static char *
716 local_files_getsum(const char *rsc, const char *param)
717 {
718 char *retval = NULL;
719 char *lf_file = NULL;
720 gchar *contents = NULL;
721
722 lf_file = pcmk__assert_asprintf(PCMK__CIB_SECRETS_DIR "/%s/%s.sign", rsc,
723 param);
724 if (g_file_get_contents(lf_file, &contents, NULL, NULL)) {
725 contents = g_strchomp(contents);
726 retval = pcmk__str_copy(contents);
727 g_free(contents);
728 }
729
730 free(lf_file);
731 return retval;
732 }
733
734 static int
735 local_files_remove(pcmk__output_t *out, rsh_fn_t rsh_fn, rcp_fn_t rcp_fn,
736 const char *rsc, const char *param)
737 {
738 int rc = pcmk_rc_ok;
739 char *lf_file = NULL;
740 gchar **argv = g_new0(gchar *, 5);
741
742 lf_file = pcmk__assert_asprintf(PCMK__CIB_SECRETS_DIR "/%s/%s", rsc, param);
743
744 argv[0] = g_strdup("rm");
745 argv[1] = g_strdup("-f");
746 argv[2] = g_strdup(lf_file);
747 argv[3] = g_strdup_printf("%s.sign", lf_file);
748 argv[4] = NULL;
749
750 rc = run_cmdline(out, argv, NULL);
751
752 if (rc == pcmk_rc_ok) {
753 rc = sync_one_file(out, rsh_fn, rcp_fn, lf_file);
754 }
755
756 free(lf_file);
757 g_strfreev(argv);
758 return rc;
759 }
760
761 static int
762 local_files_set(pcmk__output_t *out, rsh_fn_t rsh_fn, rcp_fn_t rcp_fn,
763 const char *rsc, const char *param, const char *value)
764 {
765 char *contents = NULL;
766 char *lf_dir = NULL;
767 char *lf_file = NULL;
768 char *sign_file = NULL;
769 char *calc_sum = NULL;
770 int rc = pcmk_rc_ok;
771
772 lf_dir = pcmk__assert_asprintf(PCMK__CIB_SECRETS_DIR "/%s", rsc);
773
774 if (g_mkdir_with_parents(lf_dir, 0700) != 0) {
775 rc = errno;
776 out->err(out, "Could not create directory %s: %s", lf_dir,
777 pcmk_rc_str(rc));
778 goto done;
779 }
780
781 lf_file = pcmk__assert_asprintf("%s/%s", lf_dir, param);
782 contents = pcmk__assert_asprintf("%s\n", value);
783 if (!g_file_set_contents(lf_file, contents, -1, NULL)) {
784 rc = EIO;
785 out->err(out, "Could not create file %s: %s", lf_file,
786 pcmk_rc_str(rc));
787 goto done;
788 }
789
790 free(contents);
791
792 sign_file = pcmk__assert_asprintf("%s/%s.sign", lf_dir, param);
793 calc_sum = pcmk__md5sum(value);
794 contents = pcmk__assert_asprintf("%s\n", calc_sum);
795
796 if (!g_file_set_contents(sign_file, contents, -1, NULL)) {
797 rc = EIO;
798 out->err(out, "Could not create file %s: %s", sign_file,
799 pcmk_rc_str(rc));
800 goto done;
801 }
802
803 rc = sync_one_file(out, rsh_fn, rcp_fn, lf_file);
804
805 done:
806 free(contents);
807 free(calc_sum);
808 free(sign_file);
809 free(lf_dir);
810 free(lf_file);
811 return rc;
812 }
813
814 static int
815 subcommand_check(pcmk__output_t *out, rsh_fn_t rsh_fn, rcp_fn_t rcp_fn)
816 {
817 int rc = pcmk_rc_ok;
818 const char *rsc = remainder[1];
819 const char *param = remainder[2];
820 char *value = NULL;
821 char *calc_sum = NULL;
822 char *local_sum = NULL;
823 char *local_value = NULL;
824
825 if (check_cib_rsc(out, rsc) != pcmk_rc_ok) {
826 rc = ENODEV;
827 goto done;
828 }
829
830 value = get_cib_param(out, rsc, param);
831 if ((value == NULL) || !is_secret(value)) {
832 out->err(out, "Resource %s parameter %s not set as secret, nothing to check",
833 rsc, param);
834
835 /* I don't like this error code, but (1) it maps to CRM_EX_CONFIG which
836 * is what the old cibsecret.in would return in this case, and (2) we
837 * return it all over the place for a variety of CIB checking errors.
838 */
839 rc = pcmk_rc_unpack_error;
840 goto done;
841 }
842
843 local_sum = local_files_getsum(rsc, param);
844 if (local_sum == NULL) {
845 out->err(out, "No checksum for resource %s parameter %s", rsc, param);
846 rc = ENOENT;
847 goto done;
848 }
849
850 local_value = local_files_get(rsc, param);
851 if (local_value != NULL) {
852 calc_sum = pcmk__md5sum(local_value);
853 }
854
855 if ((local_value == NULL) || !pcmk__str_eq(calc_sum, local_sum, pcmk__str_none)) {
856 out->err(out, "Checksum mismatch for resource %s parameter %s", rsc, param);
857 rc = pcmk_rc_digest_mismatch;
858 }
859
860 done:
861 free(local_sum);
862 free(local_value);
863 free(calc_sum);
864 free(value);
865 return rc;
866 }
867
868 static int
869 subcommand_delete(pcmk__output_t *out, rsh_fn_t rsh_fn, rcp_fn_t rcp_fn)
870 {
871 int rc = pcmk_rc_ok;
872 const char *rsc = remainder[1];
873 const char *param = remainder[2];
874
875 if (check_cib_rsc(out, rsc) != pcmk_rc_ok) {
876 return ENODEV;
877 }
878
879 rc = local_files_remove(out, rsh_fn, rcp_fn, rsc, param);
880 if (rc != pcmk_rc_ok) {
881 return rc;
882 }
883
884 return remove_cib_param(out, rsc, param);
885 }
886
887 static int
888 subcommand_get(pcmk__output_t *out, rsh_fn_t rsh_fn, rcp_fn_t rcp_fn)
889 {
890 int rc = subcommand_check(out, rsh_fn, rcp_fn);
891 char *value = NULL;
892 const char *rsc = remainder[1];
893 const char *param = remainder[2];
894
895 if (rc != pcmk_rc_ok) {
896 return rc;
897 }
898
899 value = local_files_get(rsc, param);
900 pcmk__assert(value != NULL);
901 out->info(out, "%s", value);
902
903 free(value);
904 return pcmk_rc_ok;
905 }
906
907 /* The previous shell implementation of cibsecret allowed passing the value
908 * to set (what would be remainder[3] here) via stdin, which we do not support
909 * here at the moment.
910 */
911 static int
912 subcommand_set(pcmk__output_t *out, rsh_fn_t rsh_fn, rcp_fn_t rcp_fn)
913 {
914 int rc = pcmk_rc_ok;
915 const char *rsc = remainder[1];
916 const char *param = remainder[2];
917 const char *value = remainder[3];
918 char *current = NULL;
919
920 if (check_cib_rsc(out, rsc) != pcmk_rc_ok) {
921 rc = ENODEV;
922 goto done;
923 }
924
925 current = get_cib_param(out, rsc, param);
926 if ((current != NULL) && !pcmk__str_any_of(current, LRM_MAGIC, value, NULL)) {
927 out->err(out, "CIB value <%s> different for %s rsc parameter %s; please "
928 "delete it first", current, rsc, param);
929
930 /* I don't like this error code, but (1) it maps to CRM_EX_CONFIG which
931 * is what the old cibsecret.in would return in this case, and (2) we
932 * return it all over the place for a variety of CIB checking errors.
933 */
934 rc = pcmk_rc_unpack_error;
935 goto done;
936 }
937
938 rc = local_files_set(out, rsh_fn, rcp_fn, rsc, param, value);
939 if (rc != pcmk_rc_ok) {
940 goto done;
941 }
942
943 rc = set_cib_param(out, rsc, param, LRM_MAGIC);
944
945 done:
946 free(current);
947 return rc;
948 }
949
950 static int
951 subcommand_stash(pcmk__output_t *out, rsh_fn_t rsh_fn, rcp_fn_t rcp_fn)
952 {
953 int rc = pcmk_rc_ok;
954 const char *rsc = remainder[1];
955 const char *param = remainder[2];
956 char *value = NULL;
957
958 if (check_cib_rsc(out, rsc) != pcmk_rc_ok) {
959 rc = ENODEV;
960 goto done;
961 }
962
963 value = get_cib_param(out, rsc, param);
964 if ((value == NULL) || is_secret(value)) {
965 if (value == NULL) {
966 out->err(out, "Nothing to stash for resource %s parameter %s", rsc,
967 param);
968 rc = ENOENT;
969 } else {
970 out->err(out, "Resource %s parameter %s already set as secret", rsc,
971 param);
972 rc = EEXIST;
973 }
974
975 goto done;
976 }
977
978 remainder = g_realloc(remainder, sizeof(gchar *) * 5);
979 remainder[3] = g_strdup(value);
980 remainder[4] = NULL;
981
982 rc = subcommand_set(out, rsh_fn, rcp_fn);
983
984 done:
985 free(value);
986 return rc;
987 }
988
989 static int
990 subcommand_sync(pcmk__output_t *out, rsh_fn_t rsh_fn, rcp_fn_t rcp_fn)
991 {
992 int rc = pcmk_rc_ok;
993 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] |
994 gchar **argv = g_new0(gchar *, 4);
995 gchar **peers = get_live_peers(out);
996 gchar *peer_str = NULL;
997
|
(7) Event path: |
Condition "peers == NULL", taking true branch. |
998 if (peers == NULL) {
|
CID (unavailable; MK=97acb324a3cf6de34bd37751adc5a3e6) (#1 of 1): Resource leak (RESOURCE_LEAK): |
999 return pcmk_rc_ok;
1000 }
1001
1002 peer_str = g_strjoinv(" ", peers);
1003
1004 out->info(out, "Syncing %s to %s ...", PCMK__CIB_SECRETS_DIR, peer_str);
1005 g_free(peer_str);
1006
1007 dirname = g_path_get_dirname(PCMK__CIB_SECRETS_DIR);
1008
1009 argv[0] = g_strdup("rm");
1010 argv[1] = g_strdup("-rf");
1011 argv[2] = g_strdup(PCMK__CIB_SECRETS_DIR);
1012 argv[3] = NULL;
1013
1014 rc = rsh_fn(out, peers, argv);
1015 if (rc != pcmk_rc_ok) {
1016 goto done;
1017 }
1018
1019 g_strfreev(argv);
1020 argv = g_new0(gchar *, 4);
1021
1022 argv[0] = g_strdup("mkdir");
1023 argv[1] = g_strdup("-p");
1024 argv[2] = g_strdup(dirname);
1025 argv[3] = NULL;
1026
1027 rc = rsh_fn(out, peers, argv);
1028 if (rc != pcmk_rc_ok) {
1029 goto done;
1030 }
1031
1032 rc = rcp_fn(out, peers, dirname, PCMK__CIB_SECRETS_DIR);
1033
1034 done:
1035 g_strfreev(argv);
1036 g_strfreev(peers);
1037 g_free(dirname);
1038 return rc;
1039 }
1040
1041 static int
1042 subcommand_unstash(pcmk__output_t *out, rsh_fn_t rsh_fn, rcp_fn_t rcp_fn)
1043 {
1044 int rc = pcmk_rc_ok;
1045 const char *rsc = remainder[1];
1046 const char *param = remainder[2];
1047 char *local_value = NULL;
1048 char *cib_value = NULL;
1049
1050 local_value = local_files_get(rsc, param);
1051 if (local_value == NULL) {
1052 out->err(out, "Nothing to unstash for resource %s parameter %s",
1053 rsc, param);
1054 rc = ENOENT;
1055 goto done;
1056 }
1057
1058 if (check_cib_rsc(out, rsc) != pcmk_rc_ok) {
1059 rc = ENODEV;
1060 goto done;
1061 }
1062
1063 cib_value = get_cib_param(out, rsc, param);
1064 if (!is_secret(cib_value)) {
1065 out->info(out, "Resource %s parameter %s is not set as secret, but we "
1066 "have a local value so proceeding anyway", rsc, param);
1067 }
1068
1069 rc = local_files_remove(out, rsh_fn, rcp_fn, rsc, param);
1070 if (rc != pcmk_rc_ok) {
1071 goto done;
1072 }
1073
1074 rc = set_cib_param(out, rsc, param, local_value);
1075
1076 done:
1077 free(cib_value);
1078 free(local_value);
1079 return rc;
1080 }
1081
1082 static struct subcommand_entry subcommand_table[] = {
1083 { "check", 2, "check <resource-id> <resource-parameter>", false,
1084 subcommand_check },
1085 { "delete", 2, "delete <resource-id> <resource-parameter>", false,
1086 subcommand_delete },
1087 { "get", 2, "get <resource-id> <resource-parameter>", false,
1088 subcommand_get },
1089 { "set", 3, "set <resource-id> <resource-parameter> <value>", false,
1090 subcommand_set },
1091 { "stash", 2, "stash <resource-id> <resource-parameter>", true,
1092 subcommand_stash },
1093 { "sync", 0, "sync", false, subcommand_sync },
1094 { "unstash", 2, "unstash <resource-id> <resource-parameter>", true,
1095 subcommand_unstash },
1096 { NULL },
1097 };
1098
1099 static bool
1100 tools_installed(pcmk__output_t *out, rsh_fn_t *rsh_fn, rcp_fn_t *rcp_fn,
1101 GError **error)
1102 {
1103 gchar *path = NULL;
1104
1105 path = g_find_program_in_path("pssh");
1106 if (path != NULL) {
1107 g_free(path);
1108 *rsh_fn = pssh;
1109 *rcp_fn = pscp;
1110 return true;
1111 }
1112
1113 path = g_find_program_in_path("pdsh");
1114 if (path != NULL) {
1115 g_free(path);
1116 *rsh_fn = pdsh;
1117 *rcp_fn = pdcp;
1118 return true;
1119 }
1120
1121 path = g_find_program_in_path("ssh");
1122 if (path != NULL) {
1123 g_free(path);
1124 *rsh_fn = ssh;
1125 *rcp_fn = scp;
1126 return true;
1127 }
1128
1129 out->err(out, "Please install one of pssh, pdsh, or ssh");
1130 return false;
1131 }
1132
1133 static pcmk__supported_format_t formats[] = {
1134 PCMK__SUPPORTED_FORMAT_NONE,
1135 PCMK__SUPPORTED_FORMAT_TEXT,
1136 PCMK__SUPPORTED_FORMAT_XML,
1137 { NULL, NULL, NULL }
1138 };
1139
1140 static GOptionContext *
1141 build_arg_context(pcmk__common_args_t *args, GOptionGroup **group) {
1142 const char *desc = NULL;
1143 GOptionContext *context = NULL;
1144
1145 desc = "This command manages sensitive resource parameter values that should not be\n"
1146 "stored directly in Pacemaker's Cluster Information Base (CIB). Such values\n"
1147 "are handled by storing a special string directly in the CIB that tells\n"
1148 "Pacemaker to look in a separate, protected file for the actual value.\n\n"
1149
1150 "The secret files are not encrypted, but protected by file system permissions\n"
1151 "such that only root can read or modify them.\n\n"
1152
1153 "Since the secret files are stored locally, they must be synchronized across all\n"
1154 "cluster nodes. This command handles the synchronization using (in order of\n"
1155 "preference) pssh, pdsh, or ssh, so one of those must be installed. Before\n"
1156 "synchronizing, this command will ping the cluster nodes to determine which are\n"
1157 "alive, using fping if it is installed, otherwise the ping command. Installing\n"
1158 "fping is strongly recommended for better performance.\n\n"
1159
1160 "Commands and their parameters:\n\n"
1161 "check <resource-id> <resource-parameter>\n"
1162 "\tVerify that the locally stored value of a sensitive resource parameter\n"
1163 "\tmatches its locally stored MD5 hash.\n\n"
1164 "delete <resource-id> <resource-parameter>\n"
1165 "\tRemove a sensitive resource parameter value.\n\n"
1166 "get <resource-id> <resource-parameter>\n"
1167 "\tDisplay the locally stored value of a sensitive resource parameter.\n\n"
1168 "set <resource-id> <resource-parameter> <value>\n"
1169 "\tSet the value of a sensitive resource parameter.\n\n"
1170 "stash <resource-id> <resource-parameter>\n"
1171 "\tMake a non-sensitive resource parameter that is already in the CIB\n"
1172 "\tsensitive (move its value to a locally stored and protected file).\n"
1173 "\tThis may not be used with -C.\n\n"
1174 "sync\n"
1175 "\tCopy all locally stored secrets to all other nodes.\n\n"
1176 "unstash <resource-id> <resource-parameter>\n"
1177 "\tMake a sensitive resource parameter that is already in the CIB\n"
1178 "\tnon-sensitive (move its value from the locally stored file to the CIB).\n"
1179 "\tThis may not be used with -C.\n\n\n"
1180
1181 "Known limitations:\n\n"
1182
1183 "This command can only be run from full cluster nodes (not Pacemaker Remote\n"
1184 "nodes).\n\n"
1185
1186 "Changes are not atomic, so the cluster may use different values while a\n"
1187 "change is in progress. To avoid problems, it is recommended to put the\n"
1188 "cluster in maintenance mode when making changes with this command.\n\n"
1189
1190 "Changes in secret values do not trigger an agent reload or restart of the\n"
1191 "affected resource, since they do not change the CIB. If a response is\n"
1192 "desired before the next cluster recheck interval, any CIB change (such as\n"
1193 "setting a node attribute) will trigger it.\n\n"
1194
1195 "If any node is down when changes to secrets are made, or a new node is\n"
1196 "later added to the cluster, it may have different values when it joins the\n"
1197 "cluster, before 'cibsecret sync' is run. To avoid this, it is recommended to\n"
1198 "run the sync command (from another node) before starting Pacemaker on the\n"
1199 "node.\n\n"
1200
1201 "Examples:\n\n"
1202
1203 "# cibsecret set ipmi_node1 passwd SecreT_PASS\n\n"
1204 "# cibsecret get ipmi_node1 passwd\n\n"
1205 "# cibsecret check ipmi_node1 passwd\n\n"
1206 "# cibsecret stash ipmi_node2 passwd\n\n"
1207 "# cibsecret sync\n";
1208
1209 context = pcmk__build_arg_context(args, "text (default), xml", group,
1210 "<subcommand> [options]");
1211 g_option_context_set_description(context, desc);
1212 pcmk__add_main_args(context, entries);
1213 return context;
1214 }
1215
1216 int
1217 main(int argc, char **argv)
1218 {
1219 crm_exit_t exit_code = CRM_EX_OK;
1220 int rc = pcmk_rc_ok;
1221
1222 pcmk__output_t *out = NULL;
1223
1224 GError *error = NULL;
1225
1226 GOptionGroup *output_group = NULL;
1227 pcmk__common_args_t *args = pcmk__new_common_args(SUMMARY);
1228 gchar **processed_args = pcmk__cmdline_preproc(argv, NULL);
1229 GOptionContext *context = build_arg_context(args, &output_group);
1230
1231 struct subcommand_entry cmd;
1232 rsh_fn_t rsh_fn;
1233 rcp_fn_t rcp_fn;
1234
1235 // Load locale information for the local host from the environment
1236 setlocale(LC_ALL, "");
1237
1238 pcmk__register_formats(output_group, formats);
1239 if (!g_option_context_parse_strv(context, &processed_args, &error)) {
1240 exit_code = CRM_EX_USAGE;
1241 goto done;
1242 }
1243
1244 pcmk__cli_init_logging("cibsecret", args->verbosity);
1245
1246 rc = pcmk__output_new(&out, args->output_ty, args->output_dest, argv);
1247 if (rc != pcmk_rc_ok) {
1248 exit_code = CRM_EX_ERROR;
1249 g_set_error(&error, PCMK__EXITC_ERROR, exit_code,
1250 "Error creating output format %s: %s", args->output_ty,
1251 pcmk_rc_str(rc));
1252 goto done;
1253 }
1254
1255 if (args->version) {
1256 out->version(out);
1257 goto done;
1258 }
1259
1260 /* No subcommand was given */
1261 if ((remainder == NULL) || (g_strv_length(remainder) == 0)) {
1262 gchar *help = g_option_context_get_help(context, TRUE, NULL);
1263
1264 exit_code = CRM_EX_USAGE;
1265 g_set_error(&error, PCMK__EXITC_ERROR, exit_code,
1266 "Must specify a command option\n\n%s", help);
1267 g_free(help);
1268 goto done;
1269 }
1270
1271 /* Traverse the subcommand table looking for a match. */
1272 for (int i = 0; i < PCMK__NELEM(subcommand_table); i++) {
1273 cmd = subcommand_table[i];
1274
1275 if (!pcmk__str_eq(remainder[0], cmd.name, pcmk__str_none)) {
1276 continue;
1277 }
1278
1279 /* We found a match. Check that enough arguments were given and
1280 * display a usage message if not. The "+ 1" is because the table
1281 * entry lists how many arguments the subcommand takes, which does not
1282 * include the subcommand itself.
1283 */
1284 if (g_strv_length(remainder) != cmd.args + 1) {
1285 exit_code = CRM_EX_USAGE;
1286 g_set_error(&error, PCMK__EXITC_ERROR, exit_code, "Usage: %s",
1287 cmd.usage);
1288 goto done;
1289 }
1290
1291 /* We've found the subcommand handler and it's used correctly. */
1292 break;
1293 }
1294
1295 /* If we didn't find a match, a valid subcommand wasn't given. */
1296 if (cmd.name == NULL) {
1297 exit_code = CRM_EX_USAGE;
1298 g_set_error(&error, PCMK__EXITC_ERROR, exit_code,
1299 "Invalid subcommand given; valid subcommands: "
1300 "check, delete, get, set, stash, sync, unstash");
1301 goto done;
1302 }
1303
1304 /* Check that we have the tools necessary to manage secrets */
1305 if (!tools_installed(out, &rsh_fn, &rcp_fn, &error)) {
1306 exit_code = CRM_EX_NOT_INSTALLED;
1307 goto done;
1308 }
1309
1310 /* Set a default umask so files we create are only accessible by the
1311 * cluster user.
1312 */
1313 umask(S_IRGRP | S_IWGRP | S_IXGRP | S_IROTH | S_IWOTH | S_IXOTH);
1314
1315 /* Call the subcommand handler. If the handler fails, it will have already
1316 * set exit_code to the reason why so there's no need to worry with
1317 * additional error checking here at the moment.
1318 */
1319 if (cmd.requires_cib && no_cib) {
1320 out->err(out, "No access to Pacemaker, %s not supported", cmd.name);
1321 exit_code = CRM_EX_USAGE;
1322 goto done;
1323 }
1324
1325 rc = cmd.handler(out, rsh_fn, rcp_fn);
1326 exit_code = pcmk_rc2exitc(rc);
1327
1328 done:
1329 g_strfreev(processed_args);
1330 g_strfreev(remainder);
1331 pcmk__free_arg_context(context);
1332
1333 pcmk__output_and_clear_error(&error, out);
1334
1335 if (out != NULL) {
1336 out->finish(out, exit_code, true, NULL);
1337 pcmk__output_free(out);
1338 }
1339 pcmk__unregister_formats();
1340 crm_exit(exit_code);
1341 }
1342