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:
parent
2f301d4fec
commit
6e50c85c85
@ -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
|
|
||||||
|
@ -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))
|
||||||
|
@ -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"))
|
||||||
|
@ -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"],
|
||||||
|
@ -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
|
||||||
|
162
certidude/cli.py
162
certidude/cli.py
@ -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")
|
||||||
|
@ -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):
|
||||||
|
@ -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:
|
||||||
|
@ -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"
|
||||||
}
|
}
|
||||||
|
7
certidude/templates/server/housekeeping-daily.service
Normal file
7
certidude/templates/server/housekeeping-daily.service
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=Run daily housekeeping jobs, eg certificate expiration notifications
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=oneshot
|
||||||
|
ExecStart={{ certidude_path }} housekeeping daily
|
||||||
|
|
6
certidude/templates/server/housekeeping-daily.timer
Normal file
6
certidude/templates/server/housekeeping-daily.timer
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
[Timer]
|
||||||
|
Persistent=true
|
||||||
|
OnCalendar=daily
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
@ -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/;
|
||||||
|
0
certidude/templates/server/responder.service
Normal file
0
certidude/templates/server/responder.service
Normal 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
|
|
||||||
|
|
@ -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
|
|
||||||
|
@ -7,3 +7,4 @@ oscrypto
|
|||||||
requests
|
requests
|
||||||
jinja2
|
jinja2
|
||||||
ipsecparse
|
ipsecparse
|
||||||
|
setproctitle
|
||||||
|
@ -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"):
|
||||||
|
Loading…
Reference in New Issue
Block a user