improve updating of .ssh/authorized_keys
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.
This commit is contained in:
parent
d5b12ce1ac
commit
c67578bbd9
@ -44,6 +44,9 @@
|
||||
This was done by changing all users of util.subp to have None input unless specified
|
||||
- Add some debug info to the console when cloud-init runs.
|
||||
This is useful if debugging, IP and route information is printed to the console.
|
||||
- change the mechanism for handling .ssh/authorized_keys, to update entries
|
||||
rather than appending. This ensures that the authorized_keys that are being
|
||||
inserted actually do something (LP: #434076, LP: #833499)
|
||||
|
||||
0.6.1:
|
||||
- fix bug in fixing permission on /var/log/cloud-init.log (LP: #704509)
|
||||
|
@ -16,6 +16,7 @@
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
import cloudinit.util as util
|
||||
import cloudinit.SshUtil as sshutil
|
||||
import os
|
||||
import glob
|
||||
import subprocess
|
||||
@ -86,57 +87,15 @@ def handle(name,cfg,cloud,log,args):
|
||||
def send_ssh_keys_to_console():
|
||||
subprocess.call(('/usr/lib/cloud-init/write-ssh-key-fingerprints',))
|
||||
|
||||
def apply_credentials(keys, user, disable_root, disable_root_opts=DISABLE_ROOT_OPTS):
|
||||
def apply_credentials(keys, user, disable_root, disable_root_opts=DISABLE_ROOT_OPTS, log=global_log):
|
||||
keys = set(keys)
|
||||
if user:
|
||||
setup_user_keys(keys, user, '')
|
||||
sshutil.setup_user_keys(keys, user, '', log)
|
||||
|
||||
if disable_root:
|
||||
key_prefix = disable_root_opts.replace('$USER', user)
|
||||
else:
|
||||
key_prefix = ''
|
||||
|
||||
setup_user_keys(keys, 'root', key_prefix)
|
||||
|
||||
def setup_user_keys(keys, user, key_prefix):
|
||||
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
|
||||
util.logexc(global_log)
|
||||
|
||||
fp = open(authorized_keys, 'a')
|
||||
key_prefix = key_prefix.replace("\n"," ")
|
||||
fp.write(''.join(['%s %s\n' % (key_prefix.strip(), key) for key in keys]))
|
||||
fp.close()
|
||||
|
||||
os.chown(authorized_keys, pwent.pw_uid, pwent.pw_gid)
|
||||
|
||||
os.umask(saved_umask)
|
||||
|
||||
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)
|
||||
sshutil.setup_user_keys(keys, 'root', key_prefix, log)
|
||||
|
||||
|
195
cloudinit/SshUtil.py
Normal file
195
cloudinit/SshUtil.py
Normal file
@ -0,0 +1,195 @@
|
||||
#!/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)
|
||||
|
Loading…
x
Reference in New Issue
Block a user