1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
|
#!/usr/bin/python
# vi: ts=4 expandtab
#
# Copyright (C) 2012 Canonical Ltd.
# Copyright (C) 2012 Hewlett-Packard Development Company, L.P.
#
# Author: Scott Moser <scott.moser@canonical.com>
# Author: Juerg Hafliger <juerg.haefliger@hp.com>
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License version 3, as
# published by the Free Software Foundation.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
from StringIO import StringIO
import csv
import os
import pwd
from cloudinit import log as logging
from cloudinit import util
LOG = logging.getLogger(__name__)
DEF_SSHD_CFG = "/etc/ssh/sshd_config"
class AuthKeyEntry(object):
"""
AUTHORIZED_KEYS FILE FORMAT
AuthorizedKeysFile specifies the file containing public keys for public
key authentication; if none is specified, the default is
~/.ssh/authorized_keys. Each line of the file contains one key (empty
(because of the size of the public key encoding) up to a limit of 8 kilo-
bytes, which permits DSA keys up to 8 kilobits and RSA keys up to 16
kilobits. You don't want to type them in; instead, copy the
identity.pub, id_dsa.pub, or the id_rsa.pub file and edit it.
sshd enforces a minimum RSA key modulus size for protocol 1 and protocol
2 keys of 768 bits.
The options (if present) consist of comma-separated option specifica-
tions. No spaces are permitted, except within double quotes. The fol-
lowing option specifications are supported (note that option keywords are
case-insensitive):
"""
def __init__(self, line, def_opt=None):
self.line = str(line)
(self.value, self.components) = self._parse(self.line, def_opt)
def _extract_options(self, ent):
"""
The options (if present) consist of comma-separated option specifica-
tions. No spaces are permitted, except within double quotes.
Note that option keywords are case-insensitive.
"""
quoted = False
i = 0
while (i < len(ent) and
((quoted) or (ent[i] not in (" ", "\t")))):
curc = ent[i]
if i + 1 >= len(ent):
i = i + 1
break
nextc = ent[i + 1]
if curc == "\\" and nextc == '"':
i = i + 1
elif curc == '"':
quoted = not quoted
i = i + 1
options = ent[0:i]
options_lst = []
reader = csv.reader(StringIO(options), quoting=csv.QUOTE_NONE)
for row in reader:
for e in row:
e = e.strip()
if e:
options_lst.append(e)
toks = []
if i + 1 < len(ent):
toks = ent[i + 1:].split(None, 3)
return (options_lst, toks)
def _form_components(self, toks):
components = {}
if len(toks) == 1:
components['base64'] = toks[0]
elif len(toks) == 2:
components['base64'] = toks[0]
components['comment'] = toks[1]
elif len(toks) == 3:
components['keytype'] = toks[0]
components['base64'] = toks[1]
components['comment'] = toks[2]
return components
def get(self, piece):
return self.components.get(piece)
def _parse(self, in_line, def_opt):
line = in_line.rstrip("\r\n")
if line.startswith("#") or line.strip() == '':
return (False, {})
else:
ent = line.strip()
toks = ent.split(None, 3)
tmp_components = {}
if def_opt:
tmp_components['options'] = def_opt
if len(toks) < 4:
tmp_components.update(self._form_components(toks))
else:
(options, toks) = self._extract_options(ent)
if options:
tmp_components['options'] = ",".join(options)
tmp_components.update(self._form_components(toks))
# We got some useful value!
return (True, tmp_components)
def __str__(self):
if not self.value:
return self.line
else:
toks = []
if 'options' in self.components:
toks.append(self.components['options'])
if 'keytype' in self.components:
toks.append(self.components['keytype'])
if 'base64' in self.components:
toks.append(self.components['base64'])
if 'comment' in self.components:
toks.append(self.components['comment'])
if not toks:
return ''
return ' '.join(toks)
def update_authorized_keys(fname, keys):
lines = []
try:
if os.path.isfile(fname):
lines = util.load_file(fname).splitlines()
except (IOError, OSError):
util.logexc(LOG, "Error reading lines from %s", fname)
lines = []
to_add = list(keys)
for i in range(0, len(lines)):
ent = AuthKeyEntry(lines[i])
if not ent.value:
continue
# Replace those with the same base64
for k in keys:
if not k.value:
continue
if k.get('base64') == ent.get('base64'):
# Replace it with our better one
ent = k
# Don't add it later
to_add.remove(k)
lines[i] = str(ent)
# Now append any entries we did not match above
for key in to_add:
lines.append(str(key))
# Ensure it ends with a newline
lines.append('')
return '\n'.join(lines)
def setup_user_keys(keys, user, key_prefix, sshd_config_fn=None):
if not sshd_config_fn:
sshd_config_fn = DEF_SSHD_CFG
pwent = pwd.getpwnam(user)
ssh_dir = os.path.join(pwent.pw_dir, '.ssh')
if not os.path.exists(ssh_dir):
util.ensure_dir(ssh_dir, mode=0700)
util.chownbyid(ssh_dir, pwent.pw_uid, pwent.pw_gid)
key_entries = []
for k in keys:
key_entries.append(AuthKeyEntry(k, def_opt=key_prefix))
with util.SeLinuxGuard(ssh_dir, recursive=True):
try:
# AuthorizedKeysFile may contain tokens
# of the form %T which are substituted during connection set-up.
# The following tokens are defined: %% is replaced by a literal
# '%', %h is replaced by the home directory of the user being
# authenticated and %u is replaced by the username of that user.
ssh_cfg = parse_ssh_config(sshd_config_fn)
akeys = ssh_cfg.get("authorizedkeysfile", '')
akeys = akeys.strip()
if not akeys:
akeys = "%h/.ssh/authorized_keys"
akeys = akeys.replace("%h", pwent.pw_dir)
akeys = akeys.replace("%u", user)
akeys = akeys.replace("%%", '%')
if not akeys.startswith('/'):
akeys = os.path.join(pwent.pw_dir, akeys)
authorized_keys = akeys
except (IOError, OSError):
authorized_keys = os.path.join(ssh_dir, 'authorized_keys')
util.logexc(LOG, ("Failed extracting 'AuthorizedKeysFile'"
" in ssh config"
" from %s, using 'AuthorizedKeysFile' file"
" %s instead"),
sshd_config_fn, authorized_keys)
content = update_authorized_keys(authorized_keys, key_entries)
util.ensure_dir(os.path.dirname(authorized_keys), mode=0700)
util.write_file(authorized_keys, content, mode=0600)
util.chownbyid(authorized_keys, pwent.pw_uid, pwent.pw_gid)
def parse_ssh_config(fname):
# The file contains keyword-argument pairs, one per line.
# Lines starting with '#' and empty lines are interpreted as comments.
# Note: key-words are case-insensitive and arguments are case-sensitive
ret = {}
if not os.path.isfile(fname):
return ret
for line in util.load_file(fname).splitlines():
line = line.strip()
if not line or line.startswith("#"):
continue
(key, val) = line.split(None, 1)
key = key.strip().lower()
if key:
ret[key] = val
return ret
|