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)
|
||||||
|
if modified:
|
||||||
push.publish("attribute-update", cn)
|
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,13 +20,6 @@ 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"):
|
|
||||||
url = config.LONG_POLL_SUBSCRIBE % "crl"
|
|
||||||
resp.status = falcon.HTTP_SEE_OTHER
|
|
||||||
resp.set_header("Location", url)
|
|
||||||
logger.debug("Redirecting to CRL request to %s", url)
|
|
||||||
resp.body = "Redirecting to %s" % url
|
|
||||||
else:
|
|
||||||
resp.set_header("Content-Type", "application/x-pem-file")
|
resp.set_header("Content-Type", "application/x-pem-file")
|
||||||
resp.append_header(
|
resp.append_header(
|
||||||
"Content-Disposition",
|
"Content-Disposition",
|
||||||
|
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,18 +83,34 @@ 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)
|
||||||
|
|
||||||
|
if kerberized:
|
||||||
logger.debug("No Kerberos ticket offered while attempting to access %s from %s",
|
logger.debug("No Kerberos ticket offered while attempting to access %s from %s",
|
||||||
req.env["PATH_INFO"], req.context.get("remote_addr"))
|
req.env["PATH_INFO"], req.context.get("remote_addr"))
|
||||||
raise falcon.HTTPUnauthorized("Unauthorized",
|
raise falcon.HTTPUnauthorized("Unauthorized",
|
||||||
"No Kerberos ticket offered, are you sure you've logged in with domain user account?",
|
"No Kerberos ticket offered, are you sure you've logged in with domain user account?",
|
||||||
["Negotiate"])
|
["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",))
|
||||||
|
|
||||||
|
if kerberized:
|
||||||
|
if not req.auth.startswith("Negotiate "):
|
||||||
|
raise falcon.HTTPBadRequest("Bad request",
|
||||||
|
"Bad header, expected Negotiate: %s" % req.auth)
|
||||||
|
|
||||||
os.environ["KRB5_KTNAME"] = config.KERBEROS_KEYTAB
|
os.environ["KRB5_KTNAME"] = config.KERBEROS_KEYTAB
|
||||||
|
|
||||||
@ -107,9 +125,6 @@ def authenticate(optional=False):
|
|||||||
|
|
||||||
context = gssapi.sec_contexts.SecurityContext(creds=server_creds)
|
context = gssapi.sec_contexts.SecurityContext(creds=server_creds)
|
||||||
|
|
||||||
if not req.auth.startswith("Negotiate "):
|
|
||||||
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:
|
||||||
@ -141,30 +156,21 @@ def authenticate(optional=False):
|
|||||||
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)
|
return func(resource, req, resp, *args, **kwargs)
|
||||||
|
|
||||||
|
else:
|
||||||
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)
|
|
||||||
|
|
||||||
import ldap
|
|
||||||
|
|
||||||
if not req.auth:
|
|
||||||
raise falcon.HTTPUnauthorized("Unauthorized",
|
|
||||||
"No authentication header provided",
|
|
||||||
("Basic",))
|
|
||||||
|
|
||||||
if not req.auth.startswith("Basic "):
|
if not req.auth.startswith("Basic "):
|
||||||
raise falcon.HTTPBadRequest("Bad request", "Bad header, expected Basic: %s" % req.auth)
|
raise falcon.HTTPBadRequest("Bad request", "Bad header, expected Basic: %s" % req.auth)
|
||||||
|
|
||||||
from base64 import b64decode
|
|
||||||
basic, token = req.auth.split(" ", 1)
|
basic, token = req.auth.split(" ", 1)
|
||||||
user, passwd = b64decode(token).decode("ascii").split(":", 1)
|
user, passwd = b64decode(token).decode("ascii").split(":", 1)
|
||||||
|
|
||||||
upn = "%s@%s" % (user, const.DOMAIN)
|
if config.AUTHENTICATION_BACKENDS == {"pam"}:
|
||||||
|
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",))
|
||||||
|
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))
|
click.echo("Connecting to %s as %s" % (config.LDAP_AUTHENTICATION_URI, upn))
|
||||||
conn = ldap.initialize(config.LDAP_AUTHENTICATION_URI, bytes_mode=False)
|
conn = ldap.initialize(config.LDAP_AUTHENTICATION_URI, bytes_mode=False)
|
||||||
conn.set_option(ldap.OPT_REFERRALS, 0)
|
conn.set_option(ldap.OPT_REFERRALS, 0)
|
||||||
@ -186,53 +192,18 @@ def authenticate(optional=False):
|
|||||||
("Basic",))
|
("Basic",))
|
||||||
|
|
||||||
req.context["ldap_conn"] = conn
|
req.context["ldap_conn"] = conn
|
||||||
|
else:
|
||||||
|
raise NotImplementedError("No suitable authentication method configured")
|
||||||
|
|
||||||
|
try:
|
||||||
req.context["user"] = User.objects.get(user)
|
req.context["user"] = User.objects.get(user)
|
||||||
|
except User.DoesNotExist:
|
||||||
|
raise falcon.HTTPUnauthorized("Unauthorized", "Invalid credentials", ("Basic",))
|
||||||
|
|
||||||
retval = func(resource, req, resp, *args, **kwargs)
|
retval = func(resource, req, resp, *args, **kwargs)
|
||||||
|
if conn:
|
||||||
conn.unbind_s()
|
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