Key Manager Code
Status: Beta
Brought to you by:
zapman449
#!/usr/bin/python
################################################################################
# This file is part of key-manager.
#
# key-manager is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# key-manager 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 key-manager. If not, see <http://www.gnu.org/licenses/>.
################################################################################
import cPickle
import getopt
import getpass
import os
import os.path
import pexpect
import pwd
import pxssh
import socket
import sys
import telnetlib
################################################################################
class userKeyDataHolder :
"""A class to hold user and key information."""
# groups[groupID] = { pubkey : string,
# privkey : string,
# password : string,
# passphrase : string }
# servers = {}
# servers[servername] = [user1, user2, user3...]
#
# Definitions: server (or servername) is a name of a server (could be IP)
# remoteUser: a username on a remote system
# groupID: a name of an arbitrary group
# serverConn: an active/logged in pxssh connection.
def __init__(self, userPrefix='mgr', masterGroup='all',
accessName='access_', sshDirName='.ssh', keyType='rsa') :
"""This should never get called after the first run"""
self.groups = {}
self.toUpdate = [] # [ (server,user), (server2,user2)... ]
self.toReconcile = [] # [ (server,user), (server2,user2)... ]
self.userprefix = userPrefix
self.mastergroup = masterGroup
self.masteruser = self.userprefix + self.mastergroup # aka 'mgrall'
self.rootuser = 'root'
self.accessname = accessName # each groups' user will have
# access_<remoteuser>.txt in their home
# dir. This file lists all the servers
# they can access with the passphrase.
self.sshdirname = sshDirName
self.keytype = keyType # rsa or dsa
self.privfilename = os.path.join(self.sshdirname, 'id_' + self.keytype)
self.pubfilename = os.path.join(self.sshdirname, 'id_' +
self.keytype + '.pub')
# must be root
if who_am_i() != self.rootuser :
print "You're NOT ROOT! Non root users can not initialize!"
sys.exit(2)
if not self.add_group(self.mastergroup):
print "FAILURE TO CREATE 'ALL' GROUP! EXITING."
sys.exit(2)
########################################
# Functions to manage group creation and management.
########################################
def add_group(self, groupID) :
"""create a new group, create keys, generate passphrase, and users
password."""
# can be self.masteruser
if who_am_i() != self.rootuser :
print "NOT ROOT USER!"
return 0
if self.groups.has_key(groupID) :
print 'in add_group() BAD! Groupname already exists!'
return 0
localuser = self.groupID_to_user(groupID)
if check_user_exists(localuser) :
print 'in add_group(): local user %s already exists. Non fatal.' % (localuser)
print 'Their password and key will be reset'
userPassWord = nicepass()
keyPassPhrase, privKey, pubKey = generate_key(localuser + '@' )
# TODO: add correct local host name
if keyPassPhrase == 0 :
print 'error in generate key'
return 0
self.groups.setdefault(groupID,{})['pubkey'] = pubKey
self.groups[groupID]['privkey'] = privKey
self.groups[groupID]['password'] = userPassWord
self.groups[groupID]['passphrase'] = keyPassPhrase
self.groups[groupID]['servers'] = {}
if not self.__create_user(groupID):
print "FAILURE TO CREATE USER FOR GROUP %s! EXITING." % (groupID)
sys.exit(2)
return 1
########################################
def __create_user(self, groupID):
"""creates the user, or silently succeeds if it already exists.
It will change the users password though."""
# must be root
if who_am_i() != self.rootuser :
print 'You are not user: %s.' % ( self.rootuser)
return 0
#first see if user is in /etc/passwd
mgruser = self.groupID_to_user(groupID)
if check_user_exists(mgruser) :
#print 'true in check_user'
homedir = get_users_home_dir(mgruser)
if homedir == 0 :
print "User %s has an error in home directory. Can not continue." % (mgruser)
return 0
# else, pass through to reset password.
else :
#print 'false in check_user'
#third, make the user if needed.
if os.path.isfile('/bin/bash') :
shell = '/bin/bash'
elif os.path.isfile('/bin/ksh') :
shell = '/bin/ksh'
else :
shell = '/bin/sh'
if os.path.isfile('/usr/sbin/useradd') :
useradd='/usr/sbin/useradd'
elif os.path.isfile('/sbin/useradd') :
useradd='/sbin/useradd'
elif os.path.isfile('/usr/local/sbin/useradd') :
useradd='/usr/local/sbin/useradd'
else :
print 'I can not find a useradd program. Exiting.'
sys.exit(2)
os.system('%s -s %s -c "key manager %s" -m %s' % (useradd, shell,
mgruser,
mgruser))
try :
os.wait() # check return code?
except OSError, msg :
# assume it was successful?
pass
child = pexpect.spawn('/usr/bin/passwd %s' % (mgruser))
result = child.expect(['UNIX password:', 'passwd: Only root'])
if result == 0 :
child.sendline(self.groups[groupID]['password'])
child.expect('Retype new UNIX password:')
child.sendline(self.groups[groupID]['password'])
child.expect('\n')
child.close()
else :
print "FAILURE TO SET PASSWD FOR USER %s! EXITING" % (mgruser)
sys.exit(2)
#fourth, publish the keys to their .ssh directory
self.sync_local_user(groupID)
return 1
#fifth, publish the keys to the central key management.
#needed?, since I'll just pickle this class instance?
########################################
def sync_local_user(self, groupID, newkey=False, oldpw=''):
"""write the correct identities and server lists to the user. if
newkey is true, we're publishing a new key to the group. When
newkey is true, and you're not root, pass in the old password
as well, so, you can force a new password to the manager user."""
# user checking
# must be root or just modifying yourself.
if who_am_i() != self.rootuser :
if who_am_i() == self.groupID_to_user(groupID) :
#print 'syncing yourself is fine, I guess...'
root = False
else :
print 'You are not user: %s.' % ( self.rootuser)
return 0
else :
root = True
username, uid, gid, homedir = get_users_passwd_spec(
self.groupID_to_user(groupID))
print 'usercheck passed'
# update the key files
sshpath = os.path.join(homedir, self.sshdirname)
if not os.path.isdir(sshpath) :
os.mkdir(sshpath, 0700)
if root :
os.chown(sshpath, uid, gid)
# sorry for the slight obfuscation of using homedir vs sshpath to get
# to the same place... I'm trying to enforce use of self.privfilename
# and self.pubfilename instead of 'id_rsa'. Note, self.privfilename
# is effectivly .ssh/id_rsa
# write the private key
privname = os.path.join(homedir, self.privfilename)
if newkey :
if os.path.isfile(privname) :
os.rename(privname,
os.path.join(sshpath, 'id_' + self.keytype + '_old'))
keyfile = open(privname, 'w')
keyfile.write(self.groups[groupID]['privkey'])
keyfile.flush()
keyfile.close()
if root :
os.chown(privname, uid, gid)
os.chmod(privname, 0600)
# write the public key
pubname = os.path.join(homedir, self.pubfilename)
if newkey :
if os.path.isfile(pubname) :
os.rename(pubname,
os.path.join(sshpath,
'id_' + self.keytype + '_old.pub'))
keyfile = open(pubname, 'w')
keyfile.write(self.groups[groupID]['pubkey'])
keyfile.flush()
keyfile.close()
if root :
os.chown(pubname, uid, gid)
os.chmod(pubname, 0600)
print 'sshkey re-write passed'
# reset the users password if needed
if newkey :
if root :
child = pexpect.spawn('/usr/bin/passwd %s' % (username))
result = child.expect(['UNIX password:',
'passwd: Only root'])
if result == 0 :
child.sendline(self.groups[groupID]['password'])
child.expect('Retype new UNIX password:')
child.sendline(self.groups[groupID]['password'])
child.expect('\n')
child.close()
else :
print "FAILURE TO SET PASSWD FOR USER %s! EXITING" % (mgruser)
return 0
elif oldpw == self.groups[groupID]['password'] :
pass # if they're the same, non-root will fail.
elif oldpw == '' :
pass # we're not changing passwords.
else : # non root expect
child = pexpect.spawn('/usr/bin/passwd')
result = child.expect('assword:')
child.sendline(oldpw)
result = child.expect('assword:')
child.sendline(self.groups[groupID]['password'])
result = child.expect([ 'Retype new UNIX password:',
'BAD PASSWORD'])
if result == 0 : # Success!
child.sendline(self.groups[groupID]['password'])
else :
print 'failed to set your password correctly. please have'
print 'root force the issue. (having root run'
print 'sync_all_local_users() would be a great idea...)'
child.close()
print 'password passed'
# write out the access_<user>.txt files.
holder = {}
for server in self.groups[groupID]['servers'] :
for remoteUser in self.groups[groupID]['servers'][server] :
holder.setdefault(remoteUser, []).append(server)
for remoteUser in holder :
outFileName = os.path.join(homedir,
self.accessname + remoteUser + '.txt')
outFile = open(outFileName, 'w')
for server in holder[remoteUser] :
outFile.write(server + '\n')
outFile.flush()
outFile.close()
os.chown(outFileName, uid, gid)
os.chmod(outFileName, 0600)
print 'access_user passed'
return 1
########################################
def sync_all_local_users(self, newkey=False) :
"""call sync_local_user for everyone."""
for groupID in self.groups :
self.sync_local_user(groupID, newkey=newkey)
########################################
def sync_local_user_mask(self, username) :
groupID = self.user_to_groupID(username)
if groupID == 0 :
print 'malformed username'
return 0
elif self.groups.has_key(groupID) :
return self.sync_local_user(groupID)
else :
print 'User does not exist in key database'
return 0
########################################
def delete_group(self, groupID, force=False):
# can't delete the 'all' groupID.
# should I just check to see if it's being used, and just scream?
# should I just ask for confirmation, after listing servers involved?
# can be self.masteruser
if who_am_i() != self.rootuser :
print 'You are not user: %s.' % ( self.rootuser)
return 0
if groupID == self.mastergroup :
print "I can't let you to delete the master group, Dave."
return 0
if not self.groups.has_key(groupID) :
print "Group %s does not exist." % (groupID)
return 0
# remove the groupID from the 'groups' data structure
if self.groups[groupID]['servers'].keys() == [] :
pass
elif force :
pass
else :
print 'group %s has servers still in it. Please remove them.' % (groupID)
return 0
del self.groups[groupID]
# third, update each server affected
# Can't. I'm ROOT right now... must request a resync
#for server in servers :
# serverConn = self.__login_managed_server(server, groupID)
# self.update_server(server, groupID, serverConn)
# not update anymore... get keys, and write them.
# But I *CAN* remove their private key files...
username, uid, gid, homedir = get_users_passwd_spec(
self.groupID_to_user(groupID))
if os.path.isfile(os.path.join(homedir, self.privfilename)) :
os.unlink(os.path.join(homedir, self.privfilename))
if os.path.isfile(os.path.join(homedir, self.pubfilename)) :
os.unlink(os.path.join(homedir, self.pubfilename))
print "I suggest manually removing userid %s." % (username)
return 1
########################################
# Functions to maintain group membership
########################################
def list_servers_in_group(self, groupID) :
servers = self.groups[groupID]['servers'].keys()
servers.sort()
return servers
########################################
def list_all_servers_in_groups(self) :
# should I just return self.groups[self.mastergroup]['servers'].keys() ?
holder = []
groups = self.groups.keys()
for groupID in groups :
for server in self.list_servers_in_group(groupID) :
if server not in holder :
holder.append(server)
return holder
########################################
def list_groups_for_server(self, servername) :
holder = []
for groupID in self.groups :
if servername in self.groups[groupID]['servers'].keys() :
holder.append(groupID)
return holder
########################################
def list_remote_users_for_server(self, server):
"""return a list of remote users for all groups to which server
belongs"""
holder = []
for groupID in self.groups :
if self.groups[groupID]['servers'].has_key(server) :
for remoteUser in self.groups[groupID]['servers'][server] :
if remoteUser not in holder :
holder.append(remoteUser)
return holder
########################################
def list_group_summary(self) :
"""return a dictionary of groups -> servers -> users for each group"""
holder = {}
for groupID in self.groups :
test = holder.setdefault(groupID, {})
for server in self.groups[groupID]['servers'] :
test = holder[groupID].setdefault(server, [])
for user in self.groups[groupID]['servers'][server] :
holder[groupID][server].append(user)
return holder
########################################
def get_mastergroup(self) :
return self.mastergroup
def get_masteruser(self) :
return self.masteruser
########################################
# Functions to maintain remote servers
########################################
def add_login_to_group(self, server, remoteUser, groupID) :
"""validate both group and servers existance in management.
generate the full list of keys that user on that server should have
Log in with the correct user (mananged vs unmanaged).
write those keys to authorized_keys"""
# self.masteruser
if who_am_i() != self.masteruser :
print 'You are not user: %s.' % ( self.masteruser)
return 0
if self.groups.has_key(groupID) :
if remoteUser in self.list_remote_users_for_server(server) :
serverConn = self.__login_managed_server(server, remoteUser)
else :
#serverConn = self.__login_unmanaged_server(server, remoteUser)
print 'can not add a login to a group if the login is not'
print 'initialized.'
return 0
if serverConn == 0 :
print 'failed to connect to server %s.' % (server)
return 0
if self.groups[groupID]['servers'].has_key(server) :
if remoteUser not in self.groups[groupID]['servers'][server] :
self.groups[groupID]['servers'][server].append(remoteUser)
else :
self.groups[groupID]['servers'][server] = [remoteUser]
keys = self.__return_keys_for_login(server, remoteUser)
if not self.__write_authkey_to_server(serverConn, keys) :
print 'failure to write keys'
return 1
else :
print "groupID %s does not exist." % (groupID)
return 0
########################################
def remove_login_from_group(self, server, remoteUser, groupID) :
# can be self.masteruser
if who_am_i() != self.masteruser :
print 'You are not user: %s.' % ( self.masteruser)
return 0
if self.groups[groupID]['servers'].has_key(server) :
del self.groups[groupID]['servers'][server]
serverConn = self.__login_managed_server(server, remoteUser)
keys = self.__return_keys_for_login(server, remoteUser)
if not self.__write_authkey_to_server(serverConn, keys) :
print 'failure to write keys'
return 0
return 1
else :
print 'either server %s not in group %s, or server %s not initialized' % (server, groupID, server)
return 0
########################################
def revoke_server(self, server) :
"""log into server (if possible), remove authorized_keys, and remove
server from all groups, including the 'all' group, and delete
that server from the list."""
if who_am_i() != self.masteruser :
print 'must be %s to revoke a server.' % (self.masteruser)
return 0
remuserlist = []
groupIDList = self.list_groups_for_server(server)
for groupID in groupIDList :
for user in self.groups[groupID]['servers'][server] :
if user not in remuserlist :
remuserlist.append(user)
for user in remuserlist :
serverConn = self.__login_managed_server(server, user)
if serverConn == 0 :
print 'failed to login to server %s with user %s' % (server, user)
print 'non fatal'
continue
serverConn.sendline("echo '' > .ssh/authorized_keys")
serverConn.prompt()
serverConn.sendline("[ -f .ssh/authorized_keys_unmanaged ] && cat .ssh/authorized_keys_unmanaged > .ssh/authorized_keys")
serverConn.prompt()
serverConn.logout()
serverConn.close()
for groupID in groupIDList :
del self.groups[groupID]['servers'][server]
return 1
########################################
def initialize_login(self, servername, remoteUser, force=False,
password='') :
"""call login_unmanaged, and add server to the mastergroup. and
populate the key. NOTE: force is for corner cases, like a server
is rebuilt, and is still 'under management', but doesn't have the
authorized_keys in place."""
if who_am_i() != self.masteruser :
print 'must be %s to use this function.' % (self.masteruser)
return 0
# ensure it's an FQDN
#pipe = os.popen('host %s' % (servername))
#result = pipe.readline()
#pipe.close()
#servername = result[0:result.find(' ')]
remusers = self.list_remote_users_for_server(servername)
if not force :
if remoteUser in remusers :
print 'User %s on server %s is already known' % (remoteUser, servername)
return 0
if password == '' :
serverConn = self.__login_unmanaged_server(servername, remoteUser)
else :
serverConn = self.__login_unmanaged_server(servername, remoteUser,
password=password)
if serverConn == 0 :
print 'Failure to log into unmanaged server %s' % (servername)
return 0
if self.groups[self.mastergroup]['servers'].has_key(servername) :
if self.groups[self.mastergroup]['servers'][servername] == [] :
self.groups[self.mastergroup]['servers'][servername] = [remoteUser]
else :
self.groups[self.mastergroup]['servers'][servername].append(remoteUser)
else :
self.groups[self.mastergroup]['servers'][servername] = [remoteUser]
keys = self.__return_keys_for_login(servername, remoteUser)
if not self.__write_authkey_to_server(serverConn, keys) :
print 'failure to write keys'
return 0
serverConn.logout()
serverConn.close()
return 1
########################################
def __write_authkey_to_server(self, serverConn, keys) :
"""write the authorized_keys file to a server connection."""
# can be called by either user.
if who_am_i() not in (self.rootuser, self.masteruser) :
print 'You are not the masteruser or rootuser'
return 0
serverConn.sendline("cd")
serverConn.prompt()
serverConn.sendline("[ ! -d .ssh ] && mkdir .ssh")
serverConn.prompt()
serverConn.sendline('chmod 700 .ssh')
serverConn.prompt()
serverConn.sendline("echo '%s' > .ssh/authorized_keys" % (keys))
serverConn.prompt()
serverConn.sendline("[ -f .ssh/authorized_keys_unmanaged ] && cat .ssh/authorized_keys_unmanaged >> .ssh/authorized_keys")
serverConn.prompt()
serverConn.sendline('chmod 600 .ssh/authorized_keys')
serverConn.prompt()
return 1
########################################
def __login_unmanaged_server(self, server, remoteUser, password='') :
"""Login to a server which is unmanaged. Returns either 0 or a
logged in pxssh connection."""
if who_am_i() != self.masteruser :
print 'You are not user: %s.' % ( self.masteruser)
return 0
inlist = 0
try :
tester = telnetlib.Telnet(server, 22)
tester.close()
except socket.error, msg:
print "Server %s is not responding on ssh port." % (server)
return 0
except socket.gaierror, msg:
print "Server %s is not a valid name" % (server)
return 0
passedpass = 0
while 1 :
if password == '' :
print "I need the password for user %s on server %s" % (
remoteUser, server)
print "Please type 'break' if you wish to skip this server."
password = getpass.getpass()
else :
passedpass = 1
if password == 'break' :
print "ignoring server %s." % (server)
return 0
try :
serverConn = pxssh.pxssh()
#print 'server is', server, 'remoteUser is', remoteUser
#print 'password is', password, 'is password'
result = serverConn.login(server, remoteUser, password,
login_timeout=10)
password = ''
#print 'pxssh result is', result
except :
print 'error: exception'
password = ''
result = 0
if result :
print 'successful login.'
break
else :
if passedpass :
print "incorrect passed in password."
return 0 #bad bulk passwd
print "login errors connecting to unmanged server. Please"
print "try again."
# TODO: research errors here.
serverConn.close(force=True)
continue
#print serverConn
serverConn.set_unique_prompt()
return serverConn
########################################
def __login_managed_server(self, server, remoteUser, oldPassPhrase=''):
"""Login to a server which is already managed, using groupID's
credentials. Returns either 0 or a logged in pxssh connection.
NOTE You must be master user, since you have to use the 'all'
key. The oldPassPhrase parameter is needed by regen_all_keys().
Since it will overwrite the self.groups version of keys, but not
the on disk ones (which the ssh clients use), the old passphrase
is needed."""
if who_am_i() != self.masteruser :
print "you're not user %s. Not valid." % (self.masteruser)
return 0
if server not in self.list_servers_in_group(self.mastergroup) :
print "server %s isn't in database." % (server)
return 0
if remoteUser not in self.list_remote_users_for_server(server) :
print 'invalid user: %s for server %s' % (remoteUser, server)
return 0
if oldPassPhrase == '' :
passphrase = self.groups[self.mastergroup]['passphrase']
else :
passphrase = oldPassPhrase
serverConn = pxssh.pxssh()
try :
result = serverConn.login(server, remoteUser, passphrase,
login_timeout=10)
except pexpect.EOF, msg :
result = 0
if not result :
print 'login failure for user %s on server %s' % (remoteUser, server)
return 0
serverConn.set_unique_prompt()
return serverConn
########################################
def regen_all_keys(self, newpass=True) :
"""make new key pairs for all groups. Copy out those new keys to
each part of self.groups, use the all user to
sync each remote key, then update the all user. Notes: 1) copy old
keys as id_rsa_old and id_rsa_old.pub. 2) keep a list of servers
which have issues (and store it under self, so it can be manually
reconciled)
When done, sync_all_local_users() will have to be called by root."""
if who_am_i() != self.masteruser :
print 'you are not %s user' % (self.masteruser)
#book keeping
oldAllPassword = self.groups[self.mastergroup]['password']
oldAllPassPhrase = self.groups[self.mastergroup]['passphrase']
for groupID in self.groups.keys() :
#if groupID == self.mastergroup :
# continue
# commented out because I have to generate masterusers new key so
# I only have to write out the keys to the servers once
if newpass :
userPassword = nicepass()
keyPassPhrase, privKey, pubKey = generate_key()
else :
userPassword = self.groups[groupID]['password']
keyPassPhrase = self.groups[groupID]['passphrase']
# I guess I could use a temp variable, but why?
keyPassPhrase, privKey, pubKey = generate_key(passphrase=keyPassPhrase)
self.groups[groupID]['pubkey'] = pubKey
self.groups[groupID]['privkey'] = privKey
self.groups[groupID]['password'] = userPassword
self.groups[groupID]['passphrase'] = keyPassPhrase
for server in self.groups[self.mastergroup]['servers'].keys() :
for remoteUser in self.groups[self.mastergroup]['servers'][server] :
serverConn = self.__login_managed_server(server, remoteUser,
oldPassPhrase=oldAllPassPhrase)
if serverConn == 0 :
print 'failed to connect to server %s. Skipping' % (server)
# TODO: add to reconciliation list
continue
keys = self.__return_keys_for_login(server, remoteUser)
if not self.__write_authkey_to_server(serverConn, keys) :
print 'failure to write keys'
if newpass :
self.sync_local_user(self.mastergroup, newkey=True,
oldpw=oldAllPassword)
else :
self.sync_local_user(self.mastergroup, newkey=True)
print "Please have root user call sync_all_local_users()"
print "note: all the manager accounts are useless until then."
return 1
########################################
# Helper functions
########################################
def __return_keys_for_login(self, server, remoteUser) :
"""generate a list of public keys needed for this server/user pair"""
pubkeys = ''
pubkeys += self.groups[self.mastergroup]['pubkey']
groups = self.list_groups_for_server(server)
for groupID in groups :
if groupID == self.mastergroup :
continue
if self.groups[groupID]['servers'].has_key(server) :
if remoteUser in self.groups[groupID]['servers'][server] :
pubkeys += self.groups[groupID]['pubkey']
return pubkeys
########################################
def groupID_to_user(self, groupID):
return self.userprefix + groupID
########################################
def user_to_groupID(self, user):
if user.startswith(self.userprefix) :
return user[3:]
print "Bad username in user_to_groupID %s! Exiting." % (user)
return 0
########################################
def print_pass(self, groupID) :
"""Output a user id, password and passphrase"""
if who_am_i() != self.rootuser :
print "you're not user %s. Not valid." % (self.rootuser)
return 0
print "user: %10s password: %s, passphrase: %s" % (
self.groupID_to_user(groupID),
self.groups[groupID]['password'],
self.groups[groupID]['passphrase'])
########################################
def print_all_pass(self) :
"""print ALL user and password/passphrases"""
if who_am_i() != self.rootuser :
print "you're not user %s. Not valid." % (self.rootuser)
return 0
keys = self.groups.keys()
keys.remove(self.mastergroup)
self.print_pass(self.mastergroup) # print 'all' first.
keys.sort()
for key in keys :
self.print_pass(key)
########################################
def print_group_report(self) :
holder = self.list_group_summary()
print "group: %s" % (self.mastergroup)
for server in holder[self.mastergroup] :
print " server: %s" % (server)
print " ",
for user in holder[self.mastergroup][server] :
print user,
print
for groupID in holder :
if groupID == self.mastergroup :
continue
print "group: %s" % (groupID)
for server in holder[groupID] :
print " server: %s" % (server)
print " ",
for user in holder[groupID][server] :
print user,
print
################################################################################
################################################################################
################################################################################
def generate_key(comment='', passphrase='') :
"""Create a key pair and passphrase"""
if passphrase == '' :
keyPassPhrase = nicepass()
else :
keyPassPhrase = passphrase
privKeyFileName = 'privKeyFile'
pubKeyFileName = 'privKeyFile.pub'
try :
if os.path.isfile(privKeyFileName) :
os.unlink(privKeyFileName)
if os.path.isfile(pubKeyFileName) :
os.unlink(pubKeyFileName)
test = open(pubKeyFileName, 'w')
test.close()
except OSError, msg :
print "you don't have rights to write to this directory."
return 0,0,0
# TODO: need to add checks to see if I can actually write to these files.
if comment == '' :
child = pexpect.spawn('/usr/bin/ssh-keygen -t rsa -N %s -f %s' % (
keyPassPhrase, privKeyFileName) )
else :
child = pexpect.spawn('/usr/bin/ssh-keygen -t rsa -N %s -f %s -C %s' % (
keyPassPhrase, privKeyFileName, comment) )
child.expect('The key fingerprint is')
# Add some more error checking
child.close()
file = open(privKeyFileName, 'r')
privKey = file.read()
file.close()
os.unlink(privKeyFileName)
file = open(pubKeyFileName, 'r')
pubKey = file.read()
file.close()
os.unlink(pubKeyFileName)
return keyPassPhrase, privKey, pubKey
################################################################################
#### Stolen from Active State Python Cookbook:
#### http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/410076
## Generate a human readable 'random' password
## password will be generated in the form 'word'+digits+'word'
## eg.,nice137pass
## parameters: number of 'characters' , number of 'digits'
## Pradeep Kishore Gowda <pradeep at btbytes.com >
## License : GPL
## Date : 2005.April.15
## Revision 1.2
## ChangeLog:
## 1.1 - fixed typos
## 1.2 - renamed functions _apart & _npart to a_part & n_part as zope does
## not allow functions to
## start with _
# modified by Jason to add a random 'non-alpha-numeric' character.
def nicepass(alpha=6,numeric=2,nonalpha=1):
"""
returns a human-readble password (say rol86din instead of
a difficult to remember K8Yn9muL )
"""
import string
import random
vowels = ['a','e','i','o','u']
nonalphas = ['!', '?', '-', '_', '@', '#', '$', '%', '^', '&', '*']
consonants = [a for a in string.ascii_lowercase if a not in vowels]
digits = string.digits
####utility functions
def a_part(slen):
ret = ''
for i in range(slen):
if i%2 ==0:
randid = random.randint(0,20) #number of consonants
ret += consonants[randid]
else:
randid = random.randint(0,4) #number of vowels
ret += vowels[randid]
return ret
def n_part(slen):
ret = ''
for i in range(slen):
randid = random.randint(0,9) #number of digits
ret += digits[randid]
return ret
def p_part(slen):
ret = ''
for i in range(slen):
randid = random.randint(0,len(nonalphas) - 1)
ret += nonalphas[randid]
return ret
####
test = random.randint(0,2)
if test == 0 :
fpl = int(alpha/2) + random.randint(0,1)
elif test == 1 :
fpl = int(alpha/2)
else :
fpl = int(alpha/2) - random.randint(0,1)
lpl = alpha - fpl
start = a_part(fpl)
mid = n_part(numeric)
mid2 = p_part(nonalpha)
end = a_part(lpl)
if random.randint(0,1) :
return "%s%s%s%s" % (start,mid2,mid,end)
else :
return "%s%s%s%s" % (start,mid,mid2,end)
################################################################################
def check_user_exists(username) :
"""see if a user exists in /etc/passwd, and has a valid home dir"""
try :
entry = pwd.getpwnam(username)
except KeyError, msg :
return 0
return 1
################################################################################
def get_users_home_dir(username) :
"""return 0 if user does not exist, or if dir not found. else string"""
try :
entry = pwd.getpwnam(username)
except KeyError, msg :
return 0
return entry[5]
################################################################################
def get_users_passwd_spec(username) :
"""return 0 if user does not exist, or if home dir doesn't exist.
else, return tuple of (username, uid, gid, homedir)"""
try :
entry = pwd.getpwnam(username)
except KeyError, msg :
return 0,0,0,0
return entry[0], entry[2], entry[3], entry[5]
################################################################################
def who_am_i() :
"""get the username of the current user."""
whoami=os.popen('whoami')
username = whoami.readline()
return username.strip('\n')
################################################################################
def pickle_keys(keydb, file) :
"""write the keydb to the given file"""
username = who_am_i()
if username == 'root' :
if not os.path.isfile(file) :
temp = open(file, 'w')
temp.flush()
temp.close()
masteruser = keydb.get_masteruser()
temp, uid, gid, homedir = get_users_passwd_spec(masteruser)
os.chown(file, uid, gid)
os.chmod(file, 0600)
dumpfile = open(file, 'w')
cPickle.dump(keydb, dumpfile)
dumpfile.flush()
dumpfile.close()
return 1
################################################################################
def unpickle_keys(file) :
try :
dumpfile = open(file, 'r')
except IOError, msg :
usage()
keydb = cPickle.load(dumpfile)
dumpfile.close()
return keydb
################################################################################
# Bulk functions
################################################################################
def bulk_group_create(keydb, filename, keyStateFile) :
"""create a bunch of groups/users all at once. groupList is just a
list of group names."""
username = who_am_i()
if username != 'root' :
print 'you are not the root user.'
usage(user='root')
if not os.path.isfile(filename) :
print 'file must exist.'
sys.exit(2)
input_file = open(filename, 'r')
for uline in input_file :
line = uline.strip('\n')
words = line.split()
if len(words) != 1 :
print 'invalid entry:', line
continue
if keydb.add_group(words[0]) :
print "Successfully added group %s" % (words[0])
print """NOTE: I *STRONGLY* recommend adding logic to this users
shell initialization files. In the login files (.bash_profile, etc),
please add something like the following (note: code for sh derived shells):
if [ ${SSH_AGENT_PID:-x} == 'x' ]; then
#echo 'running exec'
exec ssh-agent $SHELL -l
else
#echo 'running ssh-add'
ssh-add
fi
This will launch ssh-agent, and then run ssh-add, prompting for the passphrase.
Please also add this to the logout file (.bash_logout, etc):
ssh-agent -k
"""
else :
print "Failure adding group %s" % (words[0])
pickle_keys(keydb, keyStateFile)
sys.exit(0)
################################################################################
def bulk_initialize_login(keydb, filename, keyStateFile) :
"""parse file, split into server, user, and password, and attempt init on
each one."""
username = who_am_i()
if username != keydb.get_masteruser() :
print 'you are not the master user.'
usage(user='master')
if not os.path.isfile(filename) :
print 'file must exist.'
sys.exit(2)
input_file = open(filename, 'r')
for uline in input_file :
line = uline.strip('\n')
words = line.split()
# words[0] is servername, words[1] is remote user, words[2] is password
if len(words) != 3 :
print 'invalid entry:', line
continue
result = keydb.initialize_login(words[0], words[1], force=True,
password=words[2])
if result :
print "Successfully initialized %s@%s" % (words[1], words[0])
else :
print "Failure initializing %s@%s" % (words[1], words[0])
pickle_keys(keydb, keyStateFile)
################################################################################
def bulk_add_login_to_group(keydb, filename, keyStateFile) :
username = who_am_i()
if username != keydb.get_masteruser() :
print 'you are not the master user.'
usage(user='master')
if not os.path.isfile(filename) :
print 'file must exist.'
sys.exit(2)
input_file = open(filename, 'r')
for uline in input_file :
line = uline.strip('\n')
words = line.split()
# words[0] is server, words[1] is user, words[2] is group
if len(words) != 3 :
print 'invalid entry:', line
continue
result = keydb.add_login_to_group(words[0], words[1], words[2])
if result :
print "Successfully added login to group %s" % (words[2])
else :
print "Failure adding login to group %s" % (words[2])
pickle_keys(keydb, keyStateFile)
################################################################################
# begin the shell(ing)
################################################################################
def usage(user='') :
"""Usage description"""
# Copyright notice
print """key-manager Copyright (C) 2008 Jason Price
This program comes with ABSOLUTELY NO WARRANTY; for details see
the file LICENSE in the origional distribution or http://www.gnu.org/licenses/
This is free software, and you are welcome to redistribute it
under certain conditions; see the LICENSE for details."""
if user == 'root' :
print """USAGE:
Root Functions:
-S Syncronize database to all local users
-s Syncronize database for a specific user
-n create New group
-N Bulk create new groups (requires a formated file)
-d delete group
-D delete group (force delete, even if group is not empty)
-p Print a specific password/passphrase
-P Print all user names, passwords and passphrases.
-l Generates a list of each group, and which logins are associated with it.
-h Print this message and exit
"""
elif user == 'master' :
print """USAGE:
Master User Functions:
-i Initialize a login on a server (bring it under mgmt)
-I Force initialize a login on a server (useful if server is rebuilt)
-J Bulk Initialize (requires a formated file)
-a Add a login to a group
-A Bulk Add a login to a group (requires a formated file)
-r Remove a login from a group
-R Revoke a server (attempt to login, clean keys, remove server from mgmt)
-B Revoke all servers (attempt to login, clean keys, remove each server
from mgmt)
-k Regenerate all keys (does not change passwords/passphrases)
-K Regenerate all keys (creates new passwords/passphrases)
-s Sync database to current user
-l Generates a list of each group, and which logins are associated with it.
-h Print this message and exit
"""
else :
print 'you should not be running this program...'
sys.exit(2)
sys.exit(0)
################################################################################
def parse_options_root(keydb, keyStateFile) :
username = who_am_i()
if username != 'root' :
print 'you are not the root user.'
usage(user='root')
try :
opts, args = getopt.getopt(sys.argv[1:], "hlSsnNdDPp")
except getopt.GetoptError:
usage(user='root')
holder = keydb.list_group_summary()
if opts == [] :
usage(user='root')
for o, a in opts:
if o == '-h' :
usage(user='root')
elif o == '-l' :
keydb.print_group_report()
elif o == '-S' :
keydb.sync_all_local_users()
elif o == '-s' :
syncuser = raw_input('Which user would you like to sync? ')
if check_user_exists(syncuser) :
syncgroup = keydb.user_to_groupID(syncuser)
if syncgroup == 0 :
print 'invalid user'
elif not holder.has_key(syncgroup) :
print 'user not in group database'
elif not keydb.sync_local_user_mask(syncuser) :
print 'failed to sync local user: %s' % (syncuser)
else :
print 'sync succeeded.'
elif o == '-n' :
groupID = raw_input('New group name? ')
if keydb.add_group(groupID) :
print 'successfully created new group.'
pickle_keys(keydb, keyStateFile)
else :
print 'error adding group.'
sys.exit(2)
elif o == '-N' :
print "Bulk creation of new groups. Please type in a filename"
print "where each new group name is listed, one group name per line"
filename = raw_input('Bulk group list filename? ')
bulk_group_create(keydb, filename, keyStateFile)
elif o == '-d' :
groupID = raw_input('Which group would you like to delete? ')
if not holder.has_key(groupID) :
print 'group does not exist in group database'
else :
if keydb.delete_group(groupID) :
print 'successfully deleted group.'
pickle_keys(keydb, keyStateFile)
else :
print 'error deleting group.'
sys.exit(2)
elif o == '-D' :
groupID = raw_input('Which group would you like to force delete? ')
if not holder.has_key(groupID) :
print 'group does not exist in group database'
else :
if keydb.delete_group(groupID, force=True) :
print 'successfully deleted group.'
pickle_keys(keydb, keyStateFile)
else :
print 'error force deleting group.'
sys.exit(2)
elif o == '-P' :
keydb.print_all_pass()
elif o == '-p' :
groupID = raw_input('Which group would you like passwords for? ')
if not holder.has_key(groupID) :
print 'group does not exist in group database'
else :
keydb.print_pass(groupID)
else :
usage(user='root')
sys.exit(0)
################################################################################
def parse_options_masteruser(keydb, keyStateFile) :
username = who_am_i()
try :
opts, args = getopt.getopt(sys.argv[1:], "hlsiIJaArRBkK")
except getopt.GetoptError:
usage('master')
if username != keydb.get_masteruser() :
print 'you are not the master user.'
usage('master')
holder = keydb.list_group_summary()
mastergroup = keydb.get_mastergroup()
if opts == [] :
usage('master')
for o, a in opts:
if o == '-h' :
usage('master')
elif o == '-l' :
keydb.print_group_report()
elif o == '-s' :
if not keydb.sync_local_user_mask(username) :
print 'failed to sync the current user: %s' % (username)
elif o == '-i' :
server = raw_input('What server would you like to bring under management? ')
user = raw_input('What user would you like to bring under management? ')
print ""
print "Note that this procedure will overwrite the"
print ".ssh/authorized_keys file for user %s on " % (user)
print "server %s. If you need to preserve existing keys," % (server)
print "please copy them to .ssh/authorized_keys_unmanaged ,"
print "and they will be automagically included."
reply = raw_input("Are you sure you wish to continue? (yes/no) ")
if reply != 'yes' :
sys.exit(0)
if keydb.initialize_login(server, user) :
print 'successful initialization'
pickle_keys(keydb, keyStateFile)
else :
print 'error initializing logon.'
sys.exit(2)
elif o == '-I' :
server = raw_input('What server would you like to bring under management? ')
user = raw_input('What user would you like to bring under management? ')
print ""
print "Note that this procedure will overwrite the"
print ".ssh/authorized_keys file for user %s on " % (user)
print "server %s. If you need to preserve existing keys," % (server)
print "please copy them to .ssh/authorized_keys_unmanaged ,"
print "and they will be automagically included."
reply = raw_input("Are you sure you wish to continue? (yes/no) ")
if reply != 'yes' :
sys.exit(0)
if keydb.initialize_login(server, user, force=True) :
print 'successful initialization'
pickle_keys(keydb, keyStateFile)
else :
print 'error initializing logon.'
sys.exit(2)
elif o == '-J' :
print "Bulk initialize logins. Please type in a filename where"
print "each login information is on a line by itself. The format"
print "of the line should be:"
print "servername remoteuser password"
filename = raw_input('Bulk login initialization filename? ')
bulk_initialize_login(keydb, filename, keyStateFile)
elif o == '-a' :
groupID = raw_input('What group would you like to add too? ')
if not holder.has_key(groupID) :
print 'invalid group.'
sys.exit(2)
server = raw_input('What server would you like to add to group %s? ' % (groupID))
if not holder[mastergroup].has_key(server) :
print 'invalid server. Please initialize server first.'
sys.exit(2)
user = raw_input('What user would you like to log in as? ')
if user not in holder[mastergroup][server] :
print 'invalid user.'
sys.exit(2)
if keydb.add_login_to_group(server, user, groupID) :
print 'successfully added login to group.'
pickle_keys(keydb, keyStateFile)
else :
print 'error adding logon to group.'
sys.exit(2)
elif o == '-A' :
print "Bulk add logins to group. Please type in a filename where"
print "each login and group information is on a line by itself."
print "The format of the line should be:"
print "servername remoteuser groupname"
print "where groupname is the name of the group that the login"
print "should be added too."
filename = raw_input('Bulk add login to group filename? ')
bulk_add_login_to_group(keydb, filename, keyStateFile)
elif o == '-r' :
groupID = raw_input('What group would you like to remove from? ')
if not holder.has_key(groupID) :
print 'invalid group.'
sys.exit(2)
server = raw_input('What server would you like to remove from group %s? ' % (groupID))
if not holder[groupID].has_key(server) :
print 'invalid server.'
sys.exit(2)
user = raw_input('What user would you like to remove from group? ')
if user not in holder[groupID][server] :
print 'invalid user.'
sys.exit(2)
if keydb.remove_login_from_group(server, user, groupID) :
print 'successfully removed login from group.'
pickle_keys(keydb, keyStateFile)
else :
print 'error removing logon to group.'
sys.exit(2)
elif o == '-R' :
server = raw_input('Which server would you like to revoke? ')
if not holder[mastergroup].has_key(server) :
print 'invalid server'
if keydb.revoke_server(server) :
print 'successfully revoked server.'
pickle_keys(keydb, keyStateFile)
else :
print 'error revoking server.'
sys.exit(2)
elif o == '-B' :
print "WARNING: THIS WILL REMOVE ALL SERVERS FROM MANAGEMENT."
result = raw_input('Are you sure you wish to do this? (Y/n)')
if result != "Y" :
print "ok. exiting."
sys.exit(2)
servers = keydb.list_servers_in_group(keydb.get_mastergroup())
for server in servers :
if keydb.revoke_server(server) :
print 'successfully revoked server %s' % (server)
else :
print 'error revoking server %s' % (server)
pickle_keys(keydb, keyStateFile)
elif o == '-k' :
if keydb.regen_all_keys(newpass=False) :
print 'successfully regenerated keys.'
pickle_keys(keydb, keyStateFile)
else :
print 'error regenerating all keys.'
sys.exit(2)
elif o == '-K' :
if keydb.regen_all_keys() :
print 'successfully regenerated keys.'
pickle_keys(keydb, keyStateFile)
else :
print 'error regenerating all keys.'
sys.exit(2)
else :
usage('master')
sys.exit(0)
################################################################################
def parse_options(keydb, keyStateFile) :
"""parse the command line options"""
username = who_am_i()
if username == 'root' :
parse_options_root(keydb, keyStateFile)
elif username == keydb.get_masteruser() :
parse_options_masteruser(keydb, keyStateFile)
else :
print 'you are not allowed to run this program.'
sys.exit(2)
################################################################################
def initial_configuration() :
print "Begin initial configuration."
print "Each management user will be named in a specific way. The first"
print "part of the name is the 'user prefix', and the second is the name"
print "of the group. The 'user prefix' will be the same across all"
print "management users. I suggest that the 'user prefix' be 'mgr'."
userprefix = raw_input("What would you like the 'user prefix' to be? [mgr] ")
if userprefix == '' :
userprefix = 'mgr'
print ""
print "This program needs a single 'master' group. I suggest that"
print "the master group be 'all'"
mastergroup = raw_input("What would you like the 'mastergroup' to be? [all] ")
if mastergroup == '' :
mastergroup = 'all'
print ""
print "Each management user will have one or more files placed in their"
print "home directory. These files will contain a list of all systems that"
print "they have access to, through their passphrase. There will be one"
print "file per remote user (a file for systems which they can access"
print "'root', a file for systems which they can access as 'oracle', etc)."
print "These files need a prefix. If the prefix is 'access_', then the"
print "filename would be 'access_root.txt' for the 'root' remote user."
accessname = raw_input("What would you like the file prefix to be? [access_] ")
if accessname == '' :
accessname = 'access_'
print ""
print "I also need to know a few defaults about SSH in your environment."
print "Unless you know what you're doing, I suggest taking the defaults."
sshdirname = raw_input("What is the name of each users ssh directory? [.ssh] ")
if sshdirname == '' :
sshdirname = '.ssh'
print ""
keytype = raw_input("What type of keys do you use? rsa or dsa? [rsa] ")
if keytype == '' :
keytype = 'rsa'
elif keytype == 'rsa' or keytype == 'dsa' :
pass
else :
print 'invalid key type'
sys.exit(2)
return userKeyDataHolder(userPrefix=userprefix, masterGroup=mastergroup,
accessName=accessname, sshDirName=sshdirname,
keyType=keytype)
################################################################################
################################################################################
################################################################################
def main():
"""main."""
keyStateFile = '/usr/local/etc/key-manager-state' # NOT VALIDATED
if os.path.isfile(keyStateFile) :
keydb = unpickle_keys(keyStateFile)
else :
if who_am_i() == 'root' :
keydb = initial_configuration()
pickle_keys(keydb, keyStateFile)
print 'initial run complete.'
sys.exit(0)
else :
print 'program has not been initialized, or state file does not exist.'
print 'please run this program as root to initialize correctly.'
sys.exit(2)
parse_options(keydb, keyStateFile)
################################################################################
if __name__ == '__main__' :
pass
main()