2019-08-15 07:01:58 -04:00
|
|
|
#!/usr/bin/env python3
|
|
|
|
|
|
|
|
import argparse
|
|
|
|
import base64
|
|
|
|
import binascii
|
|
|
|
import os
|
|
|
|
import pwd
|
|
|
|
import re
|
|
|
|
import subprocess
|
|
|
|
|
|
|
|
|
|
|
|
class UserAdder(object):
|
|
|
|
def __init__(self, *args, **kwargs):
|
|
|
|
# This doesn't *really* need to be a class, but I may do something with it in the future.
|
|
|
|
_tmpargs = locals()
|
|
|
|
del(_tmpargs['self'])
|
|
|
|
for k, v in _tmpargs.items():
|
|
|
|
setattr(self, k, v)
|
|
|
|
self.users = {}
|
|
|
|
|
|
|
|
def addUser(self, user, *args, **kwargs):
|
|
|
|
# We ideally should do this purely pythonically, but libuser is external and not available everywhere...
|
|
|
|
# We *could* do it by hand (add to /etc/passwd, etc.) but that's not guaranteed to be totally compatible.
|
|
|
|
# Don't try to add a user if they exist. Doesn't support e.g. LDAP auth.
|
|
|
|
try:
|
|
|
|
u = pwd.getpwnam(user)
|
|
|
|
homedir = u.pw_dir
|
|
|
|
except KeyError:
|
|
|
|
homedir = '/home/{0}'.format(user)
|
|
|
|
subprocess.run(['useradd',
|
|
|
|
'-M',
|
|
|
|
'-c',
|
|
|
|
'Added by add-borguser.py',
|
|
|
|
'-d',
|
|
|
|
homedir,
|
|
|
|
user])
|
|
|
|
sshdir = os.path.join(homedir, '.ssh')
|
|
|
|
authkeys = os.path.join(sshdir, 'authorized_keys')
|
|
|
|
userent = pwd.getpwnam(user)
|
|
|
|
uid, gid = userent.pw_uid, userent.pw_gid
|
|
|
|
os.makedirs(homedir, mode = 0o700, exist_ok = True)
|
|
|
|
os.makedirs(sshdir, mode = 0o700, exist_ok = True)
|
|
|
|
os.chown(homedir, uid, gid)
|
|
|
|
os.chown(sshdir, uid, gid)
|
|
|
|
if not os.path.isfile(authkeys):
|
|
|
|
with open(authkeys, 'w') as f:
|
|
|
|
f.write('')
|
|
|
|
os.chmod(authkeys, 0o0400)
|
|
|
|
os.chown(authkeys, uid, gid)
|
|
|
|
self.users[user] = authkeys
|
|
|
|
return()
|
|
|
|
|
|
|
|
def addKey(self, ssh_key, *args, **kwargs):
|
|
|
|
key_template = ('command='
|
2019-08-15 08:32:51 -04:00
|
|
|
#'"cd {homedir};'
|
|
|
|
#'borg serve --restrict-to-path {homedir}",'
|
2021-06-30 04:25:59 -04:00
|
|
|
'"/usr/local/bin/borg-restricted.py ${SSH_ORIGINAL_COMMAND}",'
|
2019-08-15 07:01:58 -04:00
|
|
|
'no-port-forwarding,'
|
|
|
|
'no-X11-forwarding,'
|
|
|
|
'no-pty,'
|
|
|
|
'no-agent-forwarding,'
|
|
|
|
'no-user-rc '
|
|
|
|
'{keystr}\n')
|
2019-08-15 07:08:42 -04:00
|
|
|
for u, kp in self.users.items():
|
2019-08-15 07:01:58 -04:00
|
|
|
userent = pwd.getpwnam(u)
|
|
|
|
homedir = userent.pw_dir
|
2019-08-15 08:32:51 -04:00
|
|
|
sshdir = os.path.join(homedir, '.ssh')
|
2019-08-15 07:01:58 -04:00
|
|
|
key_insert = key_template.format(user = u,
|
|
|
|
homedir = homedir,
|
|
|
|
keystr = ssh_key)
|
|
|
|
with open(kp, 'a') as f:
|
|
|
|
f.write(key_insert)
|
2019-08-15 08:32:51 -04:00
|
|
|
# When CentOS/RHEL move to python3 native, and port policycoreutils, do this natively.
|
|
|
|
# But for now...
|
|
|
|
subprocess.run(['chcon',
|
|
|
|
'-R unconfined_u:object_r:user_home_t:s0',
|
|
|
|
sshdir])
|
|
|
|
subprocess.run(['semanage',
|
|
|
|
'fcontext',
|
|
|
|
'-a',
|
|
|
|
'-t',
|
|
|
|
'ssh_home_t',
|
|
|
|
sshdir])
|
2019-08-15 07:01:58 -04:00
|
|
|
return()
|
|
|
|
|
|
|
|
def clean(self):
|
|
|
|
self.users = {}
|
|
|
|
return()
|
|
|
|
|
|
|
|
def parseArgs():
|
|
|
|
def _valid_posix_user(username):
|
|
|
|
# http://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap03.html#tag_03_437
|
|
|
|
# http://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap03.html#tag_03_282
|
|
|
|
# https://unix.stackexchange.com/a/435120/284004
|
|
|
|
if not re.search('^[a-z_]([a-z0-9_-]{0,31}|[a-z0-9_-]{0,30}\$)$', username):
|
|
|
|
raise argparse.ArgumentTypeError('user must be a POSIX-compliant username')
|
|
|
|
return(username)
|
|
|
|
def _valid_ssh_key(keystr):
|
|
|
|
# This validation is *super* cursory. We could probably do some better parsing at some point.
|
|
|
|
key_components = keystr.split()
|
|
|
|
keytype = re.sub('^ssh-(.*)', '\g<1>', key_components[0])
|
|
|
|
# We don't support anything but ED25519 or RSA, given that they used the hardening guide.
|
|
|
|
if keytype not in ('ed25519', 'rsa'):
|
|
|
|
raise argparse.ArgumentTypeError('Not a valid SSH pubkey type (must be RSA or ED25519)')
|
|
|
|
try:
|
|
|
|
base64.b64decode(key_components[1].encode('utf-8'))
|
|
|
|
except binascii.Error:
|
|
|
|
raise argparse.ArgumentTypeError('Not a valid SSH pubkey')
|
|
|
|
return(keystr)
|
|
|
|
args = argparse.ArgumentParser(description = ('Add local users to a borg server'))
|
|
|
|
args.add_argument('user',
|
|
|
|
type = _valid_posix_user,
|
|
|
|
help = 'The username/machine name to add')
|
|
|
|
args.add_argument('ssh_key',
|
|
|
|
type = _valid_ssh_key,
|
|
|
|
help = ('The full SSH pubkey (remember to enclose in quotes)'))
|
|
|
|
return(args)
|
|
|
|
|
|
|
|
def main():
|
|
|
|
if not os.geteuid() == 0:
|
|
|
|
raise PermissionError('This script must be run as root or with root-like privileges')
|
|
|
|
args = vars(parseArgs().parse_args())
|
|
|
|
um = UserAdder(**args)
|
|
|
|
um.addUser(**args)
|
|
|
|
um.addKey(**args)
|
|
|
|
um.clean()
|
|
|
|
return()
|
|
|
|
|
|
|
|
if __name__ == '__main__':
|
|
|
|
main()
|