1
0
mirror of https://github.com/laurivosandi/certidude synced 2024-12-22 08:15:18 +00:00

Move to pre-forking model for backend API-s

This commit is contained in:
Lauri Võsandi 2018-10-05 10:41:40 +03:00
parent 2f301d4fec
commit 6e50c85c85
17 changed files with 289 additions and 197 deletions

View File

@ -2,10 +2,13 @@
import falcon import falcon
import ipaddress import ipaddress
import logging
import os import os
from certidude import config from certidude import config
from certidude.common import drop_privileges
from user_agents import parse from user_agents import parse
from wsgiref.simple_server import make_server, WSGIServer
from setproctitle import setproctitle
class NormalizeMiddleware(object): class NormalizeMiddleware(object):
def process_request(self, req, resp, *args): def process_request(self, req, resp, *args):
@ -15,89 +18,168 @@ class NormalizeMiddleware(object):
else: else:
req.context["user_agent"] = "Unknown user agent" 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()) class App(object):
app.req_options.auto_parse_form_urlencoded = True PORT = 8080
FORKS = None
DROP_PRIVILEGES = True
# Certificate authority API calls def __init__(self):
app.add_route("/api/certificate/", CertificateAuthorityResource()) app = falcon.API(middleware=NormalizeMiddleware())
app.add_route("/api/signed/{cn}/", SignedCertificateDetailResource(authority)) app.req_options.auto_parse_form_urlencoded = True
app.add_route("/api/request/{cn}/", RequestDetailResource(authority)) self.attach(app)
app.add_route("/api/request/", RequestListResource(authority))
token_resource = None # Set up log handlers
token_manager = None log_handlers = []
if config.USER_ENROLLMENT_ALLOWED: # TODO: add token enable/disable flag for config if config.LOGGING_BACKEND == "sql":
if config.TOKEN_BACKEND == "sql": from certidude.mysqllog import LogHandler
token_manager = TokenManager(config.TOKEN_DATABASE) from certidude.api.log import LogResource
token_resource = TokenResource(authority, token_manager) uri = config.cp.get("logging", "database")
app.add_route("/api/token/", token_resource) log_handlers.append(LogHandler(uri))
elif not config.TOKEN_BACKEND: elif config.LOGGING_BACKEND == "syslog":
pass 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: 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 class ReadWriteApp(App):
app.add_route("/api/signed/{cn}/tag/{tag}/", TagDetailResource(authority)) NAME = "backend server"
# Gateways can submit leases via this API call def attach(self, app):
app.add_route("/api/lease/", LeaseResource(authority)) 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 # Certificate authority API calls
app.add_route("/api/bootstrap/", BootstrapResource(authority)) 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 token_resource = None
app.add_route("/api/build/{profile}/{suggested_filename}", ImageBuilderResource()) 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 app.add_route("/api/", SessionResource(authority, token_manager))
if config.CRL_SUBNETS:
from .revoked import RevocationListResource
app.add_route("/api/revoked/", RevocationListResource(authority))
# Add SCEP handler if we have any whitelisted subnets # Extended attributes for scripting etc.
if config.SCEP_SUBNETS: app.add_route("/api/signed/{cn}/attr/", AttributeResource(authority, namespace="machine"))
from .scep import SCEPResource app.add_route("/api/signed/{cn}/script/", ScriptResource(authority))
app.add_route("/api/scep/", SCEPResource(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 from .ocsp import OCSPResource
app.add_sink(OCSPResource(authority), prefix="/api/ocsp") app.add_sink(OCSPResource(authority), prefix="/api/ocsp")
return app
# Set up log handlers
if config.LOGGING_BACKEND == "sql": class RevocationListApp(App):
from certidude.mysqllog import LogHandler 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 from certidude.api.log import LogResource
uri = config.cp.get("logging", "database") uri = config.cp.get("logging", "database")
log_handlers.append(LogHandler(uri))
app.add_route("/api/log/", LogResource(uri)) app.add_route("/api/log/", LogResource(uri))
elif config.LOGGING_BACKEND == "syslog": return app
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

View File

@ -12,4 +12,4 @@ class LogResource(RelationalMixin):
def on_get(self, req, resp): def on_get(self, req, resp):
# TODO: Add last id parameter # TODO: Add last id parameter
return self.iterfetch("select * from log order by created desc limit ?", 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))

View File

@ -30,3 +30,18 @@ class RevocationListResource(AuthorityHandler):
logger.debug("Client %s asked revocation list in unsupported format" % req.context.get("remote_addr")) logger.debug("Client %s asked revocation list in unsupported format" % req.context.get("remote_addr"))
raise falcon.HTTPUnsupportedMediaType( raise falcon.HTTPUnsupportedMediaType(
"Client did not accept application/x-pkcs7-crl or application/x-pem-file") "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"))

View File

@ -85,12 +85,10 @@ class SessionResource(AuthorityHandler):
# Extract lease information from filesystem # Extract lease information from filesystem
try: try:
last_seen = datetime.strptime(getxattr(path, "user.lease.last_seen").decode("ascii"), "%Y-%m-%dT%H:%M:%S.%fZ")
lease = dict( lease = dict(
inner_address = getxattr(path, "user.lease.inner_address").decode("ascii"), inner_address = getxattr(path, "user.lease.inner_address").decode("ascii"),
outer_address = getxattr(path, "user.lease.outer_address").decode("ascii"), outer_address = getxattr(path, "user.lease.outer_address").decode("ascii"),
last_seen = last_seen, last_seen = datetime.strptime(getxattr(path, "user.lease.last_seen").decode("ascii"), "%Y-%m-%dT%H:%M:%S.%fZ")
age = datetime.utcnow() - last_seen
) )
except IOError: # No such attribute(s) except IOError: # No such attribute(s)
lease = None lease = None
@ -166,10 +164,6 @@ class SessionResource(AuthorityHandler):
hostname = const.FQDN, hostname = const.FQDN,
tokens = self.token_manager.list() if self.token_manager else None, 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], 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( certificate = dict(
algorithm = self.authority.public_key.algorithm, algorithm = self.authority.public_key.algorithm,
common_name = self.authority.certificate.subject.native["common_name"], common_name = self.authority.certificate.subject.native["common_name"],

View File

@ -13,7 +13,7 @@ from asn1crypto.csr import CertificationRequest
from certbuilder import CertificateBuilder from certbuilder import CertificateBuilder
from certidude import config, push, mailer, const from certidude import config, push, mailer, const
from certidude import errors 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 crlbuilder import CertificateListBuilder, pem_armor_crl
from csrbuilder import CSRBuilder, pem_armor_csr from csrbuilder import CSRBuilder, pem_armor_csr
from datetime import datetime, timedelta from datetime import datetime, timedelta

View File

@ -23,6 +23,7 @@ from datetime import datetime, timedelta
from glob import glob from glob import glob
from ipaddress import ip_network from ipaddress import ip_network
from oscrypto import asymmetric from oscrypto import asymmetric
from setproctitle import setproctitle
try: try:
import coverage import coverage
@ -43,6 +44,18 @@ logger = logging.getLogger(__name__)
NOW = datetime.utcnow() 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 fqdn_required(func):
def wrapped(**args): def wrapped(**args):
common_name = args.get("common_name") 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 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" 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): def verbose_symlink(name, target):
if not os.path.islink(name): if not os.path.islink(name):
click.echo("Symlinking %s to %s" % (name, target)) 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): def verbose_render_systemd_service(template, target, context):
target_path = "/etc/systemd/system/%s" % target target_path = "/etc/systemd/system/%s" % target
if os.path.exists(target_path): buf = env.get_template(template).render(context)
click.echo("File %s already exists, remove to regenerate" % target_path) with open(target_path, "w") as fh:
else: fh.write(buf)
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")
verbose_symlink("/etc/nginx/sites-enabled/certidude.conf", "../sites-available/certidude.conf") 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("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.service", "certidude-ocsp-cache.service", vars())
verbose_render_systemd_service("snippets/nginx-ocsp-cache.timer", "certidude-ocsp-cache.timer", 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: else:
raise NotImplementedError("Not systemd based OS, don't know how to set up initscripts") 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/nginx/sites-available/certidude.conf").st_mode == 0o100600
assert os.stat("/etc/certidude/server.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("To enable e-mail notifications install Postfix as sattelite system and set mailer address in %s" % const.SERVER_CONFIG_PATH)
click.echo() click.echo()
click.echo("Use following commands to inspect the newly created files:") 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 certidude-backend.service")
os.system("systemctl enable nginx") os.system("systemctl enable nginx")
os.system("systemctl enable certidude-ocsp-cache.timer") 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") os.system("systemctl start certidude-ocsp-cache.timer")
if realm: if realm:
os.system("systemctl enable certidude-ldap-kinit.timer") 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") @click.command("kinit", help="Initialize Kerberos credential cache for LDAP")
@make_runtime_dirs
def certidude_housekeeping_kinit(): def certidude_housekeeping_kinit():
from certidude import config from certidude import config
# Update LDAP service ticket if Certidude is joined to domain # Update LDAP service ticket if Certidude is joined to domain
if os.path.exists("/etc/krb5.keytab"): if not os.path.exists("/etc/krb5.keytab"):
if not os.path.exists("/run/certidude"): raise click.ClickException("No Kerberos keytab configured")
os.makedirs("/run/certidude")
_, kdc = config.LDAP_ACCOUNTS_URI.rsplit("/", 1) _, kdc = config.LDAP_ACCOUNTS_URI.rsplit("/", 1)
cmd = "KRB5CCNAME=/run/certidude/krb5cc.part kinit -k %s$ -S ldap/%s@%s -t /etc/krb5.keytab" % ( cmd = "KRB5CCNAME=%s.part kinit -k %s$ -S ldap/%s@%s -t /etc/krb5.keytab" % (
const.HOSTNAME.upper(), kdc, config.KERBEROS_REALM config.LDAP_GSSAPI_CRED_CACHE,
) const.HOSTNAME.upper(), kdc, config.KERBEROS_REALM
click.echo("Executing: %s" % cmd) )
os.system(cmd) click.echo("Executing: %s" % cmd)
os.system("chown certidude:certidude /run/certidude/krb5cc.part") if os.system(cmd):
os.rename("/run/certidude/krb5cc.part", "/run/certidude/krb5cc") 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") @click.command("daily", help="Send notifications about expired certificates")
@ -1614,44 +1628,18 @@ def certidude_housekeeping_expiration():
# TODO: Send separate e-mails to subjects # TODO: Send separate e-mails to subjects
@click.command("serve", help="Run server") @click.command("serve", help="Run API backend server")
@click.option("-p", "--port", default=8080, help="Listen port")
@click.option("-l", "--listen", default="127.0.1.1", help="Listen address")
@click.option("-f", "--fork", default=False, is_flag=True, help="Fork to background") @click.option("-f", "--fork", default=False, is_flag=True, help="Fork to background")
def certidude_serve(port, listen, fork): @make_runtime_dirs
from certidude import authority, const, push def certidude_serve(fork):
from certidude import const, push, config
if port == 80:
click.echo("WARNING: Please run Certidude behind nginx, remote address is assumed to be forwarded by nginx!")
click.echo("Using configuration from: %s" % const.SERVER_CONFIG_PATH) 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("SCEP subnets: %s" % config.SCEP_SUBNETS)
click.echo("Loading signature profiles:") click.echo("Loading signature profiles:")
for profile in config.PROFILES.values(): for profile in config.PROFILES.values():
click.echo("- %s" % profile) click.echo("- %s" % profile)
click.echo() 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" % click.echo("Users subnets: %s" %
", ".join([str(j) for j in config.USER_SUBNETS])) ", ".join([str(j) for j in config.USER_SUBNETS]))
click.echo("Administrative subnets: %s" % click.echo("Administrative subnets: %s" %
@ -1661,47 +1649,43 @@ def certidude_serve(port, listen, fork):
click.echo("Request submissions allowed from following subnets: %s" % click.echo("Request submissions allowed from following subnets: %s" %
", ".join([str(j) for j in config.REQUEST_SUBNETS])) ", ".join([str(j) for j in config.REQUEST_SUBNETS]))
click.echo("Serving API at %s:%d" % (listen, port)) from certidude.api import ReadWriteApp, BuilderApp, ResponderApp, RevocationListApp, LogApp
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)
if not fork or not os.fork(): if not fork or not os.fork():
pid = os.getpid() pid = os.getpid()
with open(const.SERVER_PID_PATH, "w") as pidfile: with open(const.SERVER_PID_PATH, "w") as pidfile:
pidfile.write("%d\n" % pid) 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") push.publish("server-started")
logger.debug("Started Certidude at %s", const.FQDN) logger.debug("Started Certidude at %s", const.FQDN)
drop_privileges() if fork and config.OCSP_SUBNETS:
try: click.echo("OCSP responder subnets: %s" % config.OCSP_SUBNETS)
httpd.serve_forever() if ResponderApp().fork():
except KeyboardInterrupt: return
click.echo("Caught Ctrl-C, exiting...") if fork and config.CRL_SUBNETS:
push.publish("server-stopped") click.echo("CRL subnets: %s" % config.CRL_SUBNETS)
logger.debug("Shutting down Certidude") if RevocationListApp().fork():
return 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") @click.command("yubikey", help="Set up Yubikey as client authentication token")

View File

@ -2,6 +2,7 @@
import os import os
import click import click
import subprocess import subprocess
from setproctitle import getproctitle
from random import SystemRandom from random import SystemRandom
random = SystemRandom() random = SystemRandom()
@ -98,8 +99,8 @@ def drop_privileges():
os.setgroups(restricted_groups) os.setgroups(restricted_groups)
os.setgid(gid) os.setgid(gid)
os.setuid(uid) os.setuid(uid)
click.echo("Switched to user %s (uid=%d, gid=%d); member of groups %s" % click.echo("Switched %s (pid=%d) to user %s (uid=%d, gid=%d); member of groups %s" %
("certidude", os.getuid(), os.getgid(), ", ".join([str(j) for j in os.getgroups()]))) (getproctitle(), os.getpid(), "certidude", os.getuid(), os.getgid(), ", ".join([str(j) for j in os.getgroups()])))
os.umask(0o007) os.umask(0o007)
def apt(packages): def apt(packages):

View File

@ -22,7 +22,6 @@ PROFILE_CONFIG_PATH = os.path.join(CONFIG_DIR, "profile.conf")
CLIENT_CONFIG_PATH = os.path.join(CONFIG_DIR, "client.conf") CLIENT_CONFIG_PATH = os.path.join(CONFIG_DIR, "client.conf")
SERVICES_CONFIG_PATH = os.path.join(CONFIG_DIR, "services.conf") SERVICES_CONFIG_PATH = os.path.join(CONFIG_DIR, "services.conf")
SERVER_PID_PATH = os.path.join(RUN_DIR, "server.pid") SERVER_PID_PATH = os.path.join(RUN_DIR, "server.pid")
SERVER_LOG_PATH = "/var/log/certidude-server.log"
STORAGE_PATH = "/var/lib/certidude/" STORAGE_PATH = "/var/lib/certidude/"
try: try:

View File

@ -1,4 +1,4 @@
{ {
"title": "502 Bad Gateway", "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"
} }

View File

@ -0,0 +1,7 @@
[Unit]
Description=Run daily housekeeping jobs, eg certificate expiration notifications
[Service]
Type=oneshot
ExecStart={{ certidude_path }} housekeeping daily

View File

@ -0,0 +1,6 @@
[Timer]
Persistent=true
OnCalendar=daily
[Install]
WantedBy=multi-user.target

View File

@ -31,6 +31,16 @@ server {
server_name {{ common_name }}; server_name {{ common_name }};
listen 80 default_server; 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 # Proxy pass to backend
location /api/ { location /api/ {
proxy_pass http://127.0.1.1:8080/api/; proxy_pass http://127.0.1.1:8080/api/;
@ -90,6 +100,16 @@ server {
# once it has been configured # once it has been configured
add_header Strict-Transport-Security "max-age=15768000; includeSubDomains; preload;"; 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 # Proxy pass to backend
location /api/ { location /api/ {
proxy_pass http://127.0.1.1:8080/api/; proxy_pass http://127.0.1.1:8080/api/;

View File

@ -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

View File

@ -1,7 +1,7 @@
[Unit] [Unit]
Description=Cache OCSP responses for nginx OCSP stapling Description=Cache OCSP responses for nginx OCSP stapling
Requires=nginx.service
[Service] [Service]
Type=oneshot Type=oneshot
Requires=nginx.service ExecStart=-/usr/bin/curl --cert-status https://{{ session.authority.hostname }}:8443/ --cacert /etc/certidude/authority/{{ session.authority.hostname }}/ca_cert.pem
ExecStart=-/usr/bin/curl --cert-status https://{{ common_name }}:8443/ --cacert /etc/certidude/authority/{{ session.authority.hostname }}/ca_cert.pem

View File

@ -7,3 +7,4 @@ oscrypto
requests requests
jinja2 jinja2
ipsecparse ipsecparse
setproctitle

View File

@ -7,6 +7,7 @@ import re
import shutil import shutil
import sys import sys
from asn1crypto import pem, x509 from asn1crypto import pem, x509
from glob import glob
from oscrypto import asymmetric from oscrypto import asymmetric
from csrbuilder import CSRBuilder, pem_armor_csr from csrbuilder import CSRBuilder, pem_armor_csr
from asn1crypto.util import OrderedDict from asn1crypto.util import OrderedDict
@ -159,13 +160,11 @@ def clean_server():
files = [ files = [
"/etc/krb5.keytab", "/etc/krb5.keytab",
"/etc/samba/smb.conf", "/etc/samba/smb.conf",
"/etc/certidude/server.conf", "/etc/certidude/*.conf",
"/etc/certidude/builder.conf",
"/etc/certidude/profile.conf",
"/var/log/certidude.log", "/var/log/certidude.log",
"/etc/cron.daily/certidude", "/etc/cron.daily/certidude",
"/etc/cron.hourly/certidude", "/etc/cron.hourly/certidude",
"/etc/systemd/system/certidude.service", "/etc/systemd/system/certidude*",
"/etc/nginx/sites-available/ca.conf", "/etc/nginx/sites-available/ca.conf",
"/etc/nginx/sites-enabled/ca.conf", "/etc/nginx/sites-enabled/ca.conf",
"/etc/nginx/sites-available/certidude.conf", "/etc/nginx/sites-available/certidude.conf",
@ -179,11 +178,12 @@ def clean_server():
"/usr/bin/node", "/usr/bin/node",
] ]
for filename in files: for pattern in files:
try: for filename in glob(pattern):
os.unlink(filename) try:
except: os.unlink(filename)
pass except:
pass
# Remove OpenVPN stuff # Remove OpenVPN stuff
if os.path.exists("/etc/openvpn"): if os.path.exists("/etc/openvpn"):