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