These changes update the .ssh/authorized_keys rather than simply appending This is preferable as ssh daemon picks the first key that is present. This fixes 2 issues where something had edited a .ssh/authorized_keys prior to cloud-init getting at it. a.) LP: #434076 a user prior to re-bundling b.) LP: #833499 the hypervisor If you want to enable ssh access for root user, the proper way to do it is with 'disable_root: False' in cloud-config.
		
			
				
	
	
		
			196 lines
		
	
	
		
			6.5 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
			
		
		
	
	
			196 lines
		
	
	
		
			6.5 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
#!/usr/bin/python
 | 
						|
 | 
						|
import os
 | 
						|
import os.path
 | 
						|
import cloudinit.util as util
 | 
						|
 | 
						|
class AuthKeyEntry():
 | 
						|
    # lines are options, keytype, base64-encoded key, comment
 | 
						|
    # man page says the following which I did not understand:
 | 
						|
    #   The options field is optional; its presence is determined by whether
 | 
						|
    #   the line starts with a number or not (the options field never starts
 | 
						|
    #   with a number)
 | 
						|
    options = None
 | 
						|
    keytype = None
 | 
						|
    base64 = None
 | 
						|
    comment = None
 | 
						|
    is_comment = False
 | 
						|
    line_in = ""
 | 
						|
 | 
						|
    def __init__(self, line, def_opt=None):
 | 
						|
        line=line.rstrip("\n\r")
 | 
						|
        self.line_in = line
 | 
						|
        if line.startswith("#") or line.strip() == "":
 | 
						|
            self.is_comment = True
 | 
						|
        else:
 | 
						|
            ent = line.strip()
 | 
						|
            toks = ent.split(None,3)
 | 
						|
            if len(toks) == 1:
 | 
						|
                self.base64 = toks[0]
 | 
						|
            elif len(toks) == 2:
 | 
						|
                (self.base64, self.comment) = toks
 | 
						|
            elif len(toks) == 3:
 | 
						|
                (self.keytype, self.base64, self.comment) = toks
 | 
						|
            elif len(toks) == 4:
 | 
						|
                i = 0
 | 
						|
                ent = line.strip()
 | 
						|
                quoted = False
 | 
						|
                # taken from auth_rsa_key_allowed in auth-rsa.c
 | 
						|
                try:
 | 
						|
                    while (i < len(ent) and 
 | 
						|
                           ((quoted) or (ent[i] not in (" ", "\t")))):
 | 
						|
                        curc = ent[i]
 | 
						|
                        nextc = ent[i + 1]
 | 
						|
                        if curc == "\\" and nextc == '"':
 | 
						|
                            i = i + 1
 | 
						|
                        elif curc == '"':
 | 
						|
                            quoted = not quoted
 | 
						|
                        i = i + 1
 | 
						|
                except IndexError as e:
 | 
						|
                    self.is_comment = True
 | 
						|
                    return()
 | 
						|
 | 
						|
                try:
 | 
						|
                    self.options = ent[0:i]
 | 
						|
                    (self.keytype, self.base64, self.comment) = \
 | 
						|
                        ent[i+1:].split(None,3)
 | 
						|
                except ValueError as e:
 | 
						|
                    # we did not understand this line
 | 
						|
                    self.is_comment = True
 | 
						|
 | 
						|
        if self.options == None and def_opt:
 | 
						|
            self.options = def_opt
 | 
						|
 | 
						|
        return
 | 
						|
 | 
						|
    def debug(self):
 | 
						|
        print("line_in=%s\ncomment: %s\noptions=%s\nkeytype=%s\nbase64=%s\ncomment=%s\n" %
 | 
						|
            (self.line_in, self.is_comment, self.options, self.keytype, self.base64, self.comment)),
 | 
						|
    def __repr__(self):
 | 
						|
        if self.is_comment:
 | 
						|
            return(self.line_in)
 | 
						|
        else:
 | 
						|
            toks = [ ]
 | 
						|
            for e in (self.options, self.keytype, self.base64, self.comment):
 | 
						|
                if e:
 | 
						|
                    toks.append(e)
 | 
						|
                
 | 
						|
            return(' '.join(toks))
 | 
						|
            
 | 
						|
def update_authorized_keys(fname, keys):
 | 
						|
    # keys is a list of AuthKeyEntries
 | 
						|
    # key_prefix is the prefix (options) to prepend
 | 
						|
    try:
 | 
						|
        fp = open(fname, "r")
 | 
						|
        lines = fp.readlines() # lines have carriage return
 | 
						|
        fp.close()
 | 
						|
    except IOError as e:
 | 
						|
        lines = [ ]
 | 
						|
 | 
						|
    ka_stats = { } # keys_added status
 | 
						|
    for k in keys:
 | 
						|
        ka_stats[k] = False
 | 
						|
 | 
						|
    to_add = []
 | 
						|
    for key in keys:
 | 
						|
        to_add.append(key)
 | 
						|
 | 
						|
    for i in range(0,len(lines)):
 | 
						|
        ent = AuthKeyEntry(lines[i])
 | 
						|
        for k in keys:
 | 
						|
            if k.base64 == ent.base64 and not k.is_comment:
 | 
						|
                ent = k
 | 
						|
                try:
 | 
						|
                    to_add.remove(k)
 | 
						|
                except ValueError:
 | 
						|
                    pass
 | 
						|
        lines[i] = str(ent)
 | 
						|
 | 
						|
    # now append any entries we did not match above
 | 
						|
    for key in to_add:
 | 
						|
        lines.append(str(key))
 | 
						|
 | 
						|
    if len(lines) == 0:
 | 
						|
        return("")
 | 
						|
    else:
 | 
						|
        return('\n'.join(lines) + "\n")
 | 
						|
 | 
						|
    
 | 
						|
def setup_user_keys(keys, user, key_prefix, log=None):
 | 
						|
    import pwd
 | 
						|
    saved_umask = os.umask(077)
 | 
						|
 | 
						|
    pwent = pwd.getpwnam(user)
 | 
						|
 | 
						|
    ssh_dir = '%s/.ssh' % pwent.pw_dir
 | 
						|
    if not os.path.exists(ssh_dir):
 | 
						|
        os.mkdir(ssh_dir)
 | 
						|
        os.chown(ssh_dir, pwent.pw_uid, pwent.pw_gid)
 | 
						|
 | 
						|
    try:
 | 
						|
        ssh_cfg = parse_ssh_config()
 | 
						|
        akeys = ssh_cfg.get("AuthorizedKeysFile","%h/.ssh/authorized_keys")
 | 
						|
        akeys = akeys.replace("%h", pwent.pw_dir)
 | 
						|
        akeys = akeys.replace("%u", user)
 | 
						|
        authorized_keys = akeys
 | 
						|
    except Exception as e:
 | 
						|
        authorized_keys = '%s/.ssh/authorized_keys' % pwent.pw_dir
 | 
						|
        if log:
 | 
						|
            util.logexc(log)
 | 
						|
 | 
						|
    key_entries = []
 | 
						|
    for k in keys:
 | 
						|
        ke = AuthKeyEntry(k, def_opt=key_prefix)
 | 
						|
        key_entries.append(ke)
 | 
						|
 | 
						|
    content = update_authorized_keys(authorized_keys, key_entries)
 | 
						|
    util.write_file(authorized_keys, content, 0600)
 | 
						|
 | 
						|
    os.chown(authorized_keys, pwent.pw_uid, pwent.pw_gid)
 | 
						|
 | 
						|
    os.umask(saved_umask)
 | 
						|
 | 
						|
if __name__ == "__main__":
 | 
						|
    import sys
 | 
						|
    # usage: orig_file, new_keys, [key_prefix]
 | 
						|
    #   prints out merged, where 'new_keys' will trump old
 | 
						|
    ##  example
 | 
						|
    ## ### begin authorized_keys ###
 | 
						|
    # ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAIEA28CDAGtxSucHezSKqwh1wAs39xdeZTSVmmyMcKDI5Njnd1d/Uhgj/awxP0Whep8eRSm6F+Xgwi0pH1KNPCszPvq+03K+yi3YkYkQIkVBhctK6AP/UmlVQTVmjJdEvgtrppFTjCzf16q0BT0mXX5YFV3csgm8cJn7UveKHkYjJp8= smoser-work
 | 
						|
    # ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEA3I7VUf2l5gSn5uavROsc5HRDpZdQueUq5ozemNSj8T7enqKHOEaFoU2VoPgGEWC9RyzSQVeyD6s7APMcE82EtmW4skVEgEGSbDc1pvxzxtchBj78hJP6Cf5TCMFSXw+Fz5rF1dR23QDbN1mkHs7adr8GW4kSWqU7Q7NDwfIrJJtO7Hi42GyXtvEONHbiRPOe8stqUly7MvUoN+5kfjBM8Qqpfl2+FNhTYWpMfYdPUnE7u536WqzFmsaqJctz3gBxH9Ex7dFtrxR4qiqEr9Qtlu3xGn7Bw07/+i1D+ey3ONkZLN+LQ714cgj8fRS4Hj29SCmXp5Kt5/82cD/VN3NtHw== smoser@brickies
 | 
						|
    # ### end authorized_keys ###
 | 
						|
    # 
 | 
						|
    # ### begin new_keys ###
 | 
						|
    # ssh-rsa nonmatch smoser@newhost
 | 
						|
    # ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAIEA28CDAGtxSucHezSKqwh1wAs39xdeZTSVmmyMcKDI5Njnd1d/Uhgj/awxP0Whep8eRSm6F+Xgwi0pH1KNPCszPvq+03K+yi3YkYkQIkVBhctK6AP/UmlVQTVmjJdEvgtrppFTjCzf16q0BT0mXX5YFV3csgm8cJn7UveKHkYjJp8= new_comment
 | 
						|
    # ### end new_keys ###
 | 
						|
    #
 | 
						|
    # Then run as:
 | 
						|
    #  program authorized_keys new_keys 'no-port-forwarding,command=\"echo hi world;\"'
 | 
						|
    def_prefix = None
 | 
						|
    orig_key_file = sys.argv[1]
 | 
						|
    new_key_file = sys.argv[2]
 | 
						|
    if len(sys.argv) > 3:
 | 
						|
        def_prefix = sys.argv[3]
 | 
						|
    fp = open(new_key_file)
 | 
						|
 | 
						|
    newkeys = [ ]
 | 
						|
    for line in fp.readlines():
 | 
						|
        newkeys.append(AuthKeyEntry(line, def_prefix))
 | 
						|
 | 
						|
    fp.close()
 | 
						|
    print update_authorized_keys(orig_key_file, newkeys)
 | 
						|
 | 
						|
def parse_ssh_config(fname="/etc/ssh/sshd_config"):
 | 
						|
    ret = { }
 | 
						|
    fp=open(fname)
 | 
						|
    for l in fp.readlines():
 | 
						|
        l = l.strip()
 | 
						|
        if not l or l.startswith("#"):
 | 
						|
            continue
 | 
						|
        key,val = l.split(None,1)
 | 
						|
        ret[key]=val
 | 
						|
    fp.close()
 | 
						|
    return(ret)
 | 
						|
 |