From 6f11dffae8a7c3028621743369e985b8fad08bb5 Mon Sep 17 00:00:00 2001 From: Markus Hilger Date: Wed, 8 Jul 2026 19:55:18 +0200 Subject: [PATCH] Add server-side confluentdbutil showattrib subcommand Adds `confluentdbutil showattrib ...` to print the node attribute. In contrast to nodeattrib it can shows secrets and crypted values with -u flag. It's server-side only: reads the config store and master key directly, never over the API. It's read-only and works without confluentd running. --- confluent_client/doc/man/confluentdbutil.ronn | 34 +++++- confluent_client/doc/man/nodeattrib.ronn.tmpl | 4 + .../doc/man/nodegroupattrib.ronn.tmpl | 4 + confluent_server/bin/confluentdbutil | 111 +++++++++++++++++- 4 files changed, 148 insertions(+), 5 deletions(-) diff --git a/confluent_client/doc/man/confluentdbutil.ronn b/confluent_client/doc/man/confluentdbutil.ronn index 9808e58d..820e86a9 100644 --- a/confluent_client/doc/man/confluentdbutil.ronn +++ b/confluent_client/doc/man/confluentdbutil.ronn @@ -4,6 +4,7 @@ confluentdbutil(8) -- Backup or restore confluent database ## SYNOPSIS `confluentdbutil [options] [dump|restore] ` +`confluentdbutil [-u] [-v] showattrib ...` ## DESCRIPTION @@ -13,6 +14,15 @@ In order to perform restore, the confluent service must not be running. It is required to indicate how the usernames/passwords are treated in the json files (password protected, removed from the files, or unprotected). +The `showattrib` subcommand prints the stored value of any attribute for the +given noderange, reading directly from the configuration store. Because it +does not talk to the confluent daemon, it can be used to read attribute values +even while confluent is stopped. It must be run directly on the confluent +server as root. By default, `secret.*` and `crypted.*` values are masked as +`********`, just as nodeattrib(8) shows them. With `-u`, they are revealed: +`secret.*` values decrypted to plaintext and `crypted.*` values as their +stored one-way hashes. + ## OPTIONS * `-p PASSWORD`, `--password=PASSWORD`: @@ -27,7 +37,10 @@ the json files (password protected, removed from the files, or unprotected). than including them. * `-u`, `--unprotected`: - The keys.json file will include the encryption keys without any protection. + With `dump`, the keys.json file will include the encryption keys without + any protection. With `showattrib`, show `secret.*` values decrypted to + plaintext and `crypted.*` values as their stored hashes rather than + masking them as `********`. * `-s`, `--skipkeys`: This specifies to dump the encrypted data without @@ -40,6 +53,21 @@ the json files (password protected, removed from the files, or unprotected). * `-y`, `--yaml`: Use YAML instead of JSON as file format - - * `-h`, `--help`: + +* `-v`, `--value-only`: + With `showattrib`, print only the values, without node and attribute names + (useful for scripting). + +* `-h`, `--help`: Show help message and exit + +## EXAMPLES + +* Show the decrypted BMC password confluent uses for a node (run on the + server as root): + `# confluentdbutil -u showattrib n1 secret.hardwaremanagementpassword` + `n1: secret.hardwaremanagementpassword: mypassword123` + +* Get just the value, for use in a script: + `# confluentdbutil -u -v showattrib n1 secret.hardwaremanagementpassword` + `mypassword123` diff --git a/confluent_client/doc/man/nodeattrib.ronn.tmpl b/confluent_client/doc/man/nodeattrib.ronn.tmpl index 2387a15a..2dd4e5be 100644 --- a/confluent_client/doc/man/nodeattrib.ronn.tmpl +++ b/confluent_client/doc/man/nodeattrib.ronn.tmpl @@ -38,6 +38,10 @@ If the word all is specified, then all available attributes are given. Omitting any attribute name or the word 'all' will display only attributes that are currently set. +The values of `secret.*` and `crypted.*` attributes are never shown here; they +can only be retrieved on the confluent server with +`confluentdbutil -u showattrib`. + For the `groups` attribute, it is possible to add a group by doing `groups,=` and to remove by doing `groups^=` diff --git a/confluent_client/doc/man/nodegroupattrib.ronn.tmpl b/confluent_client/doc/man/nodegroupattrib.ronn.tmpl index c88baf64..7b631130 100644 --- a/confluent_client/doc/man/nodegroupattrib.ronn.tmpl +++ b/confluent_client/doc/man/nodegroupattrib.ronn.tmpl @@ -23,6 +23,10 @@ node after using the nodeattrib(8) command will not have attributes change autom It's easiest to see by using the `nodeattrib -b` to understand how the attributes are set on the node versus a group to which a node belongs. +The values of `secret.*` and `crypted.*` attributes are never shown here; they +can only be retrieved on the confluent server with +`confluentdbutil -u showattrib`. + ## OPTIONS * `-b`, `--blame`: diff --git a/confluent_server/bin/confluentdbutil b/confluent_server/bin/confluentdbutil index 00ad6634..ce361e3f 100755 --- a/confluent_server/bin/confluentdbutil +++ b/confluent_server/bin/confluentdbutil @@ -29,9 +29,11 @@ if path.startswith('/opt'): import confluent.config.configmanager as cfm import confluent.config.conf as conf import confluent.main as main +import confluent.noderange as noderange argparser = optparse.OptionParser( - usage="Usage: %prog [options] [dump|restore|merge] [path]") + usage="Usage: %prog [options] [dump|restore|merge] [path]\n" + " %prog [-u] [-v] showattrib ...") argparser.add_option('-p', '--password', help='Password to use to protect/unlock a protected dump') argparser.add_option('-i', '--interactivepassword', help='Prompt for password', @@ -42,7 +44,10 @@ argparser.add_option('-u', '--unprotected', action='store_true', help='Specify that no password should be used to protect' ' the key information. Fields will be encrypted, ' 'but keys.json will contain unencrypted decryption' - ' keys that may be used to read the dump') + ' keys that may be used to read the dump. With ' + 'showattrib, show secret.* values decrypted to ' + 'plaintext and crypted.* values as their stored ' + 'hashes rather than masking them') argparser.add_option('-s', '--skipkeys', action='store_true', help='This specifies to dump the encrypted data without ' 'dumping the keys needed to decrypt it. This is ' @@ -53,7 +58,109 @@ argparser.add_option('-s', '--skipkeys', action='store_true', 'they do not require incremental backup') argparser.add_option('-y', '--yaml', action='store_true', help='Use YAML instead of JSON as file format') +argparser.add_option('-v', '--value-only', dest='valueonly', + action='store_true', + help='With showattrib, print only the values ' + 'without node and attribute names') (options, args) = argparser.parse_args() + + +def _dispval(value): + # Render a single scalar for display: decode bytes (e.g. decrypted + # secrets) and stringify anything else, including non-string values + if isinstance(value, bytes): + return value.decode('utf-8', errors='replace') + return str(value) + + +def show_attrib(options, args): + if len(args) < 3: + sys.stderr.write('Usage: confluentdbutil showattrib [-u] [-v] ' + ' [...]\n') + sys.exit(1) + nr = args[1] + requested = args[2:] + # Load the config store and unlock the master key, mirroring the daemon + # startup sequence in confluent.main. _initsecurity handles the + # externalcfgkey case (key protected by the contents of a key file); + # otherwise the key is stored unprotected and init_masterkey unlocks it + # without any password. autogen=False keeps this read-only (never create a + # key). + cfm.init() + try: + main._initsecurity(conf.get_config()) + if not cfm._masterkey: + cfm.init_masterkey(autogen=False) + except Exception: + sys.stderr.write("Error unlocking credential store\n") + sys.exit(1) + if not cfm._masterkey: + sys.stderr.write("Error unlocking credential store\n") + sys.exit(1) + cfg = cfm.ConfigManager(None) + try: + nodes = noderange.NodeRange(nr, config=cfg).nodes + except Exception as e: + sys.stderr.write("Invalid Noderange: " + str(e) + "\n") + sys.exit(1) + attrs = cfg.get_node_attributes(list(nodes), requested, + decrypt=options.unprotected) + # Order nodes and attributes exactly as the server/nodeattrib do: + # natural sort via humanify_nodename (identical to the client's + # naturalize_string), with the same TypeError fallback as core.py. + nodelist = list(attrs) + try: + nodelist.sort(key=noderange.humanify_nodename) + except TypeError: + nodelist.sort() + for node in nodelist: + nodeattrs = attrs[node] + anames = list(nodeattrs) + try: + anames.sort(key=noderange.humanify_nodename) + except TypeError: + anames.sort() + for aname in anames: + adata = nodeattrs[aname] + # Format like nodeattrib: a plain, list, or (decrypted) secret.* + # value lives in 'value'; some attributes come back as a bare list; + # crypted.* is a one-way hash stored as 'hashvalue'. Without -u, + # sensitive values are masked the same way nodeattrib does. + if isinstance(adata, (list, tuple)): + val = ','.join(_dispval(x) for x in adata) + elif 'value' in adata: + aval = adata['value'] + if aval is None: + val = '' + elif isinstance(aval, (list, tuple)): + val = ','.join(_dispval(x) for x in aval) + else: + val = _dispval(aval) + elif 'hashvalue' in adata: + if options.unprotected: + val = _dispval(adata['hashvalue']) + else: + val = '******** (hidden: use -u to show)' + elif 'cryptvalue' in adata: + val = '******** (hidden: use -u to show)' + else: + val = '' + if options.valueonly: + print(val) + else: + print('{0}: {1}: {2}'.format(node, aname, val)) + # Report explicitly named (non-glob) attributes that are not set. + for attr in requested: + if '*' not in attr and attr not in nodeattrs: + if options.valueonly: + print('') + else: + print('{0}: {1}:'.format(node, attr)) + sys.exit(0) + + +if args and args[0] == 'showattrib': + show_attrib(options, args) if len(args) != 2 or args[0] not in ('dump', 'restore', 'merge'): argparser.print_help() sys.exit(1)