From a26624a614f63209005ec503b794c5739c6dbcca Mon Sep 17 00:00:00 2001 From: Jarrod Johnson Date: Tue, 9 Mar 2021 13:34:00 -0500 Subject: [PATCH] Use ssh-agent to store keys Also add the 'automation' key for ansible to take advantage of. --- confluent_server/bin/osdeploy | 6 ++ confluent_server/confluent/sshutil.py | 88 +++++++++++++++++++++++---- 2 files changed, 82 insertions(+), 12 deletions(-) diff --git a/confluent_server/bin/osdeploy b/confluent_server/bin/osdeploy index d0023493..ca50b8ad 100644 --- a/confluent_server/bin/osdeploy +++ b/confluent_server/bin/osdeploy @@ -38,6 +38,7 @@ def main(args): ap = argparse.ArgumentParser(description='Manage OS deployment resources') sp = ap.add_subparsers(dest='command') wiz = sp.add_parser('initialize', help='Do OS deployment preparation') + wiz.add_argument('-a', help='Initialize SSH access by confluent to nodes for automation such as ansible playbook execution', action='store_true') wiz.add_argument('-g', help='Initialize a Genesis profile to boot systems into a rescue or staging environment', action='store_true') wiz.add_argument('-u', help='Pull in root user key for node deployment', action='store_true') wiz.add_argument('-s', help='Set up SSH CA for managing node to node ssh and known hosts', action='store_true') @@ -206,6 +207,8 @@ def initialize(cmdset): cmdset.l = input().strip().lower().startswith('y') sys.stdout.write('Update tftp directory with binaries to support PXE (-p) (y/n): ') cmdset.p = input().strip().lower().startswith('y') + sys.stdout.write('Initialize confluent ssh user key so confluent can execute remote automation (e.g. Ansible plays) (-a) (y/n): ') + cmdset.a = input().strip().lower().startswith('y') sys.stdout.write('Generate new TLS certificates for HTTP, replacing any existing certificate (-t)? (y/n): ') cmdset.t = input().strip().lower().startswith('y') if not cmdset.t: @@ -241,6 +244,9 @@ def initialize(cmdset): if cmdset.s: didsomething = True sshutil.initialize_ca() + if cmdset.a: + didsomething = True + sshutil.initialize_root_key(True, True) if cmdset.p: install_tftp_content() if cmdset.l: diff --git a/confluent_server/confluent/sshutil.py b/confluent_server/confluent/sshutil.py index c708f403..9002009b 100644 --- a/confluent_server/confluent/sshutil.py +++ b/confluent_server/confluent/sshutil.py @@ -1,5 +1,7 @@ #!/usr/bin/python +import base64 +import confluent.config.configmanager as cfm import confluent.collective.manager as collective import eventlet.green.subprocess as subprocess import glob @@ -7,6 +9,9 @@ import os import shutil import tempfile +agent_pid = None +ready_keys = {} + def normalize_uid(): curruid = os.geteuid() neededuid = os.stat('/etc/confluent').st_uid @@ -17,6 +22,34 @@ def normalize_uid(): return curruid +def assure_agent(): + global agent_pid + if agent_pid is None: + sai = subprocess.check_output(['ssh-agent']) + for line in sai.split(b'\n'): + if b';' not in line: + continue + line, _ = line.split(b';', 1) + if b'=' not in line: + continue + k, v = line.split(b'=', 1) + if not isinstance(k, str): + k = k.decode('utf8') + v = v.decode('utf8') + if k == 'SSH_AGENT_PID': + agent_pid = v + os.environ[k] = v + +def get_passphrase(): + # convert the master key to base64 + # for use in ssh passphrase context + if cfm._masterkey is None: + cfm.init_masterkey() + phrase = base64.b64encode(cfm._masterkey) + if not isinstance(phrase, str): + phrase = phrase.decode('utf8') + return phrase + def initialize_ca(): ouid = normalize_uid() try: @@ -27,10 +60,11 @@ def initialize_ca(): finally: os.seteuid(ouid) myname = collective.get_myname() - caname = '{0} SSH CA'.format(myname) + comment = '{0} SSH CA'.format(myname) subprocess.check_call( - ['ssh-keygen', '-C', caname, '-t', 'ed25519', '-f', - '/etc/confluent/ssh/ca', '-N', ''], preexec_fn=normalize_uid) + ['ssh-keygen', '-C', comment, '-t', 'ed25519', '-f', + '/etc/confluent/ssh/ca', '-N', get_passphrase()], + preexec_fn=normalize_uid) try: os.makedirs('/var/lib/confluent/public/site/ssh/', mode=0o755) except OSError as e: @@ -41,9 +75,29 @@ def initialize_ca(): # newent = '@cert-authority * ' + capub.read() +def prep_ssh_key(keyname): + assure_agent() + tmpdir = tempfile.mkdtemp() + try: + askpass = os.path.join(tmpdir, 'askpass.sh') + with open(askpass, 'w') as ap: + ap.write('#!/bin/sh\necho $CONFLUENT_SSH_PASSPHRASE\n') + os.chmod(askpass, 0o700) + os.environ['CONFLUENT_SSH_PASSPHRASE'] = get_passphrase() + os.environ['DISPLAY'] = 'NONE' + os.environ['SSH_ASKPASS'] = askpass + with open(os.devnull, 'wb') as devnull: + subprocess.check_call(['ssh-add', keyname], stdin=devnull) + del os.environ['CONFLUENT_SSH_PASSPHRASE'] + finally: + shutil.rmtree(tmpdir) + def sign_host_key(pubkey, nodename, principals=()): tmpdir = tempfile.mkdtemp() try: + if 'ca.pub' not in ready_keys: + prep_ssh_key('/etc/confluent/ssh/ca') + ready_keys['ca.pub'] = 1 pkeyname = os.path.join(tmpdir, 'hostkey.pub') with open(pkeyname, 'wb') as pubfile: pubfile.write(pubkey) @@ -51,7 +105,7 @@ def sign_host_key(pubkey, nodename, principals=()): principals.add(nodename) principals = ','.join(sorted(principals)) subprocess.check_call( - ['ssh-keygen', '-s', '/etc/confluent/ssh/ca', '-I', nodename, + ['ssh-keygen', '-Us', '/etc/confluent/ssh/ca.pub', '-I', nodename, '-n', principals, '-h', pkeyname]) certname = pkeyname.replace('.pub', '-cert.pub') with open(certname) as cert: @@ -59,15 +113,21 @@ def sign_host_key(pubkey, nodename, principals=()): finally: shutil.rmtree(tmpdir) -def initialize_root_key(generate): +def initialize_root_key(generate, automation=False): authorized = [] myname = collective.get_myname() for currkey in glob.glob('/root/.ssh/*.pub'): authorized.append(currkey) - if generate and not authorized: + if generate and not authorized and not automation: subprocess.check_call(['ssh-keygen', '-t', 'ed25519', '-f', '/root/.ssh/id_ed25519', '-N', '']) for currkey in glob.glob('/root/.ssh/*.pub'): authorized.append(currkey) + if automation and generate: + subprocess.check_call( + ['ssh-keygen', '-t', 'ed25519', + '-f','/etc/confluent/ssh/automation', '-N', get_passphrase(), + '-C', 'Confluent Automation'], preexec_fn=normalize_uid) + authorized = ['/etc/confluent/ssh/automation.pub'] try: os.makedirs('/var/lib/confluent/public/site/ssh', mode=0o755) neededuid = os.stat('/etc/confluent').st_uid @@ -79,15 +139,19 @@ def initialize_root_key(generate): if e.errno != 17: raise neededuid = os.stat('/etc/confluent').st_uid + if automation: + suffix = 'automationpubkey' + else: + suffix = 'rootpubkey' for auth in authorized: shutil.copy( auth, - '/var/lib/confluent/public/site/ssh/{0}.rootpubkey'.format( - myname)) - os.chmod('/var/lib/confluent/public/site/ssh/{0}.rootpubkey'.format( - myname), 0o644) - os.chown('/var/lib/confluent/public/site/ssh/{0}.rootpubkey'.format( - myname), neededuid, -1) + '/var/lib/confluent/public/site/ssh/{0}.{1}'.format( + myname, suffix)) + os.chmod('/var/lib/confluent/public/site/ssh/{0}.{1}'.format( + myname, suffix), 0o644) + os.chown('/var/lib/confluent/public/site/ssh/{0}.{1}'.format( + myname, suffix), neededuid, -1) def ca_exists():