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
This commit is contained in:
Lauri Võsandi 2016-03-27 23:38:14 +03:00
parent 811e6dbb08
commit 925bc0ef9a
25 changed files with 695 additions and 564 deletions

View File

@ -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 <http://www.ejbca.org/features.html>`_ or
`OpenCA <https://pki.openca.org/>`_.
@ -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 <https://nchan.slact.net/>`_.
* 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 <https://github.com/wandenberg/nginx-push-stream-module>`_,
We support `nchan <https://nchan.slact.net/>`_,
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

View File

@ -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"))

View File

@ -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):

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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, <insert awesomeness here>
# TODO: Puppet, OpenLDAP, <insert awesomeness here>
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)

View File

@ -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

View File

@ -1,4 +1,5 @@
import click
import socket
FQDN = socket.getaddrinfo(socket.gethostname(), 0, socket.AF_INET, 0, 0, socket.AI_CANONNAME)[0][3]

View File

@ -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

View File

@ -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,

View File

@ -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()

View File

@ -485,14 +485,14 @@ output += "\n E-mail disabled\n";
;
}
output += "</p>\n\n<p>Authenticated 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 </p>\n";
;
}
else {
output += "\n </p>\n <ul>\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 </ul>\n";
;
}
output += "\n\n\n<p>Request 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 </p>\n";
;
}
else {
output += "\n </p>\n <ul>\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 </ul>\n";
;
}
output += "\n\n<p>Autosign 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 </p>\n";
;
}
@ -575,14 +575,14 @@ output += "\n </ul>\n";
;
}
output += "\n\n<p>Authority 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 </p>\n";
;
}
else {
output += "\n <ul>\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 </ul>\n";
}
output += "\n\n<p>Authority administration allowed for:</p>\n\n<ul>\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 <li>";
output += runtime.suppressValue(t_21, env.opts.autoescape);
output += "</li>\n";
output += "\n <li><a href=\"mailto:";
output += runtime.suppressValue(runtime.memberLookup((t_20),"mail"), env.opts.autoescape);
output += "\">";
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 += "</a></li>\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 <li>";
output += runtime.suppressValue(t_23, env.opts.autoescape);
output += "</li>\n";
;
}
}
}
frame = frame.pop();
output += "\n</ul>\n</section>\n\n";
@ -658,14 +637,14 @@ output += "\n<p>Here you can renew your certificates</p>\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<section id=\"requests\">\n <h1>Pending requests</h1>\n\n <p>
output += runtime.suppressValue(runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "session")),"common_name"), env.opts.autoescape);
output += "</pre>\n\n <ul id=\"pending_requests\">\n ";
frame = frame.push();
var t_27 = runtime.memberLookup((runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "session")),"authority")),"requests");
if(t_27) {var t_26 = t_27.length;
for(var t_25=0; t_25 < t_27.length; t_25++) {
var t_28 = t_27[t_25];
frame.set("request", t_28);
frame.set("loop.index", t_25 + 1);
frame.set("loop.index0", t_25);
frame.set("loop.revindex", t_26 - t_25);
frame.set("loop.revindex0", t_26 - t_25 - 1);
frame.set("loop.first", t_25 === 0);
frame.set("loop.last", t_25 === t_26 - 1);
frame.set("loop.length", t_26);
var t_24 = runtime.memberLookup((runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "session")),"authority")),"requests");
if(t_24) {var t_23 = t_24.length;
for(var t_22=0; t_22 < t_24.length; t_22++) {
var t_25 = t_24[t_22];
frame.set("request", t_25);
frame.set("loop.index", t_22 + 1);
frame.set("loop.index0", t_22);
frame.set("loop.revindex", t_23 - t_22);
frame.set("loop.revindex0", t_23 - t_22 - 1);
frame.set("loop.first", t_22 === 0);
frame.set("loop.last", t_22 === t_23 - 1);
frame.set("loop.length", t_23);
output += "\n ";
env.getTemplate("views/request.html", false, "views/authority.html", null, function(t_31,t_29) {
if(t_31) { cb(t_31); return; }
t_29.render(context.getVariables(), frame, function(t_32,t_30) {
if(t_32) { cb(t_32); return; }
output += t_30
env.getTemplate("views/request.html", false, "views/authority.html", null, function(t_28,t_26) {
if(t_28) { cb(t_28); return; }
t_26.render(context.getVariables(), frame, function(t_29,t_27) {
if(t_29) { cb(t_29); return; }
output += t_27
output += "\n\t ";
})});
}
@ -698,24 +677,24 @@ output += "\n\t ";
frame = frame.pop();
output += "\n <li class=\"notify\">\n <p>No certificate signing requests to sign!</p>\n </li>\n </ul>\n</section>\n\n<section id=\"signed\">\n <h1>Signed certificates</h1>\n <input id=\"search\" type=\"search\" class=\"icon search\">\n <ul id=\"signed_certificates\">\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 </pre>\n -->\n <ul>\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 <li id=\"certificate_";
output += runtime.suppressValue(runtime.memberLookup((t_44),"sha256sum"), env.opts.autoescape);
output += runtime.suppressValue(runtime.memberLookup((t_41),"sha256sum"), env.opts.autoescape);
output += "\">\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 += " <span class=\"monospace\">";
output += runtime.suppressValue(runtime.memberLookup((t_44),"identity"), env.opts.autoescape);
output += runtime.suppressValue(runtime.memberLookup((t_41),"identity"), env.opts.autoescape);
output += "</span>\n </li>\n ";
;
}
}
if (!t_42) {
if (!t_39) {
output += "\n <li>Great job! No certificate signing requests to sign.</li>\n\t ";
}
frame = frame.pop();
@ -1098,7 +1077,7 @@ output += runtime.suppressValue(runtime.memberLookup((runtime.contextOrFrameLook
output += "</div>\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 <div class=\"person\">";
env.getTemplate("img/iconmonstr-user-5.svg", false, "views/signed.html", null, function(t_11,t_9) {

View File

@ -31,13 +31,13 @@ as such require complete reset of X509 infrastructure if some of them needs to b
<p>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
</p>
{% else %}
</p>
<ul>
{% for i in session.user_subnets %}
{% for i in session.authority.user_subnets %}
<li>{{ i }}</li>
{% endfor %}
</ul>
@ -46,20 +46,20 @@ as such require complete reset of X509 infrastructure if some of them needs to b
<p>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
</p>
{% else %}
</p>
<ul>
{% for subnet in session.request_subnets %}
{% for subnet in session.authority.request_subnets %}
<li>{{ subnet }}</li>
{% endfor %}
</ul>
{% endif %}
<p>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
</p>
{% else %}
@ -72,12 +72,12 @@ as such require complete reset of X509 infrastructure if some of them needs to b
{% endif %}
<p>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
</p>
{% else %}
<ul>
{% for subnet in session.admin_subnets %}
{% for subnet in session.authority.admin_subnets %}
<li>{{ subnet }}</li>
{% endfor %}
</ul>
@ -86,8 +86,8 @@ as such require complete reset of X509 infrastructure if some of them needs to b
<p>Authority administration allowed for:</p>
<ul>
{% for handle, full_name in session.admin_users %}
<li>{{ full_name }}</li>
{% for user in session.authority.admin_users %}
<li><a href="mailto:{{ user.mail}}">{{ user.given_name }} {{user.surname }}</a></li>
{% endfor %}
</ul>
</section>

View File

@ -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 }}

View File

@ -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.

View File

@ -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.

View File

@ -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}};
}

View File

@ -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;

View File

@ -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

View File

@ -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}}

View File

@ -5,7 +5,7 @@ processes = 1
vacuum = true
uid = {{username}}
gid = {{username}}
plugins = python34
plugins = python27
chdir = /tmp
module = certidude.wsgi
callable = app

165
certidude/user.py Normal file
View File

@ -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)

View File

@ -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