From 6e50c85c8587d3e800d01ce1d1588965c29b2c46 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lauri=20V=C3=B5sandi?= Date: Fri, 5 Oct 2018 10:41:40 +0300 Subject: [PATCH] Move to pre-forking model for backend API-s --- certidude/api/__init__.py | 216 ++++++++++++------ certidude/api/log.py | 2 +- certidude/api/revoked.py | 15 ++ certidude/api/session.py | 8 +- certidude/authority.py | 2 +- certidude/cli.py | 162 ++++++------- certidude/common.py | 5 +- certidude/const.py | 1 - certidude/static/502.json | 2 +- .../server/housekeeping-daily.service | 7 + .../templates/server/housekeeping-daily.timer | 6 + certidude/templates/server/nginx.conf | 20 ++ certidude/templates/server/responder.service | 0 certidude/templates/server/systemd.service | 17 -- .../snippets/nginx-ocsp-cache.service | 4 +- requirements.txt | 1 + tests/test_cli.py | 18 +- 17 files changed, 289 insertions(+), 197 deletions(-) create mode 100644 certidude/templates/server/housekeeping-daily.service create mode 100644 certidude/templates/server/housekeeping-daily.timer create mode 100644 certidude/templates/server/responder.service delete mode 100644 certidude/templates/server/systemd.service diff --git a/certidude/api/__init__.py b/certidude/api/__init__.py index 9039002..f1f50e6 100644 --- a/certidude/api/__init__.py +++ b/certidude/api/__init__.py @@ -2,10 +2,13 @@ import falcon import ipaddress +import logging import os from certidude import config +from certidude.common import drop_privileges from user_agents import parse - +from wsgiref.simple_server import make_server, WSGIServer +from setproctitle import setproctitle class NormalizeMiddleware(object): def process_request(self, req, resp, *args): @@ -15,89 +18,168 @@ class NormalizeMiddleware(object): else: req.context["user_agent"] = "Unknown user agent" -def certidude_app(log_handlers=[]): - from certidude import authority, config - from certidude.tokens import TokenManager - from .signed import SignedCertificateDetailResource - from .request import RequestListResource, RequestDetailResource - from .lease import LeaseResource, LeaseDetailResource - from .script import ScriptResource - from .tag import TagResource, TagDetailResource - from .attrib import AttributeResource - from .bootstrap import BootstrapResource - from .token import TokenResource - from .builder import ImageBuilderResource - from .session import SessionResource, CertificateAuthorityResource - app = falcon.API(middleware=NormalizeMiddleware()) - app.req_options.auto_parse_form_urlencoded = True +class App(object): + PORT = 8080 + FORKS = None + DROP_PRIVILEGES = True - # Certificate authority API calls - app.add_route("/api/certificate/", CertificateAuthorityResource()) - app.add_route("/api/signed/{cn}/", SignedCertificateDetailResource(authority)) - app.add_route("/api/request/{cn}/", RequestDetailResource(authority)) - app.add_route("/api/request/", RequestListResource(authority)) + def __init__(self): + app = falcon.API(middleware=NormalizeMiddleware()) + app.req_options.auto_parse_form_urlencoded = True + self.attach(app) - token_resource = None - token_manager = None - if config.USER_ENROLLMENT_ALLOWED: # TODO: add token enable/disable flag for config - if config.TOKEN_BACKEND == "sql": - token_manager = TokenManager(config.TOKEN_DATABASE) - token_resource = TokenResource(authority, token_manager) - app.add_route("/api/token/", token_resource) - elif not config.TOKEN_BACKEND: - pass + # Set up log handlers + log_handlers = [] + if config.LOGGING_BACKEND == "sql": + from certidude.mysqllog import LogHandler + from certidude.api.log import LogResource + uri = config.cp.get("logging", "database") + log_handlers.append(LogHandler(uri)) + elif config.LOGGING_BACKEND == "syslog": + from logging.handlers import SysLogHandler + log_handlers.append(SysLogHandler()) + # Browsing syslog via HTTP is obviously not possible out of the box + elif config.LOGGING_BACKEND: + raise ValueError("Invalid logging.backend = %s" % config.LOGGING_BACKEND) + from certidude.push import EventSourceLogHandler + log_handlers.append(EventSourceLogHandler()) + + for j in logging.Logger.manager.loggerDict.values(): + if isinstance(j, logging.Logger): # PlaceHolder is what? + if j.name.startswith("certidude."): + j.setLevel(logging.DEBUG) + for handler in log_handlers: + j.addHandler(handler) + + self.server = make_server("127.0.1.1", self.PORT, app, WSGIServer) + setproctitle("certidude: %s" % self.NAME) + + def run(self): + if self.DROP_PRIVILEGES: + drop_privileges() + try: + self.server.serve_forever() + except KeyboardInterrupt: + return else: - raise NotImplementedError("Token backend '%s' not supported" % config.TOKEN_BACKEND) + return - app.add_route("/api/", SessionResource(authority, token_manager)) + def fork(self): + for j in range(self.FORKS): + if not os.fork(): + self.run() + return True + return False - # Extended attributes for scripting etc. - app.add_route("/api/signed/{cn}/attr/", AttributeResource(authority, namespace="machine")) - app.add_route("/api/signed/{cn}/script/", ScriptResource(authority)) - # API calls used by pushed events on the JS end - app.add_route("/api/signed/{cn}/tag/", TagResource(authority)) - app.add_route("/api/signed/{cn}/lease/", LeaseDetailResource(authority)) - # API call used to delete existing tags - app.add_route("/api/signed/{cn}/tag/{tag}/", TagDetailResource(authority)) +class ReadWriteApp(App): + NAME = "backend server" - # Gateways can submit leases via this API call - app.add_route("/api/lease/", LeaseResource(authority)) + def attach(self, app): + from certidude import authority, config + from certidude.tokens import TokenManager + from .signed import SignedCertificateDetailResource + from .request import RequestListResource, RequestDetailResource + from .lease import LeaseResource, LeaseDetailResource + from .script import ScriptResource + from .tag import TagResource, TagDetailResource + from .attrib import AttributeResource + from .bootstrap import BootstrapResource + from .token import TokenResource + from .session import SessionResource, CertificateAuthorityResource + from .revoked import RevokedCertificateDetailResource - # Bootstrap resource - app.add_route("/api/bootstrap/", BootstrapResource(authority)) + # Certificate authority API calls + app.add_route("/api/certificate/", CertificateAuthorityResource()) + app.add_route("/api/signed/{cn}/", SignedCertificateDetailResource(authority)) + app.add_route("/api/request/{cn}/", RequestDetailResource(authority)) + app.add_route("/api/request/", RequestListResource(authority)) + app.add_route("/api/revoked/{serial_number}/", RevokedCertificateDetailResource(authority)) - # LEDE image builder resource - app.add_route("/api/build/{profile}/{suggested_filename}", ImageBuilderResource()) + token_resource = None + token_manager = None + if config.USER_ENROLLMENT_ALLOWED: # TODO: add token enable/disable flag for config + if config.TOKEN_BACKEND == "sql": + token_manager = TokenManager(config.TOKEN_DATABASE) + token_resource = TokenResource(authority, token_manager) + app.add_route("/api/token/", token_resource) + elif not config.TOKEN_BACKEND: + pass + else: + raise NotImplementedError("Token backend '%s' not supported" % config.TOKEN_BACKEND) - # Add CRL handler if we have any whitelisted subnets - if config.CRL_SUBNETS: - from .revoked import RevocationListResource - app.add_route("/api/revoked/", RevocationListResource(authority)) + app.add_route("/api/", SessionResource(authority, token_manager)) - # Add SCEP handler if we have any whitelisted subnets - if config.SCEP_SUBNETS: - from .scep import SCEPResource - app.add_route("/api/scep/", SCEPResource(authority)) + # Extended attributes for scripting etc. + app.add_route("/api/signed/{cn}/attr/", AttributeResource(authority, namespace="machine")) + app.add_route("/api/signed/{cn}/script/", ScriptResource(authority)) - if config.OCSP_SUBNETS: + # API calls used by pushed events on the JS end + app.add_route("/api/signed/{cn}/tag/", TagResource(authority)) + app.add_route("/api/signed/{cn}/lease/", LeaseDetailResource(authority)) + + # API call used to delete existing tags + app.add_route("/api/signed/{cn}/tag/{tag}/", TagDetailResource(authority)) + + # Gateways can submit leases via this API call + app.add_route("/api/lease/", LeaseResource(authority)) + + # Bootstrap resource + app.add_route("/api/bootstrap/", BootstrapResource(authority)) + + # Add SCEP handler if we have any whitelisted subnets + if config.SCEP_SUBNETS: + from .scep import SCEPResource + app.add_route("/api/scep/", SCEPResource(authority)) + return app + + +class ResponderApp(App): + PORT = 8081 + FORKS = 4 + NAME = "ocsp responder" + + def attach(self, app): + from certidude import authority from .ocsp import OCSPResource app.add_sink(OCSPResource(authority), prefix="/api/ocsp") + return app - # Set up log handlers - if config.LOGGING_BACKEND == "sql": - from certidude.mysqllog import LogHandler + +class RevocationListApp(App): + PORT = 8082 + FORKS = 2 + NAME = "crl server" + + def attach(self, app): + from certidude import authority + from .revoked import RevocationListResource + app.add_route("/api/revoked/", RevocationListResource(authority)) + return app + + +class BuilderApp(App): + PORT = 8083 + FORKS = 1 + NAME = "image builder" + + def attach(self, app): + # LEDE image builder resource + from certidude import authority + from .builder import ImageBuilderResource + app.add_route("/api/build/{profile}/{suggested_filename}", ImageBuilderResource()) + return app + + +class LogApp(App): + PORT = 8084 + FORKS = 2 + NAME = "log server" + + def attach(self, app): from certidude.api.log import LogResource uri = config.cp.get("logging", "database") - log_handlers.append(LogHandler(uri)) app.add_route("/api/log/", LogResource(uri)) - elif config.LOGGING_BACKEND == "syslog": - from logging.handlers import SysLogHandler - log_handlers.append(SysLogHandler()) - # Browsing syslog via HTTP is obviously not possible out of the box - elif config.LOGGING_BACKEND: - raise ValueError("Invalid logging.backend = %s" % config.LOGGING_BACKEND) - - return app + return app diff --git a/certidude/api/log.py b/certidude/api/log.py index 19e4701..738ac1a 100644 --- a/certidude/api/log.py +++ b/certidude/api/log.py @@ -12,4 +12,4 @@ class LogResource(RelationalMixin): def on_get(self, req, resp): # TODO: Add last id parameter return self.iterfetch("select * from log order by created desc limit ?", - req.get_param_as_int("limit")) + req.get_param_as_int("limit", required=True)) diff --git a/certidude/api/revoked.py b/certidude/api/revoked.py index b620bd9..4ac69ba 100644 --- a/certidude/api/revoked.py +++ b/certidude/api/revoked.py @@ -30,3 +30,18 @@ class RevocationListResource(AuthorityHandler): logger.debug("Client %s asked revocation list in unsupported format" % req.context.get("remote_addr")) raise falcon.HTTPUnsupportedMediaType( "Client did not accept application/x-pkcs7-crl or application/x-pem-file") + + +class RevokedCertificateDetailResource(AuthorityHandler): + def on_get(self, req, resp, serial_number): + try: + path, buf, cert, signed, expires, revoked, reason = self.authority.get_revoked(serial_number) + except EnvironmentError: + logger.warning("Failed to serve non-existant revoked certificate with serial %s to %s", + serial_number, req.context.get("remote_addr")) + raise falcon.HTTPNotFound() + resp.set_header("Content-Type", "application/x-pem-file") + resp.set_header("Content-Disposition", ("attachment; filename=%x.pem" % cert.serial_number)) + resp.body = buf + logger.debug("Served revoked certificate with serial %s to %s", + serial_number, req.context.get("remote_addr")) diff --git a/certidude/api/session.py b/certidude/api/session.py index 7919c26..a1009b7 100644 --- a/certidude/api/session.py +++ b/certidude/api/session.py @@ -85,12 +85,10 @@ class SessionResource(AuthorityHandler): # 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 + last_seen = datetime.strptime(getxattr(path, "user.lease.last_seen").decode("ascii"), "%Y-%m-%dT%H:%M:%S.%fZ") ) except IOError: # No such attribute(s) lease = None @@ -166,10 +164,6 @@ class SessionResource(AuthorityHandler): hostname = const.FQDN, tokens = self.token_manager.list() if self.token_manager else None, 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"], diff --git a/certidude/authority.py b/certidude/authority.py index 7495139..37e260c 100644 --- a/certidude/authority.py +++ b/certidude/authority.py @@ -13,7 +13,7 @@ from asn1crypto.csr import CertificationRequest from certbuilder import CertificateBuilder from certidude import config, push, mailer, const from certidude import errors -from certidude.common import cn_to_dn, generate_serial, random +from certidude.common import cn_to_dn, generate_serial from crlbuilder import CertificateListBuilder, pem_armor_crl from csrbuilder import CSRBuilder, pem_armor_csr from datetime import datetime, timedelta diff --git a/certidude/cli.py b/certidude/cli.py index 13a4977..5020485 100755 --- a/certidude/cli.py +++ b/certidude/cli.py @@ -23,6 +23,7 @@ from datetime import datetime, timedelta from glob import glob from ipaddress import ip_network from oscrypto import asymmetric +from setproctitle import setproctitle try: import coverage @@ -43,6 +44,18 @@ logger = logging.getLogger(__name__) NOW = datetime.utcnow() +def make_runtime_dirs(func): + def wrapped(**args): + # systemd doesn't support RuntimeDirectoryPreserve=yes on Xenial + # otherwise this should be part of service files + # with RuntimeDirectory=certidude + if not os.path.exists(const.RUN_DIR): + click.echo("Creating: %s" % const.RUN_DIR) + os.makedirs(const.RUN_DIR) + os.chmod(const.RUN_DIR, 0o755) + return func(**args) + return wrapped + def fqdn_required(func): def wrapped(**args): common_name = args.get("common_name") @@ -1024,6 +1037,12 @@ def certidude_provision_authority(username, kerberos_keytab, nginx_config, tls_c assert subprocess.check_output(["/usr/bin/lsb_release", "-cs"]) in (b"trusty\n", b"xenial\n", b"bionic\n"), "Only Ubuntu 16.04 supported at the moment" assert os.getuid() == 0 and os.getgid() == 0, "Authority can be set up only by root" + session = dict( + authority = dict( + hostname = common_name + ) + ) + def verbose_symlink(name, target): if not os.path.islink(name): click.echo("Symlinking %s to %s" % (name, target)) @@ -1173,14 +1192,9 @@ def certidude_provision_authority(username, kerberos_keytab, nginx_config, tls_c def verbose_render_systemd_service(template, target, context): target_path = "/etc/systemd/system/%s" % target - if os.path.exists(target_path): - click.echo("File %s already exists, remove to regenerate" % target_path) - else: - buf = env.get_template(template).render(context) - with open(target_path, "w") as fh: - fh.write(buf) - click.echo("File %s created" % target_path) - os.system("systemctl daemon-reload") + buf = env.get_template(template).render(context) + with open(target_path, "w") as fh: + fh.write(buf) verbose_symlink("/etc/nginx/sites-enabled/certidude.conf", "../sites-available/certidude.conf") @@ -1192,6 +1206,9 @@ def certidude_provision_authority(username, kerberos_keytab, nginx_config, tls_c verbose_render_systemd_service("server/ldap-kinit.timer", "certidude-ldap-kinit.timer", vars()) verbose_render_systemd_service("snippets/nginx-ocsp-cache.service", "certidude-ocsp-cache.service", vars()) verbose_render_systemd_service("snippets/nginx-ocsp-cache.timer", "certidude-ocsp-cache.timer", vars()) + verbose_render_systemd_service("server/housekeeping-daily.service", "certidude-housekeeping-daily.service", vars()) + verbose_render_systemd_service("server/housekeeping-daily.timer", "certidude-housekeeping-daily.timer", vars()) + os.system("systemctl daemon-reload") else: raise NotImplementedError("Not systemd based OS, don't know how to set up initscripts") @@ -1409,14 +1426,6 @@ def certidude_provision_authority(username, kerberos_keytab, nginx_config, tls_c assert os.stat("/etc/nginx/sites-available/certidude.conf").st_mode == 0o100600 assert os.stat("/etc/certidude/server.conf").st_mode == 0o100600 - # Disable legacy garbage - if os.path.exists("/etc/cron.hourly/certidude"): - os.unlink("/etc/cron.hourly/certidude") - if os.path.exists("/etc/cron.daily/certidude"): - os.unlink("/etc/cron.daily/certidude") - if os.path.exists("/etc/systemd/system/certidude.service"): - os.unlink("/etc/systemd/system/certidude.service") - click.echo("To enable e-mail notifications install Postfix as sattelite system and set mailer address in %s" % const.SERVER_CONFIG_PATH) click.echo() click.echo("Use following commands to inspect the newly created files:") @@ -1434,6 +1443,8 @@ def certidude_provision_authority(username, kerberos_keytab, nginx_config, tls_c os.system("systemctl enable certidude-backend.service") os.system("systemctl enable nginx") os.system("systemctl enable certidude-ocsp-cache.timer") + os.system("systemctl enable certidude-housekeeping-daily.timer") + os.system("systemctl start certidude-housekeeping-daily.timer") os.system("systemctl start certidude-ocsp-cache.timer") if realm: os.system("systemctl enable certidude-ldap-kinit.timer") @@ -1561,21 +1572,24 @@ def certidude_revoke(common_name, reason): @click.command("kinit", help="Initialize Kerberos credential cache for LDAP") +@make_runtime_dirs def certidude_housekeeping_kinit(): from certidude import config # Update LDAP service ticket if Certidude is joined to domain - if os.path.exists("/etc/krb5.keytab"): - if not os.path.exists("/run/certidude"): - os.makedirs("/run/certidude") - _, kdc = config.LDAP_ACCOUNTS_URI.rsplit("/", 1) - cmd = "KRB5CCNAME=/run/certidude/krb5cc.part kinit -k %s$ -S ldap/%s@%s -t /etc/krb5.keytab" % ( - const.HOSTNAME.upper(), kdc, config.KERBEROS_REALM - ) - click.echo("Executing: %s" % cmd) - os.system(cmd) - os.system("chown certidude:certidude /run/certidude/krb5cc.part") - os.rename("/run/certidude/krb5cc.part", "/run/certidude/krb5cc") + if not os.path.exists("/etc/krb5.keytab"): + raise click.ClickException("No Kerberos keytab configured") + + _, kdc = config.LDAP_ACCOUNTS_URI.rsplit("/", 1) + cmd = "KRB5CCNAME=%s.part kinit -k %s$ -S ldap/%s@%s -t /etc/krb5.keytab" % ( + config.LDAP_GSSAPI_CRED_CACHE, + const.HOSTNAME.upper(), kdc, config.KERBEROS_REALM + ) + click.echo("Executing: %s" % cmd) + if os.system(cmd): + raise click.ClickException("Failed to initialize Kerberos credential cache!") + os.system("chown certidude:certidude %s.part" % config.LDAP_GSSAPI_CRED_CACHE) + os.rename("%s.part" % config.LDAP_GSSAPI_CRED_CACHE, config.LDAP_GSSAPI_CRED_CACHE) @click.command("daily", help="Send notifications about expired certificates") @@ -1614,44 +1628,18 @@ def certidude_housekeeping_expiration(): # TODO: Send separate e-mails to subjects -@click.command("serve", help="Run server") -@click.option("-p", "--port", default=8080, help="Listen port") -@click.option("-l", "--listen", default="127.0.1.1", help="Listen address") +@click.command("serve", help="Run API backend server") @click.option("-f", "--fork", default=False, is_flag=True, help="Fork to background") -def certidude_serve(port, listen, fork): - from certidude import authority, const, push - - if port == 80: - click.echo("WARNING: Please run Certidude behind nginx, remote address is assumed to be forwarded by nginx!") - +@make_runtime_dirs +def certidude_serve(fork): + from certidude import const, push, config click.echo("Using configuration from: %s" % const.SERVER_CONFIG_PATH) - - log_handlers = [] - - from certidude import config - - click.echo("OCSP responder subnets: %s" % config.OCSP_SUBNETS) - click.echo("CRL subnets: %s" % config.CRL_SUBNETS) click.echo("SCEP subnets: %s" % config.SCEP_SUBNETS) - click.echo("Loading signature profiles:") for profile in config.PROFILES.values(): click.echo("- %s" % profile) click.echo() - # Rebuild reverse mapping - for cn, path, buf, cert, signed, expires in authority.list_signed(): - by_serial = os.path.join(config.SIGNED_BY_SERIAL_DIR, "%040x.pem" % cert.serial_number) - if not os.path.exists(by_serial): - click.echo("Linking %s to ../%s.pem" % (by_serial, cn)) - os.symlink("../%s.pem" % cn, by_serial) - - # Process directories - if not os.path.exists(const.RUN_DIR): - click.echo("Creating: %s" % const.RUN_DIR) - os.makedirs(const.RUN_DIR) - os.chmod(const.RUN_DIR, 0o755) - click.echo("Users subnets: %s" % ", ".join([str(j) for j in config.USER_SUBNETS])) click.echo("Administrative subnets: %s" % @@ -1661,47 +1649,43 @@ def certidude_serve(port, listen, fork): click.echo("Request submissions allowed from following subnets: %s" % ", ".join([str(j) for j in config.REQUEST_SUBNETS])) - click.echo("Serving API at %s:%d" % (listen, port)) - from wsgiref.simple_server import make_server, WSGIServer - from certidude.api import certidude_app - - - click.echo("Listening on %s:%d" % (listen, port)) - - app = certidude_app(log_handlers) - httpd = make_server(listen, port, app, WSGIServer) - - - """ - Drop privileges - """ - - from certidude.push import EventSourceLogHandler - log_handlers.append(EventSourceLogHandler()) - - for j in logging.Logger.manager.loggerDict.values(): - if isinstance(j, logging.Logger): # PlaceHolder is what? - if j.name.startswith("certidude."): - j.setLevel(logging.DEBUG) - for handler in log_handlers: - j.addHandler(handler) + from certidude.api import ReadWriteApp, BuilderApp, ResponderApp, RevocationListApp, LogApp if not fork or not os.fork(): pid = os.getpid() with open(const.SERVER_PID_PATH, "w") as pidfile: pidfile.write("%d\n" % pid) + # Rebuild reverse mapping + from certidude import authority + for cn, path, buf, cert, signed, expires in authority.list_signed(): + by_serial = os.path.join(config.SIGNED_BY_SERIAL_DIR, "%040x.pem" % cert.serial_number) + if not os.path.exists(by_serial): + click.echo("Linking %s to ../%s.pem" % (by_serial, cn)) + os.symlink("../%s.pem" % cn, by_serial) + push.publish("server-started") logger.debug("Started Certidude at %s", const.FQDN) - drop_privileges() - try: - httpd.serve_forever() - except KeyboardInterrupt: - click.echo("Caught Ctrl-C, exiting...") - push.publish("server-stopped") - logger.debug("Shutting down Certidude") - return + if fork and config.OCSP_SUBNETS: + click.echo("OCSP responder subnets: %s" % config.OCSP_SUBNETS) + if ResponderApp().fork(): + return + if fork and config.CRL_SUBNETS: + click.echo("CRL subnets: %s" % config.CRL_SUBNETS) + if RevocationListApp().fork(): + return + if fork: + if BuilderApp().fork(): + return + if fork and config.LOGGING_BACKEND == "sql": + if LogApp().fork(): + return + + ReadWriteApp().run() + push.publish("server-stopped") + logger.debug("Shutting down Certidude API backend") + return @click.command("yubikey", help="Set up Yubikey as client authentication token") diff --git a/certidude/common.py b/certidude/common.py index ca7e16c..6cdde2e 100644 --- a/certidude/common.py +++ b/certidude/common.py @@ -2,6 +2,7 @@ import os import click import subprocess +from setproctitle import getproctitle from random import SystemRandom random = SystemRandom() @@ -98,8 +99,8 @@ def drop_privileges(): os.setgroups(restricted_groups) os.setgid(gid) os.setuid(uid) - click.echo("Switched to user %s (uid=%d, gid=%d); member of groups %s" % - ("certidude", os.getuid(), os.getgid(), ", ".join([str(j) for j in os.getgroups()]))) + click.echo("Switched %s (pid=%d) to user %s (uid=%d, gid=%d); member of groups %s" % + (getproctitle(), os.getpid(), "certidude", os.getuid(), os.getgid(), ", ".join([str(j) for j in os.getgroups()]))) os.umask(0o007) def apt(packages): diff --git a/certidude/const.py b/certidude/const.py index 2782067..ba16afe 100644 --- a/certidude/const.py +++ b/certidude/const.py @@ -22,7 +22,6 @@ PROFILE_CONFIG_PATH = os.path.join(CONFIG_DIR, "profile.conf") CLIENT_CONFIG_PATH = os.path.join(CONFIG_DIR, "client.conf") SERVICES_CONFIG_PATH = os.path.join(CONFIG_DIR, "services.conf") SERVER_PID_PATH = os.path.join(RUN_DIR, "server.pid") -SERVER_LOG_PATH = "/var/log/certidude-server.log" STORAGE_PATH = "/var/lib/certidude/" try: diff --git a/certidude/static/502.json b/certidude/static/502.json index 4f302ba..e36047f 100644 --- a/certidude/static/502.json +++ b/certidude/static/502.json @@ -1,4 +1,4 @@ { "title": "502 Bad Gateway", - "description": "It seems the server had bit of a hiccup, perhaps this helps: systemctl restart certidude && journalctl -f" + "description": "It seems the server had bit of a hiccup, perhaps this helps: systemctl restart certidude-backend && journalctl -f" } diff --git a/certidude/templates/server/housekeeping-daily.service b/certidude/templates/server/housekeeping-daily.service new file mode 100644 index 0000000..327566f --- /dev/null +++ b/certidude/templates/server/housekeeping-daily.service @@ -0,0 +1,7 @@ +[Unit] +Description=Run daily housekeeping jobs, eg certificate expiration notifications + +[Service] +Type=oneshot +ExecStart={{ certidude_path }} housekeeping daily + diff --git a/certidude/templates/server/housekeeping-daily.timer b/certidude/templates/server/housekeeping-daily.timer new file mode 100644 index 0000000..973128c --- /dev/null +++ b/certidude/templates/server/housekeeping-daily.timer @@ -0,0 +1,6 @@ +[Timer] +Persistent=true +OnCalendar=daily + +[Install] +WantedBy=multi-user.target diff --git a/certidude/templates/server/nginx.conf b/certidude/templates/server/nginx.conf index dcf3a68..e690ef3 100644 --- a/certidude/templates/server/nginx.conf +++ b/certidude/templates/server/nginx.conf @@ -31,6 +31,16 @@ server { server_name {{ common_name }}; listen 80 default_server; + # Proxy pass CRL server + location /api/revoked/ { + proxy_pass http://127.0.1.1:8082/api/revoked/; + } + + # Proxy pass OCSP responder + location /api/ocsp/ { + proxy_pass http://127.0.1.1:8081/api/ocsp/; + } + # Proxy pass to backend location /api/ { proxy_pass http://127.0.1.1:8080/api/; @@ -90,6 +100,16 @@ server { # once it has been configured add_header Strict-Transport-Security "max-age=15768000; includeSubDomains; preload;"; + # Proxy pass image builder + location /api/log/ { + proxy_pass http://127.0.1.1:8084/api/log/; + } + + # Proxy pass image builder + location /api/builder/ { + proxy_pass http://127.0.1.1:8083/api/builder/; + } + # Proxy pass to backend location /api/ { proxy_pass http://127.0.1.1:8080/api/; diff --git a/certidude/templates/server/responder.service b/certidude/templates/server/responder.service new file mode 100644 index 0000000..e69de29 diff --git a/certidude/templates/server/systemd.service b/certidude/templates/server/systemd.service deleted file mode 100644 index 5b224db..0000000 --- a/certidude/templates/server/systemd.service +++ /dev/null @@ -1,17 +0,0 @@ -[Unit] -Description=Certidude server -After=network.target - -[Service] -Type=simple -EnvironmentFile=/etc/environment -Environment=LANG=C.UTF-8 -Environment=PYTHON_EGG_CACHE=/tmp/.cache -PIDFile=/run/certidude/server.pid -KillSignal=SIGINT -ExecStart={{ certidude_path }} serve -TimeoutSec=15 - -[Install] -WantedBy=multi-user.target - diff --git a/certidude/templates/snippets/nginx-ocsp-cache.service b/certidude/templates/snippets/nginx-ocsp-cache.service index d58c07f..9bd4eda 100644 --- a/certidude/templates/snippets/nginx-ocsp-cache.service +++ b/certidude/templates/snippets/nginx-ocsp-cache.service @@ -1,7 +1,7 @@ [Unit] Description=Cache OCSP responses for nginx OCSP stapling +Requires=nginx.service [Service] Type=oneshot -Requires=nginx.service -ExecStart=-/usr/bin/curl --cert-status https://{{ common_name }}:8443/ --cacert /etc/certidude/authority/{{ session.authority.hostname }}/ca_cert.pem +ExecStart=-/usr/bin/curl --cert-status https://{{ session.authority.hostname }}:8443/ --cacert /etc/certidude/authority/{{ session.authority.hostname }}/ca_cert.pem diff --git a/requirements.txt b/requirements.txt index ce332f6..4d22034 100644 --- a/requirements.txt +++ b/requirements.txt @@ -7,3 +7,4 @@ oscrypto requests jinja2 ipsecparse +setproctitle diff --git a/tests/test_cli.py b/tests/test_cli.py index 2e85352..b487116 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -7,6 +7,7 @@ import re import shutil import sys from asn1crypto import pem, x509 +from glob import glob from oscrypto import asymmetric from csrbuilder import CSRBuilder, pem_armor_csr from asn1crypto.util import OrderedDict @@ -159,13 +160,11 @@ def clean_server(): files = [ "/etc/krb5.keytab", "/etc/samba/smb.conf", - "/etc/certidude/server.conf", - "/etc/certidude/builder.conf", - "/etc/certidude/profile.conf", + "/etc/certidude/*.conf", "/var/log/certidude.log", "/etc/cron.daily/certidude", "/etc/cron.hourly/certidude", - "/etc/systemd/system/certidude.service", + "/etc/systemd/system/certidude*", "/etc/nginx/sites-available/ca.conf", "/etc/nginx/sites-enabled/ca.conf", "/etc/nginx/sites-available/certidude.conf", @@ -179,11 +178,12 @@ def clean_server(): "/usr/bin/node", ] - for filename in files: - try: - os.unlink(filename) - except: - pass + for pattern in files: + for filename in glob(pattern): + try: + os.unlink(filename) + except: + pass # Remove OpenVPN stuff if os.path.exists("/etc/openvpn"):