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 .. figure:: doc/usecase-diagram.png
Certidude is mainly designed for VPN gateway operators to make VPN adoption usage Certidude is mainly designed for VPN gateway operators to make
as simple as possible. 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 For a full-blown CA you might want to take a look at
`EJBCA <http://www.ejbca.org/features.html>`_ or `EJBCA <http://www.ejbca.org/features.html>`_ or
`OpenCA <https://pki.openca.org/>`_. `OpenCA <https://pki.openca.org/>`_.
@ -27,24 +28,29 @@ For a full-blown CA you might want to take a look at
Features Features
-------- --------
Common:
* Standard request, sign, revoke workflow via web interface. * Standard request, sign, revoke workflow via web interface.
* Colored command-line interface, check out ``certidude list``. * Kerberos and basic auth based web interface authentication.
* OpenVPN integration, check out ``certidude setup openvpn server`` and ``certidude setup openvpn client``. * PAM and Active Directory compliant authentication backends: Kerberos single sign-on, LDAP simple bind.
* strongSwan integration, check out ``certidude setup strongswan server`` and ``certidude setup strongswan client``. * 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 * Privilege isolation, separate signer process is spawned per private key isolating
private key use from the the web interface. private key use from the the web interface.
* Certificate numbering obfuscation, certificate serial numbers are intentionally * Certificate serial numbers are intentionally randomized to avoid leaking information about business practices.
randomized to avoid leaking information about business practices. * Server-side events support via `nchan <https://nchan.slact.net/>`_.
* Server-side events support via for example nginx-push-stream-module. * E-mail notifications about pending, signed and revoked certificates.
* Kerberos based web interface authentication.
* File based whitelist authorization, easy to integrate with LDAP as shown below.
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. HTTPS:
* Notifications via e-mail.
* P12 bundle generation for web browsers, seems to work well with Android
* HTTPS server setup with client verification, check out ``certidude setup nginx``
TODO TODO
@ -60,6 +66,7 @@ TODO
* Cronjob for deleting expired certificates * Cronjob for deleting expired certificates
* Signer process logging. * Signer process logging.
Install Install
------- -------
@ -67,15 +74,15 @@ To install Certidude:
.. code:: bash .. 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 \ python-pysqlite2 python-mysql.connector python-ldap \
build-essential libffi-dev libssl-dev libkrb5-dev \ build-essential libffi-dev libssl-dev libkrb5-dev \
ldap-utils krb5-user default-mta \ ldap-utils krb5-user default-mta \
libsasl2-modules-gssapi-mit libsasl2-modules-gssapi-mit
pip3 install certidude pip3 install certidude
Make sure you're running PyOpenSSL 0.15+ and netifaces 0.10.4+ from PyPI, Make sure you're running PyOpenSSL 0.15+ from PyPI,
not the outdated ones provided by APT. not the outdated one provided by APT.
Create a system user for ``certidude``: Create a system user for ``certidude``:
@ -85,10 +92,10 @@ Create a system user for ``certidude``:
mkdir /etc/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. domain name set up properly.
You can check it with: You can check it with:
@ -96,10 +103,10 @@ You can check it with:
hostname -f 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 Certidude can set up certificate authority relatively easily,
CA in /var/lib/certidude/hostname.domain.tld: following will set up certificate authority in /var/lib/certidude/hostname.domain.tld:
.. code:: bash .. code:: bash
@ -176,6 +183,7 @@ Otherwise manually configure ``uwsgi`` application in ``/etc/uwsgi/apps-availabl
env = LANG=C.UTF-8 env = LANG=C.UTF-8
env = LC_ALL=C.UTF-8 env = LC_ALL=C.UTF-8
env = KRB5_KTNAME=/etc/certidude/server.keytab env = KRB5_KTNAME=/etc/certidude/server.keytab
env = KRB5CCNAME=/run/certidude/krb5cc
Also enable the application: Also enable the application:
@ -183,7 +191,7 @@ Also enable the application:
ln -s ../apps-available/certidude.ini /etc/uwsgi/apps-enabled/certidude.ini 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: configure the site in /etc/nginx/sites-available/certidude:
.. code:: .. code::
@ -238,11 +246,9 @@ Also adjust ``/etc/nginx/nginx.conf``:
events { events {
worker_connections 768; worker_connections 768;
# multi_accept on;
} }
http { http {
push_stream_shared_memory_size 32M;
sendfile on; sendfile on;
tcp_nopush on; tcp_nopush on;
tcp_nodelay on; tcp_nodelay on;
@ -254,10 +260,12 @@ Also adjust ``/etc/nginx/nginx.conf``:
error_log /var/log/nginx/error.log; error_log /var/log/nginx/error.log;
gzip on; gzip on;
gzip_disable "msie6"; gzip_disable "msie6";
include /etc/nginx/conf.d/*;
include /etc/nginx/sites-enabled/*; 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:: .. code::
@ -309,8 +317,6 @@ Reset Kerberos configuration in ``/etc/krb5.conf``:
default_realm = EXAMPLE.LAN default_realm = EXAMPLE.LAN
dns_lookup_realm = true dns_lookup_realm = true
dns_lookup_kdc = true dns_lookup_kdc = true
forwardable = true
proxiable = true
Initialize Kerberos credentials: Initialize Kerberos credentials:
@ -332,43 +338,25 @@ Set up Kerberos keytab for the web service:
chown root:certidude /etc/certidude/server.keytab chown root:certidude /etc/certidude/server.keytab
chmod 640 /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 [authentication]
the CA web interface. backends = kerberos
You could either specify user name list
in ``/etc/ssl/openssl.cnf``:
.. 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 User filter here specified which users can log in to Certidude web interface
at all eg. for generating user certificates for HTTPS.
Or alternatively specify file path: Admin filter specifies which users are allowed to sign and revoke certificates.
Adjust admin filter according to your setup.
.. code:: bash Also make sure there is cron.hourly job for creating GSSAPI credential cache -
that's necessary for querying LDAP using Certidude machine's credentials.
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
Automating certificate setup Automating certificate setup
@ -384,7 +372,7 @@ Create ``/etc/NetworkManager/dispatcher.d/certidude`` with following content:
case "$2" in case "$2" in
up) 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 esac
@ -397,8 +385,7 @@ Finally make it executable:
Whenever a wired or wireless connection is brought up, Whenever a wired or wireless connection is brought up,
the dispatcher invokes ``certidude`` in order to generate RSA keys, the dispatcher invokes ``certidude`` in order to generate RSA keys,
submit CSR, fetch signed certificate, submit CSR, fetch signed certificate,
create NetworkManager configuration for the VPN connection and create NetworkManager configuration for the VPN connection.
finally to bring up the VPN tunnel as well.
Development Development

View File

@ -9,6 +9,7 @@ from datetime import datetime
from time import sleep from time import sleep
from certidude import authority, mailer from certidude import authority, mailer
from certidude.auth import login_required, authorize_admin from certidude.auth import login_required, authorize_admin
from certidude.user import User
from certidude.decorators import serialize, event_source, csrf_protection from certidude.decorators import serialize, event_source, csrf_protection
from certidude.wrappers import Request, Certificate from certidude.wrappers import Request, Certificate
from certidude import constants, config from certidude import constants, config
@ -33,34 +34,16 @@ class CertificateAuthorityResource(object):
logger.info("Served CA certificate to %s", req.context.get("remote_addr")) logger.info("Served CA certificate to %s", req.context.get("remote_addr"))
resp.stream = open(config.AUTHORITY_CERTIFICATE_PATH, "rb") resp.stream = open(config.AUTHORITY_CERTIFICATE_PATH, "rb")
resp.append_header("Content-Type", "application/x-x509-ca-cert") 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): class SessionResource(object):
@csrf_protection
@serialize @serialize
@login_required @login_required
@authorize_admin
@event_source @event_source
def on_get(self, req, resp): 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( return dict(
user = dict( user = dict(
@ -72,12 +55,6 @@ class SessionResource(object):
request_submission_allowed = sum( # Dirty hack! request_submission_allowed = sum( # Dirty hack!
[req.context.get("remote_addr") in j [req.context.get("remote_addr") in j
for j in config.REQUEST_SUBNETS]), 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( authority = dict(
outbox = config.OUTBOX, outbox = config.OUTBOX,
certificate = authority.certificate, certificate = authority.certificate,
@ -85,7 +62,12 @@ class SessionResource(object):
requests=authority.list_requests(), requests=authority.list_requests(),
signed=authority.list_signed(), signed=authority.list_signed(),
revoked=authority.list_revoked(), 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( features=dict(
tagging=config.TAGGING_BACKEND, tagging=config.TAGGING_BACKEND,
leases=False, #config.LEASES_BACKEND, leases=False, #config.LEASES_BACKEND,
@ -124,7 +106,7 @@ class BundleResource(object):
common_name = req.context["user"].mail common_name = req.context["user"].mail
logger.info("Signing bundle %s for %s", common_name, req.context.get("user")) 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-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, resp.body, cert = authority.generate_pkcs12_bundle(common_name,
owner=req.context.get("user")) owner=req.context.get("user"))
@ -132,7 +114,6 @@ class BundleResource(object):
import ipaddress import ipaddress
class NormalizeMiddleware(object): class NormalizeMiddleware(object):
@csrf_protection
def process_request(self, req, resp, *args): def process_request(self, req, resp, *args):
assert not req.get_param("unicode") or req.get_param("unicode") == u"", "Unicode sanity check failed" 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")) req.context["remote_addr"] = ipaddress.ip_address(req.env["REMOTE_ADDR"].decode("utf-8"))

View File

@ -6,7 +6,7 @@ import ipaddress
import os import os
from certidude import config, authority, helpers, push, errors from certidude import config, authority, helpers, push, errors
from certidude.auth import login_required, login_optional, authorize_admin 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.wrappers import Request, Certificate
from certidude.firewall import whitelist_subnets, whitelist_content_types from certidude.firewall import whitelist_subnets, whitelist_content_types
@ -19,6 +19,7 @@ class RequestListResource(object):
def on_get(self, req, resp): def on_get(self, req, resp):
return authority.list_requests() return authority.list_requests()
@login_optional @login_optional
@whitelist_subnets(config.REQUEST_SUBNETS) @whitelist_subnets(config.REQUEST_SUBNETS)
@whitelist_content_types("application/pkcs10") @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 # Process automatic signing if the IP address is whitelisted and autosigning was requested
if req.get_param_as_bool("autosign"): if req.get_param_as_bool("autosign"):
for subnet in config.AUTOSIGN_SUBNETS: for subnet in config.AUTOSIGN_SUBNETS:
if subnet.overlaps(req.context.get("remote_addr")): if req.context.get("remote_addr") in subnet:
try: try:
resp.set_header("Content-Type", "application/x-x509-user-cert") resp.set_header("Content-Type", "application/x-x509-user-cert")
resp.body = authority.sign(csr).dump() resp.body = authority.sign(csr).dump()
@ -103,6 +104,8 @@ class RequestDetailResource(object):
csr.common_name, req.context.get("remote_addr")) csr.common_name, req.context.get("remote_addr"))
return csr return csr
@csrf_protection
@login_required @login_required
@authorize_admin @authorize_admin
def on_patch(self, req, resp, cn): 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, logger.info("Signing request %s signed by %s from %s", csr.common_name,
req.context.get("user"), req.context.get("remote_addr")) req.context.get("user"), req.context.get("remote_addr"))
@csrf_protection
@login_required @login_required
@authorize_admin @authorize_admin
def on_delete(self, req, resp, cn): def on_delete(self, req, resp, cn):

View File

@ -3,7 +3,7 @@ import falcon
import logging import logging
from certidude import authority from certidude import authority
from certidude.auth import login_required, authorize_admin from certidude.auth import login_required, authorize_admin
from certidude.decorators import serialize from certidude.decorators import serialize, csrf_protection
logger = logging.getLogger("api") logger = logging.getLogger("api")
@ -24,20 +24,21 @@ class SignedCertificateDetailResource(object):
try: try:
cert = authority.get_signed(cn) cert = authority.get_signed(cn)
except EnvironmentError: 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")) cn, req.context.get("remote_addr"))
resp.body = "No certificate CN=%s found" % cn resp.body = "No certificate CN=%s found" % cn
raise falcon.HTTPNotFound() raise falcon.HTTPNotFound()
else: else:
logger.debug("Served certificate %s to %s", logger.debug(u"Served certificate %s to %s",
cn, req.context.get("remote_addr")) cn, req.context.get("remote_addr"))
return cert return cert
@csrf_protection
@login_required @login_required
@authorize_admin @authorize_admin
def on_delete(self, req, resp, cn): 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")) cn, req.context.get("user"), req.context.get("remote_addr"))
authority.revoke_certificate(cn) authority.revoke_certificate(cn)

View File

@ -3,7 +3,7 @@ import falcon
import logging import logging
from certidude.relational import RelationalMixin from certidude.relational import RelationalMixin
from certidude.auth import login_required, authorize_admin from certidude.auth import login_required, authorize_admin
from certidude.decorators import serialize from certidude.decorators import serialize, csrf_protection
logger = logging.getLogger("api") logger = logging.getLogger("api")
@ -17,6 +17,7 @@ class TagResource(RelationalMixin):
return self.iterfetch("select * from tag") return self.iterfetch("select * from tag")
@csrf_protection
@serialize @serialize
@login_required @login_required
@authorize_admin @authorize_admin
@ -51,6 +52,7 @@ class TagDetailResource(RelationalMixin):
raise falcon.HTTPNotFound() raise falcon.HTTPNotFound()
@csrf_protection
@serialize @serialize
@login_required @login_required
@authorize_admin @authorize_admin
@ -63,6 +65,7 @@ class TagDetailResource(RelationalMixin):
push.publish("tag-updated", identifier) push.publish("tag-updated", identifier)
@csrf_protection
@serialize @serialize
@login_required @login_required
@authorize_admin @authorize_admin

View File

@ -6,14 +6,13 @@ import logging
import os import os
import re import re
import socket import socket
from certidude.user import User
from certidude.firewall import whitelist_subnets from certidude.firewall import whitelist_subnets
from certidude import config, constants from certidude import config, constants
logger = logging.getLogger("api") logger = logging.getLogger("api")
FQDN = socket.getaddrinfo(socket.gethostname(), 0, socket.AF_INET, 0, 0, socket.AI_CANONNAME)[0][3] if "kerberos" in config.AUTHENTICATION_BACKENDS:
if config.AUTHENTICATION_BACKENDS == {"kerberos"}:
ktname = os.getenv("KRB5_KTNAME") ktname = os.getenv("KRB5_KTNAME")
if not ktname: if not ktname:
@ -24,139 +23,13 @@ if config.AUTHENTICATION_BACKENDS == {"kerberos"}:
exit(248) exit(248)
try: try:
principal = kerberos.getServerPrincipalDetails("HTTP", FQDN) principal = kerberos.getServerPrincipalDetails("HTTP", constants.FQDN)
except kerberos.KrbError as exc: 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) exit(249)
else: else:
click.echo("Kerberos enabled, service principal is HTTP/%s" % FQDN) click.echo("Kerberos enabled, service principal is HTTP/%s" % constants.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)
def authenticate(optional=False): def authenticate(optional=False):
@ -167,7 +40,7 @@ def authenticate(optional=False):
if not req.auth: if not req.auth:
resp.append_header("WWW-Authenticate", "Negotiate") 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")) req.env["PATH_INFO"], req.context.get("remote_addr"))
raise falcon.HTTPUnauthorized("Unauthorized", raise falcon.HTTPUnauthorized("Unauthorized",
"No Kerberos ticket offered, are you sure you've logged in with domain user account?") "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:]) token = ''.join(req.auth.split()[1:])
try: try:
result, context = kerberos.authGSSServerInit("HTTP@" + FQDN) result, context = kerberos.authGSSServerInit("HTTP@" + constants.FQDN)
except kerberos.GSSError as ex: except kerberos.GSSError as ex:
# TODO: logger.error # TODO: logger.error
raise falcon.HTTPForbidden("Forbidden", raise falcon.HTTPForbidden("Forbidden",
@ -185,34 +58,46 @@ def authenticate(optional=False):
result = kerberos.authGSSServerStep(context, token) result = kerberos.authGSSServerStep(context, token)
except kerberos.GSSError as ex: except kerberos.GSSError as ex:
kerberos.authGSSServerClean(context) 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", raise falcon.HTTPForbidden("Forbidden",
"Bad credentials: %s (%d)" % (ex.args[0][0], ex.args[0][1])) "Bad credentials: %s (%d)" % (ex.args[0][0], ex.args[0][1]))
except kerberos.KrbError as ex: except kerberos.KrbError as ex:
kerberos.authGSSServerClean(context) 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", raise falcon.HTTPForbidden("Forbidden",
"Bad credentials: %s" % (ex.args[0],)) "Bad credentials: %s" % (ex.args[0],))
user = kerberos.authGSSServerUserName(context) user = kerberos.authGSSServerUserName(context)
req.context["user"] = User(user) req.context["user"] = User.objects.get(user)
req.context["groups"] = set()
try: try:
kerberos.authGSSServerClean(context) kerberos.authGSSServerClean(context)
except kerberos.GSSError as ex: 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])) raise falcon.HTTPUnauthorized("Authentication System Failure %s (%s)" % (ex.args[0][0], ex.args[1][0]))
if result == kerberos.AUTH_GSS_COMPLETE: 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"]) 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: 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") raise falcon.HTTPUnauthorized("Unauthorized", "Tried GSSAPI")
else: 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") raise falcon.HTTPForbidden("Forbidden", "Tried GSSAPI")
@ -238,27 +123,26 @@ def authenticate(optional=False):
basic, token = req.auth.split(" ", 1) basic, token = req.auth.split(" ", 1)
user, passwd = b64decode(token).split(":", 1) user, passwd = b64decode(token).split(":", 1)
if "ldap_conn" not in req.context: for server in config.LDAP_SERVERS:
for server in config.LDAP_SERVERS: click.echo("Connecting to %s as %s" % (server, user))
click.echo("Connecting to %s as %s" % (server, user)) conn = ldap.initialize(server)
conn = ldap.initialize(server) conn.set_option(ldap.OPT_REFERRALS, 0)
conn.set_option(ldap.OPT_REFERRALS, 0) try:
try: conn.simple_bind_s(user if "@" in user else "%s@%s" % (user, constants.DOMAIN), passwd)
conn.simple_bind_s(user if "@" in user else "%s@%s" % (user, constants.DOMAIN), passwd) except ldap.LDAPError, e:
except ldap.LDAPError, e: resp.append_header("WWW-Authenticate", "Basic")
resp.append_header("WWW-Authenticate", "Basic") logger.critical("LDAP bind authentication failed for user %s from %s",
logger.debug("Failed to authenticate with user '%s'", user) repr(user), req.context.get("remote_addr"))
raise falcon.HTTPUnauthorized("Forbidden", raise falcon.HTTPUnauthorized("Forbidden",
"Please authenticate with %s domain account or supply UPN" % constants.DOMAIN) "Please authenticate with %s domain account or supply UPN" % constants.DOMAIN)
req.context["ldap_conn"] = conn req.context["ldap_conn"] = conn
break break
else: else:
raise ValueError("No LDAP servers!") raise ValueError("No LDAP servers!")
req.context["user"] = User(user) req.context["user"] = User.objects.get(user)
req.context["groups"] = set() return func(resource, req, resp, *args, **kwargs)
return account_info(func)(resource, req, resp, *args, **kwargs)
def pam_authenticate(resource, req, resp, *args, **kwargs): def pam_authenticate(resource, req, resp, *args, **kwargs):
@ -282,11 +166,12 @@ def authenticate(optional=False):
import simplepam import simplepam
if not simplepam.authenticate(user, passwd, "sshd"): 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") raise falcon.HTTPUnauthorized("Forbidden", "Invalid password")
req.context["user"] = User(user) req.context["user"] = User.objects.get(user)
req.context["groups"] = set() return func(resource, req, resp, *args, **kwargs)
return account_info(func)(resource, req, resp, *args, **kwargs)
if config.AUTHENTICATION_BACKENDS == {"kerberos"}: if config.AUTHENTICATION_BACKENDS == {"kerberos"}:
return kerberos_authenticate return kerberos_authenticate
@ -302,14 +187,11 @@ def authenticate(optional=False):
def login_required(func): def login_required(func):
return authenticate()(func) return authenticate()(func)
def login_optional(func): def login_optional(func):
return authenticate(optional=True)(func) return authenticate(optional=True)(func)
def authorize_admin(func): def authorize_admin(func):
def whitelist_authorize_admin(resource, req, resp, *args, **kwargs):
def whitelist_authorize(resource, req, resp, *args, **kwargs):
# Check for username whitelist # Check for username whitelist
if not req.context.get("user") or req.context.get("user") not in config.ADMIN_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", 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")) raise falcon.HTTPForbidden("Forbidden", "User %s not whitelisted" % req.context.get("user"))
return func(resource, req, resp, *args, **kwargs) return func(resource, req, resp, *args, **kwargs)
if config.AUTHORIZATION_BACKEND == "whitelist": def authorize_admin(resource, req, resp, *args, **kwargs):
return whitelist_authorize if req.context.get("user").is_admin():
else: req.context["admin_authorized"] = True
return member_of(config.ADMINS_GROUP)(func) 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) cert = func(csr, *args, **kwargs)
assert isinstance(cert, Certificate), "notify wrapped function %s returned %s" % (func, type(cert)) assert isinstance(cert, Certificate), "notify wrapped function %s returned %s" % (func, type(cert))
if cert.email_address: mailer.send(
mailer.send( "certificate-signed.md",
"%s %s <%s>" % (cert.given_name, cert.surname, cert.email_address), to= "%s %s <%s>" % (cert.given_name, cert.surname, cert.email_address) if
"certificate-signed.md", cert.given_name and cert.surname else cert.email_address,
attachments=(cert,), attachments=(cert,),
certificate=cert) certificate=cert)
if config.PUSH_PUBLISH: if config.PUSH_PUBLISH:
url = config.PUSH_PUBLISH % csr.fingerprint() url = config.PUSH_PUBLISH % csr.fingerprint()
@ -85,7 +85,9 @@ def store_request(buf, overwrite=False):
fh.write(buf) fh.write(buf)
os.rename(request_path + ".part", request_path) 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): 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) revoked_filename = os.path.join(config.REVOKED_DIR, "%s.pem" % cert.serial_number)
os.rename(cert.path, revoked_filename) os.rename(cert.path, revoked_filename)
push.publish("certificate-revoked", cert.common_name) push.publish("certificate-revoked", cert.common_name)
mailer.send("certificate-revoked.md", attachments=(cert,), certificate=cert)
def list_requests(directory=config.REQUESTS_DIR): 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: if owner.surname:
csr.get_subject().SN = owner.surname csr.get_subject().SN = owner.surname
csr.add_extensions([ 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) buf = crypto.dump_certificate_request(crypto.FILETYPE_PEM, csr)

View File

@ -15,6 +15,7 @@ import subprocess
import sys import sys
from configparser import ConfigParser from configparser import ConfigParser
from certidude import constants from certidude import constants
from certidude.helpers import certidude_request_certificate
from certidude.common import expand_paths, ip_address, ip_network from certidude.common import expand_paths, ip_address, ip_network
from datetime import datetime from datetime import datetime
from humanize import naturaltime 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.command("spawn", help="Run processes for requesting certificates and configuring services")
@click.option("-f", "--fork", default=False, is_flag=True, help="Fork to background") @click.option("-f", "--fork", default=False, is_flag=True, help="Fork to background")
def certidude_request_spawn(fork): def certidude_request_spawn(fork):
from certidude.helpers import certidude_request_certificate
clients = ConfigParser() clients = ConfigParser()
clients.readfp(open("/etc/certidude/client.conf")) clients.readfp(open("/etc/certidude/client.conf"))
@ -80,7 +79,7 @@ def certidude_request_spawn(fork):
os.makedirs(run_dir) os.makedirs(run_dir)
for server in clients.sections(): for server in clients.sections():
if clients.get(server, "managed") != "true": if clients.get(server, "trigger") != "interface up":
continue continue
pid_path = os.path.join(run_dir, server + ".pid") 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, "request_path"),
clients.get(server, "certificate_path"), clients.get(server, "certificate_path"),
clients.get(server, "authority_path"), clients.get(server, "authority_path"),
clients.get(server, "revocations_path"),
socket.gethostname(), socket.gethostname(),
None, None,
autosign=True, autosign=True,
@ -133,9 +133,47 @@ def certidude_request_spawn(fork):
csum = csummer.hexdigest() csum = csummer.hexdigest()
uuid = csum[:8] + "-" + csum[8:12] + "-" + csum[12:16] + "-" + csum[16:20] + "-" + csum[20:32] 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 # Set up IPsec via NetworkManager
if services.get(endpoint, "service") == "network-manager/strongswan": if services.get(endpoint, "service") == "network-manager/strongswan":
config = ConfigParser() config = ConfigParser()
config.add_section("connection") config.add_section("connection")
config.add_section("vpn") config.add_section("vpn")
@ -146,14 +184,14 @@ def certidude_request_spawn(fork):
config.set("connection", "type", "vpn") config.set("connection", "type", "vpn")
config.set("vpn", "service-type", "org.freedesktop.NetworkManager.strongswan") 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", "encap", "no")
config.set("vpn", "address", services.get(endpoint, "remote"))
config.set("vpn", "virtual", "yes") config.set("vpn", "virtual", "yes")
config.set("vpn", "method", "key") config.set("vpn", "method", "key")
config.set("vpn", "certificate", clients.get(server, "authority_path"))
config.set("vpn", "ipcomp", "no") 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") config.set("ipv4", "method", "auto")
@ -203,9 +241,7 @@ def certidude_request_spawn(fork):
os.system("ipsec start") os.system("ipsec start")
continue continue
# TODO: Puppet, OpenLDAP, <insert awesomeness here>
# TODO: OpenVPN, Puppet, OpenLDAP, intranet HTTPS, <insert awesomeness here>
os.unlink(pid_path) os.unlink(pid_path)
@ -284,9 +320,8 @@ def certidude_signer_spawn(kill, no_interaction):
asyncore.loop() asyncore.loop()
@click.command("client", help="Setup X.509 certificates for application") @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("--common-name", "-cn", default=HOSTNAME, help="Common name, '%s' by default" % HOSTNAME)
@click.option("--org-unit", "-ou", help="Organizational unit") @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", 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("--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("--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("--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): def certidude_setup_client(quiet, **kwargs):
from certidude.helpers import certidude_request_certificate
return certidude_request_certificate(**kwargs) return certidude_request_certificate(**kwargs)
@click.command("server", help="Set up OpenVPN server") @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("--common-name", "-cn", default=FQDN, help="Common name, %s by default" % FQDN)
@click.option("--org-unit", "-ou", help="Organizational unit") @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", 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("--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("--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('--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") @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), type=click.File(mode="w", atomic=True, lazy=True),
help="OpenVPN configuration file") help="OpenVPN configuration file")
@click.option("--directory", "-d", default="/etc/openvpn/keys", help="Directory for keys, /etc/openvpn/keys 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 --directory by default" % HOSTNAME) @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 --directory 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 --directory by default") @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("--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() @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 # TODO: Intelligent way of getting last IP address in the subnet
from certidude.helpers import certidude_request_certificate
subnet_first = None subnet_first = None
subnet_last = None subnet_last = None
subnet_second = 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("use following command to sign on Certidude server instead of web interface:")
click.echo() click.echo()
click.echo(" certidude sign %s" % common_name) click.echo(" certidude sign %s" % common_name)
retval = certidude_request_certificate( retval = certidude_request_certificate(server,
url, key_path, request_path, certificate_path, authority_path, revocations_path,
key_path, common_name, org_unit, email_address,
request_path,
certificate_path,
authority_path,
common_name,
org_unit,
email_address,
key_usage="digitalSignature,keyEncipherment", key_usage="digitalSignature,keyEncipherment",
extended_key_usage="serverAuth", extended_key_usage="serverAuth",
wait=True) 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.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("--common-name", "-cn", default=FQDN, help="Common name, %s by default" % FQDN)
@click.option("--org-unit", "-ou", help="Organizational unit") @click.option("--org-unit", "-ou", help="Organizational unit")
@click.option("--tls-config", @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("--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("--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("--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("--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("--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'])) @click.option("--verify-client", "-vc", type=click.Choice(['optional', 'on', 'off']))
@expand_paths() @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 # 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): if not os.path.exists(certificate_path):
click.echo("As HTTPS server certificate needs specific key usage extensions please") 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()
click.echo(" certidude sign %s" % common_name) click.echo(" certidude sign %s" % common_name)
click.echo() click.echo()
retval = certidude_request_certificate(url, key_path, request_path, retval = certidude_request_certificate(server, key_path, request_path,
certificate_path, authority_path, common_name, org_unit, certificate_path, authority_path, revocations_path, common_name, org_unit,
key_usage="digitalSignature,keyEncipherment", key_usage="digitalSignature,keyEncipherment",
extended_key_usage="serverAuth", extended_key_usage="serverAuth",
dns = constants.FQDN, wait=True, bundle=True) 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.command("client", help="Set up OpenVPN client")
@click.argument("url") @click.argument("server")
@click.argument("remote") @click.argument("remote")
@click.option('--proto', "-t", default="udp", type=click.Choice(['udp', 'tcp']), help="OpenVPN transport protocol, UDP by default") @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("--common-name", "-cn", default=HOSTNAME, help="Common name, %s by default" % HOSTNAME)
@click.option("--org-unit", "-ou", help="Organizational unit") @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", @click.option("--config", "-o",
default="/etc/openvpn/client-to-site.conf", default="/etc/openvpn/client-to-site.conf",
type=click.File(mode="w", atomic=True, lazy=True), type=click.File(mode="w", atomic=True, lazy=True),
help="OpenVPN configuration file") help="OpenVPN configuration file")
@click.option("--directory", "-d", default="/etc/openvpn/keys", help="Directory for keys, /etc/openvpn/keys by default") @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("--key-path", "-key", default=HOSTNAME + ".key", help="Key path, %s.key relative to -d by default" % HOSTNAME)
@click.option("--request-path", "-r", default=HOSTNAME + ".csr", help="Request path, %s.csr relative to --directory 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", "-c", 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("--authority-path", "-a", default="ca.crt", help="Certificate authority certificate path, ca.crt relative to --dir 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() @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): 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):
from certidude.helpers import certidude_request_certificate
retval = certidude_request_certificate( retval = certidude_request_certificate(server,
url, key_path, request_path, certificate_path, authority_path, revocations_path,
key_path, common_name, org_unit, email_address,
request_path,
certificate_path,
authority_path,
common_name,
org_unit,
email_address,
wait=True) wait=True)
if retval: 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.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("--common-name", "-cn", default=FQDN, help="Common name, %s by default" % FQDN)
@click.option("--org-unit", "-ou", help="Organizational unit") @click.option("--org-unit", "-ou", help="Organizational unit")
@click.option("--fqdn", "-f", default=FQDN, help="Fully qualified hostname associated with the certificate") @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("--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("--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("--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() @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: if "." not in common_name:
raise ValueError("Hostname has to be fully qualified!") raise ValueError("Hostname has to be fully qualified!")
if not local: 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("use following command to sign on Certidude server instead of web interface:")
click.echo() click.echo()
click.echo(" certidude sign %s" % common_name) click.echo(" certidude sign %s" % common_name)
from certidude.helpers import certidude_request_certificate click.echo()
retval = certidude_request_certificate(
url, retval = certidude_request_certificate(server,
key_path, key_path, request_path, certificate_path, authority_path, revocations_path,
request_path, common_name, org_unit, email_address,
certificate_path,
authority_path,
common_name,
org_unit,
email_address,
key_usage="digitalSignature,keyEncipherment", key_usage="digitalSignature,keyEncipherment",
extended_key_usage="serverAuth,1.3.6.1.5.5.8.2.2", extended_key_usage="serverAuth,1.3.6.1.5.5.8.2.2",
ip_address=local,
dns=fqdn, dns=fqdn,
wait=True) 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.command("client", help="Set up strongSwan client")
@click.argument("url") @click.argument("server")
@click.argument("remote") @click.argument("remote")
@click.option("--common-name", "-cn", default=HOSTNAME, help="Common name, %s by default" % HOSTNAME) @click.option("--common-name", "-cn", default=HOSTNAME, help="Common name, %s by default" % HOSTNAME)
@click.option("--org-unit", "-ou", help="Organizational unit") @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("--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("--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("--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() @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): 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):
from certidude.helpers import certidude_request_certificate retval = certidude_request_certificate(server,
retval = certidude_request_certificate( key_path, request_path, certificate_path, authority_path,
url, common_name, org_unit, email_address,
key_path,
request_path,
certificate_path,
authority_path,
common_name,
org_unit,
email_address,
wait=True) wait=True)
if retval: 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.command("networkmanager", help="Set up strongSwan client via NetworkManager")
@click.argument("url") @click.argument("server") # Certidude server
@click.argument("remote") @click.argument("remote") # StrongSwan gateway
@click.option("--common-name", "-cn", default=HOSTNAME, help="Common name, %s by default" % HOSTNAME) @click.option("--common-name", "-cn", default=HOSTNAME, help="Common name, %s by default" % HOSTNAME)
@click.option("--org-unit", "-ou", help="Organizational unit") @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", 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("--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("--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("--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() @expand_paths()
def certidude_setup_strongswan_networkmanager(url, email_address, common_name, org_unit, directory, key_path, request_path, certificate_path, authority_path, remote): def certidude_setup_strongswan_networkmanager(server, email_address, common_name, org_unit, directory, key_path, request_path, certificate_path, authority_path, revocations_path, remote):
from certidude.helpers import certidude_request_certificate retval = certidude_request_certificate(server,
retval = certidude_request_certificate( key_path, request_path, certificate_path, authority_path, revocations_path,
url, common_name, org_unit, email_address,
key_path,
request_path,
certificate_path,
authority_path,
common_name,
org_unit,
email_address,
wait=True) wait=True)
if retval: if retval:
return retval return retval
csummer = hashlib.sha1() services = ConfigParser()
csummer.update(remote.encode("ascii")) if os.path.exists("/etc/certidude/services.conf"):
csum = csummer.hexdigest() services.readfp(open("/etc/certidude/services.conf"))
uuid = csum[:8] + "-" + csum[8:12] + "-" + csum[12:16] + "-" + csum[16:20] + "-" + csum[20:32]
config = ConfigParser() endpoint = "IPSec to %s" % remote
config.add_section("connection")
config.add_section("vpn")
config.add_section("ipv4")
config.set("connection", "id", remote) if services.has_section(endpoint):
config.set("connection", "uuid", uuid) click.echo("Section %s already exists in /etc/certidude/services.conf, not reconfiguring" % endpoint)
config.set("connection", "type", "vpn") else:
config.set("connection", "autoconnect", "true") 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 if retval:
os.umask(0o277) return retval
# Write keyfile services = ConfigParser()
with open(os.path.join("/etc/NetworkManager/system-connections", remote), "w") as configfile: if os.path.exists("/etc/certidude/services.conf"):
config.write(configfile) services.readfp(open("/etc/certidude/services.conf"))
# TODO: Avoid race condition here endpoint = "OpenVPN to %s" % remote
sleep(3)
# Tell NetworkManager to bring up the VPN connection
subprocess.call(("nmcli", "c", "up", "uuid", uuid))
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.command("production", help="Set up nginx, uwsgi and cron")
@click.option("--username", default="certidude", help="Service user account, created if necessary, 'certidude' by default") @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("--push-server", default="", help="Streaming nginx push server")
@click.option("--email-address", default="certidude@" + FQDN, help="E-mail address of the CA") @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") @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 # Make sure common_name is valid
if not re.match(r"^[\.\-_a-zA-Z0-9]+$", common_name): 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_strongswan.add_command(certidude_setup_strongswan_networkmanager)
certidude_setup_openvpn.add_command(certidude_setup_openvpn_server) 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_client)
certidude_setup_openvpn.add_command(certidude_setup_openvpn_networkmanager)
certidude_setup.add_command(certidude_setup_authority) certidude_setup.add_command(certidude_setup_authority)
certidude_setup.add_command(certidude_setup_openvpn) certidude_setup.add_command(certidude_setup_openvpn)
certidude_setup.add_command(certidude_setup_strongswan) certidude_setup.add_command(certidude_setup_strongswan)

View File

@ -57,7 +57,6 @@ except configparser.NoOptionError:
PUSH_LONG_POLL = PUSH_SERVER + "/lp/%s" PUSH_LONG_POLL = PUSH_SERVER + "/lp/%s"
PUSH_PUBLISH = PUSH_SERVER + "/pub?id=%s" PUSH_PUBLISH = PUSH_SERVER + "/pub?id=%s"
TAGGING_BACKEND = cp.get("tagging", "backend") TAGGING_BACKEND = cp.get("tagging", "backend")
LOGGING_BACKEND = cp.get("logging", "backend") LOGGING_BACKEND = cp.get("logging", "backend")
LEASES_BACKEND = cp.get("leases", "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]) ADMINS_WHITELIST = set([j for j in cp.get("authorization", "admins whitelist").split(" ") if j])
elif "posix" == AUTHORIZATION_BACKEND: elif "posix" == AUTHORIZATION_BACKEND:
USERS_GROUP = cp.get("authorization", "posix user group") 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: elif "ldap" == AUTHORIZATION_BACKEND:
USERS_GROUP = cp.get("authorization", "ldap user group") LDAP_GSSAPI_CRED_CACHE = cp.get("authorization", "ldap gssapi credential cache")
ADMINS_GROUP = cp.get("authorization", "ldap admin group") 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: else:
raise NotImplementedError("Unknown authorization backend '%s'" % AUTHORIZATION_BACKEND) 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"): for line in open("/etc/ldap/ldap.conf"):
line = line.strip().lower() line = line.strip().lower()
if "#" in line: if "#" in line:
@ -92,6 +89,5 @@ for line in open("/etc/ldap/ldap.conf"):
click.echo("LDAP servers: %s" % " ".join(LDAP_SERVERS)) click.echo("LDAP servers: %s" % " ".join(LDAP_SERVERS))
elif key == "base": elif key == "base":
LDAP_BASE = value 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 import socket
FQDN = socket.getaddrinfo(socket.gethostname(), 0, socket.AF_INET, 0, 0, socket.AI_CANONNAME)[0][3] 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 import types
from datetime import date, time, datetime from datetime import date, time, datetime
from OpenSSL import crypto from OpenSSL import crypto
from certidude.auth import User
from certidude.wrappers import Request, Certificate from certidude.wrappers import Request, Certificate
from urllib.parse import urlparse from urllib.parse import urlparse
@ -76,6 +77,9 @@ class MyEncoder(json.JSONEncoder):
if isinstance(obj, Certificate): if isinstance(obj, Certificate):
return dict([(key, getattr(obj, key)) for key in self.CERTIFICATE_ATTRIBUTES \ return dict([(key, getattr(obj, key)) for key in self.CERTIFICATE_ATTRIBUTES \
if hasattr(obj, key) and getattr(obj, key)]) 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"): if hasattr(obj, "serialize"):
return obj.serialize() return obj.serialize()
return json.JSONEncoder.default(self, obj) return json.JSONEncoder.default(self, obj)
@ -95,16 +99,20 @@ def serialize(func):
resp.set_header("Content-Type", "application/json") resp.set_header("Content-Type", "application/json")
resp.set_header("Content-Disposition", "inline") resp.set_header("Content-Disposition", "inline")
resp.body = json.dumps(r, cls=MyEncoder) resp.body = json.dumps(r, cls=MyEncoder)
elif hasattr(r, "content_type") and req.client_accepts(r.content_type): elif hasattr(r, "content_type") and req.client_accepts(r.content_type):
resp.set_header("Content-Type", r.content_type) resp.set_header("Content-Type", r.content_type)
resp.set_header("Content-Disposition", resp.set_header("Content-Disposition",
("attachment; filename=%s" % r.suggested_filename).encode("ascii")) ("attachment; filename=%s" % r.suggested_filename).encode("ascii"))
resp.body = r.dump() resp.body = r.dump()
else: elif hasattr(r, "content_type"):
logger.debug("Client did not accept application/json or %s, client expected %s" % (r.content_type, req.accept)) logger.debug("Client did not accept application/json or %s, "
"client expected %s", r.content_type, req.accept)
raise falcon.HTTPUnsupportedMediaType( raise falcon.HTTPUnsupportedMediaType(
"Client did not accept application/json or %s" % r.content_type) "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 r
return wrapped return wrapped

View File

@ -2,11 +2,14 @@
import click import click
import os import os
import requests import requests
import subprocess
import tempfile
from certidude import errors from certidude import errors
from certidude.wrappers import Certificate, Request from certidude.wrappers import Certificate, Request
from configparser import ConfigParser
from OpenSSL import crypto 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 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: if wait:
request_params.add("wait=forever") request_params.add("wait=forever")
# Expand ca.example.com to http://ca.example.com/api/ # Expand ca.example.com
if not url.endswith("/"): authority_url = "http://%s/api/certificate/" % server
url += "/api/" request_url = "http://%s/api/request/" % server
if "//" not in url: revoked_url = "http://%s/api/revoked/" % server
url = "http://" + url
authority_url = url + "certificate"
request_url = url + "request"
if request_params: if request_params:
request_url = request_url + "?" + "&".join(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): 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: else:
click.echo("Attempting to fetch CA certificate from %s" % authority_url) click.echo("Attempting to fetch authority certificate from %s" % authority_url)
try: try:
r = requests.get(authority_url, r = requests.get(authority_url,
headers={"Accept": "application/x-x509-ca-cert,application/x-pem-file"}) headers={"Accept": "application/x-x509-ca-cert,application/x-pem-file"})
cert = crypto.load_certificate(crypto.FILETYPE_PEM, r.text) cert = crypto.load_certificate(crypto.FILETYPE_PEM, r.text)
except crypto.Error: except crypto.Error:
raise ValueError("Failed to parse PEM: %s" % r.text) 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) oh.write(r.text)
click.echo("Writing CA certificate to: %s" % authority_path) click.echo("Writing authority certificate to: %s" % authority_path)
os.rename(authority_path + ".part", 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: try:
request = Request(open(request_path)) 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) key.generate_key(crypto.TYPE_RSA, 4096)
# Dump private key # Dump private key
key_partial = tempfile.mktemp(prefix=key_path + ".part")
os.umask(0o077) 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)) fh.write(crypto.dump_privatekey(crypto.FILETYPE_PEM, key))
# Construct CSR # Construct CSR
@ -107,10 +153,38 @@ def certidude_request_certificate(url, key_path, request_path, certificate_path,
fh.write(request.dump()) fh.write(request.dump())
click.echo("Writing private key to: %s" % key_path) 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) click.echo("Writing certificate signing request to: %s" % request_path)
os.rename(request_path + ".part", 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) click.echo("Submitting to %s, waiting for response..." % request_url)
submission = requests.post(request_url, submission = requests.post(request_url,

View File

@ -1,6 +1,8 @@
import click
import os import os
import smtplib import smtplib
from certidude.user import User
from markdown import markdown from markdown import markdown
from jinja2 import Environment, PackageLoader from jinja2 import Environment, PackageLoader
from email.mime.multipart import MIMEMultipart from email.mime.multipart import MIMEMultipart
@ -10,14 +12,18 @@ from urllib.parse import urlparse
env = Environment(loader=PackageLoader("certidude", "templates/mail")) 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 from certidude import authority, config
if not config.OUTBOX: if not config.OUTBOX:
# Mailbox disabled, don't send e-mail # Mailbox disabled, don't send e-mail
return return
if not recipients: recipients = u", ".join([unicode(j) for j in User.objects.filter_admins()])
raise ValueError("No e-mail recipients specified!")
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, netloc, path, params, query, fragment = urlparse(config.OUTBOX)
scheme = scheme.lower() 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"; 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"; output += "\n anywhere\n </p>\n";
; ;
} }
else { else {
output += "\n </p>\n <ul>\n "; output += "\n </p>\n <ul>\n ";
frame = frame.push(); 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; if(t_3) {var t_2 = t_3.length;
for(var t_1=0; t_1 < t_3.length; t_1++) { for(var t_1=0; t_1 < t_3.length; t_1++) {
var t_4 = t_3[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"; 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"; output += "\n anywhere\n </p>\n";
; ;
} }
else { else {
output += "\n </p>\n <ul>\n "; output += "\n </p>\n <ul>\n ";
frame = frame.push(); 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; if(t_7) {var t_6 = t_7.length;
for(var t_5=0; t_5 < t_7.length; t_5++) { for(var t_5=0; t_5 < t_7.length; t_5++) {
var t_8 = t_7[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"; 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"; output += "\n anywhere\n </p>\n";
; ;
} }
@ -575,14 +575,14 @@ output += "\n </ul>\n";
; ;
} }
output += "\n\n<p>Authority administration is allowed from:\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"; output += "\n anywhere\n </p>\n";
; ;
} }
else { else {
output += "\n <ul>\n "; output += "\n <ul>\n ";
frame = frame.push(); 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; if(t_15) {var t_14 = t_15.length;
for(var t_13=0; t_13 < t_15.length; t_13++) { for(var t_13=0; t_13 < t_15.length; t_13++) {
var t_16 = t_15[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"; output += "\n\n<p>Authority administration allowed for:</p>\n\n<ul>\n";
frame = frame.push(); frame = frame.push();
var t_19 = runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "session")),"admin_users"); var t_19 = runtime.memberLookup((runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "session")),"authority")),"admin_users");
if(t_19) {var t_17; if(t_19) {var t_18 = t_19.length;
if(runtime.isArray(t_19)) { for(var t_17=0; t_17 < t_19.length; t_17++) {
var t_18 = t_19.length; var t_20 = t_19[t_17];
for(t_17=0; t_17 < t_19.length; t_17++) { frame.set("user", t_20);
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]);
frame.set("loop.index", t_17 + 1); frame.set("loop.index", t_17 + 1);
frame.set("loop.index0", t_17); frame.set("loop.index0", t_17);
frame.set("loop.revindex", t_18 - 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.first", t_17 === 0);
frame.set("loop.last", t_17 === t_18 - 1); frame.set("loop.last", t_17 === t_18 - 1);
frame.set("loop.length", t_18); frame.set("loop.length", t_18);
output += "\n <li>"; output += "\n <li><a href=\"mailto:";
output += runtime.suppressValue(t_21, env.opts.autoescape); output += runtime.suppressValue(runtime.memberLookup((t_20),"mail"), env.opts.autoescape);
output += "</li>\n"; 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(); frame = frame.pop();
output += "\n</ul>\n</section>\n\n"; 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"; output += "\n\n";
var t_24; var t_21;
t_24 = runtime.memberLookup((runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "session")),"certificate")),"identity"); t_21 = runtime.memberLookup((runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "session")),"certificate")),"identity");
frame.set("s", t_24, true); frame.set("s", t_21, true);
if(frame.topLevel) { if(frame.topLevel) {
context.setVariable("s", t_24); context.setVariable("s", t_21);
} }
if(frame.topLevel) { if(frame.topLevel) {
context.addExport("s", t_24); context.addExport("s", t_21);
} }
output += "\n\n\n"; output += "\n\n\n";
if(runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "session")),"authority")) { 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 += runtime.suppressValue(runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "session")),"common_name"), env.opts.autoescape);
output += "</pre>\n\n <ul id=\"pending_requests\">\n "; output += "</pre>\n\n <ul id=\"pending_requests\">\n ";
frame = frame.push(); frame = frame.push();
var t_27 = runtime.memberLookup((runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "session")),"authority")),"requests"); var t_24 = runtime.memberLookup((runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "session")),"authority")),"requests");
if(t_27) {var t_26 = t_27.length; if(t_24) {var t_23 = t_24.length;
for(var t_25=0; t_25 < t_27.length; t_25++) { for(var t_22=0; t_22 < t_24.length; t_22++) {
var t_28 = t_27[t_25]; var t_25 = t_24[t_22];
frame.set("request", t_28); frame.set("request", t_25);
frame.set("loop.index", t_25 + 1); frame.set("loop.index", t_22 + 1);
frame.set("loop.index0", t_25); frame.set("loop.index0", t_22);
frame.set("loop.revindex", t_26 - t_25); frame.set("loop.revindex", t_23 - t_22);
frame.set("loop.revindex0", t_26 - t_25 - 1); frame.set("loop.revindex0", t_23 - t_22 - 1);
frame.set("loop.first", t_25 === 0); frame.set("loop.first", t_22 === 0);
frame.set("loop.last", t_25 === t_26 - 1); frame.set("loop.last", t_22 === t_23 - 1);
frame.set("loop.length", t_26); frame.set("loop.length", t_23);
output += "\n "; output += "\n ";
env.getTemplate("views/request.html", false, "views/authority.html", null, function(t_31,t_29) { env.getTemplate("views/request.html", false, "views/authority.html", null, function(t_28,t_26) {
if(t_31) { cb(t_31); return; } if(t_28) { cb(t_28); return; }
t_29.render(context.getVariables(), frame, function(t_32,t_30) { t_26.render(context.getVariables(), frame, function(t_29,t_27) {
if(t_32) { cb(t_32); return; } if(t_29) { cb(t_29); return; }
output += t_30 output += t_27
output += "\n\t "; output += "\n\t ";
})}); })});
} }
@ -698,24 +677,24 @@ output += "\n\t ";
frame = frame.pop(); 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 "; 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(); 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"))); 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_35) {var t_34 = t_35.length; if(t_32) {var t_31 = t_32.length;
for(var t_33=0; t_33 < t_35.length; t_33++) { for(var t_30=0; t_30 < t_32.length; t_30++) {
var t_36 = t_35[t_33]; var t_33 = t_32[t_30];
frame.set("certificate", t_36); frame.set("certificate", t_33);
frame.set("loop.index", t_33 + 1); frame.set("loop.index", t_30 + 1);
frame.set("loop.index0", t_33); frame.set("loop.index0", t_30);
frame.set("loop.revindex", t_34 - t_33); frame.set("loop.revindex", t_31 - t_30);
frame.set("loop.revindex0", t_34 - t_33 - 1); frame.set("loop.revindex0", t_31 - t_30 - 1);
frame.set("loop.first", t_33 === 0); frame.set("loop.first", t_30 === 0);
frame.set("loop.last", t_33 === t_34 - 1); frame.set("loop.last", t_30 === t_31 - 1);
frame.set("loop.length", t_34); frame.set("loop.length", t_31);
output += "\n "; output += "\n ";
env.getTemplate("views/signed.html", false, "views/authority.html", null, function(t_39,t_37) { env.getTemplate("views/signed.html", false, "views/authority.html", null, function(t_36,t_34) {
if(t_39) { cb(t_39); return; } if(t_36) { cb(t_36); return; }
t_37.render(context.getVariables(), frame, function(t_40,t_38) { t_34.render(context.getVariables(), frame, function(t_37,t_35) {
if(t_40) { cb(t_40); return; } if(t_37) { cb(t_37); return; }
output += t_38 output += t_35
output += "\n\t "; 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 += runtime.suppressValue(runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "request")),"url"), env.opts.autoescape);
output += "/ocsp/ -serial 0x\n </pre>\n -->\n <ul>\n "; output += "/ocsp/ -serial 0x\n </pre>\n -->\n <ul>\n ";
frame = frame.push(); frame = frame.push();
var t_43 = runtime.memberLookup((runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "session")),"authority")),"revoked"); var t_40 = runtime.memberLookup((runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "session")),"authority")),"revoked");
if(t_43) {var t_42 = t_43.length; if(t_40) {var t_39 = t_40.length;
for(var t_41=0; t_41 < t_43.length; t_41++) { for(var t_38=0; t_38 < t_40.length; t_38++) {
var t_44 = t_43[t_41]; var t_41 = t_40[t_38];
frame.set("j", t_44); frame.set("j", t_41);
frame.set("loop.index", t_41 + 1); frame.set("loop.index", t_38 + 1);
frame.set("loop.index0", t_41); frame.set("loop.index0", t_38);
frame.set("loop.revindex", t_42 - t_41); frame.set("loop.revindex", t_39 - t_38);
frame.set("loop.revindex0", t_42 - t_41 - 1); frame.set("loop.revindex0", t_39 - t_38 - 1);
frame.set("loop.first", t_41 === 0); frame.set("loop.first", t_38 === 0);
frame.set("loop.last", t_41 === t_42 - 1); frame.set("loop.last", t_38 === t_39 - 1);
frame.set("loop.length", t_42); frame.set("loop.length", t_39);
output += "\n <li id=\"certificate_"; 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 += "\">\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 += "\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 += " <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 "; 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 "; output += "\n <li>Great job! No certificate signing requests to sign.</li>\n\t ";
} }
frame = frame.pop(); frame = frame.pop();
@ -1098,7 +1077,7 @@ output += runtime.suppressValue(runtime.memberLookup((runtime.contextOrFrameLook
output += "</div>\n "; 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")) { if(runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "certificate")),"given_name") || runtime.memberLookup((runtime.contextOrFrameLookup(context, frame, "certificate")),"surname")) {
output += "\n <div class=\"person\">"; output += "\n <div class=\"person\">";
env.getTemplate("img/iconmonstr-user-5.svg", false, "views/signed.html", null, function(t_11,t_9) { 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: <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 anywhere
</p> </p>
{% else %} {% else %}
</p> </p>
<ul> <ul>
{% for i in session.user_subnets %} {% for i in session.authority.user_subnets %}
<li>{{ i }}</li> <li>{{ i }}</li>
{% endfor %} {% endfor %}
</ul> </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: <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 anywhere
</p> </p>
{% else %} {% else %}
</p> </p>
<ul> <ul>
{% for subnet in session.request_subnets %} {% for subnet in session.authority.request_subnets %}
<li>{{ subnet }}</li> <li>{{ subnet }}</li>
{% endfor %} {% endfor %}
</ul> </ul>
{% endif %} {% endif %}
<p>Autosign is allowed from: <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 anywhere
</p> </p>
{% else %} {% else %}
@ -72,12 +72,12 @@ as such require complete reset of X509 infrastructure if some of them needs to b
{% endif %} {% endif %}
<p>Authority administration is allowed from: <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 anywhere
</p> </p>
{% else %} {% else %}
<ul> <ul>
{% for subnet in session.admin_subnets %} {% for subnet in session.authority.admin_subnets %}
<li>{{ subnet }}</li> <li>{{ subnet }}</li>
{% endfor %} {% endfor %}
</ul> </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> <p>Authority administration allowed for:</p>
<ul> <ul>
{% for handle, full_name in session.admin_users %} {% for user in session.authority.admin_users %}
<li>{{ full_name }}</li> <li><a href="mailto:{{ user.mail}}">{{ user.given_name }} {{user.surname }}</a></li>
{% endfor %} {% endfor %}
</ul> </ul>
</section> </section>

View File

@ -57,4 +57,4 @@ certificate path = {{ ca_crt }}
requests dir = {{ directory }}/requests/ requests dir = {{ directory }}/requests/
signed dir = {{ directory }}/signed/ signed dir = {{ directory }}/signed/
revoked dir = {{ directory }}/revoked/ 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 {{certificate_path}};
ssl_certificate_key {{key_path}}; ssl_certificate_key {{key_path}};
ssl_client_certificate {{authority_path}}; ssl_client_certificate {{authority_path}};
ssl_crl {{revocations_path}};
ssl_verify_client {{verify_client}}; ssl_verify_client {{verify_client}};
} }

View File

@ -7,9 +7,6 @@ events {
} }
http { http {
{% if not push_server %}
push_stream_shared_memory_size 32M;
{% endif %}
include mime.types; include mime.types;
default_type application/octet-stream; default_type application/octet-stream;
sendfile on; sendfile on;
@ -21,7 +18,7 @@ http {
} }
server { server {
server_name {{hostname}}; server_name {{hostname}}; # TODO: FQDN, SSL
listen 80 default_server; listen 80 default_server;
listen [::]:80 default_server ipv6only=on; listen [::]:80 default_server ipv6only=on;
error_page 500 502 503 504 /50x.html; error_page 500 502 503 504 /50x.html;

View File

@ -2,7 +2,7 @@ client
remote {{remote}} remote {{remote}}
remote-cert-tls server remote-cert-tls server
proto {{proto}} proto {{proto}}
dev tap0 dev tap
nobind nobind
key {{key_path}} key {{key_path}}
cert {{certificate_path}} cert {{certificate_path}}
@ -10,4 +10,5 @@ ca {{authority_path}}
comp-lzo comp-lzo
user nobody user nobody
group nogroup group nogroup
persist-tun
persist-key

View File

@ -2,15 +2,18 @@ mode server
tls-server tls-server
proto {{proto}} proto {{proto}}
port {{port}} port {{port}}
dev tap0 dev tap
local {{local}} local {{local}}
key {{key_path}} key {{key_path}}
cert {{certificate_path}} cert {{certificate_path}}
ca {{authority_path}} ca {{authority_path}}
crl-verify {{revocations_path}}
dh {{dhparam_path}} dh {{dhparam_path}}
comp-lzo comp-lzo
user nobody user nobody
group nogroup group nogroup
persist-tun
persist-key
ifconfig-pool-persist /tmp/openvpn-leases.txt ifconfig-pool-persist /tmp/openvpn-leases.txt
ifconfig {{subnet_first}} {{subnet.netmask}} ifconfig {{subnet_first}} {{subnet.netmask}}
server-bridge {{subnet_first}} {{subnet.netmask}} {{subnet_second}} {{subnet_last}} server-bridge {{subnet_first}} {{subnet.netmask}} {{subnet_second}} {{subnet_last}}

View File

@ -5,7 +5,7 @@ processes = 1
vacuum = true vacuum = true
uid = {{username}} uid = {{username}}
gid = {{username}} gid = {{username}}
plugins = python34 plugins = python27
chdir = /tmp chdir = /tmp
module = certidude.wsgi module = certidude.wsgi
callable = app 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 ipsecparse==0.1.0
Jinja2==2.8 Jinja2==2.8
Markdown==2.6.5 Markdown==2.6.5
MarkupSafe==0.23
pyasn1==0.1.8 pyasn1==0.1.8
pycrypto==2.6.1 pycrypto==2.6.1
pykerberos==1.1.8 pykerberos==1.1.8