mirror of
https://github.com/laurivosandi/certidude
synced 2024-12-22 16:25:17 +00:00
Added preliminary Kerberos authentication support
This commit is contained in:
parent
c5d27e8a76
commit
e2f27078d1
80
README.rst
80
README.rst
@ -21,6 +21,7 @@ Features
|
|||||||
* Certificate numbering obfuscation, certificate serial numbers are intentionally
|
* Certificate numbering obfuscation, certificate serial numbers are intentionally
|
||||||
randomized to avoid leaking information about business practices.
|
randomized to avoid leaking information about business practices.
|
||||||
* Server-side events support via for example nginx-push-stream-module.
|
* Server-side events support via for example nginx-push-stream-module.
|
||||||
|
* Kerberos based authentication
|
||||||
|
|
||||||
|
|
||||||
TODO
|
TODO
|
||||||
@ -42,9 +43,12 @@ To install Certidude:
|
|||||||
|
|
||||||
.. code:: bash
|
.. code:: bash
|
||||||
|
|
||||||
apt-get install -y python3 python3-netifaces python3-pip python3-dev cython3 build-essential libffi-dev libssl-dev
|
apt-get install -y python3 python3-pip python3-dev cython3 build-essential libffi-dev libssl-dev
|
||||||
pip3 install certidude
|
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.
|
||||||
|
|
||||||
Create a system user for ``certidude``:
|
Create a system user for ``certidude``:
|
||||||
|
|
||||||
.. code:: bash
|
.. code:: bash
|
||||||
@ -106,7 +110,7 @@ Install uWSGI:
|
|||||||
|
|
||||||
.. code:: bash
|
.. code:: bash
|
||||||
|
|
||||||
apt-get install uwsgi uwsgi-plugin-python3
|
apt-get install nginx uwsgi uwsgi-plugin-python3
|
||||||
|
|
||||||
To set up ``nginx`` and ``uwsgi`` is suggested:
|
To set up ``nginx`` and ``uwsgi`` is suggested:
|
||||||
|
|
||||||
@ -132,8 +136,8 @@ Otherwise manually configure uUWSGI application in ``/etc/uwsgi/apps-available/c
|
|||||||
callable = app
|
callable = app
|
||||||
chmod-socket = 660
|
chmod-socket = 660
|
||||||
chown-socket = certidude:www-data
|
chown-socket = certidude:www-data
|
||||||
env = CERTIDUDE_EVENT_PUBLISH=http://localhost/event/publish/%(channel)s
|
env = PUSH_PUBLISH=http://localhost/event/publish/%(channel)s
|
||||||
env = CERTIDUDE_EVENT_SUBSCRIBE=http://localhost/event/subscribe/%(channel)s
|
env = PUSH_SUBSCRIBE=http://localhost/event/subscribe/%(channel)s
|
||||||
|
|
||||||
Also enable the application:
|
Also enable the application:
|
||||||
|
|
||||||
@ -213,3 +217,71 @@ Restart the services:
|
|||||||
|
|
||||||
service uwsgi restart
|
service uwsgi restart
|
||||||
service nginx restart
|
service nginx restart
|
||||||
|
|
||||||
|
|
||||||
|
Setting up Kerberos authentication
|
||||||
|
----------------------------------
|
||||||
|
|
||||||
|
Following assumes you have already set up Kerberos infrastructure and
|
||||||
|
Certidude is simply one of the servers making use of that infrastructure.
|
||||||
|
|
||||||
|
Install dependencies:
|
||||||
|
|
||||||
|
.. code:: bash
|
||||||
|
|
||||||
|
apt-get install samba-common-bin krb5-user ldap-utils
|
||||||
|
|
||||||
|
Set up Samba client configuration in ``/etc/samba/smb.conf``:
|
||||||
|
|
||||||
|
.. code:: ini
|
||||||
|
|
||||||
|
[global]
|
||||||
|
security = ads
|
||||||
|
netbios name = CERTIDUDE
|
||||||
|
workgroup = WORKGROUP
|
||||||
|
realm = EXAMPLE.LAN
|
||||||
|
kerberos method = system keytab
|
||||||
|
|
||||||
|
Set up Kerberos keytab for the web service:
|
||||||
|
|
||||||
|
.. code:: bash
|
||||||
|
|
||||||
|
KRB5_KTNAME=FILE:/etc/certidude.keytab net ads keytab add HTTP -U Administrator
|
||||||
|
|
||||||
|
|
||||||
|
Setting up authorization
|
||||||
|
------------------------
|
||||||
|
|
||||||
|
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``:
|
||||||
|
|
||||||
|
.. code:: bash
|
||||||
|
|
||||||
|
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.id.stipit.com -s sub -x -LLL \
|
||||||
|
-D 'cn=certidude,cn=Users,dc=id,dc=stipit,dc=com' \
|
||||||
|
-w 'certidudepass' \
|
||||||
|
-b 'ou=sso,dc=id,dc=stipit,dc=com' \
|
||||||
|
'(objectClass=user)' 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
|
||||||
|
@ -7,6 +7,7 @@ import urllib.request
|
|||||||
import click
|
import click
|
||||||
from time import sleep
|
from time import sleep
|
||||||
from certidude.wrappers import Request, Certificate
|
from certidude.wrappers import Request, Certificate
|
||||||
|
from certidude.auth import login_required
|
||||||
from certidude.mailer import Mailer
|
from certidude.mailer import Mailer
|
||||||
from pyasn1.codec.der import decoder
|
from pyasn1.codec.der import decoder
|
||||||
from datetime import datetime, date
|
from datetime import datetime, date
|
||||||
@ -20,9 +21,20 @@ RE_HOSTNAME = "^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)*([A-Za-z0
|
|||||||
def omit(**kwargs):
|
def omit(**kwargs):
|
||||||
return dict([(key,value) for (key, value) in kwargs.items() if value])
|
return dict([(key,value) for (key, value) in kwargs.items() if value])
|
||||||
|
|
||||||
|
def authorize_admin(func):
|
||||||
|
def wrapped(self, req, resp, *args, **kwargs):
|
||||||
|
kerberos_username, kerberos_realm = kwargs.get("user")
|
||||||
|
if kerberos_username not in kwargs.get("ca").admin_users:
|
||||||
|
raise falcon.HTTPForbidden("User %s not whitelisted" % kerberos_username)
|
||||||
|
kwargs["user"] = kerberos_username
|
||||||
|
return func(self, req, resp, *args, **kwargs)
|
||||||
|
return wrapped
|
||||||
|
|
||||||
|
|
||||||
def pop_certificate_authority(func):
|
def pop_certificate_authority(func):
|
||||||
def wrapped(self, req, resp, *args, **kwargs):
|
def wrapped(self, req, resp, *args, **kwargs):
|
||||||
kwargs["ca"] = self.config.instantiate_authority(kwargs["ca"])
|
kwargs["ca"] = self.config.instantiate_authority(kwargs["ca"])
|
||||||
|
print(func)
|
||||||
return func(self, req, resp, *args, **kwargs)
|
return func(self, req, resp, *args, **kwargs)
|
||||||
return wrapped
|
return wrapped
|
||||||
|
|
||||||
@ -72,9 +84,11 @@ def serialize(func):
|
|||||||
def templatize(path):
|
def templatize(path):
|
||||||
template = env.get_template(path)
|
template = env.get_template(path)
|
||||||
def wrapper(func):
|
def wrapper(func):
|
||||||
def wrapped(instance, req, resp, **kwargs):
|
def wrapped(instance, req, resp, *args, **kwargs):
|
||||||
assert not req.get_param("unicode") or req.get_param("unicode") == u"✓", "Unicode sanity check failed"
|
assert not req.get_param("unicode") or req.get_param("unicode") == u"✓", "Unicode sanity check failed"
|
||||||
r = func(instance, req, resp, **kwargs)
|
print("templatize would call", func, "with", args, kwargs)
|
||||||
|
r = func(instance, req, resp, *args, **kwargs)
|
||||||
|
r.pop("self")
|
||||||
if not resp.body:
|
if not resp.body:
|
||||||
if req.get_header("Accept") == "application/json":
|
if req.get_header("Accept") == "application/json":
|
||||||
resp.set_header("Cache-Control", "no-cache, no-store, must-revalidate");
|
resp.set_header("Cache-Control", "no-cache, no-store, must-revalidate");
|
||||||
@ -114,9 +128,11 @@ class SignedCertificateDetailResource(CertificateAuthorityBase):
|
|||||||
resp.stream = open(path, "rb")
|
resp.stream = open(path, "rb")
|
||||||
resp.append_header("Content-Disposition", "attachment; filename=%s.crt" % cn)
|
resp.append_header("Content-Disposition", "attachment; filename=%s.crt" % cn)
|
||||||
|
|
||||||
|
@login_required
|
||||||
@pop_certificate_authority
|
@pop_certificate_authority
|
||||||
|
@authorize_admin
|
||||||
@validate_common_name
|
@validate_common_name
|
||||||
def on_delete(self, req, resp, ca, cn):
|
def on_delete(self, req, resp, ca, cn, user):
|
||||||
ca.revoke(cn)
|
ca.revoke(cn)
|
||||||
|
|
||||||
class SignedCertificateListResource(CertificateAuthorityBase):
|
class SignedCertificateListResource(CertificateAuthorityBase):
|
||||||
@ -152,9 +168,11 @@ class RequestDetailResource(CertificateAuthorityBase):
|
|||||||
resp.append_header("Content-Type", "application/x-x509-user-cert")
|
resp.append_header("Content-Type", "application/x-x509-user-cert")
|
||||||
resp.append_header("Content-Disposition", "attachment; filename=%s.csr" % cn)
|
resp.append_header("Content-Disposition", "attachment; filename=%s.csr" % cn)
|
||||||
|
|
||||||
|
@login_required
|
||||||
@pop_certificate_authority
|
@pop_certificate_authority
|
||||||
|
@authorize_admin
|
||||||
@validate_common_name
|
@validate_common_name
|
||||||
def on_patch(self, req, resp, ca, cn):
|
def on_patch(self, req, resp, ca, cn, user):
|
||||||
"""
|
"""
|
||||||
Sign a certificate signing request
|
Sign a certificate signing request
|
||||||
"""
|
"""
|
||||||
@ -165,8 +183,10 @@ class RequestDetailResource(CertificateAuthorityBase):
|
|||||||
resp.status = falcon.HTTP_201
|
resp.status = falcon.HTTP_201
|
||||||
resp.location = os.path.join(req.relative_uri, "..", "..", "signed", cn)
|
resp.location = os.path.join(req.relative_uri, "..", "..", "signed", cn)
|
||||||
|
|
||||||
|
@login_required
|
||||||
@pop_certificate_authority
|
@pop_certificate_authority
|
||||||
def on_delete(self, req, resp, ca, cn):
|
@authorize_admin
|
||||||
|
def on_delete(self, req, resp, ca, cn, user):
|
||||||
ca.delete_request(cn)
|
ca.delete_request(cn)
|
||||||
|
|
||||||
class RequestListResource(CertificateAuthorityBase):
|
class RequestListResource(CertificateAuthorityBase):
|
||||||
@ -195,8 +215,8 @@ class RequestListResource(CertificateAuthorityBase):
|
|||||||
remote_addr = ipaddress.ip_address(req.env["REMOTE_ADDR"])
|
remote_addr = ipaddress.ip_address(req.env["REMOTE_ADDR"])
|
||||||
|
|
||||||
# Check for CSR submission whitelist
|
# Check for CSR submission whitelist
|
||||||
if ca.request_whitelist:
|
if ca.request_subnets:
|
||||||
for subnet in ca.request_whitelist:
|
for subnet in ca.request_subnets:
|
||||||
if subnet.overlaps(remote_addr):
|
if subnet.overlaps(remote_addr):
|
||||||
break
|
break
|
||||||
else:
|
else:
|
||||||
@ -225,11 +245,11 @@ class RequestListResource(CertificateAuthorityBase):
|
|||||||
|
|
||||||
# Process automatic signing if the IP address is whitelisted and autosigning was requested
|
# Process automatic signing if the IP address is whitelisted and autosigning was requested
|
||||||
if req.get_param("autosign").lower() in ("yes", "1", "true"):
|
if req.get_param("autosign").lower() in ("yes", "1", "true"):
|
||||||
for subnet in ca.autosign_whitelist:
|
for subnet in ca.autosign_subnets:
|
||||||
if subnet.overlaps(remote_addr):
|
if subnet.overlaps(remote_addr):
|
||||||
try:
|
try:
|
||||||
resp.append_header("Content-Type", "application/x-x509-user-cert")
|
resp.append_header("Content-Type", "application/x-x509-user-cert")
|
||||||
resp.body = ca.sign(req).dump()
|
resp.body = ca.sign(csr).dump()
|
||||||
return
|
return
|
||||||
except FileExistsError: # Certificate already exists, try to save the request
|
except FileExistsError: # Certificate already exists, try to save the request
|
||||||
pass
|
pass
|
||||||
@ -245,7 +265,7 @@ class RequestListResource(CertificateAuthorityBase):
|
|||||||
|
|
||||||
# Wait the certificate to be signed if waiting is requested
|
# Wait the certificate to be signed if waiting is requested
|
||||||
if req.get_param("wait"):
|
if req.get_param("wait"):
|
||||||
url_template = os.getenv("CERTIDUDE_EVENT_SUBSCRIBE")
|
url_template = os.getenv("PUSH_SUBSCRIBE")
|
||||||
if url_template:
|
if url_template:
|
||||||
# Redirect to nginx pub/sub
|
# Redirect to nginx pub/sub
|
||||||
url = url_template % dict(channel=request.fingerprint())
|
url = url_template % dict(channel=request.fingerprint())
|
||||||
@ -286,15 +306,15 @@ class CertificateAuthorityResource(CertificateAuthorityBase):
|
|||||||
resp.append_header("Content-Disposition", "attachment; filename=%s.crt" % ca.slug)
|
resp.append_header("Content-Disposition", "attachment; filename=%s.crt" % ca.slug)
|
||||||
|
|
||||||
class IndexResource(CertificateAuthorityBase):
|
class IndexResource(CertificateAuthorityBase):
|
||||||
@templatize("index.html")
|
@login_required
|
||||||
@pop_certificate_authority
|
@pop_certificate_authority
|
||||||
def on_get(self, req, resp, ca):
|
@templatize("index.html")
|
||||||
return {
|
def on_get(self, req, resp, ca, user):
|
||||||
"authority": ca }
|
return locals()
|
||||||
|
|
||||||
class ApplicationConfigurationResource(CertificateAuthorityBase):
|
class ApplicationConfigurationResource(CertificateAuthorityBase):
|
||||||
@validate_common_name
|
|
||||||
@pop_certificate_authority
|
@pop_certificate_authority
|
||||||
|
@validate_common_name
|
||||||
def on_get(self, req, resp, ca, cn):
|
def on_get(self, req, resp, ca, cn):
|
||||||
ctx = dict(
|
ctx = dict(
|
||||||
cn = cn,
|
cn = cn,
|
||||||
@ -304,9 +324,11 @@ class ApplicationConfigurationResource(CertificateAuthorityBase):
|
|||||||
resp.append_header("Content-Disposition", "attachment; filename=%s.ovpn" % cn)
|
resp.append_header("Content-Disposition", "attachment; filename=%s.ovpn" % cn)
|
||||||
resp.body = Template(open("/etc/openvpn/%s.template" % ca.slug).read()).render(ctx)
|
resp.body = Template(open("/etc/openvpn/%s.template" % ca.slug).read()).render(ctx)
|
||||||
|
|
||||||
@validate_common_name
|
@login_required
|
||||||
@pop_certificate_authority
|
@pop_certificate_authority
|
||||||
def on_put(self, req, resp, ca, cn=None):
|
@authorize_admin
|
||||||
|
@validate_common_name
|
||||||
|
def on_put(self, req, resp, user, ca, cn=None):
|
||||||
pkey_buf, req_buf, cert_buf = ca.create_bundle(cn)
|
pkey_buf, req_buf, cert_buf = ca.create_bundle(cn)
|
||||||
|
|
||||||
ctx = dict(
|
ctx = dict(
|
||||||
|
60
certidude/auth.py
Normal file
60
certidude/auth.py
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
|
||||||
|
import click
|
||||||
|
import falcon
|
||||||
|
import kerberos
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import socket
|
||||||
|
|
||||||
|
# Vanilla Kerberos provides only username.
|
||||||
|
# AD also embeds PAC (Privilege Attribute Certificate), which
|
||||||
|
# is supposed to be sent via HTTP headers and it contains
|
||||||
|
# the groups user is part of.
|
||||||
|
# Even then we would have to manually look up the e-mail
|
||||||
|
# address eg via LDAP, hence to keep things simple
|
||||||
|
# we simply use Kerberos to authenticate.
|
||||||
|
|
||||||
|
FQDN = socket.getaddrinfo(socket.gethostname(), 0, flags=socket.AI_CANONNAME)[0][3]
|
||||||
|
|
||||||
|
if not os.getenv("KRB5_KTNAME"):
|
||||||
|
click.echo("Kerberos keytab not specified, set environment variable 'KRB5_KTNAME'", err=True)
|
||||||
|
exit(250)
|
||||||
|
|
||||||
|
try:
|
||||||
|
principal = kerberos.getServerPrincipalDetails("HTTP", FQDN)
|
||||||
|
except kerberos.KrbError as exc:
|
||||||
|
click.echo("Failed to initialize Kerberos, reason: %s" % exc, err=True)
|
||||||
|
exit(249)
|
||||||
|
else:
|
||||||
|
click.echo("Kerberos enabled, service principal is HTTP/%s" % FQDN)
|
||||||
|
|
||||||
|
def login_required(func):
|
||||||
|
def wrapped(resource, req, resp, *args, **kwargs):
|
||||||
|
authorization = req.get_header("Authorization")
|
||||||
|
|
||||||
|
if not authorization:
|
||||||
|
resp.append_header("WWW-Authenticate", "Negotiate")
|
||||||
|
raise falcon.HTTPUnauthorized("Unauthorized", "No Kerberos ticket offered?")
|
||||||
|
|
||||||
|
token = ''.join(authorization.split()[1:])
|
||||||
|
|
||||||
|
rc, context = kerberos.authGSSServerInit("HTTP@" + FQDN)
|
||||||
|
if rc != kerberos.AUTH_GSS_COMPLETE:
|
||||||
|
raise falcon.HTTPForbidden("Forbidden", "Kerberos ticket expired?")
|
||||||
|
|
||||||
|
rc = kerberos.authGSSServerStep(context, token)
|
||||||
|
kerberos_user = kerberos.authGSSServerUserName(context).split("@")
|
||||||
|
|
||||||
|
# References lost beyond this point! Results in
|
||||||
|
# ValueError: PyCapsule_SetPointer called with null pointer
|
||||||
|
kerberos.authGSSServerClean(context)
|
||||||
|
|
||||||
|
if rc == kerberos.AUTH_GSS_COMPLETE:
|
||||||
|
kwargs["user"] = kerberos_user
|
||||||
|
return func(resource, req, resp, *args, **kwargs)
|
||||||
|
elif rc == kerberos.AUTH_GSS_CONTINUE:
|
||||||
|
raise falcon.HTTPUnauthorized("Unauthorized", "Tried GSSAPI")
|
||||||
|
else:
|
||||||
|
raise falcon.HTTPForbidden("Forbidden", "Tried GSSAPI")
|
||||||
|
|
||||||
|
return wrapped
|
@ -402,6 +402,7 @@ def certidude_setup_strongswan_client(url, config, secrets, email_address, commo
|
|||||||
@click.option("--username", default="certidude", help="Service user account, created if necessary, 'certidude' by default")
|
@click.option("--username", default="certidude", help="Service user account, created if necessary, 'certidude' by default")
|
||||||
@click.option("--hostname", default=HOSTNAME, help="nginx hostname, '%s' by default" % HOSTNAME)
|
@click.option("--hostname", default=HOSTNAME, help="nginx hostname, '%s' by default" % HOSTNAME)
|
||||||
@click.option("--static-path", default=os.path.join(os.path.dirname(__file__), "static"), help="Static files")
|
@click.option("--static-path", default=os.path.join(os.path.dirname(__file__), "static"), help="Static files")
|
||||||
|
@click.option("--kerberos-keytab", default="/etc/certidude.keytab", help="Specify Kerberos keytab")
|
||||||
@click.option("--nginx-config", "-n",
|
@click.option("--nginx-config", "-n",
|
||||||
default="/etc/nginx/nginx.conf",
|
default="/etc/nginx/nginx.conf",
|
||||||
type=click.File(mode="w", atomic=True, lazy=True),
|
type=click.File(mode="w", atomic=True, lazy=True),
|
||||||
@ -411,7 +412,7 @@ def certidude_setup_strongswan_client(url, config, secrets, email_address, commo
|
|||||||
type=click.File(mode="w", atomic=True, lazy=True),
|
type=click.File(mode="w", atomic=True, lazy=True),
|
||||||
help="uwsgi configuration, /etc/uwsgi/ by default")
|
help="uwsgi configuration, /etc/uwsgi/ by default")
|
||||||
@click.option("--push-server", help="Push server URL, in case of different nginx instance")
|
@click.option("--push-server", help="Push server URL, in case of different nginx instance")
|
||||||
def certidude_setup_production(username, hostname, push_server, nginx_config, uwsgi_config, static_path):
|
def certidude_setup_production(username, hostname, push_server, nginx_config, uwsgi_config, static_path, kerberos_keytab):
|
||||||
try:
|
try:
|
||||||
pwd.getpwnam(username)
|
pwd.getpwnam(username)
|
||||||
click.echo("Username '%s' already exists, excellent!" % username)
|
click.echo("Username '%s' already exists, excellent!" % username)
|
||||||
@ -419,8 +420,13 @@ def certidude_setup_production(username, hostname, push_server, nginx_config, uw
|
|||||||
cmd = "adduser", "--system", "--no-create-home", "--group", username
|
cmd = "adduser", "--system", "--no-create-home", "--group", username
|
||||||
subprocess.check_call(cmd)
|
subprocess.check_call(cmd)
|
||||||
|
|
||||||
# cmd = "gpasswd", "-a", username, "www-data"
|
if subprocess.call("net ads testjoin", shell=True):
|
||||||
# subprocess.check_call(cmd)
|
click.echo("Domain membership check failed, 'net ads testjoin' returned non-zero value", stderr=True)
|
||||||
|
exit(255)
|
||||||
|
|
||||||
|
if not os.path.exists(kerberos_keytab):
|
||||||
|
subprocess.call("KRB5_KTNAME=FILE:" + kerberos_keytab + " net ads keytab add HTTP -P")
|
||||||
|
click.echo("Created Kerberos keytab in '%s'" % kerberos_keytab)
|
||||||
|
|
||||||
if not static_path.endswith("/"):
|
if not static_path.endswith("/"):
|
||||||
static_path += "/"
|
static_path += "/"
|
||||||
|
@ -15,12 +15,16 @@
|
|||||||
|
|
||||||
<h1>Submit signing request</h1>
|
<h1>Submit signing request</h1>
|
||||||
|
|
||||||
<p>Request submission is allowed from: {% for i in authority.request_whitelist %}{{ i }} {% endfor %}</p>
|
<p>Hi, {{user}}</p>
|
||||||
<p>Autosign is allowed from: {% for i in authority.autosign_whitelist %}{{ i }} {% endfor %}</p>
|
|
||||||
|
<p>Request submission is allowed from: {% if ca.request_subnets %}{% for i in ca.request_subnets %}{{ i }} {% endfor %}{% else %}anywhere{% endif %}</p>
|
||||||
|
<p>Autosign is allowed from: {% if ca.autosign_subnets %}{% for i in ca.autosign_subnets %}{{ i }} {% endfor %}{% else %}nowhere{% endif %}</p>
|
||||||
|
<p>Authority administration is allowed from: {% if ca.admin_subnets %}{% for i in ca.admin_subnets %}{{ i }} {% endfor %}{% else %}anywhere{% endif %}
|
||||||
|
<p>Authority administration allowed for: {% for i in ca.admin_users %}{{ i }} {% endfor %}</p>
|
||||||
|
|
||||||
<h2>IPsec gateway on OpenWrt</h2>
|
<h2>IPsec gateway on OpenWrt</h2>
|
||||||
|
|
||||||
{% set s = authority.certificate.subject %}
|
{% set s = ca.certificate.subject %}
|
||||||
|
|
||||||
<pre>
|
<pre>
|
||||||
opkg update
|
opkg update
|
||||||
@ -70,15 +74,15 @@ certidude setup openvpn client {{request.url}}
|
|||||||
<h1>Pending requests</h1>
|
<h1>Pending requests</h1>
|
||||||
|
|
||||||
<ul>
|
<ul>
|
||||||
{% for j in authority.get_requests() %}
|
{% for j in ca.get_requests() %}
|
||||||
<li>
|
<li>
|
||||||
<a class="button" href="/api/{{authority.slug}}/request/{{j.common_name}}/">Fetch</a>
|
<a class="button" href="/api/{{ca.slug}}/request/{{j.common_name}}/">Fetch</a>
|
||||||
{% if j.signable %}
|
{% if j.signable %}
|
||||||
<button onClick="javascript:$.ajax({url:'/api/{{authority.slug}}/request/{{j.common_name}}/',type:'patch'});">Sign</button>
|
<button onClick="javascript:$.ajax({url:'/api/{{ca.slug}}/request/{{j.common_name}}/',type:'patch'});">Sign</button>
|
||||||
{% else %}
|
{% else %}
|
||||||
<button title="Please use certidude command-line utility to sign unusual requests" disabled>Sign</button>
|
<button title="Please use certidude command-line utility to sign unusual requests" disabled>Sign</button>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<button onClick="javascript:$.ajax({url:'/api/{{authority.slug}}/request/{{j.common_name}}/',type:'delete'});">Delete</button>
|
<button onClick="javascript:$.ajax({url:'/api/{{ca.slug}}/request/{{j.common_name}}/',type:'delete'});">Delete</button>
|
||||||
|
|
||||||
|
|
||||||
<div class="monospace">
|
<div class="monospace">
|
||||||
@ -124,10 +128,10 @@ curl -f {{request.url}}/signed/$CN > $CN.crt
|
|||||||
</pre>
|
</pre>
|
||||||
|
|
||||||
<ul>
|
<ul>
|
||||||
{% for j in authority.get_signed() | sort | reverse %}
|
{% for j in ca.get_signed() | sort | reverse %}
|
||||||
<li>
|
<li>
|
||||||
<a class="button" href="/api/{{authority.slug}}/signed/{{j.subject.CN}}/">Fetch</a>
|
<a class="button" href="/api/{{ca.slug}}/signed/{{j.subject.CN}}/">Fetch</a>
|
||||||
<button onClick="javascript:$.ajax({url:'/api/{{authority.slug}}/signed/{{j.subject.CN}}/',type:'delete'});">Revoke</button>
|
<button onClick="javascript:$.ajax({url:'/api/{{ca.slug}}/signed/{{j.subject.CN}}/',type:'delete'});">Revoke</button>
|
||||||
|
|
||||||
<div class="monospace">
|
<div class="monospace">
|
||||||
{% include 'iconmonstr-certificate-15-icon.svg' %}
|
{% include 'iconmonstr-certificate-15-icon.svg' %}
|
||||||
@ -172,7 +176,7 @@ openssl ocsp -issuer ca.pem -CAfile ca.pem -url {{request.url}}/ocsp/ -serial 0x
|
|||||||
</pre>
|
</pre>
|
||||||
-->
|
-->
|
||||||
<ul>
|
<ul>
|
||||||
{% for j in authority.get_revoked() %}
|
{% for j in ca.get_revoked() %}
|
||||||
<li>
|
<li>
|
||||||
{{j.changed}}
|
{{j.changed}}
|
||||||
{{j.serial_number}} <span class="monospace">{{j.distinguished_name}}</span>
|
{{j.serial_number}} <span class="monospace">{{j.distinguished_name}}</span>
|
||||||
|
@ -17,8 +17,12 @@ emailAddress = {{email_address}}
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
x509_extensions = {{slug}}_cert
|
x509_extensions = {{slug}}_cert
|
||||||
policy = poliy_{{slug}}
|
policy = poliy_{{slug}}
|
||||||
request_whitelist =
|
|
||||||
autosign_whitelist = 127.0.0.0/8
|
# Certidude specific stuff, TODO: move to separate section?
|
||||||
|
request_subnets = 10.0.0.0/8 192.168.0.0/16 172.168.0.0/16
|
||||||
|
autosign_subnets = 127.0.0.0/8
|
||||||
|
admin_subnets = 127.0.0.0/8
|
||||||
|
admin_users =
|
||||||
inbox = {{inbox}}
|
inbox = {{inbox}}
|
||||||
outbox = {{outbox}}
|
outbox = {{outbox}}
|
||||||
|
|
||||||
|
@ -15,11 +15,11 @@ conn %default
|
|||||||
|
|
||||||
conn home
|
conn home
|
||||||
auto={{auto}}
|
auto={{auto}}
|
||||||
type=tunnel
|
|
||||||
left=%defaultroute # Use IP of default route for listening
|
left=%defaultroute # Use IP of default route for listening
|
||||||
|
leftsourceip=%config # Accept server suggested virtual IP as inner address for tunnel
|
||||||
leftcert={{certificate_path}} # Client certificate
|
leftcert={{certificate_path}} # Client certificate
|
||||||
leftid={{common_name}} # Client certificate identifier
|
leftid={{common_name}} # Client certificate identifier
|
||||||
leftfirewall=yes
|
leftfirewall=yes # Local machine may be behind NAT
|
||||||
right={{remote}} # Gateway IP address
|
right={{remote}} # Gateway IP address
|
||||||
rightid=%any # Allow any common name
|
rightid=%any # Allow any common name
|
||||||
rightsubnet=0.0.0.0/0 # Accept all subnets suggested by server
|
rightsubnet=0.0.0.0/0 # Accept all subnets suggested by server
|
||||||
|
@ -15,6 +15,7 @@ conn %default
|
|||||||
conn rw
|
conn rw
|
||||||
auto=add
|
auto=add
|
||||||
right=%any # Allow connecting from any IP address
|
right=%any # Allow connecting from any IP address
|
||||||
|
rightsourceip={{subnet}} # Serve virtual IP-s from this pool
|
||||||
left={{local}} # Gateway IP address
|
left={{local}} # Gateway IP address
|
||||||
leftcert={{certificate_path}} # Gateway certificate
|
leftcert={{certificate_path}} # Gateway certificate
|
||||||
leftfirewall=yes
|
leftfirewall=yes
|
||||||
|
@ -11,13 +11,14 @@ module = certidude.wsgi
|
|||||||
callable = app
|
callable = app
|
||||||
chmod-socket = 660
|
chmod-socket = 660
|
||||||
chown-socket = {{username}}:www-data
|
chown-socket = {{username}}:www-data
|
||||||
|
buffer-size = 32768
|
||||||
{% if push_server %}
|
{% if push_server %}
|
||||||
env = CERTIDUDE_EVENT_PUBLISH={{push_server}}/publish/%(channel)s
|
env = PUSH_PUBLISH={{push_server}}/publish/%(channel)s
|
||||||
env = CERTIDUDE_EVENT_SUBSCRIBE={{push_server}}/subscribe/%(channel)s
|
env = PUSH_SUBSCRIBE={{push_server}}/subscribe/%(channel)s
|
||||||
{% else %}
|
{% else %}
|
||||||
env = CERTIDUDE_EVENT_PUBLISH=http://localhost/event/publish/%(channel)s
|
env = PUSH_PUBLISH=http://localhost/event/publish/%(channel)s
|
||||||
env = CERTIDUDE_EVENT_SUBSCRIBE=http://localhost/event/subscribe/%(channel)s
|
env = PUSH_SUBSCRIBE=http://localhost/event/subscribe/%(channel)s
|
||||||
{% endif %}
|
{% endif %}
|
||||||
env = LANG=C.UTF-8
|
env = LANG=C.UTF-8
|
||||||
env = LC_ALL=C.UTF-8
|
env = LC_ALL=C.UTF-8
|
||||||
|
env = KRB5_KTNAME={{kerberos_keytab}}
|
||||||
|
@ -27,7 +27,7 @@ def notify(func):
|
|||||||
def wrapped(instance, csr, *args, **kwargs):
|
def wrapped(instance, csr, *args, **kwargs):
|
||||||
cert = func(instance, csr, *args, **kwargs)
|
cert = func(instance, csr, *args, **kwargs)
|
||||||
assert isinstance(cert, Certificate), "notify wrapped function %s returned %s" % (func, type(cert))
|
assert isinstance(cert, Certificate), "notify wrapped function %s returned %s" % (func, type(cert))
|
||||||
url_template = os.getenv("CERTIDUDE_EVENT_PUBLISH")
|
url_template = os.getenv("PUSH_PUBLISH")
|
||||||
if url_template:
|
if url_template:
|
||||||
url = url_template % dict(channel=csr.fingerprint())
|
url = url_template % dict(channel=csr.fingerprint())
|
||||||
notification = urllib.request.Request(url, cert.dump().encode("ascii"))
|
notification = urllib.request.Request(url, cert.dump().encode("ascii"))
|
||||||
@ -79,7 +79,7 @@ class CertificateAuthorityConfig(object):
|
|||||||
section = "CA_" + slug
|
section = "CA_" + slug
|
||||||
|
|
||||||
dirs = dict([(key, self.get(section, key))
|
dirs = dict([(key, self.get(section, key))
|
||||||
for key in ("dir", "certificate", "crl", "certs", "new_certs_dir", "private_key", "revoked_certs_dir", "request_whitelist", "autosign_whitelist")])
|
for key in ("dir", "certificate", "crl", "certs", "new_certs_dir", "private_key", "revoked_certs_dir", "request_subnets", "autosign_subnets", "admin_subnets", "admin_users")])
|
||||||
|
|
||||||
# Variable expansion, eg $dir
|
# Variable expansion, eg $dir
|
||||||
for key, value in dirs.items():
|
for key, value in dirs.items():
|
||||||
@ -391,8 +391,7 @@ class Certificate(CertificateBase):
|
|||||||
return self.signed <= other.signed
|
return self.signed <= other.signed
|
||||||
|
|
||||||
class CertificateAuthority(object):
|
class CertificateAuthority(object):
|
||||||
|
def __init__(self, slug, certificate, crl, certs, new_certs_dir, revoked_certs_dir=None, private_key=None, autosign_subnets=None, request_subnets=None, admin_subnets=None, admin_users=None, email_address=None, inbox=None, outbox=None, basic_constraints="CA:FALSE", key_usage="digitalSignature,keyEncipherment", extended_key_usage="clientAuth", certificate_lifetime=5*365, revocation_list_lifetime=1):
|
||||||
def __init__(self, slug, certificate, crl, certs, new_certs_dir, revoked_certs_dir=None, private_key=None, autosign=False, autosign_whitelist=None, request_whitelist=None, email_address=None, inbox=None, outbox=None, basic_constraints="CA:FALSE", key_usage="digitalSignature,keyEncipherment", extended_key_usage="clientAuth", certificate_lifetime=5*365, revocation_list_lifetime=1):
|
|
||||||
self.slug = slug
|
self.slug = slug
|
||||||
self.revocation_list = crl
|
self.revocation_list = crl
|
||||||
self.signed_dir = certs
|
self.signed_dir = certs
|
||||||
@ -400,8 +399,9 @@ class CertificateAuthority(object):
|
|||||||
self.revoked_dir = revoked_certs_dir
|
self.revoked_dir = revoked_certs_dir
|
||||||
self.private_key = private_key
|
self.private_key = private_key
|
||||||
|
|
||||||
self.autosign_whitelist = set([ipaddress.ip_network(j) for j in autosign_whitelist.split(" ") if j])
|
self.admin_subnets = set([ipaddress.ip_network(j) for j in admin_subnets.split(" ") if j])
|
||||||
self.request_whitelist = set([ipaddress.ip_network(j) for j in request_whitelist.split(" ") if j]).union(self.autosign_whitelist)
|
self.autosign_subnets = set([ipaddress.ip_network(j) for j in autosign_subnets.split(" ") if j])
|
||||||
|
self.request_subnets = set([ipaddress.ip_network(j) for j in request_subnets.split(" ") if j]).union(self.autosign_subnets)
|
||||||
|
|
||||||
self.certificate = Certificate(open(certificate))
|
self.certificate = Certificate(open(certificate))
|
||||||
self.mailer = Mailer(outbox) if outbox else None
|
self.mailer = Mailer(outbox) if outbox else None
|
||||||
@ -411,6 +411,18 @@ class CertificateAuthority(object):
|
|||||||
self.key_usage = key_usage
|
self.key_usage = key_usage
|
||||||
self.extended_key_usage = extended_key_usage
|
self.extended_key_usage = extended_key_usage
|
||||||
|
|
||||||
|
self.admin_emails = dict()
|
||||||
|
self.admin_users = set()
|
||||||
|
if admin_users:
|
||||||
|
if admin_users.startswith("/"):
|
||||||
|
for user in open(admin_users):
|
||||||
|
if ":" in user:
|
||||||
|
user, email, first_name, last_name = user.split(":")
|
||||||
|
self.admin_emails[user] = email
|
||||||
|
self.admin_users.add(user)
|
||||||
|
else:
|
||||||
|
self.admin_users = set([j for j in admin_users.split(" ") if j])
|
||||||
|
|
||||||
def _signer_exec(self, cmd, *bits):
|
def _signer_exec(self, cmd, *bits):
|
||||||
sock = self.connect_signer()
|
sock = self.connect_signer()
|
||||||
sock.send(cmd.encode("ascii"))
|
sock.send(cmd.encode("ascii"))
|
||||||
|
@ -13,8 +13,8 @@ from certidude.api import CertificateAuthorityResource, \
|
|||||||
|
|
||||||
config = CertificateAuthorityConfig("/etc/ssl/openssl.cnf")
|
config = CertificateAuthorityConfig("/etc/ssl/openssl.cnf")
|
||||||
|
|
||||||
assert os.getenv("CERTIDUDE_EVENT_SUBSCRIBE"), "Please set CERTIDUDE_EVENT_SUBSCRIBE to your web server's subscription URL"
|
assert os.getenv("PUSH_SUBSCRIBE"), "Please set PUSH_SUBSCRIBE to your web server's subscription URL"
|
||||||
assert os.getenv("CERTIDUDE_EVENT_PUBLISH"), "Please set CERTIDUDE_EVENT_PUBLISH to your web server's publishing URL"
|
assert os.getenv("PUSH_PUBLISH"), "Please set PUSH_PUBLISH to your web server's publishing URL"
|
||||||
|
|
||||||
app = falcon.API()
|
app = falcon.API()
|
||||||
app.add_route("/api/{ca}/ocsp/", CertificateStatusResource(config))
|
app.add_route("/api/{ca}/ocsp/", CertificateStatusResource(config))
|
||||||
|
Loading…
Reference in New Issue
Block a user