From 443bf08f77a527b76cbebee6780be51fd1269674 Mon Sep 17 00:00:00 2001 From: Frantisek Hrbata Date: Mon, 4 Jan 2021 17:53:38 +0100 Subject: [PATCH] gimrr-server-krb: simple server for adding public keys with krb auth Signed-off-by: Frantisek Hrbata --- gimrr-server-krb | 166 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 166 insertions(+) create mode 100755 gimrr-server-krb diff --git a/gimrr-server-krb b/gimrr-server-krb new file mode 100755 index 0000000..7860a7a --- /dev/null +++ b/gimrr-server-krb @@ -0,0 +1,166 @@ +#!/usr/bin/env python3 + +# vim: tabstop=8 expandtab shiftwidth=4 softtabstop=4 + +""" +Copyright © 2020 Frantisek Hrbata +This program is free software. It comes without any warranty, to +the extent permitted by applicable law. You can redistribute it +and/or modify it under the terms of the Do What The Fuck You Want +To Public License, Version 2, as published by Sam Hocevar. See +http://www.wtfpl.net/ for more details. +""" + +import argparse +import socket +import gssapi +import sys +import os +import gimrrdataprot +import select +import signal +import tempfile +import re +import fcntl +import logging + +parser = argparse.ArgumentParser(description="Server for adding gimrr users public keys using krb authentication."); +parser.add_argument("--address", metavar="ADDR", default="", + help="Bind server to ADDR (default: INADDR_ANY)") +parser.add_argument("--port", metavar="PORT", default=9876, type=int, + help="Listen at specified port (default: 9876)") +parser.add_argument("--timeout", metavar="SECONDS", default=3, type=int, + help="Socket timeout for client connections (default: 3 sec.)") +parser.add_argument("--principal", metavar="PRINCIPAL", + help="Kerberos principal name.") +parser.add_argument("--keytab", metavar="FILE", default="/etc/krb5.keytab", + help="Keytab with key for server principal (default: /etc/krb5.keytab)") +parser.add_argument("--auth-keys", metavar="FILE", required=True, + help="Ssh authorized_keys file where to store public keys.") +parser.add_argument("--daemon", action="store_true", + help="Start server as daemon in background.") +parser.add_argument("--log", metavar="FILE", + help="Server log file.") + +args = parser.parse_args() +os.environ["KRB5_KTNAME"] = args.keytab + +logger = logging.getLogger() +logger.setLevel(logging.DEBUG) +logfmt = logging.Formatter("%(asctime)s: %(levelname)s - %(message)s") + +if args.log: + h = logging.FileHandler(args.log) + h.setFormatter(logfmt) + logger.addHandler(h) + +if not args.daemon: + h = logging.StreamHandler() + h.setFormatter(logfmt) + logger.addHandler(h) + +server_name = gssapi.Name(args.principal) +server_cred = gssapi.Credentials(name=server_name, usage='accept') + +ss = socket.socket(socket.AF_INET, socket.SOCK_STREAM) +ss.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) +ss.bind((args.address, args.port)) +ss.listen() +cs = None +run = True + +def stop(signum, frame): + global run + run = False + +signal.signal(signal.SIGHUP, stop) +signal.signal(signal.SIGINT, stop) +signal.signal(signal.SIGTERM, stop) + +if args.daemon: + pid = os.fork() + if pid > 0: + sys.exit() + + os.setsid() + sys.stdin.close() + sys.stdout.close() + sys.stderr.close() + +tmpdir = tempfile.TemporaryDirectory(dir=os.path.dirname(os.path.realpath(args.auth_keys))) +auth_keys_fn = "{}/auth_keys".format(tmpdir.name) + +logger.info("server started") + +while run: + try: + r, w, e = select.select([ss], [], [], 1) + if ss not in r: + continue + + # serving users synchronously should be fine for this use case + cs, addr = ss.accept() + logger.info("connection accepted from {}:{}".format(addr[0], addr[1])) + + ctx = gssapi.SecurityContext(creds=server_cred, usage='accept') + while not ctx.complete: + data_type, token_in = gimrrdataprot.recv_data(cs) + if data_type != gimrrdataprot.DATA_TYPE_AUTH_TOKEN: + raise Exception("expecting gssapi auth token") + + token_out = ctx.step(token_in) + if not token_out: + break + gimrrdataprot.send_data(cs, gimrrdataprot.DATA_TYPE_AUTH_TOKEN, token_out) + + if not ctx.complete: + raise Exception("no security context") + + user = str(ctx.initiator_name) + user = user.split("@")[0] + if not user: + raise Exception("no user found") + + logger.info("user {} authenticated".format(user)) + + data_type, key = gimrrdataprot.recv_data_dec(ctx, cs) + if data_type != gimrrdataprot.DATA_TYPE_PUB_KEY: + raise Exception("expecting public key") + + # TODO Probably check at least the key format as described in RFC 4253 6.6 + + logger.info("received key: {}".format(key.decode().strip())) + + user_re = re.compile('"GIMRR_USER={}"'.format(user)) + + with open(args.auth_keys, "a+") as ofd: + fcntl.lockf(ofd.fileno(), fcntl.LOCK_EX | fcntl.LOCK_NB) + ofd.seek(0) + lines = ofd.readlines() + with open(os.open(auth_keys_fn, os.O_CREAT | os.O_WRONLY | os.O_TRUNC, 0o600), "w") as nfd: + for line in lines: + match = user_re.search(line) + if not match: + nfd.write(line) + else: + logger.info("removing: {}".format(line.strip())) + + nfd.write('environment="GIMRR_USER={}",restrict,\ +command="/usr/bin/git-shell -c \\"$SSH_ORIGINAL_COMMAND\\"" \ +{}'.format(user, key.decode())) + + os.rename(auth_keys_fn, args.auth_keys) + logger.info("new key added for user {}".format(user)) + + gimrrdataprot.send_data_enc(ctx, cs, gimrrdataprot.DATA_TYPE_MSG, + "public key successfully registered for '{}'".format(user)) + except Exception as e: + logger.error(e) + if cs: + gimrrdataprot.send_data(cs, gimrrdataprot.DATA_TYPE_ERR, str(e)) + finally: + if cs: + cs.close() + cs = None + +logger.info("server stopped") -- 1.7.10.4