summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorScott Moser <smoser@ubuntu.com>2011-08-29 20:26:17 -0400
committerScott Moser <smoser@ubuntu.com>2011-08-29 20:26:17 -0400
commitc7123a7f3bc08a2d45ce6b2d66107a669284e3f1 (patch)
tree019ba24b8da4ba9b549883f1de4b4f6ab12a02bd
parent5e0edd8cf0a8431d453649037da913285e28850f (diff)
downloadvyos-cloud-init-c7123a7f3bc08a2d45ce6b2d66107a669284e3f1.tar.gz
vyos-cloud-init-c7123a7f3bc08a2d45ce6b2d66107a669284e3f1.zip
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. LP: #434076, #833499
-rw-r--r--ChangeLog3
-rw-r--r--cloudinit/CloudConfig/cc_ssh.py49
-rw-r--r--cloudinit/SshUtil.py195
3 files changed, 202 insertions, 45 deletions
diff --git a/ChangeLog b/ChangeLog
index 71762a9e..175e9b15 100644
--- a/ChangeLog
+++ b/ChangeLog
@@ -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)
diff --git a/cloudinit/CloudConfig/cc_ssh.py b/cloudinit/CloudConfig/cc_ssh.py
index ddeb5009..50b6a73c 100644
--- a/cloudinit/CloudConfig/cc_ssh.py
+++ b/cloudinit/CloudConfig/cc_ssh.py
@@ -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)
diff --git a/cloudinit/SshUtil.py b/cloudinit/SshUtil.py
new file mode 100644
index 00000000..bc699a61
--- /dev/null
+++ b/cloudinit/SshUtil.py
@@ -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)
+