diff --git a/certidude/api/__init__.py b/certidude/api/__init__.py index 7556f44..5babe55 100644 --- a/certidude/api/__init__.py +++ b/certidude/api/__init__.py @@ -8,6 +8,7 @@ import click import hashlib from datetime import datetime from time import sleep +from xattr import listxattr, getxattr from certidude import authority, mailer from certidude.auth import login_required, authorize_admin from certidude.user import User @@ -32,7 +33,6 @@ class SessionResource(object): @serialize @login_required def on_get(self, req, resp): - import xattr def serialize_requests(g): for common_name, path, buf, obj, server in g(): @@ -50,7 +50,7 @@ class SessionResource(object): # Extract certificate tags from filesystem try: tags = [] - for tag in xattr.getxattr(path, "user.xdg.tags").split(","): + for tag in getxattr(path, "user.xdg.tags").split(","): if "=" in tag: k, v = tag.split("=", 1) else: @@ -59,12 +59,17 @@ class SessionResource(object): except IOError: # No such attribute(s) tags = None + attributes = {} + for key in listxattr(path): + if key.startswith("user.machine."): + attributes[key[13:]] = getxattr(path, key) + # Extract lease information from filesystem try: - last_seen = datetime.strptime(xattr.getxattr(path, "user.lease.last_seen"), "%Y-%m-%dT%H:%M:%S.%fZ") + last_seen = datetime.strptime(getxattr(path, "user.lease.last_seen"), "%Y-%m-%dT%H:%M:%S.%fZ") lease = dict( - inner_address = xattr.getxattr(path, "user.lease.inner_address"), - outer_address = xattr.getxattr(path, "user.lease.outer_address"), + inner_address = getxattr(path, "user.lease.inner_address"), + outer_address = getxattr(path, "user.lease.outer_address"), last_seen = last_seen, age = datetime.utcnow() - last_seen ) @@ -80,7 +85,8 @@ class SessionResource(object): expires = obj.not_valid_after, sha256sum = hashlib.sha256(buf).hexdigest(), lease = lease, - tags = tags + tags = tags, + attributes = attributes or None, ) if req.context.get("user").is_admin(): @@ -160,7 +166,7 @@ import ipaddress class NormalizeMiddleware(object): def process_request(self, req, resp, *args): assert not req.get_param("unicode") or req.get_param("unicode") == u"✓", "Unicode sanity check failed" - req.context["remote_addr"] = ipaddress.ip_address(req.env["REMOTE_ADDR"].decode("utf-8")) + req.context["remote_addr"] = ipaddress.ip_address(req.access_route[0].decode("utf-8")) def process_response(self, req, resp, resource=None): # wtf falcon?! @@ -181,6 +187,7 @@ def certidude_app(log_handlers=[]): app = falcon.API(middleware=NormalizeMiddleware()) app.req_options.auto_parse_form_urlencoded = True + #app.req_options.strip_url_path_trailing_slash = False # Certificate authority API calls app.add_route("/api/certificate/", CertificateAuthorityResource()) @@ -194,7 +201,7 @@ def certidude_app(log_handlers=[]): app.add_route("/api/token/", TokenResource()) # Extended attributes for scripting etc. - app.add_route("/api/signed/{cn}/attr/", AttributeResource()) + app.add_route("/api/signed/{cn}/attr/", AttributeResource(namespace="machine")) app.add_route("/api/signed/{cn}/script/", ScriptResource()) # API calls used by pushed events on the JS end @@ -215,18 +222,13 @@ def certidude_app(log_handlers=[]): from .scep import SCEPResource app.add_route("/api/scep/", SCEPResource()) - if config.OCSP_SUBNETS: - from .ocsp import OCSPResource - app.add_route("/api/ocsp/", OCSPResource()) # Add sink for serving static files app.add_sink(StaticResource(os.path.join(__file__, "..", "..", "static"))) - def log_exceptions(ex, req, resp, params): - logger.debug("Caught exception: %s" % ex) - raise ex - - app.add_error_handler(Exception, log_exceptions) + if config.OCSP_SUBNETS: + from .ocsp import OCSPResource + app.add_sink(OCSPResource(), prefix="/api/ocsp") # Set up log handlers if config.LOGGING_BACKEND == "sql": diff --git a/certidude/api/attrib.py b/certidude/api/attrib.py index 722c4ce..2277026 100644 --- a/certidude/api/attrib.py +++ b/certidude/api/attrib.py @@ -1,14 +1,24 @@ +import click import falcon import logging -from ipaddress import ip_address +import re +from xattr import setxattr, listxattr, removexattr from datetime import datetime -from certidude import config, authority -from certidude.decorators import serialize +from certidude import config, authority, push +from certidude.decorators import serialize, csrf_protection +from certidude.firewall import whitelist_subject +from certidude.auth import login_required, login_optional, authorize_admin +from ipaddress import ip_address logger = logging.getLogger(__name__) class AttributeResource(object): + def __init__(self, namespace): + self.namespace = namespace + @serialize + @login_required + @authorize_admin def on_get(self, req, resp, cn): """ Return extended attributes stored on the server. @@ -17,22 +27,32 @@ class AttributeResource(object): Results made available only to lease IP address. """ try: - path, buf, cert, attribs = authority.get_attributes(cn) + path, buf, cert, attribs = authority.get_attributes(cn, namespace=self.namespace) except IOError: raise falcon.HTTPNotFound() else: - try: - whitelist = ip_address(attribs.get("user").get("lease").get("inner_address").decode("ascii")) - except AttributeError: # TODO: probably race condition - raise falcon.HTTPForbidden("Forbidden", - "Attributes only accessible to the machine") - - if req.context.get("remote_addr") != whitelist: - logger.info("Attribute access denied from %s, expected %s for %s", - req.context.get("remote_addr"), - whitelist, - cn) - raise falcon.HTTPForbidden("Forbidden", - "Attributes only accessible to the machine") - return attribs + + @csrf_protection + @whitelist_subject # TODO: sign instead + def on_post(self, req, resp, cn): + try: + path, buf, cert = authority.get_signed(cn) + except IOError: + raise falcon.HTTPNotFound() + else: + for key in req.params: + if not re.match("[a-z0-9_\.]+$", key): + raise falcon.HTTPBadRequest("Invalid key") + valid = set() + for key, value in req.params.items(): + identifier = ("user.%s.%s" % (self.namespace, key)).encode("ascii") + setxattr(path, identifier, value.encode("utf-8")) + valid.add(identifier) + for key in listxattr(path): + if not key.startswith("user.%s." % self.namespace): + continue + if key not in valid: + removexattr(path, key) + push.publish("attribute-update", cn) + diff --git a/certidude/api/ocsp.py b/certidude/api/ocsp.py index e4776b0..2f07632 100644 --- a/certidude/api/ocsp.py +++ b/certidude/api/ocsp.py @@ -1,4 +1,3 @@ -from __future__ import unicode_literals, division, absolute_import, print_function import click import hashlib import os @@ -14,26 +13,35 @@ from oscrypto import keys, asymmetric, symmetric from oscrypto.errors import SignatureError class OCSPResource(object): - def on_post(self, req, resp): + def __call__(self, req, resp): + if req.method == "GET": + _, _, _, tail = req.path.split("/", 3) + body = b64decode(tail) + elif req.method == "POST": + body = req.stream.read(req.content_length or 0) + else: + raise falcon.HTTPMethodNotAllowed() + fh = open(config.AUTHORITY_CERTIFICATE_PATH) server_certificate = asymmetric.load_certificate(fh.read()) fh.close() - ocsp_req = ocsp.OCSPRequest.load(req.stream.read()) - print(ocsp_req["tbs_request"].native) - + ocsp_req = ocsp.OCSPRequest.load(body) now = datetime.now(timezone.utc) response_extensions = [] - for ext in ocsp_req["tbs_request"]["request_extensions"]: - if ext["extn_id"] == "nonce": - response_extensions.append( - ocsp.ResponseDataExtension({ - 'extn_id': "nonce", - 'critical': False, - 'extn_value': ext["extn_value"] - }) - ) + try: + for ext in ocsp_req["tbs_request"]["request_extensions"]: + if ext["extn_id"].native == "nonce": + response_extensions.append( + ocsp.ResponseDataExtension({ + 'extn_id': "nonce", + 'critical': False, + 'extn_value': ext["extn_value"] + }) + ) + except ValueError: # https://github.com/wbond/asn1crypto/issues/56 + pass responses = [] for item in ocsp_req["tbs_request"]["request_list"]: diff --git a/certidude/api/scep.py b/certidude/api/scep.py index 44bb4e7..ffb0b66 100644 --- a/certidude/api/scep.py +++ b/certidude/api/scep.py @@ -1,4 +1,3 @@ -from __future__ import unicode_literals, division, absolute_import, print_function import click import hashlib import os diff --git a/certidude/api/script.py b/certidude/api/script.py index fefa8c2..a3e78a8 100644 --- a/certidude/api/script.py +++ b/certidude/api/script.py @@ -1,21 +1,37 @@ import falcon import logging -from certidude import config, authority +from certidude import const, config, authority from certidude.decorators import serialize from jinja2 import Environment, FileSystemLoader +from certidude.firewall import whitelist_subject logger = logging.getLogger(__name__) env = Environment(loader=FileSystemLoader(config.SCRIPT_DIR), trim_blocks=True) class ScriptResource(): + @whitelist_subject def on_get(self, req, resp, cn): - try: path, buf, cert, attribs = authority.get_attributes(cn) except IOError: raise falcon.HTTPNotFound() else: - resp.set_header("Content-Type", "text/x-shellscript") - resp.body = env.get_template(config.SCRIPT_DEFAULT).render(attributes=attribs) + script = config.SCRIPT_DEFAULT + tags = [] + for tag in attribs.get("user").get("xdg").get("tags").split(","): + if "=" in tag: + k, v = tag.split("=", 1) + else: + k, v = "other", tag + if k == "script": + script = v + tags.append(dict(id=tag, key=k, value=v)) + resp.set_header("Content-Type", "text/x-shellscript") + resp.body = env.get_template(script).render( + authority_name=const.FQDN, + common_name=cn, + tags=tags, + attributes=attribs.get("user").get("machine")) + logger.info("Served script %s for %s at %s" % (script, cn, req.context["remote_addr"])) # TODO: Assert time is within reasonable range diff --git a/certidude/authority.py b/certidude/authority.py index de37684..61bfc96 100644 --- a/certidude/authority.py +++ b/certidude/authority.py @@ -1,4 +1,4 @@ -from __future__ import unicode_literals, division, absolute_import, print_function +from __future__ import division, absolute_import, print_function import click import os import random @@ -57,17 +57,19 @@ def get_revoked(serial): datetime.utcfromtimestamp(os.stat(path).st_ctime) -def get_attributes(cn): +def get_attributes(cn, namespace=None): path, buf, cert = get_signed(cn) attribs = dict() for key in listxattr(path): if not key.startswith("user."): continue + if namespace and not key.startswith("user.%s." % namespace): + continue value = getxattr(path, key) current = attribs if "." in key: - namespace, key = key.rsplit(".", 1) - for component in namespace.split("."): + prefix, key = key.rsplit(".", 1) + for component in prefix.split("."): if component not in current: current[component] = dict() current = current[component] @@ -324,7 +326,6 @@ def sign(common_name, overwrite=False): def _sign(csr, buf, overwrite=False): assert buf.startswith("-----BEGIN CERTIFICATE REQUEST-----\n") assert isinstance(csr, x509.CertificateSigningRequest) - from xattr import getxattr, listxattr, setxattr common_name, = csr.subject.get_attributes_for_oid(NameOID.COMMON_NAME) cert_path = os.path.join(config.SIGNED_DIR, "%s.pem" % common_name.value) diff --git a/certidude/cli.py b/certidude/cli.py index 2798944..eb77290 100755 --- a/certidude/cli.py +++ b/certidude/cli.py @@ -1004,24 +1004,15 @@ def certidude_setup_authority(username, kerberos_keytab, nginx_config, country, static_path = os.path.join(os.path.realpath(os.path.dirname(__file__)), "static") certidude_path = sys.argv[0] - # Push server config generation - if os.path.exists("/etc/nginx"): - listen = "127.0.1.1" - port = "8080" - click.echo("Generating: %s" % nginx_config.name) - nginx_config.write(env.get_template("server/nginx.conf").render(vars())) - nginx_config.close() - if not os.path.exists("/etc/nginx/sites-enabled/certidude.conf"): - os.symlink("../sites-available/certidude.conf", "/etc/nginx/sites-enabled/certidude.conf") - click.echo("Symlinked %s -> /etc/nginx/sites-enabled/" % nginx_config.name) - if os.path.exists("/etc/nginx/sites-enabled/default"): - os.unlink("/etc/nginx/sites-enabled/default") - os.system("service nginx restart") - else: - click.echo("Directory /etc/nginx does not exist, hence not creating nginx configuration") - click.echo("Remember to install/configure nchan capable nginx instead of regular nginx!") - listen = "0.0.0.0" - port = "80" + click.echo("Generating: %s" % nginx_config.name) + nginx_config.write(env.get_template("server/nginx.conf").render(vars())) + nginx_config.close() + if not os.path.exists("/etc/nginx/sites-enabled/certidude.conf"): + os.symlink("../sites-available/certidude.conf", "/etc/nginx/sites-enabled/certidude.conf") + click.echo("Symlinked %s -> /etc/nginx/sites-enabled/" % nginx_config.name) + if os.path.exists("/etc/nginx/sites-enabled/default"): + os.unlink("/etc/nginx/sites-enabled/default") + os.system("service nginx restart") if os.path.exists("/etc/systemd"): if os.path.exists("/etc/systemd/system/certidude.service"): @@ -1284,16 +1275,19 @@ def certidude_cron(): @click.command("serve", help="Run server") @click.option("-e", "--exit-handler", default=False, is_flag=True, help="Install /api/exit/ handler") -@click.option("-p", "--port", default=80, help="Listen port") -@click.option("-l", "--listen", default="0.0.0.0", help="Listen address") +@click.option("-p", "--port", default=8080, help="Listen port") +@click.option("-l", "--listen", default="127.0.0.1", help="Listen address") @click.option("-f", "--fork", default=False, is_flag=True, help="Fork to background") def certidude_serve(port, listen, fork, exit_handler): import pwd from setproctitle import setproctitle from certidude.signer import SignServer from certidude import authority, const - click.echo("Using configuration from: %s" % const.CONFIG_PATH) + 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.CONFIG_PATH) log_handlers = [] diff --git a/certidude/config.py b/certidude/config.py index 13a6de8..9768faf 100644 --- a/certidude/config.py +++ b/certidude/config.py @@ -98,4 +98,4 @@ TOKEN_SECRET = cp.get("token", "secret") # The API call for looking up scripts uses following directory as root SCRIPT_DIR = os.path.join(os.path.dirname(__file__), "templates", "script") -SCRIPT_DEFAULT = "openwrt.sh" +SCRIPT_DEFAULT = "default.sh" diff --git a/certidude/firewall.py b/certidude/firewall.py index b36df49..74a1936 100644 --- a/certidude/firewall.py +++ b/certidude/firewall.py @@ -1,4 +1,5 @@ +import falcon import logging logger = logging.getLogger("api") @@ -20,7 +21,7 @@ def whitelist_subnets(subnets): req.env["PATH_INFO"], req.context.get("user", "unauthenticated user"), req.context.get("remote_addr")) - raise falcon.HTTPForbidden("Forbidden", "Remote address %s not whitelisted" % remote_addr) + raise falcon.HTTPForbidden("Forbidden", "Remote address %s not whitelisted" % req.context.get("remote_addr")) return func(self, req, resp, *args, **kwargs) return wrapped @@ -39,3 +40,19 @@ def whitelist_content_types(*content_types): return wrapped return wrapper +def whitelist_subject(func): + def wrapped(self, req, resp, cn, *args, **kwargs): + from ipaddress import ip_address + from certidude import authority + from xattr import getxattr + try: + path, buf, cert = authority.get_signed(cn) + except IOError: + raise falcon.HTTPNotFound() + else: + inner_address = getxattr(path, "user.lease.inner_address").decode("ascii") + if req.context.get("remote_addr") != ip_address(inner_address): + raise falcon.HTTPForbidden("Forbidden", "Remote address %s not whitelisted" % req.context.get("remote_addr")) + return func(self, req, resp, cn, *args, **kwargs) + return wrapped + diff --git a/certidude/static/css/style.css b/certidude/static/css/style.css index 3b84e7d..54d31a9 100644 --- a/certidude/static/css/style.css +++ b/certidude/static/css/style.css @@ -183,7 +183,8 @@ pre { padding-bottom: 2px; } -.tags .tag { +.tags .tag, +.attributes .attribute { display: inline; background-size: 24px; background-position: 0 4px; @@ -195,6 +196,15 @@ pre { white-space: nowrap; } +.attribute { + opacity: 0.25; +} + +.attribute:hover { + opacity: 1; +} + + select { -webkit-appearance: none; -moz-appearance: none; @@ -205,7 +215,8 @@ select { } -.icon.tag { background-image: url("../img/iconmonstr-tag-3.svg"); } +.icon.tag, +.icon.attribute { background-image: url("../img/iconmonstr-tag-3.svg"); } .icon.critical { background-image: url("../img/iconmonstr-error-4.svg"); } .icon.error { background-image: url("../img/iconmonstr-error-4.svg"); } @@ -227,5 +238,12 @@ select { .icon.upload { background-image: url("../img/iconmonstr-upload-17.svg"); } +.icon.dist, +.icon.kernel { background-image: url("../img/iconmonstr-linux-os-1.svg"); } +.icon.if { background-image: url("../img/iconmonstr-sitemap-5.svg"); } +.icon.cpu, +.icon.mem, +.icon.ram { background-image: url("../img/iconmonstr-cpu-1.svg"); } + /* Make sure this is the last one */ .icon.busy{background-image:url("https://software.opensuse.org/assets/ajax-loader-ea46060b6c9f42822a3d58d075c83ea2.gif");} diff --git a/certidude/static/img/iconmonstr-cpu-1.svg b/certidude/static/img/iconmonstr-cpu-1.svg new file mode 100644 index 0000000..545383b --- /dev/null +++ b/certidude/static/img/iconmonstr-cpu-1.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/certidude/static/img/iconmonstr-linux-os-1.svg b/certidude/static/img/iconmonstr-linux-os-1.svg new file mode 100644 index 0000000..01e7883 --- /dev/null +++ b/certidude/static/img/iconmonstr-linux-os-1.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/certidude/static/img/iconmonstr-sitemap-5.svg b/certidude/static/img/iconmonstr-sitemap-5.svg new file mode 100644 index 0000000..b13e1ba --- /dev/null +++ b/certidude/static/img/iconmonstr-sitemap-5.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/certidude/static/js/certidude.js b/certidude/static/js/certidude.js index 17782ba..b2e1a60 100644 --- a/certidude/static/js/certidude.js +++ b/certidude/static/js/certidude.js @@ -181,6 +181,24 @@ function onTagUpdated(e) { }) } +function onAttributeUpdated(e) { + var cn = e.data; + console.log("Attributes updated", cn); + $.ajax({ + method: "GET", + url: "/api/signed/" + cn + "/attr/", + dataType: "json", + success:function(attributes, status, xhr) { + console.info("Updated", cn, "attributes", attributes); + $(".attributes[data-cn='" + cn + "']").html( + nunjucks.render('views/attributes.html', { + certificate: { + common_name: cn, + attributes:attributes }})); + } + }) +} + $(document).ready(function() { console.info("Loading CA, to debug: curl " + window.location.href + " --negotiate -u : -H 'Accept: application/json'"); $.ajax({ @@ -228,6 +246,7 @@ $(document).ready(function() { source.addEventListener("request-signed", onRequestSigned); source.addEventListener("certificate-revoked", onCertificateRevoked); source.addEventListener("tag-update", onTagUpdated); + source.addEventListener("attribute-update", onAttributeUpdated); console.info("Swtiching to requests section"); $("section").hide(); diff --git a/certidude/static/views/attributes.html b/certidude/static/views/attributes.html new file mode 100644 index 0000000..5e37128 --- /dev/null +++ b/certidude/static/views/attributes.html @@ -0,0 +1,3 @@ +{% for key, value in certificate.attributes %} +{{ value }} +{% endfor %} diff --git a/certidude/static/views/signed.html b/certidude/static/views/signed.html index c7b9126..b474750 100644 --- a/certidude/static/views/signed.html +++ b/certidude/static/views/signed.html @@ -61,4 +61,8 @@
{% include 'views/status.html' %}
+ +
+ {% include 'views/attributes.html' %} +
diff --git a/certidude/templates/script/default.sh b/certidude/templates/script/default.sh new file mode 100644 index 0000000..2729274 --- /dev/null +++ b/certidude/templates/script/default.sh @@ -0,0 +1,15 @@ +#!/bin/sh + +# Tags: +{% for tag in tags %} +# {{ tag }} +{% endfor %} + +curl http://{{ authority_name }}/api/signed/{{ common_name }}/attr -X POST -d "\ +dmi.product_name=$(cat /sys/class/dmi/id/product_name)&\ +dmi.product_serial=$(cat /sys/class/dmi/id/product_serial)&\ +kernel=$(uname -sr)&\ +dist=$(lsb_release -si) $(lsb_release -sr)&\ +cpu=$(cat /proc/cpuinfo | grep '^model name' | head -n1 | cut -d ":" -f2 | xargs)&\ +mem=$(dmidecode -t 17 | grep Size | cut -d ":" -f 2 | cut -d " " -f 2 | paste -sd+ | bc) MB&\ +$(for j in /sys/class/net/[we]*; do echo -en if.$(basename $j).ether=$(cat $j/address)\&; done)" diff --git a/certidude/templates/script/openwrt.sh b/certidude/templates/script/openwrt.sh index 78045c7..61f7664 100644 --- a/certidude/templates/script/openwrt.sh +++ b/certidude/templates/script/openwrt.sh @@ -1,6 +1,6 @@ #!/bin/sh -# This script can executed on a preconfigured OpenWrt box +# This script can be executed on a preconfigured OpenWrt box # https://lauri.vosandi.com/2017/01/reconfiguring-openwrt-as-dummy-ap.html # Password protected wireless area @@ -10,13 +10,13 @@ for band in 2ghz 5ghz; do uci set wireless.lan$band.mode=ap uci set wireless.lan$band.device=radio$band uci set wireless.lan$band.encryption=psk2 - {% if attributes.protected and attributes.protected.ssid %} - uci set wireless.lan$band.ssid={{ attrbutes.protected.ssid }} + {% if attributes.wireless.protected and attributes.wireless.protected.ssid %} + uci set wireless.lan$band.ssid={{ attrbutes.wireless.protected.ssid }} {% else %} uci set wireless.lan$band.ssid=$(uci get system.@system[0].hostname)-protected {% endif %} - {% if attributes.protected and attributes.protected.psk %} - uci set wireless.lan$band.key={{ attributes.protected.psk }} + {% if attributes.wireless.protected and attributes.wireless.protected.psk %} + uci set wireless.lan$band.key={{ attributes.wireless.protected.psk }} {% else %} uci set wireless.lan$band.key=salakala {% endif %} @@ -29,8 +29,8 @@ for band in 2ghz 5ghz; do uci set wireless.guest$band.mode=ap uci set wireless.guest$band.device=radio$band uci set wireless.guest$band.encryption=none - {% if attributes.public and attributes.public.ssid %} - uci set wireless.guest$band.ssid={{ attrbutes.public.ssid }} + {% if attributes.wireless.public and attributes.wireless.public.ssid %} + uci set wireless.guest$band.ssid={{ attrbutes.wireless.public.ssid }} {% else %} uci set wireless.guest$band.ssid=$(uci get system.@system[0].hostname)-public {% endif %} diff --git a/certidude/templates/server/nginx.conf b/certidude/templates/server/nginx.conf index a92d91d..8073842 100644 --- a/certidude/templates/server/nginx.conf +++ b/certidude/templates/server/nginx.conf @@ -23,7 +23,7 @@ server { root {{static_path}}; location /api/ { - proxy_pass http://127.0.1.1{% if port != 80 %}:{{ port }}{% endif %}/api/; + proxy_pass http://127.0.1.1:8080/api/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_connect_timeout 600; diff --git a/certidude/templates/server/systemd.service b/certidude/templates/server/systemd.service index 489b79c..850343c 100644 --- a/certidude/templates/server/systemd.service +++ b/certidude/templates/server/systemd.service @@ -7,8 +7,7 @@ Environment=PYTHON_EGG_CACHE=/tmp/.cache PIDFile=/run/certidude/server.pid ExecReload=/bin/kill -s HUP $MAINPID ExecStop=/bin/kill -s TERM $MAINPID -ExecStart={{ certidude_path }} serve {% if listen %} -l {{listen}}{% endif %}{% if port %} -p {{port}}{% endif %} - +ExecStart={{ certidude_path }} serve [Install] WantedBy=multi-user.target