mirror of
https://github.com/laurivosandi/certidude
synced 2024-12-22 08:15:18 +00:00
Several updates #3
* Move SessionResource and CertificateAuthorityResource to api/session.py * Log browser user agent for logins * Remove static sink from backend, nginx always serves static now * Don't emit 'attribute-update' event if no attributes were changed * Better CN extraction from DN during lease update * Log user who deleted request * Remove long polling CRL fetch API call and relevant test * Merge auth decorators ldap_authenticate, kerberos_authenticate, pam_authenticate * Add 'kerberos subnets' to distinguish authentication method * Add 'admin subnets' to filter traffic to administrative API calls * Highlight recent log events * Links to switch between 2, 3 and 4 column layouts in the dashboard * Restored certidude client snippets in request dialog * Various bugfixes, improved log messages
This commit is contained in:
parent
4348458d30
commit
bfdd8c4887
@ -7,7 +7,8 @@ after_success:
|
|||||||
- codecov
|
- codecov
|
||||||
script:
|
script:
|
||||||
- echo registry=http://registry.npmjs.org/ | sudo tee /root/.npmrc
|
- echo registry=http://registry.npmjs.org/ | sudo tee /root/.npmrc
|
||||||
- sudo apt install software-properties-common python3-setuptools python3-mysql.connector python3-pyxattr
|
- sudo apt install software-properties-common python3-setuptools build-essential python3-dev libsasl2-dev libkrb5-dev
|
||||||
|
- sudo apt remove python3-mimeparse
|
||||||
- sudo mkdir -p /etc/systemd/system # Until Travis is stuck with 14.04
|
- sudo mkdir -p /etc/systemd/system # Until Travis is stuck with 14.04
|
||||||
- sudo easy_install3 pip
|
- sudo easy_install3 pip
|
||||||
- sudo -H pip3 install -r requirements.txt
|
- sudo -H pip3 install -r requirements.txt
|
||||||
|
@ -1,218 +1,19 @@
|
|||||||
# encoding: utf-8
|
# encoding: utf-8
|
||||||
|
|
||||||
import falcon
|
import falcon
|
||||||
import mimetypes
|
|
||||||
import logging
|
|
||||||
import os
|
|
||||||
import hashlib
|
|
||||||
from datetime import datetime
|
|
||||||
from xattr import listxattr, getxattr
|
|
||||||
from certidude.common import cert_to_dn
|
|
||||||
from certidude.user import User
|
|
||||||
from certidude.decorators import serialize, csrf_protection
|
|
||||||
from certidude import const, config, authority
|
|
||||||
from .utils import AuthorityHandler
|
|
||||||
from .utils.firewall import login_required, authorize_admin
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
class CertificateAuthorityResource(object):
|
|
||||||
def on_get(self, req, resp):
|
|
||||||
logger.info("Served CA certificate to %s", req.context.get("remote_addr"))
|
|
||||||
resp.stream = open(config.AUTHORITY_CERTIFICATE_PATH, "rb")
|
|
||||||
resp.append_header("Content-Type", "application/x-x509-ca-cert")
|
|
||||||
resp.append_header("Content-Disposition", "attachment; filename=%s.crt" %
|
|
||||||
const.HOSTNAME.encode("ascii"))
|
|
||||||
|
|
||||||
|
|
||||||
class SessionResource(AuthorityHandler):
|
|
||||||
@csrf_protection
|
|
||||||
@serialize
|
|
||||||
@login_required
|
|
||||||
@authorize_admin
|
|
||||||
def on_get(self, req, resp):
|
|
||||||
|
|
||||||
def serialize_requests(g):
|
|
||||||
for common_name, path, buf, req, submitted, server in g():
|
|
||||||
try:
|
|
||||||
submission_address = getxattr(path, "user.request.address").decode("ascii") # TODO: move to authority.py
|
|
||||||
except IOError:
|
|
||||||
submission_address = None
|
|
||||||
try:
|
|
||||||
submission_hostname = getxattr(path, "user.request.hostname").decode("ascii") # TODO: move to authority.py
|
|
||||||
except IOError:
|
|
||||||
submission_hostname = None
|
|
||||||
yield dict(
|
|
||||||
submitted = submitted,
|
|
||||||
common_name = common_name,
|
|
||||||
address = submission_address,
|
|
||||||
hostname = submission_hostname if submission_hostname != submission_address else None,
|
|
||||||
md5sum = hashlib.md5(buf).hexdigest(),
|
|
||||||
sha1sum = hashlib.sha1(buf).hexdigest(),
|
|
||||||
sha256sum = hashlib.sha256(buf).hexdigest(),
|
|
||||||
sha512sum = hashlib.sha512(buf).hexdigest()
|
|
||||||
)
|
|
||||||
|
|
||||||
def serialize_revoked(g):
|
|
||||||
for common_name, path, buf, cert, signed, expired, revoked, reason in g(limit=5):
|
|
||||||
yield dict(
|
|
||||||
serial = "%x" % cert.serial_number,
|
|
||||||
common_name = common_name,
|
|
||||||
# TODO: key type, key length, key exponent, key modulo
|
|
||||||
signed = signed,
|
|
||||||
expired = expired,
|
|
||||||
revoked = revoked,
|
|
||||||
reason = reason,
|
|
||||||
sha256sum = hashlib.sha256(buf).hexdigest())
|
|
||||||
|
|
||||||
def serialize_certificates(g):
|
|
||||||
for common_name, path, buf, cert, signed, expires in g():
|
|
||||||
# Extract certificate tags from filesystem
|
|
||||||
try:
|
|
||||||
tags = []
|
|
||||||
for tag in getxattr(path, "user.xdg.tags").decode("utf-8").split(","):
|
|
||||||
if "=" in tag:
|
|
||||||
k, v = tag.split("=", 1)
|
|
||||||
else:
|
|
||||||
k, v = "other", tag
|
|
||||||
tags.append(dict(id=tag, key=k, value=v))
|
|
||||||
except IOError: # No such attribute(s)
|
|
||||||
tags = None
|
|
||||||
|
|
||||||
attributes = {}
|
|
||||||
for key in listxattr(path):
|
|
||||||
if key.startswith(b"user.machine."):
|
|
||||||
attributes[key[13:].decode("ascii")] = getxattr(path, key).decode("ascii")
|
|
||||||
|
|
||||||
# Extract lease information from filesystem
|
|
||||||
try:
|
|
||||||
last_seen = datetime.strptime(getxattr(path, "user.lease.last_seen").decode("ascii"), "%Y-%m-%dT%H:%M:%S.%fZ")
|
|
||||||
lease = dict(
|
|
||||||
inner_address = getxattr(path, "user.lease.inner_address").decode("ascii"),
|
|
||||||
outer_address = getxattr(path, "user.lease.outer_address").decode("ascii"),
|
|
||||||
last_seen = last_seen,
|
|
||||||
age = datetime.utcnow() - last_seen
|
|
||||||
)
|
|
||||||
except IOError: # No such attribute(s)
|
|
||||||
lease = None
|
|
||||||
|
|
||||||
try:
|
|
||||||
signer_username = getxattr(path, "user.signature.username").decode("ascii")
|
|
||||||
except IOError:
|
|
||||||
signer_username = None
|
|
||||||
|
|
||||||
# TODO: dedup
|
|
||||||
yield dict(
|
|
||||||
serial = "%x" % cert.serial_number,
|
|
||||||
organizational_unit = cert.subject.native.get("organizational_unit_name"),
|
|
||||||
common_name = common_name,
|
|
||||||
# TODO: key type, key length, key exponent, key modulo
|
|
||||||
signed = signed,
|
|
||||||
expires = expires,
|
|
||||||
sha256sum = hashlib.sha256(buf).hexdigest(),
|
|
||||||
signer = signer_username,
|
|
||||||
lease = lease,
|
|
||||||
tags = tags,
|
|
||||||
attributes = attributes or None,
|
|
||||||
extensions = dict([
|
|
||||||
(e["extn_id"].native, e["extn_value"].native)
|
|
||||||
for e in cert["tbs_certificate"]["extensions"]
|
|
||||||
if e["extn_id"].native in ("extended_key_usage",)])
|
|
||||||
)
|
|
||||||
|
|
||||||
logger.info("Logged in authority administrator %s from %s" % (req.context.get("user"), req.context.get("remote_addr")))
|
|
||||||
return dict(
|
|
||||||
user = dict(
|
|
||||||
name=req.context.get("user").name,
|
|
||||||
gn=req.context.get("user").given_name,
|
|
||||||
sn=req.context.get("user").surname,
|
|
||||||
mail=req.context.get("user").mail
|
|
||||||
),
|
|
||||||
request_submission_allowed = config.REQUEST_SUBMISSION_ALLOWED,
|
|
||||||
service = dict(
|
|
||||||
protocols = config.SERVICE_PROTOCOLS,
|
|
||||||
routers = [j[0] for j in authority.list_signed(
|
|
||||||
common_name=config.SERVICE_ROUTERS)]
|
|
||||||
),
|
|
||||||
authority = dict(
|
|
||||||
builder = dict(
|
|
||||||
profiles = config.IMAGE_BUILDER_PROFILES
|
|
||||||
),
|
|
||||||
tagging = [dict(name=t[0], type=t[1], title=t[2]) for t in config.TAG_TYPES],
|
|
||||||
lease = dict(
|
|
||||||
offline = 600, # Seconds from last seen activity to consider lease offline, OpenVPN reneg-sec option
|
|
||||||
dead = 604800 # Seconds from last activity to consider lease dead, X509 chain broken or machine discarded
|
|
||||||
),
|
|
||||||
certificate = dict(
|
|
||||||
algorithm = authority.public_key.algorithm,
|
|
||||||
common_name = self.authority.certificate.subject.native["common_name"],
|
|
||||||
distinguished_name = cert_to_dn(self.authority.certificate),
|
|
||||||
md5sum = hashlib.md5(self.authority.certificate_buf).hexdigest(),
|
|
||||||
blob = self.authority.certificate_buf.decode("ascii"),
|
|
||||||
),
|
|
||||||
mailer = dict(
|
|
||||||
name = config.MAILER_NAME,
|
|
||||||
address = config.MAILER_ADDRESS
|
|
||||||
) if config.MAILER_ADDRESS else None,
|
|
||||||
machine_enrollment_subnets=config.MACHINE_ENROLLMENT_SUBNETS,
|
|
||||||
user_enrollment_allowed=config.USER_ENROLLMENT_ALLOWED,
|
|
||||||
user_multiple_certificates=config.USER_MULTIPLE_CERTIFICATES,
|
|
||||||
events = config.EVENT_SOURCE_SUBSCRIBE % config.EVENT_SOURCE_TOKEN,
|
|
||||||
requests=serialize_requests(self.authority.list_requests),
|
|
||||||
signed=serialize_certificates(self.authority.list_signed),
|
|
||||||
revoked=serialize_revoked(self.authority.list_revoked),
|
|
||||||
admin_users = User.objects.filter_admins(),
|
|
||||||
user_subnets = config.USER_SUBNETS or None,
|
|
||||||
autosign_subnets = config.AUTOSIGN_SUBNETS or None,
|
|
||||||
request_subnets = config.REQUEST_SUBNETS or None,
|
|
||||||
admin_subnets=config.ADMIN_SUBNETS or None,
|
|
||||||
signature = dict(
|
|
||||||
revocation_list_lifetime=config.REVOCATION_LIST_LIFETIME,
|
|
||||||
profiles = sorted([p.serialize() for p in config.PROFILES.values()], key=lambda p:p.get("slug")),
|
|
||||||
|
|
||||||
)
|
|
||||||
),
|
|
||||||
features=dict(
|
|
||||||
ocsp=bool(config.OCSP_SUBNETS),
|
|
||||||
crl=bool(config.CRL_SUBNETS),
|
|
||||||
token=bool(config.TOKEN_URL),
|
|
||||||
tagging=True,
|
|
||||||
leases=True,
|
|
||||||
logging=config.LOGGING_BACKEND)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
class StaticResource(object):
|
|
||||||
def __init__(self, root):
|
|
||||||
self.root = os.path.realpath(root)
|
|
||||||
|
|
||||||
def __call__(self, req, resp):
|
|
||||||
path = os.path.realpath(os.path.join(self.root, req.path[1:]))
|
|
||||||
if not path.startswith(self.root):
|
|
||||||
raise falcon.HTTPBadRequest()
|
|
||||||
|
|
||||||
if os.path.isdir(path):
|
|
||||||
path = os.path.join(path, "index.html")
|
|
||||||
|
|
||||||
if os.path.exists(path):
|
|
||||||
content_type, content_encoding = mimetypes.guess_type(path)
|
|
||||||
if content_type:
|
|
||||||
resp.append_header("Content-Type", content_type)
|
|
||||||
if content_encoding:
|
|
||||||
resp.append_header("Content-Encoding", content_encoding)
|
|
||||||
resp.stream = open(path, "rb")
|
|
||||||
logger.debug("Serving '%s' from '%s'", req.path, path)
|
|
||||||
else:
|
|
||||||
resp.status = falcon.HTTP_404
|
|
||||||
resp.body = "File '%s' not found" % req.path
|
|
||||||
logger.info("File '%s' not found, path resolved to '%s'", req.path, path)
|
|
||||||
import ipaddress
|
import ipaddress
|
||||||
|
import os
|
||||||
|
from certidude import config
|
||||||
|
from user_agents import parse
|
||||||
|
|
||||||
|
|
||||||
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"
|
|
||||||
req.context["remote_addr"] = ipaddress.ip_address(req.access_route[0])
|
req.context["remote_addr"] = ipaddress.ip_address(req.access_route[0])
|
||||||
|
if req.user_agent:
|
||||||
|
req.context["user_agent"] = parse(req.user_agent)
|
||||||
|
else:
|
||||||
|
req.context["user_agent"] = "Unknown user agent"
|
||||||
|
|
||||||
def certidude_app(log_handlers=[]):
|
def certidude_app(log_handlers=[]):
|
||||||
from certidude import authority, config
|
from certidude import authority, config
|
||||||
@ -225,10 +26,10 @@ def certidude_app(log_handlers=[]):
|
|||||||
from .bootstrap import BootstrapResource
|
from .bootstrap import BootstrapResource
|
||||||
from .token import TokenResource
|
from .token import TokenResource
|
||||||
from .builder import ImageBuilderResource
|
from .builder import ImageBuilderResource
|
||||||
|
from .session import SessionResource, CertificateAuthorityResource
|
||||||
|
|
||||||
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())
|
||||||
@ -270,9 +71,6 @@ def certidude_app(log_handlers=[]):
|
|||||||
from .scep import SCEPResource
|
from .scep import SCEPResource
|
||||||
app.add_route("/api/scep/", SCEPResource(authority))
|
app.add_route("/api/scep/", SCEPResource(authority))
|
||||||
|
|
||||||
# Add sink for serving static files
|
|
||||||
app.add_sink(StaticResource(os.path.join(__file__, "..", "..", "static")))
|
|
||||||
|
|
||||||
if config.OCSP_SUBNETS:
|
if config.OCSP_SUBNETS:
|
||||||
from .ocsp import OCSPResource
|
from .ocsp import OCSPResource
|
||||||
app.add_sink(OCSPResource(authority), prefix="/api/ocsp")
|
app.add_sink(OCSPResource(authority), prefix="/api/ocsp")
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import falcon
|
import falcon
|
||||||
import logging
|
import logging
|
||||||
import re
|
import re
|
||||||
from xattr import setxattr, listxattr, removexattr
|
from xattr import setxattr, listxattr, removexattr, getxattr
|
||||||
from certidude import push
|
from certidude import push
|
||||||
from certidude.decorators import serialize, csrf_protection
|
from certidude.decorators import serialize, csrf_protection
|
||||||
from .utils.firewall import login_required, authorize_admin, whitelist_subject
|
from .utils.firewall import login_required, authorize_admin, whitelist_subject
|
||||||
@ -21,7 +21,6 @@ class AttributeResource(object):
|
|||||||
Return extended attributes stored on the server.
|
Return extended attributes stored on the server.
|
||||||
This not only contains tags and lease information,
|
This not only contains tags and lease information,
|
||||||
but might also contain some other sensitive information.
|
but might also contain some other sensitive information.
|
||||||
Results made available only to lease IP address.
|
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
path, buf, cert, attribs = self.authority.get_attributes(cn,
|
path, buf, cert, attribs = self.authority.get_attributes(cn,
|
||||||
@ -44,14 +43,22 @@ class AttributeResource(object):
|
|||||||
if not re.match("[a-z0-9_\.]+$", key):
|
if not re.match("[a-z0-9_\.]+$", key):
|
||||||
raise falcon.HTTPBadRequest("Invalid key %s" % key)
|
raise falcon.HTTPBadRequest("Invalid key %s" % key)
|
||||||
valid = set()
|
valid = set()
|
||||||
|
modified = False
|
||||||
for key, value in req.params.items():
|
for key, value in req.params.items():
|
||||||
identifier = ("user.%s.%s" % (self.namespace, key)).encode("ascii")
|
identifier = ("user.%s.%s" % (self.namespace, key)).encode("ascii")
|
||||||
|
try:
|
||||||
|
if getxattr(path, identifier).decode("utf-8") != value:
|
||||||
|
modified = True
|
||||||
|
except OSError: # no such attribute
|
||||||
|
pass
|
||||||
setxattr(path, identifier, value.encode("utf-8"))
|
setxattr(path, identifier, value.encode("utf-8"))
|
||||||
valid.add(identifier)
|
valid.add(identifier)
|
||||||
for key in listxattr(path):
|
for key in listxattr(path):
|
||||||
if not key.startswith(namespace):
|
if not key.startswith(namespace):
|
||||||
continue
|
continue
|
||||||
if key not in valid:
|
if key not in valid:
|
||||||
|
modified = True
|
||||||
removexattr(path, key)
|
removexattr(path, key)
|
||||||
push.publish("attribute-update", cn)
|
if modified:
|
||||||
|
push.publish("attribute-update", cn)
|
||||||
|
|
||||||
|
@ -33,9 +33,9 @@ class LeaseResource(AuthorityHandler):
|
|||||||
@authorize_server
|
@authorize_server
|
||||||
def on_post(self, req, resp):
|
def on_post(self, req, resp):
|
||||||
client_common_name = req.get_param("client", required=True)
|
client_common_name = req.get_param("client", required=True)
|
||||||
m = re.match("CN=(.+?),", client_common_name) # It's actually DN, resolve it to CN
|
m = re.match("^(.*, )*CN=(.+?)(, .*)*$", client_common_name) # It's actually DN, resolve it to CN
|
||||||
if m:
|
if m:
|
||||||
client_common_name, = m.groups()
|
_, client_common_name, _ = m.groups()
|
||||||
|
|
||||||
path, buf, cert, signed, expires = self.authority.get_signed(client_common_name) # TODO: catch exceptions
|
path, buf, cert, signed, expires = self.authority.get_signed(client_common_name) # TODO: catch exceptions
|
||||||
if req.get_param("serial") and cert.serial_number != req.get_param_as_int("serial"): # OCSP-ish solution for OpenVPN, not exposed for StrongSwan
|
if req.get_param("serial") and cert.serial_number != req.get_param_as_int("serial"): # OCSP-ish solution for OpenVPN, not exposed for StrongSwan
|
||||||
|
@ -140,7 +140,7 @@ class RequestListResource(AuthorityHandler):
|
|||||||
resp.set_header("Content-Type", "application/x-pem-file")
|
resp.set_header("Content-Type", "application/x-pem-file")
|
||||||
_, resp.body = self.authority._sign(csr, body,
|
_, resp.body = self.authority._sign(csr, body,
|
||||||
overwrite=overwrite_allowed, profile=config.PROFILES["rw"])
|
overwrite=overwrite_allowed, profile=config.PROFILES["rw"])
|
||||||
logger.info("Autosigned %s as %s is whitelisted", common_name, req.context.get("remote_addr"))
|
logger.info("Signed %s as %s is whitelisted for autosign", common_name, req.context.get("remote_addr"))
|
||||||
return
|
return
|
||||||
except EnvironmentError:
|
except EnvironmentError:
|
||||||
logger.info("Autosign for %s from %s failed, signed certificate already exists",
|
logger.info("Autosign for %s from %s failed, signed certificate already exists",
|
||||||
@ -148,7 +148,7 @@ class RequestListResource(AuthorityHandler):
|
|||||||
reasons.append("autosign failed, signed certificate already exists")
|
reasons.append("autosign failed, signed certificate already exists")
|
||||||
break
|
break
|
||||||
else:
|
else:
|
||||||
reasons.append("autosign failed, IP address not whitelisted")
|
reasons.append("IP address not whitelisted for autosign")
|
||||||
else:
|
else:
|
||||||
reasons.append("autosign not requested")
|
reasons.append("autosign not requested")
|
||||||
|
|
||||||
@ -170,7 +170,7 @@ class RequestListResource(AuthorityHandler):
|
|||||||
push.publish("request-submitted", common_name)
|
push.publish("request-submitted", common_name)
|
||||||
|
|
||||||
# Wait the certificate to be signed if waiting is requested
|
# Wait the certificate to be signed if waiting is requested
|
||||||
logger.info("Stored signing request %s from %s, reasons: %s", common_name, req.context.get("remote_addr"), reasons)
|
logger.info("Signing request %s from %s put on hold, %s", common_name, req.context.get("remote_addr"), ", ".join(reasons))
|
||||||
|
|
||||||
if req.get_param("wait"):
|
if req.get_param("wait"):
|
||||||
# Redirect to nginx pub/sub
|
# Redirect to nginx pub/sub
|
||||||
@ -178,7 +178,6 @@ class RequestListResource(AuthorityHandler):
|
|||||||
click.echo("Redirecting to: %s" % url)
|
click.echo("Redirecting to: %s" % url)
|
||||||
resp.status = falcon.HTTP_SEE_OTHER
|
resp.status = falcon.HTTP_SEE_OTHER
|
||||||
resp.set_header("Location", url)
|
resp.set_header("Location", url)
|
||||||
logger.debug("Redirecting signing request from %s to %s, reasons: %s", req.context.get("remote_addr"), url, ", ".join(reasons))
|
|
||||||
else:
|
else:
|
||||||
# Request was accepted, but not processed
|
# Request was accepted, but not processed
|
||||||
resp.status = falcon.HTTP_202
|
resp.status = falcon.HTTP_202
|
||||||
@ -256,7 +255,7 @@ class RequestDetailResource(AuthorityHandler):
|
|||||||
@authorize_admin
|
@authorize_admin
|
||||||
def on_delete(self, req, resp, cn):
|
def on_delete(self, req, resp, cn):
|
||||||
try:
|
try:
|
||||||
self.authority.delete_request(cn)
|
self.authority.delete_request(cn, user=req.context.get("user"))
|
||||||
# Logging implemented in the function above
|
# Logging implemented in the function above
|
||||||
except errors.RequestDoesNotExist as e:
|
except errors.RequestDoesNotExist as e:
|
||||||
resp.body = "No certificate signing request for %s found" % cn
|
resp.body = "No certificate signing request for %s found" % cn
|
||||||
|
@ -20,19 +20,12 @@ class RevocationListResource(AuthorityHandler):
|
|||||||
logger.debug("Serving revocation list (DER) to %s", req.context.get("remote_addr"))
|
logger.debug("Serving revocation list (DER) to %s", req.context.get("remote_addr"))
|
||||||
resp.body = self.authority.export_crl(pem=False)
|
resp.body = self.authority.export_crl(pem=False)
|
||||||
elif req.client_accepts("application/x-pem-file"):
|
elif req.client_accepts("application/x-pem-file"):
|
||||||
if req.get_param_as_bool("wait"):
|
resp.set_header("Content-Type", "application/x-pem-file")
|
||||||
url = config.LONG_POLL_SUBSCRIBE % "crl"
|
resp.append_header(
|
||||||
resp.status = falcon.HTTP_SEE_OTHER
|
"Content-Disposition",
|
||||||
resp.set_header("Location", url)
|
("attachment; filename=%s-crl.pem" % const.HOSTNAME))
|
||||||
logger.debug("Redirecting to CRL request to %s", url)
|
logger.debug("Serving revocation list (PEM) to %s", req.context.get("remote_addr"))
|
||||||
resp.body = "Redirecting to %s" % url
|
resp.body = self.authority.export_crl()
|
||||||
else:
|
|
||||||
resp.set_header("Content-Type", "application/x-pem-file")
|
|
||||||
resp.append_header(
|
|
||||||
"Content-Disposition",
|
|
||||||
("attachment; filename=%s-crl.pem" % const.HOSTNAME))
|
|
||||||
logger.debug("Serving revocation list (PEM) to %s", req.context.get("remote_addr"))
|
|
||||||
resp.body = self.authority.export_crl()
|
|
||||||
else:
|
else:
|
||||||
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(
|
||||||
|
178
certidude/api/session.py
Normal file
178
certidude/api/session.py
Normal file
@ -0,0 +1,178 @@
|
|||||||
|
from datetime import datetime
|
||||||
|
from xattr import listxattr, getxattr
|
||||||
|
import falcon
|
||||||
|
import hashlib
|
||||||
|
import logging
|
||||||
|
from certidude import const, config
|
||||||
|
from certidude.common import cert_to_dn
|
||||||
|
from certidude.decorators import serialize, csrf_protection
|
||||||
|
from certidude.user import User
|
||||||
|
from .utils import AuthorityHandler
|
||||||
|
from .utils.firewall import login_required, authorize_admin
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
class CertificateAuthorityResource(object):
|
||||||
|
def on_get(self, req, resp):
|
||||||
|
logger.info("Served CA certificate to %s", req.context.get("remote_addr"))
|
||||||
|
resp.stream = open(config.AUTHORITY_CERTIFICATE_PATH, "rb")
|
||||||
|
resp.append_header("Content-Type", "application/x-x509-ca-cert")
|
||||||
|
resp.append_header("Content-Disposition", "attachment; filename=%s.crt" %
|
||||||
|
const.HOSTNAME.encode("ascii"))
|
||||||
|
|
||||||
|
class SessionResource(AuthorityHandler):
|
||||||
|
@csrf_protection
|
||||||
|
@serialize
|
||||||
|
@login_required
|
||||||
|
@authorize_admin
|
||||||
|
def on_get(self, req, resp):
|
||||||
|
|
||||||
|
def serialize_requests(g):
|
||||||
|
for common_name, path, buf, req, submitted, server in g():
|
||||||
|
try:
|
||||||
|
submission_address = getxattr(path, "user.request.address").decode("ascii") # TODO: move to authority.py
|
||||||
|
except IOError:
|
||||||
|
submission_address = None
|
||||||
|
try:
|
||||||
|
submission_hostname = getxattr(path, "user.request.hostname").decode("ascii") # TODO: move to authority.py
|
||||||
|
except IOError:
|
||||||
|
submission_hostname = None
|
||||||
|
yield dict(
|
||||||
|
submitted = submitted,
|
||||||
|
common_name = common_name,
|
||||||
|
address = submission_address,
|
||||||
|
hostname = submission_hostname if submission_hostname != submission_address else None,
|
||||||
|
md5sum = hashlib.md5(buf).hexdigest(),
|
||||||
|
sha1sum = hashlib.sha1(buf).hexdigest(),
|
||||||
|
sha256sum = hashlib.sha256(buf).hexdigest(),
|
||||||
|
sha512sum = hashlib.sha512(buf).hexdigest()
|
||||||
|
)
|
||||||
|
|
||||||
|
def serialize_revoked(g):
|
||||||
|
for common_name, path, buf, cert, signed, expired, revoked, reason in g(limit=5):
|
||||||
|
yield dict(
|
||||||
|
serial = "%x" % cert.serial_number,
|
||||||
|
common_name = common_name,
|
||||||
|
# TODO: key type, key length, key exponent, key modulo
|
||||||
|
signed = signed,
|
||||||
|
expired = expired,
|
||||||
|
revoked = revoked,
|
||||||
|
reason = reason,
|
||||||
|
sha256sum = hashlib.sha256(buf).hexdigest())
|
||||||
|
|
||||||
|
def serialize_certificates(g):
|
||||||
|
for common_name, path, buf, cert, signed, expires in g():
|
||||||
|
# Extract certificate tags from filesystem
|
||||||
|
try:
|
||||||
|
tags = []
|
||||||
|
for tag in getxattr(path, "user.xdg.tags").decode("utf-8").split(","):
|
||||||
|
if "=" in tag:
|
||||||
|
k, v = tag.split("=", 1)
|
||||||
|
else:
|
||||||
|
k, v = "other", tag
|
||||||
|
tags.append(dict(id=tag, key=k, value=v))
|
||||||
|
except IOError: # No such attribute(s)
|
||||||
|
tags = None
|
||||||
|
|
||||||
|
attributes = {}
|
||||||
|
for key in listxattr(path):
|
||||||
|
if key.startswith(b"user.machine."):
|
||||||
|
attributes[key[13:].decode("ascii")] = getxattr(path, key).decode("ascii")
|
||||||
|
|
||||||
|
# Extract lease information from filesystem
|
||||||
|
try:
|
||||||
|
last_seen = datetime.strptime(getxattr(path, "user.lease.last_seen").decode("ascii"), "%Y-%m-%dT%H:%M:%S.%fZ")
|
||||||
|
lease = dict(
|
||||||
|
inner_address = getxattr(path, "user.lease.inner_address").decode("ascii"),
|
||||||
|
outer_address = getxattr(path, "user.lease.outer_address").decode("ascii"),
|
||||||
|
last_seen = last_seen,
|
||||||
|
age = datetime.utcnow() - last_seen
|
||||||
|
)
|
||||||
|
except IOError: # No such attribute(s)
|
||||||
|
lease = None
|
||||||
|
|
||||||
|
try:
|
||||||
|
signer_username = getxattr(path, "user.signature.username").decode("ascii")
|
||||||
|
except IOError:
|
||||||
|
signer_username = None
|
||||||
|
|
||||||
|
# TODO: dedup
|
||||||
|
yield dict(
|
||||||
|
serial = "%x" % cert.serial_number,
|
||||||
|
organizational_unit = cert.subject.native.get("organizational_unit_name"),
|
||||||
|
common_name = common_name,
|
||||||
|
# TODO: key type, key length, key exponent, key modulo
|
||||||
|
signed = signed,
|
||||||
|
expires = expires,
|
||||||
|
sha256sum = hashlib.sha256(buf).hexdigest(),
|
||||||
|
signer = signer_username,
|
||||||
|
lease = lease,
|
||||||
|
tags = tags,
|
||||||
|
attributes = attributes or None,
|
||||||
|
extensions = dict([
|
||||||
|
(e["extn_id"].native, e["extn_value"].native)
|
||||||
|
for e in cert["tbs_certificate"]["extensions"]
|
||||||
|
if e["extn_id"].native in ("extended_key_usage",)])
|
||||||
|
)
|
||||||
|
|
||||||
|
logger.info("Logged in authority administrator %s from %s with %s" % (
|
||||||
|
req.context.get("user"), req.context.get("remote_addr"), req.context.get("user_agent")))
|
||||||
|
return dict(
|
||||||
|
user = dict(
|
||||||
|
name=req.context.get("user").name,
|
||||||
|
gn=req.context.get("user").given_name,
|
||||||
|
sn=req.context.get("user").surname,
|
||||||
|
mail=req.context.get("user").mail
|
||||||
|
),
|
||||||
|
request_submission_allowed = config.REQUEST_SUBMISSION_ALLOWED,
|
||||||
|
service = dict(
|
||||||
|
protocols = config.SERVICE_PROTOCOLS,
|
||||||
|
routers = [j[0] for j in self.authority.list_signed(
|
||||||
|
common_name=config.SERVICE_ROUTERS)]
|
||||||
|
),
|
||||||
|
authority = dict(
|
||||||
|
builder = dict(
|
||||||
|
profiles = config.IMAGE_BUILDER_PROFILES
|
||||||
|
),
|
||||||
|
tagging = [dict(name=t[0], type=t[1], title=t[2]) for t in config.TAG_TYPES],
|
||||||
|
lease = dict(
|
||||||
|
offline = 600, # Seconds from last seen activity to consider lease offline, OpenVPN reneg-sec option
|
||||||
|
dead = 604800 # Seconds from last activity to consider lease dead, X509 chain broken or machine discarded
|
||||||
|
),
|
||||||
|
certificate = dict(
|
||||||
|
algorithm = self.authority.public_key.algorithm,
|
||||||
|
common_name = self.authority.certificate.subject.native["common_name"],
|
||||||
|
distinguished_name = cert_to_dn(self.authority.certificate),
|
||||||
|
md5sum = hashlib.md5(self.authority.certificate_buf).hexdigest(),
|
||||||
|
blob = self.authority.certificate_buf.decode("ascii"),
|
||||||
|
),
|
||||||
|
mailer = dict(
|
||||||
|
name = config.MAILER_NAME,
|
||||||
|
address = config.MAILER_ADDRESS
|
||||||
|
) if config.MAILER_ADDRESS else None,
|
||||||
|
machine_enrollment_subnets=config.MACHINE_ENROLLMENT_SUBNETS,
|
||||||
|
user_enrollment_allowed=config.USER_ENROLLMENT_ALLOWED,
|
||||||
|
user_multiple_certificates=config.USER_MULTIPLE_CERTIFICATES,
|
||||||
|
events = config.EVENT_SOURCE_SUBSCRIBE % config.EVENT_SOURCE_TOKEN,
|
||||||
|
requests=serialize_requests(self.authority.list_requests),
|
||||||
|
signed=serialize_certificates(self.authority.list_signed),
|
||||||
|
revoked=serialize_revoked(self.authority.list_revoked),
|
||||||
|
admin_users = User.objects.filter_admins(),
|
||||||
|
user_subnets = config.USER_SUBNETS or None,
|
||||||
|
autosign_subnets = config.AUTOSIGN_SUBNETS or None,
|
||||||
|
request_subnets = config.REQUEST_SUBNETS or None,
|
||||||
|
admin_subnets=config.ADMIN_SUBNETS or None,
|
||||||
|
signature = dict(
|
||||||
|
revocation_list_lifetime=config.REVOCATION_LIST_LIFETIME,
|
||||||
|
profiles = sorted([p.serialize() for p in config.PROFILES.values()], key=lambda p:p.get("slug")),
|
||||||
|
|
||||||
|
)
|
||||||
|
),
|
||||||
|
features=dict(
|
||||||
|
ocsp=bool(config.OCSP_SUBNETS),
|
||||||
|
crl=bool(config.CRL_SUBNETS),
|
||||||
|
token=bool(config.TOKEN_URL),
|
||||||
|
tagging=True,
|
||||||
|
leases=True,
|
||||||
|
logging=config.LOGGING_BACKEND)
|
||||||
|
)
|
@ -68,8 +68,8 @@ class SignedCertificateDetailResource(AuthorityHandler):
|
|||||||
@login_required
|
@login_required
|
||||||
@authorize_admin
|
@authorize_admin
|
||||||
def on_delete(self, req, resp, cn):
|
def on_delete(self, req, resp, cn):
|
||||||
logger.info("Revoked certificate %s by %s from %s",
|
|
||||||
cn, req.context.get("user"), req.context.get("remote_addr"))
|
|
||||||
self.authority.revoke(cn,
|
self.authority.revoke(cn,
|
||||||
reason=req.get_param("reason", default="key_compromise"))
|
reason=req.get_param("reason", default="key_compromise"),
|
||||||
|
user=req.context.get("user")
|
||||||
|
)
|
||||||
|
|
||||||
|
@ -41,7 +41,7 @@ class TagResource(AuthorityHandler):
|
|||||||
else:
|
else:
|
||||||
tags.add("%s=%s" % (key,value))
|
tags.add("%s=%s" % (key,value))
|
||||||
setxattr(path, "user.xdg.tags", ",".join(tags).encode("utf-8"))
|
setxattr(path, "user.xdg.tags", ",".join(tags).encode("utf-8"))
|
||||||
logger.debug("Tag %s=%s set for %s" % (key, value, cn))
|
logger.info("Tag %s=%s set for %s by %s" % (key, value, cn, req.context.get("user")))
|
||||||
push.publish("tag-update", cn)
|
push.publish("tag-update", cn)
|
||||||
|
|
||||||
|
|
||||||
@ -68,7 +68,7 @@ class TagDetailResource(object):
|
|||||||
else:
|
else:
|
||||||
tags.add(value)
|
tags.add(value)
|
||||||
setxattr(path, "user.xdg.tags", ",".join(tags).encode("utf-8"))
|
setxattr(path, "user.xdg.tags", ",".join(tags).encode("utf-8"))
|
||||||
logger.debug("Tag %s set to %s for %s" % (tag, value, cn))
|
logger.info("Tag %s set to %s for %s by %s" % (tag, value, cn, req.context.get("user")))
|
||||||
push.publish("tag-update", cn)
|
push.publish("tag-update", cn)
|
||||||
|
|
||||||
@csrf_protection
|
@csrf_protection
|
||||||
@ -82,5 +82,5 @@ class TagDetailResource(object):
|
|||||||
removexattr(path, "user.xdg.tags")
|
removexattr(path, "user.xdg.tags")
|
||||||
else:
|
else:
|
||||||
setxattr(path, "user.xdg.tags", ",".join(tags))
|
setxattr(path, "user.xdg.tags", ",".join(tags))
|
||||||
logger.debug("Tag %s removed for %s" % (tag, cn))
|
logger.info("Tag %s removed for %s by %s" % (tag, cn, req.context.get("user")))
|
||||||
push.publish("tag-update", cn)
|
push.publish("tag-update", cn)
|
||||||
|
@ -4,15 +4,17 @@ import logging
|
|||||||
import binascii
|
import binascii
|
||||||
import click
|
import click
|
||||||
import gssapi
|
import gssapi
|
||||||
|
import ldap
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
|
import simplepam
|
||||||
import socket
|
import socket
|
||||||
from asn1crypto import pem, x509
|
from asn1crypto import pem, x509
|
||||||
from base64 import b64decode
|
from base64 import b64decode
|
||||||
from certidude.user import User
|
from certidude.user import User
|
||||||
from certidude import config, const
|
from certidude import config, const
|
||||||
|
|
||||||
logger = logging.getLogger("api")
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
def whitelist_subnets(subnets):
|
def whitelist_subnets(subnets):
|
||||||
"""
|
"""
|
||||||
@ -81,158 +83,127 @@ def whitelist_subject(func):
|
|||||||
|
|
||||||
def authenticate(optional=False):
|
def authenticate(optional=False):
|
||||||
def wrapper(func):
|
def wrapper(func):
|
||||||
def kerberos_authenticate(resource, req, resp, *args, **kwargs):
|
def wrapped(resource, req, resp, *args, **kwargs):
|
||||||
# Try pre-emptive authentication
|
kerberized = False
|
||||||
if not req.auth:
|
|
||||||
if optional:
|
if "kerberos" in config.AUTHENTICATION_BACKENDS:
|
||||||
|
for subnet in config.KERBEROS_SUBNETS:
|
||||||
|
if req.context.get("remote_addr") in subnet:
|
||||||
|
kerberized = True
|
||||||
|
|
||||||
|
if not req.auth: # no credentials provided
|
||||||
|
if optional: # optional allowed
|
||||||
req.context["user"] = None
|
req.context["user"] = None
|
||||||
return func(resource, req, resp, *args, **kwargs)
|
return func(resource, req, resp, *args, **kwargs)
|
||||||
|
|
||||||
logger.debug("No Kerberos ticket offered while attempting to access %s from %s",
|
if kerberized:
|
||||||
req.env["PATH_INFO"], req.context.get("remote_addr"))
|
logger.debug("No Kerberos ticket offered while attempting to access %s from %s",
|
||||||
raise falcon.HTTPUnauthorized("Unauthorized",
|
req.env["PATH_INFO"], req.context.get("remote_addr"))
|
||||||
"No Kerberos ticket offered, are you sure you've logged in with domain user account?",
|
raise falcon.HTTPUnauthorized("Unauthorized",
|
||||||
["Negotiate"])
|
"No Kerberos ticket offered, are you sure you've logged in with domain user account?",
|
||||||
|
["Negotiate"])
|
||||||
|
else:
|
||||||
|
logger.debug("No credentials offered while attempting to access %s from %s",
|
||||||
|
req.env["PATH_INFO"], req.context.get("remote_addr"))
|
||||||
|
raise falcon.HTTPUnauthorized("Unauthorized", "Please authenticate", ("Basic",))
|
||||||
|
|
||||||
os.environ["KRB5_KTNAME"] = config.KERBEROS_KEYTAB
|
if kerberized:
|
||||||
|
if not req.auth.startswith("Negotiate "):
|
||||||
|
raise falcon.HTTPBadRequest("Bad request",
|
||||||
|
"Bad header, expected Negotiate: %s" % req.auth)
|
||||||
|
|
||||||
try:
|
os.environ["KRB5_KTNAME"] = config.KERBEROS_KEYTAB
|
||||||
server_creds = gssapi.creds.Credentials(
|
|
||||||
usage='accept',
|
|
||||||
name=gssapi.names.Name('HTTP/%s'% const.FQDN))
|
|
||||||
except gssapi.raw.exceptions.BadNameError:
|
|
||||||
logger.error("Failed initialize HTTP service principal, possibly bad permissions for %s or /etc/krb5.conf" %
|
|
||||||
config.KERBEROS_KEYTAB)
|
|
||||||
raise
|
|
||||||
|
|
||||||
context = gssapi.sec_contexts.SecurityContext(creds=server_creds)
|
try:
|
||||||
|
server_creds = gssapi.creds.Credentials(
|
||||||
|
usage='accept',
|
||||||
|
name=gssapi.names.Name('HTTP/%s'% const.FQDN))
|
||||||
|
except gssapi.raw.exceptions.BadNameError:
|
||||||
|
logger.error("Failed initialize HTTP service principal, possibly bad permissions for %s or /etc/krb5.conf" %
|
||||||
|
config.KERBEROS_KEYTAB)
|
||||||
|
raise
|
||||||
|
|
||||||
if not req.auth.startswith("Negotiate "):
|
context = gssapi.sec_contexts.SecurityContext(creds=server_creds)
|
||||||
raise falcon.HTTPBadRequest("Bad request", "Bad header, expected Negotiate: %s" % req.auth)
|
|
||||||
|
|
||||||
token = ''.join(req.auth.split()[1:])
|
token = ''.join(req.auth.split()[1:])
|
||||||
|
|
||||||
try:
|
try:
|
||||||
context.step(b64decode(token))
|
context.step(b64decode(token))
|
||||||
except binascii.Error: # base64 errors
|
except binascii.Error: # base64 errors
|
||||||
raise falcon.HTTPBadRequest("Bad request", "Malformed token")
|
raise falcon.HTTPBadRequest("Bad request", "Malformed token")
|
||||||
except gssapi.raw.exceptions.BadMechanismError:
|
except gssapi.raw.exceptions.BadMechanismError:
|
||||||
raise falcon.HTTPBadRequest("Bad request", "Unsupported authentication mechanism (NTLM?) was offered. Please make sure you've logged into the computer with domain user account. The web interface should not prompt for username or password.")
|
raise falcon.HTTPBadRequest("Bad request", "Unsupported authentication mechanism (NTLM?) was offered. Please make sure you've logged into the computer with domain user account. The web interface should not prompt for username or password.")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
username, realm = str(context.initiator_name).split("@")
|
username, realm = str(context.initiator_name).split("@")
|
||||||
except AttributeError: # TODO: Better exception
|
except AttributeError: # TODO: Better exception
|
||||||
raise falcon.HTTPForbidden("Failed to determine username, are you trying to log in with correct domain account?")
|
raise falcon.HTTPForbidden("Failed to determine username, are you trying to log in with correct domain account?")
|
||||||
|
|
||||||
if realm != config.KERBEROS_REALM:
|
if realm != config.KERBEROS_REALM:
|
||||||
raise falcon.HTTPForbidden("Forbidden",
|
raise falcon.HTTPForbidden("Forbidden",
|
||||||
"Cross-realm trust not supported")
|
"Cross-realm trust not supported")
|
||||||
|
|
||||||
if username.endswith("$") and optional:
|
if username.endswith("$") and optional:
|
||||||
# Extract machine hostname
|
# Extract machine hostname
|
||||||
# TODO: Assert LDAP group membership
|
# TODO: Assert LDAP group membership
|
||||||
req.context["machine"] = username[:-1].lower()
|
req.context["machine"] = username[:-1].lower()
|
||||||
req.context["user"] = None
|
req.context["user"] = None
|
||||||
else:
|
else:
|
||||||
# Attempt to look up real user
|
# Attempt to look up real user
|
||||||
req.context["user"] = User.objects.get(username)
|
req.context["user"] = User.objects.get(username)
|
||||||
|
|
||||||
logger.debug("Succesfully authenticated user %s for %s from %s",
|
logger.debug("Succesfully authenticated user %s for %s from %s",
|
||||||
req.context["user"], req.env["PATH_INFO"], req.context["remote_addr"])
|
req.context["user"], req.env["PATH_INFO"], req.context["remote_addr"])
|
||||||
return func(resource, req, resp, *args, **kwargs)
|
|
||||||
|
|
||||||
|
|
||||||
def ldap_authenticate(resource, req, resp, *args, **kwargs):
|
|
||||||
"""
|
|
||||||
Authenticate against LDAP with WWW Basic Auth credentials
|
|
||||||
"""
|
|
||||||
|
|
||||||
if optional and not req.get_param_as_bool("authenticate"):
|
|
||||||
return func(resource, req, resp, *args, **kwargs)
|
return func(resource, req, resp, *args, **kwargs)
|
||||||
|
|
||||||
import ldap
|
else:
|
||||||
|
if not req.auth.startswith("Basic "):
|
||||||
|
raise falcon.HTTPBadRequest("Bad request", "Bad header, expected Basic: %s" % req.auth)
|
||||||
|
basic, token = req.auth.split(" ", 1)
|
||||||
|
user, passwd = b64decode(token).decode("ascii").split(":", 1)
|
||||||
|
|
||||||
if not req.auth:
|
if config.AUTHENTICATION_BACKENDS == {"pam"}:
|
||||||
raise falcon.HTTPUnauthorized("Unauthorized",
|
if not simplepam.authenticate(user, passwd, "sshd"):
|
||||||
"No authentication header provided",
|
logger.critical("Basic authentication failed for user %s from %s, "
|
||||||
("Basic",))
|
"are you sure server process has read access to /etc/shadow?",
|
||||||
|
repr(user), req.context.get("remote_addr"))
|
||||||
|
raise falcon.HTTPUnauthorized("Forbidden", "Invalid password", ("Basic",))
|
||||||
|
conn = None
|
||||||
|
elif "ldap" in config.AUTHENTICATION_BACKENDS:
|
||||||
|
upn = "%s@%s" % (user, config.KERBEROS_REALM)
|
||||||
|
click.echo("Connecting to %s as %s" % (config.LDAP_AUTHENTICATION_URI, upn))
|
||||||
|
conn = ldap.initialize(config.LDAP_AUTHENTICATION_URI, bytes_mode=False)
|
||||||
|
conn.set_option(ldap.OPT_REFERRALS, 0)
|
||||||
|
|
||||||
if not req.auth.startswith("Basic "):
|
try:
|
||||||
raise falcon.HTTPBadRequest("Bad request", "Bad header, expected Basic: %s" % req.auth)
|
conn.simple_bind_s(upn, passwd)
|
||||||
|
except ldap.STRONG_AUTH_REQUIRED:
|
||||||
|
logger.critical("LDAP server demands encryption, use ldaps:// instead of ldaps://")
|
||||||
|
raise
|
||||||
|
except ldap.SERVER_DOWN:
|
||||||
|
logger.critical("Failed to connect LDAP server at %s, are you sure LDAP server's CA certificate has been copied to this machine?",
|
||||||
|
config.LDAP_AUTHENTICATION_URI)
|
||||||
|
raise
|
||||||
|
except ldap.INVALID_CREDENTIALS:
|
||||||
|
logger.critical("LDAP bind authentication failed for user %s from %s",
|
||||||
|
repr(user), req.context.get("remote_addr"))
|
||||||
|
raise falcon.HTTPUnauthorized("Forbidden",
|
||||||
|
"Please authenticate with %s domain account username" % const.DOMAIN,
|
||||||
|
("Basic",))
|
||||||
|
|
||||||
from base64 import b64decode
|
req.context["ldap_conn"] = conn
|
||||||
basic, token = req.auth.split(" ", 1)
|
else:
|
||||||
user, passwd = b64decode(token).decode("ascii").split(":", 1)
|
raise NotImplementedError("No suitable authentication method configured")
|
||||||
|
|
||||||
upn = "%s@%s" % (user, const.DOMAIN)
|
|
||||||
click.echo("Connecting to %s as %s" % (config.LDAP_AUTHENTICATION_URI, upn))
|
|
||||||
conn = ldap.initialize(config.LDAP_AUTHENTICATION_URI, bytes_mode=False)
|
|
||||||
conn.set_option(ldap.OPT_REFERRALS, 0)
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
conn.simple_bind_s(upn, passwd)
|
req.context["user"] = User.objects.get(user)
|
||||||
except ldap.STRONG_AUTH_REQUIRED:
|
except User.DoesNotExist:
|
||||||
logger.critical("LDAP server demands encryption, use ldaps:// instead of ldaps://")
|
raise falcon.HTTPUnauthorized("Unauthorized", "Invalid credentials", ("Basic",))
|
||||||
raise
|
|
||||||
except ldap.SERVER_DOWN:
|
|
||||||
logger.critical("Failed to connect LDAP server at %s, are you sure LDAP server's CA certificate has been copied to this machine?",
|
|
||||||
config.LDAP_AUTHENTICATION_URI)
|
|
||||||
raise
|
|
||||||
except ldap.INVALID_CREDENTIALS:
|
|
||||||
logger.critical("LDAP bind authentication failed for user %s from %s",
|
|
||||||
repr(user), req.context.get("remote_addr"))
|
|
||||||
raise falcon.HTTPUnauthorized("Forbidden",
|
|
||||||
"Please authenticate with %s domain account username" % const.DOMAIN,
|
|
||||||
("Basic",))
|
|
||||||
|
|
||||||
req.context["ldap_conn"] = conn
|
|
||||||
req.context["user"] = User.objects.get(user)
|
|
||||||
retval = func(resource, req, resp, *args, **kwargs)
|
retval = func(resource, req, resp, *args, **kwargs)
|
||||||
conn.unbind_s()
|
if conn:
|
||||||
|
conn.unbind_s()
|
||||||
return retval
|
return retval
|
||||||
|
|
||||||
|
|
||||||
def pam_authenticate(resource, req, resp, *args, **kwargs):
|
|
||||||
"""
|
|
||||||
Authenticate against PAM with WWW Basic Auth credentials
|
|
||||||
"""
|
|
||||||
|
|
||||||
if optional and not req.get_param_as_bool("authenticate"):
|
|
||||||
return func(resource, req, resp, *args, **kwargs)
|
|
||||||
|
|
||||||
if not req.auth:
|
|
||||||
raise falcon.HTTPUnauthorized("Forbidden", "Please authenticate", ("Basic",))
|
|
||||||
|
|
||||||
if not req.auth.startswith("Basic "):
|
|
||||||
raise falcon.HTTPBadRequest("Bad request", "Bad header: %s" % req.auth)
|
|
||||||
|
|
||||||
basic, token = req.auth.split(" ", 1)
|
|
||||||
user, passwd = b64decode(token).decode("ascii").split(":", 1)
|
|
||||||
|
|
||||||
import simplepam
|
|
||||||
if not simplepam.authenticate(user, passwd, "sshd"):
|
|
||||||
logger.critical("Basic authentication failed for user %s from %s, "
|
|
||||||
"are you sure server process has read access to /etc/shadow?",
|
|
||||||
repr(user), req.context.get("remote_addr"))
|
|
||||||
raise falcon.HTTPUnauthorized("Forbidden", "Invalid password", ("Basic",))
|
|
||||||
|
|
||||||
req.context["user"] = User.objects.get(user)
|
|
||||||
return func(resource, req, resp, *args, **kwargs)
|
|
||||||
|
|
||||||
def wrapped(resource, req, resp, *args, **kwargs):
|
|
||||||
# If LDAP enabled and device is not Kerberos capable fall
|
|
||||||
# back to LDAP bind authentication
|
|
||||||
if "ldap" in config.AUTHENTICATION_BACKENDS:
|
|
||||||
if "Android" in req.user_agent or "iPhone" in req.user_agent:
|
|
||||||
return ldap_authenticate(resource, req, resp, *args, **kwargs)
|
|
||||||
if "kerberos" in config.AUTHENTICATION_BACKENDS:
|
|
||||||
return kerberos_authenticate(resource, req, resp, *args, **kwargs)
|
|
||||||
elif config.AUTHENTICATION_BACKENDS == {"pam"}:
|
|
||||||
return pam_authenticate(resource, req, resp, *args, **kwargs)
|
|
||||||
elif config.AUTHENTICATION_BACKENDS == {"ldap"}:
|
|
||||||
return ldap_authenticate(resource, req, resp, *args, **kwargs)
|
|
||||||
else:
|
|
||||||
raise NotImplementedError("Authentication backend %s not supported" % config.AUTHENTICATION_BACKENDS)
|
|
||||||
return wrapped
|
return wrapped
|
||||||
return wrapper
|
return wrapper
|
||||||
|
|
||||||
@ -247,7 +218,6 @@ def authorize_admin(func):
|
|||||||
@whitelist_subnets(config.ADMIN_SUBNETS)
|
@whitelist_subnets(config.ADMIN_SUBNETS)
|
||||||
def wrapped(resource, req, resp, *args, **kwargs):
|
def wrapped(resource, req, resp, *args, **kwargs):
|
||||||
if req.context.get("user").is_admin():
|
if req.context.get("user").is_admin():
|
||||||
req.context["admin_authorized"] = True
|
|
||||||
return func(resource, req, resp, *args, **kwargs)
|
return func(resource, req, resp, *args, **kwargs)
|
||||||
logger.info("User '%s' not authorized to access administrative API", req.context.get("user").name)
|
logger.info("User '%s' not authorized to access administrative API", req.context.get("user").name)
|
||||||
raise falcon.HTTPForbidden("Forbidden", "User not authorized to perform administrative operations")
|
raise falcon.HTTPForbidden("Forbidden", "User not authorized to perform administrative operations")
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
from __future__ import division, absolute_import, print_function
|
from __future__ import division, absolute_import, print_function
|
||||||
import click
|
import click
|
||||||
|
import logging
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
import requests
|
import requests
|
||||||
@ -20,6 +21,7 @@ from jinja2 import Template
|
|||||||
from random import SystemRandom
|
from random import SystemRandom
|
||||||
from xattr import getxattr, listxattr, setxattr
|
from xattr import getxattr, listxattr, setxattr
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
random = SystemRandom()
|
random = SystemRandom()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@ -214,7 +216,7 @@ def store_request(buf, overwrite=False, address="", user=""):
|
|||||||
return request_path, csr, common_name
|
return request_path, csr, common_name
|
||||||
|
|
||||||
|
|
||||||
def revoke(common_name, reason):
|
def revoke(common_name, reason, user="root"):
|
||||||
"""
|
"""
|
||||||
Revoke valid certificate
|
Revoke valid certificate
|
||||||
"""
|
"""
|
||||||
@ -228,18 +230,13 @@ def revoke(common_name, reason):
|
|||||||
setxattr(signed_path, "user.revocation.reason", reason)
|
setxattr(signed_path, "user.revocation.reason", reason)
|
||||||
revoked_path = os.path.join(config.REVOKED_DIR, "%040x.pem" % cert.serial_number)
|
revoked_path = os.path.join(config.REVOKED_DIR, "%040x.pem" % cert.serial_number)
|
||||||
|
|
||||||
|
logger.info("Revoked certificate %s by %s", common_name, user)
|
||||||
|
|
||||||
os.unlink(os.path.join(config.SIGNED_BY_SERIAL_DIR, "%040x.pem" % cert.serial_number))
|
os.unlink(os.path.join(config.SIGNED_BY_SERIAL_DIR, "%040x.pem" % cert.serial_number))
|
||||||
os.rename(signed_path, revoked_path)
|
os.rename(signed_path, revoked_path)
|
||||||
|
|
||||||
|
|
||||||
push.publish("certificate-revoked", common_name)
|
push.publish("certificate-revoked", common_name)
|
||||||
|
|
||||||
# Publish CRL for long polls
|
|
||||||
url = config.LONG_POLL_PUBLISH % "crl"
|
|
||||||
click.echo("Publishing CRL at %s ..." % url)
|
|
||||||
requests.post(url, data=export_crl(),
|
|
||||||
headers={"User-Agent": "Certidude API", "Content-Type": "application/x-pem-file"})
|
|
||||||
|
|
||||||
attach_cert = buf, "application/x-pem-file", common_name + ".crt"
|
attach_cert = buf, "application/x-pem-file", common_name + ".crt"
|
||||||
mailer.send("certificate-revoked.md",
|
mailer.send("certificate-revoked.md",
|
||||||
attachments=(attach_cert,),
|
attachments=(attach_cert,),
|
||||||
@ -334,7 +331,7 @@ def export_crl(pem=True):
|
|||||||
return certificate_list.dump()
|
return certificate_list.dump()
|
||||||
|
|
||||||
|
|
||||||
def delete_request(common_name):
|
def delete_request(common_name, user="root"):
|
||||||
# Validate CN
|
# Validate CN
|
||||||
if not re.match(const.RE_COMMON_NAME, common_name):
|
if not re.match(const.RE_COMMON_NAME, common_name):
|
||||||
raise ValueError("Invalid common name")
|
raise ValueError("Invalid common name")
|
||||||
@ -342,6 +339,9 @@ def delete_request(common_name):
|
|||||||
path, buf, csr, submitted = get_request(common_name)
|
path, buf, csr, submitted = get_request(common_name)
|
||||||
os.unlink(path)
|
os.unlink(path)
|
||||||
|
|
||||||
|
logger.info("Rejected signing request %s by %s" % (
|
||||||
|
common_name, user))
|
||||||
|
|
||||||
# Publish event at CA channel
|
# Publish event at CA channel
|
||||||
push.publish("request-deleted", common_name)
|
push.publish("request-deleted", common_name)
|
||||||
|
|
||||||
@ -350,7 +350,7 @@ def delete_request(common_name):
|
|||||||
config.LONG_POLL_PUBLISH % hashlib.sha256(buf).hexdigest(),
|
config.LONG_POLL_PUBLISH % hashlib.sha256(buf).hexdigest(),
|
||||||
headers={"User-Agent": "Certidude API"})
|
headers={"User-Agent": "Certidude API"})
|
||||||
|
|
||||||
def sign(common_name, profile, skip_notify=False, skip_push=False, overwrite=False, signer=None):
|
def sign(common_name, profile, skip_notify=False, skip_push=False, overwrite=False, signer="root"):
|
||||||
"""
|
"""
|
||||||
Sign certificate signing request by it's common name
|
Sign certificate signing request by it's common name
|
||||||
"""
|
"""
|
||||||
|
@ -1018,26 +1018,28 @@ def certidude_setup_authority(username, kerberos_keytab, nginx_config, organizat
|
|||||||
click.echo("Not attempting to install packages from APT as requested...")
|
click.echo("Not attempting to install packages from APT as requested...")
|
||||||
else:
|
else:
|
||||||
click.echo("Installing packages...")
|
click.echo("Installing packages...")
|
||||||
os.system("DEBIAN_FRONTEND=noninteractive apt-get install -qq -y \
|
cmd = "DEBIAN_FRONTEND=noninteractive apt-get install -qq -y \
|
||||||
cython3 python3-dev python3-mimeparse \
|
cython3 python3-dev \
|
||||||
python3-markdown python3-pyxattr python3-jinja2 python3-cffi \
|
python3-markdown python3-pyxattr python3-jinja2 python3-cffi \
|
||||||
software-properties-common libsasl2-modules-gssapi-mit npm nodejs \
|
software-properties-common libsasl2-modules-gssapi-mit npm nodejs \
|
||||||
libkrb5-dev libldap2-dev libsasl2-dev gawk libncurses5-dev \
|
libkrb5-dev libldap2-dev libsasl2-dev gawk libncurses5-dev \
|
||||||
rsync attr wget unzip")
|
rsync attr wget unzip"
|
||||||
os.system("pip3 install -q --upgrade gssapi falcon humanize ipaddress simplepam")
|
click.echo("Running: %s" % cmd)
|
||||||
os.system("pip3 install -q --pre --upgrade python-ldap")
|
if os.system(cmd): sys.exit(254)
|
||||||
|
if os.system("pip3 install -q --upgrade gssapi falcon humanize ipaddress simplepam user-agents"): sys.exit(253)
|
||||||
|
if os.system("pip3 install -q --pre --upgrade python-ldap"): exit(252)
|
||||||
|
|
||||||
if not os.path.exists("/usr/lib/nginx/modules/ngx_nchan_module.so"):
|
if not os.path.exists("/usr/lib/nginx/modules/ngx_nchan_module.so"):
|
||||||
click.echo("Enabling nginx PPA")
|
click.echo("Enabling nginx PPA")
|
||||||
os.system("add-apt-repository -y ppa:nginx/stable")
|
if os.system("add-apt-repository -y ppa:nginx/stable"): sys.exit(251)
|
||||||
os.system("apt-get update -q")
|
if os.system("apt-get update -q"): sys.exit(250)
|
||||||
os.system("apt-get install -y -q libnginx-mod-nchan")
|
if os.system("apt-get install -y -q libnginx-mod-nchan"): sys.exit(249)
|
||||||
else:
|
else:
|
||||||
click.echo("PPA for nginx already enabled")
|
click.echo("PPA for nginx already enabled")
|
||||||
|
|
||||||
if not os.path.exists("/usr/sbin/nginx"):
|
if not os.path.exists("/usr/sbin/nginx"):
|
||||||
click.echo("Installing nginx from PPA")
|
click.echo("Installing nginx from PPA")
|
||||||
os.system("apt-get install -y -q nginx")
|
if os.system("apt-get install -y -q nginx"): sys.exit(248)
|
||||||
else:
|
else:
|
||||||
click.echo("Web server nginx already installed")
|
click.echo("Web server nginx already installed")
|
||||||
|
|
||||||
@ -1160,16 +1162,16 @@ def certidude_setup_authority(username, kerberos_keytab, nginx_config, organizat
|
|||||||
else:
|
else:
|
||||||
cmd = "npm install --silent -g nunjucks@2.5.2 nunjucks-date@1.2.0 node-forge bootstrap@4.0.0-alpha.6 jquery timeago tether font-awesome qrcode-svg"
|
cmd = "npm install --silent -g nunjucks@2.5.2 nunjucks-date@1.2.0 node-forge bootstrap@4.0.0-alpha.6 jquery timeago tether font-awesome qrcode-svg"
|
||||||
click.echo("Installing JavaScript packages: %s" % cmd)
|
click.echo("Installing JavaScript packages: %s" % cmd)
|
||||||
assert os.system(cmd) == 0
|
if os.system(cmd): sys.exit(230)
|
||||||
|
|
||||||
# Copy fonts
|
# Copy fonts
|
||||||
click.echo("Copying fonts...")
|
click.echo("Copying fonts...")
|
||||||
os.system("rsync -avq /usr/local/lib/node_modules/font-awesome/fonts/ %s/fonts/" % assets_dir)
|
if os.system("rsync -avq /usr/local/lib/node_modules/font-awesome/fonts/ %s/fonts/" % assets_dir): sys.exit(229)
|
||||||
|
|
||||||
# Compile nunjucks templates
|
# Compile nunjucks templates
|
||||||
cmd = 'nunjucks-precompile --include ".html$" --include ".ps1$" --include ".sh$" --include ".svg" %s > %s.part' % (static_path, bundle_js)
|
cmd = 'nunjucks-precompile --include ".html$" --include ".ps1$" --include ".sh$" --include ".svg" %s > %s.part' % (static_path, bundle_js)
|
||||||
click.echo("Compiling templates: %s" % cmd)
|
click.echo("Compiling templates: %s" % cmd)
|
||||||
assert os.system(cmd) == 0
|
if os.system(cmd): sys.exit(228)
|
||||||
|
|
||||||
# Assemble bundle.js
|
# Assemble bundle.js
|
||||||
click.echo("Assembling %s" % bundle_js)
|
click.echo("Assembling %s" % bundle_js)
|
||||||
|
@ -44,6 +44,8 @@ OVERWRITE_SUBNETS = set([ipaddress.ip_network(j) for j in
|
|||||||
cp.get("authorization", "overwrite subnets").split(" ") if j])
|
cp.get("authorization", "overwrite subnets").split(" ") if j])
|
||||||
MACHINE_ENROLLMENT_SUBNETS = set([ipaddress.ip_network(j) for j in
|
MACHINE_ENROLLMENT_SUBNETS = set([ipaddress.ip_network(j) for j in
|
||||||
cp.get("authorization", "machine enrollment subnets").split(" ") if j])
|
cp.get("authorization", "machine enrollment subnets").split(" ") if j])
|
||||||
|
KERBEROS_SUBNETS = set([ipaddress.ip_network(j) for j in
|
||||||
|
cp.get("authorization", "kerberos subnets").split(" ") if j])
|
||||||
|
|
||||||
AUTHORITY_DIR = "/var/lib/certidude"
|
AUTHORITY_DIR = "/var/lib/certidude"
|
||||||
AUTHORITY_PRIVATE_KEY_PATH = cp.get("authority", "private key path")
|
AUTHORITY_PRIVATE_KEY_PATH = cp.get("authority", "private key path")
|
||||||
|
@ -1,4 +1,14 @@
|
|||||||
|
|
||||||
|
@keyframes fresh {
|
||||||
|
from { background-color: #ffc107; }
|
||||||
|
to { background-color: white; }
|
||||||
|
}
|
||||||
|
|
||||||
|
.fresh {
|
||||||
|
animation-name: fresh;
|
||||||
|
animation-duration: 30s;
|
||||||
|
}
|
||||||
|
|
||||||
.loader-container {
|
.loader-container {
|
||||||
margin: 20% auto 0 auto;
|
margin: 20% auto 0 auto;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
|
@ -15,15 +15,21 @@
|
|||||||
<button class="navbar-toggler navbar-toggler-right" type="button" data-toggle="collapse" data-target="#navbarsExampleDefault" aria-controls="navbarsExampleDefault" aria-expanded="false" aria-label="Toggle navigation">
|
<button class="navbar-toggler navbar-toggler-right" type="button" data-toggle="collapse" data-target="#navbarsExampleDefault" aria-controls="navbarsExampleDefault" aria-expanded="false" aria-label="Toggle navigation">
|
||||||
<span class="navbar-toggler-icon"></span>
|
<span class="navbar-toggler-icon"></span>
|
||||||
</button>
|
</button>
|
||||||
<a class="navbar-brand" href="#">Certidude</a>
|
<a class="navbar-brand" href="#columns=2">Certidude</a>
|
||||||
|
|
||||||
<div class="collapse navbar-collapse" id="navbarsExampleDefault">
|
<div class="collapse navbar-collapse" id="navbarsExampleDefault">
|
||||||
<ul class="navbar-nav mr-auto">
|
<ul class="navbar-nav mr-auto">
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link disabled dashboard" href="#">Dashboard</a>
|
<a class="nav-link disabled dashboard" href="#columns=2">Dashboard</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link disabled log" href="#">Log</a>
|
<a class="nav-link" href="#columns=3">Wider</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" href="#columns=4">Widest</a>
|
||||||
|
</li>
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" href="#">Log</a>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
<form class="form-inline my-2 my-lg-0">
|
<form class="form-inline my-2 my-lg-0">
|
||||||
|
@ -74,6 +74,7 @@ function onTagClicked(tag) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
function onNewTagClicked(menu) {
|
function onNewTagClicked(menu) {
|
||||||
@ -110,6 +111,7 @@ function onTagFilterChanged() {
|
|||||||
function onLogEntry (e) {
|
function onLogEntry (e) {
|
||||||
if (e.data) {
|
if (e.data) {
|
||||||
e = JSON.parse(e.data);
|
e = JSON.parse(e.data);
|
||||||
|
e.fresh = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($("#log-level-" + e.severity).prop("checked")) {
|
if ($("#log-level-" + e.severity).prop("checked")) {
|
||||||
@ -117,7 +119,8 @@ function onLogEntry (e) {
|
|||||||
entry: {
|
entry: {
|
||||||
created: new Date(e.created).toLocaleString(),
|
created: new Date(e.created).toLocaleString(),
|
||||||
message: e.message,
|
message: e.message,
|
||||||
severity: e.severity
|
severity: e.severity,
|
||||||
|
fresh: e.fresh,
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
@ -262,7 +265,7 @@ function onServerStarted() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function onServerStopped() {
|
function onServerStopped() {
|
||||||
$("view").html('<div class="loader"></div><p>Server under maintenance</p>');
|
$("#view-dashboard").html('<div class="loader"></div><p>Server under maintenance</p>');
|
||||||
console.info("Server stopped");
|
console.info("Server stopped");
|
||||||
|
|
||||||
}
|
}
|
||||||
|
25
certidude/static/snippets/certidude-client.sh
Normal file
25
certidude/static/snippets/certidude-client.sh
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
pip3 install git+https://github.com/laurivosandi/certidude/
|
||||||
|
mkdir -p /etc/certidude/{client.conf.d,services.conf.d}
|
||||||
|
cat << EOF > /etc/certidude/client.conf.d/{{ authority_name }}.conf
|
||||||
|
[{{ authority_name }}]
|
||||||
|
trigger = interface up
|
||||||
|
common name = $HOSTNAME
|
||||||
|
system wide = true
|
||||||
|
EOF
|
||||||
|
|
||||||
|
cat << EOF > /etc/certidude/services.conf.d/{{ authority_name }}.conf
|
||||||
|
{% for router in session.service.routers %}{% if "ikev2" in session.service.protocols %}
|
||||||
|
[IPSec to {{ router }}]
|
||||||
|
authority = {{ authority_name }}
|
||||||
|
service = network-manager/strongswan
|
||||||
|
remote = {{ router }}
|
||||||
|
{% endif %}{% if "openvpn" in session.service.protocols %}
|
||||||
|
[OpenVPN to {{ router }}]
|
||||||
|
authority = {{ authority_name }}
|
||||||
|
service = network-manager/openvpn
|
||||||
|
remote = {{ router }}
|
||||||
|
{% endif %}{% endfor %}
|
||||||
|
EOF
|
||||||
|
|
||||||
|
certidude enroll
|
||||||
|
|
@ -7,6 +7,12 @@
|
|||||||
</div>
|
</div>
|
||||||
<form action="/api/request/" method="post">
|
<form action="/api/request/" method="post">
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
|
<h5>Certidude client</h5>
|
||||||
|
<p>On Ubuntu or Fedora:</p>
|
||||||
|
<div class="highlight">
|
||||||
|
<pre class="code"><code>{% include "snippets/certidude-client.sh" %}</code></pre>
|
||||||
|
</div>
|
||||||
|
|
||||||
{% if "ikev2" in session.service.protocols %}
|
{% if "ikev2" in session.service.protocols %}
|
||||||
<h5>Windows {% if session.authority.certificate.algorithm == "ec" %}10{% else %}7 and up{% endif %}</h5>
|
<h5>Windows {% if session.authority.certificate.algorithm == "ec" %}10{% else %}7 and up{% endif %}</h5>
|
||||||
<p>On Windows execute following PowerShell script</p>
|
<p>On Windows execute following PowerShell script</p>
|
||||||
@ -190,6 +196,8 @@ curl http://{{authority_name}}/api/revoked/?wait=yes -L -H "Accept: application/
|
|||||||
{% endfor %}.
|
{% endfor %}.
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
See <a href="#request_submission_modal" data-toggle="modal">here</a> for more information on manual signing request upload.
|
||||||
|
|
||||||
{% if session.authority.autosign_subnets %}
|
{% if session.authority.autosign_subnets %}
|
||||||
{% if "0.0.0.0/0" in session.authority.autosign_subnets %}
|
{% if "0.0.0.0/0" in session.authority.autosign_subnets %}
|
||||||
All requests are automatically signed.
|
All requests are automatically signed.
|
||||||
@ -202,17 +210,16 @@ curl http://{{authority_name}}/api/revoked/?wait=yes -L -H "Accept: application/
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
{% if columns >= 3 %}
|
|
||||||
</div>
|
|
||||||
<div class="col-sm-{{ column_width }}">
|
|
||||||
{% endif %}
|
|
||||||
<div id="pending_requests">
|
<div id="pending_requests">
|
||||||
{% for request in session.authority.requests | sort(attribute="submitted", reverse=true) %}
|
{% for request in session.authority.requests | sort(attribute="submitted", reverse=true) %}
|
||||||
{% include "views/request.html" %}
|
{% include "views/request.html" %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
<p><h1>Revoked certificates</h1></p>
|
{% if columns >= 3 %}
|
||||||
|
</div>
|
||||||
|
<div class="col-sm-{{ column_width }}">
|
||||||
|
{% endif %}
|
||||||
|
<h1>Revoked certificates</h1>
|
||||||
<p>Following certificates have been revoked{% if session.features.crl %}, for more information click
|
<p>Following certificates have been revoked{% if session.features.crl %}, for more information click
|
||||||
<a href="#revocation_list_modal" data-toggle="modal">here</a>{% endif %}.</p>
|
<a href="#revocation_list_modal" data-toggle="modal">here</a>{% endif %}.</p>
|
||||||
|
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
<i class="fa fa-circle" style="color:{% if certificate.lease.age > 172800 %}#d9534f{% else %}{% if certificate.lease.age > 7200 %}#0275d8{% else %}#5cb85c{% endif %}{% endif %};"/>
|
<i class="fa fa-circle" style="color:{% if certificate.lease.age > 172800 %}#d9534f{% else %}{% if certificate.lease.age > 10800 %}#0275d8{% else %}#5cb85c{% endif %}{% endif %};"/>
|
||||||
Last seen
|
Last seen
|
||||||
<time class="timeago" datetime="{{ certificate.lease.last_seen }}">{{ certificate.lease.last_seen }}</time>
|
<time class="timeago" datetime="{{ certificate.lease.last_seen }}">{{ certificate.lease.last_seen }}</time>
|
||||||
at
|
at
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
<li id="log_entry_{{ entry.id }}" class="list-group-item justify-content-between filterable">
|
<li id="log_entry_{{ entry.id }}" class="list-group-item justify-content-between filterable{% if entry.fresh %} fresh{% endif %}">
|
||||||
<span>
|
<span>
|
||||||
<i class="fa fa-{{ entry.severity }}-circle"/>
|
<i class="fa fa-{{ entry.severity }}-circle"/>
|
||||||
{{ entry.message }}
|
{{ entry.message }}
|
||||||
|
@ -4,14 +4,15 @@
|
|||||||
# sshd PAM service. In case of 'kerberos' SPNEGO is used to authenticate
|
# sshd PAM service. In case of 'kerberos' SPNEGO is used to authenticate
|
||||||
# user against eg. Active Directory or Samba4.
|
# user against eg. Active Directory or Samba4.
|
||||||
|
|
||||||
{% if realm %}
|
|
||||||
;backends = pam
|
|
||||||
backends = kerberos
|
|
||||||
{% else %}
|
|
||||||
backends = pam
|
|
||||||
;backends = kerberos
|
|
||||||
{% endif %}
|
|
||||||
;backends = ldap
|
;backends = ldap
|
||||||
|
;backends = kerberos
|
||||||
|
{% if realm %}
|
||||||
|
backends = kerberos ldap
|
||||||
|
;backends = pam
|
||||||
|
{% else %}
|
||||||
|
;backends = kerberos ldap
|
||||||
|
backends = pam
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
kerberos keytab = FILE:{{ kerberos_keytab }}
|
kerberos keytab = FILE:{{ kerberos_keytab }}
|
||||||
{% if realm %}
|
{% if realm %}
|
||||||
@ -103,9 +104,6 @@ admin whitelist =
|
|||||||
# Users are allowed to log in from user subnets
|
# Users are allowed to log in from user subnets
|
||||||
user subnets = 0.0.0.0/0
|
user subnets = 0.0.0.0/0
|
||||||
|
|
||||||
# Authority administrators are allowed to sign and revoke certificates from these subnets
|
|
||||||
admin subnets = 0.0.0.0/0
|
|
||||||
|
|
||||||
# Certificate signing requests are allowed to be submitted from these subnets
|
# Certificate signing requests are allowed to be submitted from these subnets
|
||||||
request subnets = 0.0.0.0/0
|
request subnets = 0.0.0.0/0
|
||||||
|
|
||||||
@ -135,6 +133,14 @@ renewal subnets =
|
|||||||
overwrite subnets =
|
overwrite subnets =
|
||||||
;overwrite subnets = 0.0.0.0/0
|
;overwrite subnets = 0.0.0.0/0
|
||||||
|
|
||||||
|
|
||||||
|
# Which subnets are offered Kerberos authentication, eg.
|
||||||
|
# subnet for Windows workstations or slice of VPN subnet where
|
||||||
|
# workstations are assigned to
|
||||||
|
kerberos subnets = 0.0.0.0
|
||||||
|
;kerberos subnets =
|
||||||
|
|
||||||
|
|
||||||
# Source subnets of Kerberos authenticated machines which are automatically
|
# Source subnets of Kerberos authenticated machines which are automatically
|
||||||
# allowed to enroll with CSR whose common name is set to machine's account name.
|
# allowed to enroll with CSR whose common name is set to machine's account name.
|
||||||
# Note that overwriting is not allowed by default, see 'overwrite subnets'
|
# Note that overwriting is not allowed by default, see 'overwrite subnets'
|
||||||
@ -142,6 +148,13 @@ overwrite subnets =
|
|||||||
machine enrollment subnets =
|
machine enrollment subnets =
|
||||||
;machine enrollment subnets = 0.0.0.0/0
|
;machine enrollment subnets = 0.0.0.0/0
|
||||||
|
|
||||||
|
|
||||||
|
# Authenticated users belonging to administrative LDAP or POSIX group
|
||||||
|
# are allowed to sign and revoke certificates from these subnets
|
||||||
|
admin subnets = 0.0.0.0/0
|
||||||
|
;admin subnets = 172.20.7.0/24 172.20.8.5
|
||||||
|
|
||||||
|
|
||||||
[logging]
|
[logging]
|
||||||
# Disable logging
|
# Disable logging
|
||||||
backend =
|
backend =
|
||||||
|
@ -169,6 +169,12 @@ def test_cli_setup_authority():
|
|||||||
if not os.path.exists("/etc/pki/ca-trust/source/anchors/"):
|
if not os.path.exists("/etc/pki/ca-trust/source/anchors/"):
|
||||||
os.makedirs("/etc/pki/ca-trust/source/anchors/")
|
os.makedirs("/etc/pki/ca-trust/source/anchors/")
|
||||||
|
|
||||||
|
if not os.path.exists("/bin/systemctl"):
|
||||||
|
with open("/usr/bin/systemctl", "w") as fh:
|
||||||
|
fh.write("#!/bin/bash\n")
|
||||||
|
fh.write("service $2 $1\n")
|
||||||
|
os.chmod("/usr/bin/systemctl", 0o755)
|
||||||
|
|
||||||
# Back up original DNS server
|
# Back up original DNS server
|
||||||
if not os.path.exists("/etc/resolv.conf.orig"):
|
if not os.path.exists("/etc/resolv.conf.orig"):
|
||||||
shutil.copyfile("/etc/resolv.conf", "/etc/resolv.conf.orig")
|
shutil.copyfile("/etc/resolv.conf", "/etc/resolv.conf.orig")
|
||||||
@ -205,7 +211,7 @@ def test_cli_setup_authority():
|
|||||||
assert const.HOSTNAME == "ca"
|
assert const.HOSTNAME == "ca"
|
||||||
assert const.DOMAIN == "example.lan"
|
assert const.DOMAIN == "example.lan"
|
||||||
|
|
||||||
os.system("certidude setup authority --elliptic-curve")
|
assert os.system("certidude setup authority --elliptic-curve") == 0
|
||||||
|
|
||||||
assert_cleanliness()
|
assert_cleanliness()
|
||||||
|
|
||||||
@ -289,13 +295,7 @@ def test_cli_setup_authority():
|
|||||||
assert r.status_code == 400, r.text
|
assert r.status_code == 400, r.text
|
||||||
|
|
||||||
r = client().simulate_get("/")
|
r = client().simulate_get("/")
|
||||||
assert r.status_code == 200, r.text
|
assert r.status_code == 404, r.text # backend doesn't serve static
|
||||||
r = client().simulate_get("/index.html")
|
|
||||||
assert r.status_code == 200, r.text
|
|
||||||
r = client().simulate_get("/nonexistant.html")
|
|
||||||
assert r.status_code == 404, r.text
|
|
||||||
r = client().simulate_get("/../nonexistant.html")
|
|
||||||
assert r.status_code == 400, r.text
|
|
||||||
|
|
||||||
# Test request submission
|
# Test request submission
|
||||||
buf = generate_csr(cn="test")
|
buf = generate_csr(cn="test")
|
||||||
@ -440,11 +440,6 @@ def test_cli_setup_authority():
|
|||||||
headers={"Accept":"text/plain"})
|
headers={"Accept":"text/plain"})
|
||||||
assert r.status_code == 415, r.text
|
assert r.status_code == 415, r.text
|
||||||
|
|
||||||
r = client().simulate_get("/api/revoked/",
|
|
||||||
query_string="wait=true",
|
|
||||||
headers={"Accept":"application/x-pem-file"})
|
|
||||||
assert r.status_code == 303, r.text
|
|
||||||
|
|
||||||
# Test attribute fetching API call
|
# Test attribute fetching API call
|
||||||
r = client().simulate_get("/api/signed/test/attr/")
|
r = client().simulate_get("/api/signed/test/attr/")
|
||||||
assert r.status_code == 401, r.text
|
assert r.status_code == 401, r.text
|
||||||
@ -1114,22 +1109,23 @@ def test_cli_setup_authority():
|
|||||||
|
|
||||||
# Bootstrap authority
|
# Bootstrap authority
|
||||||
assert not os.path.exists("/var/lib/certidude/ca.example.lan/ca_key.pem")
|
assert not os.path.exists("/var/lib/certidude/ca.example.lan/ca_key.pem")
|
||||||
os.system("certidude setup authority --skip-packages")
|
assert os.system("certidude setup authority --skip-packages") == 0
|
||||||
|
|
||||||
|
|
||||||
# Make modifications to /etc/certidude/server.conf so
|
# Make modifications to /etc/certidude/server.conf so
|
||||||
# Certidude would auth against domain controller
|
# Certidude would auth against domain controller
|
||||||
os.system("sed -e 's/ldap uri = ldaps:.*/ldap uri = ldaps:\\/\\/ca.example.lan/g' -i /etc/certidude/server.conf")
|
assert os.system("sed -e 's/ldap uri = ldaps:.*/ldap uri = ldaps:\\/\\/ca.example.lan/g' -i /etc/certidude/server.conf") == 0
|
||||||
os.system("sed -e 's/ldap uri = ldap:.*/ldap uri = ldap:\\/\\/ca.example.lan/g' -i /etc/certidude/server.conf")
|
assert os.system("sed -e 's/ldap uri = ldap:.*/ldap uri = ldap:\\/\\/ca.example.lan/g' -i /etc/certidude/server.conf") == 0
|
||||||
os.system("sed -e 's/autosign subnets =.*/autosign subnets =/g' -i /etc/certidude/server.conf")
|
assert os.system("sed -e 's/autosign subnets =.*/autosign subnets =/g' -i /etc/certidude/server.conf") == 0
|
||||||
os.system("sed -e 's/machine enrollment subnets =.*/machine enrollment subnets = 0.0.0.0\\/0/g' -i /etc/certidude/server.conf")
|
assert os.system("sed -e 's/machine enrollment subnets =.*/machine enrollment subnets = 0.0.0.0\\/0/g' -i /etc/certidude/server.conf") == 0
|
||||||
os.system("sed -e 's/scep subnets =.*/scep subnets = 0.0.0.0\\/0/g' -i /etc/certidude/server.conf")
|
assert os.system("sed -e 's/scep subnets =.*/scep subnets = 0.0.0.0\\/0/g' -i /etc/certidude/server.conf") == 0
|
||||||
os.system("sed -e 's/ocsp subnets =.*/ocsp subnets =/g' -i /etc/certidude/server.conf")
|
assert os.system("sed -e 's/ocsp subnets =.*/ocsp subnets =/g' -i /etc/certidude/server.conf") == 0
|
||||||
os.system("sed -e 's/crl subnets =.*/crl subnets =/g' -i /etc/certidude/server.conf")
|
assert os.system("sed -e 's/crl subnets =.*/crl subnets =/g' -i /etc/certidude/server.conf") == 0
|
||||||
os.system("sed -e 's/address = certificates@example.lan/address =/g' -i /etc/certidude/server.conf")
|
assert os.system("sed -e 's/address = certificates@example.lan/address =/g' -i /etc/certidude/server.conf") == 0
|
||||||
|
assert os.system("sed -e 's/kerberos subnets =.*/kerberos subnets = 0.0.0.0\\/0/g' -i /etc/certidude/server.conf") == 0
|
||||||
|
|
||||||
# Update server credential cache
|
# Update server credential cache
|
||||||
os.system("sed -e 's/dc1/ca/g' -i /etc/cron.hourly/certidude")
|
assert os.system("sed -e 's/dc1/ca/g' -i /etc/cron.hourly/certidude") == 0
|
||||||
with open("/etc/cron.hourly/certidude") as fh:
|
with open("/etc/cron.hourly/certidude") as fh:
|
||||||
cronjob = fh.read()
|
cronjob = fh.read()
|
||||||
assert "ldap/ca.example.lan" in cronjob, cronjob
|
assert "ldap/ca.example.lan" in cronjob, cronjob
|
||||||
|
Loading…
Reference in New Issue
Block a user