diff --git a/certidude/auth.py b/certidude/auth.py index 7ed116d..066064f 100644 --- a/certidude/auth.py +++ b/certidude/auth.py @@ -7,6 +7,7 @@ import logging import os import re import socket +from certidude import config logger = logging.getLogger("api") @@ -20,20 +21,47 @@ logger = logging.getLogger("api") FQDN = socket.getaddrinfo(socket.gethostname(), 0, socket.AF_INET, 0, 0, socket.AI_CANONNAME)[0][3] -if not os.getenv("KRB5_KTNAME"): - click.echo("Kerberos keytab not specified, set environment variable 'KRB5_KTNAME'", err=True) - exit(250) +if config.AUTHENTICATION_BACKEND == "kerberos": + if not os.getenv("KRB5_KTNAME"): + click.echo("Kerberos keytab not specified, set environment variable 'KRB5_KTNAME'", err=True) + exit(250) -try: - principal = kerberos.getServerPrincipalDetails("HTTP", FQDN) -except kerberos.KrbError as exc: - click.echo("Failed to initialize Kerberos, reason: %s" % exc, err=True) - exit(249) + try: + principal = kerberos.getServerPrincipalDetails("HTTP", FQDN) + except kerberos.KrbError as exc: + click.echo("Failed to initialize Kerberos, reason: %s" % exc, err=True) + exit(249) + else: + click.echo("Kerberos enabled, service principal is HTTP/%s" % FQDN) else: - click.echo("Kerberos enabled, service principal is HTTP/%s" % FQDN) + NotImplemented def login_required(func): - def authenticate(resource, req, resp, *args, **kwargs): + def pam_authenticate(resource, req, resp, *args, **kwargs): + """ + Authenticate against PAM with WWW Basic Auth credentials + """ + authorization = req.get_header("Authorization") + if not authorization: + resp.append_header("WWW-Authenticate", "Basic") + raise falcon.HTTPUnauthorized("Forbidden", "Please authenticate") + + if not authorization.startswith("Basic "): + raise falcon.HTTPForbidden("Forbidden", "Bad header: %s" % authorization) + + from base64 import b64decode + basic, token = authorization.split(" ", 1) + user, passwd = b64decode(token).split(":", 1) + + import simplepam + if not simplepam.authenticate(user, passwd, "sshd"): + raise falcon.HTTPForbidden("Forbidden", "Invalid password") + + req.context["user"] = user + return func(resource, req, resp, *args, **kwargs) + + + def kerberos_authenticate(resource, req, resp, *args, **kwargs): authorization = req.get_header("Authorization") if not authorization: @@ -82,7 +110,12 @@ def login_required(func): # TODO: logger.error raise falcon.HTTPForbidden("Forbidden", "Tried GSSAPI") - return authenticate + if config.AUTHENTICATION_BACKEND == "kerberos": + return kerberos_authenticate + elif config.AUTHENTICATION_BACKEND == "pam": + return pam_authenticate + else: + NotImplemented def authorize_admin(func): diff --git a/certidude/cli.py b/certidude/cli.py index 5e478f9..cfae58b 100755 --- a/certidude/cli.py +++ b/certidude/cli.py @@ -1006,6 +1006,7 @@ def certidude_serve(user, port, listen, enable_signature): from wsgiref.simple_server import make_server, WSGIServer from socketserver import ThreadingMixIn from certidude.api import certidude_app, StaticResource + from certidude import config class ThreadingWSGIServer(ThreadingMixIn, WSGIServer): pass @@ -1024,13 +1025,26 @@ def certidude_serve(user, port, listen, enable_signature): from jinja2.debug import make_traceback as _make_traceback "".encode("charmap") + if config.AUTHENTICATION_BACKEND == "pam": + # PAM needs access to /etc/shadow + import grp + name, passwd, gid, mem = grp.getgrnam("shadow") + click.echo("Adding current user to shadow group due to PAM authentication backend") + os.setgroups([gid]) + else: + os.setgroups([]) + _, _, uid, gid, gecos, root, shell = pwd.getpwnam(user) if uid == 0: click.echo("Please specify unprivileged user") exit(254) - click.echo("Switching to user %s (uid=%d, gid=%d)" % (user, uid, gid)) + os.setgid(gid) os.setuid(uid) + + click.echo("Switched to user %s (uid=%d, gid=%d); member of groups %s" % + (user, uid, gid, ", ".join([str(j) for j in os.getgroups()]))) + os.umask(0o007) elif os.getuid() == 0: click.echo("Warning: running as root, this is not recommended!") diff --git a/certidude/config.py b/certidude/config.py index 847ba95..ad5db4a 100644 --- a/certidude/config.py +++ b/certidude/config.py @@ -14,6 +14,9 @@ FQDN = socket.getaddrinfo(socket.gethostname(), 0, socket.AF_INET, 0, 0, socket. cp = configparser.ConfigParser() cp.readfp(codecs.open("/etc/certidude/server.conf", "r", "utf8")) +AUTHENTICATION_BACKEND = cp.get("authentication", "backend") # kerberos, pam +AUTHORIZATION_BACKEND = cp.get("authorization", "backend") # whitelist, ldap, pam + ADMIN_USERS = set([j for j in cp.get("authorization", "admin_users").split(" ") if j]) ADMIN_SUBNETS = set([ipaddress.ip_network(j) for j in cp.get("authorization", "admin_subnets").split(" ") if j]) AUTOSIGN_SUBNETS = set([ipaddress.ip_network(j) for j in cp.get("authorization", "autosign_subnets").split(" ") if j]) diff --git a/requirements.txt b/requirements.txt index 1b2040a..5ef995a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -18,4 +18,5 @@ pyOpenSSL==0.15.1 python-mimeparse==0.1.4 requests==2.2.1 setproctitle==1.1.9 +simplepam==0.1.5 six==1.9.0