1
// Copyright 2013 Canonical Ltd.
2
// Licensed under the AGPLv3, see LICENCE file for details.
17
"github.com/juju/errors"
18
"github.com/juju/loggo"
19
"github.com/juju/utils"
20
"golang.org/x/crypto/ssh"
23
var logger = loggo.GetLogger("juju.utils.ssh")
28
FullKeys ListMode = true
29
Fingerprints ListMode = false
33
authKeysFile = "authorized_keys"
36
type AuthorisedKey struct {
42
func authKeysDir(username string) (string, error) {
43
homeDir, err := utils.UserHomeDir(username)
47
homeDir, err = utils.NormalizePath(homeDir)
51
return filepath.Join(homeDir, ".ssh"), nil
54
// ParseAuthorisedKey parses a non-comment line from an
55
// authorized_keys file and returns the constituent parts.
56
// Based on description in "man sshd".
57
func ParseAuthorisedKey(line string) (*AuthorisedKey, error) {
58
key, comment, _, _, err := ssh.ParseAuthorizedKey([]byte(line))
60
return nil, errors.Errorf("invalid authorized_key %q", line)
62
return &AuthorisedKey{
69
// SplitAuthorisedKeys extracts a key slice from the specified key data,
70
// by splitting the key data into lines and ignoring comments and blank lines.
71
func SplitAuthorisedKeys(keyData string) []string {
73
for _, key := range strings.Split(string(keyData), "\n") {
74
key = strings.Trim(key, " \r")
81
keys = append(keys, key)
86
func readAuthorisedKeys(username string) ([]string, error) {
87
keyDir, err := authKeysDir(username)
91
sshKeyFile := filepath.Join(keyDir, authKeysFile)
92
logger.Debugf("reading authorised keys file %s", sshKeyFile)
93
keyData, err := ioutil.ReadFile(sshKeyFile)
94
if os.IsNotExist(err) {
95
return []string{}, nil
98
return nil, errors.Annotate(err, "reading ssh authorised keys file")
101
for _, key := range strings.Split(string(keyData), "\n") {
102
if len(strings.Trim(key, " \r")) == 0 {
105
keys = append(keys, key)
110
func writeAuthorisedKeys(username string, keys []string) error {
111
keyDir, err := authKeysDir(username)
115
err = os.MkdirAll(keyDir, os.FileMode(0755))
117
return errors.Annotate(err, "cannot create ssh key directory")
119
keyData := strings.Join(keys, "\n") + "\n"
121
// Get perms to use on auth keys file
122
sshKeyFile := filepath.Join(keyDir, authKeysFile)
123
perms := os.FileMode(0644)
124
info, err := os.Stat(sshKeyFile)
126
perms = info.Mode().Perm()
129
logger.Debugf("writing authorised keys file %s", sshKeyFile)
130
err = utils.AtomicWriteFile(sshKeyFile, []byte(keyData), perms)
135
// TODO (wallyworld) - what to do on windows (if anything)
136
// TODO(dimitern) - no need to use user.Current() if username
137
// is "" - it will use the current user anyway.
138
if runtime.GOOS != "windows" {
139
// Ensure the resulting authorised keys file has its ownership
140
// set to the specified username.
143
u, err = user.Current()
145
u, err = user.Lookup(username)
150
// chown requires ints but user.User has strings for windows.
151
uid, err := strconv.Atoi(u.Uid)
155
gid, err := strconv.Atoi(u.Gid)
159
err = os.Chown(sshKeyFile, uid, gid)
167
// We need a mutex because updates to the authorised keys file are done by
168
// reading the contents, updating, and writing back out. So only one caller
169
// at a time can use either Add, Delete, List.
172
// AddKeys adds the specified ssh keys to the authorized_keys file for user.
173
// Returns an error if there is an issue with *any* of the supplied keys.
174
func AddKeys(user string, newKeys ...string) error {
177
existingKeys, err := readAuthorisedKeys(user)
181
for _, newKey := range newKeys {
182
fingerprint, comment, err := KeyFingerprint(newKey)
187
return errors.Errorf("cannot add ssh key without comment")
189
for _, key := range existingKeys {
190
existingFingerprint, existingComment, err := KeyFingerprint(key)
192
// Only log a warning if the unrecognised key line is not a comment.
194
logger.Warningf("invalid existing ssh key %q: %v", key, err)
198
if existingFingerprint == fingerprint {
199
return errors.Errorf("cannot add duplicate ssh key: %v", fingerprint)
201
if existingComment == comment {
202
return errors.Errorf("cannot add ssh key with duplicate comment: %v", comment)
206
sshKeys := append(existingKeys, newKeys...)
207
return writeAuthorisedKeys(user, sshKeys)
210
// DeleteKeys removes the specified ssh keys from the authorized ssh keys file for user.
211
// keyIds may be either key comments or fingerprints.
212
// Returns an error if there is an issue with *any* of the keys to delete.
213
func DeleteKeys(user string, keyIds ...string) error {
216
existingKeyData, err := readAuthorisedKeys(user)
220
// Build up a map of keys indexed by fingerprint, and fingerprints indexed by comment
221
// so we can easily get the key represented by each keyId, which may be either a fingerprint
223
var keysToWrite []string
224
var sshKeys = make(map[string]string)
225
var keyComments = make(map[string]string)
226
for _, key := range existingKeyData {
227
fingerprint, comment, err := KeyFingerprint(key)
229
logger.Debugf("keeping unrecognised existing ssh key %q: %v", key, err)
230
keysToWrite = append(keysToWrite, key)
233
sshKeys[fingerprint] = key
235
keyComments[comment] = fingerprint
238
for _, keyId := range keyIds {
239
// assume keyId may be a fingerprint
241
_, ok := sshKeys[keyId]
243
// keyId is a comment
244
fingerprint, ok = keyComments[keyId]
247
return errors.Errorf("cannot delete non existent key: %v", keyId)
249
delete(sshKeys, fingerprint)
251
for _, key := range sshKeys {
252
keysToWrite = append(keysToWrite, key)
254
if len(keysToWrite) == 0 {
255
return errors.Errorf("cannot delete all keys")
257
return writeAuthorisedKeys(user, keysToWrite)
260
// ReplaceKeys writes the specified ssh keys to the authorized_keys file for user,
261
// replacing any that are already there.
262
// Returns an error if there is an issue with *any* of the supplied keys.
263
func ReplaceKeys(user string, newKeys ...string) error {
267
existingKeyData, err := readAuthorisedKeys(user)
271
var existingNonKeyLines []string
272
for _, line := range existingKeyData {
273
_, _, err := KeyFingerprint(line)
275
existingNonKeyLines = append(existingNonKeyLines, line)
278
return writeAuthorisedKeys(user, append(existingNonKeyLines, newKeys...))
281
// ListKeys returns either the full keys or key comments from the authorized ssh keys file for user.
282
func ListKeys(user string, mode ListMode) ([]string, error) {
285
keyData, err := readAuthorisedKeys(user)
290
for _, key := range keyData {
291
fingerprint, comment, err := KeyFingerprint(key)
293
// Only log a warning if the unrecognised key line is not a comment.
295
logger.Warningf("ignoring invalid ssh key %q: %v", key, err)
299
if mode == FullKeys {
300
keys = append(keys, key)
302
shortKey := fingerprint
304
shortKey += fmt.Sprintf(" (%s)", comment)
306
keys = append(keys, shortKey)
312
// Any ssh key added to the authorised keys list by Juju will have this prefix.
313
// This allows Juju to know which keys have been added externally and any such keys
314
// will always be retained by Juju when updating the authorised keys file.
315
const JujuCommentPrefix = "Juju:"
317
func EnsureJujuComment(key string) string {
318
ak, err := ParseAuthorisedKey(key)
319
// Just return an invalid key as is.
321
logger.Warningf("invalid Juju ssh key %s: %v", key, err)
324
if ak.Comment == "" {
325
return key + " " + JujuCommentPrefix + "sshkey"
327
// Add the Juju prefix to the comment if necessary.
328
if !strings.HasPrefix(ak.Comment, JujuCommentPrefix) {
329
commentIndex := strings.LastIndex(key, ak.Comment)
330
return key[:commentIndex] + JujuCommentPrefix + ak.Comment