mirror of
https://github.com/laurivosandi/certidude
synced 2024-12-22 16:25:17 +00:00
Several changes
* OCSP workaround for StrongSwan * Machine attributes framework * Scripting support * Default to nginx frontend
This commit is contained in:
parent
d08a3f9f92
commit
9b5511212e
@ -8,6 +8,7 @@ import click
|
|||||||
import hashlib
|
import hashlib
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from time import sleep
|
from time import sleep
|
||||||
|
from xattr import listxattr, getxattr
|
||||||
from certidude import authority, mailer
|
from certidude import authority, mailer
|
||||||
from certidude.auth import login_required, authorize_admin
|
from certidude.auth import login_required, authorize_admin
|
||||||
from certidude.user import User
|
from certidude.user import User
|
||||||
@ -32,7 +33,6 @@ class SessionResource(object):
|
|||||||
@serialize
|
@serialize
|
||||||
@login_required
|
@login_required
|
||||||
def on_get(self, req, resp):
|
def on_get(self, req, resp):
|
||||||
import xattr
|
|
||||||
|
|
||||||
def serialize_requests(g):
|
def serialize_requests(g):
|
||||||
for common_name, path, buf, obj, server in g():
|
for common_name, path, buf, obj, server in g():
|
||||||
@ -50,7 +50,7 @@ class SessionResource(object):
|
|||||||
# Extract certificate tags from filesystem
|
# Extract certificate tags from filesystem
|
||||||
try:
|
try:
|
||||||
tags = []
|
tags = []
|
||||||
for tag in xattr.getxattr(path, "user.xdg.tags").split(","):
|
for tag in getxattr(path, "user.xdg.tags").split(","):
|
||||||
if "=" in tag:
|
if "=" in tag:
|
||||||
k, v = tag.split("=", 1)
|
k, v = tag.split("=", 1)
|
||||||
else:
|
else:
|
||||||
@ -59,12 +59,17 @@ class SessionResource(object):
|
|||||||
except IOError: # No such attribute(s)
|
except IOError: # No such attribute(s)
|
||||||
tags = None
|
tags = None
|
||||||
|
|
||||||
|
attributes = {}
|
||||||
|
for key in listxattr(path):
|
||||||
|
if key.startswith("user.machine."):
|
||||||
|
attributes[key[13:]] = getxattr(path, key)
|
||||||
|
|
||||||
# Extract lease information from filesystem
|
# Extract lease information from filesystem
|
||||||
try:
|
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(
|
lease = dict(
|
||||||
inner_address = xattr.getxattr(path, "user.lease.inner_address"),
|
inner_address = getxattr(path, "user.lease.inner_address"),
|
||||||
outer_address = xattr.getxattr(path, "user.lease.outer_address"),
|
outer_address = getxattr(path, "user.lease.outer_address"),
|
||||||
last_seen = last_seen,
|
last_seen = last_seen,
|
||||||
age = datetime.utcnow() - last_seen
|
age = datetime.utcnow() - last_seen
|
||||||
)
|
)
|
||||||
@ -80,7 +85,8 @@ class SessionResource(object):
|
|||||||
expires = obj.not_valid_after,
|
expires = obj.not_valid_after,
|
||||||
sha256sum = hashlib.sha256(buf).hexdigest(),
|
sha256sum = hashlib.sha256(buf).hexdigest(),
|
||||||
lease = lease,
|
lease = lease,
|
||||||
tags = tags
|
tags = tags,
|
||||||
|
attributes = attributes or None,
|
||||||
)
|
)
|
||||||
|
|
||||||
if req.context.get("user").is_admin():
|
if req.context.get("user").is_admin():
|
||||||
@ -160,7 +166,7 @@ import ipaddress
|
|||||||
class NormalizeMiddleware(object):
|
class NormalizeMiddleware(object):
|
||||||
def process_request(self, req, resp, *args):
|
def process_request(self, req, resp, *args):
|
||||||
assert not req.get_param("unicode") or req.get_param("unicode") == u"✓", "Unicode sanity check failed"
|
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):
|
def process_response(self, req, resp, resource=None):
|
||||||
# wtf falcon?!
|
# wtf falcon?!
|
||||||
@ -181,6 +187,7 @@ def certidude_app(log_handlers=[]):
|
|||||||
|
|
||||||
app = falcon.API(middleware=NormalizeMiddleware())
|
app = falcon.API(middleware=NormalizeMiddleware())
|
||||||
app.req_options.auto_parse_form_urlencoded = True
|
app.req_options.auto_parse_form_urlencoded = True
|
||||||
|
#app.req_options.strip_url_path_trailing_slash = False
|
||||||
|
|
||||||
# Certificate authority API calls
|
# Certificate authority API calls
|
||||||
app.add_route("/api/certificate/", CertificateAuthorityResource())
|
app.add_route("/api/certificate/", CertificateAuthorityResource())
|
||||||
@ -194,7 +201,7 @@ def certidude_app(log_handlers=[]):
|
|||||||
app.add_route("/api/token/", TokenResource())
|
app.add_route("/api/token/", TokenResource())
|
||||||
|
|
||||||
# Extended attributes for scripting etc.
|
# 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())
|
app.add_route("/api/signed/{cn}/script/", ScriptResource())
|
||||||
|
|
||||||
# API calls used by pushed events on the JS end
|
# API calls used by pushed events on the JS end
|
||||||
@ -215,18 +222,13 @@ def certidude_app(log_handlers=[]):
|
|||||||
from .scep import SCEPResource
|
from .scep import SCEPResource
|
||||||
app.add_route("/api/scep/", 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
|
# Add sink for serving static files
|
||||||
app.add_sink(StaticResource(os.path.join(__file__, "..", "..", "static")))
|
app.add_sink(StaticResource(os.path.join(__file__, "..", "..", "static")))
|
||||||
|
|
||||||
def log_exceptions(ex, req, resp, params):
|
if config.OCSP_SUBNETS:
|
||||||
logger.debug("Caught exception: %s" % ex)
|
from .ocsp import OCSPResource
|
||||||
raise ex
|
app.add_sink(OCSPResource(), prefix="/api/ocsp")
|
||||||
|
|
||||||
app.add_error_handler(Exception, log_exceptions)
|
|
||||||
|
|
||||||
# Set up log handlers
|
# Set up log handlers
|
||||||
if config.LOGGING_BACKEND == "sql":
|
if config.LOGGING_BACKEND == "sql":
|
||||||
|
@ -1,14 +1,24 @@
|
|||||||
|
import click
|
||||||
import falcon
|
import falcon
|
||||||
import logging
|
import logging
|
||||||
from ipaddress import ip_address
|
import re
|
||||||
|
from xattr import setxattr, listxattr, removexattr
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from certidude import config, authority
|
from certidude import config, authority, push
|
||||||
from certidude.decorators import serialize
|
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__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
class AttributeResource(object):
|
class AttributeResource(object):
|
||||||
|
def __init__(self, namespace):
|
||||||
|
self.namespace = namespace
|
||||||
|
|
||||||
@serialize
|
@serialize
|
||||||
|
@login_required
|
||||||
|
@authorize_admin
|
||||||
def on_get(self, req, resp, cn):
|
def on_get(self, req, resp, cn):
|
||||||
"""
|
"""
|
||||||
Return extended attributes stored on the server.
|
Return extended attributes stored on the server.
|
||||||
@ -17,22 +27,32 @@ class AttributeResource(object):
|
|||||||
Results made available only to lease IP address.
|
Results made available only to lease IP address.
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
path, buf, cert, attribs = authority.get_attributes(cn)
|
path, buf, cert, attribs = authority.get_attributes(cn, namespace=self.namespace)
|
||||||
except IOError:
|
except IOError:
|
||||||
raise falcon.HTTPNotFound()
|
raise falcon.HTTPNotFound()
|
||||||
else:
|
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
|
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)
|
||||||
|
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
from __future__ import unicode_literals, division, absolute_import, print_function
|
|
||||||
import click
|
import click
|
||||||
import hashlib
|
import hashlib
|
||||||
import os
|
import os
|
||||||
@ -14,26 +13,35 @@ from oscrypto import keys, asymmetric, symmetric
|
|||||||
from oscrypto.errors import SignatureError
|
from oscrypto.errors import SignatureError
|
||||||
|
|
||||||
class OCSPResource(object):
|
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)
|
fh = open(config.AUTHORITY_CERTIFICATE_PATH)
|
||||||
server_certificate = asymmetric.load_certificate(fh.read())
|
server_certificate = asymmetric.load_certificate(fh.read())
|
||||||
fh.close()
|
fh.close()
|
||||||
|
|
||||||
ocsp_req = ocsp.OCSPRequest.load(req.stream.read())
|
ocsp_req = ocsp.OCSPRequest.load(body)
|
||||||
print(ocsp_req["tbs_request"].native)
|
|
||||||
|
|
||||||
now = datetime.now(timezone.utc)
|
now = datetime.now(timezone.utc)
|
||||||
response_extensions = []
|
response_extensions = []
|
||||||
|
|
||||||
for ext in ocsp_req["tbs_request"]["request_extensions"]:
|
try:
|
||||||
if ext["extn_id"] == "nonce":
|
for ext in ocsp_req["tbs_request"]["request_extensions"]:
|
||||||
response_extensions.append(
|
if ext["extn_id"].native == "nonce":
|
||||||
ocsp.ResponseDataExtension({
|
response_extensions.append(
|
||||||
'extn_id': "nonce",
|
ocsp.ResponseDataExtension({
|
||||||
'critical': False,
|
'extn_id': "nonce",
|
||||||
'extn_value': ext["extn_value"]
|
'critical': False,
|
||||||
})
|
'extn_value': ext["extn_value"]
|
||||||
)
|
})
|
||||||
|
)
|
||||||
|
except ValueError: # https://github.com/wbond/asn1crypto/issues/56
|
||||||
|
pass
|
||||||
|
|
||||||
responses = []
|
responses = []
|
||||||
for item in ocsp_req["tbs_request"]["request_list"]:
|
for item in ocsp_req["tbs_request"]["request_list"]:
|
||||||
|
@ -1,4 +1,3 @@
|
|||||||
from __future__ import unicode_literals, division, absolute_import, print_function
|
|
||||||
import click
|
import click
|
||||||
import hashlib
|
import hashlib
|
||||||
import os
|
import os
|
||||||
|
@ -1,21 +1,37 @@
|
|||||||
import falcon
|
import falcon
|
||||||
import logging
|
import logging
|
||||||
from certidude import config, authority
|
from certidude import const, config, authority
|
||||||
from certidude.decorators import serialize
|
from certidude.decorators import serialize
|
||||||
from jinja2 import Environment, FileSystemLoader
|
from jinja2 import Environment, FileSystemLoader
|
||||||
|
from certidude.firewall import whitelist_subject
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
env = Environment(loader=FileSystemLoader(config.SCRIPT_DIR), trim_blocks=True)
|
env = Environment(loader=FileSystemLoader(config.SCRIPT_DIR), trim_blocks=True)
|
||||||
|
|
||||||
class ScriptResource():
|
class ScriptResource():
|
||||||
|
@whitelist_subject
|
||||||
def on_get(self, req, resp, cn):
|
def on_get(self, req, resp, cn):
|
||||||
|
|
||||||
try:
|
try:
|
||||||
path, buf, cert, attribs = authority.get_attributes(cn)
|
path, buf, cert, attribs = authority.get_attributes(cn)
|
||||||
except IOError:
|
except IOError:
|
||||||
raise falcon.HTTPNotFound()
|
raise falcon.HTTPNotFound()
|
||||||
else:
|
else:
|
||||||
resp.set_header("Content-Type", "text/x-shellscript")
|
script = config.SCRIPT_DEFAULT
|
||||||
resp.body = env.get_template(config.SCRIPT_DEFAULT).render(attributes=attribs)
|
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
|
# TODO: Assert time is within reasonable range
|
||||||
|
@ -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 click
|
||||||
import os
|
import os
|
||||||
import random
|
import random
|
||||||
@ -57,17 +57,19 @@ def get_revoked(serial):
|
|||||||
datetime.utcfromtimestamp(os.stat(path).st_ctime)
|
datetime.utcfromtimestamp(os.stat(path).st_ctime)
|
||||||
|
|
||||||
|
|
||||||
def get_attributes(cn):
|
def get_attributes(cn, namespace=None):
|
||||||
path, buf, cert = get_signed(cn)
|
path, buf, cert = get_signed(cn)
|
||||||
attribs = dict()
|
attribs = dict()
|
||||||
for key in listxattr(path):
|
for key in listxattr(path):
|
||||||
if not key.startswith("user."):
|
if not key.startswith("user."):
|
||||||
continue
|
continue
|
||||||
|
if namespace and not key.startswith("user.%s." % namespace):
|
||||||
|
continue
|
||||||
value = getxattr(path, key)
|
value = getxattr(path, key)
|
||||||
current = attribs
|
current = attribs
|
||||||
if "." in key:
|
if "." in key:
|
||||||
namespace, key = key.rsplit(".", 1)
|
prefix, key = key.rsplit(".", 1)
|
||||||
for component in namespace.split("."):
|
for component in prefix.split("."):
|
||||||
if component not in current:
|
if component not in current:
|
||||||
current[component] = dict()
|
current[component] = dict()
|
||||||
current = current[component]
|
current = current[component]
|
||||||
@ -324,7 +326,6 @@ def sign(common_name, overwrite=False):
|
|||||||
def _sign(csr, buf, overwrite=False):
|
def _sign(csr, buf, overwrite=False):
|
||||||
assert buf.startswith("-----BEGIN CERTIFICATE REQUEST-----\n")
|
assert buf.startswith("-----BEGIN CERTIFICATE REQUEST-----\n")
|
||||||
assert isinstance(csr, x509.CertificateSigningRequest)
|
assert isinstance(csr, x509.CertificateSigningRequest)
|
||||||
from xattr import getxattr, listxattr, setxattr
|
|
||||||
|
|
||||||
common_name, = csr.subject.get_attributes_for_oid(NameOID.COMMON_NAME)
|
common_name, = csr.subject.get_attributes_for_oid(NameOID.COMMON_NAME)
|
||||||
cert_path = os.path.join(config.SIGNED_DIR, "%s.pem" % common_name.value)
|
cert_path = os.path.join(config.SIGNED_DIR, "%s.pem" % common_name.value)
|
||||||
|
@ -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")
|
static_path = os.path.join(os.path.realpath(os.path.dirname(__file__)), "static")
|
||||||
certidude_path = sys.argv[0]
|
certidude_path = sys.argv[0]
|
||||||
|
|
||||||
# Push server config generation
|
click.echo("Generating: %s" % nginx_config.name)
|
||||||
if os.path.exists("/etc/nginx"):
|
nginx_config.write(env.get_template("server/nginx.conf").render(vars()))
|
||||||
listen = "127.0.1.1"
|
nginx_config.close()
|
||||||
port = "8080"
|
if not os.path.exists("/etc/nginx/sites-enabled/certidude.conf"):
|
||||||
click.echo("Generating: %s" % nginx_config.name)
|
os.symlink("../sites-available/certidude.conf", "/etc/nginx/sites-enabled/certidude.conf")
|
||||||
nginx_config.write(env.get_template("server/nginx.conf").render(vars()))
|
click.echo("Symlinked %s -> /etc/nginx/sites-enabled/" % nginx_config.name)
|
||||||
nginx_config.close()
|
if os.path.exists("/etc/nginx/sites-enabled/default"):
|
||||||
if not os.path.exists("/etc/nginx/sites-enabled/certidude.conf"):
|
os.unlink("/etc/nginx/sites-enabled/default")
|
||||||
os.symlink("../sites-available/certidude.conf", "/etc/nginx/sites-enabled/certidude.conf")
|
os.system("service nginx restart")
|
||||||
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"
|
|
||||||
|
|
||||||
if os.path.exists("/etc/systemd"):
|
if os.path.exists("/etc/systemd"):
|
||||||
if os.path.exists("/etc/systemd/system/certidude.service"):
|
if os.path.exists("/etc/systemd/system/certidude.service"):
|
||||||
@ -1284,16 +1275,19 @@ def certidude_cron():
|
|||||||
|
|
||||||
@click.command("serve", help="Run server")
|
@click.command("serve", help="Run server")
|
||||||
@click.option("-e", "--exit-handler", default=False, is_flag=True, help="Install /api/exit/ handler")
|
@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("-p", "--port", default=8080, help="Listen port")
|
||||||
@click.option("-l", "--listen", default="0.0.0.0", help="Listen address")
|
@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")
|
@click.option("-f", "--fork", default=False, is_flag=True, help="Fork to background")
|
||||||
def certidude_serve(port, listen, fork, exit_handler):
|
def certidude_serve(port, listen, fork, exit_handler):
|
||||||
import pwd
|
import pwd
|
||||||
from setproctitle import setproctitle
|
from setproctitle import setproctitle
|
||||||
from certidude.signer import SignServer
|
from certidude.signer import SignServer
|
||||||
from certidude import authority, const
|
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 = []
|
log_handlers = []
|
||||||
|
|
||||||
|
@ -98,4 +98,4 @@ TOKEN_SECRET = cp.get("token", "secret")
|
|||||||
|
|
||||||
# The API call for looking up scripts uses following directory as root
|
# The API call for looking up scripts uses following directory as root
|
||||||
SCRIPT_DIR = os.path.join(os.path.dirname(__file__), "templates", "script")
|
SCRIPT_DIR = os.path.join(os.path.dirname(__file__), "templates", "script")
|
||||||
SCRIPT_DEFAULT = "openwrt.sh"
|
SCRIPT_DEFAULT = "default.sh"
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
|
|
||||||
|
import falcon
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
logger = logging.getLogger("api")
|
logger = logging.getLogger("api")
|
||||||
@ -20,7 +21,7 @@ def whitelist_subnets(subnets):
|
|||||||
req.env["PATH_INFO"],
|
req.env["PATH_INFO"],
|
||||||
req.context.get("user", "unauthenticated user"),
|
req.context.get("user", "unauthenticated user"),
|
||||||
req.context.get("remote_addr"))
|
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 func(self, req, resp, *args, **kwargs)
|
||||||
return wrapped
|
return wrapped
|
||||||
@ -39,3 +40,19 @@ def whitelist_content_types(*content_types):
|
|||||||
return wrapped
|
return wrapped
|
||||||
return wrapper
|
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
|
||||||
|
|
||||||
|
@ -183,7 +183,8 @@ pre {
|
|||||||
padding-bottom: 2px;
|
padding-bottom: 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.tags .tag {
|
.tags .tag,
|
||||||
|
.attributes .attribute {
|
||||||
display: inline;
|
display: inline;
|
||||||
background-size: 24px;
|
background-size: 24px;
|
||||||
background-position: 0 4px;
|
background-position: 0 4px;
|
||||||
@ -195,6 +196,15 @@ pre {
|
|||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.attribute {
|
||||||
|
opacity: 0.25;
|
||||||
|
}
|
||||||
|
|
||||||
|
.attribute:hover {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
select {
|
select {
|
||||||
-webkit-appearance: none;
|
-webkit-appearance: none;
|
||||||
-moz-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.critical { background-image: url("../img/iconmonstr-error-4.svg"); }
|
||||||
.icon.error { 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.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 */
|
/* Make sure this is the last one */
|
||||||
.icon.busy{background-image:url("https://software.opensuse.org/assets/ajax-loader-ea46060b6c9f42822a3d58d075c83ea2.gif");}
|
.icon.busy{background-image:url("https://software.opensuse.org/assets/ajax-loader-ea46060b6c9f42822a3d58d075c83ea2.gif");}
|
||||||
|
1
certidude/static/img/iconmonstr-cpu-1.svg
Normal file
1
certidude/static/img/iconmonstr-cpu-1.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M19 17c0 1.104-.896 2-2 2h-11c-1.104 0-2-.896-2-2v-11c0-1.104.896-2 2-2h11c1.104 0 2 .896 2 2v11zm-11 3v3h-1v-3h1zm4 0v3h-1v-3h1zm2 0v3h-1v-3h1zm-4 0v3h-1v-3h1zm6 0v3h-1v-3h1zm-8-20v3h-1v-3h1zm4 0v3h-1v-3h1zm2 0v3h-1v-3h1zm-4 0v3h-1v-3h1zm6 0v3h-1v-3h1zm4 15h3v1h-3v-1zm0-4h3v1h-3v-1zm0-2h3v1h-3v-1zm0 4h3v1h-3v-1zm0-6h3v1h-3v-1zm-20 8h3v1h-3v-1zm0-4h3v1h-3v-1zm0-2h3v1h-3v-1zm0 4h3v1h-3v-1zm0-6h3v1h-3v-1z"/></svg>
|
After Width: | Height: | Size: 507 B |
1
certidude/static/img/iconmonstr-linux-os-1.svg
Normal file
1
certidude/static/img/iconmonstr-linux-os-1.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M20.581 19.049c-.55-.446-.336-1.431-.907-1.917.553-3.365-.997-6.331-2.845-8.232-1.551-1.595-1.051-3.147-1.051-4.49 0-2.146-.881-4.41-3.55-4.41-2.853 0-3.635 2.38-3.663 3.738-.068 3.262.659 4.11-1.25 6.484-2.246 2.793-2.577 5.579-2.07 7.057-.237.276-.557.582-1.155.835-1.652.72-.441 1.925-.898 2.78-.13.243-.192.497-.192.74 0 .75.596 1.399 1.679 1.302 1.461-.13 2.809.905 3.681.905.77 0 1.402-.438 1.696-1.041 1.377-.339 3.077-.296 4.453.059.247.691.917 1.141 1.662 1.141 1.631 0 1.945-1.849 3.816-2.475.674-.225 1.013-.879 1.013-1.488 0-.39-.139-.761-.419-.988zm-9.147-10.465c-.319 0-.583-.258-1-.568-.528-.392-1.065-.618-1.059-1.03 0-.283.379-.37.869-.681.526-.333.731-.671 1.249-.671.53 0 .69.268 1.41.579.708.307 1.201.427 1.201.773 0 .355-.741.609-1.158.868-.613.378-.928.73-1.512.73zm1.665-5.215c.882.141.981 1.691.559 2.454l-.355-.145c.184-.543.181-1.437-.435-1.494-.391-.036-.643.48-.697.922-.153-.064-.32-.11-.523-.127.062-.923.658-1.737 1.451-1.61zm-3.403.331c.676-.168 1.075.618 1.078 1.435l-.31.19c-.042-.343-.195-.897-.579-.779-.411.128-.344 1.083-.115 1.279l-.306.17c-.42-.707-.419-2.133.232-2.295zm-2.115 19.243c-1.963-.893-2.63-.69-3.005-.69-.777 0-1.031-.579-.739-1.127.248-.465.171-.952.11-1.343-.094-.599-.111-.794.478-1.052.815-.346 1.177-.791 1.447-1.124.758-.937 1.523.537 2.15 1.85.407.851 1.208 1.282 1.455 2.225.227.871-.71 1.801-1.896 1.261zm6.987-1.874c-1.384.673-3.147.982-4.466.299-.195-.563-.507-.927-.843-1.293.539-.142.939-.814.46-1.489-.511-.721-1.555-1.224-2.61-2.04-.987-.763-1.299-2.644.045-4.746-.655 1.862-.272 3.578.057 4.069.068-.988.146-2.638 1.496-4.615.681-.998.691-2.316.706-3.14l.62.424c.456.337.838.708 1.386.708.81 0 1.258-.466 1.882-.853.244-.15.613-.302.923-.513.52 2.476 2.674 5.454 2.795 7.15.501-1.032-.142-3.514-.142-3.514.842 1.285.909 2.356.946 3.67.589.241 1.221.869 1.279 1.696l-.245-.028c-.126-.919-2.607-2.269-2.83-.539-1.19.181-.757 2.066-.997 3.288-.11.559-.314 1.001-.462 1.466zm4.846-.041c-.985.38-1.65 1.187-2.107 1.688-.88.966-2.044.503-2.168-.401-.131-.966.36-1.493.572-2.574.193-.987-.023-2.506.431-2.668.295 1.753 2.066 1.016 2.47.538.657 0 .712.222.859.837.092.385.219.709.578 1.09.418.447.29 1.133-.635 1.49zm-8-13.006c-.651 0-1.138-.433-1.534-.769-.203-.171.05-.487.253-.315.387.328.777.675 1.281.675.607 0 1.142-.519 1.867-.805.247-.097.388.285.143.382-.704.277-1.269.832-2.01.832z"/></svg>
|
After Width: | Height: | Size: 2.4 KiB |
1
certidude/static/img/iconmonstr-sitemap-5.svg
Normal file
1
certidude/static/img/iconmonstr-sitemap-5.svg
Normal file
@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24"><path d="M16 6h-8v-6h8v6zm-10 12h-6v6h6v-6zm18 0h-6v6h6v-6zm-11-7v-3h-2v3h-9v5h2v-3h7v3h2v-3h7v3h2v-5h-9zm2 7h-6v6h6v-6z"/></svg>
|
After Width: | Height: | Size: 212 B |
@ -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() {
|
$(document).ready(function() {
|
||||||
console.info("Loading CA, to debug: curl " + window.location.href + " --negotiate -u : -H 'Accept: application/json'");
|
console.info("Loading CA, to debug: curl " + window.location.href + " --negotiate -u : -H 'Accept: application/json'");
|
||||||
$.ajax({
|
$.ajax({
|
||||||
@ -228,6 +246,7 @@ $(document).ready(function() {
|
|||||||
source.addEventListener("request-signed", onRequestSigned);
|
source.addEventListener("request-signed", onRequestSigned);
|
||||||
source.addEventListener("certificate-revoked", onCertificateRevoked);
|
source.addEventListener("certificate-revoked", onCertificateRevoked);
|
||||||
source.addEventListener("tag-update", onTagUpdated);
|
source.addEventListener("tag-update", onTagUpdated);
|
||||||
|
source.addEventListener("attribute-update", onAttributeUpdated);
|
||||||
|
|
||||||
console.info("Swtiching to requests section");
|
console.info("Swtiching to requests section");
|
||||||
$("section").hide();
|
$("section").hide();
|
||||||
|
3
certidude/static/views/attributes.html
Normal file
3
certidude/static/views/attributes.html
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
{% for key, value in certificate.attributes %}
|
||||||
|
<span class="attribute icon {{ key | replace('.', ' ') }}">{{ value }}</span>
|
||||||
|
{% endfor %}
|
@ -61,4 +61,8 @@
|
|||||||
<div class="status">
|
<div class="status">
|
||||||
{% include 'views/status.html' %}
|
{% include 'views/status.html' %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="attributes" data-cn="{{ certificate.common_name }}">
|
||||||
|
{% include 'views/attributes.html' %}
|
||||||
|
</div>
|
||||||
</li>
|
</li>
|
||||||
|
15
certidude/templates/script/default.sh
Normal file
15
certidude/templates/script/default.sh
Normal file
@ -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)"
|
@ -1,6 +1,6 @@
|
|||||||
#!/bin/sh
|
#!/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
|
# https://lauri.vosandi.com/2017/01/reconfiguring-openwrt-as-dummy-ap.html
|
||||||
|
|
||||||
# Password protected wireless area
|
# 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.mode=ap
|
||||||
uci set wireless.lan$band.device=radio$band
|
uci set wireless.lan$band.device=radio$band
|
||||||
uci set wireless.lan$band.encryption=psk2
|
uci set wireless.lan$band.encryption=psk2
|
||||||
{% if attributes.protected and attributes.protected.ssid %}
|
{% if attributes.wireless.protected and attributes.wireless.protected.ssid %}
|
||||||
uci set wireless.lan$band.ssid={{ attrbutes.protected.ssid }}
|
uci set wireless.lan$band.ssid={{ attrbutes.wireless.protected.ssid }}
|
||||||
{% else %}
|
{% else %}
|
||||||
uci set wireless.lan$band.ssid=$(uci get system.@system[0].hostname)-protected
|
uci set wireless.lan$band.ssid=$(uci get system.@system[0].hostname)-protected
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if attributes.protected and attributes.protected.psk %}
|
{% if attributes.wireless.protected and attributes.wireless.protected.psk %}
|
||||||
uci set wireless.lan$band.key={{ attributes.protected.psk }}
|
uci set wireless.lan$band.key={{ attributes.wireless.protected.psk }}
|
||||||
{% else %}
|
{% else %}
|
||||||
uci set wireless.lan$band.key=salakala
|
uci set wireless.lan$band.key=salakala
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@ -29,8 +29,8 @@ for band in 2ghz 5ghz; do
|
|||||||
uci set wireless.guest$band.mode=ap
|
uci set wireless.guest$band.mode=ap
|
||||||
uci set wireless.guest$band.device=radio$band
|
uci set wireless.guest$band.device=radio$band
|
||||||
uci set wireless.guest$band.encryption=none
|
uci set wireless.guest$band.encryption=none
|
||||||
{% if attributes.public and attributes.public.ssid %}
|
{% if attributes.wireless.public and attributes.wireless.public.ssid %}
|
||||||
uci set wireless.guest$band.ssid={{ attrbutes.public.ssid }}
|
uci set wireless.guest$band.ssid={{ attrbutes.wireless.public.ssid }}
|
||||||
{% else %}
|
{% else %}
|
||||||
uci set wireless.guest$band.ssid=$(uci get system.@system[0].hostname)-public
|
uci set wireless.guest$band.ssid=$(uci get system.@system[0].hostname)-public
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
@ -23,7 +23,7 @@ server {
|
|||||||
root {{static_path}};
|
root {{static_path}};
|
||||||
|
|
||||||
location /api/ {
|
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 Host $host;
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
proxy_set_header X-Real-IP $remote_addr;
|
||||||
proxy_connect_timeout 600;
|
proxy_connect_timeout 600;
|
||||||
|
@ -7,8 +7,7 @@ Environment=PYTHON_EGG_CACHE=/tmp/.cache
|
|||||||
PIDFile=/run/certidude/server.pid
|
PIDFile=/run/certidude/server.pid
|
||||||
ExecReload=/bin/kill -s HUP $MAINPID
|
ExecReload=/bin/kill -s HUP $MAINPID
|
||||||
ExecStop=/bin/kill -s TERM $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]
|
[Install]
|
||||||
WantedBy=multi-user.target
|
WantedBy=multi-user.target
|
||||||
|
Loading…
Reference in New Issue
Block a user