mirror of
https://github.com/laurivosandi/certidude
synced 2024-12-22 08:15:18 +00:00
Several updates #2
* Reverse RDN components for all certs * Less side effects in unittests * Split help dialog shell snippets into separate files * Restore 'admin subnets' config option * Embedded subnets, IKE and ESP proposals now configurable in builder.conf * Use expr instead of bc for math operations in shell * Better frontend support for Let's Encrypt certificates
This commit is contained in:
parent
5e9251f365
commit
4e4b551cc2
@ -7,12 +7,12 @@ import os
|
||||
import hashlib
|
||||
from datetime import datetime
|
||||
from xattr import listxattr, getxattr
|
||||
from certidude.auth import login_required
|
||||
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__)
|
||||
|
||||
@ -30,6 +30,7 @@ class SessionResource(AuthorityHandler):
|
||||
@csrf_protection
|
||||
@serialize
|
||||
@login_required
|
||||
@authorize_admin
|
||||
def on_get(self, req, resp):
|
||||
|
||||
def serialize_requests(g):
|
||||
@ -43,7 +44,6 @@ class SessionResource(AuthorityHandler):
|
||||
except IOError:
|
||||
submission_hostname = None
|
||||
yield dict(
|
||||
server = self.authority.server_flags(common_name),
|
||||
submitted = submitted,
|
||||
common_name = common_name,
|
||||
address = submission_address,
|
||||
@ -55,7 +55,7 @@ class SessionResource(AuthorityHandler):
|
||||
)
|
||||
|
||||
def serialize_revoked(g):
|
||||
for common_name, path, buf, cert, signed, expired, revoked, reason in 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,
|
||||
@ -121,10 +121,7 @@ class SessionResource(AuthorityHandler):
|
||||
if e["extn_id"].native in ("extended_key_usage",)])
|
||||
)
|
||||
|
||||
if req.context.get("user").is_admin():
|
||||
logger.info("Logged in authority administrator %s from %s" % (req.context.get("user"), req.context.get("remote_addr")))
|
||||
else:
|
||||
logger.info("Logged in authority user %s from %s" % (req.context.get("user"), req.context.get("remote_addr")))
|
||||
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,
|
||||
@ -175,14 +172,15 @@ class SessionResource(AuthorityHandler):
|
||||
profiles = sorted([p.serialize() for p in config.PROFILES.values()], key=lambda p:p.get("slug")),
|
||||
|
||||
)
|
||||
) if req.context.get("user").is_admin() else None,
|
||||
),
|
||||
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))
|
||||
logging=config.LOGGING_BACKEND)
|
||||
)
|
||||
|
||||
|
||||
class StaticResource(object):
|
||||
|
@ -4,9 +4,7 @@ import re
|
||||
from xattr import setxattr, listxattr, removexattr
|
||||
from certidude import push
|
||||
from certidude.decorators import serialize, csrf_protection
|
||||
from certidude.auth import login_required, authorize_admin
|
||||
|
||||
from .utils.firewall import whitelist_subject
|
||||
from .utils.firewall import login_required, authorize_admin, whitelist_subject
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
@ -5,9 +5,10 @@ import logging
|
||||
import os
|
||||
import subprocess
|
||||
from certidude import config, const, authority
|
||||
from certidude.auth import login_required, authorize_admin
|
||||
from certidude.common import cert_to_dn
|
||||
from ipaddress import ip_network
|
||||
from jinja2 import Template
|
||||
from .utils.firewall import login_required, authorize_admin
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@ -17,6 +18,7 @@ class ImageBuilderResource(object):
|
||||
def on_get(self, req, resp, profile, suggested_filename):
|
||||
router = [j[0] for j in authority.list_signed(
|
||||
common_name=config.cp2.get(profile, "router"))][0]
|
||||
subnets = set([ip_network(j) for j in config.cp2.get(profile, "subnets").split(" ")])
|
||||
model = config.cp2.get(profile, "model")
|
||||
build_script_path = config.cp2.get(profile, "command")
|
||||
overlay_path = config.cp2.get(profile, "overlay")
|
||||
@ -40,6 +42,9 @@ class ImageBuilderResource(object):
|
||||
cwd=os.path.dirname(os.path.realpath(build_script_path)),
|
||||
env={"PROFILE": model, "PATH":"/usr/sbin:/usr/bin:/sbin:/bin",
|
||||
"ROUTER": router,
|
||||
"IKE": config.cp2.get(profile, "ike"),
|
||||
"ESP": config.cp2.get(profile, "esp"),
|
||||
"SUBNETS": ",".join(str(j) for j in subnets),
|
||||
"AUTHORITY_CERTIFICATE_ALGORITHM": authority.public_key.algorithm,
|
||||
"AUTHORITY_CERTIFICATE_DISTINGUISHED_NAME": cert_to_dn(authority.certificate),
|
||||
"BUILD":build, "OVERLAY":build + "/overlay/"},
|
||||
|
@ -5,9 +5,9 @@ import re
|
||||
import xattr
|
||||
from datetime import datetime
|
||||
from certidude import config, push
|
||||
from certidude.auth import login_required, authorize_admin, authorize_server
|
||||
from certidude.decorators import serialize
|
||||
from .utils import AuthorityHandler
|
||||
from .utils.firewall import login_required, authorize_admin, authorize_server
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
|
||||
from certidude.auth import login_required, authorize_admin
|
||||
from certidude.decorators import serialize
|
||||
from certidude.relational import RelationalMixin
|
||||
from .utils.firewall import login_required, authorize_admin
|
||||
|
||||
class LogResource(RelationalMixin):
|
||||
SQL_CREATE_TABLES = "log_tables.sql"
|
||||
|
@ -8,7 +8,6 @@ from asn1crypto import pem, x509
|
||||
from asn1crypto.csr import CertificationRequest
|
||||
from base64 import b64decode
|
||||
from certidude import config, push, errors
|
||||
from certidude.auth import login_required, login_optional, authorize_admin
|
||||
from certidude.decorators import csrf_protection, MyEncoder
|
||||
from certidude.profile import SignatureProfile
|
||||
from datetime import datetime
|
||||
@ -16,7 +15,8 @@ from oscrypto import asymmetric
|
||||
from oscrypto.errors import SignatureError
|
||||
from xattr import getxattr, setxattr
|
||||
from .utils import AuthorityHandler
|
||||
from .utils.firewall import whitelist_subnets, whitelist_content_types
|
||||
from .utils.firewall import whitelist_subnets, whitelist_content_types, \
|
||||
login_required, login_optional, authorize_admin
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@ -219,7 +219,6 @@ class RequestDetailResource(AuthorityHandler):
|
||||
resp.body = json.dumps(dict(
|
||||
submitted = submitted,
|
||||
common_name = cn,
|
||||
server = self.authority.server_flags(cn),
|
||||
address = getxattr(path, "user.request.address").decode("ascii"), # TODO: move to authority.py
|
||||
md5sum = hashlib.md5(buf).hexdigest(),
|
||||
sha1sum = hashlib.sha1(buf).hexdigest(),
|
||||
|
@ -3,10 +3,10 @@ import falcon
|
||||
import logging
|
||||
import json
|
||||
import hashlib
|
||||
from certidude.auth import login_required, authorize_admin
|
||||
from certidude.decorators import csrf_protection
|
||||
from xattr import listxattr, getxattr
|
||||
from .utils import AuthorityHandler
|
||||
from .utils.firewall import login_required, authorize_admin
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
@ -1,9 +1,9 @@
|
||||
import logging
|
||||
from xattr import getxattr, removexattr, setxattr
|
||||
from certidude import push
|
||||
from certidude.auth import login_required, authorize_admin
|
||||
from certidude.decorators import serialize, csrf_protection
|
||||
from .utils import AuthorityHandler
|
||||
from .utils.firewall import login_required, authorize_admin
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
@ -9,8 +9,8 @@ from certidude import mailer
|
||||
from certidude.decorators import serialize
|
||||
from certidude.user import User
|
||||
from certidude import config
|
||||
from certidude.auth import login_required, authorize_admin
|
||||
from .utils import AuthorityHandler
|
||||
from .utils.firewall import login_required, authorize_admin
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
@ -1,7 +1,16 @@
|
||||
|
||||
import falcon
|
||||
import logging
|
||||
import binascii
|
||||
import click
|
||||
import gssapi
|
||||
import os
|
||||
import re
|
||||
import socket
|
||||
from asn1crypto import pem, x509
|
||||
from base64 import b64decode
|
||||
from certidude.user import User
|
||||
from certidude import config, const
|
||||
|
||||
logger = logging.getLogger("api")
|
||||
|
||||
@ -68,3 +77,200 @@ def whitelist_subject(func):
|
||||
else:
|
||||
return func(self, req, resp, cn, *args, **kwargs)
|
||||
return wrapped
|
||||
|
||||
|
||||
def authenticate(optional=False):
|
||||
def wrapper(func):
|
||||
def kerberos_authenticate(resource, req, resp, *args, **kwargs):
|
||||
# Try pre-emptive authentication
|
||||
if not req.auth:
|
||||
if optional:
|
||||
req.context["user"] = None
|
||||
return func(resource, req, resp, *args, **kwargs)
|
||||
|
||||
logger.debug("No Kerberos ticket offered while attempting to access %s from %s",
|
||||
req.env["PATH_INFO"], req.context.get("remote_addr"))
|
||||
raise falcon.HTTPUnauthorized("Unauthorized",
|
||||
"No Kerberos ticket offered, are you sure you've logged in with domain user account?",
|
||||
["Negotiate"])
|
||||
|
||||
os.environ["KRB5_KTNAME"] = config.KERBEROS_KEYTAB
|
||||
|
||||
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
|
||||
|
||||
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:])
|
||||
|
||||
try:
|
||||
context.step(b64decode(token))
|
||||
except binascii.Error: # base64 errors
|
||||
raise falcon.HTTPBadRequest("Bad request", "Malformed token")
|
||||
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.")
|
||||
|
||||
try:
|
||||
username, realm = str(context.initiator_name).split("@")
|
||||
except AttributeError: # TODO: Better exception
|
||||
raise falcon.HTTPForbidden("Failed to determine username, are you trying to log in with correct domain account?")
|
||||
|
||||
if realm != config.KERBEROS_REALM:
|
||||
raise falcon.HTTPForbidden("Forbidden",
|
||||
"Cross-realm trust not supported")
|
||||
|
||||
if username.endswith("$") and optional:
|
||||
# Extract machine hostname
|
||||
# TODO: Assert LDAP group membership
|
||||
req.context["machine"] = username[:-1].lower()
|
||||
req.context["user"] = None
|
||||
else:
|
||||
# Attempt to look up real user
|
||||
req.context["user"] = User.objects.get(username)
|
||||
|
||||
logger.debug("Succesfully authenticated user %s for %s from %s",
|
||||
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)
|
||||
|
||||
import ldap
|
||||
|
||||
if not req.auth:
|
||||
raise falcon.HTTPUnauthorized("Unauthorized",
|
||||
"No authentication header provided",
|
||||
("Basic",))
|
||||
|
||||
if not req.auth.startswith("Basic "):
|
||||
raise falcon.HTTPBadRequest("Bad request", "Bad header, expected Basic: %s" % req.auth)
|
||||
|
||||
from base64 import b64decode
|
||||
basic, token = req.auth.split(" ", 1)
|
||||
user, passwd = b64decode(token).decode("ascii").split(":", 1)
|
||||
|
||||
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:
|
||||
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",))
|
||||
|
||||
req.context["ldap_conn"] = conn
|
||||
req.context["user"] = User.objects.get(user)
|
||||
retval = func(resource, req, resp, *args, **kwargs)
|
||||
conn.unbind_s()
|
||||
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 wrapper
|
||||
|
||||
|
||||
def login_required(func):
|
||||
return authenticate()(func)
|
||||
|
||||
def login_optional(func):
|
||||
return authenticate(optional=True)(func)
|
||||
|
||||
def authorize_admin(func):
|
||||
@whitelist_subnets(config.ADMIN_SUBNETS)
|
||||
def wrapped(resource, req, resp, *args, **kwargs):
|
||||
if req.context.get("user").is_admin():
|
||||
req.context["admin_authorized"] = True
|
||||
return func(resource, req, resp, *args, **kwargs)
|
||||
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")
|
||||
return wrapped
|
||||
|
||||
def authorize_server(func):
|
||||
"""
|
||||
Make sure the request originator has a certificate with server flags
|
||||
"""
|
||||
from asn1crypto import pem, x509
|
||||
def wrapped(resource, req, resp, *args, **kwargs):
|
||||
buf = req.get_header("X-SSL-CERT")
|
||||
if not buf:
|
||||
logger.info("No TLS certificate presented to access administrative API call")
|
||||
raise falcon.HTTPForbidden("Forbidden", "Machine not authorized to perform the operation")
|
||||
|
||||
header, _, der_bytes = pem.unarmor(buf.replace("\t", "").encode("ascii"))
|
||||
cert = x509.Certificate.load(der_bytes) # TODO: validate serial
|
||||
for extension in cert["tbs_certificate"]["extensions"]:
|
||||
if extension["extn_id"].native == "extended_key_usage":
|
||||
if "server_auth" in extension["extn_value"].native:
|
||||
req.context["machine"] = cert.subject.native["common_name"]
|
||||
return func(resource, req, resp, *args, **kwargs)
|
||||
logger.info("TLS authenticated machine '%s' not authorized to access administrative API", cert.subject.native["common_name"])
|
||||
raise falcon.HTTPForbidden("Forbidden", "Machine not authorized to perform the operation")
|
||||
return wrapped
|
||||
|
@ -1,209 +0,0 @@
|
||||
|
||||
import binascii
|
||||
import click
|
||||
import gssapi
|
||||
import falcon
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
import socket
|
||||
from base64 import b64decode
|
||||
from certidude.user import User
|
||||
from certidude import config, const
|
||||
|
||||
logger = logging.getLogger("api")
|
||||
|
||||
def authenticate(optional=False):
|
||||
def wrapper(func):
|
||||
def kerberos_authenticate(resource, req, resp, *args, **kwargs):
|
||||
# Try pre-emptive authentication
|
||||
if not req.auth:
|
||||
if optional:
|
||||
req.context["user"] = None
|
||||
return func(resource, req, resp, *args, **kwargs)
|
||||
|
||||
logger.debug("No Kerberos ticket offered while attempting to access %s from %s",
|
||||
req.env["PATH_INFO"], req.context.get("remote_addr"))
|
||||
raise falcon.HTTPUnauthorized("Unauthorized",
|
||||
"No Kerberos ticket offered, are you sure you've logged in with domain user account?",
|
||||
["Negotiate"])
|
||||
|
||||
os.environ["KRB5_KTNAME"] = config.KERBEROS_KEYTAB
|
||||
|
||||
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
|
||||
|
||||
context = gssapi.sec_contexts.SecurityContext(creds=server_creds)
|
||||
|
||||
if not req.auth.startswith("Negotiate "):
|
||||
raise falcon.HTTPBadRequest("Bad request", "Bad header: %s" % req.auth)
|
||||
|
||||
token = ''.join(req.auth.split()[1:])
|
||||
|
||||
try:
|
||||
context.step(b64decode(token))
|
||||
except binascii.Error: # base64 errors
|
||||
raise falcon.HTTPBadRequest("Bad request", "Malformed token")
|
||||
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.")
|
||||
|
||||
try:
|
||||
username, realm = str(context.initiator_name).split("@")
|
||||
except AttributeError: # TODO: Better exception
|
||||
raise falcon.HTTPForbidden("Failed to determine username, are you trying to log in with correct domain account?")
|
||||
|
||||
if realm != config.KERBEROS_REALM:
|
||||
raise falcon.HTTPForbidden("Forbidden",
|
||||
"Cross-realm trust not supported")
|
||||
|
||||
if username.endswith("$") and optional:
|
||||
# Extract machine hostname
|
||||
# TODO: Assert LDAP group membership
|
||||
req.context["machine"] = username[:-1].lower()
|
||||
req.context["user"] = None
|
||||
else:
|
||||
# Attempt to look up real user
|
||||
req.context["user"] = User.objects.get(username)
|
||||
|
||||
logger.debug("Succesfully authenticated user %s for %s from %s",
|
||||
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)
|
||||
|
||||
import ldap
|
||||
|
||||
if not req.auth:
|
||||
raise falcon.HTTPUnauthorized("Unauthorized",
|
||||
"No authentication header provided",
|
||||
("Basic",))
|
||||
|
||||
if not req.auth.startswith("Basic "):
|
||||
raise falcon.HTTPBadRequest("Bad request", "Bad header: %s" % req.auth)
|
||||
|
||||
from base64 import b64decode
|
||||
basic, token = req.auth.split(" ", 1)
|
||||
user, passwd = b64decode(token).decode("ascii").split(":", 1)
|
||||
|
||||
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:
|
||||
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",))
|
||||
|
||||
req.context["ldap_conn"] = conn
|
||||
req.context["user"] = User.objects.get(user)
|
||||
retval = func(resource, req, resp, *args, **kwargs)
|
||||
conn.unbind_s()
|
||||
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 wrapper
|
||||
|
||||
|
||||
def login_required(func):
|
||||
return authenticate()(func)
|
||||
|
||||
def login_optional(func):
|
||||
return authenticate(optional=True)(func)
|
||||
|
||||
def authorize_admin(func):
|
||||
def wrapped(resource, req, resp, *args, **kwargs):
|
||||
if req.context.get("user").is_admin():
|
||||
req.context["admin_authorized"] = True
|
||||
return func(resource, req, resp, *args, **kwargs)
|
||||
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")
|
||||
return wrapped
|
||||
|
||||
def authorize_server(func):
|
||||
"""
|
||||
Make sure the request originator has a certificate with server flags
|
||||
"""
|
||||
from asn1crypto import pem, x509
|
||||
def wrapped(resource, req, resp, *args, **kwargs):
|
||||
buf = req.get_header("X-SSL-CERT")
|
||||
if not buf:
|
||||
logger.info("No TLS certificate presented to access administrative API call")
|
||||
raise falcon.HTTPForbidden("Forbidden", "Machine not authorized to perform the operation")
|
||||
|
||||
header, _, der_bytes = pem.unarmor(buf.replace("\t", "").encode("ascii"))
|
||||
cert = x509.Certificate.load(der_bytes) # TODO: validate serial
|
||||
for extension in cert["tbs_certificate"]["extensions"]:
|
||||
if extension["extn_id"].native == "extended_key_usage":
|
||||
if "server_auth" in extension["extn_value"].native:
|
||||
req.context["machine"] = cert.subject.native["common_name"]
|
||||
return func(resource, req, resp, *args, **kwargs)
|
||||
logger.info("TLS authenticated machine '%s' not authorized to access administrative API", cert.subject.native["common_name"])
|
||||
raise falcon.HTTPForbidden("Forbidden", "Machine not authorized to perform the operation")
|
||||
return wrapped
|
@ -48,7 +48,7 @@ with open(config.AUTHORITY_PRIVATE_KEY_PATH, "rb") as fh:
|
||||
header, _, key_der_bytes = pem.unarmor(key_buf)
|
||||
private_key = asymmetric.load_private_key(key_der_bytes)
|
||||
|
||||
def self_enroll():
|
||||
def self_enroll(skip_notify=False):
|
||||
assert os.getuid() == 0 and os.getgid() == 0, "Can self-enroll only as root"
|
||||
|
||||
from certidude import const
|
||||
@ -87,7 +87,7 @@ def self_enroll():
|
||||
click.echo("Writing request to %s" % path)
|
||||
with open(path, "wb") as fh:
|
||||
fh.write(pem_armor_csr(request)) # Write CSR with certidude permissions
|
||||
authority.sign(common_name, skip_push=True, overwrite=True, profile=config.PROFILES["srv"])
|
||||
authority.sign(common_name, skip_notify=skip_notify, skip_push=True, overwrite=True, profile=config.PROFILES["srv"])
|
||||
sys.exit(0)
|
||||
else:
|
||||
os.waitpid(pid, 0)
|
||||
@ -243,22 +243,10 @@ def revoke(common_name, reason):
|
||||
attach_cert = buf, "application/x-pem-file", common_name + ".crt"
|
||||
mailer.send("certificate-revoked.md",
|
||||
attachments=(attach_cert,),
|
||||
serial_hex="%040x" % cert.serial_number,
|
||||
serial_hex="%x" % cert.serial_number,
|
||||
common_name=common_name)
|
||||
return revoked_path
|
||||
|
||||
def server_flags(cn):
|
||||
if config.USER_ENROLLMENT_ALLOWED and not config.USER_MULTIPLE_CERTIFICATES:
|
||||
# Common name set to username, used for only HTTPS client validation anyway
|
||||
return False
|
||||
if "@" in cn:
|
||||
# username@hostname is user certificate anyway, can't be server
|
||||
return False
|
||||
if "." in cn:
|
||||
# CN is hostname, if contains dot has to be FQDN, hence a server
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def list_requests(directory=config.REQUESTS_DIR):
|
||||
for filename in os.listdir(directory):
|
||||
@ -297,12 +285,16 @@ def list_signed(directory=config.SIGNED_DIR, common_name=None):
|
||||
path, buf, cert, signed, expires = get_signed(basename)
|
||||
yield basename, path, buf, cert, signed, expires
|
||||
|
||||
def list_revoked(directory=config.REVOKED_DIR):
|
||||
for filename in os.listdir(directory):
|
||||
def list_revoked(directory=config.REVOKED_DIR, limit=0):
|
||||
for filename in sorted(os.listdir(directory), reverse=True):
|
||||
if filename.endswith(".pem"):
|
||||
common_name = filename[:-4]
|
||||
path, buf, cert, signed, expired, revoked, reason = get_revoked(common_name)
|
||||
yield cert.subject.native["common_name"], path, buf, cert, signed, expired, revoked, reason
|
||||
if limit:
|
||||
limit -= 1
|
||||
if limit <= 0:
|
||||
return
|
||||
|
||||
|
||||
def list_server_names():
|
||||
@ -324,7 +316,10 @@ def export_crl(pem=True):
|
||||
serial_number = filename[:-4]
|
||||
# TODO: Assert serial against regex
|
||||
revoked_path = os.path.join(config.REVOKED_DIR, filename)
|
||||
reason = getxattr(revoked_path, "user.revocation.reason").decode("ascii") # TODO: dedup
|
||||
try:
|
||||
reason = getxattr(revoked_path, "user.revocation.reason").decode("ascii") # TODO: dedup
|
||||
except IOError: # TODO: make sure it's not required
|
||||
reason = "key_compromise"
|
||||
|
||||
# TODO: Skip expired certificates
|
||||
s = os.stat(revoked_path)
|
||||
@ -404,7 +399,7 @@ def _sign(csr, buf, profile, skip_notify=False, skip_push=False, overwrite=False
|
||||
|
||||
if overwrite:
|
||||
# TODO: is this the best approach?
|
||||
prev_serial_hex = "%040x" % prev.serial_number
|
||||
prev_serial_hex = "%x" % prev.serial_number
|
||||
revoked_path = os.path.join(config.REVOKED_DIR, "%s.pem" % prev_serial_hex)
|
||||
os.rename(cert_path, revoked_path)
|
||||
attachments += [(prev_buf, "application/x-pem-file", "deprecated.crt" if renew else "overwritten.crt")]
|
||||
@ -433,7 +428,7 @@ def _sign(csr, buf, profile, skip_notify=False, skip_push=False, overwrite=False
|
||||
|
||||
os.rename(cert_path + ".part", cert_path)
|
||||
attachments.append((end_entity_cert_buf, "application/x-pem-file", common_name + ".crt"))
|
||||
cert_serial_hex = "%040x" % end_entity_cert.serial_number
|
||||
cert_serial_hex = "%x" % end_entity_cert.serial_number
|
||||
|
||||
# Create symlink
|
||||
link_name = os.path.join(config.SIGNED_BY_SERIAL_DIR, "%040x.pem" % end_entity_cert.serial_number)
|
||||
|
@ -24,7 +24,6 @@ from glob import glob
|
||||
from ipaddress import ip_network
|
||||
from oscrypto import asymmetric
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# http://www.mad-hacking.net/documentation/linux/security/ssl-tls/creating-ca.xml
|
||||
@ -569,6 +568,10 @@ def certidude_enroll(fork, renew, no_wait, kerberos, skip_self):
|
||||
nm_config.set("vpn", "key", key_path)
|
||||
nm_config.set("vpn", "cert", certificate_path)
|
||||
nm_config.set("vpn", "ca", authority_path)
|
||||
nm_config.set("vpn", "tls-cipher", "TLS-%s-WITH-AES-128-GCM-SHA384" % (
|
||||
"ECDHE-ECDSA" if authority_public_key.algorithm == "ec" else "DHE-RSA"))
|
||||
nm_config.set("vpn", "cipher", "AES-128-GCM")
|
||||
nm_config.set("vpn", "auth", "SHA384")
|
||||
nm_config.add_section("ipv4")
|
||||
nm_config.set("ipv4", "method", "auto")
|
||||
nm_config.set("ipv4", "never-default", "true")
|
||||
@ -617,6 +620,11 @@ def certidude_enroll(fork, renew, no_wait, kerberos, skip_self):
|
||||
nm_config.set("vpn", "userkey", key_path)
|
||||
nm_config.set("vpn", "usercert", certificate_path)
|
||||
nm_config.set("vpn", "certificate", authority_path)
|
||||
dhgroup = "ecp384" if authority_public_key.algorithm == "ec" else "modp2048"
|
||||
nm_config.set("vpn", "ike", "aes256-sha384-prfsha384-" + dhgroup)
|
||||
nm_config.set("vpn", "esp", "aes128gcm16-aes128gmac-" + dhgroup)
|
||||
nm_config.set("vpn", "proposal", "yes")
|
||||
|
||||
nm_config.add_section("ipv4")
|
||||
nm_config.set("ipv4", "method", "auto")
|
||||
|
||||
@ -982,13 +990,12 @@ def certidude_setup_openvpn_networkmanager(authority, remote, common_name, **pat
|
||||
@click.option("--organizational-unit", "-ou", default="Certificate Authority")
|
||||
@click.option("--push-server", help="Push server, by default http://%s" % const.FQDN)
|
||||
@click.option("--directory", help="Directory for authority files")
|
||||
@click.option("--server-flags", is_flag=True, help="Add TLS Server and IKE Intermediate extended key usage flags")
|
||||
@click.option("--outbox", default="smtp://smtp.%s" % const.DOMAIN, help="SMTP server, smtp://smtp.%s by default" % const.DOMAIN)
|
||||
@click.option("--skip-packages", is_flag=True, help="Don't attempt to install apt/pip/npm packages")
|
||||
@click.option("--elliptic-curve", "-e", is_flag=True, help="Generate EC instead of RSA keypair")
|
||||
@fqdn_required
|
||||
def certidude_setup_authority(username, kerberos_keytab, nginx_config, organization, organizational_unit, common_name, directory, authority_lifetime, push_server, outbox, server_flags, title, skip_packages, elliptic_curve):
|
||||
assert subprocess.check_output(["/usr/bin/lsb_release", "-cs"]) == b"xenial\n", "Only Ubuntu 16.04 supported at the moment"
|
||||
def certidude_setup_authority(username, kerberos_keytab, nginx_config, organization, organizational_unit, common_name, directory, authority_lifetime, push_server, outbox, title, skip_packages, elliptic_curve):
|
||||
assert subprocess.check_output(["/usr/bin/lsb_release", "-cs"]) in (b"trusty\n", b"xenial\n", b"bionic\n"), "Only Ubuntu 16.04 supported at the moment"
|
||||
assert os.getuid() == 0 and os.getgid() == 0, "Authority can be set up only by root"
|
||||
|
||||
import pwd
|
||||
@ -1047,6 +1054,9 @@ def certidude_setup_authority(username, kerberos_keytab, nginx_config, organizat
|
||||
ca_cert = os.path.join(directory, "ca_cert.pem")
|
||||
sqlite_path = os.path.join(directory, "meta", "db.sqlite")
|
||||
|
||||
# Builder variables
|
||||
dhgroup = "ecp384" if elliptic_curve else "modp2048"
|
||||
|
||||
try:
|
||||
pwd.getpwnam("certidude")
|
||||
click.echo("User 'certidude' already exists")
|
||||
@ -1084,6 +1094,9 @@ def certidude_setup_authority(username, kerberos_keytab, nginx_config, organizat
|
||||
else:
|
||||
click.echo("Warning: /etc/krb5.keytab or /etc/samba/smb.conf not found, Kerberos unconfigured")
|
||||
|
||||
letsencrypt_fullchain = "/etc/letsencrypt/live/%s/fullchain.pem" % common_name
|
||||
letsencrypt_privkey = "/etc/letsencrypt/live/%s/privkey.pem" % common_name
|
||||
letsencrypt = os.path.exists(letsencrypt_fullchain)
|
||||
doc_path = os.path.join(os.path.realpath(os.path.dirname(os.path.dirname(__file__))), "doc")
|
||||
script_dir = os.path.join(os.path.realpath(os.path.dirname(__file__)), "templates", "script")
|
||||
|
||||
@ -1140,9 +1153,9 @@ def certidude_setup_authority(username, kerberos_keytab, nginx_config, organizat
|
||||
os.system("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")
|
||||
|
||||
# Compile nunjucks templates
|
||||
cmd = 'nunjucks-precompile --include ".html$" --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)
|
||||
os.system(cmd)
|
||||
assert os.system(cmd) == 0
|
||||
|
||||
# Assemble bundle.js
|
||||
click.echo("Assembling %s" % bundle_js)
|
||||
@ -1265,8 +1278,17 @@ def certidude_setup_authority(username, kerberos_keytab, nginx_config, organizat
|
||||
else:
|
||||
os.waitpid(bootstrap_pid, 0)
|
||||
from certidude import authority
|
||||
authority.self_enroll()
|
||||
authority.self_enroll(skip_notify=True)
|
||||
assert os.getuid() == 0 and os.getgid() == 0, "Enroll contaminated environment"
|
||||
click.echo("Enabling and starting Certidude backend")
|
||||
os.system("systemctl enable certidude")
|
||||
os.system("systemctl restart certidude")
|
||||
click.echo("Enabling and starting nginx")
|
||||
os.system("systemctl enable nginx")
|
||||
os.system("systemctl start nginx")
|
||||
os.system("systemctl reload nginx")
|
||||
click.echo()
|
||||
|
||||
click.echo("To enable e-mail notifications install Postfix as sattelite system and set mailer address in %s" % const.SERVER_CONFIG_PATH)
|
||||
click.echo()
|
||||
click.echo("Use following commands to inspect the newly created files:")
|
||||
@ -1274,11 +1296,6 @@ def certidude_setup_authority(username, kerberos_keytab, nginx_config, organizat
|
||||
click.echo(" openssl x509 -text -noout -in %s | less" % ca_cert)
|
||||
click.echo(" openssl rsa -check -in %s" % ca_key)
|
||||
click.echo(" openssl verify -CAfile %s %s" % (ca_cert, ca_cert))
|
||||
click.echo()
|
||||
click.echo("To enable and start the service:")
|
||||
click.echo()
|
||||
click.echo(" systemctl enable certidude")
|
||||
click.echo(" systemctl start certidude")
|
||||
return 0
|
||||
|
||||
|
||||
|
@ -23,22 +23,12 @@ def cn_to_dn(common_name, namespace, o=None, ou=None):
|
||||
from asn1crypto.x509 import Name, RelativeDistinguishedName, NameType, DirectoryString, RDNSequence, NameTypeAndValue, UTF8String, DNSName
|
||||
|
||||
rdns = []
|
||||
rdns.append(RelativeDistinguishedName([
|
||||
NameTypeAndValue({
|
||||
'type': NameType.map("common_name"),
|
||||
'value': DirectoryString(
|
||||
name="utf8_string",
|
||||
value=UTF8String(common_name))
|
||||
})
|
||||
]))
|
||||
|
||||
if ou:
|
||||
for dc in reversed(namespace.split(".")):
|
||||
rdns.append(RelativeDistinguishedName([
|
||||
NameTypeAndValue({
|
||||
'type': NameType.map("organizational_unit_name"),
|
||||
'value': DirectoryString(
|
||||
name="utf8_string",
|
||||
value=UTF8String(ou))
|
||||
'type': NameType.map("domain_component"),
|
||||
'value': DNSName(value=dc)
|
||||
})
|
||||
]))
|
||||
|
||||
@ -52,14 +42,25 @@ def cn_to_dn(common_name, namespace, o=None, ou=None):
|
||||
})
|
||||
]))
|
||||
|
||||
for dc in namespace.split("."):
|
||||
if ou:
|
||||
rdns.append(RelativeDistinguishedName([
|
||||
NameTypeAndValue({
|
||||
'type': NameType.map("domain_component"),
|
||||
'value': DNSName(value=dc)
|
||||
'type': NameType.map("organizational_unit_name"),
|
||||
'value': DirectoryString(
|
||||
name="utf8_string",
|
||||
value=UTF8String(ou))
|
||||
})
|
||||
]))
|
||||
|
||||
rdns.append(RelativeDistinguishedName([
|
||||
NameTypeAndValue({
|
||||
'type': NameType.map("common_name"),
|
||||
'value': DirectoryString(
|
||||
name="utf8_string",
|
||||
value=UTF8String(common_name))
|
||||
})
|
||||
]))
|
||||
|
||||
return Name(name='', value=RDNSequence(rdns))
|
||||
|
||||
def selinux_fixup(path):
|
||||
|
@ -27,7 +27,7 @@ LDAP_BASE = cp.get("accounts", "ldap base")
|
||||
USER_SUBNETS = set([ipaddress.ip_network(j) for j in
|
||||
cp.get("authorization", "user subnets").split(" ") if j])
|
||||
ADMIN_SUBNETS = set([ipaddress.ip_network(j) for j in
|
||||
cp.get("authorization", "admin subnets").split(" ") if j]).union(USER_SUBNETS)
|
||||
cp.get("authorization", "admin subnets").split(" ") if j])
|
||||
AUTOSIGN_SUBNETS = set([ipaddress.ip_network(j) for j in
|
||||
cp.get("authorization", "autosign subnets").split(" ") if j])
|
||||
REQUEST_SUBNETS = set([ipaddress.ip_network(j) for j in
|
||||
|
@ -42,7 +42,7 @@ def csrf_protection(func):
|
||||
|
||||
class MyEncoder(json.JSONEncoder):
|
||||
def default(self, obj):
|
||||
from certidude.auth import User
|
||||
from certidude.user import User
|
||||
if isinstance(obj, ipaddress._IPAddressBase):
|
||||
return str(obj)
|
||||
if isinstance(obj, set):
|
||||
|
@ -19,40 +19,23 @@
|
||||
|
||||
<div class="collapse navbar-collapse" id="navbarsExampleDefault">
|
||||
<ul class="navbar-nav mr-auto">
|
||||
<li class="nav-item active">
|
||||
<a class="nav-link" href="#">Dashboard <span class="sr-only">(current)</span></a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link" href="#">Settings</a>
|
||||
<a class="nav-link disabled dashboard" href="#">Dashboard</a>
|
||||
</li>
|
||||
<li class="nav-item">
|
||||
<a class="nav-link disabled log" href="#">Log</a>
|
||||
</li>
|
||||
</ul>
|
||||
<form class="form-inline my-2 my-lg-0">
|
||||
<input class="form-control mr-sm-2" type="search" placeholder="🔍">
|
||||
<input id="search" class="form-control mr-sm-2" type="search" placeholder="🔍">
|
||||
</form>
|
||||
</div>
|
||||
</nav>
|
||||
<div id="view-dashboard" class="container-fluid">
|
||||
<div id="view">
|
||||
<div class="loader-container">
|
||||
<div class="loader"></div>
|
||||
<p>Loading certificate authority...</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="view-log" class="container-fluid" style="margin: 5em 0 0 0; display: none;">
|
||||
<h1>Log</h1>
|
||||
<div class="btn-group" data-toggle="buttons">
|
||||
<label class="btn btn-primary active"><input id="log-level-critical" type="checkbox" autocomplete="off" checked> Critical</label>
|
||||
<label class="btn btn-primary active"><input id="log-level-errors" type="checkbox" autocomplete="off" checked> Errors</label>
|
||||
<label class="btn btn-primary active"><input id="log-level-warnings" type="checkbox" autocomplete="off" checked> Warnings</label>
|
||||
<label class="btn btn-primary active"><input id="log-level-info" type="checkbox" autocomplete="off" checked> Info</label>
|
||||
<label class="btn btn-primary"><input id="log-level-debug" type="checkbox" autocomplete="off"> Debug</label>
|
||||
<div id="view-dashboard" class="container-fluid" style="margin: 5em 0 0 0;">
|
||||
<div class="loader-container">
|
||||
<div class="loader"></div>
|
||||
<p>Loading certificate authority...</p>
|
||||
</div>
|
||||
<ul id="log-entries" class="list-group">
|
||||
</ul>
|
||||
</div>
|
||||
<footer class="footer">
|
||||
<div class="container">
|
||||
|
@ -25,9 +25,24 @@ function onHashChanged() {
|
||||
query[decodeURIComponent(b[0])] = decodeURIComponent(b[1] || '');
|
||||
}
|
||||
|
||||
if (query.columns) { query.columns = parseInt(query.columns) };
|
||||
|
||||
if (query.columns < 2 || query.columns > 4) {
|
||||
query.columns = 2;
|
||||
}
|
||||
|
||||
console.info("Hash is now:", query);
|
||||
|
||||
loadAuthority();
|
||||
if (window.location.protocol != "https:") {
|
||||
$.get("/api/certificate/", function(blob) {
|
||||
$("#view-dashboard").html(env.render('views/insecure.html', { window: window,
|
||||
authority_name: window.location.hostname,
|
||||
session: { authority: { certificate: { blob: blob }}}
|
||||
}));
|
||||
});
|
||||
} else {
|
||||
loadAuthority(query);
|
||||
}
|
||||
}
|
||||
|
||||
function onTagClicked(tag) {
|
||||
@ -277,12 +292,9 @@ function onSendToken() {
|
||||
alert(e);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
function loadAuthority() {
|
||||
function loadAuthority(query) {
|
||||
console.info("Loading CA, to debug: curl " + window.location.href + " --negotiate -u : -H 'Accept: application/json'");
|
||||
$.ajax({
|
||||
method: "GET",
|
||||
@ -294,7 +306,7 @@ function loadAuthority() {
|
||||
} else {
|
||||
var msg = { title: "Error " + response.status, description: response.statusText }
|
||||
}
|
||||
$("#view").html(env.render('views/error.html', { message: msg }));
|
||||
$("#view-dashboard").html(env.render('views/error.html', { message: msg }));
|
||||
},
|
||||
success: function(session, status, xhr) {
|
||||
window.session = session;
|
||||
@ -302,10 +314,18 @@ function loadAuthority() {
|
||||
console.info("Loaded:", session);
|
||||
$("#login").hide();
|
||||
|
||||
if (!query.columns) {
|
||||
query.columns = 2;
|
||||
}
|
||||
|
||||
/**
|
||||
* Render authority views
|
||||
**/
|
||||
$("#view").html(env.render('views/authority.html', { session: session, window: window,
|
||||
$("#view-dashboard").html(env.render('views/authority.html', {
|
||||
session: session,
|
||||
window: window,
|
||||
columns: query.columns,
|
||||
column_width: 12 / query.columns,
|
||||
authority_name: window.location.hostname }));
|
||||
$("time").timeago();
|
||||
if (session.authority) {
|
||||
@ -320,7 +340,7 @@ function loadAuthority() {
|
||||
|
||||
console.info("Opening EventSource from:", session.authority.events);
|
||||
|
||||
var source = new EventSource(session.authority.events);
|
||||
window.source = new EventSource(session.authority.events);
|
||||
|
||||
source.onmessage = function(event) {
|
||||
console.log("Received server-sent event:", event);
|
||||
@ -337,15 +357,6 @@ function loadAuthority() {
|
||||
source.addEventListener("server-started", onServerStarted);
|
||||
source.addEventListener("server-stopped", onServerStopped);
|
||||
|
||||
console.info("Swtiching to requests section");
|
||||
$("section").hide();
|
||||
$("section#requests").show();
|
||||
$("#section-revoked").show();
|
||||
$("#section-signed").show();
|
||||
$("#section-requests").show();
|
||||
$("#section-token").show();
|
||||
|
||||
|
||||
}
|
||||
|
||||
$("nav#menu li").click(function(e) {
|
||||
@ -383,7 +394,7 @@ function loadAuthority() {
|
||||
$(window).on("search", function() {
|
||||
var q = $("#search").val();
|
||||
$(".filterable").each(function(i, e) {
|
||||
if ($(e).attr("data-cn").toLowerCase().indexOf(q) >= 0) {
|
||||
if ($(e).attr("data-keywords").toLowerCase().indexOf(q) >= 0) {
|
||||
$(e).show();
|
||||
} else {
|
||||
$(e).hide();
|
||||
@ -432,32 +443,53 @@ function loadAuthority() {
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
$("nav .nav-link.dashboard").removeClass("disabled").click(function() {
|
||||
$("#column-requests").show();
|
||||
$("#column-signed").show();
|
||||
$("#column-revoked").show();
|
||||
$("#column-log").hide();
|
||||
});
|
||||
|
||||
/**
|
||||
* Fetch log entries
|
||||
*/
|
||||
if (session.features.logging) {
|
||||
$("nav .nav-link.log").removeClass("disabled").click(function() {
|
||||
$("#view-dashboard").hide();
|
||||
$("#view-log").show();
|
||||
$.ajax({
|
||||
method: "GET",
|
||||
url: "/api/log/",
|
||||
dataType: "json",
|
||||
success: function(entries, status, xhr) {
|
||||
console.info("Got", entries.length, "log entries");
|
||||
console.info("j=", entries.length-1);
|
||||
for (var j = entries.length-1; j--; ) {
|
||||
onLogEntry(entries[j]);
|
||||
};
|
||||
source.addEventListener("log-entry", onLogEntry);
|
||||
}
|
||||
if (query.columns == 4) {
|
||||
loadLog();
|
||||
} else {
|
||||
$("nav .nav-link.log").removeClass("disabled").click(function() {
|
||||
$("#column-requests").show();
|
||||
$("#column-signed").show();
|
||||
$("#column-revoked").show();
|
||||
$("#column-log").hide();
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function loadLog() {
|
||||
if (window.log_initialized) return;
|
||||
window.log_initialized = true;
|
||||
$.ajax({
|
||||
method: "GET",
|
||||
url: "/api/log/",
|
||||
dataType: "json",
|
||||
success: function(entries, status, xhr) {
|
||||
console.info("Got", entries.length, "log entries");
|
||||
console.info("j=", entries.length-1);
|
||||
for (var j = entries.length-1; j--; ) {
|
||||
onLogEntry(entries[j]);
|
||||
};
|
||||
source.addEventListener("log-entry", onLogEntry);
|
||||
$("#column-log .loader-container").hide();
|
||||
$("#column-log .content").show();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function datetimeFilter(s) {
|
||||
return new Date(s);
|
||||
}
|
||||
|
19
certidude/static/snippets/gateway-updown.sh
Normal file
19
certidude/static/snippets/gateway-updown.sh
Normal file
@ -0,0 +1,19 @@
|
||||
# Create VPN gateway up/down script for reporting client IP addresses to CA
|
||||
cat <<\EOF > /etc/certidude/authority/{{ authority_name }}/updown
|
||||
#!/bin/sh
|
||||
|
||||
CURL="curl -m 3 -f --key /etc/certidude/authority/{{ authority_name }}/host_key.pem --cert /etc/certidude/authority/{{ authority_name }}/host_cert.pem --cacert /etc/certidude/authority/{{ authority_name }}/ca_cert.pem https://{{ authority_name }}:8443/api/lease/"
|
||||
|
||||
case $PLUTO_VERB in
|
||||
up-client) $CURL --data-urlencode "outer_address=$PLUTO_PEER" --data-urlencode "inner_address=$PLUTO_PEER_SOURCEIP" --data-urlencode "client=$PLUTO_PEER_ID" ;;
|
||||
*) ;;
|
||||
esac
|
||||
|
||||
case $script_type in
|
||||
client-connect) $CURL --data-urlencode client=$X509_0_CN --data-urlencode serial=$tls_serial_0 --data-urlencode outer_address=$untrusted_ip --data-urlencode inner_address=$ifconfig_pool_remote_ip ;;
|
||||
*) ;;
|
||||
esac
|
||||
EOF
|
||||
|
||||
chmod +x /etc/certidude/authority/{{ authority_name }}/updown
|
||||
|
27
certidude/static/snippets/openvpn-client.sh
Normal file
27
certidude/static/snippets/openvpn-client.sh
Normal file
@ -0,0 +1,27 @@
|
||||
# Install packages on Ubuntu & Fedora
|
||||
which apt && apt install openvpn
|
||||
which dnf && dnf install openvpn
|
||||
|
||||
cat > /etc/openvpn/{{ authority_name }}.conf << EOF
|
||||
client
|
||||
nobind
|
||||
{% for router in session.service.routers %}
|
||||
remote {{ router }} 1194 udp
|
||||
remote {{ router }} 443 tcp-client
|
||||
{% endfor %}
|
||||
tls-version-min 1.2
|
||||
tls-cipher TLS-{% if session.authority.certificate.algorithm == "ec" %}ECDHE-ECDSA{% else %}DHE-RSA{% endif %}-WITH-AES-128-GCM-SHA384
|
||||
cipher AES-128-GCM
|
||||
auth SHA384
|
||||
mute-replay-warnings
|
||||
reneg-sec 0
|
||||
remote-cert-tls server
|
||||
dev tun
|
||||
persist-tun
|
||||
persist-key
|
||||
ca /etc/certidude/authority/{{ authority_name }}/ca_cert.pem
|
||||
key /etc/certidude/authority/{{ authority_name }}/host_key.pem
|
||||
cert /etc/certidude/authority/{{ authority_name }}/host_cert.pem
|
||||
EOF
|
||||
|
||||
systemctl restart openvpn
|
89
certidude/static/snippets/openwrt-openvpn.sh
Normal file
89
certidude/static/snippets/openwrt-openvpn.sh
Normal file
@ -0,0 +1,89 @@
|
||||
opkg update
|
||||
opkg install curl openssl-util openvpn-openssl
|
||||
|
||||
{% if session.authority.certificate.algorithm != "ec" %}
|
||||
# Generate Diffie-Hellman parameters file for OpenVPN
|
||||
test -e /etc/certidude/dh.pem \
|
||||
|| openssl dhparam 2048 -out /etc/certidude/dh.pem
|
||||
{% endif %}
|
||||
# Create interface definition for tunnel
|
||||
uci set network.vpn=interface
|
||||
uci set network.vpn.name='vpn'
|
||||
uci set network.vpn.ifname=tun_s2c_udp tun_s2c_tcp
|
||||
uci set network.vpn.proto='none'
|
||||
|
||||
# Create zone definition for VPN interface
|
||||
uci set firewall.vpn=zone
|
||||
uci set firewall.vpn.name='vpn'
|
||||
uci set firewall.vpn.input='ACCEPT'
|
||||
uci set firewall.vpn.forward='ACCEPT'
|
||||
uci set firewall.vpn.output='ACCEPT'
|
||||
uci set firewall.vpn.network='vpn'
|
||||
|
||||
# Allow UDP 1194 on WAN interface
|
||||
uci set firewall.openvpn=rule
|
||||
uci set firewall.openvpn.name='Allow OpenVPN'
|
||||
uci set firewall.openvpn.src='wan'
|
||||
uci set firewall.openvpn.dest_port=1194
|
||||
uci set firewall.openvpn.proto='udp'
|
||||
uci set firewall.openvpn.target='ACCEPT'
|
||||
|
||||
# Allow TCP 443 on WAN interface
|
||||
uci set firewall.openvpn=rule
|
||||
uci set firewall.openvpn.name='Allow OpenVPN over TCP'
|
||||
uci set firewall.openvpn.src='wan'
|
||||
uci set firewall.openvpn.dest_port=443
|
||||
uci set firewall.openvpn.proto='tcp'
|
||||
uci set firewall.openvpn.target='ACCEPT'
|
||||
|
||||
# Forward traffic from VPN to LAN
|
||||
uci set firewall.c2s=forwarding
|
||||
uci set firewall.c2s.src='vpn'
|
||||
uci set firewall.c2s.dest='lan'
|
||||
|
||||
# Permit DNS queries from VPN
|
||||
uci set dhcp.@dnsmasq[0].localservice='0'
|
||||
|
||||
touch /etc/config/openvpn
|
||||
|
||||
# Configure OpenVPN over TCP
|
||||
uci set openvpn.s2c_tcp=openvpn
|
||||
uci set openvpn.s2c_tcp.local=$(uci get network.wan.ipaddr)
|
||||
uci set openvpn.s2c_tcp.server='10.179.43.0 255.255.255.128'
|
||||
uci set openvpn.s2c_tcp.proto='tcp-server'
|
||||
uci set openvpn.s2c_tcp.port='443'
|
||||
uci set openvpn.s2c_tcp.dev=tun_s2c_tcp
|
||||
|
||||
# Configure OpenVPN over UDP
|
||||
uci set openvpn.s2c_udp=openvpn
|
||||
uci set openvpn.s2c_udp.local=$(uci get network.wan.ipaddr)
|
||||
uci set openvpn.s2c_udp.server='10.179.43.128 255.255.255.128'
|
||||
uci set openvpn.s2c_tcp.dev=tun_s2c_udp
|
||||
|
||||
for section in s2c_tcp s2c_udp; do
|
||||
|
||||
# Common paths
|
||||
uci set openvpn.$section.script_security=2
|
||||
uci set openvpn.$section.client_connect='/etc/certidude/updown'
|
||||
uci set openvpn.$section.key='/etc/certidude/authority/{{ authority_name }}/host_key.pem'
|
||||
uci set openvpn.$section.cert='/etc/certidude/authority/{{ authority_name }}/host_cert.pem'
|
||||
uci set openvpn.$section.ca='/etc/certidude/authority/{{ authority_name }}/ca_cert.pem'
|
||||
{% if session.authority.certificate.algorithm != "ec" %}uci set openvpn.$section.dh='/etc/certidude/dh.pem'{% endif %}
|
||||
uci set openvpn.$section.enabled=1
|
||||
|
||||
# DNS and routes
|
||||
uci add_list openvpn.$section.push="route-metric 1000"
|
||||
uci add_list openvpn.$section.push="route $(uci get network.lan.ipaddr) $(uci get network.lan.netmask)"
|
||||
uci add_list openvpn.$section.push="dhcp-option DNS $(uci get network.lan.ipaddr)"
|
||||
uci add_list openvpn.$section.push="dhcp-option DOMAIN $(uci get dhcp.@dnsmasq[0].domain)"
|
||||
|
||||
# Security hardening
|
||||
uci set openvpn.$section.tls_version_min='1.2'
|
||||
uci set openvpn.$section.tls_cipher='TLS-{% if session.authority.certificate.algorithm == "ec" %}ECDHE-ECDSA{% else %}DHE-RSA{% endif %}-WITH-AES-128-GCM-SHA384'
|
||||
uci set openvpn.$section.cipher='AES-128-GCM'
|
||||
uci set openvpn.$section.auth='SHA384'
|
||||
|
||||
done
|
||||
|
||||
/etc/init.d/openvpn restart
|
||||
/etc/init.d/firewall restart
|
7
certidude/static/snippets/renew.sh
Normal file
7
certidude/static/snippets/renew.sh
Normal file
@ -0,0 +1,7 @@
|
||||
curl -f -L -H "Content-type: application/pkcs10" \
|
||||
--cacert /etc/certidude/authority/{{ authority_name }}/ca_cert.pem \
|
||||
--key /etc/certidude/authority/{{ authority_name }}/host_key.pem \
|
||||
--cert /etc/certidude/authority/{{ authority_name }}/host_cert.pem \
|
||||
--data-binary @/etc/certidude/authority/{{ authority_name }}/host_req.pem \
|
||||
-o /etc/certidude/authority/{{ authority_name }}/host_cert.pem \
|
||||
'https://{{ authority_name }}:8443/api/request/?wait=yes'
|
15
certidude/static/snippets/request-client.sh
Normal file
15
certidude/static/snippets/request-client.sh
Normal file
@ -0,0 +1,15 @@
|
||||
test -e /sbin/uci && NAME=$(uci get system.@system[0].hostname)
|
||||
test -e /bin/hostname && NAME=$(hostname)
|
||||
test -n "$NAME" || NAME=$(cat /proc/sys/kernel/hostname)
|
||||
|
||||
{% include "snippets/update-trust.sh" %}
|
||||
|
||||
{% include "snippets/request-common.sh" %}
|
||||
|
||||
curl -f -L -H "Content-type: application/pkcs10" \
|
||||
--data-binary @/etc/certidude/authority/{{ authority_name }}/host_req.pem \
|
||||
-o /etc/certidude/authority/{{ authority_name }}/host_cert.pem \
|
||||
'http://{{ authority_name }}/api/request/?wait=yes&autosign=yes'
|
||||
|
||||
|
||||
|
14
certidude/static/snippets/request-common.sh
Normal file
14
certidude/static/snippets/request-common.sh
Normal file
@ -0,0 +1,14 @@
|
||||
echo {{ session.authority.certificate.md5sum }} /etc/certidude/authority/{{ authority_name }}/ca_cert.pem | md5sum -c \
|
||||
|| rm -fv /etc/certidude/authority/{{ authority_name }}/*.pem
|
||||
{% include "snippets/store-authority.sh" %}
|
||||
test -e /etc/certidude/authority/{{ authority_name }}/host_key.pem \
|
||||
|| {% if session.authority.certificate.algorithm == "ec" %}openssl ecparam -name secp384r1 -genkey -noout \
|
||||
-out /etc/certidude/authority/{{ authority_name }}/host_key.pem{% else %}openssl genrsa \
|
||||
-out /etc/certidude/authority/{{ authority_name }}/host_key.pem 2048{% endif %}
|
||||
test -e /etc/certidude/authority/{{ authority_name }}/host_req.pem \
|
||||
|| openssl req -new -sha384 -subj "/CN=$NAME" \
|
||||
-key /etc/certidude/authority/{{ authority_name }}/host_key.pem \
|
||||
-out /etc/certidude/authority/{{ authority_name }}/host_req.pem
|
||||
echo "If CSR submission fails, you can copy paste it to Certidude:"
|
||||
cat /etc/certidude/authority/{{ authority_name }}/host_req.pem
|
||||
|
13
certidude/static/snippets/request-server.sh
Normal file
13
certidude/static/snippets/request-server.sh
Normal file
@ -0,0 +1,13 @@
|
||||
test -e /sbin/uci && NAME=$(nslookup $(uci get network.wan.ipaddr) | grep "name =" | head -n1 | cut -d "=" -f 2 | xargs)
|
||||
test -e /bin/hostname && NAME=$(hostname -f)
|
||||
test -n "$NAME" || NAME=$(cat /proc/sys/kernel/hostname)
|
||||
|
||||
{% include "snippets/update-trust.sh" %}
|
||||
|
||||
{% include "snippets/request-common.sh" %}
|
||||
|
||||
curl -f -L -H "Content-type: application/pkcs10" \
|
||||
--cacert /etc/certidude/authority/{{ authority_name }}/ca_cert.pem \
|
||||
--data-binary @/etc/certidude/authority/{{ authority_name }}/host_req.pem \
|
||||
-o /etc/certidude/authority/{{ authority_name }}/host_cert.pem \
|
||||
'https://{{ authority_name }}:8443/api/request/?wait=yes'
|
5
certidude/static/snippets/store-authority.sh
Normal file
5
certidude/static/snippets/store-authority.sh
Normal file
@ -0,0 +1,5 @@
|
||||
mkdir -p /etc/certidude/authority/{{ authority_name }}/
|
||||
test -e /etc/certidude/authority/{{ authority_name }}/ca_cert.pem \
|
||||
|| cat << EOF > /etc/certidude/authority/{{ authority_name }}/ca_cert.pem
|
||||
{{ session.authority.certificate.blob }}EOF
|
||||
|
29
certidude/static/snippets/strongswan-client.sh
Normal file
29
certidude/static/snippets/strongswan-client.sh
Normal file
@ -0,0 +1,29 @@
|
||||
cat > /etc/ipsec.conf << EOF
|
||||
|
||||
ca {{ authority_name }}
|
||||
auto=add
|
||||
cacert=/etc/certidude/authority/{{ authority_name }}/ca_cert.pem
|
||||
{% if session.features.crl %} crluri=http://{{ authority_name }}/api/revoked/{% endif %}
|
||||
{% if session.features.ocsp %} ocspuri=http://{{ authority_name }}/api/ocsp/{% endif %}
|
||||
|
||||
conn client-to-site
|
||||
auto=start
|
||||
right={{ session.service.routers[0] }}
|
||||
rightsubnet=0.0.0.0/0
|
||||
rightca="{{ session.authority.certificate.distinguished_name }}"
|
||||
left=%defaultroute
|
||||
leftcert=/etc/certidude/authority/{{ authority_name }}/host_cert.pem
|
||||
leftsourceip=%config
|
||||
leftca="{{ session.authority.certificate.distinguished_name }}"
|
||||
keyexchange=ikev2
|
||||
keyingtries=%forever
|
||||
dpdaction=restart
|
||||
closeaction=restart
|
||||
ike=aes256-sha384-{% if session.authority.certificate.algorithm == "ec" %}ecp384{% else %}modp2048{% endif %}!
|
||||
esp=aes128gcm16-aes128gmac-{% if session.authority.certificate.algorithm == "ec" %}ecp384{% else %}modp2048{% endif %}!
|
||||
|
||||
EOF
|
||||
|
||||
echo ": {% if session.authority.certificate.algorithm == "ec" %}ECDSA{% else %}RSA{% endif %} {{ authority_name }}.pem" > /etc/ipsec.secrets
|
||||
|
||||
ipsec restart
|
16
certidude/static/snippets/strongswan-patching.sh
Normal file
16
certidude/static/snippets/strongswan-patching.sh
Normal file
@ -0,0 +1,16 @@
|
||||
# Install packages on Ubuntu & Fedora, patch Fedora paths
|
||||
which apt && apt install strongswan
|
||||
which dnf && dnf install strongswan
|
||||
test -e /etc/strongswan && test -e /etc/ipsec.conf || ln -s strongswan/ipsec.conf /etc/ipsec.conf
|
||||
test -e /etc/strongswan && test -e /etc/ipsec.d || ln -s strongswan/ipsec.d /etc/ipsec.d
|
||||
test -e /etc/strongswan && test -e /etc/ipsec.secrets || ln -s strongswan/ipsec.secrets /etc/ipsec.secrets
|
||||
|
||||
# Set SELinux context
|
||||
chcon --type=home_cert_t /etc/certidude/authority/{{ authority_name }}/ca_cert.pem /etc/ipsec.d/cacerts/{{ authority_name }}.pem
|
||||
chcon --type=home_cert_t /etc/certidude/authority/{{ authority_name }}/host_cert.pem /etc/ipsec.d/certs/{{ authority_name }}.pem
|
||||
chcon --type=home_cert_t /etc/certidude/authority/{{ authority_name }}/host_key.pem /etc/ipsec.d/private/{{ authority_name }}.pem
|
||||
|
||||
# Patch AppArmor
|
||||
cat << EOF > /etc/apparmor.d/local/usr.lib.ipsec.charon
|
||||
/etc/certidude/authority/**
|
||||
EOF
|
41
certidude/static/snippets/strongswan-server.sh
Normal file
41
certidude/static/snippets/strongswan-server.sh
Normal file
@ -0,0 +1,41 @@
|
||||
# Generate StrongSwan config
|
||||
cat > /etc/ipsec.conf << EOF
|
||||
config setup
|
||||
strictcrlpolicy=yes
|
||||
uniqueids=yes
|
||||
|
||||
ca {{ authority_name }}
|
||||
auto=add
|
||||
cacert=/etc/certidude/authority/{{ authority_name }}/ca_cert.pem
|
||||
{% if session.features.crl %} crluri=http://{{ authority_name }}/api/revoked/{% endif %}
|
||||
{% if session.features.ocsp %} ocspuri=http://{{ authority_name }}/api/ocsp/{% endif %}
|
||||
|
||||
conn default-{{ authority_name }}
|
||||
ike=aes256-sha384-{% if session.authority.certificate.algorithm == "ec" %}ecp384{% else %}modp2048{% endif %}!
|
||||
esp=aes128gcm16-aes128gmac-{% if session.authority.certificate.algorithm == "ec" %}ecp384{% else %}modp2048{% endif %}!
|
||||
left=$(uci get network.wan.ipaddr) # Bind to this IP address
|
||||
leftid={{ session.service.routers | first }}
|
||||
leftupdown=/etc/certidude/authority/{{ authority_name }}/updown
|
||||
leftcert=/etc/certidude/authority/{{ authority_name }}/host_cert.pem
|
||||
leftsubnet=$(uci get network.lan.ipaddr | cut -d . -f 1-3).0/24 # Subnets pushed to roadwarriors
|
||||
leftdns=$(uci get network.lan.ipaddr) # IP of DNS server advertised to roadwarriors
|
||||
leftca="{{ session.authority.certificate.distinguished_name }}"
|
||||
rightca="{{ session.authority.certificate.distinguished_name }}"
|
||||
rightsourceip=172.21.0.0/24 # Roadwarrior virtual IP pool
|
||||
dpddelay=0
|
||||
dpdaction=clear
|
||||
|
||||
conn site-to-clients
|
||||
auto=add
|
||||
also=default-{{ authority_name }}
|
||||
|
||||
conn site-to-client1
|
||||
auto=ignore
|
||||
also=default-{{ authority_name }}
|
||||
rightid="CN=*, OU=IP Camera, O=*, DC=*, DC=*, DC=*"
|
||||
rightsourceip=172.21.0.1
|
||||
|
||||
EOF
|
||||
|
||||
echo ": {% if session.authority.certificate.algorithm == "ec" %}ECDSA{% else %}RSA{% endif %} /etc/certidude/authority/{{ authority_name }}/host_key.pem" > /etc/ipsec.secrets
|
||||
|
7
certidude/static/snippets/update-trust.sh
Normal file
7
certidude/static/snippets/update-trust.sh
Normal file
@ -0,0 +1,7 @@
|
||||
test -e /etc/pki/ca-trust/source/anchors \
|
||||
&& ln -s /etc/certidude/authority/{{ authority_name }}/ca_cert.pem /etc/pki/ca-trust/source/anchors/{{ authority_name }} \
|
||||
&& update-ca-trust
|
||||
test -e /usr/local/share/ca-certificates/ \
|
||||
&& ln -s /etc/certidude/authority/{{ authority_name }}/ca_cert.pem /usr/local/share/ca-certificates/{{ authority_name }}.crt \
|
||||
&& update-ca-certificates
|
||||
|
61
certidude/static/snippets/windows.ps1
Normal file
61
certidude/static/snippets/windows.ps1
Normal file
@ -0,0 +1,61 @@
|
||||
# Install CA certificate
|
||||
@"
|
||||
{{ session.authority.certificate.blob }}
|
||||
"@ | Out-File ca_cert.pem
|
||||
{% if session.authority.certificate.algorithm == "ec" %}
|
||||
Import-Certificate -FilePath ca_cert.pem -CertStoreLocation Cert:\LocalMachine\Root
|
||||
{% else %}
|
||||
C:\Windows\system32\certutil.exe -addstore Root ca_cert.pem
|
||||
{% endif %}
|
||||
|
||||
# Generate keypair and submit CSR
|
||||
$hostname = $env:computername.ToLower()
|
||||
@"
|
||||
[NewRequest]
|
||||
Subject = "CN=$hostname"
|
||||
Exportable = FALSE
|
||||
KeySpec = 1
|
||||
KeyUsage = 0xA0
|
||||
MachineKeySet = True
|
||||
ProviderType = 12
|
||||
RequestType = PKCS10
|
||||
{% if session.authority.certificate.algorithm == "ec" %}ProviderName = "Microsoft Software Key Storage Provider"
|
||||
KeyAlgorithm = ECDSA_P384
|
||||
{% else %}ProviderName = "Microsoft RSA SChannel Cryptographic Provider"
|
||||
KeyLength = 2048
|
||||
{% endif %}"@ | Out-File req.inf
|
||||
C:\Windows\system32\certreq.exe -new -f -q req.inf host_csr.pem
|
||||
Invoke-WebRequest -TimeoutSec 900 -Uri 'https://{{ authority_name }}:8443/api/request/?wait=yes&autosign=yes' -InFile host_csr.pem -ContentType application/pkcs10 -Method POST -MaximumRedirection 3 -OutFile host_cert.pem
|
||||
|
||||
# Import certificate
|
||||
{% if session.authority.certificate.algorithm == "ec" %}Import-Certificate -FilePath host_cert.pem -CertStoreLocation Cert:\LocalMachine\My
|
||||
{% else %}C:\Windows\system32\certutil.exe -addstore My host_cert.pem
|
||||
{% endif %}
|
||||
# Set up IPSec VPN tunnel
|
||||
Remove-VpnConnection -AllUserConnection -Force k-space
|
||||
Add-VpnConnection `
|
||||
-Name k-space `
|
||||
-ServerAddress guests.k-space.ee `
|
||||
-AuthenticationMethod MachineCertificate `
|
||||
-SplitTunneling `
|
||||
-TunnelType ikev2 `
|
||||
-PassThru -AllUserConnection
|
||||
|
||||
# Security hardening
|
||||
Set-VpnConnectionIPsecConfiguration `
|
||||
-ConnectionName k-space `
|
||||
-AuthenticationTransformConstants GCMAES128 `
|
||||
-CipherTransformConstants GCMAES128 `
|
||||
-EncryptionMethod AES256 `
|
||||
-IntegrityCheckMethod SHA384 `
|
||||
-DHGroup {% if session.authority.certificate.algorithm == "ec" %}ECP384{% else %}Group14{% endif %} `
|
||||
-PfsGroup {% if session.authority.certificate.algorithm == "ec" %}ECP384{% else %}PFS2048{% endif %} `
|
||||
-PassThru -AllUserConnection -Force
|
||||
{#
|
||||
AuthenticationTransformConstants - ESP integrity algorithm, one of: None MD596 SHA196 SHA256128 GCMAES128 GCMAES192 GCMAES256
|
||||
CipherTransformConstants - ESP symmetric cipher, one of: DES DES3 AES128 AES192 AES256 GCMAES128 GCMAES192 GCMAES256
|
||||
EncryptionMethod - IKE symmetric cipher, one of: DES DES3 AES128 AES192 AES256
|
||||
IntegrityCheckMethod - IKE hash algorithm, one of: MD5 SHA196 SHA256 SHA384
|
||||
DHGroup = IKE key exchange, one of: None Group1 Group2 Group14 ECP256 ECP384 Group24
|
||||
PfsGroup = ESP key exchange, one of: None PFS1 PFS2 PFS2048 ECP256 ECP384 PFSMM PFS24
|
||||
#}
|
@ -1,625 +1,162 @@
|
||||
<div class="modal fade" id="request_submission_modal" role="dialog">
|
||||
<div class="modal-dialog modal-lg">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<button type="button" class="close" data-dismiss="modal">×</button>
|
||||
<h4 class="modal-title">Request submission</h4>
|
||||
</div>
|
||||
<form action="/api/request/" method="post">
|
||||
<div class="modal-body">
|
||||
{% if "ikev2" in session.service.protocols %}
|
||||
<div class="modal fade" id="request_submission_modal" role="dialog">
|
||||
<div class="modal-dialog modal-lg">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<button type="button" class="close" data-dismiss="modal">×</button>
|
||||
<h4 class="modal-title">Request submission</h4>
|
||||
</div>
|
||||
<form action="/api/request/" method="post">
|
||||
<div class="modal-body">
|
||||
{% if "ikev2" in session.service.protocols %}
|
||||
<h5>Windows {% if session.authority.certificate.algorithm == "ec" %}10{% else %}7 and up{% endif %}</h5>
|
||||
|
||||
<p>On Windows execute following PowerShell script</p>
|
||||
<div class="highlight"><pre class="code"><code>{% include "snippets/windows.ps1" %}</code></pre></div>
|
||||
{% endif %}
|
||||
|
||||
<div class="highlight">
|
||||
<pre class="code"><code># Install CA certificate
|
||||
@"
|
||||
{{ session.authority.certificate.blob }}
|
||||
"@ | Out-File ca_cert.pem
|
||||
{% if session.authority.certificate.algorithm == "ec" %}
|
||||
Import-Certificate -FilePath ca_cert.pem -CertStoreLocation Cert:\LocalMachine\Root
|
||||
{% else %}
|
||||
C:\Windows\system32\certutil.exe -addstore Root ca_cert.pem
|
||||
{% endif %}
|
||||
<h5>UNIX & UNIX-like</h5>
|
||||
<p>For client certificates generate key pair and submit the signing request with common name set to short hostname:</p>
|
||||
<div class="highlight">
|
||||
|
||||
# Generate keypair and submit CSR
|
||||
$hostname = $env:computername.ToLower()
|
||||
@"
|
||||
[NewRequest]
|
||||
Subject = "CN=$hostname"
|
||||
Exportable = FALSE
|
||||
KeySpec = 1
|
||||
KeyUsage = 0xA0
|
||||
MachineKeySet = True
|
||||
ProviderType = 12
|
||||
RequestType = PKCS10
|
||||
{% if session.authority.certificate.algorithm == "ec" %}ProviderName = "Microsoft Software Key Storage Provider"
|
||||
KeyAlgorithm = ECDSA_P384
|
||||
{% else %}ProviderName = "Microsoft RSA SChannel Cryptographic Provider"
|
||||
KeyLength = 2048
|
||||
{% endif %}"@ | Out-File req.inf
|
||||
C:\Windows\system32\certreq.exe -new -f -q req.inf host_csr.pem
|
||||
Invoke-WebRequest -TimeoutSec 900 -Uri 'https://{{ authority_name }}:8443/api/request/?wait=yes&autosign=yes' -InFile host_csr.pem -ContentType application/pkcs10 -Method POST -MaximumRedirection 3 -OutFile host_cert.pem
|
||||
<pre class="code"><code>{% include "snippets/request-client.sh" %}</code></pre>
|
||||
</div>
|
||||
|
||||
# Import certificate
|
||||
{% if session.authority.certificate.algorithm == "ec" %}Import-Certificate -FilePath host_cert.pem -CertStoreLocation Cert:\LocalMachine\My
|
||||
{% else %}C:\Windows\system32\certutil.exe -addstore My host_cert.pem
|
||||
{% endif %}
|
||||
# Set up IPSec VPN tunnel
|
||||
Remove-VpnConnection -AllUserConnection -Force k-space
|
||||
Add-VpnConnection `
|
||||
-Name k-space `
|
||||
-ServerAddress guests.k-space.ee `
|
||||
-AuthenticationMethod MachineCertificate `
|
||||
-SplitTunneling `
|
||||
-TunnelType ikev2 `
|
||||
-PassThru -AllUserConnection
|
||||
<p>For server certificates use fully qualified hostname as common name and sign request accordingly:</p>
|
||||
<div class="highlight">
|
||||
<pre class="code"><code>{% include "snippets/request-server.sh" %}</code></pre>
|
||||
</div>
|
||||
|
||||
# Security hardening
|
||||
Set-VpnConnectionIPsecConfiguration `
|
||||
-ConnectionName k-space `
|
||||
-AuthenticationTransformConstants GCMAES128 `
|
||||
-CipherTransformConstants GCMAES128 `
|
||||
-EncryptionMethod AES256 `
|
||||
-IntegrityCheckMethod SHA384 `
|
||||
-DHGroup {% if session.authority.certificate.algorithm == "ec" %}ECP384{% else %}Group14{% endif %} `
|
||||
-PfsGroup {% if session.authority.certificate.algorithm == "ec" %}ECP384{% else %}PFS2048{% endif %} `
|
||||
-PassThru -AllUserConnection -Force</code></pre>
|
||||
</div>
|
||||
<p>To renew:</p>
|
||||
|
||||
<!--
|
||||
AuthenticationTransformConstants - ESP integrity algorithm, one of: None MD596 SHA196 SHA256128 GCMAES128 GCMAES192 GCMAES256
|
||||
CipherTransformConstants - ESP symmetric cipher, one of: DES DES3 AES128 AES192 AES256 GCMAES128 GCMAES192 GCMAES256
|
||||
EncryptionMethod - IKE symmetric cipher, one of: DES DES3 AES128 AES192 AES256
|
||||
IntegrityCheckMethod - IKE hash algorithm, one of: MD5 SHA196 SHA256 SHA384
|
||||
DHGroup = IKE key exchange, one of: None Group1 Group2 Group14 ECP256 ECP384 Group24
|
||||
PfsGroup = one of: None PFS1 PFS2 PFS2048 ECP256 ECP384 PFSMM PFS24
|
||||
-->
|
||||
{% endif %}
|
||||
|
||||
<h5>UNIX & UNIX-like</h5>
|
||||
|
||||
<p>For client certificates generate key pair and submit the signing request with common name set to short hostname:</p>
|
||||
<div class="highlight">
|
||||
<pre class="code"><code>test -e /sbin/uci && NAME=$(uci get system.@system[0].hostname)
|
||||
test -e /bin/hostname && NAME=$(hostname)
|
||||
test -n "$NAME" || NAME=$(cat /proc/sys/kernel/hostname)
|
||||
|
||||
mkdir -p /etc/certidude/authority/{{ authority_name }}/
|
||||
echo {{ session.authority.certificate.md5sum }} /etc/certidude/authority/{{ authority_name }}/ca_cert.pem | md5sum -c \
|
||||
|| rm -fv /etc/certidude/authority/{{ authority_name }}/*.pem
|
||||
test -e /etc/certidude/authority/{{ authority_name }}/ca_cert.pem \
|
||||
|| cat << EOF > /etc/certidude/authority/{{ authority_name }}/ca_cert.pem
|
||||
{{ session.authority.certificate.blob }}EOF
|
||||
test -e /etc/certidude/authority/{{ authority_name }}/host_key.pem \
|
||||
|| {% if session.authority.certificate.algorithm == "ec" %}openssl ecparam -name secp384r1 -genkey -noout \
|
||||
-out /etc/certidude/authority/{{ authority_name }}/host_key.pem{% else %}openssl genrsa \
|
||||
-out /etc/certidude/authority/{{ authority_name }}/host_key.pem 2048{% endif %}
|
||||
test -e /etc/certidude/authority/{{ authority_name }}/host_req.pem \
|
||||
|| openssl req -new -sha384 -subj "/CN=$NAME" \
|
||||
-key /etc/certidude/authority/{{ authority_name }}/host_key.pem \
|
||||
-out /etc/certidude/authority/{{ authority_name }}/host_req.pem
|
||||
echo "If CSR submission fails, you can copy paste it to Certidude:"
|
||||
cat /etc/certidude/authority/{{ authority_name }}/host_req.pem
|
||||
|
||||
test -e /etc/pki/ca-trust/source/anchors \
|
||||
&& ln -s /etc/certidude/authority/{{ authority_name }}/ca_cert.pem /etc/pki/ca-trust/source/anchors/{{ authority_name }} \
|
||||
&& update-ca-trust
|
||||
test -e /usr/local/share/ca-certificates/ \
|
||||
&& ln -s /etc/certidude/authority/{{ authority_name }}/ca_cert.pem /usr/local/share/ca-certificates/{{ authority_name }}.crt \
|
||||
&& update-ca-certificates
|
||||
|
||||
curl -f -L -H "Content-type: application/pkcs10" \
|
||||
--data-binary @/etc/certidude/authority/{{ authority_name }}/host_req.pem \
|
||||
-o /etc/certidude/authority/{{ authority_name }}/host_cert.pem \
|
||||
'http://{{ authority_name }}/api/request/?wait=yes&autosign=yes'
|
||||
</code></pre>
|
||||
</div>
|
||||
|
||||
<p>For server certificates use fully qualified hostname as common name and sign request accordingly:</p>
|
||||
<div class="highlight">
|
||||
<pre class="code"><code>test -e /sbin/uci && NAME=$(nslookup $(uci get network.wan.ipaddr) | grep "name =" | head -n1 | cut -d "=" -f 2 | xargs)
|
||||
test -e /bin/hostname && NAME=$(hostname -f)
|
||||
test -n "$NAME" || NAME=$(cat /proc/sys/kernel/hostname)
|
||||
|
||||
mkdir -p /etc/certidude/authority/{{ authority_name }}/
|
||||
echo {{ session.authority.certificate.md5sum }} /etc/certidude/authority/{{ authority_name }}/ca_cert.pem | md5sum -c \
|
||||
|| rm -fv /etc/certidude/authority/{{ authority_name }}/*.pem
|
||||
test -e /etc/certidude/authority/{{ authority_name }}/ca_cert.pem \
|
||||
|| cat << EOF > /etc/certidude/authority/{{ authority_name }}/ca_cert.pem
|
||||
{{ session.authority.certificate.blob }}EOF
|
||||
test -e /etc/certidude/authority/{{ authority_name }}/host_key.pem \
|
||||
|| {% if session.authority.certificate.algorithm == "ec" %}openssl ecparam -name secp384r1 -genkey -noout \
|
||||
-out /etc/certidude/authority/{{ authority_name }}/host_key.pem{% else %}openssl genrsa \
|
||||
-out /etc/certidude/authority/{{ authority_name }}/host_key.pem 2048{% endif %}
|
||||
test -e /etc/certidude/authority/{{ authority_name }}/host_req.pem \
|
||||
|| openssl req -new -sha384 -subj "/CN=$NAME" \
|
||||
-key /etc/certidude/authority/{{ authority_name }}/host_key.pem \
|
||||
-out /etc/certidude/authority/{{ authority_name }}/host_req.pem
|
||||
echo "If CSR submission fails, you can copy paste it to Certidude:"
|
||||
cat /etc/certidude/authority/{{ authority_name }}/host_req.pem
|
||||
|
||||
curl -f -L -H "Content-type: application/pkcs10" \
|
||||
--cacert /etc/certidude/authority/{{ authority_name }}/ca_cert.pem \
|
||||
--data-binary @/etc/certidude/authority/{{ authority_name }}/host_req.pem \
|
||||
-o /etc/certidude/authority/{{ authority_name }}/host_cert.pem \
|
||||
'https://{{ authority_name }}:8443/api/request/?wait=yes'
|
||||
</code></pre>
|
||||
</div>
|
||||
|
||||
<p>To renew:</p>
|
||||
|
||||
<div class="highlight">
|
||||
<pre class="code"><code>curl -f -L -H "Content-type: application/pkcs10" \
|
||||
--cacert /etc/certidude/authority/{{ authority_name }}/ca_cert.pem \
|
||||
--key /etc/certidude/authority/{{ authority_name }}/host_key.pem \
|
||||
--cert /etc/certidude/authority/{{ authority_name }}/host_cert.pem \
|
||||
--data-binary @/etc/certidude/authority/{{ authority_name }}/host_req.pem \
|
||||
-o /etc/certidude/authority/{{ authority_name }}/host_cert.pem \
|
||||
'https://{{ authority_name }}:8443/api/request/?wait=yes'
|
||||
</code></pre>
|
||||
</div>
|
||||
|
||||
{% if "openvpn" in session.service.protocols %}
|
||||
<div class="highlight">
|
||||
<pre class="code"><code>{% include "snippets/renew.sh" %}</code></pre>
|
||||
</div>
|
||||
|
||||
{% if "openvpn" in session.service.protocols %}
|
||||
<h5>OpenVPN as client</h5>
|
||||
|
||||
<p>First acquire certificates using the snippet above.</p>
|
||||
|
||||
<p>Then install software:</p>
|
||||
|
||||
<div class="highlight">
|
||||
<pre class="code"><code># Install packages on Ubuntu & Fedora
|
||||
which apt && apt install openvpn
|
||||
which dnf && dnf install openvpn
|
||||
<div class="highlight"><pre class="code"><code>{% include "snippets/openvpn-client.sh" %}</code></pre></div>
|
||||
{% endif %}
|
||||
|
||||
cat > /etc/openvpn/{{ authority_name }}.conf << EOF
|
||||
client
|
||||
nobind
|
||||
{% for router in session.service.routers %}
|
||||
remote {{ router }} 1194 udp
|
||||
remote {{ router }} 443 tcp-client
|
||||
{% endfor %}
|
||||
tls-version-min 1.2
|
||||
tls-cipher TLS-DHE-RSA-WITH-AES-256-GCM-SHA384
|
||||
cipher AES-128-GCM
|
||||
auth SHA384
|
||||
mute-replay-warnings
|
||||
reneg-sec 0
|
||||
remote-cert-tls server
|
||||
dev tun
|
||||
persist-tun
|
||||
persist-key
|
||||
ca /etc/certidude/authority/{{ authority_name }}/ca_cert.pem
|
||||
key /etc/certidude/authority/{{ authority_name }}/host_key.pem
|
||||
cert /etc/certidude/authority/{{ authority_name }}/host_cert.pem
|
||||
EOF
|
||||
|
||||
systemctl restart openvpn
|
||||
</code></pre>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% if "ikev2" in session.service.protocols %}
|
||||
{% if "ikev2" in session.service.protocols %}
|
||||
<h5>StrongSwan as client</h5>
|
||||
|
||||
<p>First acquire certificates using the snippet above.</p>
|
||||
|
||||
<p>Then install software:</p>
|
||||
<div class="highlight">
|
||||
<pre class="code"><code># Install packages on Ubuntu & Fedora, patch Fedora paths
|
||||
which apt && apt install strongswan
|
||||
which dnf && dnf install strongswan
|
||||
test -e /etc/strongswan && test -e /etc/ipsec.conf || ln -s strongswan/ipsec.conf /etc/ipsec.conf
|
||||
test -e /etc/strongswan && test -e /etc/ipsec.d || ln -s strongswan/ipsec.d /etc/ipsec.d
|
||||
test -e /etc/strongswan && test -e /etc/ipsec.secrets || ln -s strongswan/ipsec.secrets /etc/ipsec.secrets
|
||||
|
||||
# Hard link files to prevent Apparmor issues and have more manageable config
|
||||
ln /etc/certidude/authority/{{ authority_name }}/ca_cert.pem /etc/ipsec.d/cacerts/{{ authority_name }}.pem
|
||||
ln /etc/certidude/authority/{{ authority_name }}/host_cert.pem /etc/ipsec.d/certs/{{ authority_name }}.pem
|
||||
ln /etc/certidude/authority/{{ authority_name }}/host_key.pem /etc/ipsec.d/private/{{ authority_name }}.pem
|
||||
</code></pre>
|
||||
<pre class="code"><code>{% include "snippets/strongswan-patching.sh" %}</code></pre>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<p>To configure StrongSwan as roadwarrior:</p>
|
||||
<div class="highlight">
|
||||
<pre class="code"><code>cat > /etc/ipsec.conf << EOF
|
||||
|
||||
ca {{ authority_name }}
|
||||
auto=add
|
||||
cacert = {{ authority_name }}.pem
|
||||
{% if session.features.crl %} crluri = http://{{ authority_name }}/api/revoked/{% endif %}
|
||||
{% if session.features.ocsp %} ocspuri = http://{{ authority_name }}/api/ocsp/{% endif %}
|
||||
|
||||
conn client-to-site
|
||||
auto=start
|
||||
right={{ session.service.routers[0] }}
|
||||
rightsubnet=0.0.0.0/0
|
||||
rightca="{{ session.authority.certificate.distinguished_name }}"
|
||||
left=%defaultroute
|
||||
leftcert={{ authority_name }}.pem
|
||||
leftsourceip=%config
|
||||
leftca="{{ session.authority.certificate.distinguished_name }}"
|
||||
keyexchange=ikev2
|
||||
keyingtries=%forever
|
||||
dpdaction=restart
|
||||
closeaction=restart
|
||||
ike=aes256-sha384-{% if session.authority.certificate.algorithm == "ec" %}ecp384{% else %}modp2048{% endif %}!
|
||||
esp=aes128gcm16-aes128gmac!
|
||||
|
||||
EOF
|
||||
|
||||
echo ": {% if session.authority.certificate.algorithm == "ec" %}ECDSA{% else %}RSA{% endif %} {{ authority_name }}.pem" > /etc/ipsec.secrets
|
||||
|
||||
ipsec restart</code></pre>
|
||||
</div>
|
||||
<div class="highlight"><pre class="code"><code>{% include "snippets/strongswan-client.sh" %}</code></pre></div>
|
||||
{% endif %}
|
||||
|
||||
|
||||
{% endif %}
|
||||
<h5>OpenWrt/LEDE as VPN gateway</h5>
|
||||
|
||||
<p>First enroll certificates using the snippet from UNIX section above</p>
|
||||
|
||||
<h5>OpenWrt/LEDE as VPN gateway</h5>
|
||||
|
||||
<p>First enroll certificates using the snippet from UNIX section above</p>
|
||||
|
||||
<p>Then:</p>
|
||||
<div class="highlight">
|
||||
<pre class="code"><code>opkg install curl libmbedtls
|
||||
# Derive FQDN from WAN interface's reverse DNS record
|
||||
FQDN=$(nslookup $(uci get network.wan.ipaddr) | grep "name =" | head -n1 | cut -d "=" -f 2 | xargs)
|
||||
grep -c certidude /etc/sysupgrade.conf || echo /etc/certidude >> /etc/sysupgrade.conf
|
||||
|
||||
# Create VPN gateway up/down script for reporting client IP addresses to CA
|
||||
cat <<\EOF > /etc/certidude/authority/{{ authority_name }}/updown
|
||||
#!/bin/sh
|
||||
|
||||
CURL="curl -m 3 -f --key /etc/certidude/authority/{{ authority_name }}/host_key.pem --cert /etc/certidude/authority/{{ authority_name }}/host_cert.pem --cacert /etc/certidude/authority/{{ authority_name }}/ca_cert.pem https://{{ authority_name }}:8443/api/lease/"
|
||||
|
||||
case $PLUTO_VERB in
|
||||
up-client) $CURL --data-urlencode "outer_address=$PLUTO_PEER" --data-urlencode "inner_address=$PLUTO_PEER_SOURCEIP" --data-urlencode "client=$PLUTO_PEER_ID" ;;
|
||||
*) ;;
|
||||
esac
|
||||
|
||||
case $script_type in
|
||||
client-connect) $CURL --data-urlencode client=$X509_0_CN --data-urlencode serial=$tls_serial_0 --data-urlencode outer_address=$untrusted_ip --data-urlencode inner_address=$ifconfig_pool_remote_ip ;;
|
||||
*) ;;
|
||||
esac
|
||||
EOF
|
||||
|
||||
chmod +x /etc/certidude/authority/{{ authority_name }}/updown
|
||||
</code></pre>
|
||||
</div>
|
||||
|
||||
|
||||
{% if "openvpn" in session.service.protocols %}
|
||||
<p>Then:</p>
|
||||
<div class="highlight">
|
||||
<pre class="code"><code>opkg install curl libmbedtls
|
||||
# Derive FQDN from WAN interface's reverse DNS record
|
||||
FQDN=$(nslookup $(uci get network.wan.ipaddr) | grep "name =" | head -n1 | cut -d "=" -f 2 | xargs)
|
||||
grep -c certidude /etc/sysupgrade.conf || echo /etc/certidude >> /etc/sysupgrade.conf
|
||||
{% include "snippets/gateway-updown.sh" %}
|
||||
</code></pre>
|
||||
</div>
|
||||
|
||||
{% if "openvpn" in session.service.protocols %}
|
||||
<p>Then either set up OpenVPN service:</p>
|
||||
<div class="highlight">
|
||||
<pre class="code"><code>opkg update
|
||||
opkg install curl openssl-util openvpn-openssl
|
||||
|
||||
{% if session.authority.certificate.algorithm != "ec" %}
|
||||
# Generate Diffie-Hellman parameters file for OpenVPN
|
||||
test -e /etc/certidude/dh.pem \
|
||||
|| openssl dhparam 2048 -out /etc/certidude/dh.pem
|
||||
{% endif %}
|
||||
# Create interface definition for tunnel
|
||||
uci set network.vpn=interface
|
||||
uci set network.vpn.name='vpn'
|
||||
uci set network.vpn.ifname=tun_s2c_udp tun_s2c_tcp
|
||||
uci set network.vpn.proto='none'
|
||||
|
||||
# Create zone definition for VPN interface
|
||||
uci set firewall.vpn=zone
|
||||
uci set firewall.vpn.name='vpn'
|
||||
uci set firewall.vpn.input='ACCEPT'
|
||||
uci set firewall.vpn.forward='ACCEPT'
|
||||
uci set firewall.vpn.output='ACCEPT'
|
||||
uci set firewall.vpn.network='vpn'
|
||||
|
||||
# Allow UDP 1194 on WAN interface
|
||||
uci set firewall.openvpn=rule
|
||||
uci set firewall.openvpn.name='Allow OpenVPN'
|
||||
uci set firewall.openvpn.src='wan'
|
||||
uci set firewall.openvpn.dest_port=1194
|
||||
uci set firewall.openvpn.proto='udp'
|
||||
uci set firewall.openvpn.target='ACCEPT'
|
||||
|
||||
# Allow TCP 443 on WAN interface
|
||||
uci set firewall.openvpn=rule
|
||||
uci set firewall.openvpn.name='Allow OpenVPN over TCP'
|
||||
uci set firewall.openvpn.src='wan'
|
||||
uci set firewall.openvpn.dest_port=443
|
||||
uci set firewall.openvpn.proto='tcp'
|
||||
uci set firewall.openvpn.target='ACCEPT'
|
||||
|
||||
# Forward traffic from VPN to LAN
|
||||
uci set firewall.c2s=forwarding
|
||||
uci set firewall.c2s.src='vpn'
|
||||
uci set firewall.c2s.dest='lan'
|
||||
|
||||
# Permit DNS queries from VPN
|
||||
uci set dhcp.@dnsmasq[0].localservice='0'
|
||||
|
||||
touch /etc/config/openvpn
|
||||
|
||||
# Configure OpenVPN over TCP
|
||||
uci set openvpn.s2c_tcp=openvpn
|
||||
uci set openvpn.s2c_tcp.local=$(uci get network.wan.ipaddr)
|
||||
uci set openvpn.s2c_tcp.server='10.179.43.0 255.255.255.128'
|
||||
uci set openvpn.s2c_tcp.proto='tcp-server'
|
||||
uci set openvpn.s2c_tcp.port='443'
|
||||
uci set openvpn.s2c_tcp.dev=tun_s2c_tcp
|
||||
|
||||
# Configure OpenVPN over UDP
|
||||
uci set openvpn.s2c_udp=openvpn
|
||||
uci set openvpn.s2c_udp.local=$(uci get network.wan.ipaddr)
|
||||
uci set openvpn.s2c_udp.server='10.179.43.128 255.255.255.128'
|
||||
uci set openvpn.s2c_tcp.dev=tun_s2c_udp
|
||||
|
||||
for section in s2c_tcp s2c_udp; do
|
||||
|
||||
# Common paths
|
||||
uci set openvpn.$section.script_security=2
|
||||
uci set openvpn.$section.client_connect='/etc/certidude/updown'
|
||||
uci set openvpn.$section.key='/etc/certidude/authority/{{ authority_name }}/host_key.pem'
|
||||
uci set openvpn.$section.cert='/etc/certidude/authority/{{ authority_name }}/host_cert.pem'
|
||||
uci set openvpn.$section.ca='/etc/certidude/authority/{{ authority_name }}/ca_cert.pem'
|
||||
{% if session.authority.certificate.algorithm != "ec" %}uci set openvpn.$section.dh='/etc/certidude/dh.pem'{% endif %}
|
||||
uci set openvpn.$section.enabled=1
|
||||
|
||||
# DNS and routes
|
||||
uci add_list openvpn.$section.push="route-metric 1000"
|
||||
uci add_list openvpn.$section.push="route $(uci get network.lan.ipaddr) $(uci get network.lan.netmask)"
|
||||
uci add_list openvpn.$section.push="dhcp-option DNS $(uci get network.lan.ipaddr)"
|
||||
uci add_list openvpn.$section.push="dhcp-option DOMAIN $(uci get dhcp.@dnsmasq[0].domain)"
|
||||
|
||||
# Security hardening
|
||||
uci set openvpn.$section.tls_version_min='1.2'
|
||||
uci set openvpn.$section.tls_cipher='TLS-{% if session.authority.certificate.algorithm == "ec" %}ECDHE-ECDSA{% else %}DHE-RSA{% endif %}-WITH-AES-128-GCM-SHA384'
|
||||
uci set openvpn.$section.cipher='AES-128-GCM'
|
||||
uci set openvpn.$section.auth='SHA384'
|
||||
|
||||
done
|
||||
|
||||
/etc/init.d/openvpn restart
|
||||
/etc/init.d/firewall restart</code></pre>
|
||||
<pre class="code"><code>{% include "snippets/openwrt-openvpn.sh" %}</code></pre>
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
{% endif %}
|
||||
|
||||
{% if "ikev2" in session.service.protocols %}
|
||||
{% if "ikev2" in session.service.protocols %}
|
||||
<p>Alternatively or additionally set up StrongSwan:</p>
|
||||
<div class="highlight">
|
||||
<pre class="code"><code>opkg update
|
||||
opkg install curl openssl-util strongswan-full strongswan-mod-openssl kmod-crypto-echainiv kmod-crypto-gcm
|
||||
|
||||
# Generate StrongSwan config
|
||||
cat > /etc/ipsec.conf << EOF
|
||||
config setup
|
||||
strictcrlpolicy=yes
|
||||
uniqueids = yes
|
||||
|
||||
ca {{ authority_name }}
|
||||
auto=add
|
||||
cacert = {{ authority_name }}.pem
|
||||
{% if session.features.crl %} crluri = http://{{ authority_name }}/api/revoked/{% endif %}
|
||||
{% if session.features.ocsp %} ocspuri = http://{{ authority_name }}/api/ocsp/{% endif %}
|
||||
|
||||
conn default-{{ authority_name }}
|
||||
ike=aes256-sha384-{% if session.authority.certificate.algorithm == "ec" %}ecp384{% else %}modp2048{% endif %}!
|
||||
esp=aes128gcm16-aes128gmac!
|
||||
left=$(uci get network.wan.ipaddr) # Bind to this IP address
|
||||
leftid={{ session.service.routers | first }}
|
||||
leftupdown=/etc/certidude/authority/{{ authority_name }}/updown
|
||||
leftcert={{ authority_name }}.pem
|
||||
leftsubnet=$(uci get network.lan.ipaddr | cut -d . -f 1-3).0/24 # Subnets pushed to roadwarriors
|
||||
leftdns=$(uci get network.lan.ipaddr) # IP of DNS server advertised to roadwarriors
|
||||
leftca="{{ session.authority.certificate.distinguished_name }}"
|
||||
rightca="{{ session.authority.certificate.distinguished_name }}"
|
||||
rightsourceip=172.21.0.0/24 # Roadwarrior virtual IP pool
|
||||
dpddelay=0
|
||||
dpdaction=clear
|
||||
|
||||
conn site-to-clients
|
||||
auto=add
|
||||
also=default-{{ authority_name }}
|
||||
|
||||
conn site-to-client1
|
||||
auto=ignore
|
||||
also=default-{{ authority_name }}
|
||||
rightid="CN=*, OU=IP Camera, O=*, DC=*, DC=*, DC=*"
|
||||
rightsourceip=172.21.0.1
|
||||
|
||||
|
||||
|
||||
EOF
|
||||
|
||||
echo ": {% if session.authority.certificate.algorithm == "ec" %}ECDSA{% else %}RSA{% endif %} /etc/certidude/authority/{{ authority_name }}/host_key.pem" > /etc/ipsec.secrets
|
||||
|
||||
ipsec restart</code></pre>
|
||||
<pre class="code"><code>opkg update
|
||||
opkg install curl openssl-util strongswan-full strongswan-mod-openssl kmod-crypto-echainiv kmod-crypto-gcm
|
||||
{% include "snippets/strongswan-server.sh" %}
|
||||
ipsec restart</code></pre>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
{% if session.authority.builder %}
|
||||
{% if session.authority.builder %}
|
||||
<h5>OpenWrt/LEDE image builder</h5>
|
||||
<p>Hit a link to generate machine specific image. Note that this might take couple minutes to finish.</p>
|
||||
<ul>
|
||||
{% for name, title, filename in session.authority.builder.profiles %}
|
||||
{% for name, title, filename in session.authority.builder.profiles %}
|
||||
<li><a href="/api/build/{{ name }}/{{ filename }}">{{ title }}</a></li>
|
||||
{% endfor %}
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
|
||||
<h5>SCEP</h5>
|
||||
<p>Use following as the enrollment URL: http://{{ authority_name }}/cgi-bin/pkiclient.exe</p>
|
||||
<h5>SCEP</h5>
|
||||
<p>Use following as the enrollment URL: http://{{ authority_name }}/cgi-bin/pkiclient.exe</p>
|
||||
|
||||
<h5>Copy & paste</h5>
|
||||
<h5>Copy & paste</h5>
|
||||
|
||||
<p>Use whatever tools you have available on your platform to generate
|
||||
keypair and just paste ASCII armored PEM file contents here and hit submit:</p>
|
||||
<p>Use whatever tools you have available on your platform to generate
|
||||
keypair and just paste ASCII armored PEM file contents here and hit submit:</p>
|
||||
|
||||
<textarea id="request_body" style="width:100%; min-height: 10em;" placeholder="-----BEGIN CERTIFICATE REQUEST-----"></textarea>
|
||||
<textarea id="request_body" style="width:100%; min-height: 10em;"
|
||||
placeholder="-----BEGIN CERTIFICATE REQUEST-----"></textarea>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<div class="btn-group">
|
||||
<button type="button" onclick="onSubmitRequest();" class="btn btn-primary"><i class="fa fa-upload"></i> Submit</button>
|
||||
<button type="button" class="btn btn-secondary" data-dismiss="modal"><i class="fa fa-ban"></i> Close</button>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<div class="btn-group">
|
||||
<button type="button" onclick="onSubmitRequest();" class="btn btn-primary"><i class="fa fa-upload"></i> Submit</button>
|
||||
<button type="button" class="btn btn-secondary" data-dismiss="modal"><i class="fa fa-ban"></i> Close</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal fade" id="revocation_list_modal" role="dialog">
|
||||
<div class="modal-dialog modal-lg">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<button type="button" class="close" data-dismiss="modal">×</button>
|
||||
<h4 class="modal-title">Revocation lists</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p>To fetch <a href="http://{{authority_name}}/api/revoked/">certificate revocation list</a>:</p>
|
||||
<div class="modal fade" id="revocation_list_modal" role="dialog">
|
||||
<div class="modal-dialog modal-lg">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<button type="button" class="close" data-dismiss="modal">×</button>
|
||||
<h4 class="modal-title">Revocation lists</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p>To fetch <a href="http://{{authority_name}}/api/revoked/">certificate revocation list</a>:</p>
|
||||
<pre><code>curl http://{{authority_name}}/api/revoked/ > crl.der
|
||||
curl http://{{authority_name}}/api/revoked/ -L -H "Accept: application/x-pem-file"
|
||||
curl http://{{authority_name}}/api/revoked/?wait=yes -L -H "Accept: application/x-pem-file" > crl.pem</code></pre>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn" data-dismiss="modal">Close</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn" data-dismiss="modal">Close</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<section id="about">
|
||||
<h2>{{ session.user.gn }} {{ session.user.sn }} ({{session.user.name }}) settings</h2>
|
||||
|
||||
<p title="Bundles are mainly intended for Android and iOS users">
|
||||
Click <button id="enroll">here</button> to generate Android or iOS bundle for current user account.</p>
|
||||
|
||||
<p>Mails will be sent to: {{ session.user.mail }}</p>
|
||||
|
||||
{% if session.authority %}
|
||||
|
||||
<h2>Authority certificate</h2>
|
||||
|
||||
<p>Several things are hardcoded into the <a href="/api/certificate">certificate</a> and
|
||||
as such require complete reset of X509 infrastructure if some of them needs to be changed.</p>
|
||||
|
||||
<h2>Authority settings</h2>
|
||||
|
||||
<p>These can be reconfigured via /etc/certidude/server.conf on the server.</p>
|
||||
|
||||
{% if session.authority.mailer %}
|
||||
<p>Mails will appear from: {{ session.authority.mailer.name }} <{{ session.authority.mailer.address }}></p>
|
||||
{% else %}
|
||||
<p>E-mail disabled</p>
|
||||
{% endif %}
|
||||
|
||||
|
||||
<p>User enrollment:
|
||||
{% if session.authority.user_enrollment_allowed %}
|
||||
{% if session.authority.user_multiple_certificates %}
|
||||
multiple
|
||||
{% else %}
|
||||
single
|
||||
{% endif %}
|
||||
allowed
|
||||
{% else %}
|
||||
forbidden
|
||||
{% endif %}
|
||||
</p>
|
||||
|
||||
|
||||
<p>Machine enrollment:
|
||||
{% if session.authority.machine_enrollment_allowed %}
|
||||
allowed
|
||||
{% else %}
|
||||
forbidden
|
||||
{% endif %}
|
||||
</p>
|
||||
|
||||
|
||||
<p>Certificate attributes:</p>
|
||||
|
||||
<ul>
|
||||
<li>Server certificate lifetime: {{ session.authority.signature.server_certificate_lifetime }} days</li>
|
||||
<li>Client certificate lifetime: {{ session.authority.signature.client_certificate_lifetime }} days</li>
|
||||
{% if session.features.crl %}
|
||||
<li>Revocation list lifetime: {{ session.authority.signature.revocation_list_lifetime }} seconds</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
|
||||
<p>Authenticated users allowed from:
|
||||
{% if not session.authority.user_subnets %}
|
||||
nowhere</p>
|
||||
{% elif "0.0.0.0/0" in session.authority.user_subnets %}
|
||||
anywhere</p>
|
||||
{% else %}
|
||||
</p>
|
||||
<ul>
|
||||
{% for i in session.authority.user_subnets %}
|
||||
<li>{{ i }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
|
||||
|
||||
|
||||
<p>Authority administration is allowed from:
|
||||
{% if not session.authority.admin_subnets %}
|
||||
nowhere</p>
|
||||
{% elif "0.0.0.0/0" in session.authority.admin_subnets %}
|
||||
anywhere</p>
|
||||
{% else %}
|
||||
<ul>
|
||||
{% for subnet in session.authority.admin_subnets %}
|
||||
<li>{{ subnet }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
|
||||
<p>Authority administration allowed for:</p>
|
||||
|
||||
<ul>
|
||||
{% for user in session.authority.admin_users %}
|
||||
<li><a href="mailto:{{ user.mail}}">{{ user.given_name }} {{user.surname }}</a></li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
{% else %}
|
||||
<p>Here you can renew your certificates</p>
|
||||
|
||||
{% endif %}
|
||||
|
||||
{% set s = session.certificate.identity %}
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
|
||||
|
||||
<div class="col-sm-6">
|
||||
<div class="col-sm-{{ column_width }}">
|
||||
<h1>Signed certificates</h1>
|
||||
<p>Following certificates have been signed:</p>
|
||||
<p>Authority administration allowed for
|
||||
{% for user in session.authority.admin_users %}<a href="mailto:{{ user.mail}}">{{ user.given_name }} {{user.surname }}</a>{% if not loop.last %}, {% endif %}{% endfor %} from {% if "0.0.0.0/0" in session.authority.admin_subnets %}anywhere{% else %}
|
||||
{% for subnet in session.authority.admin_subnets %}{{ subnet }}{% if not loop.last %}, {% endif %}{% endfor %}{% endif %}.
|
||||
Authority certificate can be downloaded from <a href="/api/certificate/">here</a>.
|
||||
Following certificates have been signed:</p>
|
||||
<div id="signed_certificates">
|
||||
{% for certificate in session.authority.signed | sort(attribute="signed", reverse=true) %}
|
||||
{% include "views/signed.html" %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-sm-6">
|
||||
|
||||
|
||||
{% if session.authority %}
|
||||
<div class="col-sm-{{ column_width }}">
|
||||
{% if session.authority %}
|
||||
{% if session.features.token %}
|
||||
<h1>Tokens</h1>
|
||||
|
||||
<p>Tokens allow enrolling smartphones and third party devices.</p>
|
||||
<ul>
|
||||
<li>You can issue yourself a token to be used on a mobile device</li>
|
||||
@ -635,7 +172,6 @@ forbidden
|
||||
</span>
|
||||
</div>
|
||||
</p>
|
||||
|
||||
<div id="token_qrcode"></div>
|
||||
{% endif %}
|
||||
|
||||
@ -649,30 +185,32 @@ forbidden
|
||||
Request submission is enabled.
|
||||
{% else %}
|
||||
Request submission allowed from
|
||||
{% for subnet in session.authority.request_subnets %}{{ subnet }},{% endfor %}.
|
||||
{% for subnet in session.authority.request_subnets %}
|
||||
{{ subnet }}{% if not loop.last %}, {% endif %}
|
||||
{% endfor %}.
|
||||
{% endif %}
|
||||
|
||||
{# if session.request_submission_allowed #}
|
||||
See <a href="#request_submission_modal" data-toggle="modal">here</a> for more information on manual signing request upload.
|
||||
{# endif #}
|
||||
|
||||
{% if session.authority.autosign_subnets %}
|
||||
{% if "0.0.0.0/0" in session.authority.autosign_subnets %}
|
||||
All requests are automatically signed.
|
||||
{% else %}
|
||||
Requests from
|
||||
{% for subnet in session.authority.autosign_subnets %}
|
||||
{{ subnet }},
|
||||
{{ subnet }}{% if not loop.last %}, {% endif %}
|
||||
{% endfor %}
|
||||
are automatically signed.
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</p>
|
||||
|
||||
{% if columns >= 3 %}
|
||||
</div>
|
||||
<div class="col-sm-{{ column_width }}">
|
||||
{% endif %}
|
||||
<div id="pending_requests">
|
||||
{% for request in session.authority.requests | sort(attribute="submitted", reverse=true) %}
|
||||
{% include "views/request.html" %}
|
||||
{% endfor %}
|
||||
{% for request in session.authority.requests | sort(attribute="submitted", reverse=true) %}
|
||||
{% include "views/request.html" %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
<p><h1>Revoked certificates</h1></p>
|
||||
<p>Following certificates have been revoked{% if session.features.crl %}, for more information click
|
||||
@ -682,8 +220,24 @@ forbidden
|
||||
{% include "views/revoked.html" %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
<div id="column-log" class="col-sm-{% if columns == 4 %}{{ column_width }}{% else %}12{% endif %}" {% if columns < 4 %}style="display:none;"{% endif %}>
|
||||
<div class="loader-container">
|
||||
<div class="loader"></div>
|
||||
<p>Loading logs, this might take a while...</p>
|
||||
</div>
|
||||
<div class="content" style="display:none;">
|
||||
<h1>Log</h1>
|
||||
<div class="btn-group" data-toggle="buttons">
|
||||
<label class="btn btn-primary active"><input id="log-level-critical" type="checkbox" autocomplete="off" checked> Critical</label>
|
||||
<label class="btn btn-primary active"><input id="log-level-errors" type="checkbox" autocomplete="off" checked> Errors</label>
|
||||
<label class="btn btn-primary active"><input id="log-level-warnings" type="checkbox" autocomplete="off" checked> Warnings</label>
|
||||
<label class="btn btn-primary active"><input id="log-level-info" type="checkbox" autocomplete="off" checked> Info</label>
|
||||
<label class="btn btn-primary"><input id="log-level-debug" type="checkbox" autocomplete="off"> Debug</label>
|
||||
</div>
|
||||
<ul id="log-entries" class="list-group">
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<section id="config">
|
||||
</section>
|
||||
|
||||
{% endif %}
|
||||
|
14
certidude/static/views/insecure.html
Normal file
14
certidude/static/views/insecure.html
Normal file
@ -0,0 +1,14 @@
|
||||
<p>You're viewing this page over insecure channel.
|
||||
You can give it a try and <a href="https://{{ authority_name }}">connect over HTTPS</a>,
|
||||
if that succeeds all subsequents accesses of this page will go over HTTPS.
|
||||
</p>
|
||||
<p>
|
||||
Click <a href="/api/certificate">here</a> to fetch the certificate of this authority.
|
||||
Alternatively install certificate on Fedora or Ubuntu with following copy-pastable snippet:
|
||||
</p>
|
||||
|
||||
<div class="highlight">
|
||||
<pre class="code"><code>{% include "snippets/store-authority.sh" %}
|
||||
{% include "snippets/update-trust.sh" %}</code></pre>
|
||||
</div>
|
||||
|
@ -1,8 +1,8 @@
|
||||
<i class="fa fa-circle" style="color:{% if certificate.lease.age > 172800 %}#d9534f{% else %}{% if certificate.lease.age > 3600 %}#0275d8{% else %}#5cb85c{% endif %}{% endif %};"/>
|
||||
<i class="fa fa-circle" style="color:{% if certificate.lease.age > 172800 %}#d9534f{% else %}{% if certificate.lease.age > 7200 %}#0275d8{% else %}#5cb85c{% endif %}{% endif %};"/>
|
||||
Last seen
|
||||
<time class="timeago" datetime="{{ certificate.lease.last_seen }}">{{ certificate.lease.last_seen }}</time>
|
||||
at
|
||||
<a href="http://{{ certificate.lease.inner_address }}">{{ certificate.lease.inner_address }}</a>{% if certificate.lease.outer_address %}
|
||||
<a target="_blank" href="http://{{ certificate.lease.inner_address }}">{{ certificate.lease.inner_address }}</a>{% if certificate.lease.outer_address %}
|
||||
from
|
||||
<a target="{{ certificate.lease.outer_address }}" href="https://geoiplookup.net/ip/{{ certificate.lease.outer_address }}">{{ certificate.lease.outer_address }}</a>{% endif %}.
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
<p>
|
||||
<div id="request-{{ request.common_name | replace('@', '--') | replace('.', '-') }}" class="card">
|
||||
<div id="request-{{ request.common_name | replace('@', '--') | replace('.', '-') }}" class="card filterable mt-3"
|
||||
data-keywords="{{ request.common_name }}|">
|
||||
<div class="card-header">
|
||||
{% if certificate.server %}
|
||||
<i class="fa fa-server"></i>
|
||||
@ -62,4 +62,3 @@ curl http://{{ window.location.hostname }}/api/request/{{ request.common_name }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</p>
|
||||
|
@ -1,4 +1,5 @@
|
||||
<div id="certificate-{{ certificate.common_name | replace('@', '--') | replace('.', '-') }}" class="card">
|
||||
<div id="certificate-{{ certificate.common_name | replace('@', '--') | replace('.', '-') }}" class="card filterable mt-3"
|
||||
data-keywords="{{ certificate.common_name }}|">
|
||||
<div class="card-body">
|
||||
<div class="card-header">
|
||||
{% if certificate.server %}
|
||||
@ -62,10 +63,7 @@ openssl ocsp -issuer session.pem -CAfile session.pem \
|
||||
</tbody>
|
||||
</table>
|
||||
</p>
|
||||
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<br/>
|
||||
|
@ -1,5 +1,5 @@
|
||||
<p>
|
||||
<div id="certificate-{{ certificate.common_name | replace('@', '--') | replace('.', '-') }}" class="card">
|
||||
<div id="certificate-{{ certificate.common_name | replace('@', '--') | replace('.', '-') }}" class="card filterable mt-3"
|
||||
data-keywords="{{ certificate.common_name }}|{% if session.authority.tagging %}{% for tag in certificate.tags %}{{ tag.id }}|{% endfor %}{% endif %}{% for key, value in certificate.attributes %}{{ key }}={{ value }}|{% endfor %}">
|
||||
<div class="card-header">
|
||||
{% if certificate.organizational_unit %}
|
||||
<i class="fa fa-folder" aria-hidden="true"></i>
|
||||
@ -121,4 +121,3 @@ openssl ocsp -issuer session.pem -CAfile session.pem \
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</p>
|
||||
|
0
certidude/static/views/snippets.html
Normal file
0
certidude/static/views/snippets.html
Normal file
@ -2,5 +2,5 @@
|
||||
<span data-cn="{{ certificate.common_name }}"
|
||||
title="{{ tag.id }}"
|
||||
class="badge badge-default"
|
||||
onClick="onTagClicked(this);"><i class="fa fa-{{ tag.key }}"></i> {{ tag.value }}</span>
|
||||
onClick="onTagClicked(this);">{{ tag.value }}</span>
|
||||
{% endfor %}
|
||||
|
@ -28,7 +28,7 @@ if [ -e /sys/class/dmi ]; then
|
||||
ARGS="$ARGS&&mem=$(dmidecode -t 17 | grep Size | cut -d ":" -f 2 | cut -d " " -f 2 | paste -sd+ | bc) MB"
|
||||
else
|
||||
ARGS="$ARGS&dmi.product_name=$(cat /proc/cpuinfo | grep '^machine' | head -n 1 | cut -d ":" -f 2 | xargs)"
|
||||
ARGS="$ARGS&mem=$(echo $(cat /proc/meminfo | grep MemTotal | cut -d ":" -f 2 | xargs | cut -d " " -f 1)/1000+1 | bc) MB"
|
||||
ARGS="$ARGS&mem=$(expr $(cat /proc/meminfo | grep MemTotal | cut -d ":" -f 2 | xargs | cut -d " " -f 1) / 1024 + 1 ) MB"
|
||||
fi
|
||||
|
||||
# Submit some stats to CA
|
||||
|
@ -9,6 +9,13 @@ router = ^router\d?\.
|
||||
# use it to include SSH keys, set passwords, etc
|
||||
script =
|
||||
|
||||
# Which subnets are routed to the tunnel
|
||||
subnets = 192.168.0.0/16 172.16.0.0/12 10.0.0.0/8
|
||||
|
||||
# Strongswan IKEv2 proposals
|
||||
ike=aes256-sha384-{{ dhgroup }}!
|
||||
esp=aes128gcm16-aes128gmac-{{ dhgroup }}!
|
||||
|
||||
[tpl-archer-c7]
|
||||
# Title shown in the UI
|
||||
title = TP-Link Archer C7 (Access Point)
|
||||
|
@ -8,7 +8,6 @@
|
||||
limit_conn addr 10;
|
||||
client_body_timeout 5s;
|
||||
client_header_timeout 5s;
|
||||
limit_req_zone $binary_remote_addr zone=api:10m rate=30r/m;
|
||||
limit_conn_zone $binary_remote_addr zone=addr:10m;
|
||||
|
||||
# Backend configuration
|
||||
@ -23,19 +22,10 @@ send_timeout 600;
|
||||
# Don't buffer any messages
|
||||
nchan_message_buffer_length 0;
|
||||
|
||||
# To use CA-s own certificate for HTTPS
|
||||
# To use CA-s own certificate for frontend and mutually authenticated connections
|
||||
ssl_certificate /var/lib/certidude/{{ common_name }}/signed/{{ common_name }}.pem;
|
||||
ssl_certificate_key /var/lib/certidude/{{common_name}}/self_key.pem;
|
||||
|
||||
# To use Let's Encrypt certificates
|
||||
#ssl_certificate /etc/letsencrypt/live/{{common_name}}/fullchain.pem;
|
||||
#ssl_certificate_key /etc/letsencrypt/live/{{common_name}}/privkey.pem;
|
||||
|
||||
# Also run the following to set up Let's Encrypt certificates:
|
||||
#
|
||||
# apt install letsencrypt
|
||||
# certbot certonly -d {{common_name}} --webroot /var/www/html/
|
||||
|
||||
server {
|
||||
# Section for serving insecure HTTP, note that this is suitable for
|
||||
# OCSP, SCEP, CRL-s etc which is already covered by PKI protection mechanisms.
|
||||
@ -47,7 +37,6 @@ server {
|
||||
# Proxy pass to backend
|
||||
location /api/ {
|
||||
proxy_pass http://127.0.1.1:8080/api/;
|
||||
limit_req zone=api burst=5;
|
||||
}
|
||||
|
||||
# Path to static files
|
||||
@ -90,6 +79,15 @@ server {
|
||||
listen 443 ssl http2 default_server;
|
||||
server_name {{ common_name }};
|
||||
|
||||
# To use Let's Encrypt certificates
|
||||
{% if not letsencrypt %}#{% endif %}ssl_certificate {{ letsencrypt_fullchain }};
|
||||
{% if not letsencrypt %}#{% endif %}ssl_certificate_key {{ letsencrypt_privkey }};
|
||||
|
||||
# Also run the following to set up Let's Encrypt certificates:
|
||||
#
|
||||
# apt install letsencrypt
|
||||
# certbot certonly -d {{common_name}} --webroot /var/www/html/
|
||||
|
||||
# HSTS header below should make sure web interface will be accessed over HTTPS only
|
||||
# once it has been configured
|
||||
add_header Strict-Transport-Security "max-age=15768000; includeSubDomains; preload;";
|
||||
@ -97,7 +95,6 @@ server {
|
||||
# Proxy pass to backend
|
||||
location /api/ {
|
||||
proxy_pass http://127.0.1.1:8080/api/;
|
||||
limit_req zone=api burst=5;
|
||||
}
|
||||
|
||||
# Path to static files
|
||||
@ -146,7 +143,6 @@ server {
|
||||
# Proxy pass to backend
|
||||
location /api/ {
|
||||
proxy_pass http://127.0.1.1:8080/api/;
|
||||
limit_req zone=api burst=5;
|
||||
}
|
||||
|
||||
# Long poll
|
||||
|
@ -82,6 +82,7 @@ class DirectoryConnection(object):
|
||||
|
||||
def __exit__(self, type, value, traceback):
|
||||
self.conn.unbind_s()
|
||||
del os.environ["KRB5CCNAME"] # prevent contaminating environment
|
||||
|
||||
|
||||
class ActiveDirectoryUserManager(object):
|
||||
|
@ -2,7 +2,7 @@
|
||||
|
||||
source common.sh
|
||||
|
||||
uci set certidude.@authority[0].trigger=lan
|
||||
sed -e 's/trigger wan/trigger lan/' -i $OVERLAY/etc/config/certidude
|
||||
|
||||
cat << \EOF > $OVERLAY/etc/uci-defaults/40-hostname
|
||||
|
||||
@ -108,11 +108,11 @@ esac
|
||||
|
||||
EOF
|
||||
|
||||
make -C $BUILD/$BASENAME image FILES=$OVERLAY PROFILE=$PROFILE PACKAGES="luci luci-app-commands \
|
||||
make -C $BUILD/$BASENAME image FILES=$OVERLAY PROFILE=$PROFILE PACKAGES="luci \
|
||||
openssl-util curl ca-certificates dropbear \
|
||||
strongswan-mod-kernel-libipsec kmod-tun strongswan-default strongswan-mod-openssl strongswan-mod-curl strongswan-mod-ccm strongswan-mod-gcm \
|
||||
htop iftop tcpdump nmap nano -odhcp6c -odhcpd -dnsmasq \
|
||||
-luci-app-firewall \
|
||||
-pppd -luci-proto-ppp -kmod-ppp -ppp -ppp-mod-pppoe \
|
||||
-kmod-ip6tables -ip6tables -luci-proto-ipv6 -kmod-iptunnel6 -kmod-ipsec6 bc"
|
||||
-kmod-ip6tables -ip6tables -luci-proto-ipv6 -kmod-iptunnel6 -kmod-ipsec6"
|
||||
|
||||
|
@ -45,19 +45,12 @@ config authority
|
||||
|
||||
EOF
|
||||
|
||||
|
||||
cat << EOF > $OVERLAY/etc/uci-defaults/40-disable-ipsec
|
||||
/etc/init.d/ipsec disable
|
||||
EOF
|
||||
|
||||
case $AUTHORITY_CERTIFICATE_ALGORITHM in
|
||||
rsa)
|
||||
echo ": RSA /etc/certidude/authority/$AUTHORITY/host_key.pem" >> $OVERLAY/etc/ipsec.secrets
|
||||
DHGROUP=modp2048
|
||||
;;
|
||||
ec)
|
||||
echo ": ECDSA /etc/certidude/authority/$AUTHORITY/host_key.pem" >> $OVERLAY/etc/ipsec.secrets
|
||||
DHGROUP=ecp384
|
||||
;;
|
||||
*)
|
||||
echo "Unknown algorithm $AUTHORITY_CERTIFICATE_ALGORITHM"
|
||||
@ -96,8 +89,8 @@ conn %default
|
||||
keyingtries=%forever
|
||||
dpdaction=restart
|
||||
closeaction=restart
|
||||
ike=aes256-sha384-ecp384!
|
||||
esp=aes128gcm16-aes128gmac!
|
||||
ike=$IKE
|
||||
esp=$ESP
|
||||
left=%defaultroute
|
||||
leftcert=/etc/certidude/authority/$AUTHORITY/host_cert.pem
|
||||
leftca="$AUTHORITY_CERTIFICATE_DISTINGUISHED_NAME"
|
||||
@ -106,7 +99,7 @@ conn %default
|
||||
conn client-to-site
|
||||
auto=start
|
||||
right="$ROUTER"
|
||||
rightsubnet=0.0.0.0/0
|
||||
rightsubnet="$SUBNETS"
|
||||
leftsourceip=%config
|
||||
leftupdown=/etc/certidude/authority/$AUTHORITY/updown
|
||||
|
||||
|
@ -41,4 +41,4 @@ EOF
|
||||
make -C $BUILD/$BASENAME image FILES=$OVERLAY PROFILE=$PROFILE PACKAGES="openssl-util curl ca-certificates \
|
||||
strongswan-default strongswan-mod-openssl strongswan-mod-curl strongswan-mod-ccm strongswan-mod-gcm htop \
|
||||
iftop tcpdump nmap nano mtr patch diffutils ipset usbutils luci luci-app-mjpg-streamer kmod-video-uvc dropbear \
|
||||
pciutils -dnsmasq -odhcpd -odhcp6c -kmod-ath9k picocom strongswan-mod-kernel-libipsec kmod-tun bc"
|
||||
pciutils -dnsmasq -odhcpd -odhcp6c -kmod-ath9k picocom strongswan-mod-kernel-libipsec kmod-tun"
|
||||
|
@ -7,4 +7,4 @@ AUTHORITY=certidude.@authority[0]
|
||||
[ $ACTION == "ifup" ] || exit 0
|
||||
[ $INTERFACE == "$(uci get $AUTHORITY.trigger)" ] || exit 0
|
||||
|
||||
/usr/bin/certidude-enroll
|
||||
/usr/bin/certidude-enroll > /var/log/certidude.log 2>&1
|
||||
|
@ -1,5 +1,8 @@
|
||||
#!/bin/sh
|
||||
|
||||
set -e
|
||||
set -x
|
||||
|
||||
AUTHORITY=certidude.@authority[0]
|
||||
|
||||
# TODO: iterate over all authorities
|
||||
@ -29,7 +32,6 @@ logger -t certidude -s "Time is now: $(date)"
|
||||
if [ -f $CERTIFICATE_PATH ]; then
|
||||
SERIAL=$(openssl x509 -in $CERTIFICATE_PATH -noout -serial | cut -d "=" -f 2 | tr [A-F] [a-f])
|
||||
logger -t certidude -s "Certificate with serial $SERIAL already exists in $CERTIFICATE_PATH, attempting to bring up VPN tunnel..."
|
||||
ipsec restart
|
||||
exit 0
|
||||
fi
|
||||
|
||||
@ -39,16 +41,20 @@ fi
|
||||
#########################################
|
||||
|
||||
if [ ! -f $KEY_PATH ]; then
|
||||
|
||||
logger -t certidude -s "Generating $KEY_TYPE key for VPN..."
|
||||
|
||||
case $KEY_TYPE in
|
||||
rsa)
|
||||
logger -t certidude -s "Generating $KEY_LENGTH-bit RSA key..."
|
||||
openssl genrsa -out $KEY_PATH.part $KEY_LENGTH
|
||||
openssl rsa -in $KEY_PATH.part -noout
|
||||
;;
|
||||
ec)
|
||||
logger -t certidude -s "Generating $KEY_CURVE ECDSA key..."
|
||||
openssl ecparam -name $KEY_CURVE -genkey -noout -out $KEY_PATH.part
|
||||
;;
|
||||
*)
|
||||
logger -t certidude -s "Unsupported key type $KEY_TYPE"
|
||||
exit 255
|
||||
;;
|
||||
esac
|
||||
mv $KEY_PATH.part $KEY_PATH
|
||||
fi
|
||||
@ -120,4 +126,5 @@ mv $CERTIFICATE_PATH.part $CERTIFICATE_PATH
|
||||
|
||||
# Start services
|
||||
logger -t certidude -s "Starting IPSec IKEv2 daemon..."
|
||||
ipsec restart
|
||||
/etc/init.d/ipsec enable
|
||||
/etc/init.d/ipsec restart
|
||||
|
@ -49,7 +49,7 @@ def client():
|
||||
|
||||
def generate_csr(cn=None):
|
||||
|
||||
public_key, private_key = asymmetric.generate_pair('rsa', bit_size=2048)
|
||||
public_key, private_key = asymmetric.generate_pair('ec', curve="secp384r1")
|
||||
builder = CSRBuilder({ 'common_name': cn }, public_key)
|
||||
request = builder.build(private_key)
|
||||
return pem_armor_csr(request)
|
||||
@ -116,6 +116,10 @@ def clean_server():
|
||||
"/etc/nginx/sites-enabled/certidude.conf",
|
||||
"/etc/nginx/conf.d/tls.conf",
|
||||
"/etc/certidude/server.keytab",
|
||||
"/tmp/sscep/ca.pem",
|
||||
"/tmp/key.pem",
|
||||
"/tmp/req.pem",
|
||||
"/tmp/cert.pem",
|
||||
]
|
||||
|
||||
for filename in files:
|
||||
@ -142,14 +146,19 @@ def clean_server():
|
||||
# Restore initial resolv.conf
|
||||
shutil.copyfile("/etc/resolv.conf.orig", "/etc/resolv.conf")
|
||||
|
||||
def assert_cleanliness():
|
||||
assert os.getuid() == 0, "Environment contaminated, UID: %d" % os.getuid()
|
||||
assert os.getgid() == 0, "Environment contaminated, GID: %d" % os.getgid()
|
||||
assert not os.environ.get("KRB5_KTNAME"), "Environment contaminated, KRB5_KTNAME=%s" % os.environ.get("KRB5_KTNAME")
|
||||
assert not os.environ.get("KRB5CCNAME"), "Environment contaminated, KRB5CCNAME=%s" % os.environ.get("KRB5CCNAME")
|
||||
|
||||
def test_cli_setup_authority():
|
||||
assert os.getuid() == 0, "Run tests as root in a clean VM or container"
|
||||
assert check_output(["/bin/hostname", "-f"]) == b"ca.example.lan\n", "As a safety precaution, unittests only run in a machine whose hostanme -f is ca.example.lan"
|
||||
|
||||
os.system("DEBIAN_FRONTEND=noninteractive apt-get install -qq -y git build-essential python-dev libkrb5-dev")
|
||||
os.system("DEBIAN_FRONTEND=noninteractive apt-get install -qq -y git build-essential python-dev libkrb5-dev samba krb5-user winbind bc")
|
||||
|
||||
assert not os.environ.get("KRB5CCNAME"), "Environment contaminated"
|
||||
assert not os.environ.get("KRB5_KTNAME"), "Environment contaminated"
|
||||
assert_cleanliness()
|
||||
|
||||
# Mock Fedora
|
||||
for util in "/usr/bin/chcon", "/usr/bin/dnf", "/usr/bin/update-ca-trust", "/usr/sbin/dmidecode":
|
||||
@ -196,34 +205,23 @@ def test_cli_setup_authority():
|
||||
assert const.HOSTNAME == "ca"
|
||||
assert const.DOMAIN == "example.lan"
|
||||
|
||||
# Bootstrap authority
|
||||
bootstrap_pid = os.fork() # TODO: this shouldn't be necessary
|
||||
if not bootstrap_pid:
|
||||
assert os.getuid() == 0 and os.getgid() == 0
|
||||
result = runner.invoke(cli, ["setup", "authority"])
|
||||
assert not result.exception, result.output
|
||||
return
|
||||
else:
|
||||
os.waitpid(bootstrap_pid, 0)
|
||||
os.system("certidude setup authority --elliptic-curve")
|
||||
|
||||
assert os.getuid() == 0 and os.getgid() == 0, "Environment contaminated"
|
||||
assert_cleanliness()
|
||||
|
||||
|
||||
# Make sure nginx is running
|
||||
assert os.system("nginx -t") == 0, "invalid nginx configuration"
|
||||
os.system("service nginx restart")
|
||||
assert os.path.exists("/run/nginx.pid"), "nginx wasn't started up properly"
|
||||
|
||||
# Make sure we generated legit CA certificate
|
||||
from certidude import config, authority, auth, user
|
||||
from certidude import config, authority, user
|
||||
assert authority.certificate.serial_number >= 0x100000000000000000000000000000000000000
|
||||
assert authority.certificate.serial_number <= 0xfffffffffffffffffffffffffffffffffffffff
|
||||
assert authority.certificate["tbs_certificate"]["validity"]["not_before"].native.replace(tzinfo=None) < datetime.utcnow()
|
||||
assert authority.certificate["tbs_certificate"]["validity"]["not_after"].native.replace(tzinfo=None) > datetime.utcnow() + timedelta(days=7000)
|
||||
assert authority.server_flags("lauri@fedora-123") == False
|
||||
assert authority.server_flags("fedora-123") == False
|
||||
assert authority.server_flags("vpn.example.lan") == True
|
||||
assert authority.server_flags("lauri@a.b.c") == False
|
||||
assert authority.certificate["tbs_certificate"]["validity"]["not_before"].native.replace(tzinfo=None) < datetime.utcnow()
|
||||
assert authority.public_key.algorithm == "ec"
|
||||
|
||||
# Generate garbage
|
||||
with open("/var/lib/certidude/ca.example.lan/bla", "w") as fh:
|
||||
@ -237,19 +235,18 @@ def test_cli_setup_authority():
|
||||
|
||||
# Start server before any signing operations are performed
|
||||
config.CERTIFICATE_RENEWAL_ALLOWED = True
|
||||
assert_cleanliness()
|
||||
|
||||
server_pid = os.fork()
|
||||
if not server_pid:
|
||||
# Fork to prevent umask, setuid, setgid side effects
|
||||
result = runner.invoke(cli, ['serve'])
|
||||
assert not result.exception, result.output
|
||||
return
|
||||
|
||||
sleep(1) # Wait for serve to start up
|
||||
import requests
|
||||
for j in range(0,10):
|
||||
r = requests.get("http://ca.example.lan/api/")
|
||||
if r.status_code != 502:
|
||||
break
|
||||
sleep(1)
|
||||
assert r.status_code == 401, "Timed out starting up the API backend"
|
||||
|
||||
# TODO: check that port 8080 is listening, otherwise app probably crashed
|
||||
|
||||
import requests
|
||||
|
||||
# Test CA certificate fetch
|
||||
buf = open("/var/lib/certidude/ca.example.lan/ca_cert.pem").read()
|
||||
@ -621,10 +618,7 @@ def test_cli_setup_authority():
|
||||
assert r.status_code == 415 # invalid media type
|
||||
|
||||
r = client().simulate_get("/api/", headers={"Authorization":usertoken})
|
||||
assert r.status_code == 200
|
||||
assert r.headers.get('content-type').startswith("application/json")
|
||||
assert r.json, r.text
|
||||
assert not r.json.get("authority"), r.text # No permissions to admin
|
||||
assert r.status_code == 403 # regular users have no access
|
||||
|
||||
r = client().simulate_get("/api/", headers={"Authorization":admintoken})
|
||||
assert r.status_code == 200
|
||||
@ -665,7 +659,6 @@ def test_cli_setup_authority():
|
||||
assert not result.exception, result.output # client conf already exists, remove to regenerate
|
||||
|
||||
with open("/etc/certidude/client.conf", "a") as fh:
|
||||
fh.write("insecure = true\n")
|
||||
fh.write("autosign = false\n")
|
||||
|
||||
assert not os.path.exists("/etc/certidude/authority/ca.example.lan/server_cert.pem")
|
||||
@ -721,7 +714,6 @@ def test_cli_setup_authority():
|
||||
assert not result.exception, result.output # client conf already exists, remove to regenerate
|
||||
|
||||
with open("/etc/certidude/client.conf", "a") as fh:
|
||||
fh.write("insecure = true\n")
|
||||
fh.write("autosign = false\n")
|
||||
|
||||
assert not os.path.exists("/etc/certidude/authority/ca.example.lan/server_cert.pem")
|
||||
@ -761,9 +753,6 @@ def test_cli_setup_authority():
|
||||
result = runner.invoke(cli, ['setup', 'openvpn', 'client', "-cn", "roadwarrior1", "ca.example.lan", "vpn.example.lan"])
|
||||
assert not result.exception, result.output # client conf already exists, remove to regenerate
|
||||
|
||||
with open("/etc/certidude/client.conf", "a") as fh:
|
||||
fh.write("insecure = true\n")
|
||||
|
||||
result = runner.invoke(cli, ["enroll", "--skip-self", "--no-wait"])
|
||||
assert not result.exception, result.output
|
||||
assert not os.path.exists("/run/certidude/ca.example.lan.pid"), result.output
|
||||
@ -778,9 +767,6 @@ def test_cli_setup_authority():
|
||||
result = runner.invoke(cli, ['setup', 'openvpn', 'networkmanager', "-cn", "roadwarrior3", "ca.example.lan", "vpn.example.lan"])
|
||||
assert not result.exception, result.output
|
||||
|
||||
with open("/etc/certidude/client.conf", "a") as fh:
|
||||
fh.write("insecure = true\n")
|
||||
|
||||
result = runner.invoke(cli, ["enroll", "--skip-self", "--no-wait"])
|
||||
assert not result.exception, result.output
|
||||
assert not os.path.exists("/run/certidude/ca.example.lan.pid"), result.output
|
||||
@ -947,7 +933,6 @@ def test_cli_setup_authority():
|
||||
assert not os.path.exists("/var/lib/certidude/ca.example.lan/signed/ipsec.example.lan.pem")
|
||||
|
||||
with open("/etc/certidude/client.conf", "a") as fh:
|
||||
fh.write("insecure = true\n")
|
||||
fh.write("autosign = false\n")
|
||||
|
||||
result = runner.invoke(cli, ["enroll", "--skip-self", "--no-wait"])
|
||||
@ -985,9 +970,6 @@ def test_cli_setup_authority():
|
||||
result = runner.invoke(cli, ['setup', 'strongswan', 'client', "-cn", "roadwarrior2", "ca.example.lan", "ipsec.example.lan"])
|
||||
assert not result.exception, result.output # client conf already exists, remove to regenerate
|
||||
|
||||
with open("/etc/certidude/client.conf", "a") as fh:
|
||||
fh.write("insecure = true\n")
|
||||
|
||||
result = runner.invoke(cli, ["enroll", "--skip-self", "--no-wait"])
|
||||
assert not result.exception, result.output
|
||||
assert not os.path.exists("/run/certidude/ca.example.lan.pid"), result.output
|
||||
@ -1001,9 +983,6 @@ def test_cli_setup_authority():
|
||||
result = runner.invoke(cli, ['setup', 'strongswan', 'networkmanager', "-cn", "roadwarrior4", "ca.example.lan", "ipsec.example.lan"])
|
||||
assert not result.exception, result.output
|
||||
|
||||
with open("/etc/certidude/client.conf", "a") as fh:
|
||||
fh.write("insecure = true\n")
|
||||
|
||||
result = runner.invoke(cli, ["enroll", "--skip-self", "--no-wait"])
|
||||
assert not result.exception, result.output
|
||||
assert not os.path.exists("/run/certidude/ca.example.lan.pid"), result.output
|
||||
@ -1075,15 +1054,9 @@ def test_cli_setup_authority():
|
||||
### Switch to Kerberos/LDAP auth ###
|
||||
####################################
|
||||
|
||||
# Shut down current instance
|
||||
os.kill(server_pid, 15)
|
||||
requests.get("http://ca.example.lan/api/")
|
||||
# sleep(2)
|
||||
# os.kill(server_pid, 9) # TODO: Figure out why doesn't shut down gracefully
|
||||
os.waitpid(server_pid, 0)
|
||||
os.system("systemctl stop certidude")
|
||||
|
||||
# Install packages
|
||||
os.system("apt-get install -y samba krb5-user winbind bc")
|
||||
clean_server()
|
||||
|
||||
# Bootstrap domain controller here,
|
||||
@ -1114,17 +1087,6 @@ def test_cli_setup_authority():
|
||||
else:
|
||||
assert False, "Samba startup timed out"
|
||||
|
||||
# Bootstrap authority
|
||||
bootstrap_pid = os.fork() # TODO: this shouldn't be necessary
|
||||
if not bootstrap_pid:
|
||||
result = runner.invoke(cli, ["setup", "authority", "--skip-packages", "--elliptic-curve"])
|
||||
assert not result.exception, result.output
|
||||
return
|
||||
else:
|
||||
os.waitpid(bootstrap_pid, 0)
|
||||
|
||||
assert os.getuid() == 0 and os.getgid() == 0, "Environment contaminated"
|
||||
|
||||
# (re)auth against DC
|
||||
assert os.system("kdestroy") == 0
|
||||
assert not os.path.exists("/tmp/krb5cc_0")
|
||||
@ -1144,45 +1106,55 @@ def test_cli_setup_authority():
|
||||
else:
|
||||
os.waitpid(spn_pid, 0)
|
||||
|
||||
assert_cleanliness()
|
||||
r = requests.get("http://ca.example.lan/api/")
|
||||
assert r.status_code == 502, r.text
|
||||
|
||||
|
||||
|
||||
# Bootstrap authority
|
||||
assert not os.path.exists("/var/lib/certidude/ca.example.lan/ca_key.pem")
|
||||
os.system("certidude setup authority --skip-packages")
|
||||
|
||||
|
||||
# Make modifications to /etc/certidude/server.conf so
|
||||
# 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")
|
||||
os.system("sed -e 's/ldap uri = ldap:.*/ldap uri = ldap:\\/\\/ca.example.lan/g' -i /etc/certidude/server.conf")
|
||||
os.system("sed -e 's/dc1/ca/g' -i /etc/cron.hourly/certidude")
|
||||
os.system("sed -e 's/autosign subnets =.*/autosign subnets =/g' -i /etc/certidude/server.conf")
|
||||
os.system("sed -e 's/machine enrollment subnets =.*/machine enrollment subnets = 0.0.0.0\\/0/g' -i /etc/certidude/server.conf")
|
||||
os.system("sed -e 's/scep subnets =.*/scep subnets = 0.0.0.0\\/0/g' -i /etc/certidude/server.conf")
|
||||
os.system("sed -e 's/ocsp subnets =.*/ocsp subnets =/g' -i /etc/certidude/server.conf")
|
||||
os.system("sed -e 's/crl subnets =.*/crl subnets =/g' -i /etc/certidude/server.conf")
|
||||
os.system("sed -e 's/address = certificates@example.lan/address =/g' -i /etc/certidude/server.conf")
|
||||
from certidude.common import pip
|
||||
|
||||
# Update server credential cache
|
||||
os.system("sed -e 's/dc1/ca/g' -i /etc/cron.hourly/certidude")
|
||||
with open("/etc/cron.hourly/certidude") as fh:
|
||||
cronjob = fh.read()
|
||||
assert "ldap/ca.example.lan" in cronjob, cronjob
|
||||
os.system("/etc/cron.hourly/certidude")
|
||||
assert os.system("/etc/cron.hourly/certidude") == 0
|
||||
assert os.path.exists("/run/certidude/krb5cc")
|
||||
assert os.stat("/run/certidude/krb5cc").st_uid != 0, "Incorrect persmissions for /run/certidude/krb5cc"
|
||||
|
||||
server_pid = os.fork() # Fork to prevent environment contamination
|
||||
if not server_pid:
|
||||
# Apply /etc/certidude/server.conf changes
|
||||
reload(config)
|
||||
reload(user)
|
||||
reload(auth)
|
||||
assert isinstance(user.User.objects, user.ActiveDirectoryUserManager), user.User.objects
|
||||
# Start certidude backend
|
||||
assert os.system("systemctl restart certidude") == 0
|
||||
assert_cleanliness()
|
||||
|
||||
result = runner.invoke(cli, ['users'])
|
||||
assert not result.exception, result.output
|
||||
assert "user;userbot;User;Bot;userbot@example.lan" in result.output
|
||||
assert "admin;adminbot;Admin;Bot;adminbot@example.lan" in result.output
|
||||
assert "admin;Administrator;Administrator;;Administrator@example.lan" in result.output
|
||||
# Apply /etc/certidude/server.conf changes
|
||||
reload(config)
|
||||
reload(user)
|
||||
reload(authority)
|
||||
|
||||
assert authority.public_key.algorithm == "rsa"
|
||||
assert isinstance(user.User.objects, user.ActiveDirectoryUserManager), user.User.objects
|
||||
|
||||
result = runner.invoke(cli, ['users'])
|
||||
assert not result.exception, result.output
|
||||
assert "user;userbot;User;Bot;userbot@example.lan" in result.output
|
||||
assert "admin;adminbot;Admin;Bot;adminbot@example.lan" in result.output
|
||||
assert "admin;Administrator;Administrator;;Administrator@example.lan" in result.output
|
||||
|
||||
result = runner.invoke(cli, ['serve'])
|
||||
assert not result.exception, result.output
|
||||
return
|
||||
|
||||
# Wait for serve to start up
|
||||
for j in range(0,10):
|
||||
@ -1215,10 +1187,15 @@ def test_cli_setup_authority():
|
||||
### Kerberos auth ###
|
||||
#####################
|
||||
|
||||
# TODO: pip install requests-kerberos
|
||||
# TODO: pip3 install requests-kerberos
|
||||
assert_cleanliness()
|
||||
|
||||
assert os.stat("/run/certidude/krb5cc").st_uid != 0, "Incorrect persmissions for /run/certidude/krb5cc"
|
||||
|
||||
from requests_kerberos import HTTPKerberosAuth, OPTIONAL
|
||||
auth = HTTPKerberosAuth(mutual_authentication=OPTIONAL, force_preemptive=True)
|
||||
|
||||
|
||||
# Test Kerberos auth
|
||||
r = requests.get("http://ca.example.lan/api/")
|
||||
assert r.status_code == 401, r.text
|
||||
@ -1229,6 +1206,8 @@ def test_cli_setup_authority():
|
||||
r = requests.get("http://ca.example.lan/api/", headers={"Authorization": "Negotiate TlRMTVNTUAABAAAAl4II4gAAAAAAAAAAAAAAAAAAAAAKADk4AAAADw=="})
|
||||
assert r.status_code == 400, r.text
|
||||
assert "Unsupported authentication mechanism (NTLM" in r.text
|
||||
assert os.system("echo S4l4k4l4 | kinit administrator") == 0
|
||||
assert os.stat("/run/certidude/krb5cc").st_uid != 0, "Incorrect persmissions for /run/certidude/krb5cc"
|
||||
r = requests.get("http://ca.example.lan/api/", auth=auth)
|
||||
assert r.status_code == 200, r.text
|
||||
|
||||
@ -1247,14 +1226,15 @@ def test_cli_setup_authority():
|
||||
# curl http://ca.example.lan/api/ -u adminbot:S4l4k4l4 -H "User-agent: Android" -H "Referer: http://ca.example.lan"
|
||||
r = requests.get("http://ca.example.lan/api/",
|
||||
headers={"Authorization":usertoken, "User-Agent": "Android", "Referer":"http://ca.example.lan/"})
|
||||
#assert r.status_code == 200, r.text # TODO: Fails with 500 in Travis
|
||||
assert r.status_code == 400, r.text
|
||||
assert "expected Negotiate" in r.text, r.text
|
||||
|
||||
|
||||
###########################
|
||||
### Machine keytab auth ###
|
||||
###########################
|
||||
|
||||
assert not os.environ.get("KRB5_KTNAME"), "Environment contaminated"
|
||||
assert_cleanliness()
|
||||
|
||||
mach_pid = os.fork() # Otherwise results in Terminated, needs investigation why
|
||||
if not mach_pid:
|
||||
@ -1264,9 +1244,6 @@ def test_cli_setup_authority():
|
||||
result = runner.invoke(cli, ['setup', 'openvpn', 'client', "-cn", "somethingelse", "ca.example.lan", "vpn.example.lan"])
|
||||
assert not result.exception, result.output
|
||||
|
||||
with open("/etc/certidude/client.conf", "a") as fh:
|
||||
fh.write("insecure = true\n")
|
||||
|
||||
result = runner.invoke(cli, ["enroll", "--skip-self", "--no-wait", "--kerberos"])
|
||||
assert result.exception, result.output # Bad request 400
|
||||
|
||||
@ -1276,9 +1253,6 @@ def test_cli_setup_authority():
|
||||
result = runner.invoke(cli, ['setup', 'openvpn', 'client', "-cn", "ca", "ca.example.lan", "vpn.example.lan"])
|
||||
assert not result.exception, result.output
|
||||
|
||||
with open("/etc/certidude/client.conf", "a") as fh:
|
||||
fh.write("insecure = true\n")
|
||||
|
||||
result = runner.invoke(cli, ["enroll", "--skip-self", "--no-wait", "--kerberos"])
|
||||
assert not result.exception, result.output
|
||||
assert "Writing certificate to:" in result.output, result.output
|
||||
@ -1291,6 +1265,8 @@ def test_cli_setup_authority():
|
||||
### SCEP tests ###
|
||||
##################
|
||||
|
||||
assert not os.path.exists("/tmp/sscep/ca.pem")
|
||||
|
||||
if not os.path.exists("/tmp/sscep"):
|
||||
assert not os.system("git clone https://github.com/certnanny/sscep /tmp/sscep")
|
||||
if not os.path.exists("/tmp/sscep/sscep_dyn"):
|
||||
@ -1302,6 +1278,7 @@ def test_cli_setup_authority():
|
||||
assert not os.system("echo '.\n.\n.\n.\nGateway\ntest8\n\n\n\n' | openssl req -new -sha256 -key /tmp/key.pem -out /tmp/req.pem")
|
||||
assert not os.system("/tmp/sscep/sscep_dyn enroll -c /tmp/sscep/ca.pem -u http://ca.example.lan/cgi-bin/pkiclient.exe -k /tmp/key.pem -r /tmp/req.pem -l /tmp/cert.pem")
|
||||
# TODO: test e-mails at this point
|
||||
# TODO: add strongswan scep client tests here
|
||||
|
||||
|
||||
###################
|
||||
@ -1314,12 +1291,7 @@ def test_cli_setup_authority():
|
||||
result = runner.invoke(cli, ['expire'])
|
||||
assert not result.exception, result.output
|
||||
|
||||
# Shut down server
|
||||
assert os.path.exists("/proc/%d" % server_pid)
|
||||
os.kill(server_pid, 15)
|
||||
# sleep(2)
|
||||
# os.kill(server_pid, 9)
|
||||
os.waitpid(server_pid, 0)
|
||||
assert os.system("systemctl stop certidude") == 0
|
||||
|
||||
# Note: STORAGE_PATH was mangled above, hence it's /tmp not /var/lib/certidude
|
||||
assert open("/etc/apparmor.d/local/usr.lib.ipsec.charon").read() == \
|
||||
|
Loading…
Reference in New Issue
Block a user