From 925bc0ef9a4ce1fcf2b6230a5b44a87b5d395faa Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Lauri=20V=C3=B5sandi?=
Date: Sun, 27 Mar 2016 23:38:14 +0300
Subject: [PATCH] Refactor users, add OpenVPN and mailing support
* Add abstraction for user objects
* Mail authority admins about pending, revoked and signed certificates
* Add NetworkManager's OpenVPN plugin support
* Improve CRL support
* Refactor CSRF protection
* Update documentation
---
README.rst | 117 ++++----
certidude/api/__init__.py | 41 +--
certidude/api/request.py | 9 +-
certidude/api/signed.py | 9 +-
certidude/api/tag.py | 5 +-
certidude/auth.py | 235 ++++-----------
certidude/authority.py | 19 +-
certidude/cli.py | 273 ++++++++++--------
certidude/config.py | 18 +-
certidude/constants.py | 1 +
certidude/decorators.py | 14 +-
certidude/helpers.py | 118 ++++++--
certidude/mailer.py | 12 +-
certidude/static/js/templates.js | 173 +++++------
certidude/static/views/authority.html | 18 +-
certidude/templates/certidude.conf | 2 +-
.../templates/mail/certificate-revoked.md | 6 +
certidude/templates/mail/request-stored.md | 5 +
certidude/templates/nginx-https-site.conf | 1 +
certidude/templates/nginx.conf | 5 +-
.../templates/openvpn-client-to-site.ovpn | 5 +-
.../templates/openvpn-site-to-client.ovpn | 5 +-
certidude/templates/uwsgi.ini | 2 +-
certidude/user.py | 165 +++++++++++
requirements.txt | 1 -
25 files changed, 695 insertions(+), 564 deletions(-)
create mode 100644 certidude/templates/mail/certificate-revoked.md
create mode 100644 certidude/templates/mail/request-stored.md
create mode 100644 certidude/user.py
diff --git a/README.rst b/README.rst
index 435971f..a7fcf92 100644
--- a/README.rst
+++ b/README.rst
@@ -17,8 +17,9 @@ eventually support PKCS#11 and in far future WebCrypto.
.. figure:: doc/usecase-diagram.png
-Certidude is mainly designed for VPN gateway operators to make VPN adoption usage
-as simple as possible.
+Certidude is mainly designed for VPN gateway operators to make
+desktop/laptop VPN setup as easy as possible.
+User certificate management eg. for HTTPS is also made reasonably simple.
For a full-blown CA you might want to take a look at
`EJBCA `_ or
`OpenCA `_.
@@ -27,24 +28,29 @@ For a full-blown CA you might want to take a look at
Features
--------
+Common:
+
* Standard request, sign, revoke workflow via web interface.
-* Colored command-line interface, check out ``certidude list``.
-* OpenVPN integration, check out ``certidude setup openvpn server`` and ``certidude setup openvpn client``.
-* strongSwan integration, check out ``certidude setup strongswan server`` and ``certidude setup strongswan client``.
+* Kerberos and basic auth based web interface authentication.
+* PAM and Active Directory compliant authentication backends: Kerberos single sign-on, LDAP simple bind.
+* POSIX groups and Active Directory (LDAP) group membership based authorization.
+* Command-line interface, check out ``certidude list``.
* Privilege isolation, separate signer process is spawned per private key isolating
private key use from the the web interface.
-* Certificate numbering obfuscation, certificate serial numbers are intentionally
- randomized to avoid leaking information about business practices.
-* Server-side events support via for example nginx-push-stream-module.
-* Kerberos based web interface authentication.
-* File based whitelist authorization, easy to integrate with LDAP as shown below.
+* Certificate serial numbers are intentionally randomized to avoid leaking information about business practices.
+* Server-side events support via `nchan `_.
+* E-mail notifications about pending, signed and revoked certificates.
+Virtual private networking:
-Coming soon
------------
+* OpenVPN integration, check out ``certidude setup openvpn server`` and ``certidude setup openvpn client``.
+* strongSwan integration, check out ``certidude setup strongswan server`` and ``certidude setup strongswan client``.
+* NetworkManager integration, check out ``certidude setup openvpn networkmanager`` and ``certidude setup strongswan networkmanager``.
-* Refactor mailing subsystem and server-side events to use hooks.
-* Notifications via e-mail.
+HTTPS:
+
+* P12 bundle generation for web browsers, seems to work well with Android
+* HTTPS server setup with client verification, check out ``certidude setup nginx``
TODO
@@ -60,6 +66,7 @@ TODO
* Cronjob for deleting expired certificates
* Signer process logging.
+
Install
-------
@@ -67,15 +74,15 @@ To install Certidude:
.. code:: bash
- apt-get install -y python python-pip python-dev cython \
+ apt-get install -y python python-pip python-dev cython python-configparser \
python-pysqlite2 python-mysql.connector python-ldap \
build-essential libffi-dev libssl-dev libkrb5-dev \
ldap-utils krb5-user default-mta \
libsasl2-modules-gssapi-mit
pip3 install certidude
-Make sure you're running PyOpenSSL 0.15+ and netifaces 0.10.4+ from PyPI,
-not the outdated ones provided by APT.
+Make sure you're running PyOpenSSL 0.15+ from PyPI,
+not the outdated one provided by APT.
Create a system user for ``certidude``:
@@ -85,10 +92,10 @@ Create a system user for ``certidude``:
mkdir /etc/certidude
-Setting up CA
---------------
+Setting up authority
+--------------------
-First make sure the machine used for CA has fully qualified
+First make sure the machine used for certificate authority has fully qualified
domain name set up properly.
You can check it with:
@@ -96,10 +103,10 @@ You can check it with:
hostname -f
-The command should return ca.example.co
+The command should return ca.example.com
-Certidude can set up CA relatively easily, following will set up
-CA in /var/lib/certidude/hostname.domain.tld:
+Certidude can set up certificate authority relatively easily,
+following will set up certificate authority in /var/lib/certidude/hostname.domain.tld:
.. code:: bash
@@ -176,6 +183,7 @@ Otherwise manually configure ``uwsgi`` application in ``/etc/uwsgi/apps-availabl
env = LANG=C.UTF-8
env = LC_ALL=C.UTF-8
env = KRB5_KTNAME=/etc/certidude/server.keytab
+ env = KRB5CCNAME=/run/certidude/krb5cc
Also enable the application:
@@ -183,7 +191,7 @@ Also enable the application:
ln -s ../apps-available/certidude.ini /etc/uwsgi/apps-enabled/certidude.ini
-We support `nginx-push-stream-module `_,
+We support `nchan `_,
configure the site in /etc/nginx/sites-available/certidude:
.. code::
@@ -238,11 +246,9 @@ Also adjust ``/etc/nginx/nginx.conf``:
events {
worker_connections 768;
- # multi_accept on;
}
http {
- push_stream_shared_memory_size 32M;
sendfile on;
tcp_nopush on;
tcp_nodelay on;
@@ -254,10 +260,12 @@ Also adjust ``/etc/nginx/nginx.conf``:
error_log /var/log/nginx/error.log;
gzip on;
gzip_disable "msie6";
+ include /etc/nginx/conf.d/*;
include /etc/nginx/sites-enabled/*;
}
-In your CA ssl.cnf make sure Certidude is aware of your nginx setup:
+In your Certidude server's /etc/certidude/server.conf make sure Certidude
+is aware of your nginx setup:
.. code::
@@ -309,8 +317,6 @@ Reset Kerberos configuration in ``/etc/krb5.conf``:
default_realm = EXAMPLE.LAN
dns_lookup_realm = true
dns_lookup_kdc = true
- forwardable = true
- proxiable = true
Initialize Kerberos credentials:
@@ -332,43 +338,25 @@ Set up Kerberos keytab for the web service:
chown root:certidude /etc/certidude/server.keytab
chmod 640 /etc/certidude/server.keytab
+Reconfigure /etc/certidude/server.conf:
-Setting up authorization
-------------------------
+.. code:: ini
-Obviously arbitrary Kerberos authenticated user should not have access to
-the CA web interface.
-You could either specify user name list
-in ``/etc/ssl/openssl.cnf``:
+ [authentication]
+ backends = kerberos
-.. code:: bash
+ [authorization]
+ backend = ldap
+ ldap gssapi credential cache = /run/certidude/krb5cc
+ ldap user filter = (&(objectclass=user)(objectcategory=person)(samaccountname=%s))
+ ldap admin filter = (&(objectclass=user)(objectclass=person)(memberOf=cn=Domain Admins,cn=Users,dc=example,dc=com)(samaccountname=%s))
- admin_users=alice bob john kate
-
-Or alternatively specify file path:
-
-.. code:: bash
-
- admin_users=/run/certidude/user.whitelist
-
-Use following shell snippets eg in ``/etc/cron.hourly/update-certidude-user-whitelist``
-to generate user whitelist via LDAP:
-
-.. code:: bash
-
- ldapsearch -H ldap://dc1.example.com -s sub -x -LLL \
- -D 'cn=certidude,cn=Users,dc=example,dc=com' \
- -w 'certidudepass' \
- -b 'dc=example,dc=com' \
- '(&(objectClass=user)(memberOf=cn=Domain Admins,cn=Users,dc=example,dc=com))' sAMAccountName userPrincipalName givenName sn \
- | python3 -c "import ldif3; import sys; [sys.stdout.write('%s:%s:%s:%s\n' % (a.pop('sAMAccountName')[0], a.pop('userPrincipalName')[0], a.pop('givenName')[0], a.pop('sn')[0])) for _, a in ldif3.LDIFParser(sys.stdin.buffer).parse()]" \
- > /run/certidude/user.whitelist
-
-Set permissions:
-
-.. code:: bash
-
- chmod 700 /etc/cron.hourly/update-certidude-user-whitelist
+User filter here specified which users can log in to Certidude web interface
+at all eg. for generating user certificates for HTTPS.
+Admin filter specifies which users are allowed to sign and revoke certificates.
+Adjust admin filter according to your setup.
+Also make sure there is cron.hourly job for creating GSSAPI credential cache -
+that's necessary for querying LDAP using Certidude machine's credentials.
Automating certificate setup
@@ -384,7 +372,7 @@ Create ``/etc/NetworkManager/dispatcher.d/certidude`` with following content:
case "$2" in
up)
- LANG=C.UTF-8 /usr/local/bin/certidude setup strongswan networkmanager ca.example.com gateway.example.com
+ LANG=C.UTF-8 /usr/local/bin/certidude request spawn -k
;;
esac
@@ -397,8 +385,7 @@ Finally make it executable:
Whenever a wired or wireless connection is brought up,
the dispatcher invokes ``certidude`` in order to generate RSA keys,
submit CSR, fetch signed certificate,
-create NetworkManager configuration for the VPN connection and
-finally to bring up the VPN tunnel as well.
+create NetworkManager configuration for the VPN connection.
Development
diff --git a/certidude/api/__init__.py b/certidude/api/__init__.py
index 547ab52..98619be 100644
--- a/certidude/api/__init__.py
+++ b/certidude/api/__init__.py
@@ -9,6 +9,7 @@ from datetime import datetime
from time import sleep
from certidude import authority, mailer
from certidude.auth import login_required, authorize_admin
+from certidude.user import User
from certidude.decorators import serialize, event_source, csrf_protection
from certidude.wrappers import Request, Certificate
from certidude import constants, config
@@ -33,34 +34,16 @@ class CertificateAuthorityResource(object):
logger.info("Served CA certificate to %s", req.context.get("remote_addr"))
resp.stream = open(config.AUTHORITY_CERTIFICATE_PATH, "rb")
resp.append_header("Content-Type", "application/x-x509-ca-cert")
- resp.append_header("Content-Disposition", "attachment; filename=ca.crt")
+ resp.append_header("Content-Disposition", "attachment; filename=%s.crt" %
+ constants.HOSTNAME.encode("ascii"))
class SessionResource(object):
+ @csrf_protection
@serialize
@login_required
- @authorize_admin
@event_source
def on_get(self, req, resp):
- if config.ACCOUNTS_BACKEND == "ldap":
- import ldap
- ft = config.LDAP_MEMBERS_FILTER % (config.ADMINS_GROUP, "*")
- r = req.context.get("ldap_conn").search_s(config.LDAP_BASE,
- ldap.SCOPE_SUBTREE, ft.encode("utf-8"), ["cn", "member"])
-
- for dn,entry in r:
- cn, = entry.get("cn")
- break
- else:
- raise ValueError("Failed to look up group %s in LDAP" % repr(group_name))
-
- admins = dict([(j, j.split(",")[0].split("=")[1]) for j in entry.get("member")])
- elif config.ACCOUNTS_BACKEND == "posix":
- import grp
- _, _, gid, members = grp.getgrnam(config.ADMINS_GROUP)
- admins = dict([(j, j) for j in members])
- else:
- raise NotImplementedError("Authorization backend %s not supported" % config.AUTHORIZATION_BACKEND)
return dict(
user = dict(
@@ -72,12 +55,6 @@ class SessionResource(object):
request_submission_allowed = sum( # Dirty hack!
[req.context.get("remote_addr") in j
for j in config.REQUEST_SUBNETS]),
- user_subnets = config.USER_SUBNETS,
- autosign_subnets = config.AUTOSIGN_SUBNETS,
- request_subnets = config.REQUEST_SUBNETS,
- admin_subnets=config.ADMIN_SUBNETS,
- admin_users = admins,
- #admin_users=config.ADMIN_USERS,
authority = dict(
outbox = config.OUTBOX,
certificate = authority.certificate,
@@ -85,7 +62,12 @@ class SessionResource(object):
requests=authority.list_requests(),
signed=authority.list_signed(),
revoked=authority.list_revoked(),
- ) if config.ADMINS_GROUP in req.context.get("groups") else None,
+ admin_users = User.objects.filter_admins(),
+ user_subnets = config.USER_SUBNETS,
+ autosign_subnets = config.AUTOSIGN_SUBNETS,
+ request_subnets = config.REQUEST_SUBNETS,
+ admin_subnets=config.ADMIN_SUBNETS,
+ ) if req.context.get("user").is_admin() else None,
features=dict(
tagging=config.TAGGING_BACKEND,
leases=False, #config.LEASES_BACKEND,
@@ -124,7 +106,7 @@ class BundleResource(object):
common_name = req.context["user"].mail
logger.info("Signing bundle %s for %s", common_name, req.context.get("user"))
resp.set_header("Content-Type", "application/x-pkcs12")
- resp.set_header("Content-Disposition", "attachment; filename=%s.p12" % common_name)
+ resp.set_header("Content-Disposition", "attachment; filename=%s.p12" % common_name.encode("ascii"))
resp.body, cert = authority.generate_pkcs12_bundle(common_name,
owner=req.context.get("user"))
@@ -132,7 +114,6 @@ class BundleResource(object):
import ipaddress
class NormalizeMiddleware(object):
- @csrf_protection
def process_request(self, req, resp, *args):
assert not req.get_param("unicode") or req.get_param("unicode") == u"✓", "Unicode sanity check failed"
req.context["remote_addr"] = ipaddress.ip_address(req.env["REMOTE_ADDR"].decode("utf-8"))
diff --git a/certidude/api/request.py b/certidude/api/request.py
index 6462b49..1188918 100644
--- a/certidude/api/request.py
+++ b/certidude/api/request.py
@@ -6,7 +6,7 @@ import ipaddress
import os
from certidude import config, authority, helpers, push, errors
from certidude.auth import login_required, login_optional, authorize_admin
-from certidude.decorators import serialize
+from certidude.decorators import serialize, csrf_protection
from certidude.wrappers import Request, Certificate
from certidude.firewall import whitelist_subnets, whitelist_content_types
@@ -19,6 +19,7 @@ class RequestListResource(object):
def on_get(self, req, resp):
return authority.list_requests()
+
@login_optional
@whitelist_subnets(config.REQUEST_SUBNETS)
@whitelist_content_types("application/pkcs10")
@@ -53,7 +54,7 @@ class RequestListResource(object):
# Process automatic signing if the IP address is whitelisted and autosigning was requested
if req.get_param_as_bool("autosign"):
for subnet in config.AUTOSIGN_SUBNETS:
- if subnet.overlaps(req.context.get("remote_addr")):
+ if req.context.get("remote_addr") in subnet:
try:
resp.set_header("Content-Type", "application/x-x509-user-cert")
resp.body = authority.sign(csr).dump()
@@ -103,6 +104,8 @@ class RequestDetailResource(object):
csr.common_name, req.context.get("remote_addr"))
return csr
+
+ @csrf_protection
@login_required
@authorize_admin
def on_patch(self, req, resp, cn):
@@ -118,6 +121,8 @@ class RequestDetailResource(object):
logger.info("Signing request %s signed by %s from %s", csr.common_name,
req.context.get("user"), req.context.get("remote_addr"))
+
+ @csrf_protection
@login_required
@authorize_admin
def on_delete(self, req, resp, cn):
diff --git a/certidude/api/signed.py b/certidude/api/signed.py
index 36adcb4..14319c3 100644
--- a/certidude/api/signed.py
+++ b/certidude/api/signed.py
@@ -3,7 +3,7 @@ import falcon
import logging
from certidude import authority
from certidude.auth import login_required, authorize_admin
-from certidude.decorators import serialize
+from certidude.decorators import serialize, csrf_protection
logger = logging.getLogger("api")
@@ -24,20 +24,21 @@ class SignedCertificateDetailResource(object):
try:
cert = authority.get_signed(cn)
except EnvironmentError:
- logger.warning("Failed to serve non-existant certificate %s to %s",
+ logger.warning(u"Failed to serve non-existant certificate %s to %s",
cn, req.context.get("remote_addr"))
resp.body = "No certificate CN=%s found" % cn
raise falcon.HTTPNotFound()
else:
- logger.debug("Served certificate %s to %s",
+ logger.debug(u"Served certificate %s to %s",
cn, req.context.get("remote_addr"))
return cert
+ @csrf_protection
@login_required
@authorize_admin
def on_delete(self, req, resp, cn):
- logger.info("Revoked certificate %s by %s from %s",
+ logger.info(u"Revoked certificate %s by %s from %s",
cn, req.context.get("user"), req.context.get("remote_addr"))
authority.revoke_certificate(cn)
diff --git a/certidude/api/tag.py b/certidude/api/tag.py
index 8009370..48b6ee7 100644
--- a/certidude/api/tag.py
+++ b/certidude/api/tag.py
@@ -3,7 +3,7 @@ import falcon
import logging
from certidude.relational import RelationalMixin
from certidude.auth import login_required, authorize_admin
-from certidude.decorators import serialize
+from certidude.decorators import serialize, csrf_protection
logger = logging.getLogger("api")
@@ -17,6 +17,7 @@ class TagResource(RelationalMixin):
return self.iterfetch("select * from tag")
+ @csrf_protection
@serialize
@login_required
@authorize_admin
@@ -51,6 +52,7 @@ class TagDetailResource(RelationalMixin):
raise falcon.HTTPNotFound()
+ @csrf_protection
@serialize
@login_required
@authorize_admin
@@ -63,6 +65,7 @@ class TagDetailResource(RelationalMixin):
push.publish("tag-updated", identifier)
+ @csrf_protection
@serialize
@login_required
@authorize_admin
diff --git a/certidude/auth.py b/certidude/auth.py
index ea18622..20bd96b 100644
--- a/certidude/auth.py
+++ b/certidude/auth.py
@@ -6,14 +6,13 @@ import logging
import os
import re
import socket
+from certidude.user import User
from certidude.firewall import whitelist_subnets
from certidude import config, constants
logger = logging.getLogger("api")
-FQDN = socket.getaddrinfo(socket.gethostname(), 0, socket.AF_INET, 0, 0, socket.AI_CANONNAME)[0][3]
-
-if config.AUTHENTICATION_BACKENDS == {"kerberos"}:
+if "kerberos" in config.AUTHENTICATION_BACKENDS:
ktname = os.getenv("KRB5_KTNAME")
if not ktname:
@@ -24,139 +23,13 @@ if config.AUTHENTICATION_BACKENDS == {"kerberos"}:
exit(248)
try:
- principal = kerberos.getServerPrincipalDetails("HTTP", FQDN)
+ principal = kerberos.getServerPrincipalDetails("HTTP", constants.FQDN)
except kerberos.KrbError as exc:
- click.echo("Failed to initialize Kerberos, service principal is HTTP/%s, reason: %s" % (FQDN, exc), err=True)
+ click.echo("Failed to initialize Kerberos, service principal is HTTP/%s, reason: %s" % (
+ constants.FQDN, exc), err=True)
exit(249)
else:
- click.echo("Kerberos enabled, service principal is HTTP/%s" % FQDN)
-
-
-class User(object):
- def __init__(self, name):
- if "@" in name:
- self.mail = name
- self.name, self.domain = name.split("@")
- else:
- self.mail = None
- self.name, self.domain = name, None
- self.given_name, self.surname = None, None
-
- def __repr__(self):
- if self.given_name and self.surname:
- return u"%s %s <%s>" % (self.given_name, self.surname, self.mail)
- else:
- return self.mail
-
-
-def member_of(group_name):
- """
- Check if requesting user is member of an UNIX group
- """
-
- def wrapper(func):
- def posix_check_group_membership(resource, req, resp, *args, **kwargs):
- import grp
- _, _, gid, members = grp.getgrnam(group_name)
- if req.context.get("user").name not in members:
- logger.info("User '%s' not member of group '%s'", req.context.get("user").name, group_name)
- raise falcon.HTTPForbidden("Forbidden", "User not member of designated group")
- req.context.get("groups").add(group_name)
- return func(resource, req, resp, *args, **kwargs)
-
- def ldap_check_group_membership(resource, req, resp, *args, **kwargs):
- import ldap
-
- ft = config.LDAP_MEMBERS_FILTER % (group_name, req.context.get("user").dn)
- r = req.context.get("ldap_conn").search_s(config.LDAP_BASE, ldap.SCOPE_SUBTREE,
- ft.encode("utf-8"),
- ["member"])
-
- for dn,entry in r:
- if not dn: continue
- logger.debug("User %s is member of group %s" % (
- req.context.get("user"), repr(group_name)))
- req.context.get("groups").add(group_name)
- break
- else:
- raise ValueError("Failed to look up group '%s' with '%s' listed as member in LDAP" % (group_name, req.context.get("user").name))
-
- return func(resource, req, resp, *args, **kwargs)
-
- if config.AUTHORIZATION_BACKEND == "ldap":
- return ldap_check_group_membership
- elif config.AUTHORIZATION_BACKEND == "posix":
- return posix_check_group_membership
- else:
- raise NotImplementedError("Authorization backend %s not supported" % config.AUTHORIZATION_BACKEND)
- return wrapper
-
-
-def account_info(func):
- # TODO: Use Privilege Account Certificate for Kerberos
-
- def posix_account_info(resource, req, resp, *args, **kwargs):
- import pwd
- _, _, _, _, gecos, _, _ = pwd.getpwnam(req.context["user"].name)
- gecos = gecos.decode("utf-8").split(",")
- full_name = gecos[0]
- if full_name and " " in full_name:
- req.context["user"].given_name, req.context["user"].surname = full_name.split(" ", 1)
- req.context["user"].mail = req.context["user"].name + "@" + constants.DOMAIN
- return func(resource, req, resp, *args, **kwargs)
-
- def ldap_account_info(resource, req, resp, *args, **kwargs):
- import ldap
- import ldap.sasl
-
- if "ldap_conn" not in req.context:
- for server in config.LDAP_SERVERS:
- conn = ldap.initialize(server)
- conn.set_option(ldap.OPT_REFERRALS, 0)
- if os.path.exists("/etc/krb5.keytab"):
- ticket_cache = os.getenv("KRB5CCNAME")
- if not ticket_cache:
- raise ValueError("Ticket cache not initialized, unable to authenticate with computer account against LDAP server!")
- click.echo("Connecing to %s using Kerberos ticket cache from %s" % (server, ticket_cache))
- conn.sasl_interactive_bind_s('', ldap.sasl.gssapi())
- else:
- raise NotImplementedError("LDAP simple bind not supported, use Kerberos")
- req.context["ldap_conn"] = conn
- break
- else:
- raise ValueError("No LDAP servers!")
-
- ft = config.LDAP_USER_FILTER % req.context.get("user").name
- r = req.context.get("ldap_conn").search_s(config.LDAP_BASE, ldap.SCOPE_SUBTREE,
- ft,
- ["cn", "givenname", "sn", "mail", "userPrincipalName"])
-
- for dn, entry in r:
- if not dn: continue
- if entry.get("givenname") and entry.get("sn"):
- given_name, = entry.get("givenName")
- surname, = entry.get("sn")
- req.context["user"].given_name = given_name.decode("utf-8")
- req.context["user"].surname = surname.decode("utf-8")
- else:
- cn, = entry.get("cn")
- if " " in cn:
- req.context["user"].given_name, req.context["user"].surname = cn.decode("utf-8").split(" ", 1)
-
- req.context["user"].dn = dn.decode("utf-8")
- req.context["user"].mail, = entry.get("mail") or entry.get("userPrincipalName") or (None,)
- retval = func(resource, req, resp, *args, **kwargs)
- req.context.get("ldap_conn").unbind_s()
- return retval
- else:
- raise ValueError("Failed to look up %s in LDAP" % req.context.get("user"))
-
- if config.ACCOUNTS_BACKEND == "ldap":
- return ldap_account_info
- elif config.ACCOUNTS_BACKEND == "posix":
- return posix_account_info
- else:
- raise NotImplementedError("Accounts backend %s not supported" % config.ACCOUNTS_BACKEND)
+ click.echo("Kerberos enabled, service principal is HTTP/%s" % constants.FQDN)
def authenticate(optional=False):
@@ -167,7 +40,7 @@ def authenticate(optional=False):
if not req.auth:
resp.append_header("WWW-Authenticate", "Negotiate")
- logger.debug("No Kerberos ticket offered while attempting to access %s from %s",
+ logger.debug(u"No Kerberos ticket offered while attempting to access %s from %s",
req.env["PATH_INFO"], req.context.get("remote_addr"))
raise falcon.HTTPUnauthorized("Unauthorized",
"No Kerberos ticket offered, are you sure you've logged in with domain user account?")
@@ -175,7 +48,7 @@ def authenticate(optional=False):
token = ''.join(req.auth.split()[1:])
try:
- result, context = kerberos.authGSSServerInit("HTTP@" + FQDN)
+ result, context = kerberos.authGSSServerInit("HTTP@" + constants.FQDN)
except kerberos.GSSError as ex:
# TODO: logger.error
raise falcon.HTTPForbidden("Forbidden",
@@ -185,34 +58,46 @@ def authenticate(optional=False):
result = kerberos.authGSSServerStep(context, token)
except kerberos.GSSError as ex:
kerberos.authGSSServerClean(context)
- # TODO: logger.error
+ logger.error(u"Kerberos authentication failed from %s. "
+ "Bad credentials: %s (%d)",
+ req.context.get("remote_addr"),
+ ex.args[0][0], ex.args[0][1])
raise falcon.HTTPForbidden("Forbidden",
"Bad credentials: %s (%d)" % (ex.args[0][0], ex.args[0][1]))
except kerberos.KrbError as ex:
kerberos.authGSSServerClean(context)
- # TODO: logger.error
+ logger.error(u"Kerberos authentication failed from %s. "
+ "Bad credentials: %s (%d)",
+ req.context.get("remote_addr"),
+ ex.args[0][0], ex.args[0][1])
raise falcon.HTTPForbidden("Forbidden",
"Bad credentials: %s" % (ex.args[0],))
user = kerberos.authGSSServerUserName(context)
- req.context["user"] = User(user)
- req.context["groups"] = set()
+ req.context["user"] = User.objects.get(user)
try:
kerberos.authGSSServerClean(context)
except kerberos.GSSError as ex:
- # TODO: logger.error
+ logger.error(u"Kerberos authentication failed for user %s from %s. "
+ "Authentication system failure: %s (%d)",
+ user, req.context.get("remote_addr"),
+ ex.args[0][0], ex.args[0][1])
raise falcon.HTTPUnauthorized("Authentication System Failure %s (%s)" % (ex.args[0][0], ex.args[1][0]))
if result == kerberos.AUTH_GSS_COMPLETE:
- logger.debug("Succesfully authenticated user %s for %s from %s",
+ logger.debug(u"Succesfully authenticated user %s for %s from %s",
req.context["user"], req.env["PATH_INFO"], req.context["remote_addr"])
- return account_info(func)(resource, req, resp, *args, **kwargs)
+ return func(resource, req, resp, *args, **kwargs)
elif result == kerberos.AUTH_GSS_CONTINUE:
- # TODO: logger.error
+ logger.error(u"Kerberos authentication failed for user %s from %s. "
+ "Unauthorized, tried GSSAPI.",
+ user, req.context.get("remote_addr"))
raise falcon.HTTPUnauthorized("Unauthorized", "Tried GSSAPI")
else:
- # TODO: logger.error
+ logger.error(u"Kerberos authentication failed for user %s from %s. "
+ "Forbidden, tried GSSAPI.",
+ user, req.context.get("remote_addr"))
raise falcon.HTTPForbidden("Forbidden", "Tried GSSAPI")
@@ -238,27 +123,26 @@ def authenticate(optional=False):
basic, token = req.auth.split(" ", 1)
user, passwd = b64decode(token).split(":", 1)
- if "ldap_conn" not in req.context:
- for server in config.LDAP_SERVERS:
- click.echo("Connecting to %s as %s" % (server, user))
- conn = ldap.initialize(server)
- conn.set_option(ldap.OPT_REFERRALS, 0)
- try:
- conn.simple_bind_s(user if "@" in user else "%s@%s" % (user, constants.DOMAIN), passwd)
- except ldap.LDAPError, e:
- resp.append_header("WWW-Authenticate", "Basic")
- logger.debug("Failed to authenticate with user '%s'", user)
- raise falcon.HTTPUnauthorized("Forbidden",
- "Please authenticate with %s domain account or supply UPN" % constants.DOMAIN)
+ for server in config.LDAP_SERVERS:
+ click.echo("Connecting to %s as %s" % (server, user))
+ conn = ldap.initialize(server)
+ conn.set_option(ldap.OPT_REFERRALS, 0)
+ try:
+ conn.simple_bind_s(user if "@" in user else "%s@%s" % (user, constants.DOMAIN), passwd)
+ except ldap.LDAPError, e:
+ resp.append_header("WWW-Authenticate", "Basic")
+ logger.critical("LDAP bind authentication failed for user %s from %s",
+ repr(user), req.context.get("remote_addr"))
+ raise falcon.HTTPUnauthorized("Forbidden",
+ "Please authenticate with %s domain account or supply UPN" % constants.DOMAIN)
- req.context["ldap_conn"] = conn
- break
- else:
- raise ValueError("No LDAP servers!")
+ req.context["ldap_conn"] = conn
+ break
+ else:
+ raise ValueError("No LDAP servers!")
- req.context["user"] = User(user)
- req.context["groups"] = set()
- return account_info(func)(resource, req, resp, *args, **kwargs)
+ req.context["user"] = User.objects.get(user)
+ return func(resource, req, resp, *args, **kwargs)
def pam_authenticate(resource, req, resp, *args, **kwargs):
@@ -282,11 +166,12 @@ def authenticate(optional=False):
import simplepam
if not simplepam.authenticate(user, passwd, "sshd"):
+ logger.critical("Basic authentication failed for user %s from %s",
+ repr(user), req.context.get("remote_addr"))
raise falcon.HTTPUnauthorized("Forbidden", "Invalid password")
- req.context["user"] = User(user)
- req.context["groups"] = set()
- return account_info(func)(resource, req, resp, *args, **kwargs)
+ req.context["user"] = User.objects.get(user)
+ return func(resource, req, resp, *args, **kwargs)
if config.AUTHENTICATION_BACKENDS == {"kerberos"}:
return kerberos_authenticate
@@ -302,14 +187,11 @@ def authenticate(optional=False):
def login_required(func):
return authenticate()(func)
-
def login_optional(func):
return authenticate(optional=True)(func)
-
def authorize_admin(func):
-
- def whitelist_authorize(resource, req, resp, *args, **kwargs):
+ def whitelist_authorize_admin(resource, req, resp, *args, **kwargs):
# Check for username whitelist
if not req.context.get("user") or req.context.get("user") not in config.ADMIN_WHITELIST:
logger.info("Rejected access to administrative call %s by %s from %s, user not whitelisted",
@@ -317,8 +199,13 @@ def authorize_admin(func):
raise falcon.HTTPForbidden("Forbidden", "User %s not whitelisted" % req.context.get("user"))
return func(resource, req, resp, *args, **kwargs)
- if config.AUTHORIZATION_BACKEND == "whitelist":
- return whitelist_authorize
- else:
- return member_of(config.ADMINS_GROUP)(func)
+ def authorize_admin(resource, req, resp, *args, **kwargs):
+ if req.context.get("user").is_admin():
+ req.context["admin_authorized"] = True
+ return func(resource, req, resp, *args, **kwargs)
+ logger.info("User '%s' not authorized to access administrative API", req.context.get("user").name)
+ raise falcon.HTTPForbidden("Forbidden", "User not authorized to perform administrative operations")
+ if config.AUTHORIZATION_BACKEND == "whitelist":
+ return whitelist_authorize_admin
+ return authorize_admin
diff --git a/certidude/authority.py b/certidude/authority.py
index 815e8ea..2e51bee 100644
--- a/certidude/authority.py
+++ b/certidude/authority.py
@@ -26,12 +26,12 @@ def publish_certificate(func):
cert = func(csr, *args, **kwargs)
assert isinstance(cert, Certificate), "notify wrapped function %s returned %s" % (func, type(cert))
- if cert.email_address:
- mailer.send(
- "%s %s <%s>" % (cert.given_name, cert.surname, cert.email_address),
- "certificate-signed.md",
- attachments=(cert,),
- certificate=cert)
+ mailer.send(
+ "certificate-signed.md",
+ to= "%s %s <%s>" % (cert.given_name, cert.surname, cert.email_address) if
+ cert.given_name and cert.surname else cert.email_address,
+ attachments=(cert,),
+ certificate=cert)
if config.PUSH_PUBLISH:
url = config.PUSH_PUBLISH % csr.fingerprint()
@@ -85,7 +85,9 @@ def store_request(buf, overwrite=False):
fh.write(buf)
os.rename(request_path + ".part", request_path)
- return Request(open(request_path))
+ req = Request(open(request_path))
+ mailer.send("request-stored.md", attachments=(req,), request=req)
+ return req
def signer_exec(cmd, *bits):
@@ -110,6 +112,7 @@ def revoke_certificate(common_name):
revoked_filename = os.path.join(config.REVOKED_DIR, "%s.pem" % cert.serial_number)
os.rename(cert.path, revoked_filename)
push.publish("certificate-revoked", cert.common_name)
+ mailer.send("certificate-revoked.md", attachments=(cert,), certificate=cert)
def list_requests(directory=config.REQUESTS_DIR):
@@ -184,7 +187,7 @@ def generate_pkcs12_bundle(common_name, key_size=4096, owner=None):
if owner.surname:
csr.get_subject().SN = owner.surname
csr.add_extensions([
- crypto.X509Extension("subjectAltName", True, "email:%s" % owner.mail)])
+ crypto.X509Extension("subjectAltName", True, "email:%s" % owner.mail.encode("ascii"))])
buf = crypto.dump_certificate_request(crypto.FILETYPE_PEM, csr)
diff --git a/certidude/cli.py b/certidude/cli.py
index b698fdd..7cf1f68 100755
--- a/certidude/cli.py
+++ b/certidude/cli.py
@@ -15,6 +15,7 @@ import subprocess
import sys
from configparser import ConfigParser
from certidude import constants
+from certidude.helpers import certidude_request_certificate
from certidude.common import expand_paths, ip_address, ip_network
from datetime import datetime
from humanize import naturaltime
@@ -63,8 +64,6 @@ if os.getuid() >= 1000:
@click.command("spawn", help="Run processes for requesting certificates and configuring services")
@click.option("-f", "--fork", default=False, is_flag=True, help="Fork to background")
def certidude_request_spawn(fork):
- from certidude.helpers import certidude_request_certificate
-
clients = ConfigParser()
clients.readfp(open("/etc/certidude/client.conf"))
@@ -80,7 +79,7 @@ def certidude_request_spawn(fork):
os.makedirs(run_dir)
for server in clients.sections():
- if clients.get(server, "managed") != "true":
+ if clients.get(server, "trigger") != "interface up":
continue
pid_path = os.path.join(run_dir, server + ".pid")
@@ -115,6 +114,7 @@ def certidude_request_spawn(fork):
clients.get(server, "request_path"),
clients.get(server, "certificate_path"),
clients.get(server, "authority_path"),
+ clients.get(server, "revocations_path"),
socket.gethostname(),
None,
autosign=True,
@@ -133,9 +133,47 @@ def certidude_request_spawn(fork):
csum = csummer.hexdigest()
uuid = csum[:8] + "-" + csum[8:12] + "-" + csum[12:16] + "-" + csum[16:20] + "-" + csum[20:32]
+ # Intranet HTTPS handled by PKCS#12 bundle generation,
+ # so it will not be implemented here
+
+ if services.get(endpoint, "service") == "network-manager/openvpn":
+ config = ConfigParser()
+ config.add_section("connection")
+ config.add_section("vpn")
+ config.add_section("ipv4")
+ config.add_section("ipv6")
+
+ config.set("connection", "id", endpoint)
+ config.set("connection", "uuid", uuid)
+ config.set("connection", "type", "vpn")
+
+ config.set("vpn", "service-type", "org.freedesktop.NetworkManager.openvpn")
+ config.set("vpn", "connection-type", "tls")
+ config.set("vpn", "comp-lzo", "yes")
+ config.set("vpn", "cert-pass-flags", "0")
+ config.set("vpn", "tap-dev", "yes")
+ config.set("vpn", "remote-cert-tls", "server") # Assert TLS Server flag of X.509 certificate
+ config.set("vpn", "remote", services.get(endpoint, "remote"))
+ config.set("vpn", "key", clients.get(server, "key_path"))
+ config.set("vpn", "cert", clients.get(server, "certificate_path"))
+ config.set("vpn", "ca", clients.get(server, "authority_path"))
+
+ config.set("ipv6", "method", "auto")
+
+ config.set("ipv4", "method", "auto")
+ config.set("ipv4", "never-default", "true")
+
+ # Prevent creation of files with liberal permissions
+ os.umask(0o177)
+
+ # Write keyfile
+ with open(os.path.join("/etc/NetworkManager/system-connections", endpoint), "w") as configfile:
+ config.write(configfile)
+ continue
+
+
# Set up IPsec via NetworkManager
if services.get(endpoint, "service") == "network-manager/strongswan":
-
config = ConfigParser()
config.add_section("connection")
config.add_section("vpn")
@@ -146,14 +184,14 @@ def certidude_request_spawn(fork):
config.set("connection", "type", "vpn")
config.set("vpn", "service-type", "org.freedesktop.NetworkManager.strongswan")
- config.set("vpn", "userkey", clients.get(server, "key_path"))
- config.set("vpn", "usercert", clients.get(server, "certificate_path"))
config.set("vpn", "encap", "no")
- config.set("vpn", "address", services.get(endpoint, "remote"))
config.set("vpn", "virtual", "yes")
config.set("vpn", "method", "key")
- config.set("vpn", "certificate", clients.get(server, "authority_path"))
config.set("vpn", "ipcomp", "no")
+ config.set("vpn", "address", services.get(endpoint, "remote"))
+ config.set("vpn", "userkey", clients.get(server, "key_path"))
+ config.set("vpn", "usercert", clients.get(server, "certificate_path"))
+ config.set("vpn", "certificate", clients.get(server, "authority_path"))
config.set("ipv4", "method", "auto")
@@ -203,9 +241,7 @@ def certidude_request_spawn(fork):
os.system("ipsec start")
continue
-
-
- # TODO: OpenVPN, Puppet, OpenLDAP, intranet HTTPS,
+ # TODO: Puppet, OpenLDAP,
os.unlink(pid_path)
@@ -284,9 +320,8 @@ def certidude_signer_spawn(kill, no_interaction):
asyncore.loop()
-
@click.command("client", help="Setup X.509 certificates for application")
-@click.argument("url") #, help="Certidude authority endpoint URL")
+@click.argument("server")
@click.option("--common-name", "-cn", default=HOSTNAME, help="Common name, '%s' by default" % HOSTNAME)
@click.option("--org-unit", "-ou", help="Organizational unit")
@click.option("--email-address", "-m", default=EMAIL, help="E-mail associated with the request, '%s' by default" % EMAIL)
@@ -301,18 +336,18 @@ def certidude_signer_spawn(kill, no_interaction):
@click.option("--request-path", "-r", default=HOSTNAME + ".csr", help="Request path, %s.csr by default" % HOSTNAME)
@click.option("--certificate-path", "-c", default=HOSTNAME + ".crt", help="Certificate path, %s.crt by default" % HOSTNAME)
@click.option("--authority-path", "-a", default="ca.crt", help="Certificate authority certificate path, ca.crt by default")
+@click.option("--revocations-path", "-crl", default="ca.crl", help="Certificate revocation list, ca.crl by default")
def certidude_setup_client(quiet, **kwargs):
- from certidude.helpers import certidude_request_certificate
return certidude_request_certificate(**kwargs)
@click.command("server", help="Set up OpenVPN server")
-@click.argument("url")
+@click.argument("server")
@click.option("--common-name", "-cn", default=FQDN, help="Common name, %s by default" % FQDN)
@click.option("--org-unit", "-ou", help="Organizational unit")
@click.option("--email-address", "-m", default=EMAIL, help="E-mail associated with the request, '%s' by default" % EMAIL)
@click.option("--subnet", "-s", default="192.168.33.0/24", type=ip_network, help="OpenVPN subnet, 192.168.33.0/24 by default")
-@click.option("--local", "-l", default="127.0.0.1", help="OpenVPN listening address, defaults to 127.0.0.1")
+@click.option("--local", "-l", default="0.0.0.0", help="OpenVPN listening address, defaults to all interfaces")
@click.option("--port", "-p", default=1194, type=click.IntRange(1,60000), help="OpenVPN listening port, 1194 by default")
@click.option('--proto', "-t", default="udp", type=click.Choice(['udp', 'tcp']), help="OpenVPN transport protocol, UDP by default")
@click.option("--route", "-r", type=ip_network, multiple=True, help="Subnets to advertise via this connection, multiple allowed")
@@ -321,15 +356,15 @@ def certidude_setup_client(quiet, **kwargs):
type=click.File(mode="w", atomic=True, lazy=True),
help="OpenVPN configuration file")
@click.option("--directory", "-d", default="/etc/openvpn/keys", help="Directory for keys, /etc/openvpn/keys by default")
-@click.option("--key-path", "-key", default=HOSTNAME + ".key", help="Key path, %s.key relative to --directory by default" % HOSTNAME)
-@click.option("--request-path", "-csr", default=HOSTNAME + ".csr", help="Request path, %s.csr relative to --directory by default" % HOSTNAME)
-@click.option("--certificate-path", "-crt", default=HOSTNAME + ".crt", help="Certificate path, %s.crt relative to --directory by default" % HOSTNAME)
-@click.option("--dhparam-path", "-dh", default="dhparam2048.pem", help="Diffie/Hellman parameters path, dhparam2048.pem relative to --directory by default")
+@click.option("--key-path", "-key", default=HOSTNAME + ".key", help="Key path, %s.key relative to -d by default" % HOSTNAME)
+@click.option("--request-path", "-csr", default=HOSTNAME + ".csr", help="Request path, %s.csr relative to -d by default" % HOSTNAME)
+@click.option("--certificate-path", "-crt", default=HOSTNAME + ".crt", help="Certificate path, %s.crt relative to -d by default" % HOSTNAME)
+@click.option("--dhparam-path", "-dh", default="dhparam2048.pem", help="Diffie/Hellman parameters path, dhparam2048.pem relative to -d by default")
@click.option("--authority-path", "-ca", default="ca.crt", help="Certificate authority certificate path, ca.crt relative to --dir by default")
+@click.option("--revocations-path", "-crl", default="ca.crl", help="Certificate revocation list, ca.crl relative to -d by default")
@expand_paths()
-def certidude_setup_openvpn_server(url, config, subnet, route, email_address, common_name, org_unit, directory, key_path, request_path, certificate_path, authority_path, dhparam_path, local, proto, port):
+def certidude_setup_openvpn_server(server, config, subnet, route, email_address, common_name, org_unit, directory, key_path, request_path, certificate_path, authority_path, revocations_path, dhparam_path, local, proto, port):
# TODO: Intelligent way of getting last IP address in the subnet
- from certidude.helpers import certidude_request_certificate
subnet_first = None
subnet_last = None
subnet_second = None
@@ -346,15 +381,9 @@ def certidude_setup_openvpn_server(url, config, subnet, route, email_address, co
click.echo("use following command to sign on Certidude server instead of web interface:")
click.echo()
click.echo(" certidude sign %s" % common_name)
- retval = certidude_request_certificate(
- url,
- key_path,
- request_path,
- certificate_path,
- authority_path,
- common_name,
- org_unit,
- email_address,
+ retval = certidude_request_certificate(server,
+ key_path, request_path, certificate_path, authority_path, revocations_path,
+ common_name, org_unit, email_address,
key_usage="digitalSignature,keyEncipherment",
extended_key_usage="serverAuth",
wait=True)
@@ -378,7 +407,7 @@ def certidude_setup_openvpn_server(url, config, subnet, route, email_address, co
@click.command("nginx", help="Set up nginx as HTTPS server")
-@click.argument("url")
+@click.argument("server")
@click.option("--common-name", "-cn", default=FQDN, help="Common name, %s by default" % FQDN)
@click.option("--org-unit", "-ou", help="Organizational unit")
@click.option("--tls-config",
@@ -392,14 +421,14 @@ def certidude_setup_openvpn_server(url, config, subnet, route, email_address, co
@click.option("--directory", "-d", default="/etc/nginx/ssl", help="Directory for keys, /etc/nginx/ssl by default")
@click.option("--key-path", "-key", default=HOSTNAME + ".key", help="Key path, %s.key relative to -d by default" % HOSTNAME)
@click.option("--request-path", "-csr", default=HOSTNAME + ".csr", help="Request path, %s.csr relative to -d by default" % HOSTNAME)
-@click.option("--certificate-path", "-crt", default=HOSTNAME + ".crt", help="Certificate path, %s.crt relative to --directory by default" % HOSTNAME)
+@click.option("--certificate-path", "-crt", default=HOSTNAME + ".crt", help="Certificate path, %s.crt relative to -d by default" % HOSTNAME)
@click.option("--dhparam-path", "-dh", default="dhparam2048.pem", help="Diffie/Hellman parameters path, dhparam2048.pem relative to -d by default")
@click.option("--authority-path", "-ca", default="ca.crt", help="Certificate authority certificate path, ca.crt relative to -d by default")
+@click.option("--revocations-path", "-crl", default="ca.crl", help="Certificate revocation list, ca.crl relative to -d by default")
@click.option("--verify-client", "-vc", type=click.Choice(['optional', 'on', 'off']))
@expand_paths()
-def certidude_setup_nginx(url, site_config, tls_config, common_name, org_unit, directory, key_path, request_path, certificate_path, authority_path, dhparam_path, verify_client):
+def certidude_setup_nginx(server, site_config, tls_config, common_name, org_unit, directory, key_path, request_path, certificate_path, authority_path, revocations_path, dhparam_path, verify_client):
# TODO: Intelligent way of getting last IP address in the subnet
- from certidude.helpers import certidude_request_certificate
if not os.path.exists(certificate_path):
click.echo("As HTTPS server certificate needs specific key usage extensions please")
@@ -407,8 +436,8 @@ def certidude_setup_nginx(url, site_config, tls_config, common_name, org_unit, d
click.echo()
click.echo(" certidude sign %s" % common_name)
click.echo()
- retval = certidude_request_certificate(url, key_path, request_path,
- certificate_path, authority_path, common_name, org_unit,
+ retval = certidude_request_certificate(server, key_path, request_path,
+ certificate_path, authority_path, revocations_path, common_name, org_unit,
key_usage="digitalSignature,keyEncipherment",
extended_key_usage="serverAuth",
dns = constants.FQDN, wait=True, bundle=True)
@@ -446,33 +475,28 @@ def certidude_setup_nginx(url, site_config, tls_config, common_name, org_unit, d
@click.command("client", help="Set up OpenVPN client")
-@click.argument("url")
+@click.argument("server")
@click.argument("remote")
@click.option('--proto', "-t", default="udp", type=click.Choice(['udp', 'tcp']), help="OpenVPN transport protocol, UDP by default")
@click.option("--common-name", "-cn", default=HOSTNAME, help="Common name, %s by default" % HOSTNAME)
@click.option("--org-unit", "-ou", help="Organizational unit")
-@click.option("--email-address", "-m", default=EMAIL, help="E-mail associated with the request, '%s' by default" % EMAIL)
+@click.option("--email-address", "-m", help="E-mail associated with the request, none by default")
@click.option("--config", "-o",
default="/etc/openvpn/client-to-site.conf",
type=click.File(mode="w", atomic=True, lazy=True),
help="OpenVPN configuration file")
@click.option("--directory", "-d", default="/etc/openvpn/keys", help="Directory for keys, /etc/openvpn/keys by default")
-@click.option("--key-path", "-k", default=HOSTNAME + ".key", help="Key path, %s.key relative to --directory by default" % HOSTNAME)
-@click.option("--request-path", "-r", default=HOSTNAME + ".csr", help="Request path, %s.csr relative to --directory by default" % HOSTNAME)
-@click.option("--certificate-path", "-c", default=HOSTNAME + ".crt", help="Certificate path, %s.crt relative to --directory by default" % HOSTNAME)
-@click.option("--authority-path", "-a", default="ca.crt", help="Certificate authority certificate path, ca.crt relative to --dir by default")
+@click.option("--key-path", "-key", default=HOSTNAME + ".key", help="Key path, %s.key relative to -d by default" % HOSTNAME)
+@click.option("--request-path", "-csr", default=HOSTNAME + ".csr", help="Request path, %s.csr relative to -d by default" % HOSTNAME)
+@click.option("--certificate-path", "-crt", default=HOSTNAME + ".crt", help="Certificate path, %s.crt relative to -d by default" % HOSTNAME)
+@click.option("--authority-path", "-ca", default="ca.crt", help="Certificate authority certificate path, ca.crt relative to --dir by default")
+@click.option("--revocations-path", "-crl", default="ca.crl", help="Certificate revocation list, ca.crl relative to -d by default")
@expand_paths()
-def certidude_setup_openvpn_client(url, config, email_address, common_name, org_unit, directory, key_path, request_path, certificate_path, authority_path, proto, remote):
- from certidude.helpers import certidude_request_certificate
- retval = certidude_request_certificate(
- url,
- key_path,
- request_path,
- certificate_path,
- authority_path,
- common_name,
- org_unit,
- email_address,
+def certidude_setup_openvpn_client(server, config, email_address, common_name, org_unit, directory, key_path, request_path, certificate_path, authority_path, revocations_path, proto, remote):
+
+ retval = certidude_request_certificate(server,
+ key_path, request_path, certificate_path, authority_path, revocations_path,
+ common_name, org_unit, email_address,
wait=True)
if retval:
@@ -490,7 +514,7 @@ def certidude_setup_openvpn_client(url, config, email_address, common_name, org_
@click.command("server", help="Set up strongSwan server")
-@click.argument("url")
+@click.argument("server")
@click.option("--common-name", "-cn", default=FQDN, help="Common name, %s by default" % FQDN)
@click.option("--org-unit", "-ou", help="Organizational unit")
@click.option("--fqdn", "-f", default=FQDN, help="Fully qualified hostname associated with the certificate")
@@ -511,8 +535,9 @@ def certidude_setup_openvpn_client(url, config, email_address, common_name, org_
@click.option("--request-path", "-csr", default="reqs/%s.pem" % HOSTNAME, help="Request path, reqs/%s.pem by default" % HOSTNAME)
@click.option("--certificate-path", "-crt", default="certs/%s.pem" % HOSTNAME, help="Certificate path, certs/%s.pem by default" % HOSTNAME)
@click.option("--authority-path", "-ca", default="cacerts/ca.pem", help="Certificate authority certificate path, cacerts/ca.pem by default")
+@click.option("--revocations-path", "-crl", default="crls/ca.pem", help="Certificate revocation list, crls/ca.pem by default")
@expand_paths()
-def certidude_setup_strongswan_server(url, config, secrets, subnet, route, email_address, common_name, org_unit, directory, key_path, request_path, certificate_path, authority_path, local, fqdn):
+def certidude_setup_strongswan_server(server, config, secrets, subnet, route, email_address, common_name, org_unit, directory, key_path, request_path, certificate_path, authority_path, revocations_path, local, fqdn):
if "." not in common_name:
raise ValueError("Hostname has to be fully qualified!")
if not local:
@@ -523,19 +548,13 @@ def certidude_setup_strongswan_server(url, config, secrets, subnet, route, email
click.echo("use following command to sign on Certidude server instead of web interface:")
click.echo()
click.echo(" certidude sign %s" % common_name)
- from certidude.helpers import certidude_request_certificate
- retval = certidude_request_certificate(
- url,
- key_path,
- request_path,
- certificate_path,
- authority_path,
- common_name,
- org_unit,
- email_address,
+ click.echo()
+
+ retval = certidude_request_certificate(server,
+ key_path, request_path, certificate_path, authority_path, revocations_path,
+ common_name, org_unit, email_address,
key_usage="digitalSignature,keyEncipherment",
extended_key_usage="serverAuth,1.3.6.1.5.5.8.2.2",
- ip_address=local,
dns=fqdn,
wait=True)
@@ -555,7 +574,7 @@ def certidude_setup_strongswan_server(url, config, secrets, subnet, route, email
@click.command("client", help="Set up strongSwan client")
-@click.argument("url")
+@click.argument("server")
@click.argument("remote")
@click.option("--common-name", "-cn", default=HOSTNAME, help="Common name, %s by default" % HOSTNAME)
@click.option("--org-unit", "-ou", help="Organizational unit")
@@ -581,18 +600,12 @@ def certidude_setup_strongswan_server(url, config, secrets, subnet, route, email
@click.option("--request-path", "-csr", default="reqs/%s.pem" % HOSTNAME, help="Request path, reqs/%s.pem by default" % HOSTNAME)
@click.option("--certificate-path", "-crt", default="certs/%s.pem" % HOSTNAME, help="Certificate path, certs/%s.pem by default" % HOSTNAME)
@click.option("--authority-path", "-ca", default="cacerts/ca.pem", help="Certificate authority certificate path, cacerts/ca.pem by default")
+@click.option("--revocations-path", "-crl", default="crls/ca.pemf", help="Certificate revocation list, ca.crl relative to -d by default")
@expand_paths()
-def certidude_setup_strongswan_client(url, config, secrets, email_address, common_name, org_unit, directory, key_path, request_path, certificate_path, authority_path, remote, auto, dpdaction):
- from certidude.helpers import certidude_request_certificate
- retval = certidude_request_certificate(
- url,
- key_path,
- request_path,
- certificate_path,
- authority_path,
- common_name,
- org_unit,
- email_address,
+def certidude_setup_strongswan_client(server, config, secrets, email_address, common_name, org_unit, directory, key_path, request_path, certificate_path, authority_path, remote, auto, dpdaction):
+ retval = certidude_request_certificate(server,
+ key_path, request_path, certificate_path, authority_path,
+ common_name, org_unit, email_address,
wait=True)
if retval:
@@ -612,8 +625,8 @@ def certidude_setup_strongswan_client(url, config, secrets, email_address, commo
@click.command("networkmanager", help="Set up strongSwan client via NetworkManager")
-@click.argument("url")
-@click.argument("remote")
+@click.argument("server") # Certidude server
+@click.argument("remote") # StrongSwan gateway
@click.option("--common-name", "-cn", default=HOSTNAME, help="Common name, %s by default" % HOSTNAME)
@click.option("--org-unit", "-ou", help="Organizational unit")
@click.option("--email-address", "-m", default=EMAIL, help="E-mail associated with the request, '%s' by default" % EMAIL)
@@ -622,63 +635,71 @@ def certidude_setup_strongswan_client(url, config, secrets, email_address, commo
@click.option("--request-path", "-csr", default="reqs/%s.pem" % HOSTNAME, help="Request path, reqs/%s.pem by default" % HOSTNAME)
@click.option("--certificate-path", "-crt", default="certs/%s.pem" % HOSTNAME, help="Certificate path, certs/%s.pem by default" % HOSTNAME)
@click.option("--authority-path", "-ca", default="cacerts/ca.pem", help="Certificate authority certificate path, cacerts/ca.pem by default")
+@click.option("--revocations-path", "-crl", default="crls/ca.pem", help="Certificate revocation list, crls/ca.pem by default")
@expand_paths()
-def certidude_setup_strongswan_networkmanager(url, email_address, common_name, org_unit, directory, key_path, request_path, certificate_path, authority_path, remote):
- from certidude.helpers import certidude_request_certificate
- retval = certidude_request_certificate(
- url,
- key_path,
- request_path,
- certificate_path,
- authority_path,
- common_name,
- org_unit,
- email_address,
+def certidude_setup_strongswan_networkmanager(server, email_address, common_name, org_unit, directory, key_path, request_path, certificate_path, authority_path, revocations_path, remote):
+ retval = certidude_request_certificate(server,
+ key_path, request_path, certificate_path, authority_path, revocations_path,
+ common_name, org_unit, email_address,
wait=True)
if retval:
return retval
- csummer = hashlib.sha1()
- csummer.update(remote.encode("ascii"))
- csum = csummer.hexdigest()
- uuid = csum[:8] + "-" + csum[8:12] + "-" + csum[12:16] + "-" + csum[16:20] + "-" + csum[20:32]
+ services = ConfigParser()
+ if os.path.exists("/etc/certidude/services.conf"):
+ services.readfp(open("/etc/certidude/services.conf"))
- config = ConfigParser()
- config.add_section("connection")
- config.add_section("vpn")
- config.add_section("ipv4")
+ endpoint = "IPSec to %s" % remote
- config.set("connection", "id", remote)
- config.set("connection", "uuid", uuid)
- config.set("connection", "type", "vpn")
- config.set("connection", "autoconnect", "true")
+ if services.has_section(endpoint):
+ click.echo("Section %s already exists in /etc/certidude/services.conf, not reconfiguring" % endpoint)
+ else:
+ click.echo("Section %s added to /etc/certidude/client.conf" % endpoint)
+ services.add_section(endpoint)
+ services.set(endpoint, "authority", server)
+ services.set(endpoint, "remote", remote)
+ services.set(endpoint, "service", "network-manager/strongswan")
+ services.write(open("/etc/certidude/services.conf", "w"))
- config.set("vpn", "service-type", "org.freedesktop.NetworkManager.strongswan")
- config.set("vpn", "userkey", key_path)
- config.set("vpn", "usercert", certificate_path)
- config.set("vpn", "encap", "no")
- config.set("vpn", "address", remote)
- config.set("vpn", "virtual", "yes")
- config.set("vpn", "method", "key")
- config.set("vpn", "certificate", authority_path)
- config.set("vpn", "ipcomp", "no")
- config.set("ipv4", "method", "auto")
+@click.command("networkmanager", help="Set up OpenVPN client via NetworkManager")
+@click.argument("server") # Certidude server
+@click.argument("remote") # OpenVPN gateway
+@click.option("--common-name", "-cn", default=HOSTNAME, help="Common name, %s by default" % HOSTNAME)
+@click.option("--org-unit", "-ou", help="Organizational unit")
+@click.option("--email-address", "-m", help="E-mail associated with the request, none by default")
+@click.option("--directory", "-d", default="/etc/openvpn/keys", help="Directory for keys, /etc/openvpn/keys by default")
+@click.option("--key-path", "-key", default=HOSTNAME + ".key", help="Key path, %s.key relative to -d by default" % HOSTNAME)
+@click.option("--request-path", "-csr", default=HOSTNAME + ".csr", help="Request path, %s.csr relative to -d by default" % HOSTNAME)
+@click.option("--certificate-path", "-crt", default=HOSTNAME + ".crt", help="Certificate path, %s.crt relative to -d by default" % HOSTNAME)
+@click.option("--authority-path", "-ca", default="ca.crt", help="Certificate path, ca.crt relative to -d by default")
+@click.option("--revocations-path", "-crl", default="ca.crl", help="Certificate revocation list, ca.crl by default")
+@expand_paths()
+def certidude_setup_openvpn_networkmanager(server, email_address, common_name, org_unit, directory, key_path, request_path, certificate_path, authority_path, revocations_path, remote):
+ retval = certidude_request_certificate(server,
+ key_path, request_path, certificate_path, authority_path, revocations_path,
+ common_name, org_unit, email_address,
+ wait=True)
- # Prevent creation of files with liberal permissions
- os.umask(0o277)
+ if retval:
+ return retval
- # Write keyfile
- with open(os.path.join("/etc/NetworkManager/system-connections", remote), "w") as configfile:
- config.write(configfile)
+ services = ConfigParser()
+ if os.path.exists("/etc/certidude/services.conf"):
+ services.readfp(open("/etc/certidude/services.conf"))
- # TODO: Avoid race condition here
- sleep(3)
-
- # Tell NetworkManager to bring up the VPN connection
- subprocess.call(("nmcli", "c", "up", "uuid", uuid))
+ endpoint = "OpenVPN to %s" % remote
+ if services.has_section(endpoint):
+ click.echo("Section %s already exists in /etc/certidude/services.conf, not reconfiguring" % endpoint)
+ else:
+ click.echo("Section %s added to /etc/certidude/client.conf" % endpoint)
+ services.add_section(endpoint)
+ services.set(endpoint, "authority", server)
+ services.set(endpoint, "remote", remote)
+ services.set(endpoint, "service", "network-manager/openvpn")
+ services.write(open("/etc/certidude/services.conf", "w"))
@click.command("production", help="Set up nginx, uwsgi and cron")
@click.option("--username", default="certidude", help="Service user account, created if necessary, 'certidude' by default")
@@ -761,7 +782,8 @@ def certidude_setup_production(username, hostname, push_server, nginx_config, uw
@click.option("--push-server", default="", help="Streaming nginx push server")
@click.option("--email-address", default="certidude@" + FQDN, help="E-mail address of the CA")
@click.option("--directory", default=os.path.join("/var/lib/certidude", FQDN), help="Directory for authority files, /var/lib/certidude/ by default")
-def certidude_setup_authority(parent, country, state, locality, organization, organizational_unit, common_name, directory, certificate_lifetime, authority_lifetime, revocation_list_lifetime, pkcs11, crl_distribution_url, ocsp_responder_url, push_server, email_address):
+@click.option("--outbox", default="smtp://smtp.%s" % constants.DOMAIN, help="SMTP server, smtp://smtp.%s by default" % constants.DOMAIN)
+def certidude_setup_authority(parent, country, state, locality, organization, organizational_unit, common_name, directory, certificate_lifetime, authority_lifetime, revocation_list_lifetime, pkcs11, crl_distribution_url, ocsp_responder_url, push_server, email_address, outbox):
# Make sure common_name is valid
if not re.match(r"^[\.\-_a-zA-Z0-9]+$", common_name):
@@ -1155,6 +1177,7 @@ certidude_setup_strongswan.add_command(certidude_setup_strongswan_client)
certidude_setup_strongswan.add_command(certidude_setup_strongswan_networkmanager)
certidude_setup_openvpn.add_command(certidude_setup_openvpn_server)
certidude_setup_openvpn.add_command(certidude_setup_openvpn_client)
+certidude_setup_openvpn.add_command(certidude_setup_openvpn_networkmanager)
certidude_setup.add_command(certidude_setup_authority)
certidude_setup.add_command(certidude_setup_openvpn)
certidude_setup.add_command(certidude_setup_strongswan)
diff --git a/certidude/config.py b/certidude/config.py
index 7571a48..1af7512 100644
--- a/certidude/config.py
+++ b/certidude/config.py
@@ -57,7 +57,6 @@ except configparser.NoOptionError:
PUSH_LONG_POLL = PUSH_SERVER + "/lp/%s"
PUSH_PUBLISH = PUSH_SERVER + "/pub?id=%s"
-
TAGGING_BACKEND = cp.get("tagging", "backend")
LOGGING_BACKEND = cp.get("logging", "backend")
LEASES_BACKEND = cp.get("leases", "backend")
@@ -68,18 +67,16 @@ if "whitelist" == AUTHORIZATION_BACKEND:
ADMINS_WHITELIST = set([j for j in cp.get("authorization", "admins whitelist").split(" ") if j])
elif "posix" == AUTHORIZATION_BACKEND:
USERS_GROUP = cp.get("authorization", "posix user group")
- ADMINS_GROUP = cp.get("authorization", "posix admin group")
+ ADMIN_GROUP = cp.get("authorization", "posix admin group")
elif "ldap" == AUTHORIZATION_BACKEND:
- USERS_GROUP = cp.get("authorization", "ldap user group")
- ADMINS_GROUP = cp.get("authorization", "ldap admin group")
+ LDAP_GSSAPI_CRED_CACHE = cp.get("authorization", "ldap gssapi credential cache")
+ LDAP_USER_FILTER = cp.get("authorization", "ldap user filter")
+ LDAP_ADMIN_FILTER = cp.get("authorization", "ldap admin filter")
+ if "%s" not in LDAP_USER_FILTER: raise ValueError("No placeholder %s for username in 'ldap user filter'")
+ if "%s" not in LDAP_ADMIN_FILTER: raise ValueError("No placeholder %s for username in 'ldap admin filter'")
else:
raise NotImplementedError("Unknown authorization backend '%s'" % AUTHORIZATION_BACKEND)
-LDAP_USER_FILTER = cp.get("authorization", "ldap user filter")
-LDAP_GROUP_FILTER = cp.get("authorization", "ldap group filter")
-LDAP_MEMBERS_FILTER = cp.get("authorization", "ldap members filter")
-LDAP_MEMBER_OF_FILTER = cp.get("authorization", "ldap member of filter")
-
for line in open("/etc/ldap/ldap.conf"):
line = line.strip().lower()
if "#" in line:
@@ -92,6 +89,5 @@ for line in open("/etc/ldap/ldap.conf"):
click.echo("LDAP servers: %s" % " ".join(LDAP_SERVERS))
elif key == "base":
LDAP_BASE = value
-else:
- click.echo("No LDAP servers specified in /etc/ldap/ldap.conf")
+# TODO: Check if we don't have base or servers
diff --git a/certidude/constants.py b/certidude/constants.py
index d04b661..86cbf04 100644
--- a/certidude/constants.py
+++ b/certidude/constants.py
@@ -1,4 +1,5 @@
+import click
import socket
FQDN = socket.getaddrinfo(socket.gethostname(), 0, socket.AF_INET, 0, 0, socket.AI_CANONNAME)[0][3]
diff --git a/certidude/decorators.py b/certidude/decorators.py
index 3aaac48..83ea9f6 100644
--- a/certidude/decorators.py
+++ b/certidude/decorators.py
@@ -6,6 +6,7 @@ import re
import types
from datetime import date, time, datetime
from OpenSSL import crypto
+from certidude.auth import User
from certidude.wrappers import Request, Certificate
from urllib.parse import urlparse
@@ -76,6 +77,9 @@ class MyEncoder(json.JSONEncoder):
if isinstance(obj, Certificate):
return dict([(key, getattr(obj, key)) for key in self.CERTIFICATE_ATTRIBUTES \
if hasattr(obj, key) and getattr(obj, key)])
+ if isinstance(obj, User):
+ return dict(name=obj.name, given_name=obj.given_name,
+ surname=obj.surname, mail=obj.mail)
if hasattr(obj, "serialize"):
return obj.serialize()
return json.JSONEncoder.default(self, obj)
@@ -95,16 +99,20 @@ def serialize(func):
resp.set_header("Content-Type", "application/json")
resp.set_header("Content-Disposition", "inline")
resp.body = json.dumps(r, cls=MyEncoder)
-
elif hasattr(r, "content_type") and req.client_accepts(r.content_type):
resp.set_header("Content-Type", r.content_type)
resp.set_header("Content-Disposition",
("attachment; filename=%s" % r.suggested_filename).encode("ascii"))
resp.body = r.dump()
- else:
- logger.debug("Client did not accept application/json or %s, client expected %s" % (r.content_type, req.accept))
+ elif hasattr(r, "content_type"):
+ logger.debug("Client did not accept application/json or %s, "
+ "client expected %s", r.content_type, req.accept)
raise falcon.HTTPUnsupportedMediaType(
"Client did not accept application/json or %s" % r.content_type)
+ else:
+ logger.debug("Client did not accept application/json, client expected %s", req.accept)
+ raise falcon.HTTPUnsupportedMediaType(
+ "Client did not accept application/json")
return r
return wrapped
diff --git a/certidude/helpers.py b/certidude/helpers.py
index 35ba5fc..ac00b16 100644
--- a/certidude/helpers.py
+++ b/certidude/helpers.py
@@ -2,11 +2,14 @@
import click
import os
import requests
+import subprocess
+import tempfile
from certidude import errors
from certidude.wrappers import Certificate, Request
+from configparser import ConfigParser
from OpenSSL import crypto
-def certidude_request_certificate(url, key_path, request_path, certificate_path, authority_path, common_name, org_unit=None, email_address=None, given_name=None, surname=None, autosign=False, wait=False, key_usage=None, extended_key_usage=None, ip_address=None, dns=None, bundle=False):
+def certidude_request_certificate(server, key_path, request_path, certificate_path, authority_path, revocations_path, common_name, org_unit=None, email_address=None, given_name=None, surname=None, autosign=False, wait=False, key_usage=None, extended_key_usage=None, ip_address=None, dns=None, bundle=False):
"""
Exchange CSR for certificate using Certidude HTTP API server
"""
@@ -18,38 +21,80 @@ def certidude_request_certificate(url, key_path, request_path, certificate_path,
if wait:
request_params.add("wait=forever")
- # Expand ca.example.com to http://ca.example.com/api/
- if not url.endswith("/"):
- url += "/api/"
- if "//" not in url:
- url = "http://" + url
-
- authority_url = url + "certificate"
- request_url = url + "request"
+ # Expand ca.example.com
+ authority_url = "http://%s/api/certificate/" % server
+ request_url = "http://%s/api/request/" % server
+ revoked_url = "http://%s/api/revoked/" % server
if request_params:
request_url = request_url + "?" + "&".join(request_params)
- if os.path.exists(certificate_path):
- click.echo("Found certificate: %s" % certificate_path)
- # TODO: Check certificate validity, download CRL?
- return
-
if os.path.exists(authority_path):
- click.echo("Found CA certificate in: %s" % authority_path)
+ click.echo("Found authority certificate in: %s" % authority_path)
else:
- click.echo("Attempting to fetch CA certificate from %s" % authority_url)
-
+ click.echo("Attempting to fetch authority certificate from %s" % authority_url)
try:
r = requests.get(authority_url,
headers={"Accept": "application/x-x509-ca-cert,application/x-pem-file"})
cert = crypto.load_certificate(crypto.FILETYPE_PEM, r.text)
except crypto.Error:
raise ValueError("Failed to parse PEM: %s" % r.text)
- with open(authority_path + ".part", "w") as oh:
+ authority_partial = tempfile.mktemp(prefix=authority_path + ".part")
+ with open(authority_partial, "w") as oh:
oh.write(r.text)
- click.echo("Writing CA certificate to: %s" % authority_path)
- os.rename(authority_path + ".part", authority_path)
+ click.echo("Writing authority certificate to: %s" % authority_path)
+ os.rename(authority_partial, authority_path)
+
+ # Fetch certificate revocation list
+ r = requests.get(revoked_url, stream=True)
+ click.echo("Fetching CRL from %s to %s" % (revoked_url, revocations_path))
+ revocations_partial = tempfile.mktemp(prefix=revocations_path + ".part")
+ with open(revocations_partial, 'wb') as f:
+ for chunk in r.iter_content(chunk_size=8192):
+ if chunk:
+ f.write(chunk)
+ if subprocess.call(("openssl", "crl", "-CAfile", authority_path, "-in", revocations_partial, "-noout")):
+ raise ValueError("Failed to verify CRL in %s" % revocations_partial)
+ else:
+ # TODO: Check monotonically increasing CRL number
+ click.echo("Certificate revocation list passed verification")
+ os.rename(revocations_partial, revocations_path)
+
+ # Check if we have been inserted into CRL
+ if os.path.exists(certificate_path):
+ cert = crypto.load_certificate(crypto.FILETYPE_PEM, open(certificate_path).read())
+ revocation_list = crypto.load_crl(crypto.FILETYPE_PEM, open(revocations_path).read())
+ for revocation in revocation_list.get_revoked():
+ if int(revocation.get_serial(), 16) == cert.get_serial_number():
+ if revocation.get_reason() == "Certificate Hold": # TODO: 'Remove From CRL'
+ # TODO: Disable service for time being
+ click.echo("Certificate put on hold, doing nothing for now")
+ break
+
+ # Disable the client if operation has been ceased or
+ # the certificate has been superseded by other
+ if revocation.get_reason() in ("Cessation Of Operation", "Superseded"):
+ if os.path.exists("/etc/certidude/client.conf"):
+ clients.readfp(open("/etc/certidude/client.conf"))
+ if clients.has_section(server):
+ clients.set(server, "trigger", "operation ceased")
+ clients.write(open("/etc/certidude/client.conf", "w"))
+ click.echo("Authority operation ceased, disabling in /etc/certidude/client.conf")
+ # TODO: Disable related services
+ if revocation.get_reason() in ("CA Compromise", "AA Compromise"):
+ if os.path.exists(authority_path):
+ os.remove(key_path)
+
+ click.echo("Certificate has been revoked, wiping keys and certificates!")
+ if os.path.exists(key_path):
+ os.remove(key_path)
+ if os.path.exists(request_path):
+ os.remove(request_path)
+ if os.path.exists(certificate_path):
+ os.remove(certificate_path)
+ break
+ else:
+ click.echo("Certificate does not seem to be revoked. Good!")
try:
request = Request(open(request_path))
@@ -62,8 +107,9 @@ def certidude_request_certificate(url, key_path, request_path, certificate_path,
key.generate_key(crypto.TYPE_RSA, 4096)
# Dump private key
+ key_partial = tempfile.mktemp(prefix=key_path + ".part")
os.umask(0o077)
- with open(key_path + ".part", "wb") as fh:
+ with open(key_partial, "wb") as fh:
fh.write(crypto.dump_privatekey(crypto.FILETYPE_PEM, key))
# Construct CSR
@@ -107,10 +153,38 @@ def certidude_request_certificate(url, key_path, request_path, certificate_path,
fh.write(request.dump())
click.echo("Writing private key to: %s" % key_path)
- os.rename(key_path + ".part", key_path)
+ os.rename(key_partial, key_path)
click.echo("Writing certificate signing request to: %s" % request_path)
os.rename(request_path + ".part", request_path)
+ # We have CSR now, save the paths to client.conf so we could:
+ # Update CRL, renew certificate, maybe something extra?
+
+ if not os.path.exists("/etc/certidude"):
+ os.makedirs("/etc/certidude")
+
+ clients = ConfigParser()
+ if os.path.exists("/etc/certidude/client.conf"):
+ clients.readfp(open("/etc/certidude/client.conf"))
+
+ if clients.has_section(server):
+ click.echo("Section %s already exists in /etc/certidude/client.conf, not reconfiguring" % server)
+ else:
+ clients.add_section(server)
+ clients.set(server, "trigger", "interface up")
+ clients.set(server, "key_path", key_path)
+ clients.set(server, "request_path", request_path)
+ clients.set(server, "certificate_path", certificate_path)
+ clients.set(server, "authority_path", authority_path)
+ clients.set(server, "key_path", key_path)
+ clients.set(server, "revocations_path", revocations_path)
+ clients.write(open("/etc/certidude/client.conf", "w"))
+ click.echo("Section %s added to /etc/certidude/client.conf" % repr(server))
+
+ if os.path.exists(certificate_path):
+ click.echo("Found certificate: %s" % certificate_path)
+ # TODO: Check certificate validity, download CRL?
+ return
click.echo("Submitting to %s, waiting for response..." % request_url)
submission = requests.post(request_url,
diff --git a/certidude/mailer.py b/certidude/mailer.py
index 6fd1b6e..2d92e8c 100644
--- a/certidude/mailer.py
+++ b/certidude/mailer.py
@@ -1,6 +1,8 @@
+import click
import os
import smtplib
+from certidude.user import User
from markdown import markdown
from jinja2 import Environment, PackageLoader
from email.mime.multipart import MIMEMultipart
@@ -10,14 +12,18 @@ from urllib.parse import urlparse
env = Environment(loader=PackageLoader("certidude", "templates/mail"))
-def send(recipients, template, attachments=(), **context):
+def send(template, to=None, attachments=(), **context):
from certidude import authority, config
if not config.OUTBOX:
# Mailbox disabled, don't send e-mail
return
- if not recipients:
- raise ValueError("No e-mail recipients specified!")
+ recipients = u", ".join([unicode(j) for j in User.objects.filter_admins()])
+
+ if to:
+ recipients = to + u", " + recipients
+
+ click.echo("Sending e-mail %s to %s" % (template, recipients))
scheme, netloc, path, params, query, fragment = urlparse(config.OUTBOX)
scheme = scheme.lower()
diff --git a/certidude/static/js/templates.js b/certidude/static/js/templates.js
index b63863d..918cdfe 100644
--- a/certidude/static/js/templates.js
+++ b/certidude/static/js/templates.js
@@ -485,14 +485,14 @@ output += "\n E-mail disabled\n";
;
}
output += "
\n\nAuthenticated users allowed from:\n\n";
-if(runtime.inOperator("0.0.0.0/0",runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "session")),"user_subnets"))) {
+if(runtime.inOperator("0.0.0.0/0",runtime.memberLookup((runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "session")),"authority")),"user_subnets"))) {
output += "\n anywhere\n
\n";
;
}
else {
output += "\n \n \n ";
frame = frame.push();
-var t_3 = runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "session")),"user_subnets");
+var t_3 = runtime.memberLookup((runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "session")),"authority")),"user_subnets");
if(t_3) {var t_2 = t_3.length;
for(var t_1=0; t_1 < t_3.length; t_1++) {
var t_4 = t_3[t_1];
@@ -515,14 +515,14 @@ output += "\n \n";
;
}
output += "\n\n\nRequest submission is allowed from:\n\n";
-if(runtime.inOperator("0.0.0.0/0",runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "session")),"request_subnets"))) {
+if(runtime.inOperator("0.0.0.0/0",runtime.memberLookup((runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "session")),"authority")),"request_subnets"))) {
output += "\n anywhere\n
\n";
;
}
else {
output += "\n \n \n ";
frame = frame.push();
-var t_7 = runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "session")),"request_subnets");
+var t_7 = runtime.memberLookup((runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "session")),"authority")),"request_subnets");
if(t_7) {var t_6 = t_7.length;
for(var t_5=0; t_5 < t_7.length; t_5++) {
var t_8 = t_7[t_5];
@@ -545,7 +545,7 @@ output += "\n \n";
;
}
output += "\n\nAutosign is allowed from:\n";
-if(runtime.inOperator("0.0.0.0/0",runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "session")),"autosign_subnets"))) {
+if(runtime.inOperator("0.0.0.0/0",runtime.memberLookup((runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "session")),"authority")),"autosign_subnets"))) {
output += "\n anywhere\n
\n";
;
}
@@ -575,14 +575,14 @@ output += "\n \n";
;
}
output += "\n\nAuthority administration is allowed from:\n";
-if(runtime.inOperator("0.0.0.0/0",runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "session")),"admin_subnets"))) {
+if(runtime.inOperator("0.0.0.0/0",runtime.memberLookup((runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "session")),"authority")),"admin_subnets"))) {
output += "\n anywhere\n
\n";
;
}
else {
output += "\n \n ";
frame = frame.push();
-var t_15 = runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "session")),"admin_subnets");
+var t_15 = runtime.memberLookup((runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "session")),"authority")),"admin_subnets");
if(t_15) {var t_14 = t_15.length;
for(var t_13=0; t_13 < t_15.length; t_13++) {
var t_16 = t_15[t_13];
@@ -606,15 +606,11 @@ output += "\n \n";
}
output += "\n\nAuthority administration allowed for:
\n\n\n";
frame = frame.push();
-var t_19 = runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "session")),"admin_users");
-if(t_19) {var t_17;
-if(runtime.isArray(t_19)) {
-var t_18 = t_19.length;
-for(t_17=0; t_17 < t_19.length; t_17++) {
-var t_20 = t_19[t_17][0]
-frame.set("handle", t_19[t_17][0]);
-var t_21 = t_19[t_17][1]
-frame.set("full_name", t_19[t_17][1]);
+var t_19 = runtime.memberLookup((runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "session")),"authority")),"admin_users");
+if(t_19) {var t_18 = t_19.length;
+for(var t_17=0; t_17 < t_19.length; t_17++) {
+var t_20 = t_19[t_17];
+frame.set("user", t_20);
frame.set("loop.index", t_17 + 1);
frame.set("loop.index0", t_17);
frame.set("loop.revindex", t_18 - t_17);
@@ -622,32 +618,15 @@ frame.set("loop.revindex0", t_18 - t_17 - 1);
frame.set("loop.first", t_17 === 0);
frame.set("loop.last", t_17 === t_18 - 1);
frame.set("loop.length", t_18);
-output += "\n ";
-output += runtime.suppressValue(t_21, env.opts.autoescape);
-output += " \n";
+output += "\n ";
+output += runtime.suppressValue(runtime.memberLookup((t_20),"given_name"), env.opts.autoescape);
+output += " ";
+output += runtime.suppressValue(runtime.memberLookup((t_20),"surname"), env.opts.autoescape);
+output += " \n";
;
}
-} else {
-t_17 = -1;
-var t_18 = runtime.keys(t_19).length;
-for(var t_22 in t_19) {
-t_17++;
-var t_23 = t_19[t_22];
-frame.set("handle", t_22);
-frame.set("full_name", t_23);
-frame.set("loop.index", t_17 + 1);
-frame.set("loop.index0", t_17);
-frame.set("loop.revindex", t_18 - t_17);
-frame.set("loop.revindex0", t_18 - t_17 - 1);
-frame.set("loop.first", t_17 === 0);
-frame.set("loop.last", t_17 === t_18 - 1);
-frame.set("loop.length", t_18);
-output += "\n ";
-output += runtime.suppressValue(t_23, env.opts.autoescape);
-output += " \n";
-;
-}
-}
}
frame = frame.pop();
output += "\n \n\n\n";
@@ -658,14 +637,14 @@ output += "\nHere you can renew your certificates
\n\n";
;
}
output += "\n\n";
-var t_24;
-t_24 = runtime.memberLookup((runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "session")),"certificate")),"identity");
-frame.set("s", t_24, true);
+var t_21;
+t_21 = runtime.memberLookup((runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "session")),"certificate")),"identity");
+frame.set("s", t_21, true);
if(frame.topLevel) {
-context.setVariable("s", t_24);
+context.setVariable("s", t_21);
}
if(frame.topLevel) {
-context.addExport("s", t_24);
+context.addExport("s", t_21);
}
output += "\n\n\n";
if(runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "session")),"authority")) {
@@ -673,24 +652,24 @@ output += "\n\n Pending requests \n\n
output += runtime.suppressValue(runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "session")),"common_name"), env.opts.autoescape);
output += "\n\n
\n \n\n\n Signed certificates \n \n \n ";
frame = frame.push();
-var t_35 = env.getFilter("reverse").call(context, env.getFilter("sort").call(context, runtime.memberLookup((runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "session")),"authority")),"signed")));
-if(t_35) {var t_34 = t_35.length;
-for(var t_33=0; t_33 < t_35.length; t_33++) {
-var t_36 = t_35[t_33];
-frame.set("certificate", t_36);
-frame.set("loop.index", t_33 + 1);
-frame.set("loop.index0", t_33);
-frame.set("loop.revindex", t_34 - t_33);
-frame.set("loop.revindex0", t_34 - t_33 - 1);
-frame.set("loop.first", t_33 === 0);
-frame.set("loop.last", t_33 === t_34 - 1);
-frame.set("loop.length", t_34);
+var t_32 = env.getFilter("reverse").call(context, env.getFilter("sort").call(context, runtime.memberLookup((runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "session")),"authority")),"signed")));
+if(t_32) {var t_31 = t_32.length;
+for(var t_30=0; t_30 < t_32.length; t_30++) {
+var t_33 = t_32[t_30];
+frame.set("certificate", t_33);
+frame.set("loop.index", t_30 + 1);
+frame.set("loop.index0", t_30);
+frame.set("loop.revindex", t_31 - t_30);
+frame.set("loop.revindex0", t_31 - t_30 - 1);
+frame.set("loop.first", t_30 === 0);
+frame.set("loop.last", t_30 === t_31 - 1);
+frame.set("loop.length", t_31);
output += "\n ";
-env.getTemplate("views/signed.html", false, "views/authority.html", null, function(t_39,t_37) {
-if(t_39) { cb(t_39); return; }
-t_37.render(context.getVariables(), frame, function(t_40,t_38) {
-if(t_40) { cb(t_40); return; }
-output += t_38
+env.getTemplate("views/signed.html", false, "views/authority.html", null, function(t_36,t_34) {
+if(t_36) { cb(t_36); return; }
+t_34.render(context.getVariables(), frame, function(t_37,t_35) {
+if(t_37) { cb(t_37); return; }
+output += t_35
output += "\n\t ";
})});
}
@@ -729,31 +708,31 @@ output += "/certificate/ > session.pem\n openssl ocsp -issuer session.pem -CA
output += runtime.suppressValue(runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "request")),"url"), env.opts.autoescape);
output += "/ocsp/ -serial 0x\n \n -->\n \n ";
frame = frame.push();
-var t_43 = runtime.memberLookup((runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "session")),"authority")),"revoked");
-if(t_43) {var t_42 = t_43.length;
-for(var t_41=0; t_41 < t_43.length; t_41++) {
-var t_44 = t_43[t_41];
-frame.set("j", t_44);
-frame.set("loop.index", t_41 + 1);
-frame.set("loop.index0", t_41);
-frame.set("loop.revindex", t_42 - t_41);
-frame.set("loop.revindex0", t_42 - t_41 - 1);
-frame.set("loop.first", t_41 === 0);
-frame.set("loop.last", t_41 === t_42 - 1);
-frame.set("loop.length", t_42);
+var t_40 = runtime.memberLookup((runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "session")),"authority")),"revoked");
+if(t_40) {var t_39 = t_40.length;
+for(var t_38=0; t_38 < t_40.length; t_38++) {
+var t_41 = t_40[t_38];
+frame.set("j", t_41);
+frame.set("loop.index", t_38 + 1);
+frame.set("loop.index0", t_38);
+frame.set("loop.revindex", t_39 - t_38);
+frame.set("loop.revindex0", t_39 - t_38 - 1);
+frame.set("loop.first", t_38 === 0);
+frame.set("loop.last", t_38 === t_39 - 1);
+frame.set("loop.length", t_39);
output += "\n \n ";
-output += runtime.suppressValue(runtime.memberLookup((t_44),"changed"), env.opts.autoescape);
+output += runtime.suppressValue(runtime.memberLookup((t_41),"changed"), env.opts.autoescape);
output += "\n ";
-output += runtime.suppressValue(runtime.memberLookup((t_44),"serial_number"), env.opts.autoescape);
+output += runtime.suppressValue(runtime.memberLookup((t_41),"serial_number"), env.opts.autoescape);
output += " ";
-output += runtime.suppressValue(runtime.memberLookup((t_44),"identity"), env.opts.autoescape);
+output += runtime.suppressValue(runtime.memberLookup((t_41),"identity"), env.opts.autoescape);
output += " \n \n ";
;
}
}
-if (!t_42) {
+if (!t_39) {
output += "\n Great job! No certificate signing requests to sign. \n\t ";
}
frame = frame.pop();
@@ -1098,7 +1077,7 @@ output += runtime.suppressValue(runtime.memberLookup((runtime.contextOrFrameLook
output += "\n ";
})});
}
-output += "\n \n ";
+output += "\n\n ";
if(runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "certificate")),"given_name") || runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "certificate")),"surname")) {
output += "\n ";
env.getTemplate("img/iconmonstr-user-5.svg", false, "views/signed.html", null, function(t_11,t_9) {
diff --git a/certidude/static/views/authority.html b/certidude/static/views/authority.html
index 8f50479..05bb553 100644
--- a/certidude/static/views/authority.html
+++ b/certidude/static/views/authority.html
@@ -31,13 +31,13 @@ as such require complete reset of X509 infrastructure if some of them needs to b
Authenticated users allowed from:
-{% if "0.0.0.0/0" in session.user_subnets %}
+{% if "0.0.0.0/0" in session.authority.user_subnets %}
anywhere
{% else %}
- {% for i in session.user_subnets %}
+ {% for i in session.authority.user_subnets %}
{{ i }}
{% endfor %}
@@ -46,20 +46,20 @@ as such require complete reset of X509 infrastructure if some of them needs to b
Request submission is allowed from:
-{% if "0.0.0.0/0" in session.request_subnets %}
+{% if "0.0.0.0/0" in session.authority.request_subnets %}
anywhere
{% else %}
- {% for subnet in session.request_subnets %}
+ {% for subnet in session.authority.request_subnets %}
{{ subnet }}
{% endfor %}
{% endif %}
Autosign is allowed from:
-{% if "0.0.0.0/0" in session.autosign_subnets %}
+{% if "0.0.0.0/0" in session.authority.autosign_subnets %}
anywhere
{% else %}
@@ -72,12 +72,12 @@ as such require complete reset of X509 infrastructure if some of them needs to b
{% endif %}
Authority administration is allowed from:
-{% if "0.0.0.0/0" in session.admin_subnets %}
+{% if "0.0.0.0/0" in session.authority.admin_subnets %}
anywhere
{% else %}
- {% for subnet in session.admin_subnets %}
+ {% for subnet in session.authority.admin_subnets %}
{{ subnet }}
{% endfor %}
@@ -86,8 +86,8 @@ as such require complete reset of X509 infrastructure if some of them needs to b
Authority administration allowed for:
diff --git a/certidude/templates/certidude.conf b/certidude/templates/certidude.conf
index a19c97b..18ddb9d 100644
--- a/certidude/templates/certidude.conf
+++ b/certidude/templates/certidude.conf
@@ -57,4 +57,4 @@ certificate path = {{ ca_crt }}
requests dir = {{ directory }}/requests/
signed dir = {{ directory }}/signed/
revoked dir = {{ directory }}/revoked/
-outbox = smtp://localhost
+outbox = {{ outbox }}
diff --git a/certidude/templates/mail/certificate-revoked.md b/certidude/templates/mail/certificate-revoked.md
new file mode 100644
index 0000000..4fe58a8
--- /dev/null
+++ b/certidude/templates/mail/certificate-revoked.md
@@ -0,0 +1,6 @@
+Certificate {{certificate.common_name}} ({{certificate.serial_number}}) revoked
+
+This is simply to notify that certificate {{ certificate.common_name }}
+was revoked.
+
+Services making use of this certificates might become unavailable.
diff --git a/certidude/templates/mail/request-stored.md b/certidude/templates/mail/request-stored.md
new file mode 100644
index 0000000..4d8689f
--- /dev/null
+++ b/certidude/templates/mail/request-stored.md
@@ -0,0 +1,5 @@
+Certificate signing request {{request.common_name}} stored
+
+This is simply to notify that certificate signing request for {{ request.common_name }}
+was stored. You may log in with a certificate authority administration account to sign it.
+
diff --git a/certidude/templates/nginx-https-site.conf b/certidude/templates/nginx-https-site.conf
index dc29bc2..1a063a0 100644
--- a/certidude/templates/nginx-https-site.conf
+++ b/certidude/templates/nginx-https-site.conf
@@ -15,6 +15,7 @@ server {
ssl_certificate {{certificate_path}};
ssl_certificate_key {{key_path}};
ssl_client_certificate {{authority_path}};
+ ssl_crl {{revocations_path}};
ssl_verify_client {{verify_client}};
}
diff --git a/certidude/templates/nginx.conf b/certidude/templates/nginx.conf
index cd42758..1c7e1c0 100644
--- a/certidude/templates/nginx.conf
+++ b/certidude/templates/nginx.conf
@@ -7,9 +7,6 @@ events {
}
http {
- {% if not push_server %}
- push_stream_shared_memory_size 32M;
- {% endif %}
include mime.types;
default_type application/octet-stream;
sendfile on;
@@ -21,7 +18,7 @@ http {
}
server {
- server_name {{hostname}};
+ server_name {{hostname}}; # TODO: FQDN, SSL
listen 80 default_server;
listen [::]:80 default_server ipv6only=on;
error_page 500 502 503 504 /50x.html;
diff --git a/certidude/templates/openvpn-client-to-site.ovpn b/certidude/templates/openvpn-client-to-site.ovpn
index 08759c2..0beff8f 100644
--- a/certidude/templates/openvpn-client-to-site.ovpn
+++ b/certidude/templates/openvpn-client-to-site.ovpn
@@ -2,7 +2,7 @@ client
remote {{remote}}
remote-cert-tls server
proto {{proto}}
-dev tap0
+dev tap
nobind
key {{key_path}}
cert {{certificate_path}}
@@ -10,4 +10,5 @@ ca {{authority_path}}
comp-lzo
user nobody
group nogroup
-
+persist-tun
+persist-key
diff --git a/certidude/templates/openvpn-site-to-client.ovpn b/certidude/templates/openvpn-site-to-client.ovpn
index 38c76cb..d5a6b78 100644
--- a/certidude/templates/openvpn-site-to-client.ovpn
+++ b/certidude/templates/openvpn-site-to-client.ovpn
@@ -2,15 +2,18 @@ mode server
tls-server
proto {{proto}}
port {{port}}
-dev tap0
+dev tap
local {{local}}
key {{key_path}}
cert {{certificate_path}}
ca {{authority_path}}
+crl-verify {{revocations_path}}
dh {{dhparam_path}}
comp-lzo
user nobody
group nogroup
+persist-tun
+persist-key
ifconfig-pool-persist /tmp/openvpn-leases.txt
ifconfig {{subnet_first}} {{subnet.netmask}}
server-bridge {{subnet_first}} {{subnet.netmask}} {{subnet_second}} {{subnet_last}}
diff --git a/certidude/templates/uwsgi.ini b/certidude/templates/uwsgi.ini
index 97155a8..25e6737 100644
--- a/certidude/templates/uwsgi.ini
+++ b/certidude/templates/uwsgi.ini
@@ -5,7 +5,7 @@ processes = 1
vacuum = true
uid = {{username}}
gid = {{username}}
-plugins = python34
+plugins = python27
chdir = /tmp
module = certidude.wsgi
callable = app
diff --git a/certidude/user.py b/certidude/user.py
new file mode 100644
index 0000000..ae24d80
--- /dev/null
+++ b/certidude/user.py
@@ -0,0 +1,165 @@
+
+import click
+import grp
+import ldap
+import ldap.sasl
+import os
+import pwd
+from certidude import constants, config
+
+class User(object):
+ def __init__(self, username, mail, given_name="", surname=""):
+ if "@" not in mail:
+ raise ValueError("Invalid e-mail %s" % repr(mail))
+ self.name = username
+ self.mail = mail
+ self.given_name = given_name
+ self.surname = surname
+
+ def __unicode__(self):
+ if self.given_name and self.surname:
+ return u"%s %s <%s>" % (self.given_name, self.surname, self.mail)
+ else:
+ return self.mail
+
+ def __hash__(self):
+ return hash(self.mail)
+
+ def __eq__(self, other):
+ return self.mail == other.mail
+
+ def __repr__(self):
+ return unicode(self).encode("utf-8")
+
+ def is_admin(self):
+ if not hasattr(self, "_is_admin"):
+ self._is_admin = self.objects.is_admin(self)
+ return self._is_admin
+
+ class DoesNotExist(StandardError):
+ pass
+
+
+class PosixUserManager(object):
+ def get(self, username):
+ _, _, _, _, gecos, _, _ = pwd.getpwnam(username)
+ gecos = gecos.decode("utf-8").split(",")
+ full_name = gecos[0]
+ mail = username + "@" + constants.DOMAIN
+ if full_name and " " in full_name:
+ given_name, surname = full_name.split(" ", 1)
+ return User(username, mail, given_name, surname)
+ return User(username, mail)
+
+ def filter_admins(self):
+ _, _, gid, members = grp.getgrnam(config.ADMIN_GROUP)
+ for username in members:
+ yield self.get(username)
+
+ def is_admin(self, username):
+ import grp
+ _, _, gid, members = grp.getgrnam(config.ADMIN_GROUP)
+ return username in members
+
+
+class DirectoryConnection(object):
+ def __enter__(self):
+ # TODO: Implement simple bind
+ if not os.path.exists(config.LDAP_GSSAPI_CRED_CACHE):
+ raise ValueError("Ticket cache not initialized, unable to "
+ "authenticate with computer account against LDAP server!")
+ os.environ["KRB5CCNAME"] = config.LDAP_GSSAPI_CRED_CACHE
+ for server in config.LDAP_SERVERS:
+ self.conn = ldap.initialize(server)
+ self.conn.set_option(ldap.OPT_REFERRALS, 0)
+ click.echo("Connecing to %s using Kerberos ticket cache from %s" %
+ (server, config.LDAP_GSSAPI_CRED_CACHE))
+ self.conn.sasl_interactive_bind_s('', ldap.sasl.gssapi())
+ return self.conn
+ raise ValueError("No LDAP servers specified!")
+
+ def __exit__(self, type, value, traceback):
+ self.conn.unbind_s
+
+
+class ActiveDirectoryUserManager(object):
+ def get(self, username):
+ # TODO: Sanitize username
+ if "@" in username:
+ username, _ = username.split("@", 1)
+ with DirectoryConnection() as conn:
+ ft = config.LDAP_USER_FILTER % username
+ attribs = "cn", "givenName", "sn", "mail", "userPrincipalName"
+ r = conn.search_s(config.LDAP_BASE, ldap.SCOPE_SUBTREE,
+ ft.encode("utf-8"), attribs)
+ for dn, entry in r:
+ if not dn:
+ continue
+ if entry.get("givenname") and entry.get("sn"):
+ given_name, = entry.get("givenName")
+ surname, = entry.get("sn")
+ else:
+ cn, = entry.get("cn")
+ if " " in cn:
+ given_name, surname = cn.split(" ", 1)
+ else:
+ given_name, surname = cn, ""
+
+ mail, = entry.get("mail") or entry.get("userPrincipalName") or (username + "@" + constants.DOMAIN,)
+ return User(username.decode("utf-8"), mail.decode("utf-8"),
+ given_name.decode("utf-8"), surname.decode("utf-8"))
+ raise User.DoesNotExist("User %s does not exist" % username)
+
+ def filter(self, ft):
+ with DirectoryConnection() as conn:
+ attribs = "givenName", "surname", "samaccountname", "cn", "mail", "userPrincipalName"
+ r = conn.search_s(config.LDAP_BASE, ldap.SCOPE_SUBTREE,
+ ft.encode("utf-8"), attribs)
+ for dn,entry in r:
+ if not dn:
+ continue
+ username, = entry.get("sAMAccountName")
+ cn, = entry.get("cn")
+ mail, = entry.get("mail") or entry.get("userPrincipalName") or (username + "@" + constants.DOMAIN,)
+ if entry.get("givenName") and entry.get("sn"):
+ given_name, = entry.get("givenName")
+ surname, = entry.get("sn")
+ else:
+ cn, = entry.get("cn")
+ if " " in cn:
+ given_name, surname = cn.split(" ", 1)
+ else:
+ given_name, surname = cn, ""
+ yield User(username.decode("utf-8"), mail.decode("utf-8"),
+ given_name.decode("utf-8"), surname.decode("utf-8"))
+
+ def filter_admins(self):
+ """
+ Return admin User objects
+ """
+ return self.filter(config.LDAP_ADMIN_FILTER % "*")
+
+ def all(self):
+ """
+ Return all valid User objects
+ """
+ return self.filter(ft=config.LDAP_USER_FILTER % "*")
+
+ def is_admin(self, user):
+ with DirectoryConnection() as conn:
+ ft = config.LDAP_ADMIN_FILTER % user.name
+ r = conn.search_s(config.LDAP_BASE, ldap.SCOPE_SUBTREE,
+ ft.encode("utf-8"), ["cn"])
+ for dn, entry in r:
+ if not dn:
+ continue
+ return True
+ return False
+
+if config.ACCOUNTS_BACKEND == "ldap":
+ User.objects = ActiveDirectoryUserManager()
+elif config.ACCOUNTS_BACKEND == "posix":
+ User.objects = PosixUserManager()
+else:
+ raise NotImplementedError("Authorization backend %s not supported" % config.AUTHORIZATION_BACKEND)
+
diff --git a/requirements.txt b/requirements.txt
index 897f147..b431ba4 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -9,7 +9,6 @@ ipaddress==1.0.16
ipsecparse==0.1.0
Jinja2==2.8
Markdown==2.6.5
-MarkupSafe==0.23
pyasn1==0.1.8
pycrypto==2.6.1
pykerberos==1.1.8