mirror of
				https://github.com/laurivosandi/certidude
				synced 2025-10-31 17:39:12 +00:00 
			
		
		
		
	Several updates #3
* Move SessionResource and CertificateAuthorityResource to api/session.py * Log browser user agent for logins * Remove static sink from backend, nginx always serves static now * Don't emit 'attribute-update' event if no attributes were changed * Better CN extraction from DN during lease update * Log user who deleted request * Remove long polling CRL fetch API call and relevant test * Merge auth decorators ldap_authenticate, kerberos_authenticate, pam_authenticate * Add 'kerberos subnets' to distinguish authentication method * Add 'admin subnets' to filter traffic to administrative API calls * Highlight recent log events * Links to switch between 2, 3 and 4 column layouts in the dashboard * Restored certidude client snippets in request dialog * Various bugfixes, improved log messages
This commit is contained in:
		| @@ -7,7 +7,8 @@ after_success: | |||||||
|   - codecov |   - codecov | ||||||
| script: | script: | ||||||
|   - echo registry=http://registry.npmjs.org/ | sudo tee /root/.npmrc |   - echo registry=http://registry.npmjs.org/ | sudo tee /root/.npmrc | ||||||
|   - sudo apt install software-properties-common python3-setuptools python3-mysql.connector python3-pyxattr |   - sudo apt install software-properties-common python3-setuptools build-essential python3-dev libsasl2-dev libkrb5-dev | ||||||
|  |   - sudo apt remove python3-mimeparse | ||||||
|   - sudo mkdir -p /etc/systemd/system # Until Travis is stuck with 14.04 |   - sudo mkdir -p /etc/systemd/system # Until Travis is stuck with 14.04 | ||||||
|   - sudo easy_install3 pip |   - sudo easy_install3 pip | ||||||
|   - sudo -H pip3 install -r requirements.txt |   - sudo -H pip3 install -r requirements.txt | ||||||
|   | |||||||
| @@ -1,218 +1,19 @@ | |||||||
| # encoding: utf-8 | # encoding: utf-8 | ||||||
|  |  | ||||||
| import falcon | import falcon | ||||||
| import mimetypes |  | ||||||
| import logging |  | ||||||
| import os |  | ||||||
| import hashlib |  | ||||||
| from datetime import datetime |  | ||||||
| from xattr import listxattr, getxattr |  | ||||||
| from certidude.common import cert_to_dn |  | ||||||
| from certidude.user import User |  | ||||||
| from certidude.decorators import serialize, csrf_protection |  | ||||||
| from certidude import const, config, authority |  | ||||||
| from .utils import AuthorityHandler |  | ||||||
| from .utils.firewall import login_required, authorize_admin |  | ||||||
|  |  | ||||||
| logger = logging.getLogger(__name__) |  | ||||||
|  |  | ||||||
|  |  | ||||||
| class CertificateAuthorityResource(object): |  | ||||||
|     def on_get(self, req, resp): |  | ||||||
|         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=%s.crt" % |  | ||||||
|             const.HOSTNAME.encode("ascii")) |  | ||||||
|  |  | ||||||
|  |  | ||||||
| class SessionResource(AuthorityHandler): |  | ||||||
|     @csrf_protection |  | ||||||
|     @serialize |  | ||||||
|     @login_required |  | ||||||
|     @authorize_admin |  | ||||||
|     def on_get(self, req, resp): |  | ||||||
|  |  | ||||||
|         def serialize_requests(g): |  | ||||||
|             for common_name, path, buf, req, submitted, server in g(): |  | ||||||
|                 try: |  | ||||||
|                     submission_address = getxattr(path, "user.request.address").decode("ascii") # TODO: move to authority.py |  | ||||||
|                 except IOError: |  | ||||||
|                     submission_address = None |  | ||||||
|                 try: |  | ||||||
|                     submission_hostname = getxattr(path, "user.request.hostname").decode("ascii") # TODO: move to authority.py |  | ||||||
|                 except IOError: |  | ||||||
|                     submission_hostname = None |  | ||||||
|                 yield dict( |  | ||||||
|                     submitted = submitted, |  | ||||||
|                     common_name = common_name, |  | ||||||
|                     address = submission_address, |  | ||||||
|                     hostname = submission_hostname if submission_hostname != submission_address else None, |  | ||||||
|                     md5sum = hashlib.md5(buf).hexdigest(), |  | ||||||
|                     sha1sum = hashlib.sha1(buf).hexdigest(), |  | ||||||
|                     sha256sum = hashlib.sha256(buf).hexdigest(), |  | ||||||
|                     sha512sum = hashlib.sha512(buf).hexdigest() |  | ||||||
|                 ) |  | ||||||
|  |  | ||||||
|         def serialize_revoked(g): |  | ||||||
|             for common_name, path, buf, cert, signed, expired, revoked, reason in g(limit=5): |  | ||||||
|                 yield dict( |  | ||||||
|                     serial = "%x" % cert.serial_number, |  | ||||||
|                     common_name = common_name, |  | ||||||
|                     # TODO: key type, key length, key exponent, key modulo |  | ||||||
|                     signed = signed, |  | ||||||
|                     expired = expired, |  | ||||||
|                     revoked = revoked, |  | ||||||
|                     reason = reason, |  | ||||||
|                     sha256sum = hashlib.sha256(buf).hexdigest()) |  | ||||||
|  |  | ||||||
|         def serialize_certificates(g): |  | ||||||
|             for common_name, path, buf, cert, signed, expires in g(): |  | ||||||
|                 # Extract certificate tags from filesystem |  | ||||||
|                 try: |  | ||||||
|                     tags = [] |  | ||||||
|                     for tag in getxattr(path, "user.xdg.tags").decode("utf-8").split(","): |  | ||||||
|                         if "=" in tag: |  | ||||||
|                             k, v = tag.split("=", 1) |  | ||||||
|                         else: |  | ||||||
|                             k, v = "other", tag |  | ||||||
|                         tags.append(dict(id=tag, key=k, value=v)) |  | ||||||
|                 except IOError: # No such attribute(s) |  | ||||||
|                     tags = None |  | ||||||
|  |  | ||||||
|                 attributes = {} |  | ||||||
|                 for key in listxattr(path): |  | ||||||
|                     if key.startswith(b"user.machine."): |  | ||||||
|                         attributes[key[13:].decode("ascii")] = getxattr(path, key).decode("ascii") |  | ||||||
|  |  | ||||||
|                 # Extract lease information from filesystem |  | ||||||
|                 try: |  | ||||||
|                     last_seen = datetime.strptime(getxattr(path, "user.lease.last_seen").decode("ascii"), "%Y-%m-%dT%H:%M:%S.%fZ") |  | ||||||
|                     lease = dict( |  | ||||||
|                         inner_address = getxattr(path, "user.lease.inner_address").decode("ascii"), |  | ||||||
|                         outer_address = getxattr(path, "user.lease.outer_address").decode("ascii"), |  | ||||||
|                         last_seen = last_seen, |  | ||||||
|                         age = datetime.utcnow() - last_seen |  | ||||||
|                     ) |  | ||||||
|                 except IOError: # No such attribute(s) |  | ||||||
|                     lease = None |  | ||||||
|  |  | ||||||
|                 try: |  | ||||||
|                     signer_username = getxattr(path, "user.signature.username").decode("ascii") |  | ||||||
|                 except IOError: |  | ||||||
|                     signer_username = None |  | ||||||
|  |  | ||||||
|                 # TODO: dedup |  | ||||||
|                 yield dict( |  | ||||||
|                     serial = "%x" % cert.serial_number, |  | ||||||
|                     organizational_unit = cert.subject.native.get("organizational_unit_name"), |  | ||||||
|                     common_name = common_name, |  | ||||||
|                     # TODO: key type, key length, key exponent, key modulo |  | ||||||
|                     signed = signed, |  | ||||||
|                     expires = expires, |  | ||||||
|                     sha256sum = hashlib.sha256(buf).hexdigest(), |  | ||||||
|                     signer = signer_username, |  | ||||||
|                     lease = lease, |  | ||||||
|                     tags = tags, |  | ||||||
|                     attributes = attributes or None, |  | ||||||
|                     extensions = dict([ |  | ||||||
|                         (e["extn_id"].native, e["extn_value"].native) |  | ||||||
|                         for e in cert["tbs_certificate"]["extensions"] |  | ||||||
|                         if e["extn_id"].native in ("extended_key_usage",)]) |  | ||||||
|                 ) |  | ||||||
|  |  | ||||||
|         logger.info("Logged in authority administrator %s from %s" % (req.context.get("user"), req.context.get("remote_addr"))) |  | ||||||
|         return dict( |  | ||||||
|             user = dict( |  | ||||||
|                 name=req.context.get("user").name, |  | ||||||
|                 gn=req.context.get("user").given_name, |  | ||||||
|                 sn=req.context.get("user").surname, |  | ||||||
|                 mail=req.context.get("user").mail |  | ||||||
|             ), |  | ||||||
|             request_submission_allowed = config.REQUEST_SUBMISSION_ALLOWED, |  | ||||||
|             service = dict( |  | ||||||
|                 protocols = config.SERVICE_PROTOCOLS, |  | ||||||
|                 routers = [j[0] for j in authority.list_signed( |  | ||||||
|                     common_name=config.SERVICE_ROUTERS)] |  | ||||||
|             ), |  | ||||||
|             authority = dict( |  | ||||||
|                 builder = dict( |  | ||||||
|                     profiles = config.IMAGE_BUILDER_PROFILES |  | ||||||
|                 ), |  | ||||||
|                 tagging = [dict(name=t[0], type=t[1], title=t[2]) for t in config.TAG_TYPES], |  | ||||||
|                 lease = dict( |  | ||||||
|                     offline = 600, # Seconds from last seen activity to consider lease offline, OpenVPN reneg-sec option |  | ||||||
|                     dead = 604800 # Seconds from last activity to consider lease dead, X509 chain broken or machine discarded |  | ||||||
|                 ), |  | ||||||
|                 certificate = dict( |  | ||||||
|                     algorithm = authority.public_key.algorithm, |  | ||||||
|                     common_name = self.authority.certificate.subject.native["common_name"], |  | ||||||
|                     distinguished_name = cert_to_dn(self.authority.certificate), |  | ||||||
|                     md5sum = hashlib.md5(self.authority.certificate_buf).hexdigest(), |  | ||||||
|                     blob = self.authority.certificate_buf.decode("ascii"), |  | ||||||
|                 ), |  | ||||||
|                 mailer = dict( |  | ||||||
|                     name = config.MAILER_NAME, |  | ||||||
|                     address = config.MAILER_ADDRESS |  | ||||||
|                 ) if config.MAILER_ADDRESS else None, |  | ||||||
|                 machine_enrollment_subnets=config.MACHINE_ENROLLMENT_SUBNETS, |  | ||||||
|                 user_enrollment_allowed=config.USER_ENROLLMENT_ALLOWED, |  | ||||||
|                 user_multiple_certificates=config.USER_MULTIPLE_CERTIFICATES, |  | ||||||
|                 events = config.EVENT_SOURCE_SUBSCRIBE % config.EVENT_SOURCE_TOKEN, |  | ||||||
|                 requests=serialize_requests(self.authority.list_requests), |  | ||||||
|                 signed=serialize_certificates(self.authority.list_signed), |  | ||||||
|                 revoked=serialize_revoked(self.authority.list_revoked), |  | ||||||
|                 admin_users = User.objects.filter_admins(), |  | ||||||
|                 user_subnets = config.USER_SUBNETS or None, |  | ||||||
|                 autosign_subnets = config.AUTOSIGN_SUBNETS or None, |  | ||||||
|                 request_subnets = config.REQUEST_SUBNETS or None, |  | ||||||
|                 admin_subnets=config.ADMIN_SUBNETS or None, |  | ||||||
|                 signature = dict( |  | ||||||
|                     revocation_list_lifetime=config.REVOCATION_LIST_LIFETIME, |  | ||||||
|                     profiles = sorted([p.serialize() for p in config.PROFILES.values()], key=lambda p:p.get("slug")), |  | ||||||
|  |  | ||||||
|                 ) |  | ||||||
|             ), |  | ||||||
|             features=dict( |  | ||||||
|                 ocsp=bool(config.OCSP_SUBNETS), |  | ||||||
|                 crl=bool(config.CRL_SUBNETS), |  | ||||||
|                 token=bool(config.TOKEN_URL), |  | ||||||
|                 tagging=True, |  | ||||||
|                 leases=True, |  | ||||||
|                 logging=config.LOGGING_BACKEND) |  | ||||||
|         ) |  | ||||||
|  |  | ||||||
|  |  | ||||||
| class StaticResource(object): |  | ||||||
|     def __init__(self, root): |  | ||||||
|         self.root = os.path.realpath(root) |  | ||||||
|  |  | ||||||
|     def __call__(self, req, resp): |  | ||||||
|         path = os.path.realpath(os.path.join(self.root, req.path[1:])) |  | ||||||
|         if not path.startswith(self.root): |  | ||||||
|             raise falcon.HTTPBadRequest() |  | ||||||
|  |  | ||||||
|         if os.path.isdir(path): |  | ||||||
|             path = os.path.join(path, "index.html") |  | ||||||
|  |  | ||||||
|         if os.path.exists(path): |  | ||||||
|             content_type, content_encoding = mimetypes.guess_type(path) |  | ||||||
|             if content_type: |  | ||||||
|                 resp.append_header("Content-Type", content_type) |  | ||||||
|             if content_encoding: |  | ||||||
|                 resp.append_header("Content-Encoding", content_encoding) |  | ||||||
|             resp.stream = open(path, "rb") |  | ||||||
|             logger.debug("Serving '%s' from '%s'", req.path, path) |  | ||||||
|         else: |  | ||||||
|             resp.status = falcon.HTTP_404 |  | ||||||
|             resp.body = "File '%s' not found" % req.path |  | ||||||
|             logger.info("File '%s' not found, path resolved to '%s'", req.path, path) |  | ||||||
| import ipaddress | import ipaddress | ||||||
|  | import os | ||||||
|  | from certidude import config | ||||||
|  | from user_agents import parse | ||||||
|  |  | ||||||
|  |  | ||||||
| class NormalizeMiddleware(object): | class NormalizeMiddleware(object): | ||||||
|     def process_request(self, req, resp, *args): |     def process_request(self, req, resp, *args): | ||||||
|         assert not req.get_param("unicode") or req.get_param("unicode") == u"✓", "Unicode sanity check failed" |  | ||||||
|         req.context["remote_addr"] = ipaddress.ip_address(req.access_route[0]) |         req.context["remote_addr"] = ipaddress.ip_address(req.access_route[0]) | ||||||
|  |         if req.user_agent: | ||||||
|  |             req.context["user_agent"] = parse(req.user_agent) | ||||||
|  |         else: | ||||||
|  |             req.context["user_agent"] = "Unknown user agent" | ||||||
|  |  | ||||||
| def certidude_app(log_handlers=[]): | def certidude_app(log_handlers=[]): | ||||||
|     from certidude import authority, config |     from certidude import authority, config | ||||||
| @@ -225,10 +26,10 @@ def certidude_app(log_handlers=[]): | |||||||
|     from .bootstrap import BootstrapResource |     from .bootstrap import BootstrapResource | ||||||
|     from .token import TokenResource |     from .token import TokenResource | ||||||
|     from .builder import ImageBuilderResource |     from .builder import ImageBuilderResource | ||||||
|  |     from .session import SessionResource, CertificateAuthorityResource | ||||||
|  |  | ||||||
|     app = falcon.API(middleware=NormalizeMiddleware()) |     app = falcon.API(middleware=NormalizeMiddleware()) | ||||||
|     app.req_options.auto_parse_form_urlencoded = True |     app.req_options.auto_parse_form_urlencoded = True | ||||||
|     #app.req_options.strip_url_path_trailing_slash = False |  | ||||||
|  |  | ||||||
|     # Certificate authority API calls |     # Certificate authority API calls | ||||||
|     app.add_route("/api/certificate/", CertificateAuthorityResource()) |     app.add_route("/api/certificate/", CertificateAuthorityResource()) | ||||||
| @@ -270,9 +71,6 @@ def certidude_app(log_handlers=[]): | |||||||
|         from .scep import SCEPResource |         from .scep import SCEPResource | ||||||
|         app.add_route("/api/scep/", SCEPResource(authority)) |         app.add_route("/api/scep/", SCEPResource(authority)) | ||||||
|  |  | ||||||
|     # Add sink for serving static files |  | ||||||
|     app.add_sink(StaticResource(os.path.join(__file__, "..", "..", "static"))) |  | ||||||
|  |  | ||||||
|     if config.OCSP_SUBNETS: |     if config.OCSP_SUBNETS: | ||||||
|         from .ocsp import OCSPResource |         from .ocsp import OCSPResource | ||||||
|         app.add_sink(OCSPResource(authority), prefix="/api/ocsp") |         app.add_sink(OCSPResource(authority), prefix="/api/ocsp") | ||||||
|   | |||||||
| @@ -1,7 +1,7 @@ | |||||||
| import falcon | import falcon | ||||||
| import logging | import logging | ||||||
| import re | import re | ||||||
| from xattr import setxattr, listxattr, removexattr | from xattr import setxattr, listxattr, removexattr, getxattr | ||||||
| from certidude import push | from certidude import push | ||||||
| from certidude.decorators import serialize, csrf_protection | from certidude.decorators import serialize, csrf_protection | ||||||
| from .utils.firewall import login_required, authorize_admin, whitelist_subject | from .utils.firewall import login_required, authorize_admin, whitelist_subject | ||||||
| @@ -21,7 +21,6 @@ class AttributeResource(object): | |||||||
|         Return extended attributes stored on the server. |         Return extended attributes stored on the server. | ||||||
|         This not only contains tags and lease information, |         This not only contains tags and lease information, | ||||||
|         but might also contain some other sensitive information. |         but might also contain some other sensitive information. | ||||||
|         Results made available only to lease IP address. |  | ||||||
|         """ |         """ | ||||||
|         try: |         try: | ||||||
|             path, buf, cert, attribs = self.authority.get_attributes(cn, |             path, buf, cert, attribs = self.authority.get_attributes(cn, | ||||||
| @@ -44,14 +43,22 @@ class AttributeResource(object): | |||||||
|                 if not re.match("[a-z0-9_\.]+$", key): |                 if not re.match("[a-z0-9_\.]+$", key): | ||||||
|                     raise falcon.HTTPBadRequest("Invalid key %s" % key) |                     raise falcon.HTTPBadRequest("Invalid key %s" % key) | ||||||
|             valid = set() |             valid = set() | ||||||
|  |             modified = False | ||||||
|             for key, value in req.params.items(): |             for key, value in req.params.items(): | ||||||
|                 identifier = ("user.%s.%s" % (self.namespace, key)).encode("ascii") |                 identifier = ("user.%s.%s" % (self.namespace, key)).encode("ascii") | ||||||
|  |                 try: | ||||||
|  |                     if getxattr(path, identifier).decode("utf-8") != value: | ||||||
|  |                         modified = True | ||||||
|  |                 except OSError: # no such attribute | ||||||
|  |                     pass | ||||||
|                 setxattr(path, identifier, value.encode("utf-8")) |                 setxattr(path, identifier, value.encode("utf-8")) | ||||||
|                 valid.add(identifier) |                 valid.add(identifier) | ||||||
|             for key in listxattr(path): |             for key in listxattr(path): | ||||||
|                 if not key.startswith(namespace): |                 if not key.startswith(namespace): | ||||||
|                     continue |                     continue | ||||||
|                 if key not in valid: |                 if key not in valid: | ||||||
|  |                     modified = True | ||||||
|                     removexattr(path, key) |                     removexattr(path, key) | ||||||
|  |             if modified: | ||||||
|                 push.publish("attribute-update", cn) |                 push.publish("attribute-update", cn) | ||||||
|  |  | ||||||
|   | |||||||
| @@ -33,9 +33,9 @@ class LeaseResource(AuthorityHandler): | |||||||
|     @authorize_server |     @authorize_server | ||||||
|     def on_post(self, req, resp): |     def on_post(self, req, resp): | ||||||
|         client_common_name = req.get_param("client", required=True) |         client_common_name = req.get_param("client", required=True) | ||||||
|         m = re.match("CN=(.+?),", client_common_name) # It's actually DN, resolve it to CN |         m = re.match("^(.*, )*CN=(.+?)(, .*)*$", client_common_name) # It's actually DN, resolve it to CN | ||||||
|         if m: |         if m: | ||||||
|             client_common_name, = m.groups() |             _, client_common_name, _ = m.groups() | ||||||
|  |  | ||||||
|         path, buf, cert, signed, expires = self.authority.get_signed(client_common_name) # TODO: catch exceptions |         path, buf, cert, signed, expires = self.authority.get_signed(client_common_name) # TODO: catch exceptions | ||||||
|         if req.get_param("serial") and cert.serial_number != req.get_param_as_int("serial"): # OCSP-ish solution for OpenVPN, not exposed for StrongSwan |         if req.get_param("serial") and cert.serial_number != req.get_param_as_int("serial"): # OCSP-ish solution for OpenVPN, not exposed for StrongSwan | ||||||
|   | |||||||
| @@ -140,7 +140,7 @@ class RequestListResource(AuthorityHandler): | |||||||
|                         resp.set_header("Content-Type", "application/x-pem-file") |                         resp.set_header("Content-Type", "application/x-pem-file") | ||||||
|                         _, resp.body = self.authority._sign(csr, body, |                         _, resp.body = self.authority._sign(csr, body, | ||||||
|                             overwrite=overwrite_allowed, profile=config.PROFILES["rw"]) |                             overwrite=overwrite_allowed, profile=config.PROFILES["rw"]) | ||||||
|                         logger.info("Autosigned %s as %s is whitelisted", common_name, req.context.get("remote_addr")) |                         logger.info("Signed %s as %s is whitelisted for autosign", common_name, req.context.get("remote_addr")) | ||||||
|                         return |                         return | ||||||
|                     except EnvironmentError: |                     except EnvironmentError: | ||||||
|                         logger.info("Autosign for %s from %s failed, signed certificate already exists", |                         logger.info("Autosign for %s from %s failed, signed certificate already exists", | ||||||
| @@ -148,7 +148,7 @@ class RequestListResource(AuthorityHandler): | |||||||
|                         reasons.append("autosign failed, signed certificate already exists") |                         reasons.append("autosign failed, signed certificate already exists") | ||||||
|                     break |                     break | ||||||
|             else: |             else: | ||||||
|                 reasons.append("autosign failed, IP address not whitelisted") |                 reasons.append("IP address not whitelisted for autosign") | ||||||
|         else: |         else: | ||||||
|             reasons.append("autosign not requested") |             reasons.append("autosign not requested") | ||||||
|  |  | ||||||
| @@ -170,7 +170,7 @@ class RequestListResource(AuthorityHandler): | |||||||
|             push.publish("request-submitted", common_name) |             push.publish("request-submitted", common_name) | ||||||
|  |  | ||||||
|         # Wait the certificate to be signed if waiting is requested |         # Wait the certificate to be signed if waiting is requested | ||||||
|         logger.info("Stored signing request %s from %s, reasons: %s", common_name, req.context.get("remote_addr"), reasons) |         logger.info("Signing request %s from %s put on hold,  %s", common_name, req.context.get("remote_addr"), ", ".join(reasons)) | ||||||
|  |  | ||||||
|         if req.get_param("wait"): |         if req.get_param("wait"): | ||||||
|             # Redirect to nginx pub/sub |             # Redirect to nginx pub/sub | ||||||
| @@ -178,7 +178,6 @@ class RequestListResource(AuthorityHandler): | |||||||
|             click.echo("Redirecting to: %s"  % url) |             click.echo("Redirecting to: %s"  % url) | ||||||
|             resp.status = falcon.HTTP_SEE_OTHER |             resp.status = falcon.HTTP_SEE_OTHER | ||||||
|             resp.set_header("Location", url) |             resp.set_header("Location", url) | ||||||
|             logger.debug("Redirecting signing request from %s to %s, reasons: %s", req.context.get("remote_addr"), url, ", ".join(reasons)) |  | ||||||
|         else: |         else: | ||||||
|             # Request was accepted, but not processed |             # Request was accepted, but not processed | ||||||
|             resp.status = falcon.HTTP_202 |             resp.status = falcon.HTTP_202 | ||||||
| @@ -256,7 +255,7 @@ class RequestDetailResource(AuthorityHandler): | |||||||
|     @authorize_admin |     @authorize_admin | ||||||
|     def on_delete(self, req, resp, cn): |     def on_delete(self, req, resp, cn): | ||||||
|         try: |         try: | ||||||
|             self.authority.delete_request(cn) |             self.authority.delete_request(cn, user=req.context.get("user")) | ||||||
|             # Logging implemented in the function above |             # Logging implemented in the function above | ||||||
|         except errors.RequestDoesNotExist as e: |         except errors.RequestDoesNotExist as e: | ||||||
|             resp.body = "No certificate signing request for %s found" % cn |             resp.body = "No certificate signing request for %s found" % cn | ||||||
|   | |||||||
| @@ -20,13 +20,6 @@ class RevocationListResource(AuthorityHandler): | |||||||
|             logger.debug("Serving revocation list (DER) to %s", req.context.get("remote_addr")) |             logger.debug("Serving revocation list (DER) to %s", req.context.get("remote_addr")) | ||||||
|             resp.body = self.authority.export_crl(pem=False) |             resp.body = self.authority.export_crl(pem=False) | ||||||
|         elif req.client_accepts("application/x-pem-file"): |         elif req.client_accepts("application/x-pem-file"): | ||||||
|             if req.get_param_as_bool("wait"): |  | ||||||
|                 url = config.LONG_POLL_SUBSCRIBE % "crl" |  | ||||||
|                 resp.status = falcon.HTTP_SEE_OTHER |  | ||||||
|                 resp.set_header("Location", url) |  | ||||||
|                 logger.debug("Redirecting to CRL request to %s", url) |  | ||||||
|                 resp.body = "Redirecting to %s" % url |  | ||||||
|             else: |  | ||||||
|             resp.set_header("Content-Type", "application/x-pem-file") |             resp.set_header("Content-Type", "application/x-pem-file") | ||||||
|             resp.append_header( |             resp.append_header( | ||||||
|                 "Content-Disposition", |                 "Content-Disposition", | ||||||
|   | |||||||
							
								
								
									
										178
									
								
								certidude/api/session.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										178
									
								
								certidude/api/session.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,178 @@ | |||||||
|  | from datetime import datetime | ||||||
|  | from xattr import listxattr, getxattr | ||||||
|  | import falcon | ||||||
|  | import hashlib | ||||||
|  | import logging | ||||||
|  | from certidude import const, config | ||||||
|  | from certidude.common import cert_to_dn | ||||||
|  | from certidude.decorators import serialize, csrf_protection | ||||||
|  | from certidude.user import User | ||||||
|  | from .utils import AuthorityHandler | ||||||
|  | from .utils.firewall import login_required, authorize_admin | ||||||
|  |  | ||||||
|  | logger = logging.getLogger(__name__) | ||||||
|  |  | ||||||
|  | class CertificateAuthorityResource(object): | ||||||
|  |     def on_get(self, req, resp): | ||||||
|  |         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=%s.crt" % | ||||||
|  |             const.HOSTNAME.encode("ascii")) | ||||||
|  |  | ||||||
|  | class SessionResource(AuthorityHandler): | ||||||
|  |     @csrf_protection | ||||||
|  |     @serialize | ||||||
|  |     @login_required | ||||||
|  |     @authorize_admin | ||||||
|  |     def on_get(self, req, resp): | ||||||
|  |  | ||||||
|  |         def serialize_requests(g): | ||||||
|  |             for common_name, path, buf, req, submitted, server in g(): | ||||||
|  |                 try: | ||||||
|  |                     submission_address = getxattr(path, "user.request.address").decode("ascii") # TODO: move to authority.py | ||||||
|  |                 except IOError: | ||||||
|  |                     submission_address = None | ||||||
|  |                 try: | ||||||
|  |                     submission_hostname = getxattr(path, "user.request.hostname").decode("ascii") # TODO: move to authority.py | ||||||
|  |                 except IOError: | ||||||
|  |                     submission_hostname = None | ||||||
|  |                 yield dict( | ||||||
|  |                     submitted = submitted, | ||||||
|  |                     common_name = common_name, | ||||||
|  |                     address = submission_address, | ||||||
|  |                     hostname = submission_hostname if submission_hostname != submission_address else None, | ||||||
|  |                     md5sum = hashlib.md5(buf).hexdigest(), | ||||||
|  |                     sha1sum = hashlib.sha1(buf).hexdigest(), | ||||||
|  |                     sha256sum = hashlib.sha256(buf).hexdigest(), | ||||||
|  |                     sha512sum = hashlib.sha512(buf).hexdigest() | ||||||
|  |                 ) | ||||||
|  |  | ||||||
|  |         def serialize_revoked(g): | ||||||
|  |             for common_name, path, buf, cert, signed, expired, revoked, reason in g(limit=5): | ||||||
|  |                 yield dict( | ||||||
|  |                     serial = "%x" % cert.serial_number, | ||||||
|  |                     common_name = common_name, | ||||||
|  |                     # TODO: key type, key length, key exponent, key modulo | ||||||
|  |                     signed = signed, | ||||||
|  |                     expired = expired, | ||||||
|  |                     revoked = revoked, | ||||||
|  |                     reason = reason, | ||||||
|  |                     sha256sum = hashlib.sha256(buf).hexdigest()) | ||||||
|  |  | ||||||
|  |         def serialize_certificates(g): | ||||||
|  |             for common_name, path, buf, cert, signed, expires in g(): | ||||||
|  |                 # Extract certificate tags from filesystem | ||||||
|  |                 try: | ||||||
|  |                     tags = [] | ||||||
|  |                     for tag in getxattr(path, "user.xdg.tags").decode("utf-8").split(","): | ||||||
|  |                         if "=" in tag: | ||||||
|  |                             k, v = tag.split("=", 1) | ||||||
|  |                         else: | ||||||
|  |                             k, v = "other", tag | ||||||
|  |                         tags.append(dict(id=tag, key=k, value=v)) | ||||||
|  |                 except IOError: # No such attribute(s) | ||||||
|  |                     tags = None | ||||||
|  |  | ||||||
|  |                 attributes = {} | ||||||
|  |                 for key in listxattr(path): | ||||||
|  |                     if key.startswith(b"user.machine."): | ||||||
|  |                         attributes[key[13:].decode("ascii")] = getxattr(path, key).decode("ascii") | ||||||
|  |  | ||||||
|  |                 # Extract lease information from filesystem | ||||||
|  |                 try: | ||||||
|  |                     last_seen = datetime.strptime(getxattr(path, "user.lease.last_seen").decode("ascii"), "%Y-%m-%dT%H:%M:%S.%fZ") | ||||||
|  |                     lease = dict( | ||||||
|  |                         inner_address = getxattr(path, "user.lease.inner_address").decode("ascii"), | ||||||
|  |                         outer_address = getxattr(path, "user.lease.outer_address").decode("ascii"), | ||||||
|  |                         last_seen = last_seen, | ||||||
|  |                         age = datetime.utcnow() - last_seen | ||||||
|  |                     ) | ||||||
|  |                 except IOError: # No such attribute(s) | ||||||
|  |                     lease = None | ||||||
|  |  | ||||||
|  |                 try: | ||||||
|  |                     signer_username = getxattr(path, "user.signature.username").decode("ascii") | ||||||
|  |                 except IOError: | ||||||
|  |                     signer_username = None | ||||||
|  |  | ||||||
|  |                 # TODO: dedup | ||||||
|  |                 yield dict( | ||||||
|  |                     serial = "%x" % cert.serial_number, | ||||||
|  |                     organizational_unit = cert.subject.native.get("organizational_unit_name"), | ||||||
|  |                     common_name = common_name, | ||||||
|  |                     # TODO: key type, key length, key exponent, key modulo | ||||||
|  |                     signed = signed, | ||||||
|  |                     expires = expires, | ||||||
|  |                     sha256sum = hashlib.sha256(buf).hexdigest(), | ||||||
|  |                     signer = signer_username, | ||||||
|  |                     lease = lease, | ||||||
|  |                     tags = tags, | ||||||
|  |                     attributes = attributes or None, | ||||||
|  |                     extensions = dict([ | ||||||
|  |                         (e["extn_id"].native, e["extn_value"].native) | ||||||
|  |                         for e in cert["tbs_certificate"]["extensions"] | ||||||
|  |                         if e["extn_id"].native in ("extended_key_usage",)]) | ||||||
|  |                 ) | ||||||
|  |  | ||||||
|  |         logger.info("Logged in authority administrator %s from %s with %s" % ( | ||||||
|  |             req.context.get("user"), req.context.get("remote_addr"), req.context.get("user_agent"))) | ||||||
|  |         return dict( | ||||||
|  |             user = dict( | ||||||
|  |                 name=req.context.get("user").name, | ||||||
|  |                 gn=req.context.get("user").given_name, | ||||||
|  |                 sn=req.context.get("user").surname, | ||||||
|  |                 mail=req.context.get("user").mail | ||||||
|  |             ), | ||||||
|  |             request_submission_allowed = config.REQUEST_SUBMISSION_ALLOWED, | ||||||
|  |             service = dict( | ||||||
|  |                 protocols = config.SERVICE_PROTOCOLS, | ||||||
|  |                 routers = [j[0] for j in self.authority.list_signed( | ||||||
|  |                     common_name=config.SERVICE_ROUTERS)] | ||||||
|  |             ), | ||||||
|  |             authority = dict( | ||||||
|  |                 builder = dict( | ||||||
|  |                     profiles = config.IMAGE_BUILDER_PROFILES | ||||||
|  |                 ), | ||||||
|  |                 tagging = [dict(name=t[0], type=t[1], title=t[2]) for t in config.TAG_TYPES], | ||||||
|  |                 lease = dict( | ||||||
|  |                     offline = 600, # Seconds from last seen activity to consider lease offline, OpenVPN reneg-sec option | ||||||
|  |                     dead = 604800 # Seconds from last activity to consider lease dead, X509 chain broken or machine discarded | ||||||
|  |                 ), | ||||||
|  |                 certificate = dict( | ||||||
|  |                     algorithm = self.authority.public_key.algorithm, | ||||||
|  |                     common_name = self.authority.certificate.subject.native["common_name"], | ||||||
|  |                     distinguished_name = cert_to_dn(self.authority.certificate), | ||||||
|  |                     md5sum = hashlib.md5(self.authority.certificate_buf).hexdigest(), | ||||||
|  |                     blob = self.authority.certificate_buf.decode("ascii"), | ||||||
|  |                 ), | ||||||
|  |                 mailer = dict( | ||||||
|  |                     name = config.MAILER_NAME, | ||||||
|  |                     address = config.MAILER_ADDRESS | ||||||
|  |                 ) if config.MAILER_ADDRESS else None, | ||||||
|  |                 machine_enrollment_subnets=config.MACHINE_ENROLLMENT_SUBNETS, | ||||||
|  |                 user_enrollment_allowed=config.USER_ENROLLMENT_ALLOWED, | ||||||
|  |                 user_multiple_certificates=config.USER_MULTIPLE_CERTIFICATES, | ||||||
|  |                 events = config.EVENT_SOURCE_SUBSCRIBE % config.EVENT_SOURCE_TOKEN, | ||||||
|  |                 requests=serialize_requests(self.authority.list_requests), | ||||||
|  |                 signed=serialize_certificates(self.authority.list_signed), | ||||||
|  |                 revoked=serialize_revoked(self.authority.list_revoked), | ||||||
|  |                 admin_users = User.objects.filter_admins(), | ||||||
|  |                 user_subnets = config.USER_SUBNETS or None, | ||||||
|  |                 autosign_subnets = config.AUTOSIGN_SUBNETS or None, | ||||||
|  |                 request_subnets = config.REQUEST_SUBNETS or None, | ||||||
|  |                 admin_subnets=config.ADMIN_SUBNETS or None, | ||||||
|  |                 signature = dict( | ||||||
|  |                     revocation_list_lifetime=config.REVOCATION_LIST_LIFETIME, | ||||||
|  |                     profiles = sorted([p.serialize() for p in config.PROFILES.values()], key=lambda p:p.get("slug")), | ||||||
|  |  | ||||||
|  |                 ) | ||||||
|  |             ), | ||||||
|  |             features=dict( | ||||||
|  |                 ocsp=bool(config.OCSP_SUBNETS), | ||||||
|  |                 crl=bool(config.CRL_SUBNETS), | ||||||
|  |                 token=bool(config.TOKEN_URL), | ||||||
|  |                 tagging=True, | ||||||
|  |                 leases=True, | ||||||
|  |                 logging=config.LOGGING_BACKEND) | ||||||
|  |         ) | ||||||
| @@ -68,8 +68,8 @@ class SignedCertificateDetailResource(AuthorityHandler): | |||||||
|     @login_required |     @login_required | ||||||
|     @authorize_admin |     @authorize_admin | ||||||
|     def on_delete(self, req, resp, cn): |     def on_delete(self, req, resp, cn): | ||||||
|         logger.info("Revoked certificate %s by %s from %s", |  | ||||||
|             cn, req.context.get("user"), req.context.get("remote_addr")) |  | ||||||
|         self.authority.revoke(cn, |         self.authority.revoke(cn, | ||||||
|             reason=req.get_param("reason", default="key_compromise")) |             reason=req.get_param("reason", default="key_compromise"), | ||||||
|  |             user=req.context.get("user") | ||||||
|  |         ) | ||||||
|  |  | ||||||
|   | |||||||
| @@ -41,7 +41,7 @@ class TagResource(AuthorityHandler): | |||||||
|         else: |         else: | ||||||
|             tags.add("%s=%s" % (key,value)) |             tags.add("%s=%s" % (key,value)) | ||||||
|         setxattr(path, "user.xdg.tags", ",".join(tags).encode("utf-8")) |         setxattr(path, "user.xdg.tags", ",".join(tags).encode("utf-8")) | ||||||
|         logger.debug("Tag %s=%s set for %s" % (key, value, cn)) |         logger.info("Tag %s=%s set for %s by %s" % (key, value, cn, req.context.get("user"))) | ||||||
|         push.publish("tag-update", cn) |         push.publish("tag-update", cn) | ||||||
|  |  | ||||||
|  |  | ||||||
| @@ -68,7 +68,7 @@ class TagDetailResource(object): | |||||||
|         else: |         else: | ||||||
|             tags.add(value) |             tags.add(value) | ||||||
|         setxattr(path, "user.xdg.tags", ",".join(tags).encode("utf-8")) |         setxattr(path, "user.xdg.tags", ",".join(tags).encode("utf-8")) | ||||||
|         logger.debug("Tag %s set to %s for %s" % (tag, value, cn)) |         logger.info("Tag %s set to %s for %s by %s" % (tag, value, cn, req.context.get("user"))) | ||||||
|         push.publish("tag-update", cn) |         push.publish("tag-update", cn) | ||||||
|  |  | ||||||
|     @csrf_protection |     @csrf_protection | ||||||
| @@ -82,5 +82,5 @@ class TagDetailResource(object): | |||||||
|             removexattr(path, "user.xdg.tags") |             removexattr(path, "user.xdg.tags") | ||||||
|         else: |         else: | ||||||
|             setxattr(path, "user.xdg.tags", ",".join(tags)) |             setxattr(path, "user.xdg.tags", ",".join(tags)) | ||||||
|         logger.debug("Tag %s removed for %s" % (tag, cn)) |         logger.info("Tag %s removed for %s by %s" % (tag, cn, req.context.get("user"))) | ||||||
|         push.publish("tag-update", cn) |         push.publish("tag-update", cn) | ||||||
|   | |||||||
| @@ -4,15 +4,17 @@ import logging | |||||||
| import binascii | import binascii | ||||||
| import click | import click | ||||||
| import gssapi | import gssapi | ||||||
|  | import ldap | ||||||
| import os | import os | ||||||
| import re | import re | ||||||
|  | import simplepam | ||||||
| import socket | import socket | ||||||
| from asn1crypto import pem, x509 | from asn1crypto import pem, x509 | ||||||
| from base64 import b64decode | from base64 import b64decode | ||||||
| from certidude.user import User | from certidude.user import User | ||||||
| from certidude import config, const | from certidude import config, const | ||||||
|  |  | ||||||
| logger = logging.getLogger("api") | logger = logging.getLogger(__name__) | ||||||
|  |  | ||||||
| def whitelist_subnets(subnets): | def whitelist_subnets(subnets): | ||||||
|     """ |     """ | ||||||
| @@ -81,18 +83,34 @@ def whitelist_subject(func): | |||||||
|  |  | ||||||
| def authenticate(optional=False): | def authenticate(optional=False): | ||||||
|     def wrapper(func): |     def wrapper(func): | ||||||
|         def kerberos_authenticate(resource, req, resp, *args, **kwargs): |         def wrapped(resource, req, resp, *args, **kwargs): | ||||||
|             # Try pre-emptive authentication |             kerberized = False | ||||||
|             if not req.auth: |  | ||||||
|                 if optional: |             if "kerberos" in config.AUTHENTICATION_BACKENDS: | ||||||
|  |                 for subnet in config.KERBEROS_SUBNETS: | ||||||
|  |                     if req.context.get("remote_addr") in subnet: | ||||||
|  |                         kerberized = True | ||||||
|  |  | ||||||
|  |             if not req.auth: # no credentials provided | ||||||
|  |                 if optional: # optional allowed | ||||||
|                     req.context["user"] = None |                     req.context["user"] = None | ||||||
|                     return func(resource, req, resp, *args, **kwargs) |                     return func(resource, req, resp, *args, **kwargs) | ||||||
|  |  | ||||||
|  |                 if kerberized: | ||||||
|                     logger.debug("No Kerberos ticket offered while attempting to access %s from %s", |                     logger.debug("No Kerberos ticket offered while attempting to access %s from %s", | ||||||
|                         req.env["PATH_INFO"], req.context.get("remote_addr")) |                         req.env["PATH_INFO"], req.context.get("remote_addr")) | ||||||
|                     raise falcon.HTTPUnauthorized("Unauthorized", |                     raise falcon.HTTPUnauthorized("Unauthorized", | ||||||
|                         "No Kerberos ticket offered, are you sure you've logged in with domain user account?", |                         "No Kerberos ticket offered, are you sure you've logged in with domain user account?", | ||||||
|                         ["Negotiate"]) |                         ["Negotiate"]) | ||||||
|  |                 else: | ||||||
|  |                     logger.debug("No credentials offered while attempting to access %s from %s", | ||||||
|  |                         req.env["PATH_INFO"], req.context.get("remote_addr")) | ||||||
|  |                     raise falcon.HTTPUnauthorized("Unauthorized", "Please authenticate", ("Basic",)) | ||||||
|  |  | ||||||
|  |             if kerberized: | ||||||
|  |                 if not req.auth.startswith("Negotiate "): | ||||||
|  |                     raise falcon.HTTPBadRequest("Bad request", | ||||||
|  |                         "Bad header, expected Negotiate: %s" % req.auth) | ||||||
|  |  | ||||||
|                 os.environ["KRB5_KTNAME"] = config.KERBEROS_KEYTAB |                 os.environ["KRB5_KTNAME"] = config.KERBEROS_KEYTAB | ||||||
|  |  | ||||||
| @@ -107,9 +125,6 @@ def authenticate(optional=False): | |||||||
|  |  | ||||||
|                 context = gssapi.sec_contexts.SecurityContext(creds=server_creds) |                 context = gssapi.sec_contexts.SecurityContext(creds=server_creds) | ||||||
|  |  | ||||||
|             if not req.auth.startswith("Negotiate "): |  | ||||||
|                 raise falcon.HTTPBadRequest("Bad request", "Bad header, expected Negotiate: %s" % req.auth) |  | ||||||
|  |  | ||||||
|                 token = ''.join(req.auth.split()[1:]) |                 token = ''.join(req.auth.split()[1:]) | ||||||
|  |  | ||||||
|                 try: |                 try: | ||||||
| @@ -141,30 +156,21 @@ def authenticate(optional=False): | |||||||
|                     req.context["user"], req.env["PATH_INFO"], req.context["remote_addr"]) |                     req.context["user"], req.env["PATH_INFO"], req.context["remote_addr"]) | ||||||
|                 return func(resource, req, resp, *args, **kwargs) |                 return func(resource, req, resp, *args, **kwargs) | ||||||
|  |  | ||||||
|  |             else: | ||||||
|         def ldap_authenticate(resource, req, resp, *args, **kwargs): |  | ||||||
|             """ |  | ||||||
|             Authenticate against LDAP with WWW Basic Auth credentials |  | ||||||
|             """ |  | ||||||
|  |  | ||||||
|             if optional and not req.get_param_as_bool("authenticate"): |  | ||||||
|                 return func(resource, req, resp, *args, **kwargs) |  | ||||||
|  |  | ||||||
|             import ldap |  | ||||||
|  |  | ||||||
|             if not req.auth: |  | ||||||
|                 raise falcon.HTTPUnauthorized("Unauthorized", |  | ||||||
|                     "No authentication header provided", |  | ||||||
|                     ("Basic",)) |  | ||||||
|  |  | ||||||
|                 if not req.auth.startswith("Basic "): |                 if not req.auth.startswith("Basic "): | ||||||
|                     raise falcon.HTTPBadRequest("Bad request", "Bad header, expected Basic: %s" % req.auth) |                     raise falcon.HTTPBadRequest("Bad request", "Bad header, expected Basic: %s" % req.auth) | ||||||
|  |  | ||||||
|             from base64 import b64decode |  | ||||||
|                 basic, token = req.auth.split(" ", 1) |                 basic, token = req.auth.split(" ", 1) | ||||||
|                 user, passwd = b64decode(token).decode("ascii").split(":", 1) |                 user, passwd = b64decode(token).decode("ascii").split(":", 1) | ||||||
|  |  | ||||||
|             upn = "%s@%s" % (user, const.DOMAIN) |             if config.AUTHENTICATION_BACKENDS == {"pam"}: | ||||||
|  |                 if not simplepam.authenticate(user, passwd, "sshd"): | ||||||
|  |                     logger.critical("Basic authentication failed for user %s from  %s, " | ||||||
|  |                         "are you sure server process has read access to /etc/shadow?", | ||||||
|  |                         repr(user), req.context.get("remote_addr")) | ||||||
|  |                     raise falcon.HTTPUnauthorized("Forbidden", "Invalid password", ("Basic",)) | ||||||
|  |                 conn = None | ||||||
|  |             elif "ldap" in config.AUTHENTICATION_BACKENDS: | ||||||
|  |                 upn = "%s@%s" % (user, config.KERBEROS_REALM) | ||||||
|                 click.echo("Connecting to %s as %s" % (config.LDAP_AUTHENTICATION_URI, upn)) |                 click.echo("Connecting to %s as %s" % (config.LDAP_AUTHENTICATION_URI, upn)) | ||||||
|                 conn = ldap.initialize(config.LDAP_AUTHENTICATION_URI, bytes_mode=False) |                 conn = ldap.initialize(config.LDAP_AUTHENTICATION_URI, bytes_mode=False) | ||||||
|                 conn.set_option(ldap.OPT_REFERRALS, 0) |                 conn.set_option(ldap.OPT_REFERRALS, 0) | ||||||
| @@ -186,53 +192,18 @@ def authenticate(optional=False): | |||||||
|                         ("Basic",)) |                         ("Basic",)) | ||||||
|  |  | ||||||
|                 req.context["ldap_conn"] = conn |                 req.context["ldap_conn"] = conn | ||||||
|  |             else: | ||||||
|  |                 raise NotImplementedError("No suitable authentication method configured") | ||||||
|  |  | ||||||
|  |             try: | ||||||
|                 req.context["user"] = User.objects.get(user) |                 req.context["user"] = User.objects.get(user) | ||||||
|  |             except User.DoesNotExist: | ||||||
|  |                 raise falcon.HTTPUnauthorized("Unauthorized", "Invalid credentials", ("Basic",)) | ||||||
|  |  | ||||||
|             retval = func(resource, req, resp, *args, **kwargs) |             retval = func(resource, req, resp, *args, **kwargs) | ||||||
|  |             if conn: | ||||||
|                 conn.unbind_s() |                 conn.unbind_s() | ||||||
|             return retval |             return retval | ||||||
|  |  | ||||||
|  |  | ||||||
|         def pam_authenticate(resource, req, resp, *args, **kwargs): |  | ||||||
|             """ |  | ||||||
|             Authenticate against PAM with WWW Basic Auth credentials |  | ||||||
|             """ |  | ||||||
|  |  | ||||||
|             if optional and not req.get_param_as_bool("authenticate"): |  | ||||||
|                 return func(resource, req, resp, *args, **kwargs) |  | ||||||
|  |  | ||||||
|             if not req.auth: |  | ||||||
|                 raise falcon.HTTPUnauthorized("Forbidden", "Please authenticate", ("Basic",)) |  | ||||||
|  |  | ||||||
|             if not req.auth.startswith("Basic "): |  | ||||||
|                 raise falcon.HTTPBadRequest("Bad request", "Bad header: %s" % req.auth) |  | ||||||
|  |  | ||||||
|             basic, token = req.auth.split(" ", 1) |  | ||||||
|             user, passwd = b64decode(token).decode("ascii").split(":", 1) |  | ||||||
|  |  | ||||||
|             import simplepam |  | ||||||
|             if not simplepam.authenticate(user, passwd, "sshd"): |  | ||||||
|                 logger.critical("Basic authentication failed for user %s from  %s, " |  | ||||||
|                     "are you sure server process has read access to /etc/shadow?", |  | ||||||
|                     repr(user), req.context.get("remote_addr")) |  | ||||||
|                 raise falcon.HTTPUnauthorized("Forbidden", "Invalid password", ("Basic",)) |  | ||||||
|  |  | ||||||
|             req.context["user"] = User.objects.get(user) |  | ||||||
|             return func(resource, req, resp, *args, **kwargs) |  | ||||||
|  |  | ||||||
|         def wrapped(resource, req, resp, *args, **kwargs): |  | ||||||
|             # If LDAP enabled and device is not Kerberos capable fall |  | ||||||
|             # back to LDAP bind authentication |  | ||||||
|             if "ldap" in config.AUTHENTICATION_BACKENDS: |  | ||||||
|                 if "Android" in req.user_agent or "iPhone" in req.user_agent: |  | ||||||
|                     return ldap_authenticate(resource, req, resp, *args, **kwargs) |  | ||||||
|             if "kerberos" in config.AUTHENTICATION_BACKENDS: |  | ||||||
|                 return kerberos_authenticate(resource, req, resp, *args, **kwargs) |  | ||||||
|             elif config.AUTHENTICATION_BACKENDS == {"pam"}: |  | ||||||
|                 return pam_authenticate(resource, req, resp, *args, **kwargs) |  | ||||||
|             elif config.AUTHENTICATION_BACKENDS == {"ldap"}: |  | ||||||
|                 return ldap_authenticate(resource, req, resp, *args, **kwargs) |  | ||||||
|             else: |  | ||||||
|                 raise NotImplementedError("Authentication backend %s not supported" % config.AUTHENTICATION_BACKENDS) |  | ||||||
|         return wrapped |         return wrapped | ||||||
|     return wrapper |     return wrapper | ||||||
|  |  | ||||||
| @@ -247,7 +218,6 @@ def authorize_admin(func): | |||||||
|     @whitelist_subnets(config.ADMIN_SUBNETS) |     @whitelist_subnets(config.ADMIN_SUBNETS) | ||||||
|     def wrapped(resource, req, resp, *args, **kwargs): |     def wrapped(resource, req, resp, *args, **kwargs): | ||||||
|         if req.context.get("user").is_admin(): |         if req.context.get("user").is_admin(): | ||||||
|             req.context["admin_authorized"] = True |  | ||||||
|             return func(resource, req, resp, *args, **kwargs) |             return func(resource, req, resp, *args, **kwargs) | ||||||
|         logger.info("User '%s' not authorized to access administrative API", req.context.get("user").name) |         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") |         raise falcon.HTTPForbidden("Forbidden", "User not authorized to perform administrative operations") | ||||||
|   | |||||||
| @@ -1,5 +1,6 @@ | |||||||
| from __future__ import division, absolute_import, print_function | from __future__ import division, absolute_import, print_function | ||||||
| import click | import click | ||||||
|  | import logging | ||||||
| import os | import os | ||||||
| import re | import re | ||||||
| import requests | import requests | ||||||
| @@ -20,6 +21,7 @@ from jinja2 import Template | |||||||
| from random import SystemRandom | from random import SystemRandom | ||||||
| from xattr import getxattr, listxattr, setxattr | from xattr import getxattr, listxattr, setxattr | ||||||
|  |  | ||||||
|  | logger = logging.getLogger(__name__) | ||||||
| random = SystemRandom() | random = SystemRandom() | ||||||
|  |  | ||||||
| try: | try: | ||||||
| @@ -214,7 +216,7 @@ def store_request(buf, overwrite=False, address="", user=""): | |||||||
|     return request_path, csr, common_name |     return request_path, csr, common_name | ||||||
|  |  | ||||||
|  |  | ||||||
| def revoke(common_name, reason): | def revoke(common_name, reason, user="root"): | ||||||
|     """ |     """ | ||||||
|     Revoke valid certificate |     Revoke valid certificate | ||||||
|     """ |     """ | ||||||
| @@ -228,18 +230,13 @@ def revoke(common_name, reason): | |||||||
|     setxattr(signed_path, "user.revocation.reason", reason) |     setxattr(signed_path, "user.revocation.reason", reason) | ||||||
|     revoked_path = os.path.join(config.REVOKED_DIR, "%040x.pem" % cert.serial_number) |     revoked_path = os.path.join(config.REVOKED_DIR, "%040x.pem" % cert.serial_number) | ||||||
|  |  | ||||||
|  |     logger.info("Revoked certificate %s by %s", common_name, user) | ||||||
|  |  | ||||||
|     os.unlink(os.path.join(config.SIGNED_BY_SERIAL_DIR, "%040x.pem" % cert.serial_number)) |     os.unlink(os.path.join(config.SIGNED_BY_SERIAL_DIR, "%040x.pem" % cert.serial_number)) | ||||||
|     os.rename(signed_path, revoked_path) |     os.rename(signed_path, revoked_path) | ||||||
|  |  | ||||||
|  |  | ||||||
|     push.publish("certificate-revoked", common_name) |     push.publish("certificate-revoked", common_name) | ||||||
|  |  | ||||||
|     # Publish CRL for long polls |  | ||||||
|     url = config.LONG_POLL_PUBLISH % "crl" |  | ||||||
|     click.echo("Publishing CRL at %s ..." % url) |  | ||||||
|     requests.post(url, data=export_crl(), |  | ||||||
|         headers={"User-Agent": "Certidude API", "Content-Type": "application/x-pem-file"}) |  | ||||||
|  |  | ||||||
|     attach_cert = buf, "application/x-pem-file", common_name + ".crt" |     attach_cert = buf, "application/x-pem-file", common_name + ".crt" | ||||||
|     mailer.send("certificate-revoked.md", |     mailer.send("certificate-revoked.md", | ||||||
|         attachments=(attach_cert,), |         attachments=(attach_cert,), | ||||||
| @@ -334,7 +331,7 @@ def export_crl(pem=True): | |||||||
|     return certificate_list.dump() |     return certificate_list.dump() | ||||||
|  |  | ||||||
|  |  | ||||||
| def delete_request(common_name): | def delete_request(common_name, user="root"): | ||||||
|     # Validate CN |     # Validate CN | ||||||
|     if not re.match(const.RE_COMMON_NAME, common_name): |     if not re.match(const.RE_COMMON_NAME, common_name): | ||||||
|         raise ValueError("Invalid common name") |         raise ValueError("Invalid common name") | ||||||
| @@ -342,6 +339,9 @@ def delete_request(common_name): | |||||||
|     path, buf, csr, submitted = get_request(common_name) |     path, buf, csr, submitted = get_request(common_name) | ||||||
|     os.unlink(path) |     os.unlink(path) | ||||||
|  |  | ||||||
|  |     logger.info("Rejected signing request %s by %s" % ( | ||||||
|  |         common_name, user)) | ||||||
|  |  | ||||||
|     # Publish event at CA channel |     # Publish event at CA channel | ||||||
|     push.publish("request-deleted", common_name) |     push.publish("request-deleted", common_name) | ||||||
|  |  | ||||||
| @@ -350,7 +350,7 @@ def delete_request(common_name): | |||||||
|         config.LONG_POLL_PUBLISH % hashlib.sha256(buf).hexdigest(), |         config.LONG_POLL_PUBLISH % hashlib.sha256(buf).hexdigest(), | ||||||
|         headers={"User-Agent": "Certidude API"}) |         headers={"User-Agent": "Certidude API"}) | ||||||
|  |  | ||||||
| def sign(common_name, profile, skip_notify=False, skip_push=False, overwrite=False, signer=None): | def sign(common_name, profile, skip_notify=False, skip_push=False, overwrite=False, signer="root"): | ||||||
|     """ |     """ | ||||||
|     Sign certificate signing request by it's common name |     Sign certificate signing request by it's common name | ||||||
|     """ |     """ | ||||||
|   | |||||||
| @@ -1018,26 +1018,28 @@ def certidude_setup_authority(username, kerberos_keytab, nginx_config, organizat | |||||||
|         click.echo("Not attempting to install packages from APT as requested...") |         click.echo("Not attempting to install packages from APT as requested...") | ||||||
|     else: |     else: | ||||||
|         click.echo("Installing packages...") |         click.echo("Installing packages...") | ||||||
|         os.system("DEBIAN_FRONTEND=noninteractive apt-get install -qq -y \ |         cmd = "DEBIAN_FRONTEND=noninteractive apt-get install -qq -y \ | ||||||
|             cython3 python3-dev python3-mimeparse \ |             cython3 python3-dev \ | ||||||
|             python3-markdown python3-pyxattr python3-jinja2 python3-cffi \ |             python3-markdown python3-pyxattr python3-jinja2 python3-cffi \ | ||||||
|             software-properties-common libsasl2-modules-gssapi-mit npm nodejs \ |             software-properties-common libsasl2-modules-gssapi-mit npm nodejs \ | ||||||
|             libkrb5-dev libldap2-dev libsasl2-dev gawk libncurses5-dev \ |             libkrb5-dev libldap2-dev libsasl2-dev gawk libncurses5-dev \ | ||||||
|             rsync attr wget unzip") |             rsync attr wget unzip" | ||||||
|         os.system("pip3 install -q --upgrade gssapi falcon humanize ipaddress simplepam") |         click.echo("Running: %s" % cmd) | ||||||
|         os.system("pip3 install -q --pre --upgrade python-ldap") |         if os.system(cmd): sys.exit(254) | ||||||
|  |         if os.system("pip3 install -q --upgrade gssapi falcon humanize ipaddress simplepam user-agents"): sys.exit(253) | ||||||
|  |         if os.system("pip3 install -q --pre --upgrade python-ldap"): exit(252) | ||||||
|  |  | ||||||
|         if not os.path.exists("/usr/lib/nginx/modules/ngx_nchan_module.so"): |         if not os.path.exists("/usr/lib/nginx/modules/ngx_nchan_module.so"): | ||||||
|             click.echo("Enabling nginx PPA") |             click.echo("Enabling nginx PPA") | ||||||
|             os.system("add-apt-repository -y ppa:nginx/stable") |             if os.system("add-apt-repository -y ppa:nginx/stable"): sys.exit(251) | ||||||
|             os.system("apt-get update -q") |             if os.system("apt-get update -q"): sys.exit(250) | ||||||
|             os.system("apt-get install -y -q libnginx-mod-nchan") |             if os.system("apt-get install -y -q libnginx-mod-nchan"): sys.exit(249) | ||||||
|         else: |         else: | ||||||
|             click.echo("PPA for nginx already enabled") |             click.echo("PPA for nginx already enabled") | ||||||
|  |  | ||||||
|         if not os.path.exists("/usr/sbin/nginx"): |         if not os.path.exists("/usr/sbin/nginx"): | ||||||
|             click.echo("Installing nginx from PPA") |             click.echo("Installing nginx from PPA") | ||||||
|             os.system("apt-get install -y -q nginx") |             if os.system("apt-get install -y -q nginx"): sys.exit(248) | ||||||
|         else: |         else: | ||||||
|             click.echo("Web server nginx already installed") |             click.echo("Web server nginx already installed") | ||||||
|  |  | ||||||
| @@ -1160,16 +1162,16 @@ def certidude_setup_authority(username, kerberos_keytab, nginx_config, organizat | |||||||
|         else: |         else: | ||||||
|             cmd = "npm install --silent -g nunjucks@2.5.2 nunjucks-date@1.2.0 node-forge bootstrap@4.0.0-alpha.6 jquery timeago tether font-awesome qrcode-svg" |             cmd = "npm install --silent -g nunjucks@2.5.2 nunjucks-date@1.2.0 node-forge bootstrap@4.0.0-alpha.6 jquery timeago tether font-awesome qrcode-svg" | ||||||
|             click.echo("Installing JavaScript packages: %s" % cmd) |             click.echo("Installing JavaScript packages: %s" % cmd) | ||||||
|             assert os.system(cmd) == 0 |             if os.system(cmd): sys.exit(230) | ||||||
|  |  | ||||||
|         # Copy fonts |         # Copy fonts | ||||||
|         click.echo("Copying fonts...") |         click.echo("Copying fonts...") | ||||||
|         os.system("rsync -avq /usr/local/lib/node_modules/font-awesome/fonts/ %s/fonts/" % assets_dir) |         if os.system("rsync -avq /usr/local/lib/node_modules/font-awesome/fonts/ %s/fonts/" % assets_dir): sys.exit(229) | ||||||
|  |  | ||||||
|         # Compile nunjucks templates |         # Compile nunjucks templates | ||||||
|         cmd = 'nunjucks-precompile --include ".html$" --include ".ps1$" --include ".sh$" --include ".svg" %s > %s.part' % (static_path, bundle_js) |         cmd = 'nunjucks-precompile --include ".html$" --include ".ps1$" --include ".sh$" --include ".svg" %s > %s.part' % (static_path, bundle_js) | ||||||
|         click.echo("Compiling templates: %s" % cmd) |         click.echo("Compiling templates: %s" % cmd) | ||||||
|         assert os.system(cmd) == 0 |         if os.system(cmd): sys.exit(228) | ||||||
|  |  | ||||||
|         # Assemble bundle.js |         # Assemble bundle.js | ||||||
|         click.echo("Assembling %s" % bundle_js) |         click.echo("Assembling %s" % bundle_js) | ||||||
|   | |||||||
| @@ -44,6 +44,8 @@ OVERWRITE_SUBNETS = set([ipaddress.ip_network(j) for j in | |||||||
|     cp.get("authorization", "overwrite subnets").split(" ") if j]) |     cp.get("authorization", "overwrite subnets").split(" ") if j]) | ||||||
| MACHINE_ENROLLMENT_SUBNETS = set([ipaddress.ip_network(j) for j in | MACHINE_ENROLLMENT_SUBNETS = set([ipaddress.ip_network(j) for j in | ||||||
|     cp.get("authorization", "machine enrollment subnets").split(" ") if j]) |     cp.get("authorization", "machine enrollment subnets").split(" ") if j]) | ||||||
|  | KERBEROS_SUBNETS = set([ipaddress.ip_network(j) for j in | ||||||
|  |     cp.get("authorization", "kerberos subnets").split(" ") if j]) | ||||||
|  |  | ||||||
| AUTHORITY_DIR = "/var/lib/certidude" | AUTHORITY_DIR = "/var/lib/certidude" | ||||||
| AUTHORITY_PRIVATE_KEY_PATH = cp.get("authority", "private key path") | AUTHORITY_PRIVATE_KEY_PATH = cp.get("authority", "private key path") | ||||||
|   | |||||||
| @@ -1,4 +1,14 @@ | |||||||
|  |  | ||||||
|  | @keyframes fresh { | ||||||
|  |     from { background-color: #ffc107; } | ||||||
|  |     to { background-color: white; } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .fresh { | ||||||
|  |     animation-name: fresh; | ||||||
|  |     animation-duration: 30s; | ||||||
|  | } | ||||||
|  |  | ||||||
| .loader-container { | .loader-container { | ||||||
|     margin: 20% auto 0 auto; |     margin: 20% auto 0 auto; | ||||||
|     text-align: center; |     text-align: center; | ||||||
|   | |||||||
| @@ -15,15 +15,21 @@ | |||||||
|       <button class="navbar-toggler navbar-toggler-right" type="button" data-toggle="collapse" data-target="#navbarsExampleDefault" aria-controls="navbarsExampleDefault" aria-expanded="false" aria-label="Toggle navigation"> |       <button class="navbar-toggler navbar-toggler-right" type="button" data-toggle="collapse" data-target="#navbarsExampleDefault" aria-controls="navbarsExampleDefault" aria-expanded="false" aria-label="Toggle navigation"> | ||||||
|         <span class="navbar-toggler-icon"></span> |         <span class="navbar-toggler-icon"></span> | ||||||
|       </button> |       </button> | ||||||
|       <a class="navbar-brand" href="#">Certidude</a> |       <a class="navbar-brand" href="#columns=2">Certidude</a> | ||||||
|  |  | ||||||
|       <div class="collapse navbar-collapse" id="navbarsExampleDefault"> |       <div class="collapse navbar-collapse" id="navbarsExampleDefault"> | ||||||
|         <ul class="navbar-nav mr-auto"> |         <ul class="navbar-nav mr-auto"> | ||||||
|           <li class="nav-item"> |           <li class="nav-item"> | ||||||
|             <a class="nav-link disabled dashboard" href="#">Dashboard</a> |             <a class="nav-link disabled dashboard" href="#columns=2">Dashboard</a> | ||||||
|           </li> |           </li> | ||||||
|           <li class="nav-item"> |           <li class="nav-item"> | ||||||
|             <a class="nav-link disabled log" href="#">Log</a> |             <a class="nav-link" href="#columns=3">Wider</a> | ||||||
|  |           </li> | ||||||
|  |           <li class="nav-item"> | ||||||
|  |             <a class="nav-link" href="#columns=4">Widest</a> | ||||||
|  |           </li> | ||||||
|  |           <li class="nav-item"> | ||||||
|  |             <a class="nav-link" href="#">Log</a> | ||||||
|           </li> |           </li> | ||||||
|         </ul> |         </ul> | ||||||
|         <form class="form-inline my-2 my-lg-0"> |         <form class="form-inline my-2 my-lg-0"> | ||||||
|   | |||||||
| @@ -74,6 +74,7 @@ function onTagClicked(tag) { | |||||||
|             } |             } | ||||||
|         }); |         }); | ||||||
|     } |     } | ||||||
|  |     return false; | ||||||
| } | } | ||||||
|  |  | ||||||
| function onNewTagClicked(menu) { | function onNewTagClicked(menu) { | ||||||
| @@ -110,6 +111,7 @@ function onTagFilterChanged() { | |||||||
| function onLogEntry (e) { | function onLogEntry (e) { | ||||||
|     if (e.data) { |     if (e.data) { | ||||||
|         e = JSON.parse(e.data); |         e = JSON.parse(e.data); | ||||||
|  |         e.fresh = true; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     if ($("#log-level-" + e.severity).prop("checked")) { |     if ($("#log-level-" + e.severity).prop("checked")) { | ||||||
| @@ -117,7 +119,8 @@ function onLogEntry (e) { | |||||||
|             entry: { |             entry: { | ||||||
|                 created: new Date(e.created).toLocaleString(), |                 created: new Date(e.created).toLocaleString(), | ||||||
|                 message: e.message, |                 message: e.message, | ||||||
|                 severity: e.severity |                 severity: e.severity, | ||||||
|  |                 fresh: e.fresh, | ||||||
|             } |             } | ||||||
|         })); |         })); | ||||||
|     } |     } | ||||||
| @@ -262,7 +265,7 @@ function onServerStarted() { | |||||||
| } | } | ||||||
|  |  | ||||||
| function onServerStopped() { | function onServerStopped() { | ||||||
|     $("view").html('<div class="loader"></div><p>Server under maintenance</p>'); |     $("#view-dashboard").html('<div class="loader"></div><p>Server under maintenance</p>'); | ||||||
|     console.info("Server stopped"); |     console.info("Server stopped"); | ||||||
|  |  | ||||||
| } | } | ||||||
|   | |||||||
							
								
								
									
										25
									
								
								certidude/static/snippets/certidude-client.sh
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										25
									
								
								certidude/static/snippets/certidude-client.sh
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,25 @@ | |||||||
|  | pip3 install git+https://github.com/laurivosandi/certidude/ | ||||||
|  | mkdir -p /etc/certidude/{client.conf.d,services.conf.d} | ||||||
|  | cat << EOF > /etc/certidude/client.conf.d/{{ authority_name }}.conf | ||||||
|  | [{{ authority_name }}] | ||||||
|  | trigger = interface up | ||||||
|  | common name = $HOSTNAME | ||||||
|  | system wide = true | ||||||
|  | EOF | ||||||
|  |  | ||||||
|  | cat << EOF > /etc/certidude/services.conf.d/{{ authority_name }}.conf | ||||||
|  | {% for router in session.service.routers %}{% if "ikev2" in session.service.protocols %} | ||||||
|  | [IPSec to {{ router }}] | ||||||
|  | authority = {{ authority_name }} | ||||||
|  | service = network-manager/strongswan | ||||||
|  | remote = {{ router }} | ||||||
|  | {% endif %}{% if "openvpn" in session.service.protocols %} | ||||||
|  | [OpenVPN to {{ router }}] | ||||||
|  | authority = {{ authority_name }} | ||||||
|  | service = network-manager/openvpn | ||||||
|  | remote = {{ router }} | ||||||
|  | {% endif %}{% endfor %} | ||||||
|  | EOF | ||||||
|  |  | ||||||
|  | certidude enroll | ||||||
|  |  | ||||||
| @@ -7,6 +7,12 @@ | |||||||
|       </div> |       </div> | ||||||
|       <form action="/api/request/" method="post"> |       <form action="/api/request/" method="post"> | ||||||
|         <div class="modal-body"> |         <div class="modal-body"> | ||||||
|  |           <h5>Certidude client</h5> | ||||||
|  |           <p>On Ubuntu or Fedora:</p> | ||||||
|  |           <div class="highlight"> | ||||||
|  |             <pre class="code"><code>{% include "snippets/certidude-client.sh" %}</code></pre> | ||||||
|  |           </div> | ||||||
|  |  | ||||||
|           {% if "ikev2" in session.service.protocols %} |           {% if "ikev2" in session.service.protocols %} | ||||||
|             <h5>Windows {% if session.authority.certificate.algorithm == "ec" %}10{% else %}7 and up{% endif %}</h5> |             <h5>Windows {% if session.authority.certificate.algorithm == "ec" %}10{% else %}7 and up{% endif %}</h5> | ||||||
|             <p>On Windows execute following PowerShell script</p> |             <p>On Windows execute following PowerShell script</p> | ||||||
| @@ -190,6 +196,8 @@ curl http://{{authority_name}}/api/revoked/?wait=yes -L -H "Accept: application/ | |||||||
|        {% endfor %}. |        {% endfor %}. | ||||||
|     {% endif %} |     {% endif %} | ||||||
|  |  | ||||||
|  |     See <a href="#request_submission_modal" data-toggle="modal">here</a> for more information on manual signing request upload. | ||||||
|  |  | ||||||
|     {% if session.authority.autosign_subnets %} |     {% if session.authority.autosign_subnets %} | ||||||
|         {% if "0.0.0.0/0" in session.authority.autosign_subnets %} |         {% if "0.0.0.0/0" in session.authority.autosign_subnets %} | ||||||
|             All requests are automatically signed. |             All requests are automatically signed. | ||||||
| @@ -202,17 +210,16 @@ curl http://{{authority_name}}/api/revoked/?wait=yes -L -H "Accept: application/ | |||||||
|         {% endif %} |         {% endif %} | ||||||
|     {% endif %} |     {% endif %} | ||||||
|     </p> |     </p> | ||||||
|  |  | ||||||
|   {% if columns >= 3 %} |  | ||||||
|   </div> |  | ||||||
|   <div class="col-sm-{{ column_width }}"> |  | ||||||
|   {% endif %} |  | ||||||
|     <div id="pending_requests"> |     <div id="pending_requests"> | ||||||
|       {% for request in session.authority.requests | sort(attribute="submitted", reverse=true) %} |       {% for request in session.authority.requests | sort(attribute="submitted", reverse=true) %} | ||||||
|         {% include "views/request.html" %} |         {% include "views/request.html" %} | ||||||
|       {% endfor %} |       {% endfor %} | ||||||
|     </div> |     </div> | ||||||
|     <p><h1>Revoked certificates</h1></p> |   {% if columns >= 3 %} | ||||||
|  |   </div> | ||||||
|  |   <div class="col-sm-{{ column_width }}"> | ||||||
|  |   {% endif %} | ||||||
|  |     <h1>Revoked certificates</h1> | ||||||
|     <p>Following certificates have been revoked{% if session.features.crl %}, for more information click |     <p>Following certificates have been revoked{% if session.features.crl %}, for more information click | ||||||
|     <a href="#revocation_list_modal" data-toggle="modal">here</a>{% endif %}.</p> |     <a href="#revocation_list_modal" data-toggle="modal">here</a>{% endif %}.</p> | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,4 +1,4 @@ | |||||||
| <i class="fa fa-circle" style="color:{% if certificate.lease.age > 172800 %}#d9534f{% else %}{% if certificate.lease.age > 7200 %}#0275d8{% else %}#5cb85c{% endif %}{% endif %};"/> | <i class="fa fa-circle" style="color:{% if certificate.lease.age > 172800 %}#d9534f{% else %}{% if certificate.lease.age > 10800 %}#0275d8{% else %}#5cb85c{% endif %}{% endif %};"/> | ||||||
| Last seen | Last seen | ||||||
| <time class="timeago" datetime="{{ certificate.lease.last_seen }}">{{ certificate.lease.last_seen }}</time> | <time class="timeago" datetime="{{ certificate.lease.last_seen }}">{{ certificate.lease.last_seen }}</time> | ||||||
| at | at | ||||||
|   | |||||||
| @@ -1,4 +1,4 @@ | |||||||
| <li id="log_entry_{{ entry.id }}" class="list-group-item justify-content-between filterable"> | <li id="log_entry_{{ entry.id }}" class="list-group-item justify-content-between filterable{% if entry.fresh %} fresh{% endif %}"> | ||||||
| <span> | <span> | ||||||
| <i class="fa fa-{{ entry.severity }}-circle"/> | <i class="fa fa-{{ entry.severity }}-circle"/> | ||||||
| {{ entry.message }} | {{ entry.message }} | ||||||
|   | |||||||
| @@ -4,14 +4,15 @@ | |||||||
| # sshd PAM service. In case of 'kerberos' SPNEGO is used to authenticate | # sshd PAM service. In case of 'kerberos' SPNEGO is used to authenticate | ||||||
| # user against eg. Active Directory or Samba4. | # user against eg. Active Directory or Samba4. | ||||||
|  |  | ||||||
| {% if realm %} |  | ||||||
| ;backends = pam |  | ||||||
| backends = kerberos |  | ||||||
| {% else %} |  | ||||||
| backends = pam |  | ||||||
| ;backends = kerberos |  | ||||||
| {% endif %} |  | ||||||
| ;backends = ldap | ;backends = ldap | ||||||
|  | ;backends = kerberos | ||||||
|  | {% if realm %} | ||||||
|  | backends = kerberos ldap | ||||||
|  | ;backends = pam | ||||||
|  | {% else %} | ||||||
|  | ;backends = kerberos ldap | ||||||
|  | backends = pam | ||||||
|  | {% endif %} | ||||||
|  |  | ||||||
| kerberos keytab = FILE:{{ kerberos_keytab }} | kerberos keytab = FILE:{{ kerberos_keytab }} | ||||||
| {% if realm %} | {% if realm %} | ||||||
| @@ -103,9 +104,6 @@ admin whitelist = | |||||||
| # Users are allowed to log in from user subnets | # Users are allowed to log in from user subnets | ||||||
| user subnets = 0.0.0.0/0 | user subnets = 0.0.0.0/0 | ||||||
|  |  | ||||||
| # Authority administrators are allowed to sign and revoke certificates from these subnets |  | ||||||
| admin subnets = 0.0.0.0/0 |  | ||||||
|  |  | ||||||
| # Certificate signing requests are allowed to be submitted from these subnets | # Certificate signing requests are allowed to be submitted from these subnets | ||||||
| request subnets = 0.0.0.0/0 | request subnets = 0.0.0.0/0 | ||||||
|  |  | ||||||
| @@ -135,6 +133,14 @@ renewal subnets = | |||||||
| overwrite subnets = | overwrite subnets = | ||||||
| ;overwrite subnets = 0.0.0.0/0 | ;overwrite subnets = 0.0.0.0/0 | ||||||
|  |  | ||||||
|  |  | ||||||
|  | # Which subnets are offered Kerberos authentication, eg. | ||||||
|  | # subnet for Windows workstations or slice of VPN subnet where | ||||||
|  | # workstations are assigned to | ||||||
|  | kerberos subnets = 0.0.0.0 | ||||||
|  | ;kerberos subnets = | ||||||
|  |  | ||||||
|  |  | ||||||
| # Source subnets of Kerberos authenticated machines which are automatically | # Source subnets of Kerberos authenticated machines which are automatically | ||||||
| # allowed to enroll with CSR whose common name is set to machine's account name. | # allowed to enroll with CSR whose common name is set to machine's account name. | ||||||
| # Note that overwriting is not allowed by default, see 'overwrite subnets' | # Note that overwriting is not allowed by default, see 'overwrite subnets' | ||||||
| @@ -142,6 +148,13 @@ overwrite subnets = | |||||||
| machine enrollment subnets = | machine enrollment subnets = | ||||||
| ;machine enrollment subnets = 0.0.0.0/0 | ;machine enrollment subnets = 0.0.0.0/0 | ||||||
|  |  | ||||||
|  |  | ||||||
|  | # Authenticated users belonging to administrative LDAP or POSIX group | ||||||
|  | # are allowed to sign and revoke certificates from these subnets | ||||||
|  | admin subnets = 0.0.0.0/0 | ||||||
|  | ;admin subnets = 172.20.7.0/24 172.20.8.5 | ||||||
|  |  | ||||||
|  |  | ||||||
| [logging] | [logging] | ||||||
| # Disable logging | # Disable logging | ||||||
| backend = | backend = | ||||||
|   | |||||||
| @@ -169,6 +169,12 @@ def test_cli_setup_authority(): | |||||||
|     if not os.path.exists("/etc/pki/ca-trust/source/anchors/"): |     if not os.path.exists("/etc/pki/ca-trust/source/anchors/"): | ||||||
|         os.makedirs("/etc/pki/ca-trust/source/anchors/") |         os.makedirs("/etc/pki/ca-trust/source/anchors/") | ||||||
|  |  | ||||||
|  |     if not os.path.exists("/bin/systemctl"): | ||||||
|  |         with open("/usr/bin/systemctl", "w") as fh: | ||||||
|  |             fh.write("#!/bin/bash\n") | ||||||
|  |             fh.write("service $2 $1\n") | ||||||
|  |         os.chmod("/usr/bin/systemctl", 0o755) | ||||||
|  |  | ||||||
|     # Back up original DNS server |     # Back up original DNS server | ||||||
|     if not os.path.exists("/etc/resolv.conf.orig"): |     if not os.path.exists("/etc/resolv.conf.orig"): | ||||||
|         shutil.copyfile("/etc/resolv.conf", "/etc/resolv.conf.orig") |         shutil.copyfile("/etc/resolv.conf", "/etc/resolv.conf.orig") | ||||||
| @@ -205,7 +211,7 @@ def test_cli_setup_authority(): | |||||||
|     assert const.HOSTNAME == "ca" |     assert const.HOSTNAME == "ca" | ||||||
|     assert const.DOMAIN == "example.lan" |     assert const.DOMAIN == "example.lan" | ||||||
|  |  | ||||||
|     os.system("certidude setup authority --elliptic-curve") |     assert os.system("certidude setup authority --elliptic-curve") == 0 | ||||||
|  |  | ||||||
|     assert_cleanliness() |     assert_cleanliness() | ||||||
|  |  | ||||||
| @@ -289,13 +295,7 @@ def test_cli_setup_authority(): | |||||||
|     assert r.status_code == 400, r.text |     assert r.status_code == 400, r.text | ||||||
|  |  | ||||||
|     r = client().simulate_get("/") |     r = client().simulate_get("/") | ||||||
|     assert r.status_code == 200, r.text |     assert r.status_code == 404, r.text # backend doesn't serve static | ||||||
|     r = client().simulate_get("/index.html") |  | ||||||
|     assert r.status_code == 200, r.text |  | ||||||
|     r = client().simulate_get("/nonexistant.html") |  | ||||||
|     assert r.status_code == 404, r.text |  | ||||||
|     r = client().simulate_get("/../nonexistant.html") |  | ||||||
|     assert r.status_code == 400, r.text |  | ||||||
|  |  | ||||||
|     # Test request submission |     # Test request submission | ||||||
|     buf = generate_csr(cn="test") |     buf = generate_csr(cn="test") | ||||||
| @@ -440,11 +440,6 @@ def test_cli_setup_authority(): | |||||||
|         headers={"Accept":"text/plain"}) |         headers={"Accept":"text/plain"}) | ||||||
|     assert r.status_code == 415, r.text |     assert r.status_code == 415, r.text | ||||||
|  |  | ||||||
|     r = client().simulate_get("/api/revoked/", |  | ||||||
|         query_string="wait=true", |  | ||||||
|         headers={"Accept":"application/x-pem-file"}) |  | ||||||
|     assert r.status_code == 303, r.text |  | ||||||
|  |  | ||||||
|     # Test attribute fetching API call |     # Test attribute fetching API call | ||||||
|     r = client().simulate_get("/api/signed/test/attr/") |     r = client().simulate_get("/api/signed/test/attr/") | ||||||
|     assert r.status_code == 401, r.text |     assert r.status_code == 401, r.text | ||||||
| @@ -1114,22 +1109,23 @@ def test_cli_setup_authority(): | |||||||
|  |  | ||||||
|     # Bootstrap authority |     # Bootstrap authority | ||||||
|     assert not os.path.exists("/var/lib/certidude/ca.example.lan/ca_key.pem") |     assert not os.path.exists("/var/lib/certidude/ca.example.lan/ca_key.pem") | ||||||
|     os.system("certidude setup authority --skip-packages") |     assert os.system("certidude setup authority --skip-packages") == 0 | ||||||
|  |  | ||||||
|  |  | ||||||
|     # Make modifications to /etc/certidude/server.conf so |     # Make modifications to /etc/certidude/server.conf so | ||||||
|     # Certidude would auth against domain controller |     # Certidude would auth against domain controller | ||||||
|     os.system("sed -e 's/ldap uri = ldaps:.*/ldap uri = ldaps:\\/\\/ca.example.lan/g' -i /etc/certidude/server.conf") |     assert os.system("sed -e 's/ldap uri = ldaps:.*/ldap uri = ldaps:\\/\\/ca.example.lan/g' -i /etc/certidude/server.conf") == 0 | ||||||
|     os.system("sed -e 's/ldap uri = ldap:.*/ldap uri = ldap:\\/\\/ca.example.lan/g' -i /etc/certidude/server.conf") |     assert os.system("sed -e 's/ldap uri = ldap:.*/ldap uri = ldap:\\/\\/ca.example.lan/g' -i /etc/certidude/server.conf") == 0 | ||||||
|     os.system("sed -e 's/autosign subnets =.*/autosign subnets =/g' -i /etc/certidude/server.conf") |     assert os.system("sed -e 's/autosign subnets =.*/autosign subnets =/g' -i /etc/certidude/server.conf") == 0 | ||||||
|     os.system("sed -e 's/machine enrollment subnets =.*/machine enrollment subnets = 0.0.0.0\\/0/g' -i /etc/certidude/server.conf") |     assert os.system("sed -e 's/machine enrollment subnets =.*/machine enrollment subnets = 0.0.0.0\\/0/g' -i /etc/certidude/server.conf") == 0 | ||||||
|     os.system("sed -e 's/scep subnets =.*/scep subnets = 0.0.0.0\\/0/g' -i /etc/certidude/server.conf") |     assert os.system("sed -e 's/scep subnets =.*/scep subnets = 0.0.0.0\\/0/g' -i /etc/certidude/server.conf") == 0 | ||||||
|     os.system("sed -e 's/ocsp subnets =.*/ocsp subnets =/g' -i /etc/certidude/server.conf") |     assert os.system("sed -e 's/ocsp subnets =.*/ocsp subnets =/g' -i /etc/certidude/server.conf") == 0 | ||||||
|     os.system("sed -e 's/crl subnets =.*/crl subnets =/g' -i /etc/certidude/server.conf") |     assert os.system("sed -e 's/crl subnets =.*/crl subnets =/g' -i /etc/certidude/server.conf") == 0 | ||||||
|     os.system("sed -e 's/address = certificates@example.lan/address =/g' -i /etc/certidude/server.conf") |     assert os.system("sed -e 's/address = certificates@example.lan/address =/g' -i /etc/certidude/server.conf") == 0 | ||||||
|  |     assert os.system("sed -e 's/kerberos subnets =.*/kerberos subnets = 0.0.0.0\\/0/g' -i /etc/certidude/server.conf") == 0 | ||||||
|  |  | ||||||
|     # Update server credential cache |     # Update server credential cache | ||||||
|     os.system("sed -e 's/dc1/ca/g' -i /etc/cron.hourly/certidude") |     assert os.system("sed -e 's/dc1/ca/g' -i /etc/cron.hourly/certidude") == 0 | ||||||
|     with open("/etc/cron.hourly/certidude") as fh: |     with open("/etc/cron.hourly/certidude") as fh: | ||||||
|         cronjob = fh.read() |         cronjob = fh.read() | ||||||
|         assert "ldap/ca.example.lan" in cronjob, cronjob |         assert "ldap/ca.example.lan" in cronjob, cronjob | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user