mirror of
				https://github.com/laurivosandi/certidude
				synced 2025-10-31 01:19:11 +00:00 
			
		
		
		
	Added preliminary Kerberos authentication support
This commit is contained in:
		
							
								
								
									
										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)) | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user