Several updates #4
* Improved offline install docs * Migrated token mechanism backend to SQL * Preliminary token mechanism frontend integration * Add clock skew tolerance for OCSP * Add 'ldap computer filter' support for Kerberized machine enroll * Include OCSP and CRL URL-s in certificates, controlled by profile.conf * Better certificate extension handling * Place DH parameters file in /etc/ssl/dhparam.pem * Always talk to CA over port 8443 for 'certidude enroll' * Hardened frontend nginx config * Separate log files for frontend nginx * Better provisioning heuristics * Add sample site.sh config for LEDE image builder * Add more device profiles for LEDE image builder * Various bugfixes and improvements
18
README.rst
@ -336,26 +336,28 @@ To uninstall:
|
|||||||
Offline install
|
Offline install
|
||||||
---------------
|
---------------
|
||||||
|
|
||||||
To set up certificate authority in an isolated environment use a
|
To prepare packages for offline installation use following snippet on a
|
||||||
vanilla Ubuntu 16.04 or container to collect the artifacts:
|
vanilla Ubuntu 16.04 or container:
|
||||||
|
|
||||||
.. code:: bash
|
.. code:: bash
|
||||||
|
|
||||||
|
rm -fv /var/cache/apt/archives/*.deb /var/cache/certidude/wheels/*.whl
|
||||||
|
apt install --download-only python3-pip
|
||||||
|
pip3 wheel --wheel-dir=/var/cache/certidude/wheels -r requirements.txt
|
||||||
|
pip3 wheel --wheel-dir=/var/cache/certidude/wheels .
|
||||||
|
tar -cf certidude-client.tar /var/cache/certidude/wheels
|
||||||
add-apt-repository -y ppa:nginx/stable
|
add-apt-repository -y ppa:nginx/stable
|
||||||
apt-get update -q
|
apt-get update -q
|
||||||
rm -fv /var/cache/apt/archives/*.deb /var/cache/certidude/wheels/*.whl
|
|
||||||
apt install --download-only python3-markdown python3-pyxattr python3-jinja2 python3-cffi software-properties-common libnginx-mod-nchan nginx-full
|
apt install --download-only python3-markdown python3-pyxattr python3-jinja2 python3-cffi software-properties-common libnginx-mod-nchan nginx-full
|
||||||
pip3 wheel --wheel-dir=/var/cache/certidude/wheels -r requirements.txt
|
|
||||||
pip3 wheel --wheel-dir=/var/cache/certidude/wheels falcon humanize ipaddress simplepam user-agents python-ldap gssapi
|
pip3 wheel --wheel-dir=/var/cache/certidude/wheels falcon humanize ipaddress simplepam user-agents python-ldap gssapi
|
||||||
pip3 wheel --wheel-dir=/var/cache/certidude/wheels .
|
tar -cf certidude-server.tar /var/lib/certidude/assets/ /var/cache/apt/archives/ /var/cache/certidude/wheels
|
||||||
tar -cf certidude-assets.tar /var/lib/certidude/assets/ /var/cache/apt/archives/ /var/cache/certidude/wheels
|
|
||||||
|
|
||||||
Transfer certidude-artifacts.tar to the target machine and execute:
|
Transfer certidude-server.tar or certidude-client.tar to the target machine and execute:
|
||||||
|
|
||||||
.. code:: bash
|
.. code:: bash
|
||||||
|
|
||||||
rm -fv /var/cache/apt/archives/*.deb /var/cache/certidude/wheels/*.whl
|
rm -fv /var/cache/apt/archives/*.deb /var/cache/certidude/wheels/*.whl
|
||||||
tar -xvf certidude-artifacts.tar -C /
|
tar -xvf certidude-*.tar -C /
|
||||||
dpkg -i /var/cache/apt/archives/*.deb
|
dpkg -i /var/cache/apt/archives/*.deb
|
||||||
pip3 install --use-wheel --no-index --find-links /var/cache/certidude/wheels/*.whl
|
pip3 install --use-wheel --no-index --find-links /var/cache/certidude/wheels/*.whl
|
||||||
|
|
||||||
|
@ -17,6 +17,7 @@ class NormalizeMiddleware(object):
|
|||||||
|
|
||||||
def certidude_app(log_handlers=[]):
|
def certidude_app(log_handlers=[]):
|
||||||
from certidude import authority, config
|
from certidude import authority, config
|
||||||
|
from certidude.tokens import TokenManager
|
||||||
from .signed import SignedCertificateDetailResource
|
from .signed import SignedCertificateDetailResource
|
||||||
from .request import RequestListResource, RequestDetailResource
|
from .request import RequestListResource, RequestDetailResource
|
||||||
from .lease import LeaseResource, LeaseDetailResource
|
from .lease import LeaseResource, LeaseDetailResource
|
||||||
@ -36,10 +37,20 @@ def certidude_app(log_handlers=[]):
|
|||||||
app.add_route("/api/signed/{cn}/", SignedCertificateDetailResource(authority))
|
app.add_route("/api/signed/{cn}/", SignedCertificateDetailResource(authority))
|
||||||
app.add_route("/api/request/{cn}/", RequestDetailResource(authority))
|
app.add_route("/api/request/{cn}/", RequestDetailResource(authority))
|
||||||
app.add_route("/api/request/", RequestListResource(authority))
|
app.add_route("/api/request/", RequestListResource(authority))
|
||||||
app.add_route("/api/", SessionResource(authority))
|
|
||||||
|
|
||||||
|
token_resource = None
|
||||||
|
token_manager = None
|
||||||
if config.USER_ENROLLMENT_ALLOWED: # TODO: add token enable/disable flag for config
|
if config.USER_ENROLLMENT_ALLOWED: # TODO: add token enable/disable flag for config
|
||||||
app.add_route("/api/token/", TokenResource(authority))
|
if config.TOKEN_BACKEND == "sql":
|
||||||
|
token_manager = TokenManager(config.TOKEN_DATABASE)
|
||||||
|
token_resource = TokenResource(authority, token_manager)
|
||||||
|
app.add_route("/api/token/", token_resource)
|
||||||
|
elif not config.TOKEN_BACKEND:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
raise NotImplementedError("Token backend '%s' not supported" % config.TOKEN_BACKEND)
|
||||||
|
|
||||||
|
app.add_route("/api/", SessionResource(authority, token_manager))
|
||||||
|
|
||||||
# Extended attributes for scripting etc.
|
# Extended attributes for scripting etc.
|
||||||
app.add_route("/api/signed/{cn}/attr/", AttributeResource(authority, namespace="machine"))
|
app.add_route("/api/signed/{cn}/attr/", AttributeResource(authority, namespace="machine"))
|
||||||
|
@ -11,4 +11,5 @@ class LogResource(RelationalMixin):
|
|||||||
@authorize_admin
|
@authorize_admin
|
||||||
def on_get(self, req, resp):
|
def on_get(self, req, resp):
|
||||||
# TODO: Add last id parameter
|
# TODO: Add last id parameter
|
||||||
return self.iterfetch("select * from log order by created desc")
|
return self.iterfetch("select * from log order by created desc limit ?",
|
||||||
|
req.get_param_as_int("limit"))
|
||||||
|
@ -4,8 +4,8 @@ import os
|
|||||||
from asn1crypto.util import timezone
|
from asn1crypto.util import timezone
|
||||||
from asn1crypto import ocsp
|
from asn1crypto import ocsp
|
||||||
from base64 import b64decode
|
from base64 import b64decode
|
||||||
from certidude import config
|
from certidude import config, const
|
||||||
from datetime import datetime
|
from datetime import datetime, timedelta
|
||||||
from oscrypto import asymmetric
|
from oscrypto import asymmetric
|
||||||
from .utils import AuthorityHandler
|
from .utils import AuthorityHandler
|
||||||
from .utils.firewall import whitelist_subnets
|
from .utils.firewall import whitelist_subnets
|
||||||
@ -88,7 +88,8 @@ class OCSPResource(AuthorityHandler):
|
|||||||
'serial_number': serial,
|
'serial_number': serial,
|
||||||
},
|
},
|
||||||
'cert_status': status,
|
'cert_status': status,
|
||||||
'this_update': now,
|
'this_update': now - const.CLOCK_SKEW_TOLERANCE,
|
||||||
|
'next_update': now + timedelta(minutes=15) + const.CLOCK_SKEW_TOLERANCE,
|
||||||
'single_extensions': []
|
'single_extensions': []
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -10,6 +10,7 @@ from base64 import b64decode
|
|||||||
from certidude import config, push, errors
|
from certidude import config, push, errors
|
||||||
from certidude.decorators import csrf_protection, MyEncoder
|
from certidude.decorators import csrf_protection, MyEncoder
|
||||||
from certidude.profile import SignatureProfile
|
from certidude.profile import SignatureProfile
|
||||||
|
from certidude.user import DirectoryConnection
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from oscrypto import asymmetric
|
from oscrypto import asymmetric
|
||||||
from oscrypto.errors import SignatureError
|
from oscrypto.errors import SignatureError
|
||||||
@ -84,13 +85,28 @@ class RequestListResource(AuthorityHandler):
|
|||||||
"Bad request",
|
"Bad request",
|
||||||
"Common name %s differs from Kerberos credential %s!" % (common_name, machine))
|
"Common name %s differs from Kerberos credential %s!" % (common_name, machine))
|
||||||
|
|
||||||
# Automatic enroll with Kerberos machine cerdentials
|
hit = False
|
||||||
resp.set_header("Content-Type", "application/x-pem-file")
|
with DirectoryConnection() as conn:
|
||||||
cert, resp.body = self.authority._sign(csr, body,
|
ft = config.LDAP_COMPUTER_FILTER % ("%s$" % machine)
|
||||||
profile=config.PROFILES["rw"], overwrite=overwrite_allowed)
|
attribs = "cn",
|
||||||
logger.info("Automatically enrolled Kerberos authenticated machine %s from %s",
|
r = conn.search_s(config.LDAP_BASE, 2, ft, attribs)
|
||||||
machine, req.context.get("remote_addr"))
|
for dn, entry in r:
|
||||||
return
|
if not dn:
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
hit = True
|
||||||
|
break
|
||||||
|
|
||||||
|
if hit:
|
||||||
|
# Automatic enroll with Kerberos machine cerdentials
|
||||||
|
resp.set_header("Content-Type", "application/x-pem-file")
|
||||||
|
cert, resp.body = self.authority._sign(csr, body,
|
||||||
|
profile=config.PROFILES["rw"], overwrite=overwrite_allowed)
|
||||||
|
logger.info("Automatically enrolled Kerberos authenticated machine %s (%s) from %s",
|
||||||
|
machine, dn, req.context.get("remote_addr"))
|
||||||
|
return
|
||||||
|
else:
|
||||||
|
logger.error("Kerberos authenticated machine %s didn't fit the 'ldap computer filter' criteria %s" % (machine, ft))
|
||||||
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
@ -18,9 +18,13 @@ class CertificateAuthorityResource(object):
|
|||||||
resp.stream = open(config.AUTHORITY_CERTIFICATE_PATH, "rb")
|
resp.stream = open(config.AUTHORITY_CERTIFICATE_PATH, "rb")
|
||||||
resp.append_header("Content-Type", "application/x-x509-ca-cert")
|
resp.append_header("Content-Type", "application/x-x509-ca-cert")
|
||||||
resp.append_header("Content-Disposition", "attachment; filename=%s.crt" %
|
resp.append_header("Content-Disposition", "attachment; filename=%s.crt" %
|
||||||
const.HOSTNAME.encode("ascii"))
|
const.HOSTNAME)
|
||||||
|
|
||||||
class SessionResource(AuthorityHandler):
|
class SessionResource(AuthorityHandler):
|
||||||
|
def __init__(self, authority, token_manager):
|
||||||
|
AuthorityHandler.__init__(self, authority)
|
||||||
|
self.token_manager = token_manager
|
||||||
|
|
||||||
@csrf_protection
|
@csrf_protection
|
||||||
@serialize
|
@serialize
|
||||||
@login_required
|
@login_required
|
||||||
@ -97,7 +101,7 @@ class SessionResource(AuthorityHandler):
|
|||||||
signer_username = None
|
signer_username = None
|
||||||
|
|
||||||
# TODO: dedup
|
# TODO: dedup
|
||||||
yield dict(
|
serialized = dict(
|
||||||
serial = "%x" % cert.serial_number,
|
serial = "%x" % cert.serial_number,
|
||||||
organizational_unit = cert.subject.native.get("organizational_unit_name"),
|
organizational_unit = cert.subject.native.get("organizational_unit_name"),
|
||||||
common_name = common_name,
|
common_name = common_name,
|
||||||
@ -109,12 +113,37 @@ class SessionResource(AuthorityHandler):
|
|||||||
lease = lease,
|
lease = lease,
|
||||||
tags = tags,
|
tags = tags,
|
||||||
attributes = attributes or None,
|
attributes = attributes or None,
|
||||||
extensions = dict([
|
responder_url = None
|
||||||
(e["extn_id"].native, e["extn_value"].native)
|
|
||||||
for e in cert["tbs_certificate"]["extensions"]
|
|
||||||
if e["extn_id"].native in ("extended_key_usage",)])
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
for e in cert["tbs_certificate"]["extensions"].native:
|
||||||
|
if e["extn_id"] == "key_usage":
|
||||||
|
serialized["key_usage"] = e["extn_value"]
|
||||||
|
elif e["extn_id"] == "extended_key_usage":
|
||||||
|
serialized["extended_key_usage"] = e["extn_value"]
|
||||||
|
elif e["extn_id"] == "basic_constraints":
|
||||||
|
serialized["basic_constraints"] = e["extn_value"]
|
||||||
|
elif e["extn_id"] == "crl_distribution_points":
|
||||||
|
for c in e["extn_value"]:
|
||||||
|
serialized["revoked_url"] = c["distribution_point"]
|
||||||
|
break
|
||||||
|
serialized["extended_key_usage"] = e["extn_value"]
|
||||||
|
elif e["extn_id"] == "authority_information_access":
|
||||||
|
for a in e["extn_value"]:
|
||||||
|
if a["access_method"] == "ocsp":
|
||||||
|
serialized["responder_url"] = a["access_location"]
|
||||||
|
else:
|
||||||
|
raise NotImplementedError("Don't know how to handle AIA access method %s" % a["access_method"])
|
||||||
|
elif e["extn_id"] == "authority_key_identifier":
|
||||||
|
pass
|
||||||
|
elif e["extn_id"] == "key_identifier":
|
||||||
|
pass
|
||||||
|
elif e["extn_id"] == "subject_alt_name":
|
||||||
|
serialized["subject_alt_name"], = e["extn_value"]
|
||||||
|
else:
|
||||||
|
raise NotImplementedError("Don't know how to handle extension %s" % e["extn_id"])
|
||||||
|
yield serialized
|
||||||
|
|
||||||
logger.info("Logged in authority administrator %s from %s with %s" % (
|
logger.info("Logged in authority administrator %s from %s with %s" % (
|
||||||
req.context.get("user"), req.context.get("remote_addr"), req.context.get("user_agent")))
|
req.context.get("user"), req.context.get("remote_addr"), req.context.get("user_agent")))
|
||||||
return dict(
|
return dict(
|
||||||
@ -130,10 +159,12 @@ class SessionResource(AuthorityHandler):
|
|||||||
routers = [j[0] for j in self.authority.list_signed(
|
routers = [j[0] for j in self.authority.list_signed(
|
||||||
common_name=config.SERVICE_ROUTERS)]
|
common_name=config.SERVICE_ROUTERS)]
|
||||||
),
|
),
|
||||||
|
builder = dict(
|
||||||
|
profiles = config.IMAGE_BUILDER_PROFILES or None
|
||||||
|
),
|
||||||
authority = dict(
|
authority = dict(
|
||||||
builder = dict(
|
hostname = const.FQDN,
|
||||||
profiles = config.IMAGE_BUILDER_PROFILES
|
tokens = self.token_manager.list() if self.token_manager else None,
|
||||||
),
|
|
||||||
tagging = [dict(name=t[0], type=t[1], title=t[2]) for t in config.TAG_TYPES],
|
tagging = [dict(name=t[0], type=t[1], title=t[2]) for t in config.TAG_TYPES],
|
||||||
lease = dict(
|
lease = dict(
|
||||||
offline = 600, # Seconds from last seen activity to consider lease offline, OpenVPN reneg-sec option
|
offline = 600, # Seconds from last seen activity to consider lease offline, OpenVPN reneg-sec option
|
||||||
@ -145,32 +176,39 @@ class SessionResource(AuthorityHandler):
|
|||||||
distinguished_name = cert_to_dn(self.authority.certificate),
|
distinguished_name = cert_to_dn(self.authority.certificate),
|
||||||
md5sum = hashlib.md5(self.authority.certificate_buf).hexdigest(),
|
md5sum = hashlib.md5(self.authority.certificate_buf).hexdigest(),
|
||||||
blob = self.authority.certificate_buf.decode("ascii"),
|
blob = self.authority.certificate_buf.decode("ascii"),
|
||||||
|
organization = self.authority.certificate["tbs_certificate"]["subject"].native.get("organization_name"),
|
||||||
|
signed = self.authority.certificate["tbs_certificate"]["validity"]["not_before"].native.replace(tzinfo=None),
|
||||||
|
expires = self.authority.certificate["tbs_certificate"]["validity"]["not_after"].native.replace(tzinfo=None)
|
||||||
),
|
),
|
||||||
mailer = dict(
|
mailer = dict(
|
||||||
name = config.MAILER_NAME,
|
name = config.MAILER_NAME,
|
||||||
address = config.MAILER_ADDRESS
|
address = config.MAILER_ADDRESS
|
||||||
) if config.MAILER_ADDRESS else None,
|
) if config.MAILER_ADDRESS else None,
|
||||||
machine_enrollment_subnets=config.MACHINE_ENROLLMENT_SUBNETS,
|
|
||||||
user_enrollment_allowed=config.USER_ENROLLMENT_ALLOWED,
|
user_enrollment_allowed=config.USER_ENROLLMENT_ALLOWED,
|
||||||
user_multiple_certificates=config.USER_MULTIPLE_CERTIFICATES,
|
user_multiple_certificates=config.USER_MULTIPLE_CERTIFICATES,
|
||||||
events = config.EVENT_SOURCE_SUBSCRIBE % config.EVENT_SOURCE_TOKEN,
|
events = config.EVENT_SOURCE_SUBSCRIBE % config.EVENT_SOURCE_TOKEN,
|
||||||
requests=serialize_requests(self.authority.list_requests),
|
requests=serialize_requests(self.authority.list_requests),
|
||||||
signed=serialize_certificates(self.authority.list_signed),
|
signed=serialize_certificates(self.authority.list_signed),
|
||||||
revoked=serialize_revoked(self.authority.list_revoked),
|
revoked=serialize_revoked(self.authority.list_revoked),
|
||||||
admin_users = User.objects.filter_admins(),
|
|
||||||
user_subnets = config.USER_SUBNETS or None,
|
|
||||||
autosign_subnets = config.AUTOSIGN_SUBNETS or None,
|
|
||||||
request_subnets = config.REQUEST_SUBNETS or None,
|
|
||||||
admin_subnets=config.ADMIN_SUBNETS or None,
|
|
||||||
signature = dict(
|
signature = dict(
|
||||||
revocation_list_lifetime=config.REVOCATION_LIST_LIFETIME,
|
revocation_list_lifetime=config.REVOCATION_LIST_LIFETIME,
|
||||||
profiles = sorted([p.serialize() for p in config.PROFILES.values()], key=lambda p:p.get("slug")),
|
profiles = sorted([p.serialize() for p in config.PROFILES.values()], key=lambda p:p.get("slug")),
|
||||||
|
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
|
authorization = dict(
|
||||||
|
admin_users = User.objects.filter_admins(),
|
||||||
|
|
||||||
|
user_subnets = config.USER_SUBNETS or None,
|
||||||
|
autosign_subnets = config.AUTOSIGN_SUBNETS or None,
|
||||||
|
request_subnets = config.REQUEST_SUBNETS or None,
|
||||||
|
machine_enrollment_subnets=config.MACHINE_ENROLLMENT_SUBNETS or None,
|
||||||
|
admin_subnets=config.ADMIN_SUBNETS or None,
|
||||||
|
|
||||||
|
ocsp_subnets = config.OCSP_SUBNETS or None,
|
||||||
|
crl_subnets = config.CRL_SUBNETS or None,
|
||||||
|
scep_subnets = config.SCEP_SUBNETS or None,
|
||||||
|
),
|
||||||
features=dict(
|
features=dict(
|
||||||
ocsp=bool(config.OCSP_SUBNETS),
|
|
||||||
crl=bool(config.CRL_SUBNETS),
|
|
||||||
token=bool(config.TOKEN_URL),
|
token=bool(config.TOKEN_URL),
|
||||||
tagging=True,
|
tagging=True,
|
||||||
leases=True,
|
leases=True,
|
||||||
|
@ -1,11 +1,16 @@
|
|||||||
|
import click
|
||||||
|
import codecs
|
||||||
import falcon
|
import falcon
|
||||||
import logging
|
import logging
|
||||||
import hashlib
|
import os
|
||||||
|
import string
|
||||||
from asn1crypto import pem
|
from asn1crypto import pem
|
||||||
from asn1crypto.csr import CertificationRequest
|
from asn1crypto.csr import CertificationRequest
|
||||||
from datetime import datetime
|
from datetime import datetime, timedelta
|
||||||
from time import time
|
from time import time
|
||||||
from certidude import mailer
|
from certidude import mailer, const
|
||||||
|
from certidude.tokens import TokenManager
|
||||||
|
from certidude.relational import RelationalMixin
|
||||||
from certidude.decorators import serialize
|
from certidude.decorators import serialize
|
||||||
from certidude.user import User
|
from certidude.user import User
|
||||||
from certidude import config
|
from certidude import config
|
||||||
@ -15,33 +20,25 @@ from .utils.firewall import login_required, authorize_admin
|
|||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
class TokenResource(AuthorityHandler):
|
class TokenResource(AuthorityHandler):
|
||||||
|
def __init__(self, authority, manager):
|
||||||
|
AuthorityHandler.__init__(self, authority)
|
||||||
|
self.manager = manager
|
||||||
|
|
||||||
|
def on_get(self, req, resp):
|
||||||
|
return
|
||||||
|
|
||||||
def on_put(self, req, resp):
|
def on_put(self, req, resp):
|
||||||
# Consume token
|
try:
|
||||||
now = time()
|
username, mail, created, expires, profile = self.manager.consume(req.get_param("token", required=True))
|
||||||
timestamp = req.get_param_as_int("t", required=True)
|
except RelationalMixin.DoesNotExist:
|
||||||
username = req.get_param("u", required=True)
|
raise falcon.HTTPForbidden("Forbidden", "No such token or token expired")
|
||||||
user = User.objects.get(username)
|
|
||||||
csum = hashlib.sha256()
|
|
||||||
csum.update(config.TOKEN_SECRET)
|
|
||||||
csum.update(username.encode("ascii"))
|
|
||||||
csum.update(str(timestamp).encode("ascii"))
|
|
||||||
|
|
||||||
margin = 300 # Tolerate 5 minute clock skew as Kerberos does
|
|
||||||
if csum.hexdigest() != req.get_param("c", required=True):
|
|
||||||
raise falcon.HTTPForbidden("Forbidden", "Invalid token supplied, did you copy-paste link correctly?")
|
|
||||||
if now < timestamp - margin:
|
|
||||||
raise falcon.HTTPForbidden("Forbidden", "Token not valid yet, are you sure server clock is correct?")
|
|
||||||
if now > timestamp + margin + config.TOKEN_LIFETIME:
|
|
||||||
raise falcon.HTTPForbidden("Forbidden", "Token expired")
|
|
||||||
|
|
||||||
# At this point consider token to be legitimate
|
|
||||||
body = req.stream.read(req.content_length)
|
body = req.stream.read(req.content_length)
|
||||||
header, _, der_bytes = pem.unarmor(body)
|
header, _, der_bytes = pem.unarmor(body)
|
||||||
csr = CertificationRequest.load(der_bytes)
|
csr = CertificationRequest.load(der_bytes)
|
||||||
common_name = csr["certification_request_info"]["subject"].native["common_name"]
|
common_name = csr["certification_request_info"]["subject"].native["common_name"]
|
||||||
assert common_name == username or common_name.startswith(username + "@"), "Invalid common name %s" % common_name
|
assert common_name == username or common_name.startswith(username + "@"), "Invalid common name %s" % common_name
|
||||||
try:
|
try:
|
||||||
_, resp.body = self.authority._sign(csr, body, profile="default",
|
_, resp.body = self.authority._sign(csr, body, profile=config.PROFILES.get(profile),
|
||||||
overwrite=config.TOKEN_OVERWRITE_PERMITTED)
|
overwrite=config.TOKEN_OVERWRITE_PERMITTED)
|
||||||
resp.set_header("Content-Type", "application/x-pem-file")
|
resp.set_header("Content-Type", "application/x-pem-file")
|
||||||
logger.info("Autosigned %s as proven by token ownership", common_name)
|
logger.info("Autosigned %s as proven by token ownership", common_name)
|
||||||
@ -56,40 +53,7 @@ class TokenResource(AuthorityHandler):
|
|||||||
@login_required
|
@login_required
|
||||||
@authorize_admin
|
@authorize_admin
|
||||||
def on_post(self, req, resp):
|
def on_post(self, req, resp):
|
||||||
# Generate token
|
self.manager.issue(
|
||||||
issuer = req.context.get("user")
|
issuer = req.context.get("user"),
|
||||||
username = req.get_param("username")
|
subject = User.objects.get(req.get_param("username", required=True)),
|
||||||
secondary = req.get_param("mail")
|
subject_mail = req.get_param("mail"))
|
||||||
|
|
||||||
if username:
|
|
||||||
# Otherwise try to look up user so we can derive their e-mail address
|
|
||||||
user = User.objects.get(username)
|
|
||||||
else:
|
|
||||||
# If no username is specified, assume it's intended for someone outside domain
|
|
||||||
username = "guest-%s" % hashlib.sha256(secondary.encode("ascii")).hexdigest()[-8:]
|
|
||||||
if not secondary:
|
|
||||||
raise
|
|
||||||
|
|
||||||
timestamp = int(time())
|
|
||||||
csum = hashlib.sha256()
|
|
||||||
csum.update(config.TOKEN_SECRET)
|
|
||||||
csum.update(username.encode("ascii"))
|
|
||||||
csum.update(str(timestamp).encode("ascii"))
|
|
||||||
args = "u=%s&t=%d&c=%s&i=%s" % (username, timestamp, csum.hexdigest(), issuer.name)
|
|
||||||
|
|
||||||
# Token lifetime in local time, to select timezone: dpkg-reconfigure tzdata
|
|
||||||
token_created = datetime.fromtimestamp(timestamp)
|
|
||||||
token_expires = datetime.fromtimestamp(timestamp + config.TOKEN_LIFETIME)
|
|
||||||
try:
|
|
||||||
with open("/etc/timezone") as fh:
|
|
||||||
token_timezone = fh.read().strip()
|
|
||||||
except EnvironmentError:
|
|
||||||
token_timezone = None
|
|
||||||
url = "%s#%s" % (config.TOKEN_URL, args)
|
|
||||||
context = globals()
|
|
||||||
context.update(locals())
|
|
||||||
mailer.send("token.md", to=user, **context)
|
|
||||||
return {
|
|
||||||
"token": args,
|
|
||||||
"url": url,
|
|
||||||
}
|
|
||||||
|
@ -110,7 +110,7 @@ def authenticate(optional=False):
|
|||||||
if kerberized:
|
if kerberized:
|
||||||
if not req.auth.startswith("Negotiate "):
|
if not req.auth.startswith("Negotiate "):
|
||||||
raise falcon.HTTPBadRequest("Bad request",
|
raise falcon.HTTPBadRequest("Bad request",
|
||||||
"Bad header, expected Negotiate: %s" % req.auth)
|
"Bad header, expected Negotiate")
|
||||||
|
|
||||||
os.environ["KRB5_KTNAME"] = config.KERBEROS_KEYTAB
|
os.environ["KRB5_KTNAME"] = config.KERBEROS_KEYTAB
|
||||||
|
|
||||||
@ -158,7 +158,7 @@ def authenticate(optional=False):
|
|||||||
|
|
||||||
else:
|
else:
|
||||||
if not req.auth.startswith("Basic "):
|
if not req.auth.startswith("Basic "):
|
||||||
raise falcon.HTTPBadRequest("Bad request", "Bad header, expected Basic: %s" % req.auth)
|
raise falcon.HTTPBadRequest("Bad request", "Bad header, expected Basic")
|
||||||
basic, token = req.auth.split(" ", 1)
|
basic, token = req.auth.split(" ", 1)
|
||||||
user, passwd = b64decode(token).decode("ascii").split(":", 1)
|
user, passwd = b64decode(token).decode("ascii").split(":", 1)
|
||||||
|
|
||||||
|
@ -13,26 +13,14 @@ from asn1crypto.csr import CertificationRequest
|
|||||||
from certbuilder import CertificateBuilder
|
from certbuilder import CertificateBuilder
|
||||||
from certidude import config, push, mailer, const
|
from certidude import config, push, mailer, const
|
||||||
from certidude import errors
|
from certidude import errors
|
||||||
from certidude.common import cn_to_dn
|
from certidude.common import cn_to_dn, generate_serial, random
|
||||||
from crlbuilder import CertificateListBuilder, pem_armor_crl
|
from crlbuilder import CertificateListBuilder, pem_armor_crl
|
||||||
from csrbuilder import CSRBuilder, pem_armor_csr
|
from csrbuilder import CSRBuilder, pem_armor_csr
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
from jinja2 import Template
|
from jinja2 import Template
|
||||||
from random import SystemRandom
|
|
||||||
from xattr import getxattr, listxattr, setxattr
|
from xattr import getxattr, listxattr, setxattr
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
random = SystemRandom()
|
|
||||||
|
|
||||||
try:
|
|
||||||
from time import time_ns
|
|
||||||
except ImportError:
|
|
||||||
from time import time
|
|
||||||
def time_ns():
|
|
||||||
return int(time() * 10**9) # 64 bits integer, 32 ns bits
|
|
||||||
|
|
||||||
def generate_serial():
|
|
||||||
return time_ns() << 56 | random.randint(0, 2**56-1)
|
|
||||||
|
|
||||||
# https://securityblog.redhat.com/2014/06/18/openssl-privilege-separation-analysis/
|
# https://securityblog.redhat.com/2014/06/18/openssl-privilege-separation-analysis/
|
||||||
# https://jamielinux.com/docs/openssl-certificate-authority/
|
# https://jamielinux.com/docs/openssl-certificate-authority/
|
||||||
@ -61,13 +49,14 @@ def self_enroll(skip_notify=False):
|
|||||||
self_public_key = asymmetric.load_public_key(path)
|
self_public_key = asymmetric.load_public_key(path)
|
||||||
private_key = asymmetric.load_private_key(config.SELF_KEY_PATH)
|
private_key = asymmetric.load_private_key(config.SELF_KEY_PATH)
|
||||||
except FileNotFoundError: # certificate or private key not found
|
except FileNotFoundError: # certificate or private key not found
|
||||||
|
click.echo("Generating private key for frontend: %s" % config.SELF_KEY_PATH)
|
||||||
with open(config.SELF_KEY_PATH, 'wb') as fh:
|
with open(config.SELF_KEY_PATH, 'wb') as fh:
|
||||||
if public_key.algorithm == "ec":
|
if public_key.algorithm == "ec":
|
||||||
self_public_key, private_key = asymmetric.generate_pair("ec", curve=public_key.curve)
|
self_public_key, private_key = asymmetric.generate_pair("ec", curve=public_key.curve)
|
||||||
elif public_key.algorithm == "rsa":
|
elif public_key.algorithm == "rsa":
|
||||||
self_public_key, private_key = asymmetric.generate_pair("rsa", bit_size=public_key.bit_size)
|
self_public_key, private_key = asymmetric.generate_pair("rsa", bit_size=public_key.bit_size)
|
||||||
else:
|
else:
|
||||||
NotImplemented
|
raise NotImplemented("CA certificate public key algorithm %s not supported" % public_key.algorithm)
|
||||||
fh.write(asymmetric.dump_private_key(private_key, None))
|
fh.write(asymmetric.dump_private_key(private_key, None))
|
||||||
else:
|
else:
|
||||||
now = datetime.utcnow()
|
now = datetime.utcnow()
|
||||||
@ -84,10 +73,11 @@ def self_enroll(skip_notify=False):
|
|||||||
drop_privileges()
|
drop_privileges()
|
||||||
assert os.getuid() != 0 and os.getgid() != 0
|
assert os.getuid() != 0 and os.getgid() != 0
|
||||||
path = os.path.join(config.REQUESTS_DIR, common_name + ".pem")
|
path = os.path.join(config.REQUESTS_DIR, common_name + ".pem")
|
||||||
click.echo("Writing request to %s" % path)
|
click.echo("Writing certificate signing request for frontend: %s" % path)
|
||||||
with open(path, "wb") as fh:
|
with open(path, "wb") as fh:
|
||||||
fh.write(pem_armor_csr(request)) # Write CSR with certidude permissions
|
fh.write(pem_armor_csr(request)) # Write CSR with certidude permissions
|
||||||
authority.sign(common_name, skip_notify=skip_notify, 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"])
|
||||||
|
click.echo("Frontend certificate signed")
|
||||||
sys.exit(0)
|
sys.exit(0)
|
||||||
else:
|
else:
|
||||||
os.waitpid(pid, 0)
|
os.waitpid(pid, 0)
|
||||||
@ -409,13 +399,15 @@ def _sign(csr, buf, profile, skip_notify=False, skip_push=False, overwrite=False
|
|||||||
builder.serial_number = generate_serial()
|
builder.serial_number = generate_serial()
|
||||||
|
|
||||||
now = datetime.utcnow()
|
now = datetime.utcnow()
|
||||||
builder.begin_date = now - timedelta(minutes=5)
|
builder.begin_date = now - const.CLOCK_SKEW_TOLERANCE
|
||||||
builder.end_date = now + timedelta(days=profile.lifetime)
|
builder.end_date = now + timedelta(days=profile.lifetime)
|
||||||
builder.issuer = certificate
|
builder.issuer = certificate
|
||||||
builder.ca = profile.ca
|
builder.ca = profile.ca
|
||||||
builder.key_usage = profile.key_usage
|
builder.key_usage = profile.key_usage
|
||||||
builder.extended_key_usage = profile.extended_key_usage
|
builder.extended_key_usage = profile.extended_key_usage
|
||||||
builder.subject_alt_domains = [common_name]
|
builder.subject_alt_domains = [common_name]
|
||||||
|
builder.ocsp_url = profile.responder_url
|
||||||
|
builder.crl_url = profile.revoked_url
|
||||||
|
|
||||||
end_entity_cert = builder.build(private_key)
|
end_entity_cert = builder.build(private_key)
|
||||||
end_entity_cert_buf = asymmetric.dump_certificate(end_entity_cert)
|
end_entity_cert_buf = asymmetric.dump_certificate(end_entity_cert)
|
||||||
|
148
certidude/cli.py
@ -18,7 +18,7 @@ from certbuilder import CertificateBuilder, pem_armor_certificate
|
|||||||
from certidude import const
|
from certidude import const
|
||||||
from csrbuilder import CSRBuilder, pem_armor_csr
|
from csrbuilder import CSRBuilder, pem_armor_csr
|
||||||
from configparser import ConfigParser, NoOptionError
|
from configparser import ConfigParser, NoOptionError
|
||||||
from certidude.common import apt, rpm, drop_privileges, selinux_fixup, cn_to_dn
|
from certidude.common import apt, rpm, drop_privileges, selinux_fixup, cn_to_dn, generate_serial
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
from glob import glob
|
from glob import glob
|
||||||
from ipaddress import ip_network
|
from ipaddress import ip_network
|
||||||
@ -51,7 +51,7 @@ def setup_client(prefix="client_", dh=False):
|
|||||||
authority = arguments.get("authority")
|
authority = arguments.get("authority")
|
||||||
b = os.path.join("/etc/certidude/authority", authority)
|
b = os.path.join("/etc/certidude/authority", authority)
|
||||||
if dh:
|
if dh:
|
||||||
path = os.path.join(const.STORAGE_PATH, "dh.pem")
|
path = os.path.join("/etc/ssl/dhparam.pem")
|
||||||
if not os.path.exists(path):
|
if not os.path.exists(path):
|
||||||
rpm("openssl")
|
rpm("openssl")
|
||||||
apt("openssl")
|
apt("openssl")
|
||||||
@ -390,7 +390,6 @@ def certidude_enroll(fork, renew, no_wait, kerberos, skip_self):
|
|||||||
}
|
}
|
||||||
|
|
||||||
if renew: # Do mutually authenticated TLS handshake
|
if renew: # Do mutually authenticated TLS handshake
|
||||||
request_url = "https://%s:8443/api/request/" % authority_name
|
|
||||||
kwargs["cert"] = certificate_path, key_path
|
kwargs["cert"] = certificate_path, key_path
|
||||||
click.echo("Renewing using current keypair at %s %s" % kwargs["cert"])
|
click.echo("Renewing using current keypair at %s %s" % kwargs["cert"])
|
||||||
else:
|
else:
|
||||||
@ -417,8 +416,8 @@ def certidude_enroll(fork, renew, no_wait, kerberos, skip_self):
|
|||||||
kwargs["auth"] = HTTPKerberosAuth(mutual_authentication=OPTIONAL, force_preemptive=True)
|
kwargs["auth"] = HTTPKerberosAuth(mutual_authentication=OPTIONAL, force_preemptive=True)
|
||||||
else:
|
else:
|
||||||
click.echo("Not using machine keytab")
|
click.echo("Not using machine keytab")
|
||||||
request_url = "https://%s/api/request/" % authority_name
|
|
||||||
|
|
||||||
|
request_url = "https://%s:8443/api/request/" % authority_name
|
||||||
if request_params:
|
if request_params:
|
||||||
request_url = request_url + "?" + "&".join(request_params)
|
request_url = request_url + "?" + "&".join(request_params)
|
||||||
submission = requests.post(request_url, **kwargs)
|
submission = requests.post(request_url, **kwargs)
|
||||||
@ -580,7 +579,7 @@ def certidude_enroll(fork, renew, no_wait, kerberos, skip_self):
|
|||||||
nm_config.set("vpn", "key", key_path)
|
nm_config.set("vpn", "key", key_path)
|
||||||
nm_config.set("vpn", "cert", certificate_path)
|
nm_config.set("vpn", "cert", certificate_path)
|
||||||
nm_config.set("vpn", "ca", authority_path)
|
nm_config.set("vpn", "ca", authority_path)
|
||||||
nm_config.set("vpn", "tls-cipher", "TLS-%s-WITH-AES-128-GCM-SHA384" % (
|
nm_config.set("vpn", "tls-cipher", "TLS-%s-WITH-AES-256-GCM-SHA384" % (
|
||||||
"ECDHE-ECDSA" if authority_public_key.algorithm == "ec" else "DHE-RSA"))
|
"ECDHE-ECDSA" if authority_public_key.algorithm == "ec" else "DHE-RSA"))
|
||||||
nm_config.set("vpn", "cipher", "AES-128-GCM")
|
nm_config.set("vpn", "cipher", "AES-128-GCM")
|
||||||
nm_config.set("vpn", "auth", "SHA384")
|
nm_config.set("vpn", "auth", "SHA384")
|
||||||
@ -995,6 +994,10 @@ def certidude_setup_openvpn_networkmanager(authority, remote, common_name, **pat
|
|||||||
default="/etc/nginx/sites-available/certidude.conf",
|
default="/etc/nginx/sites-available/certidude.conf",
|
||||||
type=click.File(mode="w", atomic=True, lazy=True),
|
type=click.File(mode="w", atomic=True, lazy=True),
|
||||||
help="nginx site config for serving Certidude, /etc/nginx/sites-available/certidude by default")
|
help="nginx site config for serving Certidude, /etc/nginx/sites-available/certidude by default")
|
||||||
|
@click.option("--tls-config",
|
||||||
|
default="/etc/nginx/conf.d/tls.conf",
|
||||||
|
type=click.File(mode="w", atomic=True, lazy=True),
|
||||||
|
help="TLS configuration file of nginx, /etc/nginx/conf.d/tls.conf by default")
|
||||||
@click.option("--common-name", "-cn", default=const.FQDN, help="Common name of the server, %s by default" % const.FQDN)
|
@click.option("--common-name", "-cn", default=const.FQDN, help="Common name of the server, %s by default" % const.FQDN)
|
||||||
@click.option("--title", "-t", default="Certidude at %s" % const.FQDN, help="Common name of the certificate authority, 'Certidude at %s' by default" % const.FQDN)
|
@click.option("--title", "-t", default="Certidude at %s" % const.FQDN, help="Common name of the certificate authority, 'Certidude at %s' by default" % const.FQDN)
|
||||||
@click.option("--authority-lifetime", default=20*365, help="Authority certificate lifetime in days, 20 years by default")
|
@click.option("--authority-lifetime", default=20*365, help="Authority certificate lifetime in days, 20 years by default")
|
||||||
@ -1008,7 +1011,7 @@ def certidude_setup_openvpn_networkmanager(authority, remote, common_name, **pat
|
|||||||
@click.option("--elliptic-curve", "-e", is_flag=True, help="Generate EC instead of RSA keypair")
|
@click.option("--elliptic-curve", "-e", is_flag=True, help="Generate EC instead of RSA keypair")
|
||||||
@click.option("--subordinate", is_flag=True, help="Set up subordinate CA instead of root CA")
|
@click.option("--subordinate", is_flag=True, help="Set up subordinate CA instead of root CA")
|
||||||
@fqdn_required
|
@fqdn_required
|
||||||
def certidude_setup_authority(username, kerberos_keytab, nginx_config, organization, organizational_unit, common_name, directory, authority_lifetime, push_server, outbox, title, skip_assets, skip_packages, elliptic_curve, subordinate):
|
def certidude_setup_authority(username, kerberos_keytab, nginx_config, tls_config, organization, organizational_unit, common_name, directory, authority_lifetime, push_server, outbox, title, skip_assets, skip_packages, elliptic_curve, subordinate):
|
||||||
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 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"
|
assert os.getuid() == 0 and os.getgid() == 0, "Authority can be set up only by root"
|
||||||
|
|
||||||
@ -1027,21 +1030,28 @@ def certidude_setup_authority(username, kerberos_keytab, nginx_config, organizat
|
|||||||
libkrb5-dev libldap2-dev libsasl2-dev gawk libncurses5-dev \
|
libkrb5-dev libldap2-dev libsasl2-dev gawk libncurses5-dev \
|
||||||
rsync attr wget unzip"
|
rsync attr wget unzip"
|
||||||
click.echo("Running: %s" % cmd)
|
click.echo("Running: %s" % cmd)
|
||||||
if os.system(cmd): sys.exit(254)
|
if os.system(cmd):
|
||||||
if os.system("pip3 install -q --upgrade gssapi falcon humanize ipaddress simplepam user-agents"): sys.exit(253)
|
raise click.ClickException("Failed to install APT packages")
|
||||||
if os.system("pip3 install -q --pre --upgrade python-ldap"): exit(252)
|
if os.system("pip3 install -q --upgrade gssapi falcon humanize ipaddress simplepam user-agents"):
|
||||||
|
raise click.ClickException("Failed to install Python packages")
|
||||||
|
if os.system("pip3 install -q --pre --upgrade python-ldap"):
|
||||||
|
raise click.ClickException("Failed to install python-ldap")
|
||||||
|
|
||||||
if not os.path.exists("/usr/lib/nginx/modules/ngx_nchan_module.so"):
|
if not os.path.exists("/usr/lib/nginx/modules/ngx_nchan_module.so"):
|
||||||
click.echo("Enabling nginx PPA")
|
click.echo("Enabling nginx PPA")
|
||||||
if os.system("add-apt-repository -y ppa:nginx/stable"): sys.exit(251)
|
if os.system("add-apt-repository -y ppa:nginx/stable"):
|
||||||
if os.system("apt-get update -q"): sys.exit(250)
|
raise click.ClickException("Failed to add nginx PPA")
|
||||||
if os.system("apt-get install -y -q libnginx-mod-nchan"): sys.exit(249)
|
if os.system("apt-get update -q"):
|
||||||
|
raise click.ClickException("Failed to update package lists")
|
||||||
|
if os.system("apt-get install -y -q libnginx-mod-nchan"):
|
||||||
|
raise click.ClickException("Failed to install nchan")
|
||||||
else:
|
else:
|
||||||
click.echo("PPA for nginx already enabled")
|
click.echo("PPA for nginx already enabled")
|
||||||
|
|
||||||
if not os.path.exists("/usr/sbin/nginx"):
|
if not os.path.exists("/usr/sbin/nginx"):
|
||||||
click.echo("Installing nginx from PPA")
|
click.echo("Installing nginx from PPA")
|
||||||
if os.system("apt-get install -y -q nginx"): sys.exit(248)
|
if os.system("apt-get install -y -q nginx"):
|
||||||
|
raise click.ClickException("Failed to install nginx")
|
||||||
else:
|
else:
|
||||||
click.echo("Web server nginx already installed")
|
click.echo("Web server nginx already installed")
|
||||||
|
|
||||||
@ -1049,7 +1059,7 @@ def certidude_setup_authority(username, kerberos_keytab, nginx_config, organizat
|
|||||||
os.symlink("/usr/bin/nodejs", "/usr/bin/node")
|
os.symlink("/usr/bin/nodejs", "/usr/bin/node")
|
||||||
|
|
||||||
# Generate secret for tokens
|
# Generate secret for tokens
|
||||||
token_secret = ''.join(random.choice(string.ascii_letters + string.digits + '!@#$%^&*()') for i in range(50))
|
token_url = "https://" + const.FQDN + "/#action=enroll&token=%(token)s&router=%(router)s&protocol=ovpn"
|
||||||
|
|
||||||
template_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), "templates", "profile")
|
template_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), "templates", "profile")
|
||||||
click.echo("Using templates from %s" % template_path)
|
click.echo("Using templates from %s" % template_path)
|
||||||
@ -1062,6 +1072,10 @@ def certidude_setup_authority(username, kerberos_keytab, nginx_config, organizat
|
|||||||
revoked_url = "http://%s/api/revoked/" % common_name
|
revoked_url = "http://%s/api/revoked/" % common_name
|
||||||
click.echo("Setting revocation list URL to %s" % revoked_url)
|
click.echo("Setting revocation list URL to %s" % revoked_url)
|
||||||
|
|
||||||
|
responder_url = "http://%s/api/ocsp/" % common_name
|
||||||
|
click.echo("Setting OCSP responder URL to %s" % responder_url)
|
||||||
|
|
||||||
|
|
||||||
# Expand variables
|
# Expand variables
|
||||||
assets_dir = os.path.join(directory, "assets")
|
assets_dir = os.path.join(directory, "assets")
|
||||||
ca_key = os.path.join(directory, "ca_key.pem")
|
ca_key = os.path.join(directory, "ca_key.pem")
|
||||||
@ -1070,6 +1084,7 @@ def certidude_setup_authority(username, kerberos_keytab, nginx_config, organizat
|
|||||||
self_key = os.path.join(directory, "self_key.pem")
|
self_key = os.path.join(directory, "self_key.pem")
|
||||||
sqlite_path = os.path.join(directory, "meta", "db.sqlite")
|
sqlite_path = os.path.join(directory, "meta", "db.sqlite")
|
||||||
distinguished_name = cn_to_dn("Certidude at %s" % common_name, common_name, o=organization, ou=organizational_unit)
|
distinguished_name = cn_to_dn("Certidude at %s" % common_name, common_name, o=organization, ou=organizational_unit)
|
||||||
|
dhparam_path = "/etc/ssl/dhparam.pem"
|
||||||
|
|
||||||
# Builder variables
|
# Builder variables
|
||||||
dhgroup = "ecp384" if elliptic_curve else "modp2048"
|
dhgroup = "ecp384" if elliptic_curve else "modp2048"
|
||||||
@ -1080,8 +1095,7 @@ def certidude_setup_authority(username, kerberos_keytab, nginx_config, organizat
|
|||||||
except KeyError:
|
except KeyError:
|
||||||
cmd = "adduser", "--system", "--no-create-home", "--group", "certidude"
|
cmd = "adduser", "--system", "--no-create-home", "--group", "certidude"
|
||||||
if subprocess.call(cmd):
|
if subprocess.call(cmd):
|
||||||
click.echo("Failed to create system user 'certidude'")
|
raise click.ClickException("Failed to create system user 'certidude'")
|
||||||
return 255
|
|
||||||
|
|
||||||
if os.path.exists(kerberos_keytab):
|
if os.path.exists(kerberos_keytab):
|
||||||
click.echo("Service principal keytab found in '%s'" % kerberos_keytab)
|
click.echo("Service principal keytab found in '%s'" % kerberos_keytab)
|
||||||
@ -1114,6 +1128,7 @@ def certidude_setup_authority(username, kerberos_keytab, nginx_config, organizat
|
|||||||
letsencrypt_fullchain = "/etc/letsencrypt/live/%s/fullchain.pem" % common_name
|
letsencrypt_fullchain = "/etc/letsencrypt/live/%s/fullchain.pem" % common_name
|
||||||
letsencrypt_privkey = "/etc/letsencrypt/live/%s/privkey.pem" % common_name
|
letsencrypt_privkey = "/etc/letsencrypt/live/%s/privkey.pem" % common_name
|
||||||
letsencrypt = os.path.exists(letsencrypt_fullchain)
|
letsencrypt = os.path.exists(letsencrypt_fullchain)
|
||||||
|
|
||||||
doc_path = os.path.join(os.path.realpath(os.path.dirname(os.path.dirname(__file__))), "doc")
|
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")
|
script_dir = os.path.join(os.path.realpath(os.path.dirname(__file__)), "templates", "script")
|
||||||
|
|
||||||
@ -1163,26 +1178,27 @@ def certidude_setup_authority(username, kerberos_keytab, nginx_config, organizat
|
|||||||
if skip_packages:
|
if skip_packages:
|
||||||
click.echo("Not attempting to install packages from NPM as requested...")
|
click.echo("Not attempting to install packages from NPM as requested...")
|
||||||
else:
|
else:
|
||||||
cmd = "npm install --silent -g nunjucks@2.5.2 nunjucks-date@1.2.0 node-forge bootstrap@4.0.0-alpha.6 jquery timeago tether font-awesome qrcode-svg"
|
cmd = "npm install --silent --no-optional -g nunjucks@2.5.2 nunjucks-date@1.2.0 node-forge bootstrap@4.0.0-alpha.6 jquery timeago tether font-awesome qrcode-svg"
|
||||||
click.echo("Installing JavaScript packages: %s" % cmd)
|
click.echo("Installing JavaScript packages: %s" % cmd)
|
||||||
if os.system(cmd): sys.exit(230)
|
|
||||||
|
|
||||||
if skip_assets:
|
if skip_assets:
|
||||||
click.echo("Not attempting to assemble assets as requested...")
|
click.echo("Not attempting to assemble assets as requested...")
|
||||||
else:
|
else:
|
||||||
# Copy fonts
|
# Copy fonts
|
||||||
click.echo("Copying fonts...")
|
click.echo("Copying fonts...")
|
||||||
if os.system("rsync -avq /usr/local/lib/node_modules/font-awesome/fonts/ %s/fonts/" % assets_dir): sys.exit(229)
|
if os.system("rsync -avq /usr/local/lib/node_modules/font-awesome/fonts/ %s/fonts/" % assets_dir):
|
||||||
|
raise click.ClickException("Failed to copy fonts")
|
||||||
|
|
||||||
# Compile nunjucks templates
|
# Compile nunjucks templates
|
||||||
cmd = 'nunjucks-precompile --include ".html$" --include ".ps1$" --include ".sh$" --include ".svg" %s > %s.part' % (static_path, bundle_js)
|
cmd = 'nunjucks-precompile --include "\.html$" --include "\.ps1$" --include "\.sh$" --include "\.svg$" --include "\.yml$" --include "\.conf$" --include "\.mobileconfig$" %s > %s.part' % (static_path, bundle_js)
|
||||||
click.echo("Compiling templates: %s" % cmd)
|
click.echo("Compiling templates: %s" % cmd)
|
||||||
if os.system(cmd): sys.exit(228)
|
if os.system(cmd):
|
||||||
|
raise click.ClickException("Failed to compile nunjucks templates")
|
||||||
|
|
||||||
# Assemble bundle.js
|
# Assemble bundle.js
|
||||||
click.echo("Assembling %s" % bundle_js)
|
click.echo("Assembling %s" % bundle_js)
|
||||||
with open(bundle_js + ".part", "a") as fh:
|
with open(bundle_js + ".part", "a") as fh:
|
||||||
for pkg in "qrcode-svg/dist/qrcode.min.js", "jquery/dist/jquery.min.js", "timeago/*.js", "nunjucks/browser/nunjucks-slim.min.js", "tether/dist/js/*.min.js", "bootstrap/dist/js/*.min.js":
|
for pkg in "jquery/dist/jquery.min.js", "tether/dist/js/*.min.js", "bootstrap/dist/js/*.min.js", "node-forge/dist/forge.all.min.js", "qrcode-svg/dist/qrcode.min.js", "timeago/*.js", "nunjucks/browser/nunjucks-slim.min.js":
|
||||||
for j in glob(os.path.join("/usr/local/lib/node_modules", pkg)):
|
for j in glob(os.path.join("/usr/local/lib/node_modules", pkg)):
|
||||||
click.echo("- Merging: %s" % j)
|
click.echo("- Merging: %s" % j)
|
||||||
with open(j) as ih:
|
with open(j) as ih:
|
||||||
@ -1208,9 +1224,22 @@ def certidude_setup_authority(username, kerberos_keytab, nginx_config, organizat
|
|||||||
if not os.path.exists(const.CONFIG_DIR):
|
if not os.path.exists(const.CONFIG_DIR):
|
||||||
click.echo("Creating %s" % const.CONFIG_DIR)
|
click.echo("Creating %s" % const.CONFIG_DIR)
|
||||||
os.makedirs(const.CONFIG_DIR)
|
os.makedirs(const.CONFIG_DIR)
|
||||||
|
if not os.path.exists(const.SCRIPT_DIR):
|
||||||
|
click.echo("Creating %s" % const.SCRIPT_DIR)
|
||||||
|
os.makedirs(const.SCRIPT_DIR)
|
||||||
|
|
||||||
os.umask(0o177) # 600
|
os.umask(0o177) # 600
|
||||||
|
|
||||||
|
if not os.path.exists(dhparam_path):
|
||||||
|
cmd = "openssl", "dhparam", "-out", dhparam_path, ("1024" if os.getenv("TRAVIS") else str(const.KEY_SIZE))
|
||||||
|
subprocess.check_call(cmd)
|
||||||
|
|
||||||
|
if os.path.exists(tls_config.name):
|
||||||
|
click.echo("Configuration file %s already exists, not overwriting" % tls_config.name)
|
||||||
|
else:
|
||||||
|
tls_config.write(env.get_template("nginx-tls.conf").render(locals()))
|
||||||
|
click.echo("Generated %s" % tls_config.name)
|
||||||
|
|
||||||
if os.path.exists(const.SERVER_CONFIG_PATH):
|
if os.path.exists(const.SERVER_CONFIG_PATH):
|
||||||
click.echo("Configuration file %s already exists, remove to regenerate" % const.SERVER_CONFIG_PATH)
|
click.echo("Configuration file %s already exists, remove to regenerate" % const.SERVER_CONFIG_PATH)
|
||||||
else:
|
else:
|
||||||
@ -1227,6 +1256,14 @@ def certidude_setup_authority(username, kerberos_keytab, nginx_config, organizat
|
|||||||
fh.write(env.get_template("server/builder.conf").render(vars()))
|
fh.write(env.get_template("server/builder.conf").render(vars()))
|
||||||
click.echo("File %s created" % const.BUILDER_CONFIG_PATH)
|
click.echo("File %s created" % const.BUILDER_CONFIG_PATH)
|
||||||
|
|
||||||
|
# Create image builder site script
|
||||||
|
if os.path.exists(const.BUILDER_SITE_SCRIPT):
|
||||||
|
click.echo("Image builder site customization script %s already exists, remove to regenerate" % const.BUILDER_SITE_SCRIPT)
|
||||||
|
else:
|
||||||
|
with open(const.BUILDER_SITE_SCRIPT, "w") as fh:
|
||||||
|
fh.write(env.get_template("server/site.sh").render(vars()))
|
||||||
|
click.echo("File %s created" % const.BUILDER_SITE_SCRIPT)
|
||||||
|
|
||||||
# Create signature profile config
|
# Create signature profile config
|
||||||
if os.path.exists(const.PROFILE_CONFIG_PATH):
|
if os.path.exists(const.PROFILE_CONFIG_PATH):
|
||||||
click.echo("Signature profile config %s already exists, remove to regenerate" % const.PROFILE_CONFIG_PATH)
|
click.echo("Signature profile config %s already exists, remove to regenerate" % const.PROFILE_CONFIG_PATH)
|
||||||
@ -1235,20 +1272,16 @@ def certidude_setup_authority(username, kerberos_keytab, nginx_config, organizat
|
|||||||
fh.write(env.get_template("server/profile.conf").render(vars()))
|
fh.write(env.get_template("server/profile.conf").render(vars()))
|
||||||
click.echo("File %s created" % const.PROFILE_CONFIG_PATH)
|
click.echo("File %s created" % const.PROFILE_CONFIG_PATH)
|
||||||
|
|
||||||
if not os.path.exists("/var/lib/certidude/builder"):
|
|
||||||
click.echo("Creating %s" % "/var/lib/certidude/builder")
|
|
||||||
os.makedirs("/var/lib/certidude/builder")
|
|
||||||
|
|
||||||
# Create subdirectories with 770 permissions
|
# Create subdirectories with 770 permissions
|
||||||
os.umask(0o007)
|
os.umask(0o007)
|
||||||
for subdir in ("signed", "signed/by-serial", "requests", "revoked", "expired", "meta"):
|
for subdir in ("signed", "signed/by-serial", "requests", "revoked", "expired", "meta", "builder"):
|
||||||
path = os.path.join(directory, subdir)
|
path = os.path.join(directory, subdir)
|
||||||
if not os.path.exists(path):
|
if not os.path.exists(path):
|
||||||
click.echo("Creating directory %s" % path)
|
click.echo("Creating directory %s" % path)
|
||||||
os.mkdir(path)
|
os.mkdir(path)
|
||||||
else:
|
else:
|
||||||
click.echo("Directory already exists %s" % path)
|
click.echo("Directory already exists %s" % path)
|
||||||
assert os.stat(path).st_mode == 0o40770
|
assert os.stat(path).st_mode == 0o40770, path
|
||||||
|
|
||||||
# Create SQLite database file with correct permissions
|
# Create SQLite database file with correct permissions
|
||||||
os.umask(0o117)
|
os.umask(0o117)
|
||||||
@ -1293,17 +1326,15 @@ def certidude_setup_authority(username, kerberos_keytab, nginx_config, organizat
|
|||||||
click.echo(" chmod 0644 %s" % ca_cert)
|
click.echo(" chmod 0644 %s" % ca_cert)
|
||||||
click.echo()
|
click.echo()
|
||||||
click.echo("To finish setup procedure run 'certidude setup authority' again")
|
click.echo("To finish setup procedure run 'certidude setup authority' again")
|
||||||
sys.exit(1)
|
sys.exit(1) # stop this fork here with error
|
||||||
|
|
||||||
# https://technet.microsoft.com/en-us/library/aa998840(v=exchg.141).aspx
|
# https://technet.microsoft.com/en-us/library/aa998840(v=exchg.141).aspx
|
||||||
builder = CertificateBuilder(distinguished_name, public_key)
|
builder = CertificateBuilder(distinguished_name, public_key)
|
||||||
builder.self_signed = True
|
builder.self_signed = True
|
||||||
builder.ca = True
|
builder.ca = True
|
||||||
builder.serial_number = random.randint(
|
builder.serial_number = generate_serial()
|
||||||
0x100000000000000000000000000000000000000,
|
|
||||||
0xfffffffffffffffffffffffffffffffffffffff)
|
|
||||||
|
|
||||||
builder.begin_date = NOW - timedelta(minutes=5)
|
builder.begin_date = NOW - const.CLOCK_SKEW_TOLERANCE
|
||||||
builder.end_date = NOW + timedelta(days=authority_lifetime)
|
builder.end_date = NOW + timedelta(days=authority_lifetime)
|
||||||
|
|
||||||
certificate = builder.build(private_key)
|
certificate = builder.build(private_key)
|
||||||
@ -1312,14 +1343,18 @@ def certidude_setup_authority(username, kerberos_keytab, nginx_config, organizat
|
|||||||
os.umask(0o137)
|
os.umask(0o137)
|
||||||
with open(ca_cert, 'wb') as f:
|
with open(ca_cert, 'wb') as f:
|
||||||
f.write(pem_armor_certificate(certificate))
|
f.write(pem_armor_certificate(certificate))
|
||||||
|
click.echo("Authority certificate written to: %s" % ca_cert)
|
||||||
|
|
||||||
sys.exit(0) # stop this fork here
|
sys.exit(0) # stop this fork here
|
||||||
else:
|
else:
|
||||||
|
|
||||||
_, exitcode = os.waitpid(bootstrap_pid, 0)
|
_, exitcode = os.waitpid(bootstrap_pid, 0)
|
||||||
if exitcode:
|
if exitcode:
|
||||||
return 0
|
return 0
|
||||||
from certidude import authority
|
from certidude import authority
|
||||||
authority.self_enroll(skip_notify=True)
|
authority.self_enroll(skip_notify=True)
|
||||||
|
assert os.path.exists(self_key)
|
||||||
|
assert os.path.exists(os.path.join(directory, "signed", const.FQDN) + ".pem")
|
||||||
assert os.getuid() == 0 and os.getgid() == 0, "Enroll contaminated environment"
|
assert os.getuid() == 0 and os.getgid() == 0, "Enroll contaminated environment"
|
||||||
assert os.stat(sqlite_path).st_mode == 0o100660
|
assert os.stat(sqlite_path).st_mode == 0o100660
|
||||||
assert os.stat(ca_cert).st_mode == 0o100640
|
assert os.stat(ca_cert).st_mode == 0o100640
|
||||||
@ -1343,6 +1378,11 @@ def certidude_setup_authority(username, kerberos_keytab, nginx_config, organizat
|
|||||||
click.echo(" openssl x509 -text -noout -in %s | less" % ca_cert)
|
click.echo(" openssl x509 -text -noout -in %s | less" % ca_cert)
|
||||||
click.echo(" openssl rsa -check -in %s" % ca_key)
|
click.echo(" openssl rsa -check -in %s" % ca_key)
|
||||||
click.echo(" openssl verify -CAfile %s %s" % (ca_cert, ca_cert))
|
click.echo(" openssl verify -CAfile %s %s" % (ca_cert, ca_cert))
|
||||||
|
click.echo()
|
||||||
|
click.echo("To inspect logs and issued tokens:")
|
||||||
|
click.echo()
|
||||||
|
click.echo(" echo 'select * from log;' | sqlite3 /var/lib/certidude/meta/db.sqlite")
|
||||||
|
click.echo(" echo 'select * from token;' | sqlite3 /var/lib/certidude/meta/db.sqlite")
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
|
|
||||||
@ -1461,7 +1501,7 @@ def certidude_revoke(common_name, reason):
|
|||||||
@click.command("expire", help="Move expired certificates")
|
@click.command("expire", help="Move expired certificates")
|
||||||
def certidude_expire():
|
def certidude_expire():
|
||||||
from certidude import authority, config
|
from certidude import authority, config
|
||||||
threshold = datetime.utcnow() - timedelta(minutes=5) # Kerberos tolerance
|
threshold = datetime.utcnow() - const.CLOCK_SKEW_TOLERANCE
|
||||||
for common_name, path, buf, cert, signed, expires in authority.list_signed():
|
for common_name, path, buf, cert, signed, expires in authority.list_signed():
|
||||||
if expires < threshold:
|
if expires < threshold:
|
||||||
expired_path = os.path.join(config.EXPIRED_DIR, "%040x.pem" % cert.serial_number)
|
expired_path = os.path.join(config.EXPIRED_DIR, "%040x.pem" % cert.serial_number)
|
||||||
@ -1492,6 +1532,10 @@ def certidude_serve(port, listen, fork):
|
|||||||
|
|
||||||
from certidude import config
|
from certidude import config
|
||||||
|
|
||||||
|
click.echo("OCSP responder subnets: %s" % config.OCSP_SUBNETS)
|
||||||
|
click.echo("CRL subnets: %s" % config.CRL_SUBNETS)
|
||||||
|
click.echo("SCEP subnets: %s" % config.SCEP_SUBNETS)
|
||||||
|
|
||||||
click.echo("Loading signature profiles:")
|
click.echo("Loading signature profiles:")
|
||||||
for profile in config.PROFILES.values():
|
for profile in config.PROFILES.values():
|
||||||
click.echo("- %s" % profile)
|
click.echo("- %s" % profile)
|
||||||
@ -1625,6 +1669,35 @@ def certidude_test(recipient):
|
|||||||
to=recipient
|
to=recipient
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@click.command("list", help="List tokens")
|
||||||
|
def certidude_token_list():
|
||||||
|
from certidude import config
|
||||||
|
from certidude.tokens import TokenManager
|
||||||
|
token_manager = TokenManager(config.TOKEN_DATABASE)
|
||||||
|
cols = "uuid", "expires", "subject", "state"
|
||||||
|
now = datetime.utcnow()
|
||||||
|
for token in token_manager.list(expired=True, used=True, token=True):
|
||||||
|
token["state"] = "used" if token.get("used") else ("valid" if token.get("expires") > now else "expired")
|
||||||
|
print(";".join([str(token.get(col)) for col in cols]))
|
||||||
|
|
||||||
|
@click.command("purge", help="Purge tokens")
|
||||||
|
@click.option("-a", "--all", default=False, is_flag=True, help="Purge all not only expired tokens")
|
||||||
|
def certidude_token_purge(all):
|
||||||
|
from certidude import config
|
||||||
|
from certidude.tokens import TokenManager
|
||||||
|
token_manager = TokenManager(config.TOKEN_DATABASE)
|
||||||
|
print(token_manager.purge(all))
|
||||||
|
|
||||||
|
@click.command("issue", help="Issue token")
|
||||||
|
@click.option("-m", "--subject-mail", default=None, help="Subject e-mail override")
|
||||||
|
@click.argument("subject")
|
||||||
|
def certidude_token_issue(subject, subject_mail):
|
||||||
|
from certidude import config
|
||||||
|
from certidude.tokens import TokenManager
|
||||||
|
from certidude.user import User
|
||||||
|
token_manager = TokenManager(config.TOKEN_DATABASE)
|
||||||
|
token_manager.issue(None, User.objects.get(subject), subject_mail)
|
||||||
|
|
||||||
|
|
||||||
@click.group("strongswan", help="strongSwan helpers")
|
@click.group("strongswan", help="strongSwan helpers")
|
||||||
def certidude_setup_strongswan(): pass
|
def certidude_setup_strongswan(): pass
|
||||||
@ -1635,6 +1708,9 @@ def certidude_setup_openvpn(): pass
|
|||||||
@click.group("setup", help="Getting started section")
|
@click.group("setup", help="Getting started section")
|
||||||
def certidude_setup(): pass
|
def certidude_setup(): pass
|
||||||
|
|
||||||
|
@click.group("token", help="Token management")
|
||||||
|
def certidude_token(): pass
|
||||||
|
|
||||||
@click.group()
|
@click.group()
|
||||||
def entry_point(): pass
|
def entry_point(): pass
|
||||||
|
|
||||||
@ -1649,6 +1725,10 @@ certidude_setup.add_command(certidude_setup_openvpn)
|
|||||||
certidude_setup.add_command(certidude_setup_strongswan)
|
certidude_setup.add_command(certidude_setup_strongswan)
|
||||||
certidude_setup.add_command(certidude_setup_nginx)
|
certidude_setup.add_command(certidude_setup_nginx)
|
||||||
certidude_setup.add_command(certidude_setup_yubikey)
|
certidude_setup.add_command(certidude_setup_yubikey)
|
||||||
|
certidude_token.add_command(certidude_token_list)
|
||||||
|
certidude_token.add_command(certidude_token_purge)
|
||||||
|
certidude_token.add_command(certidude_token_issue)
|
||||||
|
entry_point.add_command(certidude_token)
|
||||||
entry_point.add_command(certidude_setup)
|
entry_point.add_command(certidude_setup)
|
||||||
entry_point.add_command(certidude_serve)
|
entry_point.add_command(certidude_serve)
|
||||||
entry_point.add_command(certidude_enroll)
|
entry_point.add_command(certidude_enroll)
|
||||||
|
@ -2,6 +2,16 @@
|
|||||||
import os
|
import os
|
||||||
import click
|
import click
|
||||||
import subprocess
|
import subprocess
|
||||||
|
from random import SystemRandom
|
||||||
|
|
||||||
|
random = SystemRandom()
|
||||||
|
|
||||||
|
try:
|
||||||
|
from time import time_ns
|
||||||
|
except ImportError:
|
||||||
|
from time import time
|
||||||
|
def time_ns():
|
||||||
|
return int(time() * 10**9) # 64 bits integer, 32 ns bits
|
||||||
|
|
||||||
MAPPING = dict(
|
MAPPING = dict(
|
||||||
common_name="CN",
|
common_name="CN",
|
||||||
@ -122,3 +132,6 @@ def pip(packages):
|
|||||||
pip.main(['install'] + packages.split(" "))
|
pip.main(['install'] + packages.split(" "))
|
||||||
return True
|
return True
|
||||||
|
|
||||||
|
def generate_serial():
|
||||||
|
return time_ns() << 56 | random.randint(0, 2**56-1)
|
||||||
|
|
||||||
|
@ -23,6 +23,7 @@ LDAP_AUTHENTICATION_URI = cp.get("authentication", "ldap uri")
|
|||||||
LDAP_GSSAPI_CRED_CACHE = cp.get("accounts", "ldap gssapi credential cache")
|
LDAP_GSSAPI_CRED_CACHE = cp.get("accounts", "ldap gssapi credential cache")
|
||||||
LDAP_ACCOUNTS_URI = cp.get("accounts", "ldap uri")
|
LDAP_ACCOUNTS_URI = cp.get("accounts", "ldap uri")
|
||||||
LDAP_BASE = cp.get("accounts", "ldap base")
|
LDAP_BASE = cp.get("accounts", "ldap base")
|
||||||
|
LDAP_MAIL_ATTRIBUTE = cp.get("accounts", "ldap mail attribute")
|
||||||
|
|
||||||
USER_SUBNETS = set([ipaddress.ip_network(j) for j in
|
USER_SUBNETS = set([ipaddress.ip_network(j) for j in
|
||||||
cp.get("authorization", "user subnets").split(" ") if j])
|
cp.get("authorization", "user subnets").split(" ") if j])
|
||||||
@ -71,8 +72,7 @@ USER_MULTIPLE_CERTIFICATES = {
|
|||||||
|
|
||||||
REQUEST_SUBMISSION_ALLOWED = cp.getboolean("authority", "request submission allowed")
|
REQUEST_SUBMISSION_ALLOWED = cp.getboolean("authority", "request submission allowed")
|
||||||
AUTHORITY_CERTIFICATE_URL = cp.get("signature", "authority certificate url")
|
AUTHORITY_CERTIFICATE_URL = cp.get("signature", "authority certificate url")
|
||||||
AUTHORITY_CRL_URL = cp.get("signature", "revoked url")
|
AUTHORITY_CRL_URL = "http://%s/api/revoked" % const.FQDN
|
||||||
AUTHORITY_OCSP_URL = cp.get("signature", "responder url")
|
|
||||||
|
|
||||||
REVOCATION_LIST_LIFETIME = cp.getint("signature", "revocation list lifetime")
|
REVOCATION_LIST_LIFETIME = cp.getint("signature", "revocation list lifetime")
|
||||||
|
|
||||||
@ -88,6 +88,8 @@ USERS_GROUP = cp.get("authorization", "posix user group")
|
|||||||
ADMIN_GROUP = cp.get("authorization", "posix admin group")
|
ADMIN_GROUP = cp.get("authorization", "posix admin group")
|
||||||
LDAP_USER_FILTER = cp.get("authorization", "ldap user filter")
|
LDAP_USER_FILTER = cp.get("authorization", "ldap user filter")
|
||||||
LDAP_ADMIN_FILTER = cp.get("authorization", "ldap admin filter")
|
LDAP_ADMIN_FILTER = cp.get("authorization", "ldap admin filter")
|
||||||
|
LDAP_COMPUTER_FILTER = cp.get("authorization", "ldap computer filter")
|
||||||
|
|
||||||
if "%s" not in LDAP_USER_FILTER: raise ValueError("No placeholder %s for username in 'ldap user filter'")
|
if "%s" not in LDAP_USER_FILTER: raise ValueError("No placeholder %s for username in 'ldap user filter'")
|
||||||
if "%s" not in LDAP_ADMIN_FILTER: raise ValueError("No placeholder %s for username in 'ldap admin filter'")
|
if "%s" not in LDAP_ADMIN_FILTER: raise ValueError("No placeholder %s for username in 'ldap admin filter'")
|
||||||
|
|
||||||
@ -95,9 +97,9 @@ TAG_TYPES = [j.split("/", 1) + [cp.get("tagging", j)] for j in cp.options("taggi
|
|||||||
|
|
||||||
# Tokens
|
# Tokens
|
||||||
TOKEN_URL = cp.get("token", "url")
|
TOKEN_URL = cp.get("token", "url")
|
||||||
TOKEN_LIFETIME = cp.getint("token", "lifetime") * 60 # Convert minutes to seconds
|
TOKEN_BACKEND = cp.get("token", "backend")
|
||||||
TOKEN_SECRET = cp.get("token", "secret").encode("ascii")
|
TOKEN_LIFETIME = timedelta(minutes=cp.getint("token", "lifetime")) # Convert minutes to seconds
|
||||||
|
TOKEN_DATABASE = cp.get("token", "database")
|
||||||
# TODO: Check if we don't have base or servers
|
# TODO: Check if we don't have base or servers
|
||||||
|
|
||||||
# The API call for looking up scripts uses following directory as root
|
# The API call for looking up scripts uses following directory as root
|
||||||
@ -115,11 +117,13 @@ PROFILES = dict([(key, SignatureProfile(key,
|
|||||||
profile_config.get(key, "key usage"),
|
profile_config.get(key, "key usage"),
|
||||||
profile_config.get(key, "extended key usage"),
|
profile_config.get(key, "extended key usage"),
|
||||||
profile_config.get(key, "common name"),
|
profile_config.get(key, "common name"),
|
||||||
)) for key in profile_config.sections()])
|
profile_config.get(key, "revoked url"),
|
||||||
|
profile_config.get(key, "responder url")
|
||||||
|
)) for key in profile_config.sections() if profile_config.getboolean(key, "enabled")])
|
||||||
|
|
||||||
cp2 = configparser.RawConfigParser()
|
cp2 = configparser.RawConfigParser()
|
||||||
cp2.readfp(open(const.BUILDER_CONFIG_PATH, "r"))
|
cp2.readfp(open(const.BUILDER_CONFIG_PATH, "r"))
|
||||||
IMAGE_BUILDER_PROFILES = [(j, cp2.get(j, "title"), cp2.get(j, "rename")) for j in cp2.sections()]
|
IMAGE_BUILDER_PROFILES = [(j, cp2.get(j, "title"), cp2.get(j, "rename")) for j in cp2.sections() if cp2.getboolean(j, "enabled")]
|
||||||
|
|
||||||
TOKEN_OVERWRITE_PERMITTED=True
|
TOKEN_OVERWRITE_PERMITTED=True
|
||||||
|
|
||||||
|
@ -3,17 +3,21 @@ import click
|
|||||||
import os
|
import os
|
||||||
import socket
|
import socket
|
||||||
import sys
|
import sys
|
||||||
|
from datetime import timedelta
|
||||||
|
|
||||||
KEY_SIZE = 1024 if os.getenv("TRAVIS") else 4096
|
KEY_SIZE = 1024 if os.getenv("TRAVIS") else 4096
|
||||||
CURVE_NAME = "secp384r1"
|
CURVE_NAME = "secp384r1"
|
||||||
RE_FQDN = "^(([a-z0-9]|[a-z0-9][a-z0-9\-_]*[a-z0-9])\.)+([a-z0-9]|[a-z0-9][a-z0-9\-_]*[a-z0-9])?$"
|
RE_FQDN = "^(([a-z0-9]|[a-z0-9][a-z0-9\-_]*[a-z0-9])\.)+([a-z0-9]|[a-z0-9][a-z0-9\-_]*[a-z0-9])?$"
|
||||||
RE_HOSTNAME = "^[a-z0-9]([a-z0-9\-_]{0,61}[a-z0-9])?$"
|
RE_HOSTNAME = "^[a-z0-9]([a-z0-9\-_]{0,61}[a-z0-9])?$"
|
||||||
RE_COMMON_NAME = "^[A-Za-z0-9\-\.\_@]+$"
|
RE_COMMON_NAME = "^[A-Za-z0-9\-\.\_@]+$"
|
||||||
|
CLOCK_SKEW_TOLERANCE = timedelta(minutes=5) # Kerberos-like clock skew tolerance
|
||||||
|
|
||||||
RUN_DIR = "/run/certidude"
|
RUN_DIR = "/run/certidude"
|
||||||
CONFIG_DIR = "/etc/certidude"
|
CONFIG_DIR = "/etc/certidude"
|
||||||
SERVER_CONFIG_PATH = os.path.join(CONFIG_DIR, "server.conf")
|
SERVER_CONFIG_PATH = os.path.join(CONFIG_DIR, "server.conf")
|
||||||
BUILDER_CONFIG_PATH = os.path.join(CONFIG_DIR, "builder.conf")
|
BUILDER_CONFIG_PATH = os.path.join(CONFIG_DIR, "builder.conf")
|
||||||
|
SCRIPT_DIR = os.path.join(CONFIG_DIR, "script")
|
||||||
|
BUILDER_SITE_SCRIPT = os.path.join(SCRIPT_DIR, "site.sh")
|
||||||
PROFILE_CONFIG_PATH = os.path.join(CONFIG_DIR, "profile.conf")
|
PROFILE_CONFIG_PATH = os.path.join(CONFIG_DIR, "profile.conf")
|
||||||
CLIENT_CONFIG_PATH = os.path.join(CONFIG_DIR, "client.conf")
|
CLIENT_CONFIG_PATH = os.path.join(CONFIG_DIR, "client.conf")
|
||||||
SERVICES_CONFIG_PATH = os.path.join(CONFIG_DIR, "services.conf")
|
SERVICES_CONFIG_PATH = os.path.join(CONFIG_DIR, "services.conf")
|
||||||
|
@ -4,7 +4,7 @@ from datetime import timedelta
|
|||||||
from certidude import const
|
from certidude import const
|
||||||
|
|
||||||
class SignatureProfile(object):
|
class SignatureProfile(object):
|
||||||
def __init__(self, slug, title, ou, ca, lifetime, key_usage, extended_key_usage, common_name):
|
def __init__(self, slug, title, ou, ca, lifetime, key_usage, extended_key_usage, common_name, revoked_url, responder_url):
|
||||||
self.slug = slug
|
self.slug = slug
|
||||||
self.title = title
|
self.title = title
|
||||||
self.ou = ou or None
|
self.ou = ou or None
|
||||||
@ -12,6 +12,9 @@ class SignatureProfile(object):
|
|||||||
self.lifetime = lifetime
|
self.lifetime = lifetime
|
||||||
self.key_usage = set(key_usage.split(" ")) if key_usage else set()
|
self.key_usage = set(key_usage.split(" ")) if key_usage else set()
|
||||||
self.extended_key_usage = set(extended_key_usage.split(" ")) if extended_key_usage else set()
|
self.extended_key_usage = set(extended_key_usage.split(" ")) if extended_key_usage else set()
|
||||||
|
self.responder_url = responder_url
|
||||||
|
self.revoked_url = revoked_url
|
||||||
|
|
||||||
if common_name.startswith("^"):
|
if common_name.startswith("^"):
|
||||||
self.common_name = common_name
|
self.common_name = common_name
|
||||||
elif common_name == "RE_HOSTNAME":
|
elif common_name == "RE_HOSTNAME":
|
||||||
@ -39,7 +42,7 @@ class SignatureProfile(object):
|
|||||||
|
|
||||||
def serialize(self):
|
def serialize(self):
|
||||||
return dict([(key, getattr(self,key)) for key in (
|
return dict([(key, getattr(self,key)) for key in (
|
||||||
"slug", "title", "ou", "ca", "lifetime", "key_usage", "extended_key_usage", "common_name")])
|
"slug", "title", "ou", "ca", "lifetime", "key_usage", "extended_key_usage", "common_name", "responder_url", "revoked_url")])
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
bits = []
|
bits = []
|
||||||
@ -47,6 +50,10 @@ class SignatureProfile(object):
|
|||||||
bits.append("%d years" % (self.lifetime / 365))
|
bits.append("%d years" % (self.lifetime / 365))
|
||||||
if self.lifetime % 365:
|
if self.lifetime % 365:
|
||||||
bits.append("%d days" % (self.lifetime % 365))
|
bits.append("%d days" % (self.lifetime % 365))
|
||||||
return "%s (title=%s, ca=%s, ou=%s, lifetime=%s, key_usage=%s, extended_key_usage=%s, common_name=%s)" % (
|
return "%s (title=%s, ca=%s, ou=%s, lifetime=%s, key_usage=%s, extended_key_usage=%s, common_name=%s, responder_url=%s, revoked_url=%s)" % (
|
||||||
self.slug, self.title, self.ca, self.ou, " ".join(bits), self.key_usage, self.extended_key_usage, self.common_name)
|
self.slug, self.title, self.ca, self.ou, " ".join(bits),
|
||||||
|
self.key_usage, self.extended_key_usage,
|
||||||
|
repr(self.common_name),
|
||||||
|
repr(self.responder_url),
|
||||||
|
repr(self.revoked_url))
|
||||||
|
|
||||||
|
@ -14,6 +14,9 @@ class RelationalMixin(object):
|
|||||||
|
|
||||||
SQL_CREATE_TABLES = ""
|
SQL_CREATE_TABLES = ""
|
||||||
|
|
||||||
|
class DoesNotExist(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
def __init__(self, uri):
|
def __init__(self, uri):
|
||||||
self.uri = urlparse(uri)
|
self.uri = urlparse(uri)
|
||||||
|
|
||||||
@ -29,7 +32,8 @@ class RelationalMixin(object):
|
|||||||
if self.uri.netloc:
|
if self.uri.netloc:
|
||||||
raise ValueError("Malformed database URI %s" % self.uri)
|
raise ValueError("Malformed database URI %s" % self.uri)
|
||||||
import sqlite3
|
import sqlite3
|
||||||
conn = sqlite3.connect(self.uri.path)
|
conn = sqlite3.connect(self.uri.path,
|
||||||
|
detect_types=sqlite3.PARSE_DECLTYPES | sqlite3.PARSE_COLNAMES)
|
||||||
else:
|
else:
|
||||||
raise NotImplementedError("Unsupported database scheme %s, currently only mysql://user:pass@host/database or sqlite:///path/to/database.sqlite is supported" % o.scheme)
|
raise NotImplementedError("Unsupported database scheme %s, currently only mysql://user:pass@host/database or sqlite:///path/to/database.sqlite is supported" % o.scheme)
|
||||||
|
|
||||||
@ -74,7 +78,6 @@ class RelationalMixin(object):
|
|||||||
conn.close()
|
conn.close()
|
||||||
return rowid
|
return rowid
|
||||||
|
|
||||||
|
|
||||||
def iterfetch(self, query, *args):
|
def iterfetch(self, query, *args):
|
||||||
conn = self.sql_connect()
|
conn = self.sql_connect()
|
||||||
cursor = conn.cursor()
|
cursor = conn.cursor()
|
||||||
@ -86,3 +89,24 @@ class RelationalMixin(object):
|
|||||||
cursor.close()
|
cursor.close()
|
||||||
conn.close()
|
conn.close()
|
||||||
return tuple(g())
|
return tuple(g())
|
||||||
|
|
||||||
|
def get(self, query, *args):
|
||||||
|
conn = self.sql_connect()
|
||||||
|
cursor = conn.cursor()
|
||||||
|
cursor.execute(query, args)
|
||||||
|
row = cursor.fetchone()
|
||||||
|
cursor.close()
|
||||||
|
conn.close()
|
||||||
|
if not row:
|
||||||
|
raise self.DoesNotExist("No matches for query '%s' with parameters %s" % (query, repr(args)))
|
||||||
|
return row
|
||||||
|
|
||||||
|
def execute(self, query, *args):
|
||||||
|
conn = self.sql_connect()
|
||||||
|
cursor = conn.cursor()
|
||||||
|
cursor.execute(query, args)
|
||||||
|
affected_rows = cursor.rowcount
|
||||||
|
cursor.close()
|
||||||
|
conn.commit()
|
||||||
|
conn.close()
|
||||||
|
return affected_rows
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
create table if not exists log (
|
create table if not exists log (
|
||||||
|
id integer primary key autoincrement,
|
||||||
created datetime,
|
created datetime,
|
||||||
facility varchar(30),
|
facility varchar(30),
|
||||||
level int,
|
level int,
|
||||||
|
17
certidude/sql/sqlite/token_issue.sql
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
insert into token (
|
||||||
|
created,
|
||||||
|
expires,
|
||||||
|
uuid,
|
||||||
|
issuer,
|
||||||
|
subject,
|
||||||
|
mail,
|
||||||
|
profile
|
||||||
|
) values (
|
||||||
|
?,
|
||||||
|
?,
|
||||||
|
?,
|
||||||
|
?,
|
||||||
|
?,
|
||||||
|
?,
|
||||||
|
?
|
||||||
|
);
|
13
certidude/sql/sqlite/token_tables.sql
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
create table if not exists token (
|
||||||
|
id integer primary key autoincrement,
|
||||||
|
created datetime,
|
||||||
|
used datetime,
|
||||||
|
expires datetime,
|
||||||
|
uuid char(32),
|
||||||
|
issuer char(30),
|
||||||
|
subject varchar(30),
|
||||||
|
mail varchar(128),
|
||||||
|
profile varchar(10),
|
||||||
|
|
||||||
|
constraint unique_uuid unique(uuid)
|
||||||
|
)
|
BIN
certidude/static/img/ubuntu-01-edit-connections.png
Normal file
After Width: | Height: | Size: 393 KiB |
BIN
certidude/static/img/ubuntu-02-network-connections.png
Normal file
After Width: | Height: | Size: 368 KiB |
BIN
certidude/static/img/ubuntu-03-import-saved-config.png
Normal file
After Width: | Height: | Size: 328 KiB |
BIN
certidude/static/img/ubuntu-04-select-file.png
Normal file
After Width: | Height: | Size: 139 KiB |
BIN
certidude/static/img/ubuntu-05-profile-imported.png
Normal file
After Width: | Height: | Size: 338 KiB |
BIN
certidude/static/img/ubuntu-06-ipv4-settings.png
Normal file
After Width: | Height: | Size: 318 KiB |
BIN
certidude/static/img/ubuntu-07-disable-default-route.png
Normal file
After Width: | Height: | Size: 342 KiB |
BIN
certidude/static/img/ubuntu-08-activate-connection.png
Normal file
After Width: | Height: | Size: 394 KiB |
BIN
certidude/static/img/windows-01-download-openvpn.png
Normal file
After Width: | Height: | Size: 132 KiB |
BIN
certidude/static/img/windows-02-install-openvpn.png
Normal file
After Width: | Height: | Size: 502 KiB |
BIN
certidude/static/img/windows-03-move-config-file.png
Normal file
After Width: | Height: | Size: 137 KiB |
BIN
certidude/static/img/windows-04-connect.png
Normal file
After Width: | Height: | Size: 583 KiB |
BIN
certidude/static/img/windows-05-connected.png
Normal file
After Width: | Height: | Size: 638 KiB |
@ -20,20 +20,14 @@
|
|||||||
<div class="collapse navbar-collapse" id="navbarsExampleDefault">
|
<div class="collapse navbar-collapse" id="navbarsExampleDefault">
|
||||||
<ul class="navbar-nav mr-auto">
|
<ul class="navbar-nav mr-auto">
|
||||||
<li class="nav-item">
|
<li class="nav-item">
|
||||||
<a class="nav-link disabled dashboard" href="#columns=2">Dashboard</a>
|
<a class="nav-link disabled dashboard" href="#">Dashboard</a>
|
||||||
</li>
|
</li>
|
||||||
<li class="nav-item">
|
<li class="nav-item hidden-xl-up">
|
||||||
<a class="nav-link" href="#columns=3">Wider</a>
|
|
||||||
</li>
|
|
||||||
<li class="nav-item">
|
|
||||||
<a class="nav-link" href="#columns=4">Widest</a>
|
|
||||||
</li>
|
|
||||||
<li class="nav-item">
|
|
||||||
<a class="nav-link" href="#">Log</a>
|
<a class="nav-link" href="#">Log</a>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
<form class="form-inline my-2 my-lg-0">
|
<form class="form-inline my-2 my-lg-0">
|
||||||
<input id="search" class="form-control mr-sm-2" type="search" placeholder="🔍">
|
<input id="search" class="form-control mr-sm-2" style="display:none;" type="search" placeholder="🔍">
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
</nav>
|
</nav>
|
||||||
|
@ -1,15 +1,8 @@
|
|||||||
|
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
const KEYWORDS = [
|
const KEY_SIZE = 2048;
|
||||||
["Android", "android"],
|
const DEVICE_KEYWORDS = ["Android", "iPhone", "iPad", "Windows", "Ubuntu", "Fedora", "Mac", "Linux"];
|
||||||
["iPhone", "iphone"],
|
|
||||||
["iPad", "ipad"],
|
|
||||||
["Ubuntu", "ubuntu"],
|
|
||||||
["Fedora", "fedora"],
|
|
||||||
["Linux", "linux"],
|
|
||||||
["Macintosh", "mac"],
|
|
||||||
];
|
|
||||||
|
|
||||||
jQuery.timeago.settings.allowFuture = true;
|
jQuery.timeago.settings.allowFuture = true;
|
||||||
|
|
||||||
@ -17,38 +10,217 @@ function normalizeCommonName(j) {
|
|||||||
return j.replace("@", "--").split(".").join("-"); // dafuq ?!
|
return j.replace("@", "--").split(".").join("-"); // dafuq ?!
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function onShowAll() {
|
||||||
|
var options = document.querySelectorAll(".option");
|
||||||
|
for (i = 0; i < options.length; i++) {
|
||||||
|
options[i].style.display = "block";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onKeyGen() {
|
||||||
|
if (window.navigator.userAgent.indexOf(" Edge/") >= 0) {
|
||||||
|
$("#enroll .loader-container").hide();
|
||||||
|
$("#enroll .edge-broken").show();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
window.keys = forge.pki.rsa.generateKeyPair(KEY_SIZE);
|
||||||
|
console.info('Key-pair created.');
|
||||||
|
|
||||||
|
// Device identifier
|
||||||
|
var dig = forge.md.sha384.create();
|
||||||
|
dig.update(window.navigator.userAgent);
|
||||||
|
|
||||||
|
var prefix = "unknown";
|
||||||
|
for (i in DEVICE_KEYWORDS) {
|
||||||
|
var keyword = DEVICE_KEYWORDS[i];
|
||||||
|
if (window.navigator.userAgent.indexOf(keyword) >= 0) {
|
||||||
|
prefix = keyword.toLowerCase();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
window.identifier = prefix + "-" + dig.digest().toHex().substring(0, 8);
|
||||||
|
console.info("Device identifier:", identifier);
|
||||||
|
|
||||||
|
window.common_name = query.subject + "@" + identifier;
|
||||||
|
|
||||||
|
window.csr = forge.pki.createCertificationRequest();
|
||||||
|
csr.publicKey = keys.publicKey;
|
||||||
|
csr.setSubject([{
|
||||||
|
name: 'commonName', value: common_name
|
||||||
|
}]);
|
||||||
|
|
||||||
|
csr.sign(keys.privateKey, forge.md.sha384.create());
|
||||||
|
console.info('Certification request created');
|
||||||
|
|
||||||
|
|
||||||
|
$("#enroll .loader-container").hide();
|
||||||
|
|
||||||
|
var prefix = null;
|
||||||
|
for (i in DEVICE_KEYWORDS) {
|
||||||
|
var keyword = DEVICE_KEYWORDS[i];
|
||||||
|
if (window.navigator.userAgent.indexOf(keyword) >= 0) {
|
||||||
|
prefix = keyword.toLowerCase();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (prefix == null) {
|
||||||
|
$(".option").show();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var protocols = query.protocols.split(",");
|
||||||
|
console.info("Showing snippets for:", protocols);
|
||||||
|
for (var j = 0; j < protocols.length; j++) {
|
||||||
|
var options = document.querySelectorAll(".option." + protocols[j] + "." + prefix);
|
||||||
|
for (i = 0; i < options.length; i++) {
|
||||||
|
options[i].style.display = "block";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onEnroll(encoding) {
|
||||||
|
console.info("User agent:", window.navigator.userAgent);
|
||||||
|
var xhr = new XMLHttpRequest();
|
||||||
|
xhr.open('GET', "/api/certificate");
|
||||||
|
xhr.onload = function() {
|
||||||
|
if (xhr.status === 200) {
|
||||||
|
var ca = forge.pki.certificateFromPem(xhr.responseText);
|
||||||
|
console.info("Got CA certificate:");
|
||||||
|
var xhr2 = new XMLHttpRequest();
|
||||||
|
xhr2.open("PUT", "/api/token/?token=" + query.token );
|
||||||
|
xhr2.onload = function() {
|
||||||
|
if (xhr2.status === 200) {
|
||||||
|
var a = document.createElement("a");
|
||||||
|
var cert = forge.pki.certificateFromPem(xhr2.responseText);
|
||||||
|
console.info("Got signed certificate:", xhr2.responseText);
|
||||||
|
var p12 = forge.pkcs12.toPkcs12Asn1(
|
||||||
|
keys.privateKey, [cert, ca], "", {algorithm: '3des'});
|
||||||
|
|
||||||
|
switch(encoding) {
|
||||||
|
case 'p12':
|
||||||
|
var buf = forge.asn1.toDer(p12).getBytes();
|
||||||
|
var mimetype = "application/x-pkcs12"
|
||||||
|
a.download = query.router + ".p12";
|
||||||
|
break
|
||||||
|
case 'sswan':
|
||||||
|
var buf = JSON.stringify({
|
||||||
|
uuid: "a061d140-d3f9-4db7-b2f8-32d6703f4618",
|
||||||
|
name: identifier,
|
||||||
|
type: "ikev2-cert",
|
||||||
|
'ike-proposal': 'aes256-sha384-prfsha384-modp2048',
|
||||||
|
'esp-proposal': 'aes128gcm16-aes128gmac-modp2048',
|
||||||
|
remote: { addr: query.router },
|
||||||
|
local: { p12: forge.util.encode64(forge.asn1.toDer(p12).getBytes()) }
|
||||||
|
});
|
||||||
|
console.info("Buf is:", buf);
|
||||||
|
var mimetype = "application/vnd.strongswan.profile"
|
||||||
|
a.download = query.router + ".sswan";
|
||||||
|
break
|
||||||
|
case 'ovpn':
|
||||||
|
var buf = nunjucks.render('snippets/openvpn-client.conf', {
|
||||||
|
session: {
|
||||||
|
authority: {
|
||||||
|
certificate: {
|
||||||
|
common_name: "Certidude at " + window.location.hostname,
|
||||||
|
algorithm: "rsa"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
service: {
|
||||||
|
protocols: query.protocols.split(","),
|
||||||
|
routers: [query.router],
|
||||||
|
}
|
||||||
|
},
|
||||||
|
key: forge.pki.privateKeyToPem(keys.privateKey),
|
||||||
|
cert: xhr2.responseText,
|
||||||
|
ca: xhr.responseText
|
||||||
|
});
|
||||||
|
var mimetype = "application/x-openvpn-profile";
|
||||||
|
a.download = query.router + ".ovpn";
|
||||||
|
break
|
||||||
|
case 'mobileconfig':
|
||||||
|
var p12 = forge.pkcs12.toPkcs12Asn1(
|
||||||
|
keys.privateKey, [cert, ca], "1234", {algorithm: '3des'});
|
||||||
|
var buf = nunjucks.render('snippets/ios.mobileconfig', {
|
||||||
|
session: {
|
||||||
|
authority: {
|
||||||
|
certificate: {
|
||||||
|
common_name: "Certidude at " + window.location.hostname,
|
||||||
|
algorithm: "rsa"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
common_name: common_name,
|
||||||
|
gateway: query.router,
|
||||||
|
p12: forge.util.encode64(forge.asn1.toDer(p12).getBytes()),
|
||||||
|
ca: forge.util.encode64(forge.asn1.toDer(forge.pki.certificateToAsn1(ca)).getBytes())
|
||||||
|
});
|
||||||
|
var mimetype = "application/x-apple-aspen-config";
|
||||||
|
a.download = query.router + ".mobileconfig";
|
||||||
|
break
|
||||||
|
}
|
||||||
|
a.href = "data:" + mimetype + ";base64," + forge.util.encode64(buf);
|
||||||
|
console.info("Offering bundle for download");
|
||||||
|
document.body.appendChild(a); // Firefox needs this!
|
||||||
|
a.click();
|
||||||
|
} else {
|
||||||
|
if (xhr2.status == 403) { alert("Token used or expired"); }
|
||||||
|
console.info('Request failed. Returned status of ' + xhr2.status);
|
||||||
|
try {
|
||||||
|
var r = JSON.parse(xhr2.responseText);
|
||||||
|
console.info("Server said: " + r.title);
|
||||||
|
console.info(r.description);
|
||||||
|
} catch(e) {
|
||||||
|
console.info("Server said: " + xhr2.statusText);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
xhr2.send(forge.pki.certificationRequestToPem(csr));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
xhr.send();
|
||||||
|
}
|
||||||
|
|
||||||
function onHashChanged() {
|
function onHashChanged() {
|
||||||
var query = {};
|
window.query = {};
|
||||||
var a = location.hash.substring(1).split('&');
|
var a = location.hash.substring(1).split('&');
|
||||||
for (var i = 0; i < a.length; i++) {
|
for (var i = 0; i < a.length; i++) {
|
||||||
var b = a[i].split('=');
|
var b = a[i].split('=');
|
||||||
query[decodeURIComponent(b[0])] = decodeURIComponent(b[1] || '');
|
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);
|
console.info("Hash is now:", query);
|
||||||
|
|
||||||
if (window.location.protocol != "https:") {
|
if (window.location.protocol != "https:") {
|
||||||
$.get("/api/certificate/", function(blob) {
|
$.get("/api/certificate/", function(blob) {
|
||||||
$("#view-dashboard").html(env.render('views/insecure.html', { window: window,
|
$("#view-dashboard").html(env.render('views/insecure.html', { window: window,
|
||||||
authority_name: window.location.hostname,
|
session: { authority: {
|
||||||
session: { authority: { certificate: { blob: blob }}}
|
hostname: window.location.hostname,
|
||||||
|
certificate: { blob: blob }}}
|
||||||
}));
|
}));
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
loadAuthority(query);
|
if (query.action == "enroll") {
|
||||||
|
$("#view-dashboard").html(env.render('views/enroll.html'));
|
||||||
|
var options = document.querySelectorAll(".option");
|
||||||
|
for (i = 0; i < options.length; i++) {
|
||||||
|
options[i].style.display = "none";
|
||||||
|
}
|
||||||
|
setTimeout(onKeyGen, 100);
|
||||||
|
console.info("Generating key pair...");
|
||||||
|
} else {
|
||||||
|
loadAuthority(query);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function onTagClicked(tag) {
|
function onTagClicked(e) {
|
||||||
var cn = $(tag).attr("data-cn");
|
e.preventDefault();
|
||||||
var id = $(tag).attr("title");
|
var cn = $(e.target).attr("data-cn");
|
||||||
var value = $(tag).html();
|
var id = $(e.target).attr("title");
|
||||||
|
var value = $(e.target).html();
|
||||||
var updated = prompt("Enter new tag or clear to remove the tag", value);
|
var updated = prompt("Enter new tag or clear to remove the tag", value);
|
||||||
if (updated == "") {
|
if (updated == "") {
|
||||||
$(event.target).addClass("disabled");
|
$(event.target).addClass("disabled");
|
||||||
@ -57,7 +229,7 @@ function onTagClicked(tag) {
|
|||||||
url: "/api/signed/" + cn + "/tag/" + id + "/"
|
url: "/api/signed/" + cn + "/tag/" + id + "/"
|
||||||
});
|
});
|
||||||
} else if (updated && updated != value) {
|
} else if (updated && updated != value) {
|
||||||
$(tag).addClass("disabled");
|
$(e.target).addClass("disabled");
|
||||||
$.ajax({
|
$.ajax({
|
||||||
method: "PUT",
|
method: "PUT",
|
||||||
url: "/api/signed/" + cn + "/tag/" + id + "/",
|
url: "/api/signed/" + cn + "/tag/" + id + "/",
|
||||||
@ -77,9 +249,10 @@ function onTagClicked(tag) {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
function onNewTagClicked(menu) {
|
function onNewTagClicked(e) {
|
||||||
var cn = $(menu).attr("data-cn");
|
e.preventDefault();
|
||||||
var key = $(menu).attr("data-key");
|
var cn = $(e.target).attr("data-cn");
|
||||||
|
var key = $(e.target).attr("data-key");
|
||||||
var value = prompt("Enter new " + key + " tag for " + cn);
|
var value = prompt("Enter new " + key + " tag for " + cn);
|
||||||
if (!value) return;
|
if (!value) return;
|
||||||
if (value.length == 0) return;
|
if (value.length == 0) return;
|
||||||
@ -101,6 +274,7 @@ function onNewTagClicked(menu) {
|
|||||||
alert(e);
|
alert(e);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
function onTagFilterChanged() {
|
function onTagFilterChanged() {
|
||||||
@ -121,6 +295,7 @@ function onLogEntry (e) {
|
|||||||
message: e.message,
|
message: e.message,
|
||||||
severity: e.severity,
|
severity: e.severity,
|
||||||
fresh: e.fresh,
|
fresh: e.fresh,
|
||||||
|
keywords: e.message.toLowerCase().split(/,?[ <>/]+/).join("|")
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
@ -270,7 +445,7 @@ function onServerStopped() {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function onSendToken() {
|
function onIssueToken() {
|
||||||
$.ajax({
|
$.ajax({
|
||||||
method: "POST",
|
method: "POST",
|
||||||
url: "/api/token/",
|
url: "/api/token/",
|
||||||
@ -316,20 +491,16 @@ function loadAuthority(query) {
|
|||||||
|
|
||||||
console.info("Loaded:", session);
|
console.info("Loaded:", session);
|
||||||
$("#login").hide();
|
$("#login").hide();
|
||||||
|
$("#search").show();
|
||||||
if (!query.columns) {
|
|
||||||
query.columns = 2;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Render authority views
|
* Render authority views
|
||||||
**/
|
**/
|
||||||
$("#view-dashboard").html(env.render('views/authority.html', {
|
$("#view-dashboard").html(env.render('views/authority.html', {
|
||||||
session: session,
|
session: session,
|
||||||
window: window,
|
window: window
|
||||||
columns: query.columns,
|
}));
|
||||||
column_width: 12 / query.columns,
|
|
||||||
authority_name: window.location.hostname }));
|
|
||||||
$("time").timeago();
|
$("time").timeago();
|
||||||
if (session.authority) {
|
if (session.authority) {
|
||||||
$("#log input").each(function(i, e) {
|
$("#log input").each(function(i, e) {
|
||||||
@ -414,12 +585,8 @@ function loadAuthority(query) {
|
|||||||
$("#search").on("keyup", function() {
|
$("#search").on("keyup", function() {
|
||||||
if (window.searchTimeout) { clearTimeout(window.searchTimeout); }
|
if (window.searchTimeout) { clearTimeout(window.searchTimeout); }
|
||||||
window.searchTimeout = setTimeout(function() { $(window).trigger("search"); }, 500);
|
window.searchTimeout = setTimeout(function() { $(window).trigger("search"); }, 500);
|
||||||
console.info("Setting timeout", window.searchTimeout);
|
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log("Features enabled:", session.features);
|
|
||||||
|
|
||||||
if (session.request_submission_allowed) {
|
if (session.request_submission_allowed) {
|
||||||
$("#request_submit").click(function() {
|
$("#request_submit").click(function() {
|
||||||
$(this).addClass("busy");
|
$(this).addClass("busy");
|
||||||
@ -442,11 +609,9 @@ function loadAuthority(query) {
|
|||||||
alert(e);
|
alert(e);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
$("nav .nav-link.dashboard").removeClass("disabled").click(function() {
|
$("nav .nav-link.dashboard").removeClass("disabled").click(function() {
|
||||||
$("#column-requests").show();
|
$("#column-requests").show();
|
||||||
$("#column-signed").show();
|
$("#column-signed").show();
|
||||||
@ -458,31 +623,36 @@ function loadAuthority(query) {
|
|||||||
* Fetch log entries
|
* Fetch log entries
|
||||||
*/
|
*/
|
||||||
if (session.features.logging) {
|
if (session.features.logging) {
|
||||||
if (query.columns == 4) {
|
if ($("#column-log:visible").length) {
|
||||||
loadLog();
|
loadLog();
|
||||||
} else {
|
|
||||||
$("nav .nav-link.log").removeClass("disabled").click(function() {
|
|
||||||
$("#column-requests").show();
|
|
||||||
$("#column-signed").show();
|
|
||||||
$("#column-revoked").show();
|
|
||||||
$("#column-log").hide();
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
$("nav .nav-link.log").removeClass("disabled").click(function() {
|
||||||
|
loadLog();
|
||||||
|
$("#column-requests").show();
|
||||||
|
$("#column-signed").show();
|
||||||
|
$("#column-revoked").show();
|
||||||
|
$("#column-log").hide();
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
console.info("Log disabled");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function loadLog() {
|
function loadLog() {
|
||||||
if (window.log_initialized) return;
|
if (window.log_initialized) {
|
||||||
|
console.info("Log already loaded");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
console.info("Loading log...");
|
||||||
window.log_initialized = true;
|
window.log_initialized = true;
|
||||||
$.ajax({
|
$.ajax({
|
||||||
method: "GET",
|
method: "GET",
|
||||||
url: "/api/log/",
|
url: "/api/log/?limit=100",
|
||||||
dataType: "json",
|
dataType: "json",
|
||||||
success: function(entries, status, xhr) {
|
success: function(entries, status, xhr) {
|
||||||
console.info("Got", entries.length, "log entries");
|
console.info("Got", entries.length, "log entries");
|
||||||
console.info("j=", entries.length-1);
|
|
||||||
for (var j = entries.length-1; j--; ) {
|
for (var j = entries.length-1; j--; ) {
|
||||||
onLogEntry(entries[j]);
|
onLogEntry(entries[j]);
|
||||||
};
|
};
|
||||||
|
15
certidude/static/snippets/ansible-site.yml
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
- hosts: {% for router in session.service.routers %}
|
||||||
|
{{ router }}{% endfor %}
|
||||||
|
|
||||||
|
roles:
|
||||||
|
- role: certidude
|
||||||
|
authority_name: {{ session.authority.hostname }}
|
||||||
|
|
||||||
|
- role: ipsec_mesh
|
||||||
|
mesh_name: mymesh
|
||||||
|
authority_name: {{ session.authority.hostname }}
|
||||||
|
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 %}!
|
||||||
|
auto: start
|
||||||
|
nodes:{% for router in session.service.routers %}
|
||||||
|
{{ router }}: 172.27.{{ loop.index }}.0/24{% endfor %}
|
@ -1,25 +1,24 @@
|
|||||||
pip3 install git+https://github.com/laurivosandi/certidude/
|
pip3 install git+https://github.com/laurivosandi/certidude/
|
||||||
mkdir -p /etc/certidude/{client.conf.d,services.conf.d}
|
mkdir -p /etc/certidude/{client.conf.d,services.conf.d}
|
||||||
cat << EOF > /etc/certidude/client.conf.d/{{ authority_name }}.conf
|
|
||||||
[{{ authority_name }}]
|
cat << \EOF > /etc/certidude/client.conf.d/{{ session.authority.hostname }}.conf
|
||||||
|
[{{ session.authority.hostname }}]
|
||||||
trigger = interface up
|
trigger = interface up
|
||||||
common name = $HOSTNAME
|
common name = $HOSTNAME
|
||||||
system wide = true
|
system wide = true
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
cat << EOF > /etc/certidude/services.conf.d/{{ authority_name }}.conf
|
cat << EOF > /etc/certidude/services.conf.d/{{ session.authority.hostname }}.conf{% for router in session.service.routers %}{% if "ikev2" in session.service.protocols %}
|
||||||
{% for router in session.service.routers %}{% if "ikev2" in session.service.protocols %}
|
|
||||||
[IPSec to {{ router }}]
|
[IPSec to {{ router }}]
|
||||||
authority = {{ authority_name }}
|
authority = {{ session.authority.hostname }}
|
||||||
service = network-manager/strongswan
|
service = network-manager/strongswan
|
||||||
remote = {{ router }}
|
remote = {{ router }}
|
||||||
{% endif %}{% if "openvpn" in session.service.protocols %}
|
{% endif %}{% if "openvpn" in session.service.protocols %}
|
||||||
[OpenVPN to {{ router }}]
|
[OpenVPN to {{ router }}]
|
||||||
authority = {{ authority_name }}
|
authority = {{ session.authority.hostname }}
|
||||||
service = network-manager/openvpn
|
service = network-manager/openvpn
|
||||||
remote = {{ router }}
|
remote = {{ router }}
|
||||||
{% endif %}{% endfor %}
|
{% endif %}{% endfor %}EOF
|
||||||
EOF
|
|
||||||
|
|
||||||
certidude enroll
|
certidude enroll
|
||||||
|
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
# Create VPN gateway up/down script for reporting client IP addresses to CA
|
# Create VPN gateway up/down script for reporting client IP addresses to CA
|
||||||
cat <<\EOF > /etc/certidude/authority/{{ authority_name }}/updown
|
cat <<\EOF > /etc/certidude/authority/{{ session.authority.hostname }}/updown
|
||||||
#!/bin/sh
|
#!/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/"
|
CURL="curl -m 3 -f --key /etc/certidude/authority/{{ session.authority.hostname }}/host_key.pem --cert /etc/certidude/authority/{{ session.authority.hostname }}/host_cert.pem --cacert /etc/certidude/authority/{{ session.authority.hostname }}/ca_cert.pem https://{{ session.authority.hostname }}:8443/api/lease/"
|
||||||
|
|
||||||
case $PLUTO_VERB in
|
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" ;;
|
up-client) $CURL --data-urlencode "outer_address=$PLUTO_PEER" --data-urlencode "inner_address=$PLUTO_PEER_SOURCEIP" --data-urlencode "client=$PLUTO_PEER_ID" ;;
|
||||||
@ -15,5 +15,5 @@ case $script_type in
|
|||||||
esac
|
esac
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
chmod +x /etc/certidude/authority/{{ authority_name }}/updown
|
chmod +x /etc/certidude/authority/{{ session.authority.hostname }}/updown
|
||||||
|
|
||||||
|
97
certidude/static/snippets/ios.mobileconfig
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||||
|
<plist version="1.0">
|
||||||
|
<dict>
|
||||||
|
<!-- https://developer.apple.com/library/content/featuredarticles/iPhoneConfigurationProfileRef/Introduction/Introduction.html -->
|
||||||
|
<key>PayloadDisplayName</key>
|
||||||
|
<string>{{ gateway }}</string>
|
||||||
|
<!-- This is a reverse-DNS style unique identifier used to detect duplicate profiles -->
|
||||||
|
<key>PayloadIdentifier</key>
|
||||||
|
<string>org.example.vpn2</string>
|
||||||
|
<key>PayloadUUID</key>
|
||||||
|
<string>9f93912b-5fd2-4455-99fd-13b9a47b4581</string>
|
||||||
|
<key>PayloadType</key>
|
||||||
|
<string>Configuration</string>
|
||||||
|
<key>PayloadVersion</key>
|
||||||
|
<integer>1</integer>
|
||||||
|
<key>PayloadContent</key>
|
||||||
|
<array>
|
||||||
|
<dict>
|
||||||
|
<key>PayloadIdentifier</key>
|
||||||
|
<string>org.example.vpn2.conf1</string>
|
||||||
|
<key>PayloadUUID</key>
|
||||||
|
<string>29e4456d-3f03-4f15-b46f-4225d89465b7</string>
|
||||||
|
<key>PayloadType</key>
|
||||||
|
<string>com.apple.vpn.managed</string>
|
||||||
|
<key>PayloadVersion</key>
|
||||||
|
<integer>1</integer>
|
||||||
|
<key>UserDefinedName</key>
|
||||||
|
<string>{{ gateway }}</string>
|
||||||
|
<key>VPNType</key>
|
||||||
|
<string>IKEv2</string>
|
||||||
|
<key>IKEv2</key>
|
||||||
|
<dict>
|
||||||
|
<key>RemoteAddress</key>
|
||||||
|
<string>{{ gateway }}</string>
|
||||||
|
<key>RemoteIdentifier</key>
|
||||||
|
<string>{{ gateway }}</string>
|
||||||
|
<key>LocalIdentifier</key>
|
||||||
|
<string>{{ common_name }}</string>
|
||||||
|
<key>ServerCertificateIssuerCommonName</key>
|
||||||
|
<string>{{ session.authority.certificate.common_name }}</string>
|
||||||
|
<key>ServerCertificateCommonName</key>
|
||||||
|
<string>{{ gateway }}</string>
|
||||||
|
<key>AuthenticationMethod</key>
|
||||||
|
<string>Certificate</string>
|
||||||
|
<key>IKESecurityAssociationParameters</key>
|
||||||
|
<dict>
|
||||||
|
<key>EncryptionAlgorithm</key>
|
||||||
|
<string>AES-256</string>
|
||||||
|
<key>IntegrityAlgorithm</key>
|
||||||
|
<string>SHA2-384</string>
|
||||||
|
<key>DiffieHellmanGroup</key>
|
||||||
|
<integer>14</integer>
|
||||||
|
</dict>
|
||||||
|
<key>ChildSecurityAssociationParameters</key>
|
||||||
|
<dict>
|
||||||
|
<key>EncryptionAlgorithm</key>
|
||||||
|
<string>AES-128-GCM</string>
|
||||||
|
<key>IntegrityAlgorithm</key>
|
||||||
|
<string>SHA2-256</string>
|
||||||
|
<key>DiffieHellmanGroup</key>
|
||||||
|
<integer>14</integer>
|
||||||
|
</dict>
|
||||||
|
<key>EnablePFS</key>
|
||||||
|
<integer>1</integer>
|
||||||
|
<key>PayloadCertificateUUID</key>
|
||||||
|
<string>d60488c6-328e-4944-9c8d-61db8095c865</string>
|
||||||
|
</dict>
|
||||||
|
</dict>
|
||||||
|
<dict>
|
||||||
|
<key>PayloadIdentifier</key>
|
||||||
|
<string>ee.k-space.ca2.client</string>
|
||||||
|
<key>PayloadUUID</key>
|
||||||
|
<string>d60488c6-328e-4944-9c8d-61db8095c865</string>
|
||||||
|
<key>PayloadType</key>
|
||||||
|
<string>com.apple.security.pkcs12</string>
|
||||||
|
<key>PayloadVersion</key>
|
||||||
|
<integer>1</integer>
|
||||||
|
<key>PayloadContent</key>
|
||||||
|
<data>{{ p12 }}</data>
|
||||||
|
</dict>
|
||||||
|
<dict>
|
||||||
|
<key>PayloadIdentifier</key>
|
||||||
|
<string>org.example.ca</string>
|
||||||
|
<key>PayloadUUID</key>
|
||||||
|
<string>64988b2c-33e0-4adf-a432-6fbcae543408</string>
|
||||||
|
<key>PayloadType</key>
|
||||||
|
<string>com.apple.security.root</string>
|
||||||
|
<key>PayloadVersion</key>
|
||||||
|
<integer>1</integer>
|
||||||
|
<!-- This is the Base64 (PEM) encoded CA certificate -->
|
||||||
|
<key>PayloadContent</key>
|
||||||
|
<data>{{ ca }}</data>
|
||||||
|
</dict>
|
||||||
|
</array>
|
||||||
|
</dict>
|
||||||
|
</plist>
|
||||||
|
|
35
certidude/static/snippets/openvpn-client.conf
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
client
|
||||||
|
nobind{% for router in session.service.routers %}
|
||||||
|
remote {{ router }}{% endfor %}
|
||||||
|
proto tcp-client
|
||||||
|
port 443
|
||||||
|
tls-version-min 1.2
|
||||||
|
tls-cipher TLS-{% if session.authority.certificate.algorithm == "ec" %}ECDHE-ECDSA{% else %}DHE-RSA{% endif %}-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
|
||||||
|
{% if ca %}
|
||||||
|
<ca>
|
||||||
|
{{ ca }}
|
||||||
|
</ca>
|
||||||
|
{% else %}ca /etc/certidude/authority/{{ session.authority.hostname }}/ca_cert.pem{% endif %}
|
||||||
|
{% if key %}
|
||||||
|
<key>
|
||||||
|
{{ key }}
|
||||||
|
</key>
|
||||||
|
{% else %}key /etc/certidude/authority/{{ session.authority.hostname }}/host_key.pem{% endif %}
|
||||||
|
{% if cert %}
|
||||||
|
<cert>
|
||||||
|
{{ cert }}
|
||||||
|
</cert>
|
||||||
|
{% else %}cert /etc/certidude/authority/{{ session.authority.hostname }}/host_cert.pem{% endif %}
|
||||||
|
|
||||||
|
# To enable dynamic DNS server update on Ubuntu, uncomment these
|
||||||
|
#script-security 2
|
||||||
|
#up /etc/openvpn/update-resolv-conf
|
||||||
|
#down /etc/openvpn/update-resolv-conf
|
@ -2,26 +2,19 @@
|
|||||||
which apt && apt install openvpn
|
which apt && apt install openvpn
|
||||||
which dnf && dnf install openvpn
|
which dnf && dnf install openvpn
|
||||||
|
|
||||||
cat > /etc/openvpn/{{ authority_name }}.conf << EOF
|
# Create OpenVPN configuration file
|
||||||
client
|
cat > /etc/openvpn/{{ session.authority.hostname }}.conf << EOF
|
||||||
nobind
|
{% include "snippets/openvpn-client.conf" %}
|
||||||
{% 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
|
EOF
|
||||||
|
|
||||||
|
# Restart OpenVPN service
|
||||||
systemctl restart openvpn
|
systemctl restart openvpn
|
||||||
|
{#
|
||||||
|
|
||||||
|
Some notes:
|
||||||
|
|
||||||
|
- Ubuntu 16.04 ships OpenVPN 2.3 which doesn't support AES-128-GCM
|
||||||
|
- NetworkManager's OpenVPN profile importer doesn't understand multiple remotes
|
||||||
|
- Tunnelblick and OpenVPN Connect apps don't have a method to update CRL
|
||||||
|
|
||||||
|
#}
|
||||||
|
@ -65,9 +65,9 @@ for section in s2c_tcp s2c_udp; do
|
|||||||
# Common paths
|
# Common paths
|
||||||
uci set openvpn.$section.script_security=2
|
uci set openvpn.$section.script_security=2
|
||||||
uci set openvpn.$section.client_connect='/etc/certidude/updown'
|
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.key='/etc/certidude/authority/{{ session.authority.hostname }}/host_key.pem'
|
||||||
uci set openvpn.$section.cert='/etc/certidude/authority/{{ authority_name }}/host_cert.pem'
|
uci set openvpn.$section.cert='/etc/certidude/authority/{{ session.authority.hostname }}/host_cert.pem'
|
||||||
uci set openvpn.$section.ca='/etc/certidude/authority/{{ authority_name }}/ca_cert.pem'
|
uci set openvpn.$section.ca='/etc/certidude/authority/{{ session.authority.hostname }}/ca_cert.pem'
|
||||||
{% if session.authority.certificate.algorithm != "ec" %}uci set openvpn.$section.dh='/etc/certidude/dh.pem'{% endif %}
|
{% if session.authority.certificate.algorithm != "ec" %}uci set openvpn.$section.dh='/etc/certidude/dh.pem'{% endif %}
|
||||||
uci set openvpn.$section.enabled=1
|
uci set openvpn.$section.enabled=1
|
||||||
|
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
curl -f -L -H "Content-type: application/pkcs10" \
|
curl --cert-status -f -L -H "Content-type: application/pkcs10" \
|
||||||
--cacert /etc/certidude/authority/{{ authority_name }}/ca_cert.pem \
|
--cacert /etc/certidude/authority/{{ session.authority.hostname }}/ca_cert.pem \
|
||||||
--key /etc/certidude/authority/{{ authority_name }}/host_key.pem \
|
--key /etc/certidude/authority/{{ session.authority.hostname }}/host_key.pem \
|
||||||
--cert /etc/certidude/authority/{{ authority_name }}/host_cert.pem \
|
--cert /etc/certidude/authority/{{ session.authority.hostname }}/host_cert.pem \
|
||||||
--data-binary @/etc/certidude/authority/{{ authority_name }}/host_req.pem \
|
--data-binary @/etc/certidude/authority/{{ session.authority.hostname }}/host_req.pem \
|
||||||
-o /etc/certidude/authority/{{ authority_name }}/host_cert.pem \
|
-o /etc/certidude/authority/{{ session.authority.hostname }}/host_cert.pem \
|
||||||
'https://{{ authority_name }}:8443/api/request/?wait=yes'
|
'https://{{ session.authority.hostname }}:8443/api/request/?wait=yes'
|
||||||
|
@ -1,15 +1,11 @@
|
|||||||
|
# Use short hostname as common name
|
||||||
test -e /sbin/uci && NAME=$(uci get system.@system[0].hostname)
|
test -e /sbin/uci && NAME=$(uci get system.@system[0].hostname)
|
||||||
test -e /bin/hostname && NAME=$(hostname)
|
test -e /bin/hostname && NAME=$(hostname)
|
||||||
test -n "$NAME" || NAME=$(cat /proc/sys/kernel/hostname)
|
test -n "$NAME" || NAME=$(cat /proc/sys/kernel/hostname)
|
||||||
|
|
||||||
{% include "snippets/update-trust.sh" %}
|
|
||||||
|
|
||||||
{% include "snippets/request-common.sh" %}
|
{% include "snippets/request-common.sh" %}
|
||||||
|
# Submit CSR and save signed certificate
|
||||||
curl -f -L -H "Content-type: application/pkcs10" \
|
curl --cert-status -f -L -H "Content-type: application/pkcs10" \
|
||||||
--data-binary @/etc/certidude/authority/{{ authority_name }}/host_req.pem \
|
--data-binary @/etc/certidude/authority/{{ session.authority.hostname }}/host_req.pem \
|
||||||
-o /etc/certidude/authority/{{ authority_name }}/host_cert.pem \
|
-o /etc/certidude/authority/{{ session.authority.hostname }}/host_cert.pem \
|
||||||
'http://{{ authority_name }}/api/request/?wait=yes&autosign=yes'
|
'http://{{ session.authority.hostname }}/api/request/?wait=yes&autosign=yes'
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@ -1,14 +1,16 @@
|
|||||||
echo {{ session.authority.certificate.md5sum }} /etc/certidude/authority/{{ authority_name }}/ca_cert.pem | md5sum -c \
|
# Delete CA certificate if checksum doesn't match
|
||||||
|| rm -fv /etc/certidude/authority/{{ authority_name }}/*.pem
|
echo {{ session.authority.certificate.md5sum }} /etc/certidude/authority/{{ session.authority.hostname }}/ca_cert.pem | md5sum -c \
|
||||||
|
|| rm -fv /etc/certidude/authority/{{ session.authority.hostname }}/*.pem
|
||||||
{% include "snippets/store-authority.sh" %}
|
{% include "snippets/store-authority.sh" %}
|
||||||
test -e /etc/certidude/authority/{{ authority_name }}/host_key.pem \
|
{% include "snippets/update-trust.sh" %}
|
||||||
|
# Generate private key
|
||||||
|
test -e /etc/certidude/authority/{{ session.authority.hostname }}/host_key.pem \
|
||||||
|| {% if session.authority.certificate.algorithm == "ec" %}openssl ecparam -name secp384r1 -genkey -noout \
|
|| {% 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/{{ session.authority.hostname }}/host_key.pem{% else %}openssl genrsa \
|
||||||
-out /etc/certidude/authority/{{ authority_name }}/host_key.pem 2048{% endif %}
|
-out /etc/certidude/authority/{{ session.authority.hostname }}/host_key.pem 2048{% endif %}
|
||||||
test -e /etc/certidude/authority/{{ authority_name }}/host_req.pem \
|
test -e /etc/certidude/authority/{{ session.authority.hostname }}/host_req.pem \
|
||||||
|| openssl req -new -sha384 -subj "/CN=$NAME" \
|
|| openssl req -new -sha384 -subj "/CN=$NAME" \
|
||||||
-key /etc/certidude/authority/{{ authority_name }}/host_key.pem \
|
-key /etc/certidude/authority/{{ session.authority.hostname }}/host_key.pem \
|
||||||
-out /etc/certidude/authority/{{ authority_name }}/host_req.pem
|
-out /etc/certidude/authority/{{ session.authority.hostname }}/host_req.pem
|
||||||
echo "If CSR submission fails, you can copy paste it to Certidude:"
|
echo "If CSR submission fails, you can copy paste it to Certidude:"
|
||||||
cat /etc/certidude/authority/{{ authority_name }}/host_req.pem
|
cat /etc/certidude/authority/{{ session.authority.hostname }}/host_req.pem
|
||||||
|
|
||||||
|
@ -1,13 +1,12 @@
|
|||||||
|
# Use fully qualified name
|
||||||
test -e /sbin/uci && NAME=$(nslookup $(uci get network.wan.ipaddr) | grep "name =" | head -n1 | cut -d "=" -f 2 | xargs)
|
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 -e /bin/hostname && NAME=$(hostname -f)
|
||||||
test -n "$NAME" || NAME=$(cat /proc/sys/kernel/hostname)
|
test -n "$NAME" || NAME=$(cat /proc/sys/kernel/hostname)
|
||||||
|
|
||||||
{% include "snippets/update-trust.sh" %}
|
|
||||||
|
|
||||||
{% include "snippets/request-common.sh" %}
|
{% include "snippets/request-common.sh" %}
|
||||||
|
# Submit CSR and save signed certificate
|
||||||
curl -f -L -H "Content-type: application/pkcs10" \
|
curl --cert-status -f -L -H "Content-type: application/pkcs10" \
|
||||||
--cacert /etc/certidude/authority/{{ authority_name }}/ca_cert.pem \
|
--cacert /etc/certidude/authority/{{ session.authority.hostname }}/ca_cert.pem \
|
||||||
--data-binary @/etc/certidude/authority/{{ authority_name }}/host_req.pem \
|
--data-binary @/etc/certidude/authority/{{ session.authority.hostname }}/host_req.pem \
|
||||||
-o /etc/certidude/authority/{{ authority_name }}/host_cert.pem \
|
-o /etc/certidude/authority/{{ session.authority.hostname }}/host_cert.pem \
|
||||||
'https://{{ authority_name }}:8443/api/request/?wait=yes'
|
'https://{{ session.authority.hostname }}:8443/api/request/?wait=yes'
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
mkdir -p /etc/certidude/authority/{{ authority_name }}/
|
# Save CA certificate
|
||||||
test -e /etc/certidude/authority/{{ authority_name }}/ca_cert.pem \
|
mkdir -p /etc/certidude/authority/{{ session.authority.hostname }}/
|
||||||
|| cat << EOF > /etc/certidude/authority/{{ authority_name }}/ca_cert.pem
|
test -e /etc/certidude/authority/{{ session.authority.hostname }}/ca_cert.pem \
|
||||||
|
|| cat << EOF > /etc/certidude/authority/{{ session.authority.hostname }}/ca_cert.pem
|
||||||
{{ session.authority.certificate.blob }}EOF
|
{{ session.authority.certificate.blob }}EOF
|
||||||
|
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
cat > /etc/ipsec.conf << EOF
|
cat > /etc/ipsec.conf << EOF
|
||||||
|
config setup
|
||||||
|
strictcrlpolicy=yes
|
||||||
|
|
||||||
ca {{ authority_name }}
|
ca {{ session.authority.hostname }}
|
||||||
auto=add
|
auto=add
|
||||||
cacert=/etc/certidude/authority/{{ authority_name }}/ca_cert.pem
|
cacert=/etc/certidude/authority/{{ session.authority.hostname }}/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
|
conn client-to-site
|
||||||
auto=start
|
auto=start
|
||||||
@ -12,7 +12,7 @@ conn client-to-site
|
|||||||
rightsubnet=0.0.0.0/0
|
rightsubnet=0.0.0.0/0
|
||||||
rightca="{{ session.authority.certificate.distinguished_name }}"
|
rightca="{{ session.authority.certificate.distinguished_name }}"
|
||||||
left=%defaultroute
|
left=%defaultroute
|
||||||
leftcert=/etc/certidude/authority/{{ authority_name }}/host_cert.pem
|
leftcert=/etc/certidude/authority/{{ session.authority.hostname }}/host_cert.pem
|
||||||
leftsourceip=%config
|
leftsourceip=%config
|
||||||
leftca="{{ session.authority.certificate.distinguished_name }}"
|
leftca="{{ session.authority.certificate.distinguished_name }}"
|
||||||
keyexchange=ikev2
|
keyexchange=ikev2
|
||||||
@ -21,9 +21,8 @@ conn client-to-site
|
|||||||
closeaction=restart
|
closeaction=restart
|
||||||
ike=aes256-sha384-{% if session.authority.certificate.algorithm == "ec" %}ecp384{% else %}modp2048{% endif %}!
|
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 %}!
|
esp=aes128gcm16-aes128gmac-{% if session.authority.certificate.algorithm == "ec" %}ecp384{% else %}modp2048{% endif %}!
|
||||||
|
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
echo ": {% if session.authority.certificate.algorithm == "ec" %}ECDSA{% else %}RSA{% endif %} {{ authority_name }}.pem" > /etc/ipsec.secrets
|
echo ": {% if session.authority.certificate.algorithm == "ec" %}ECDSA{% else %}RSA{% endif %} {{ session.authority.hostname }}.pem" > /etc/ipsec.secrets
|
||||||
|
|
||||||
ipsec restart
|
ipsec restart apparmor
|
||||||
|
@ -6,11 +6,12 @@ test -e /etc/strongswan && test -e /etc/ipsec.d || ln -s strongswan/ipsec.d /etc
|
|||||||
test -e /etc/strongswan && test -e /etc/ipsec.secrets || ln -s strongswan/ipsec.secrets /etc/ipsec.secrets
|
test -e /etc/strongswan && test -e /etc/ipsec.secrets || ln -s strongswan/ipsec.secrets /etc/ipsec.secrets
|
||||||
|
|
||||||
# Set SELinux context
|
# 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/{{ session.authority.hostname }}/ca_cert.pem /etc/ipsec.d/cacerts/{{ session.authority.hostname }}.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/{{ session.authority.hostname }}/host_cert.pem /etc/ipsec.d/certs/{{ session.authority.hostname }}.pem
|
||||||
chcon --type=home_cert_t /etc/certidude/authority/{{ authority_name }}/host_key.pem /etc/ipsec.d/private/{{ authority_name }}.pem
|
chcon --type=home_cert_t /etc/certidude/authority/{{ session.authority.hostname }}/host_key.pem /etc/ipsec.d/private/{{ session.authority.hostname }}.pem
|
||||||
|
|
||||||
# Patch AppArmor
|
# Patch AppArmor
|
||||||
cat << EOF > /etc/apparmor.d/local/usr.lib.ipsec.charon
|
cat << EOF > /etc/apparmor.d/local/usr.lib.ipsec.charon
|
||||||
/etc/certidude/authority/**
|
/etc/certidude/authority/** r,
|
||||||
EOF
|
EOF
|
||||||
|
systemctl restart
|
||||||
|
@ -4,19 +4,17 @@ config setup
|
|||||||
strictcrlpolicy=yes
|
strictcrlpolicy=yes
|
||||||
uniqueids=yes
|
uniqueids=yes
|
||||||
|
|
||||||
ca {{ authority_name }}
|
ca {{ session.authority.hostname }}
|
||||||
auto=add
|
auto=add
|
||||||
cacert=/etc/certidude/authority/{{ authority_name }}/ca_cert.pem
|
cacert=/etc/certidude/authority/{{ session.authority.hostname }}/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 }}
|
conn default-{{ session.authority.hostname }}
|
||||||
ike=aes256-sha384-{% if session.authority.certificate.algorithm == "ec" %}ecp384{% else %}modp2048{% endif %}!
|
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 %}!
|
esp=aes128gcm16-aes128gmac-{% if session.authority.certificate.algorithm == "ec" %}ecp384{% else %}modp2048{% endif %}!
|
||||||
left=$(uci get network.wan.ipaddr) # Bind to this IP address
|
left=$(uci get network.wan.ipaddr) # Bind to this IP address
|
||||||
leftid={{ session.service.routers | first }}
|
leftid={{ session.service.routers | first }}
|
||||||
leftupdown=/etc/certidude/authority/{{ authority_name }}/updown
|
leftupdown=/etc/certidude/authority/{{ session.authority.hostname }}/updown
|
||||||
leftcert=/etc/certidude/authority/{{ authority_name }}/host_cert.pem
|
leftcert=/etc/certidude/authority/{{ session.authority.hostname }}/host_cert.pem
|
||||||
leftsubnet=$(uci get network.lan.ipaddr | cut -d . -f 1-3).0/24 # Subnets pushed to roadwarriors
|
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
|
leftdns=$(uci get network.lan.ipaddr) # IP of DNS server advertised to roadwarriors
|
||||||
leftca="{{ session.authority.certificate.distinguished_name }}"
|
leftca="{{ session.authority.certificate.distinguished_name }}"
|
||||||
@ -27,15 +25,15 @@ conn default-{{ authority_name }}
|
|||||||
|
|
||||||
conn site-to-clients
|
conn site-to-clients
|
||||||
auto=add
|
auto=add
|
||||||
also=default-{{ authority_name }}
|
also=default-{{ session.authority.hostname }}
|
||||||
|
|
||||||
conn site-to-client1
|
conn site-to-client1
|
||||||
auto=ignore
|
auto=ignore
|
||||||
also=default-{{ authority_name }}
|
also=default-{{ session.authority.hostname }}
|
||||||
rightid="CN=*, OU=IP Camera, O=*, DC=*, DC=*, DC=*"
|
rightid="CN=*, OU=IP Camera, O=*, DC=*, DC=*, DC=*"
|
||||||
rightsourceip=172.21.0.1
|
rightsourceip=172.21.0.1
|
||||||
|
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
echo ": {% if session.authority.certificate.algorithm == "ec" %}ECDSA{% else %}RSA{% endif %} /etc/certidude/authority/{{ authority_name }}/host_key.pem" > /etc/ipsec.secrets
|
echo ": {% if session.authority.certificate.algorithm == "ec" %}ECDSA{% else %}RSA{% endif %} /etc/certidude/authority/{{ session.authority.hostname }}/host_key.pem" > /etc/ipsec.secrets
|
||||||
|
|
||||||
|
@ -1,7 +1,9 @@
|
|||||||
|
# Insert into Fedora trust store. Applies to curl, Firefox, Chrome, Chromium
|
||||||
test -e /etc/pki/ca-trust/source/anchors \
|
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 }} \
|
&& ln -s /etc/certidude/authority/{{ session.authority.hostname }}/ca_cert.pem /etc/pki/ca-trust/source/anchors/{{ session.authority.hostname }} \
|
||||||
&& update-ca-trust
|
&& 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
|
|
||||||
|
|
||||||
|
# Insert into Ubuntu trust store, only applies to curl
|
||||||
|
test -e /usr/local/share/ca-certificates/ \
|
||||||
|
&& ln -s /etc/certidude/authority/{{ session.authority.hostname }}/ca_cert.pem /usr/local/share/ca-certificates/{{ session.authority.hostname }}.crt \
|
||||||
|
&& update-ca-certificates
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
# Install CA certificate
|
# Install CA certificate
|
||||||
@"
|
@"
|
||||||
{{ session.authority.certificate.blob }}
|
{{ session.authority.certificate.blob }}"@ | Out-File ca_cert.pem
|
||||||
"@ | Out-File ca_cert.pem
|
|
||||||
{% if session.authority.certificate.algorithm == "ec" %}
|
{% if session.authority.certificate.algorithm == "ec" %}
|
||||||
Import-Certificate -FilePath ca_cert.pem -CertStoreLocation Cert:\LocalMachine\Root
|
Import-Certificate -FilePath ca_cert.pem -CertStoreLocation Cert:\LocalMachine\Root
|
||||||
{% else %}
|
{% else %}
|
||||||
@ -25,25 +24,25 @@ KeyAlgorithm = ECDSA_P384
|
|||||||
KeyLength = 2048
|
KeyLength = 2048
|
||||||
{% endif %}"@ | Out-File req.inf
|
{% endif %}"@ | Out-File req.inf
|
||||||
C:\Windows\system32\certreq.exe -new -f -q req.inf host_csr.pem
|
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
|
Invoke-WebRequest -TimeoutSec 900 -Uri 'https://{{ session.authority.hostname }}:8443/api/request/?wait=yes&autosign=yes' -InFile host_csr.pem -ContentType application/pkcs10 -Method POST -MaximumRedirection 3 -OutFile host_cert.pem
|
||||||
|
|
||||||
# Import certificate
|
# Import certificate
|
||||||
{% if session.authority.certificate.algorithm == "ec" %}Import-Certificate -FilePath host_cert.pem -CertStoreLocation Cert:\LocalMachine\My
|
{% 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
|
{% else %}C:\Windows\system32\certutil.exe -addstore My host_cert.pem
|
||||||
{% endif %}
|
{% endif %}
|
||||||
# Set up IPSec VPN tunnel
|
|
||||||
Remove-VpnConnection -AllUserConnection -Force k-space
|
{% for router in session.service.routers %}
|
||||||
|
# Set up IPSec VPN tunnel to {{ router }}
|
||||||
|
Remove-VpnConnection -AllUserConnection -Force "IPSec to {{ router }}"
|
||||||
Add-VpnConnection `
|
Add-VpnConnection `
|
||||||
-Name k-space `
|
-Name "IPSec to {{ router }}" `
|
||||||
-ServerAddress guests.k-space.ee `
|
-ServerAddress {{ router }} `
|
||||||
-AuthenticationMethod MachineCertificate `
|
-AuthenticationMethod MachineCertificate `
|
||||||
-SplitTunneling `
|
-SplitTunneling `
|
||||||
-TunnelType ikev2 `
|
-TunnelType ikev2 `
|
||||||
-PassThru -AllUserConnection
|
-PassThru -AllUserConnection
|
||||||
|
|
||||||
# Security hardening
|
|
||||||
Set-VpnConnectionIPsecConfiguration `
|
Set-VpnConnectionIPsecConfiguration `
|
||||||
-ConnectionName k-space `
|
-ConnectionName "IPSec to {{ router }}" `
|
||||||
-AuthenticationTransformConstants GCMAES128 `
|
-AuthenticationTransformConstants GCMAES128 `
|
||||||
-CipherTransformConstants GCMAES128 `
|
-CipherTransformConstants GCMAES128 `
|
||||||
-EncryptionMethod AES256 `
|
-EncryptionMethod AES256 `
|
||||||
@ -51,6 +50,8 @@ Set-VpnConnectionIPsecConfiguration `
|
|||||||
-DHGroup {% if session.authority.certificate.algorithm == "ec" %}ECP384{% else %}Group14{% endif %} `
|
-DHGroup {% if session.authority.certificate.algorithm == "ec" %}ECP384{% else %}Group14{% endif %} `
|
||||||
-PfsGroup {% if session.authority.certificate.algorithm == "ec" %}ECP384{% else %}PFS2048{% endif %} `
|
-PfsGroup {% if session.authority.certificate.algorithm == "ec" %}ECP384{% else %}PFS2048{% endif %} `
|
||||||
-PassThru -AllUserConnection -Force
|
-PassThru -AllUserConnection -Force
|
||||||
|
{% endfor %}
|
||||||
|
|
||||||
{#
|
{#
|
||||||
AuthenticationTransformConstants - ESP integrity algorithm, one of: None MD596 SHA196 SHA256128 GCMAES128 GCMAES192 GCMAES256
|
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
|
CipherTransformConstants - ESP symmetric cipher, one of: DES DES3 AES128 AES192 AES256 GCMAES128 GCMAES192 GCMAES256
|
||||||
|
@ -5,51 +5,97 @@
|
|||||||
<button type="button" class="close" data-dismiss="modal">×</button>
|
<button type="button" class="close" data-dismiss="modal">×</button>
|
||||||
<h4 class="modal-title">Request submission</h4>
|
<h4 class="modal-title">Request submission</h4>
|
||||||
</div>
|
</div>
|
||||||
<form action="/api/request/" method="post">
|
<div class="modal-body">
|
||||||
<div class="modal-body">
|
<ul class="nav nav-pills" id="myTab" role="tablist">
|
||||||
<h5>Certidude client</h5>
|
<li class="nav-item">
|
||||||
<p>On Ubuntu or Fedora:</p>
|
<a class="nav-link active" id="home-tab" data-toggle="tab" href="#snippet-certidude" role="tab" aria-controls="certidude" aria-selected="true">Certidude</a>
|
||||||
<div class="highlight">
|
</li>
|
||||||
<pre class="code"><code>{% include "snippets/certidude-client.sh" %}</code></pre>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{% if "ikev2" in session.service.protocols %}
|
<li class="nav-item">
|
||||||
<h5>Windows {% if session.authority.certificate.algorithm == "ec" %}10{% else %}7 and up{% endif %}</h5>
|
<a class="nav-link" id="profile-tab" data-toggle="tab" href="#snippet-windows" role="tab" aria-controls="windows" aria-selected="false">Windows</a>
|
||||||
<p>On Windows execute following PowerShell script</p>
|
</li>
|
||||||
<div class="highlight"><pre class="code"><code>{% include "snippets/windows.ps1" %}</code></pre></div>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
<h5>UNIX & UNIX-like</h5>
|
<li class="nav-item">
|
||||||
<p>For client certificates generate key pair and submit the signing request with common name set to short hostname:</p>
|
<a class="nav-link" id="contact-tab" data-toggle="tab" href="#snippet-unix" role="tab" aria-controls="unix" aria-selected="false">UNIX</a>
|
||||||
<div class="highlight">
|
</li>
|
||||||
|
|
||||||
<pre class="code"><code>{% include "snippets/request-client.sh" %}</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>{% include "snippets/request-server.sh" %}</code></pre>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<p>To renew:</p>
|
|
||||||
|
|
||||||
<div class="highlight">
|
|
||||||
<pre class="code"><code>{% include "snippets/renew.sh" %}</code></pre>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{% if "openvpn" in session.service.protocols %}
|
{% if "openvpn" in session.service.protocols %}
|
||||||
<h5>OpenVPN as client</h5>
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" id="contact-tab" data-toggle="tab" href="#snippet-openvpn" role="tab" aria-controls="openvpn" aria-selected="false">OpenVPN</a>
|
||||||
<p>First acquire certificates using the snippet above.</p>
|
</li>
|
||||||
|
|
||||||
<p>Then install software:</p>
|
|
||||||
|
|
||||||
<div class="highlight"><pre class="code"><code>{% include "snippets/openvpn-client.sh" %}</code></pre></div>
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% if "ikev2" in session.service.protocols %}
|
{% if "ikev2" in session.service.protocols %}
|
||||||
<h5>StrongSwan as client</h5>
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" id="contact-tab" data-toggle="tab" href="#snippet-strongswan" role="tab" aria-controls="strongswan" aria-selected="false">StrongSwan</a>
|
||||||
|
</li>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" id="contact-tab" data-toggle="tab" href="#snippet-ansible" role="tab" aria-controls="ansible" aria-selected="false">Ansible</a>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" id="contact-tab" data-toggle="tab" href="#snippet-lede" role="tab" aria-controls="lede" aria-selected="false">LEDE</a>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
{% if session.authorization.scep_subnets %}
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" id="contact-tab" data-toggle="tab" href="#snippet-scep" role="tab" aria-controls="scep" aria-selected="false">SCEP</a>
|
||||||
|
</li>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<li class="nav-item">
|
||||||
|
<a class="nav-link" id="contact-tab" data-toggle="tab" href="#snippet-copypaste" role="tab" aria-controls="copypaste" aria-selected="false">Copypasta</a>
|
||||||
|
</li>
|
||||||
|
|
||||||
|
</ul>
|
||||||
|
<div class="tab-content" id="myTabContent">
|
||||||
|
<!-- Certidude client -->
|
||||||
|
<div class="tab-pane fade show active" id="snippet-certidude" role="tabpanel" aria-labelledby="certidude">
|
||||||
|
<p>On Ubuntu or Fedora:</p>
|
||||||
|
<div class="highlight">
|
||||||
|
<pre class="code"><code>{% include "snippets/certidude-client.sh" %}</code></pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Windows -->
|
||||||
|
<div class="tab-pane fade" id="snippet-windows" role="tabpanel" aria-labelledby="windows">
|
||||||
|
<p>On Windows {% if session.authority.certificate.algorithm == "ec" %}10{% else %}7 and up{% endif %} execute following PowerShell script</p>
|
||||||
|
{% if "ikev2" in session.service.protocols %}
|
||||||
|
<div class="highlight"><pre class="code"><code>{% include "snippets/windows.ps1" %}</code></pre></div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- UNIX-like -->
|
||||||
|
<div class="tab-pane fade" id="snippet-unix" role="tabpanel" aria-labelledby="unix">
|
||||||
|
|
||||||
|
<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>{% include "snippets/request-client.sh" %}</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>{% include "snippets/request-server.sh" %}</code></pre>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p>To renew:</p>
|
||||||
|
<div class="highlight">
|
||||||
|
<pre class="code"><code>{% include "snippets/renew.sh" %}</code></pre>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- OpenVPN as client -->
|
||||||
|
<div class="tab-pane fade" id="snippet-openvpn" role="tabpanel" aria-labelledby="openvpn">
|
||||||
|
<p>First acquire certificates using the snippet above.</p>
|
||||||
|
<p>Then install software:</p>
|
||||||
|
<div class="highlight"><pre class="code"><code>{% include "snippets/openvpn-client.sh" %}</code></pre></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- StrongSwan as client -->
|
||||||
|
<div class="tab-pane fade" id="snippet-strongswan" role="tabpanel" aria-labelledby="strongswan">
|
||||||
<p>First acquire certificates using the snippet above.</p>
|
<p>First acquire certificates using the snippet above.</p>
|
||||||
|
|
||||||
<p>Then install software:</p>
|
<p>Then install software:</p>
|
||||||
@ -59,68 +105,67 @@
|
|||||||
|
|
||||||
<p>To configure StrongSwan as roadwarrior:</p>
|
<p>To configure StrongSwan as roadwarrior:</p>
|
||||||
<div class="highlight"><pre class="code"><code>{% include "snippets/strongswan-client.sh" %}</code></pre></div>
|
<div class="highlight"><pre class="code"><code>{% include "snippets/strongswan-client.sh" %}</code></pre></div>
|
||||||
{% endif %}
|
</div>
|
||||||
|
|
||||||
|
<!-- Ansible -->
|
||||||
|
<div class="tab-pane fade" id="snippet-ansible" role="tabpanel" aria-labelledby="ansible">
|
||||||
|
<p>Fetch Ansible roles from https://github.com/laurivosandi/certidude-ansible</p>
|
||||||
|
<p>In your site.yml add:</p>
|
||||||
|
<div class="highlight"><pre class="code"><code>{% include "snippets/ansible-site.yml" %}</code></pre></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<h5>OpenWrt/LEDE as VPN gateway</h5>
|
<!-- LEDE -->
|
||||||
|
<div class="tab-pane fade" id="snippet-lede" role="tabpanel" aria-labelledby="lede">
|
||||||
|
<p>To enroll from OpenWrt/LEDE and to set it up as OpenVPN/IKEv2 gateway,
|
||||||
|
first enroll certificates using the snippet from UNIX section above</p>
|
||||||
|
|
||||||
<p>First enroll certificates using the snippet from UNIX section above</p>
|
<p>Then:</p>
|
||||||
|
<div class="highlight">
|
||||||
<p>Then:</p>
|
<pre class="code"><code>opkg install curl libmbedtls
|
||||||
<div class="highlight">
|
# Derive FQDN from WAN interface's reverse DNS record
|
||||||
<pre class="code"><code>opkg install curl libmbedtls
|
FQDN=$(nslookup $(uci get network.wan.ipaddr) | grep "name =" | head -n1 | cut -d "=" -f 2 | xargs)
|
||||||
# Derive FQDN from WAN interface's reverse DNS record
|
grep -c certidude /etc/sysupgrade.conf || echo /etc/certidude >> /etc/sysupgrade.conf
|
||||||
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" %}
|
{% include "snippets/gateway-updown.sh" %}
|
||||||
</code></pre>
|
</code></pre>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if "openvpn" in session.service.protocols %}
|
||||||
|
<p>Then either set up OpenVPN service:</p>
|
||||||
|
<div class="highlight">
|
||||||
|
<pre class="code"><code>{% include "snippets/openwrt-openvpn.sh" %}</code></pre>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% 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
|
||||||
|
{% include "snippets/strongswan-server.sh" %}
|
||||||
|
ipsec restart</code></pre>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% if "openvpn" in session.service.protocols %}
|
<!-- Copy & paste -->
|
||||||
<p>Then either set up OpenVPN service:</p>
|
<div class="tab-pane fade" id="snippet-copypaste" role="tabpanel" aria-labelledby="copypaste">
|
||||||
<div class="highlight">
|
<p>Use whatever tools you have available on your platform to generate
|
||||||
<pre class="code"><code>{% include "snippets/openwrt-openvpn.sh" %}</code></pre>
|
keypair and just paste ASCII armored PEM file contents here and hit submit:</p>
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
{% if "ikev2" in session.service.protocols %}
|
<form action="/api/request/" method="post">
|
||||||
<p>Alternatively or additionally set up StrongSwan:</p>
|
<textarea id="request_body" style="width:100%; min-height: 10em;"
|
||||||
<div class="highlight">
|
placeholder="-----BEGIN CERTIFICATE REQUEST-----"></textarea>
|
||||||
<pre class="code"><code>opkg update
|
<div class="modal-footer">
|
||||||
opkg install curl openssl-util strongswan-full strongswan-mod-openssl kmod-crypto-echainiv kmod-crypto-gcm
|
<div class="btn-group">
|
||||||
{% include "snippets/strongswan-server.sh" %}
|
<button type="button" onclick="onSubmitRequest();" class="btn btn-primary"><i class="fa fa-upload"></i> Submit</button>
|
||||||
ipsec restart</code></pre>
|
<button type="button" class="btn btn-secondary" data-dismiss="modal"><i class="fa fa-ban"></i> Close</button>
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
</div>
|
||||||
|
</form>
|
||||||
{% 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 %}
|
|
||||||
<li><a href="/api/build/{{ name }}/{{ filename }}">{{ title }}</a></li>
|
|
||||||
{% endfor %}
|
|
||||||
</ul>
|
|
||||||
{% endif %}
|
|
||||||
|
|
||||||
<h5>SCEP</h5>
|
|
||||||
<p>Use following as the enrollment URL: http://{{ authority_name }}/cgi-bin/pkiclient.exe</p>
|
|
||||||
|
|
||||||
<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>
|
|
||||||
|
|
||||||
<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>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -133,10 +178,10 @@
|
|||||||
<h4 class="modal-title">Revocation lists</h4>
|
<h4 class="modal-title">Revocation lists</h4>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
<p>To fetch <a href="http://{{authority_name}}/api/revoked/">certificate revocation list</a>:</p>
|
<p>To fetch <a href="http://{{ session.authority.hostname }}/api/revoked/">certificate revocation list</a>:</p>
|
||||||
<pre><code>curl http://{{authority_name}}/api/revoked/ > crl.der
|
<pre><code>curl http://{{ session.authority.hostname }}/api/revoked/ > crl.der
|
||||||
curl http://{{authority_name}}/api/revoked/ -L -H "Accept: application/x-pem-file"
|
curl http://{{ session.authority.hostname }}/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>
|
curl http://{{ session.authority.hostname }}/api/revoked/?wait=yes -L -H "Accept: application/x-pem-file" > crl.pem</code></pre>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
<button type="button" class="btn" data-dismiss="modal">Close</button>
|
<button type="button" class="btn" data-dismiss="modal">Close</button>
|
||||||
@ -146,11 +191,17 @@ curl http://{{authority_name}}/api/revoked/?wait=yes -L -H "Accept: application/
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-sm-{{ column_width }}">
|
<div class="col-sm-6 col-lg-4 col-xl-3">
|
||||||
<h1>Signed certificates</h1>
|
<h1>Signed certificates</h1>
|
||||||
<p>Authority administration allowed for
|
<p>Authority administration
|
||||||
{% 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 %}
|
{% if session.authority.certificate.organization %}of {{ session.authority.certificate.organization }}{% endif %}
|
||||||
{% for subnet in session.authority.admin_subnets %}{{ subnet }}{% if not loop.last %}, {% endif %}{% endfor %}{% endif %}.
|
allowed for
|
||||||
|
{% for user in session.authorization.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.authorization.admin_subnets %}anywhere{% else %}
|
||||||
|
{% for subnet in session.authorization.admin_subnets %}{{ subnet }}{% if not loop.last %}, {% endif %}{% endfor %}{% endif %}.
|
||||||
|
Authority valid from
|
||||||
|
<time class="timeago" datetime="{{ session.authority.certificate.signed }}">{{ session.authority.certificate.signed }}</time>
|
||||||
|
until
|
||||||
|
<time class="timeago" datetime="{{ session.authority.certificate.expires }}">{{ session.authority.certificate.expires }}</time>.
|
||||||
Authority certificate can be downloaded from <a href="/api/certificate/">here</a>.
|
Authority certificate can be downloaded from <a href="/api/certificate/">here</a>.
|
||||||
Following certificates have been signed:</p>
|
Following certificates have been signed:</p>
|
||||||
<div id="signed_certificates">
|
<div id="signed_certificates">
|
||||||
@ -159,7 +210,7 @@ curl http://{{authority_name}}/api/revoked/?wait=yes -L -H "Accept: application/
|
|||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-sm-{{ column_width }}">
|
<div class="col-sm-6 col-lg-4 col-xl-3">
|
||||||
{% if session.authority %}
|
{% if session.authority %}
|
||||||
{% if session.features.token %}
|
{% if session.features.token %}
|
||||||
<h1>Tokens</h1>
|
<h1>Tokens</h1>
|
||||||
@ -174,51 +225,89 @@ curl http://{{authority_name}}/api/revoked/?wait=yes -L -H "Accept: application/
|
|||||||
<input id="token_username" name="username" type="text" class="form-control" placeholder="Username" aria-describedby="sizing-addon2">
|
<input id="token_username" name="username" type="text" class="form-control" placeholder="Username" aria-describedby="sizing-addon2">
|
||||||
<input id="token_mail" name="mail" type="mail" class="form-control" placeholder="Optional e-mail" aria-describedby="sizing-addon2">
|
<input id="token_mail" name="mail" type="mail" class="form-control" placeholder="Optional e-mail" aria-describedby="sizing-addon2">
|
||||||
<span class="input-group-btn">
|
<span class="input-group-btn">
|
||||||
<button class="btn btn-secondary" type="button" onClick="onSendToken();"><i class="fa fa-send"></i> Send token</button>
|
<button class="btn btn-secondary" type="button" onClick="onIssueToken();"><i class="fa fa-send"></i> Send token</button>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
<p>Issued tokens:</p>
|
||||||
|
<ul>
|
||||||
|
{% for token in session.authority.tokens %}
|
||||||
|
<li>
|
||||||
|
<a href="mailto:{{ token.mail }}">{{ token.subject }}</a>
|
||||||
|
{% if token.issuer %}{% if token.issuer != token.subject %}by {{ token.issuer }}{% else %}by himself{% endif %}{% else %}via shell{% endif %},
|
||||||
|
expires
|
||||||
|
<time class="timeago" datetime="{{ token.expires }}">{{ token.expires }}</time>
|
||||||
|
</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
|
||||||
<div id="token_qrcode"></div>
|
<div id="token_qrcode"></div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<h1>Pending requests</h1>
|
{% if session.authorization.request_subnets %}
|
||||||
|
<h1>Pending requests</h1>
|
||||||
|
|
||||||
<p>Use Certidude client to apply for a certificate.
|
<p>Use Certidude client to apply for a certificate.
|
||||||
|
|
||||||
{% if not session.authority.request_subnets %}
|
{% if not session.authorization.request_subnets %}
|
||||||
Request submission disabled.
|
Request submission disabled.
|
||||||
{% elif "0.0.0.0/0" in session.authority.request_subnets %}
|
{% elif "0.0.0.0/0" in session.authorization.request_subnets %}
|
||||||
Request submission is enabled.
|
Request submission is enabled.
|
||||||
{% else %}
|
{% else %}
|
||||||
Request submission allowed from
|
Request submission allowed from
|
||||||
{% for subnet in session.authority.request_subnets %}
|
{% for subnet in session.authorization.request_subnets %}
|
||||||
{{ subnet }}{% if not loop.last %}, {% endif %}
|
{{ subnet }}{% if not loop.last %}, {% endif %}
|
||||||
{% endfor %}.
|
{% endfor %}.
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
See <a href="#request_submission_modal" data-toggle="modal">here</a> for more information on manual signing request upload.
|
See <a href="#request_submission_modal" data-toggle="modal">here</a> for more information on manual signing request upload.
|
||||||
|
|
||||||
{% if session.authority.autosign_subnets %}
|
{% if session.authorization.autosign_subnets %}
|
||||||
{% if "0.0.0.0/0" in session.authority.autosign_subnets %}
|
{% if "0.0.0.0/0" in session.authorization.autosign_subnets %}
|
||||||
All requests are automatically signed.
|
All requests are automatically signed.
|
||||||
|
{% else %}
|
||||||
|
Requests from
|
||||||
|
{% for subnet in session.authorization.autosign_subnets %}
|
||||||
|
{{ subnet }}{% if not loop.last %}, {% endif %}
|
||||||
|
{% endfor %}
|
||||||
|
are automatically signed.
|
||||||
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if session.authorization.scep_subnets %}
|
||||||
|
To enroll via SCEP from
|
||||||
|
{% if "0.0.0.0/0" in session.authorization.scep_subnets %}
|
||||||
|
anywhere
|
||||||
{% else %}
|
{% else %}
|
||||||
Requests from
|
{% for subnet in session.authorization.scep_subnets %}
|
||||||
{% for subnet in session.authority.autosign_subnets %}
|
{{ subnet }}{% if not loop.last %}, {% endif %}
|
||||||
{{ subnet }}{% if not loop.last %}, {% endif %}
|
{% endfor %}
|
||||||
{% endfor %}
|
|
||||||
are automatically signed.
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
use http://{{ session.authority.hostname }}/cgi-bin/pkiclient.exe as the enrollment URL.
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
</p>
|
||||||
|
<div id="pending_requests">
|
||||||
|
{% for request in session.authority.requests | sort(attribute="submitted", reverse=true) %}
|
||||||
|
{% include "views/request.html" %}
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</p>
|
|
||||||
<div id="pending_requests">
|
{% if session.builder.profiles %}
|
||||||
{% for request in session.authority.requests | sort(attribute="submitted", reverse=true) %}
|
<h2>LEDE imagebuilder</h2>
|
||||||
{% include "views/request.html" %}
|
<p>Hit a link to generate machine specific image. Note that this might take couple minutes to finish.</p>
|
||||||
{% endfor %}
|
<ul>
|
||||||
</div>
|
{% for name, title, filename in session.builder.profiles %}
|
||||||
{% if columns >= 3 %}
|
<li><a href="/api/build/{{ name }}/{{ filename }}">{{ title }}</a></li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
<div class="col-sm-{{ column_width }}">
|
<div class="col-sm-6 col-lg-4 col-xl-3">
|
||||||
{% endif %}
|
|
||||||
<h1>Revoked certificates</h1>
|
<h1>Revoked certificates</h1>
|
||||||
<p>Following certificates have been revoked{% if session.features.crl %}, for more information click
|
<p>Following certificates have been revoked{% if session.features.crl %}, for more information click
|
||||||
<a href="#revocation_list_modal" data-toggle="modal">here</a>{% endif %}.</p>
|
<a href="#revocation_list_modal" data-toggle="modal">here</a>{% endif %}.</p>
|
||||||
@ -227,7 +316,7 @@ curl http://{{authority_name}}/api/revoked/?wait=yes -L -H "Accept: application/
|
|||||||
{% include "views/revoked.html" %}
|
{% include "views/revoked.html" %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
<div id="column-log" class="col-sm-{% if columns == 4 %}{{ column_width }}{% else %}12{% endif %}" {% if columns < 4 %}style="display:none;"{% endif %}>
|
<div id="column-log" class="col-sm-6 col-lg-4 col-xl-3 hidden-lg-down">
|
||||||
<div class="loader-container">
|
<div class="loader-container">
|
||||||
<div class="loader"></div>
|
<div class="loader"></div>
|
||||||
<p>Loading logs, this might take a while...</p>
|
<p>Loading logs, this might take a while...</p>
|
||||||
@ -235,14 +324,15 @@ curl http://{{authority_name}}/api/revoked/?wait=yes -L -H "Accept: application/
|
|||||||
<div class="content" style="display:none;">
|
<div class="content" style="display:none;">
|
||||||
<h1>Log</h1>
|
<h1>Log</h1>
|
||||||
<div class="btn-group" data-toggle="buttons">
|
<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-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-error" type="checkbox" autocomplete="off" checked>Error</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-warning" type="checkbox" autocomplete="off" checked>Warn</label>
|
||||||
<label class="btn btn-primary active"><input id="log-level-info" type="checkbox" autocomplete="off" checked> Info</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>
|
<label class="btn btn-primary"><input id="log-level-debug" type="checkbox" autocomplete="off">Debug</label>
|
||||||
</div>
|
</div>
|
||||||
<ul id="log-entries" class="list-group">
|
<ul id="log-entries" class="list-group">
|
||||||
</ul>
|
</ul>
|
||||||
|
<p>Click here to load more entries</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
238
certidude/static/views/enroll.html
Normal file
@ -0,0 +1,238 @@
|
|||||||
|
<!-- https://wiki.strongswan.org/projects/strongswan/wiki/AppleIKEv2Profile#Certificate-authentication -->
|
||||||
|
|
||||||
|
<!--
|
||||||
|
|
||||||
|
Browser status
|
||||||
|
|
||||||
|
- Edge doesn't work because they think data: urls are insecure
|
||||||
|
- iphone QR code scanner's webview is constrained, cant download data: links
|
||||||
|
- outlook.com via iphone mail client works
|
||||||
|
- android gmail app works
|
||||||
|
- chrome works
|
||||||
|
- firefox works
|
||||||
|
|
||||||
|
OS/soft status
|
||||||
|
|
||||||
|
- OpenVPN works on everything
|
||||||
|
- StrongSwan app works on Android
|
||||||
|
- NetworkManager doesn't support importing .sswan files yet, so no IPSec support for Ubuntu or Fedora here yet
|
||||||
|
|
||||||
|
-->
|
||||||
|
|
||||||
|
<div id="enroll" class="row">
|
||||||
|
<div class="loader-container">
|
||||||
|
<div class="loader"></div>
|
||||||
|
<p>Generating RSA keypair, this will take a while...</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-sm-12 mt-3 edge-broken" style="display:none;">
|
||||||
|
<!-- https://stackoverflow.com/questions/33154646/data-uri-link-a-href-data-doesnt-work-in-microsoft-edge?utm_medium=organic&utm_source=google_rich_qa&utm_campaign=google_rich_qa -->
|
||||||
|
Microsoft Edge not supported, open the link with Chrome or Firefox
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-sm-12 mt-3 option ubuntu linux openvpn">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-block">
|
||||||
|
<h3 class="card-title">Ubuntu 16.04+</h3>
|
||||||
|
<p class="card-text">Install OpenVPN plugin for NetworkManager by executing following two command in the terminal:
|
||||||
|
|
||||||
|
<pre><code># Ubuntu 16.04 ships with older OpenVPN 2.3, to support newer ciphers add OpenVPN's repo
|
||||||
|
if [ $(lsb_relase -cs) == "xenial" ]; then
|
||||||
|
wget -O - https://swupdate.openvpn.net/repos/repo-public.gpg|apt-key add -
|
||||||
|
echo "deb http://build.openvpn.net/debian/openvpn/release/2.4 xenial main" > /etc/apt/sources.list.d/openvpn-aptrepo.list
|
||||||
|
apt update
|
||||||
|
apt install openvpn
|
||||||
|
fi
|
||||||
|
|
||||||
|
sudo apt install -y network-manager-openvpn-gnome
|
||||||
|
sudo systemctl restart network-manager
|
||||||
|
</code></pre>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
<a href="javascript:onEnroll('ovpn');" class="btn btn-primary">Fetch OpenVPN profile</a>
|
||||||
|
<button class="btn btn-secondary" type="button" data-toggle="collapse" data-target="#ubuntu-screenshots" aria-expanded="false" aria-controls="ubuntu-screenshots">
|
||||||
|
Screenshots
|
||||||
|
</button>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div class="collapse" id="ubuntu-screenshots">
|
||||||
|
<p>Open up network connections:</p>
|
||||||
|
<p><img src="/img/ubuntu-01-edit-connections.png"/></p>
|
||||||
|
<p>Hit <i>Add button</i>:</p>
|
||||||
|
<p><img src="/img/ubuntu-02-network-connections.png"/></p>
|
||||||
|
<p>Select <i>Import a saved VPN configuration...</i>:</p>
|
||||||
|
<p><img src="/img/ubuntu-03-import-saved-config.png"/></p>
|
||||||
|
<p>Select downloaded file:</p>
|
||||||
|
<p><img src="/img/ubuntu-04-select-file.png"/></p>
|
||||||
|
<p>Once profile is successfully imported following dialog appears:</p>
|
||||||
|
<p><img src="/img/ubuntu-05-profile-imported.png"/></p>
|
||||||
|
<p>By default all traffic is routed via VPN gateway, route only intranet subnets to the gateway select <i>Routes...</i> under <i>IPv4 Settings</i>:</p>
|
||||||
|
<p><img src="/img/ubuntu-06-ipv4-settings.png"/></p>
|
||||||
|
<p>Check <i>Use this connection only for resources on its network</i>:</p>
|
||||||
|
<p><img src="/img/ubuntu-07-disable-default-route.png"/></p>
|
||||||
|
<p>To activate the connection select it under <i>VPN Connections</i>:</p>
|
||||||
|
<p><img src="/img/ubuntu-08-activate-connection.png"/></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div class="col-sm-12 mt-3 option fedora linux openvpn">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-block">
|
||||||
|
<h3 class="card-title">Fedora</h3>
|
||||||
|
<p class="card-text">Install OpenVPN plugin for NetworkManager by running following two commands:</p>
|
||||||
|
<pre><code>dnf install NetworkManager-openvpn-gnome
|
||||||
|
systemctl restart NetworkManager</code></pre>
|
||||||
|
Right click in the NetworkManager icon, select network settings. Hit the + button and select <i>Import from file...</i>, select the downloaded .ovpn file.
|
||||||
|
Remove the .ovpn file from the Downloads folder.</p>
|
||||||
|
<a href="javascript:onEnroll('ovpn');" class="btn btn-primary">Fetch OpenVPN profile</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-sm-12 mt-3 option windows ipsec">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-block">
|
||||||
|
<h3 class="card-title">Windows</h3>
|
||||||
|
<p class="card-text">
|
||||||
|
Import PKCS#12 container to your machine trust store.
|
||||||
|
Import VPN connection profile by moving the downloaded .pbk file to
|
||||||
|
<pre><code>%userprofile%\AppData\Roaming\Microsoft\Network\Connections\PBK</code></pre>
|
||||||
|
or
|
||||||
|
<pre><code>C:\ProgramData\Microsoft\Network\Connections\Pbk</code></pre></p>
|
||||||
|
<a href="javascript:onEnroll('p12');" class="btn btn-primary">Fetch PKCS#12 container</a>
|
||||||
|
<a href="#" class="btn btn-secondary">Fetch VPN profile</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-sm-12 mt-3 option windows openvpn">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-block">
|
||||||
|
<h3 class="card-title">Windows</h3>
|
||||||
|
<p class="card-text">
|
||||||
|
Install OpenVPN community edition client.
|
||||||
|
Move the downloaded .ovpn file to C:\Program Files\OpenVPN\config and
|
||||||
|
right click in the system tray on OpenVPN icon and select Connect from the menu.
|
||||||
|
For finishing touch adjust the file permissions so only local
|
||||||
|
administrator can read that file, remove regular user access to the file.
|
||||||
|
</p>
|
||||||
|
<a href="https://openvpn.net/index.php/download/community-downloads.html" class="btn btn-secondary">Get OpenVPN community edition</a>
|
||||||
|
<a href="javascript:onEnroll('ovpn');" class="btn btn-primary">Fetch OpenVPN profile</a>
|
||||||
|
<button class="btn btn-secondary" type="button" data-toggle="collapse" data-target="#windows-screenshots" aria-expanded="false" aria-controls="windows-screenshots">
|
||||||
|
Screenshots
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<div class="collapse" id="windows-screenshots">
|
||||||
|
<p>Download OpenVPN from the link supplied above:</p>
|
||||||
|
<p><img src="/img/windows-01-download-openvpn.png"/></p>
|
||||||
|
|
||||||
|
<p>Install OpenVPN:</p>
|
||||||
|
<p><img src="/img/windows-02-install-openvpn.png"/></p>
|
||||||
|
|
||||||
|
<p>Move the configuraiton file downloaded from the second button above:</p>
|
||||||
|
<p><img src="/img/windows-03-move-config-file.png"/></p>
|
||||||
|
|
||||||
|
<p>Connect from system tray:</p>
|
||||||
|
<p><img src="/img/windows-04-connect.png"/></p>
|
||||||
|
|
||||||
|
<p>Connection is successfully configured:</p>
|
||||||
|
<p><img src="/img/windows-05-connected.png"/></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-sm-12 mt-3 option mac openvpn">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-block">
|
||||||
|
<h3 class="card-title">Mac OS X</h3>
|
||||||
|
<p class="card-text">Download Tunnelblick. Tap on the button above and import the profile.</p>
|
||||||
|
<a href="https://tunnelblick.net/" target="_blank" class="btn btn-secondary">Get Tunnelblick</a>
|
||||||
|
<a href="javascript:onEnroll('ovpn');" class="btn btn-primary">Fetch OpenVPN profile</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-sm-12 mt-3 option iphone ipad openvpn">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-block">
|
||||||
|
<h3 class="card-title">iPhone/iPad</h3>
|
||||||
|
<p class="card-text">Install OpenVPN Connect app, tap on the button below.</p>
|
||||||
|
<a href="https://itunes.apple.com/us/app/openvpn-connect/id590379981?mt=8" target="_blank" class="btn btn-secondary">Get OpenVPN Connect app</a>
|
||||||
|
<a href="javascript:onEnroll('ovpn');" class="btn btn-primary">Fetch OpenVPN profile</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-sm-12 mt-3 option iphone ipad ikev2">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-block">
|
||||||
|
<h3 class="card-title">iPhone/iPad</h3>
|
||||||
|
<p class="card-text">
|
||||||
|
Tap the button below, you'll be prompted about configuration profile, tap <i>Allow</i>.
|
||||||
|
Hit <i>Install</i> in the top-right corner.
|
||||||
|
Enter your passcode to unlock trust store.
|
||||||
|
Tap <i>Install</i> and confirm by hitting <i>Install</i>.
|
||||||
|
Where password for the certificate is prompted, enter 1234.
|
||||||
|
Hit <i>Done</i>. Go to <i>Settings</i>, open VPN submenu and tap on the VPN profile to connect.
|
||||||
|
</p>
|
||||||
|
<a href="javascript:onEnroll('mobileconfig');" class="btn btn-primary">Fetch VPN profile</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-sm-12 mt-3 option mac ikev2">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-block">
|
||||||
|
<h3 class="card-title">Mac OS X</h3>
|
||||||
|
<p class="card-text">
|
||||||
|
Click on the button below, you'll be prompted about configuration profile, tap <i>Allow</i>.
|
||||||
|
Hit <i>Install</i> in the top-right corner.
|
||||||
|
Enter your passcode to unlock trust store.
|
||||||
|
Tap <i>Install</i> and confirm by hitting <i>Install</i>.
|
||||||
|
Where password for the certificate is prompted, enter 1234.
|
||||||
|
Hit <i>Done</i>. Go to <i>Settings</i>, open VPN submenu and tap on the VPN profile to connect.
|
||||||
|
</p>
|
||||||
|
<a href="javascript:onEnroll('mobileconfig');" class="btn btn-primary">Fetch VPN profile</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-sm-12 mt-3 option android openvpn">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-block">
|
||||||
|
<h3 class="card-title">Android</h3>
|
||||||
|
<p class="card-text">Intall OpenVPN Connect app on your device.
|
||||||
|
Tap on the downloaded .ovpn file, OpenVPN Connect should prompt for import.
|
||||||
|
Hit <i>Accept</i> and then <i>Connect</i>.
|
||||||
|
Remember to delete any remaining .ovpn files under the <i>Downloads</i>.
|
||||||
|
</p>
|
||||||
|
<a href="https://play.google.com/store/apps/details?id=net.openvpn.openvpn" target="_blank" class="btn btn-secondary">Get OpenVPN Connect app</a>
|
||||||
|
<a href="javascript:onEnroll('ovpn');" class="btn btn-primary">Fetch OpenVPN profile</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-sm-12 mt-3 option android ikev2">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-block">
|
||||||
|
<h3 class="card-title">Android</h3>
|
||||||
|
<p class="card-text">
|
||||||
|
Install strongSwan Client app on your device.
|
||||||
|
Tap on the downloaded .sswan file, StrongSwan Client should prompt for import.
|
||||||
|
Hit <i>Import certificate from VPN profile</i> and then <i>Import</i> in the top-right corner.
|
||||||
|
Remember to delete any remaining .sswan files under the <i>Downloads</i>.
|
||||||
|
</p>
|
||||||
|
<a href="https://play.google.com/store/apps/details?id=org.strongswan.android" class="btn btn-secondary">Get strongSwan VPN Client app</a>
|
||||||
|
<a href="javascript:onEnroll('sswan');" class="btn btn-primary">Fetch StrongSwan profile</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<!--
|
||||||
|
<a href="javascript:onShowAll();">I did't find an appropriate option for me, show all options</a>
|
||||||
|
-->
|
||||||
|
|
||||||
|
</div>
|
@ -1,5 +1,5 @@
|
|||||||
<p>You're viewing this page over insecure channel.
|
<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>,
|
You can give it a try and <a href="https://{{ session.authority.hostname }}">connect over HTTPS</a>,
|
||||||
if that succeeds all subsequents accesses of this page will go over HTTPS.
|
if that succeeds all subsequents accesses of this page will go over HTTPS.
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
<li id="log_entry_{{ entry.id }}" class="list-group-item justify-content-between filterable{% if entry.fresh %} fresh{% endif %}">
|
<li id="log_entry_{{ entry.id }}" data-keywords="{{ entry.message }}" class="list-group-item justify-content-between filterable{% if entry.fresh %} fresh{% endif %}">
|
||||||
<span>
|
<span>
|
||||||
<i class="fa fa-{{ entry.severity }}-circle"/>
|
<i class="fa fa-{{ entry.severity }}-circle"/>
|
||||||
{{ entry.message }}
|
{{ entry.message }}
|
||||||
|
@ -40,8 +40,8 @@
|
|||||||
<div class="collapse" id="details-{{ request.sha256sum }}">
|
<div class="collapse" id="details-{{ request.sha256sum }}">
|
||||||
<p>Use following to fetch the signing request:</p>
|
<p>Use following to fetch the signing request:</p>
|
||||||
<div class="bd-example">
|
<div class="bd-example">
|
||||||
<pre><code class="language-sh" data-lang="sh">wget <a href="/api/request/{{ request.common_name }}/">http://{{ window.location.hostname }}/api/request/{{ request.common_name }}/</a>
|
<pre><code class="language-sh" data-lang="sh">wget <a href="/api/request/{{ request.common_name }}/">http://{{ session.authority.hostname }}/api/request/{{ request.common_name }}/</a>
|
||||||
curl http://{{ window.location.hostname }}/api/request/{{ request.common_name }}/ \
|
curl http://{{ session.authority.hostname }}/api/request/{{ request.common_name }}/ \
|
||||||
| openssl req -text -noout</code></pre>
|
| openssl req -text -noout</code></pre>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -29,15 +29,15 @@
|
|||||||
<p>To fetch certificate:</p>
|
<p>To fetch certificate:</p>
|
||||||
|
|
||||||
<div class="bd-example">
|
<div class="bd-example">
|
||||||
<pre><code class="language-sh" data-lang="sh">wget <a href="/api/revoked/{{ certificate.serial }}/">http://{{ window.location.hostname }}/api/revoked/{{ certificate.serial }}/</a>
|
<pre><code class="language-sh" data-lang="sh">wget <a href="/api/revoked/{{ certificate.serial }}/">http://{{ session.authority.hostname }}/api/revoked/{{ certificate.serial }}/</a>
|
||||||
curl http://{{ window.location.hostname }}/api/revoked/{{ certificate.serial }}/ \
|
curl http://{{ session.authority.hostname }}/api/revoked/{{ certificate.serial }}/ \
|
||||||
| openssl x509 -text -noout</code></pre>
|
| openssl x509 -text -noout</code></pre>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p>To perform online certificate status request</p>
|
<p>To perform online certificate status request</p>
|
||||||
<pre><code class="language-bash" data-lang="bash">curl http://{{ window.location.hostname }}/api/certificate/ > session.pem
|
<pre><code class="language-bash" data-lang="bash">curl http://{{ session.authority.hostname }}/api/certificate/ > session.pem
|
||||||
openssl ocsp -issuer session.pem -CAfile session.pem \
|
openssl ocsp -issuer session.pem -CAfile session.pem \
|
||||||
-url http://{{ window.location.hostname }}/api/ocsp/ \
|
-url http://{{ session.authority.hostname }}/api/ocsp/ \
|
||||||
-serial 0x{{ certificate.serial }}</span></code></pre>
|
-serial 0x{{ certificate.serial }}</span></code></pre>
|
||||||
|
|
||||||
<p>
|
<p>
|
||||||
|
@ -56,7 +56,7 @@
|
|||||||
|
|
||||||
<div class="btn-group">
|
<div class="btn-group">
|
||||||
{% if session.authority.tagging %}
|
{% if session.authority.tagging %}
|
||||||
<button type="button" class="btn btn-default" onclick="onNewTagClicked(this);" data-key="other" data-cn="{{ certificate.common_name }}">
|
<button type="button" class="btn btn-default" onclick="onNewTagClicked(event);" data-key="other" data-cn="{{ certificate.common_name }}">
|
||||||
<i class="fa fa-tag"></i> Tag</button>
|
<i class="fa fa-tag"></i> Tag</button>
|
||||||
<button type="button" class="btn btn-default dropdown-toggle dropdown-toggle-split" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
<button type="button" class="btn btn-default dropdown-toggle dropdown-toggle-split" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
|
||||||
<span class="sr-only">Toggle Dropdown</span>
|
<span class="sr-only">Toggle Dropdown</span>
|
||||||
@ -64,7 +64,7 @@
|
|||||||
<div class="dropdown-menu">
|
<div class="dropdown-menu">
|
||||||
{% for tag_category in session.authority.tagging %}
|
{% for tag_category in session.authority.tagging %}
|
||||||
<a class="dropdown-item" href="#" data-key="{{ tag_category.name }}" data-cn="{{ certificate.common_name }}"
|
<a class="dropdown-item" href="#" data-key="{{ tag_category.name }}" data-cn="{{ certificate.common_name }}"
|
||||||
onclick="onNewTagClicked(this);">{{ tag_category.title }}</a>
|
onclick="onNewTagClicked(event);">{{ tag_category.title }}</a>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@ -74,24 +74,29 @@
|
|||||||
<p>To fetch certificate:</p>
|
<p>To fetch certificate:</p>
|
||||||
|
|
||||||
<div class="bd-example">
|
<div class="bd-example">
|
||||||
<pre><code class="language-sh" data-lang="sh">wget <a href="/api/signed/{{ certificate.common_name }}/">http://{{ window.location.hostname }}/api/signed/{{ certificate.common_name }}/</a>
|
<pre><code class="language-sh" data-lang="sh">wget <a href="/api/signed/{{ certificate.common_name }}/">http://{{ session.authority.hostname }}/api/signed/{{ certificate.common_name }}/</a>
|
||||||
curl http://{{ window.location.hostname }}/api/signed/{{ certificate.common_name }}/ \
|
curl --cert-status http://{{ session.authority.hostname }}/api/signed/{{ certificate.common_name }}/ \
|
||||||
| openssl x509 -text -noout</code></pre>
|
| openssl x509 -text -noout</code></pre>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% if session.features.ocsp %}
|
{% if session.authorization.ocsp_subnets %}
|
||||||
<p>To perform online certificate status request:</p>
|
{% if certificate.responder_url %}
|
||||||
<pre><code class="language-bash" data-lang="bash">curl http://{{ window.location.hostname }}/api/certificate/ > session.pem
|
<p>To perform online certificate status request{% if "0.0.0.0/0" not in session.authorization.ocsp_subnets %}
|
||||||
|
from whitelisted {{ session.authorization.ocsp_subnets }} subnets{% endif %}:</p>
|
||||||
|
<pre><code class="language-bash" data-lang="bash">curl http://{{ session.authority.hostname }}/api/certificate > session.pem
|
||||||
openssl ocsp -issuer session.pem -CAfile session.pem \
|
openssl ocsp -issuer session.pem -CAfile session.pem \
|
||||||
-url http://{{ window.location.hostname }}/api/ocsp/ \
|
-url {{ certificate.responder_url }} \
|
||||||
-serial 0x{{ certificate.serial }}</code></pre>
|
-serial 0x{{ certificate.serial }}</code></pre>
|
||||||
|
{% else %}
|
||||||
|
<p>Querying OCSP responder disabled for this certificate, see /etc/certidude/profile.conf how to enable if that's desired</p>
|
||||||
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<p>To fetch script:</p>
|
<p>To fetch script:</p>
|
||||||
<pre><code class="language-bash" data-lang="bash">curl https://{{ window.location.hostname }}:8443/api/signed/{{ certificate.common_name }}/script/ \
|
<pre><code class="language-bash" data-lang="bash">curl --cert-status https://{{ session.authority.hostname }}:8443/api/signed/{{ certificate.common_name }}/script/ \
|
||||||
--cacert /etc/certidude/authority/{{ window.location.hostname }}/ca_cert.pem \
|
--cacert /etc/certidude/authority/{{ session.authority.hostname }}/ca_cert.pem \
|
||||||
--key /etc/certidude/authority/{{ window.location.hostname }}/host_key.pem \
|
--key /etc/certidude/authority/{{ session.authority.hostname }}/host_key.pem \
|
||||||
--cert /etc/certidude/authority/{{ window.location.hostname }}/host_cert.pem</pre></code>
|
--cert /etc/certidude/authority/{{ session.authority.hostname }}/host_cert.pem</pre></code>
|
||||||
|
|
||||||
<div style="overflow: auto; max-width: 100%;">
|
<div style="overflow: auto; max-width: 100%;">
|
||||||
<table class="table" id="signed_certificates">
|
<table class="table" id="signed_certificates">
|
||||||
@ -112,8 +117,11 @@ openssl ocsp -issuer session.pem -CAfile session.pem \
|
|||||||
<tr><th>SHA1</th><td>{{ certificate.sha1sum }}</td></tr>
|
<tr><th>SHA1</th><td>{{ certificate.sha1sum }}</td></tr>
|
||||||
-->
|
-->
|
||||||
<tr><th>SHA256</th><td style="word-wrap:break-word; overflow-wrap: break-word; ">{{ certificate.sha256sum }}</td></tr>
|
<tr><th>SHA256</th><td style="word-wrap:break-word; overflow-wrap: break-word; ">{{ certificate.sha256sum }}</td></tr>
|
||||||
{% if certificate.extensions.extended_key_usage %}
|
{% if certificate.key_usage %}
|
||||||
<tr><th>Extended key usage</th><td>{{ certificate.extensions.extended_key_usage | join(", ") }}</td></tr>
|
<tr><th>Key usage</th><td>{{ certificate.key_usage | join(", ") | replace("_", " ") }}</td></tr>
|
||||||
|
{% endif %}
|
||||||
|
{% if certificate.extended_key_usage %}
|
||||||
|
<tr><th>Extended key usage</th><td>{{ certificate.extended_key_usage | join(", ") | replace("_", " ") }}</td></tr>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
@ -2,5 +2,5 @@
|
|||||||
<span data-cn="{{ certificate.common_name }}"
|
<span data-cn="{{ certificate.common_name }}"
|
||||||
title="{{ tag.id }}"
|
title="{{ tag.id }}"
|
||||||
class="badge badge-default"
|
class="badge badge-default"
|
||||||
onClick="onTagClicked(this);">{{ tag.value }}</span>
|
onClick="onTagClicked(event);">{{ tag.value }}</span>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
Token for {{ user.name }}
|
Token for {{ subject }}
|
||||||
|
|
||||||
{% if issuer == user %}
|
{% if issuer == subject %}
|
||||||
Token has been issued for {{ user }} for retrieving profile from link below.
|
Token has been issued for {{ subject }} for retrieving profile from link below.
|
||||||
{% else %}
|
{% else %}
|
||||||
{{ issuer }} has provided {{ user }} a token for retrieving
|
{{ issuer }} has provided {{ subject }} a token for retrieving
|
||||||
profile from the link below.
|
profile from the link below.
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
@ -1,9 +1,24 @@
|
|||||||
|
# Configure secure defaults for nginx
|
||||||
|
ssl_dhparam {{ dhparam_path }};
|
||||||
|
|
||||||
|
# Note that depending on the certificate type (RSA, ECDSA) this will be even further constrained:
|
||||||
|
ssl_ciphers ECDHE-ECDSA-AES256-GCM-SHA512:DHE-ECDSA-AES256-GCM-SHA512:ECDHE-ECDSA-AES256-GCM-SHA384:DHE-ECDSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-GCM-SHA512:DHE-RSA-AES256-GCM-SHA512:ECDHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-SHA384;
|
||||||
|
|
||||||
|
ssl_ecdh_curve secp384r1;
|
||||||
|
ssl_session_timeout 10m;
|
||||||
|
ssl_session_cache shared:SSL:10m;
|
||||||
|
ssl_session_tickets off;
|
||||||
|
ssl_trusted_certificate {{ ca_cert }}; # OCSP responder trust chain
|
||||||
|
ssl_stapling on;
|
||||||
|
ssl_stapling_verify on;
|
||||||
|
add_header X-Frame-Options DENY;
|
||||||
|
add_header X-Content-Type-Options nosniff;
|
||||||
|
add_header X-XSS-Protection "1; mode=block";
|
||||||
|
add_header X-Robots-Tag none;
|
||||||
|
|
||||||
# Following are already enabled by /etc/nginx/nginx.conf
|
# Following are already enabled by /etc/nginx/nginx.conf
|
||||||
#ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
|
#ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
|
||||||
#ssl_prefer_server_ciphers on;
|
#ssl_prefer_server_ciphers on;
|
||||||
ssl_session_cache shared:SSL:10m;
|
|
||||||
ssl_ciphers "ECDHE-RSA-AES256-GCM-SHA384:ECDHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-SHA384:ECDHE-RSA-AES128-SHA256:ECDHE-RSA-AES256-SHA:ECDHE-RSA-AES128-SHA:DHE-RSA-AES256-SHA256:DHE-RSA-AES128-SHA256:DHE-RSA-AES256-SHA:DHE-RSA-AES128-SHA:ECDHE-RSA-DES-CBC3-SHA:EDH-RSA-DES-CBC3-SHA:AES256-GCM-SHA384:AES128-GCM-SHA256:AES256-SHA256:AES128-SHA256:AES256-SHA:AES128-SHA:DES-CBC3-SHA:HIGH:!aNULL:!eNULL:!EXPORT:!DES:!MD5:!PSK:!RC4";
|
|
||||||
ssl_dhparam {{dhparam_path}};
|
|
||||||
|
|
||||||
# Add SSLUserName SSL_CLIENT_S_DN_CN style parameter support
|
# Add SSLUserName SSL_CLIENT_S_DN_CN style parameter support
|
||||||
map $ssl_client_s_dn $ssl_client_s_dn_cn {
|
map $ssl_client_s_dn $ssl_client_s_dn_cn {
|
||||||
|
@ -1,13 +1,16 @@
|
|||||||
[DEFAULT]
|
[DEFAULT]
|
||||||
|
# LEDE image builder profiles enabled by default
|
||||||
|
enabled = yes
|
||||||
|
|
||||||
# Path to filesystem overlay used
|
# Path to filesystem overlay used
|
||||||
overlay = {{ doc_path }}/overlay
|
overlay = {{ doc_path }}/overlay
|
||||||
|
|
||||||
# Hostname or regex to match the IPSec gateway included in the image
|
# Hostname or regex to match the IPSec gateway included in the image
|
||||||
router = ^router\d?\.
|
router = ^(router|vpn|gw|gateway)\d*\.
|
||||||
|
|
||||||
# Site specific script to be copied to /etc/uci-defaults/99-site-script
|
# Site specific script to be copied to /etc/uci-defaults/99-site-script
|
||||||
# use it to include SSH keys, set passwords, etc
|
# use it to include SSH keys, set passwords, etc
|
||||||
script =
|
script = /etc/certidude/script/site.sh
|
||||||
|
|
||||||
# Which subnets are routed to the tunnel
|
# Which subnets are routed to the tunnel
|
||||||
subnets = 192.168.0.0/16 172.16.0.0/12 10.0.0.0/8
|
subnets = 192.168.0.0/16 172.16.0.0/12 10.0.0.0/8
|
||||||
@ -16,37 +19,96 @@ subnets = 192.168.0.0/16 172.16.0.0/12 10.0.0.0/8
|
|||||||
ike=aes256-sha384-{{ dhgroup }}!
|
ike=aes256-sha384-{{ dhgroup }}!
|
||||||
esp=aes128gcm16-aes128gmac-{{ dhgroup }}!
|
esp=aes128gcm16-aes128gmac-{{ dhgroup }}!
|
||||||
|
|
||||||
[tpl-archer-c7]
|
|
||||||
# Title shown in the UI
|
|
||||||
title = TP-Link Archer C7 (Access Point)
|
|
||||||
|
|
||||||
# Script to build the image, copy file to /etc/certidude/ and make modifications as necessary
|
[tpl-wdr3600-factory]
|
||||||
command = {{ doc_path }}/builder/ap.sh
|
enabled = no
|
||||||
|
|
||||||
|
# Title shown in the UI
|
||||||
|
title = TP-Link WDR3600 (Access Point), TFTP-friendly
|
||||||
|
|
||||||
|
# Script to build the image, copy file to /etc/certidude/script/ and make modifications as necessary
|
||||||
|
command = /srv/certidude/doc/builder/ap.sh
|
||||||
|
|
||||||
# Device/model/profile selection
|
# Device/model/profile selection
|
||||||
model = archer-c7-v2
|
model = tl-wdr3600-v1
|
||||||
|
|
||||||
# File that will be picked from the bin/ folder
|
# File that will be picked from the bin/ folder
|
||||||
filename = archer-c7-v2-squashfs-factory-eu.bin
|
filename = tl-wdr3600-v1-squashfs-factory.bin
|
||||||
|
|
||||||
# And renamed to make it TFTP-friendly
|
# And renamed to make it TFTP-friendly
|
||||||
|
rename = wdr4300v1_tp_recovery.bin
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
[tpl-wdr4300-factory]
|
||||||
|
enabled = no
|
||||||
|
title = TP-Link WDR4300 (Access Point), TFTP-friendly
|
||||||
|
command = /srv/certidude/doc/builder/ap.sh
|
||||||
|
model = tl-wdr4300-v1
|
||||||
|
filename = tl-wdr4300-v1-squashfs-factory.bin
|
||||||
|
rename = wdr4300v1_tp_recovery.bin
|
||||||
|
|
||||||
|
[tpl-archer-c7-factory]
|
||||||
|
enabled = no
|
||||||
|
title = TP-Link Archer C7 (Access Point), TFTP-friendly
|
||||||
|
command = {{ doc_path }}/builder/ap.sh
|
||||||
|
model = archer-c7-v2
|
||||||
|
filename = archer-c7-v2-squashfs-factory-eu.bin
|
||||||
rename = ArcherC7v2_tp_recovery.bin
|
rename = ArcherC7v2_tp_recovery.bin
|
||||||
|
|
||||||
[cf-e380ac]
|
[cf-e380ac-factory]
|
||||||
title = Comfast E380AC (Access Point)
|
enabled = no
|
||||||
|
title = Comfast E380AC (Access Point), TFTP-friendly
|
||||||
command = {{ doc_path }}/builder/ap.sh
|
command = {{ doc_path }}/builder/ap.sh
|
||||||
model = cf-e380ac-v2
|
model = cf-e380ac-v2
|
||||||
filename = cf-e380ac-v2-squashfs-factory.bin
|
filename = cf-e380ac-v2-squashfs-factory.bin
|
||||||
rename = firmware_auto.bin
|
rename = firmware_auto.bin
|
||||||
|
|
||||||
[ar150-mfp]
|
|
||||||
|
|
||||||
|
[tpl-wdr3600-sysupgrade]
|
||||||
|
;enabled = yes
|
||||||
|
title = TP-Link WDR3600 (Access Point)
|
||||||
|
command = /srv/certidude/doc/builder/ap.sh
|
||||||
|
model = tl-wdr3600-v1
|
||||||
|
filename = tl-wdr3600-v1-squashfs-sysupgrade.bin
|
||||||
|
rename = ap-tl-wdr3600-v1-squashfs-sysupgrade.bin
|
||||||
|
|
||||||
|
|
||||||
|
[tpl-wdr4300-sysupgrade]
|
||||||
|
;enabled = yes
|
||||||
|
title = TP-Link WDR4300 (Access Point)
|
||||||
|
command = /srv/certidude/doc/builder/ap.sh
|
||||||
|
model = tl-wdr4300-v1
|
||||||
|
filename = tl-wdr4300-v1-squashfs-sysupgrade.bin
|
||||||
|
rename = ap-tl-wdr4300-v1-squashfs-sysupgrade.bin
|
||||||
|
|
||||||
|
[tpl-archer-c7-sysupgrade]
|
||||||
|
;enabled = yes
|
||||||
|
title = TP-Link Archer C7 (Access Point)
|
||||||
|
command = {{ doc_path }}/builder/ap.sh
|
||||||
|
model = archer-c7-v2
|
||||||
|
filename = archer-c7-v2-squashfs-factory-eu.bin
|
||||||
|
rename = ap-archer-c7-v2-squashfs-factory-eu.bin
|
||||||
|
|
||||||
|
[cf-e380ac-sysupgrade]
|
||||||
|
;enabled = yes
|
||||||
|
title = Comfast E380AC (Access Point)
|
||||||
|
command = {{ doc_path }}/builder/ap.sh
|
||||||
|
model = cf-e380ac-v2
|
||||||
|
filename = cf-e380ac-v2-squashfs-factory.bin
|
||||||
|
rename = ap-cf-e380ac-v2-squashfs-factory.bin
|
||||||
|
|
||||||
|
[ar150-mfp-sysupgrade]
|
||||||
|
;enabled = yes
|
||||||
title = GL.iNet GL-AR150 (MFP)
|
title = GL.iNet GL-AR150 (MFP)
|
||||||
command = {{ doc_path }}/builder/mfp.sh
|
command = {{ doc_path }}/builder/mfp.sh
|
||||||
model = gl-ar150
|
model = gl-ar150
|
||||||
filename = ar71xx-generic-gl-ar150-squashfs-sysupgrade.bin
|
filename = ar71xx-generic-gl-ar150-squashfs-sysupgrade.bin
|
||||||
rename = mfp-gl-ar150-squashfs-sysupgrade.bin
|
rename = mfp-gl-ar150-squashfs-sysupgrade.bin
|
||||||
|
|
||||||
[ar150-cam]
|
[ar150-cam-sysupgrade]
|
||||||
|
;enabled = yes
|
||||||
title = GL.iNet GL-AR150 (IP Camera)
|
title = GL.iNet GL-AR150 (IP Camera)
|
||||||
command = {{ doc_path }}/builder/ipcam.sh
|
command = {{ doc_path }}/builder/ipcam.sh
|
||||||
model = gl-ar150
|
model = gl-ar150
|
||||||
|
@ -1,9 +1,3 @@
|
|||||||
# To set up SSL certificates using Let's Encrypt run:
|
|
||||||
#
|
|
||||||
|
|
||||||
#
|
|
||||||
# Also uncomment URL rewriting and SSL configuration below
|
|
||||||
|
|
||||||
# Basic DoS prevention measures
|
# Basic DoS prevention measures
|
||||||
limit_conn addr 10;
|
limit_conn addr 10;
|
||||||
client_body_timeout 5s;
|
client_body_timeout 5s;
|
||||||
@ -72,6 +66,9 @@ server {
|
|||||||
|
|
||||||
# Uncomment following to enable HTTPS
|
# Uncomment following to enable HTTPS
|
||||||
#rewrite ^/$ https://$server_name$request_uri? permanent;
|
#rewrite ^/$ https://$server_name$request_uri? permanent;
|
||||||
|
|
||||||
|
access_log /var/log/nginx/certidude-plaintext-access.log;
|
||||||
|
error_log /var/log/nginx/certidude-plaintext-error.log;
|
||||||
}
|
}
|
||||||
|
|
||||||
server {
|
server {
|
||||||
@ -110,7 +107,7 @@ server {
|
|||||||
alias /var/www/html/.well-known/;
|
alias /var/www/html/.well-known/;
|
||||||
}
|
}
|
||||||
|
|
||||||
{% if not push_server %}
|
{% if not push_server %}
|
||||||
# Event stream for pushing events to web browsers
|
# Event stream for pushing events to web browsers
|
||||||
location ~ "^/ev/sub/(.*)" {
|
location ~ "^/ev/sub/(.*)" {
|
||||||
nchan_channel_id $1;
|
nchan_channel_id $1;
|
||||||
@ -122,14 +119,17 @@ server {
|
|||||||
nchan_channel_id $1;
|
nchan_channel_id $1;
|
||||||
nchan_subscriber longpoll;
|
nchan_subscriber longpoll;
|
||||||
}
|
}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
access_log /var/log/nginx/certidude-frontend-access.log;
|
||||||
|
error_log /var/log/nginx/certidude-frontend-error.log;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
server {
|
server {
|
||||||
# Section for certificate authenticated HTTPS clients,
|
# Section for certificate authenticated HTTPS clients,
|
||||||
# for submitting information to CA eg. leases,
|
# for submitting information to CA eg. leases,
|
||||||
# renewing certificates and
|
# requesting/renewing certificates and
|
||||||
# for delivering scripts to clients
|
# for delivering scripts to clients
|
||||||
|
|
||||||
server_name {{ common_name }};
|
server_name {{ common_name }};
|
||||||
@ -150,6 +150,9 @@ server {
|
|||||||
nchan_channel_id $1;
|
nchan_channel_id $1;
|
||||||
nchan_subscriber longpoll;
|
nchan_subscriber longpoll;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
access_log /var/log/nginx/certidude-mutual-auth-access.log;
|
||||||
|
error_log /var/log/nginx/certidude-mutual-auth-error.log;
|
||||||
}
|
}
|
||||||
|
|
||||||
{% if not push_server %}
|
{% if not push_server %}
|
||||||
@ -167,6 +170,10 @@ server {
|
|||||||
nchan_publisher;
|
nchan_publisher;
|
||||||
nchan_channel_id $1;
|
nchan_channel_id $1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
access_log /var/log/nginx/certidude-push-access.log;
|
||||||
|
error_log /var/log/nginx/certidude-push-error.log;
|
||||||
|
|
||||||
}
|
}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
[DEFAULT]
|
[DEFAULT]
|
||||||
|
enabled = no
|
||||||
ou =
|
ou =
|
||||||
lifetime = 120
|
lifetime = 120
|
||||||
ca = false
|
ca = false
|
||||||
@ -6,7 +7,19 @@ common name = RE_COMMON_NAME
|
|||||||
key usage = digital_signature key_encipherment
|
key usage = digital_signature key_encipherment
|
||||||
extended key usage =
|
extended key usage =
|
||||||
|
|
||||||
|
# Strongswan can automatically fetch CRL if
|
||||||
|
# CRL distribution point extension is included in the certificate
|
||||||
|
;revoked url =
|
||||||
|
revoked url = {{ revoked_url }}
|
||||||
|
|
||||||
|
# StrongSwan can automatically query OCSP responder if
|
||||||
|
# AIA extension includes OCSP responder URL
|
||||||
|
;responder url =
|
||||||
|
;responder url = no check
|
||||||
|
responder url = {{ responder_url }}
|
||||||
|
|
||||||
[ca]
|
[ca]
|
||||||
|
enabled = yes
|
||||||
title = Certificate Authority
|
title = Certificate Authority
|
||||||
common name = ^ca
|
common name = ^ca
|
||||||
ca = true
|
ca = true
|
||||||
@ -15,12 +28,14 @@ extended key usage =
|
|||||||
lifetime = 1095
|
lifetime = 1095
|
||||||
|
|
||||||
[rw]
|
[rw]
|
||||||
|
enabled = yes
|
||||||
title = Roadwarrior
|
title = Roadwarrior
|
||||||
ou = Roadwarrior
|
ou = Roadwarrior
|
||||||
common name = RE_HOSTNAME
|
common name = RE_HOSTNAME
|
||||||
extended key usage = client_auth
|
extended key usage = client_auth
|
||||||
|
|
||||||
[srv]
|
[srv]
|
||||||
|
enabled = yes
|
||||||
title = Server
|
title = Server
|
||||||
ou = Server
|
ou = Server
|
||||||
common name = RE_FQDN
|
common name = RE_FQDN
|
||||||
@ -28,6 +43,7 @@ lifetime = 120
|
|||||||
extended key usage = server_auth client_auth
|
extended key usage = server_auth client_auth
|
||||||
|
|
||||||
[gw]
|
[gw]
|
||||||
|
enabled = yes
|
||||||
title = Gateway
|
title = Gateway
|
||||||
ou = Gateway
|
ou = Gateway
|
||||||
common name = RE_FQDN
|
common name = RE_FQDN
|
||||||
@ -36,6 +52,7 @@ lifetime = 120
|
|||||||
extended key usage = server_auth 1.3.6.1.5.5.8.2.2 client_auth
|
extended key usage = server_auth 1.3.6.1.5.5.8.2.2 client_auth
|
||||||
|
|
||||||
[ap]
|
[ap]
|
||||||
|
enabled = no
|
||||||
title = Access Point
|
title = Access Point
|
||||||
ou = Access Point
|
ou = Access Point
|
||||||
common name = RE_HOSTNAME
|
common name = RE_HOSTNAME
|
||||||
@ -43,6 +60,7 @@ lifetime = 120
|
|||||||
extended key usage = client_auth
|
extended key usage = client_auth
|
||||||
|
|
||||||
[mfp]
|
[mfp]
|
||||||
|
enabled = no
|
||||||
title = Printers
|
title = Printers
|
||||||
ou = MFP
|
ou = MFP
|
||||||
common name = ^mfp\-
|
common name = ^mfp\-
|
||||||
@ -50,9 +68,16 @@ lifetime = 120
|
|||||||
extended key usage = client_auth
|
extended key usage = client_auth
|
||||||
|
|
||||||
[cam]
|
[cam]
|
||||||
|
enabled = no
|
||||||
title = Camera
|
title = Camera
|
||||||
ou = IP Camera
|
ou = IP Camera
|
||||||
common name = ^cam\-
|
common name = ^cam\-
|
||||||
lifetime = 120
|
lifetime = 120
|
||||||
extended key usage = client_auth
|
extended key usage = client_auth
|
||||||
|
|
||||||
|
[ocsp]
|
||||||
|
enabled = no
|
||||||
|
title = OCSP Responder
|
||||||
|
common name = ^ocsp
|
||||||
|
lifetime = 7
|
||||||
|
responder url = nocheck
|
||||||
|
@ -68,6 +68,9 @@ ldap base = {{ base }}
|
|||||||
ldap base = dc=example,dc=lan
|
ldap base = dc=example,dc=lan
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
ldap mail attribute = mail
|
||||||
|
;ldap mail attribute = otherMailbox
|
||||||
|
|
||||||
[authorization]
|
[authorization]
|
||||||
# The authorization backend specifies how the users are authorized.
|
# The authorization backend specifies how the users are authorized.
|
||||||
# In case of 'posix' simply group membership is asserted,
|
# In case of 'posix' simply group membership is asserted,
|
||||||
@ -182,16 +185,6 @@ revocation list lifetime = 24
|
|||||||
# URL where CA certificate can be fetched from
|
# URL where CA certificate can be fetched from
|
||||||
authority certificate url = {{ certificate_url }}
|
authority certificate url = {{ certificate_url }}
|
||||||
|
|
||||||
# Strongswan can automatically fetch CRL if
|
|
||||||
# CRL distribution point extension is included in the certificate
|
|
||||||
;revoked url =
|
|
||||||
revoked url = {{ revoked_url }}
|
|
||||||
|
|
||||||
# StrongSwan can automatically query OCSP responder if
|
|
||||||
# AIA extension includes OCSP responder URL
|
|
||||||
responder url =
|
|
||||||
;responder url = {{ responder_url }}
|
|
||||||
|
|
||||||
|
|
||||||
[push]
|
[push]
|
||||||
# This should occasionally be regenerated
|
# This should occasionally be regenerated
|
||||||
@ -242,8 +235,12 @@ expired dir = {{ directory }}/expired/
|
|||||||
# and make sure Certidude machine doesn't try to accept mails.
|
# and make sure Certidude machine doesn't try to accept mails.
|
||||||
# uncomment mail sender address to enable e-mails.
|
# uncomment mail sender address to enable e-mails.
|
||||||
# Make sure used e-mail address is reachable for end users.
|
# Make sure used e-mail address is reachable for end users.
|
||||||
name = Certificate management
|
name = Certidude at {{ common_name }}
|
||||||
address = certificates@example.lan
|
{% if domain %}
|
||||||
|
address = certificates@{{ domain }}
|
||||||
|
{% else %}
|
||||||
|
address = certificates@exaple.com
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
[tagging]
|
[tagging]
|
||||||
owner/string = Owner
|
owner/string = Owner
|
||||||
@ -259,17 +256,20 @@ services template = {{ template_path }}/bootstrap.conf
|
|||||||
|
|
||||||
[token]
|
[token]
|
||||||
# Token mechanism allows authority administrator to send invites for users.
|
# Token mechanism allows authority administrator to send invites for users.
|
||||||
# Token API call /api/token/ could be for example exposed on the internet via proxypass.
|
# Backend for tokens, set none to disable
|
||||||
# Token mechanism disabled by setting URL setting to none
|
;backend =
|
||||||
;url = http://ca.example.com/
|
backend = sql
|
||||||
url =
|
|
||||||
|
|
||||||
# Token lifetime in minutes, 30 minutes by default.
|
# Database path for SQL backend
|
||||||
|
database = sqlite://{{ directory }}/meta/db.sqlite
|
||||||
|
|
||||||
|
# URL format
|
||||||
|
url = {{ token_url }}
|
||||||
|
|
||||||
|
# Token lifetime in minutes, 48 hours by default.
|
||||||
# Note that code tolerates 5 minute clock skew.
|
# Note that code tolerates 5 minute clock skew.
|
||||||
lifetime = 30
|
lifetime = 2880
|
||||||
|
|
||||||
# Secret for generating and validating tokens, regenerate occasionally
|
|
||||||
secret = {{ token_secret }}
|
|
||||||
|
|
||||||
[script]
|
[script]
|
||||||
# Path to the folder with scripts that can be served to the clients, set none to disable scripting
|
# Path to the folder with scripts that can be served to the clients, set none to disable scripting
|
||||||
@ -279,4 +279,4 @@ path = {{ script_dir }}
|
|||||||
|
|
||||||
[service]
|
[service]
|
||||||
protocols = ikev2 https openvpn
|
protocols = ikev2 https openvpn
|
||||||
routers = ^router\d?\.
|
routers = ^(router|vpn|gw|gateway)\d*\.
|
||||||
|
34
certidude/templates/server/site.sh
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
# Configure port tagging
|
||||||
|
uci set network.lan.ifname='eth0.3' # Protected network VLAN3 tagged
|
||||||
|
uci set network.guest.ifname='eth0.4' # Public network VLAN4 tagged
|
||||||
|
|
||||||
|
# Configure wireless networks
|
||||||
|
for band in 2ghz 5ghz; do
|
||||||
|
uci delete wireless.radio$band.disabled
|
||||||
|
uci set wireless.radio$band.country=EE
|
||||||
|
|
||||||
|
uci set wireless.guest$band=wifi-iface
|
||||||
|
uci set wireless.guest$band.network=guest
|
||||||
|
uci set wireless.guest$band.mode=ap
|
||||||
|
uci set wireless.guest$band.device=radio$band
|
||||||
|
uci set wireless.guest$band.encryption=none
|
||||||
|
uci set wireless.guest$band.ssid="k-space.ee guest"
|
||||||
|
|
||||||
|
uci set wireless.lan$band=wifi-iface
|
||||||
|
uci set wireless.lan$band.network=lan
|
||||||
|
uci set wireless.lan$band.mode=ap
|
||||||
|
uci set wireless.lan$band.device=radio$band
|
||||||
|
uci set wireless.lan$band.encryption=psk2+ccmp
|
||||||
|
uci set wireless.lan$band.ssid="k-space protected"
|
||||||
|
uci set wireless.lan$band.key="salakala"
|
||||||
|
|
||||||
|
done
|
||||||
|
|
||||||
|
# Add Lauri's Yubikey
|
||||||
|
cat > /etc/dropbear/authorized_keys << \EOF
|
||||||
|
ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCb4iqSrJrA13ygAZTZb6ElPsMXrlXXrztxt3bcKuEbAiWOm9lR17puRLMZbM2tvAW+iwsDHfQAs0E6HDprP68nt+SGkQvItUtYeJBWDI405DbRodmDMySahmb6o6S3sqI4vryydOg1G+Z0DITksZzp91Ow+C++emk6aqWfXh7xATexCvKphfwXrBL+MDIwx6drIiN0FD08yd/zxGAlcQpR8o6uecmXdk32wL5W3+qqwbJrLjZmOweij5KSXuEARuQhM20KXzYzzQIAKqhIoALRSEX31L0bwxOqfVaotzk4TWKJSeetEhBOd7PtH0ZrmOHF+B20Ym+V3UkRY5P4calF
|
||||||
|
EOF
|
||||||
|
|
||||||
|
# Set root password to 'salakala'
|
||||||
|
sed -i 's|^root::|root:$1$S0wGaZqK$fzEzb0WTC5.WHm2Fz9UI9.:|' /etc/shadow
|
||||||
|
|
85
certidude/tokens.py
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
|
||||||
|
import string
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
from certidude import authority, config, mailer, const
|
||||||
|
from certidude.relational import RelationalMixin
|
||||||
|
from certidude.common import random
|
||||||
|
|
||||||
|
class TokenManager(RelationalMixin):
|
||||||
|
SQL_CREATE_TABLES = "token_tables.sql"
|
||||||
|
|
||||||
|
def consume(self, uuid):
|
||||||
|
now = datetime.utcnow()
|
||||||
|
retval = self.get(
|
||||||
|
"select subject, mail, created, expires, profile from token where uuid = ? and created < ? and ? < expires and used is null",
|
||||||
|
uuid,
|
||||||
|
now + const.CLOCK_SKEW_TOLERANCE,
|
||||||
|
now - const.CLOCK_SKEW_TOLERANCE)
|
||||||
|
self.execute(
|
||||||
|
"update token set used = ? where uuid = ?",
|
||||||
|
now,
|
||||||
|
uuid)
|
||||||
|
return retval
|
||||||
|
|
||||||
|
def issue(self, issuer, subject, subject_mail=None):
|
||||||
|
# Expand variables
|
||||||
|
subject_username = subject.name
|
||||||
|
if not subject_mail:
|
||||||
|
subject_mail = subject.mail
|
||||||
|
|
||||||
|
# Generate token
|
||||||
|
token = ''.join(random.choice(string.ascii_lowercase + string.ascii_uppercase + string.digits) for _ in range(32))
|
||||||
|
token_created = datetime.utcnow()
|
||||||
|
token_expires = token_created + config.TOKEN_LIFETIME
|
||||||
|
|
||||||
|
self.sql_execute("token_issue.sql",
|
||||||
|
token_created, token_expires, token,
|
||||||
|
issuer.name if issuer else None,
|
||||||
|
subject_username, subject_mail, "rw")
|
||||||
|
|
||||||
|
# Token lifetime in local time, to select timezone: dpkg-reconfigure tzdata
|
||||||
|
try:
|
||||||
|
with open("/etc/timezone") as fh:
|
||||||
|
token_timezone = fh.read().strip()
|
||||||
|
except EnvironmentError:
|
||||||
|
token_timezone = None
|
||||||
|
|
||||||
|
router = sorted([j[0] for j in authority.list_signed(
|
||||||
|
common_name=config.SERVICE_ROUTERS)])[0]
|
||||||
|
protocols = ",".join(config.SERVICE_PROTOCOLS)
|
||||||
|
url = config.TOKEN_URL % locals()
|
||||||
|
|
||||||
|
context = globals()
|
||||||
|
context.update(locals())
|
||||||
|
|
||||||
|
mailer.send("token.md", to=subject_mail, **context)
|
||||||
|
return {
|
||||||
|
"token": token,
|
||||||
|
"url": url,
|
||||||
|
}
|
||||||
|
|
||||||
|
def list(self, expired=False, used=False, token=False):
|
||||||
|
stmt = "select created as 'created[timestamp]', expires as 'expires[timestamp]', used as 'used[timestamp]', issuer, mail, subject"
|
||||||
|
if token:
|
||||||
|
stmt += ", uuid"
|
||||||
|
stmt += " from token"
|
||||||
|
where = []
|
||||||
|
args = []
|
||||||
|
if not expired:
|
||||||
|
where.append(" expires > ?")
|
||||||
|
args.append(datetime.utcnow())
|
||||||
|
if not used:
|
||||||
|
where.append(" used is null")
|
||||||
|
if where:
|
||||||
|
stmt = stmt + " where " + (" and ".join(where))
|
||||||
|
stmt += " order by expires"
|
||||||
|
|
||||||
|
return self.iterfetch(stmt, *args)
|
||||||
|
|
||||||
|
def purge(self, all=False):
|
||||||
|
stmt = "delete from token"
|
||||||
|
args = []
|
||||||
|
if not all:
|
||||||
|
stmt += " where expires < ?"
|
||||||
|
args.append(datetime.utcnow())
|
||||||
|
return self.execute(stmt, *args)
|
@ -22,6 +22,8 @@ class User(object):
|
|||||||
return hash(self.mail)
|
return hash(self.mail)
|
||||||
|
|
||||||
def __eq__(self, other):
|
def __eq__(self, other):
|
||||||
|
if other == None:
|
||||||
|
return False
|
||||||
assert isinstance(other, User), "%s is not instance of User" % repr(other)
|
assert isinstance(other, User), "%s is not instance of User" % repr(other)
|
||||||
return self.mail == other.mail
|
return self.mail == other.mail
|
||||||
|
|
||||||
@ -90,7 +92,7 @@ class ActiveDirectoryUserManager(object):
|
|||||||
# TODO: Sanitize username
|
# TODO: Sanitize username
|
||||||
with DirectoryConnection() as conn:
|
with DirectoryConnection() as conn:
|
||||||
ft = config.LDAP_USER_FILTER % username
|
ft = config.LDAP_USER_FILTER % username
|
||||||
attribs = "cn", "givenName", "sn", "mail", "userPrincipalName"
|
attribs = "cn", "givenName", "sn", config.LDAP_MAIL_ATTRIBUTE, "userPrincipalName"
|
||||||
r = conn.search_s(config.LDAP_BASE, 2, ft, attribs)
|
r = conn.search_s(config.LDAP_BASE, 2, ft, attribs)
|
||||||
for dn, entry in r:
|
for dn, entry in r:
|
||||||
if not dn:
|
if not dn:
|
||||||
@ -105,21 +107,21 @@ class ActiveDirectoryUserManager(object):
|
|||||||
else:
|
else:
|
||||||
given_name, surname = cn, b""
|
given_name, surname = cn, b""
|
||||||
|
|
||||||
mail, = entry.get("mail") or entry.get("userPrincipalName") or ((username + "@" + const.DOMAIN).encode("ascii"),)
|
mail, = entry.get(config.LDAP_MAIL_ATTRIBUTE) or ((username + "@" + const.DOMAIN).encode("ascii"),)
|
||||||
return User(username, mail.decode("ascii"),
|
return User(username, mail.decode("ascii"),
|
||||||
given_name.decode("utf-8"), surname.decode("utf-8"))
|
given_name.decode("utf-8"), surname.decode("utf-8"))
|
||||||
raise User.DoesNotExist("User %s does not exist" % username)
|
raise User.DoesNotExist("User %s does not exist" % username)
|
||||||
|
|
||||||
def filter(self, ft):
|
def filter(self, ft):
|
||||||
with DirectoryConnection() as conn:
|
with DirectoryConnection() as conn:
|
||||||
attribs = "givenName", "surname", "samaccountname", "cn", "mail", "userPrincipalName"
|
attribs = "givenName", "surname", "samaccountname", "cn", config.LDAP_MAIL_ATTRIBUTE, "userPrincipalName"
|
||||||
r = conn.search_s(config.LDAP_BASE, 2, ft, attribs)
|
r = conn.search_s(config.LDAP_BASE, 2, ft, attribs)
|
||||||
for dn,entry in r:
|
for dn,entry in r:
|
||||||
if not dn:
|
if not dn:
|
||||||
continue
|
continue
|
||||||
username, = entry.get("sAMAccountName")
|
username, = entry.get("sAMAccountName")
|
||||||
cn, = entry.get("cn")
|
cn, = entry.get("cn")
|
||||||
mail, = entry.get("mail") or entry.get("userPrincipalName") or (username + b"@" + const.DOMAIN.encode("ascii"),)
|
mail, = entry.get(config.LDAP_MAIL_ATTRIBUTE) or entry.get("userPrincipalName") or (username + b"@" + const.DOMAIN.encode("ascii"),)
|
||||||
if entry.get("givenName") and entry.get("sn"):
|
if entry.get("givenName") and entry.get("sn"):
|
||||||
given_name, = entry.get("givenName")
|
given_name, = entry.get("givenName")
|
||||||
surname, = entry.get("sn")
|
surname, = entry.get("sn")
|
||||||
|
@ -111,7 +111,7 @@ EOF
|
|||||||
make -C $BUILD/$BASENAME image FILES=$OVERLAY PROFILE=$PROFILE PACKAGES="luci \
|
make -C $BUILD/$BASENAME image FILES=$OVERLAY PROFILE=$PROFILE PACKAGES="luci \
|
||||||
openssl-util curl ca-certificates dropbear \
|
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 \
|
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 \
|
htop iftop netdata -odhcp6c -odhcpd -dnsmasq \
|
||||||
-luci-app-firewall \
|
-luci-app-firewall \
|
||||||
-pppd -luci-proto-ppp -kmod-ppp -ppp -ppp-mod-pppoe \
|
-pppd -luci-proto-ppp -kmod-ppp -ppp -ppp-mod-pppoe \
|
||||||
-kmod-ip6tables -ip6tables -luci-proto-ipv6 -kmod-iptunnel6 -kmod-ipsec6"
|
-kmod-ip6tables -ip6tables -luci-proto-ipv6 -kmod-iptunnel6 -kmod-ipsec6"
|
||||||
|
@ -29,9 +29,7 @@ AUTHORITY=$(hostname -f)
|
|||||||
mkdir -p $OVERLAY/etc/config
|
mkdir -p $OVERLAY/etc/config
|
||||||
mkdir -p $OVERLAY/etc/uci-defaults
|
mkdir -p $OVERLAY/etc/uci-defaults
|
||||||
mkdir -p $OVERLAY/etc/certidude/authority/$AUTHORITY/
|
mkdir -p $OVERLAY/etc/certidude/authority/$AUTHORITY/
|
||||||
cp /var/lib/certidude/$AUTHORITY/ca_cert.pem $OVERLAY/etc/certidude/authority/$AUTHORITY/
|
cp /var/lib/certidude/ca_cert.pem $OVERLAY/etc/certidude/authority/$AUTHORITY/
|
||||||
|
|
||||||
echo /etc/certidude >> $OVERLAY/etc/sysupgrade.conf
|
|
||||||
|
|
||||||
cat <<EOF > $OVERLAY/etc/config/certidude
|
cat <<EOF > $OVERLAY/etc/config/certidude
|
||||||
|
|
||||||
|
@ -40,5 +40,7 @@ EOF
|
|||||||
|
|
||||||
make -C $BUILD/$BASENAME image FILES=$OVERLAY PROFILE=$PROFILE PACKAGES="openssl-util curl ca-certificates \
|
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 \
|
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 \
|
iftop tcpdump nmap nano usbutils luci luci-app-mjpg-streamer kmod-video-uvc dropbear \
|
||||||
pciutils -dnsmasq -odhcpd -odhcp6c -kmod-ath9k picocom strongswan-mod-kernel-libipsec kmod-tun"
|
-pppd -luci-proto-ppp -kmod-ppp -ppp -ppp-mod-pppoe \
|
||||||
|
-dnsmasq -odhcpd -odhcp6c -kmod-ath9k picocom strongswan-mod-kernel-libipsec kmod-tun \
|
||||||
|
netdata"
|
||||||
|
@ -103,8 +103,9 @@ uci set uhttpd.main.listen_http=0.0.0.0:8080
|
|||||||
EOF
|
EOF
|
||||||
|
|
||||||
make -C $BUILD/$BASENAME image FILES=$OVERLAY PROFILE=$PROFILE PACKAGES="openssl-util curl ca-certificates htop \
|
make -C $BUILD/$BASENAME image FILES=$OVERLAY PROFILE=$PROFILE PACKAGES="openssl-util curl ca-certificates htop \
|
||||||
iftop tcpdump nmap nano mtr patch diffutils ipset usbutils luci dropbear kmod-tun \
|
iftop tcpdump nmap nano mtr patch diffutils ipset usbutils luci dropbear kmod-tun netdata \
|
||||||
strongswan-default strongswan-mod-kernel-libipsec strongswan-mod-openssl strongswan-mod-curl strongswan-mod-ccm strongswan-mod-gcm \
|
strongswan-default strongswan-mod-kernel-libipsec strongswan-mod-openssl strongswan-mod-curl strongswan-mod-ccm strongswan-mod-gcm \
|
||||||
pciutils -odhcpd -odhcp6c -kmod-ath9k picocom libustream-openssl kmod-crypto-gcm bc"
|
-odhcpd -odhcp6c -kmod-ath9k picocom libustream-openssl kmod-crypto-gcm \
|
||||||
|
-pppd -luci-proto-ppp -kmod-ppp -ppp -ppp-mod-pppoe \
|
||||||
|
-kmod-ip6tables -ip6tables -luci-proto-ipv6 -kmod-iptunnel6 -kmod-ipsec6"
|
||||||
|
|
||||||
|
@ -1,6 +1,11 @@
|
|||||||
|
# Randomize restart time
|
||||||
|
OFFSET=$(awk -v min=1 -v max=59 'BEGIN{srand(); print int(min+rand()*(max-min+1))}')
|
||||||
|
|
||||||
|
# wtf?! https://wiki.strongswan.org/issues/1501#note-7
|
||||||
cat << EOF > /etc/crontabs/root
|
cat << EOF > /etc/crontabs/root
|
||||||
15 1 * * * sleep 70 && touch /etc/banner && reboot
|
#$OFFSET 2 * * * sleep 70 && touch /etc/banner && reboot
|
||||||
10 1 1 */2 * /usr/bin/certidude-enroll-renew
|
$OFFSET 2 * * * ipsec restart
|
||||||
|
5 1 1 */2 * /usr/bin/certidude-enroll-renew
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
chmod 0600 /etc/crontabs/root
|
chmod 0600 /etc/crontabs/root
|
||||||
|
@ -122,6 +122,11 @@ logger -t certidude -s "Certificate md5sum: $(md5sum -b $CERTIFICATE_PATH.part)"
|
|||||||
|
|
||||||
uci commit
|
uci commit
|
||||||
|
|
||||||
|
echo $AUTHORITY_PATH >> /etc/sysupgrade.conf
|
||||||
|
echo $CERTIFICATE_PATH >> /etc/sysupgrade.conf
|
||||||
|
echo $KEY_PATH >> /etc/sysupgrade.conf
|
||||||
|
echo $REQUEST_PATH >> /etc/sysupgrade.conf
|
||||||
|
|
||||||
mv $CERTIFICATE_PATH.part $CERTIFICATE_PATH
|
mv $CERTIFICATE_PATH.part $CERTIFICATE_PATH
|
||||||
|
|
||||||
# Start services
|
# Start services
|
||||||
|
@ -8,6 +8,8 @@ CERTIFICATE_PATH=$DIR/host_cert.pem
|
|||||||
REQUEST_PATH=$DIR/host_req.pem
|
REQUEST_PATH=$DIR/host_req.pem
|
||||||
KEY_PATH=$DIR/host_key.pem
|
KEY_PATH=$DIR/host_key.pem
|
||||||
|
|
||||||
|
# TODO: fix Accepted 202 here
|
||||||
|
|
||||||
curl -f -L \
|
curl -f -L \
|
||||||
-H "Content-Type: application/pkcs10" \
|
-H "Content-Type: application/pkcs10" \
|
||||||
--data-binary @$REQUEST_PATH \
|
--data-binary @$REQUEST_PATH \
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
import pwd
|
import pwd
|
||||||
|
from asn1crypto import pem, x509
|
||||||
from oscrypto import asymmetric
|
from oscrypto import asymmetric
|
||||||
from csrbuilder import CSRBuilder, pem_armor_csr
|
from csrbuilder import CSRBuilder, pem_armor_csr
|
||||||
|
from asn1crypto.util import OrderedDict
|
||||||
from subprocess import check_output
|
from subprocess import check_output
|
||||||
from importlib import reload
|
from importlib import reload
|
||||||
from click.testing import CliRunner
|
from click.testing import CliRunner
|
||||||
@ -86,6 +88,9 @@ def clean_client():
|
|||||||
|
|
||||||
|
|
||||||
def clean_server():
|
def clean_server():
|
||||||
|
# Stop Samba
|
||||||
|
os.system("systemctl stop samba-ad-dc")
|
||||||
|
|
||||||
os.umask(0o22)
|
os.umask(0o22)
|
||||||
|
|
||||||
if os.path.exists("/run/certidude/server.pid"):
|
if os.path.exists("/run/certidude/server.pid"):
|
||||||
@ -134,14 +139,9 @@ def clean_server():
|
|||||||
if os.path.exists("/etc/openvpn/keys"):
|
if os.path.exists("/etc/openvpn/keys"):
|
||||||
shutil.rmtree("/etc/openvpn/keys")
|
shutil.rmtree("/etc/openvpn/keys")
|
||||||
|
|
||||||
# Stop samba
|
# Remove Samba stuff
|
||||||
if os.path.exists("/run/samba/samba.pid"):
|
|
||||||
with open("/run/samba/samba.pid") as fh:
|
|
||||||
try:
|
|
||||||
os.kill(int(fh.read()), 15)
|
|
||||||
except OSError:
|
|
||||||
pass
|
|
||||||
os.system("rm -Rfv /var/lib/samba/*")
|
os.system("rm -Rfv /var/lib/samba/*")
|
||||||
|
assert not os.path.exists("/var/lib/samba/private/secrets.keytab")
|
||||||
|
|
||||||
# Restore initial resolv.conf
|
# Restore initial resolv.conf
|
||||||
shutil.copyfile("/etc/resolv.conf.orig", "/etc/resolv.conf")
|
shutil.copyfile("/etc/resolv.conf.orig", "/etc/resolv.conf")
|
||||||
@ -156,7 +156,7 @@ def test_cli_setup_authority():
|
|||||||
assert os.getuid() == 0, "Run tests as root in a clean VM or container"
|
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"
|
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 samba krb5-user winbind bc")
|
os.system("DEBIAN_FRONTEND=noninteractive apt-get install -qq -y git build-essential python-dev libkrb5-dev samba krb5-user")
|
||||||
|
|
||||||
assert_cleanliness()
|
assert_cleanliness()
|
||||||
|
|
||||||
@ -211,10 +211,19 @@ def test_cli_setup_authority():
|
|||||||
assert const.HOSTNAME == "ca"
|
assert const.HOSTNAME == "ca"
|
||||||
assert const.DOMAIN == "example.lan"
|
assert const.DOMAIN == "example.lan"
|
||||||
|
|
||||||
|
# Bootstrap authority again with:
|
||||||
|
# - ECDSA certificates
|
||||||
|
# - POSIX auth
|
||||||
|
# - OCSP enabled
|
||||||
|
# - SCEP disabled
|
||||||
|
# - CRL enabled
|
||||||
|
|
||||||
assert os.system("certidude setup authority --elliptic-curve") == 0
|
assert os.system("certidude setup authority --elliptic-curve") == 0
|
||||||
|
|
||||||
assert_cleanliness()
|
assert_cleanliness()
|
||||||
|
|
||||||
|
assert os.path.exists("/var/lib/certidude/signed/ca.example.lan.pem"), "provisioning failed"
|
||||||
|
assert not os.path.exists("/etc/cron.hourly/certidude")
|
||||||
|
|
||||||
# Make sure nginx is running
|
# Make sure nginx is running
|
||||||
assert os.system("nginx -t") == 0, "invalid nginx configuration"
|
assert os.system("nginx -t") == 0, "invalid nginx configuration"
|
||||||
@ -222,12 +231,6 @@ def test_cli_setup_authority():
|
|||||||
|
|
||||||
# Make sure we generated legit CA certificate
|
# Make sure we generated legit CA certificate
|
||||||
from certidude import config, authority, 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.certificate["tbs_certificate"]["validity"]["not_before"].native.replace(tzinfo=None) < datetime.utcnow()
|
|
||||||
assert authority.public_key.algorithm == "ec"
|
|
||||||
|
|
||||||
# Generate garbage
|
# Generate garbage
|
||||||
with open("/var/lib/certidude/bla", "w") as fh:
|
with open("/var/lib/certidude/bla", "w") as fh:
|
||||||
@ -240,7 +243,6 @@ def test_cli_setup_authority():
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
# Start server before any signing operations are performed
|
# Start server before any signing operations are performed
|
||||||
config.CERTIFICATE_RENEWAL_ALLOWED = True
|
|
||||||
assert_cleanliness()
|
assert_cleanliness()
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
@ -255,16 +257,40 @@ def test_cli_setup_authority():
|
|||||||
|
|
||||||
|
|
||||||
# Test CA certificate fetch
|
# Test CA certificate fetch
|
||||||
buf = open("/var/lib/certidude/ca_cert.pem").read()
|
|
||||||
r = requests.get("http://ca.example.lan/api/certificate")
|
r = requests.get("http://ca.example.lan/api/certificate")
|
||||||
assert r.status_code == 200
|
assert r.status_code == 200
|
||||||
assert r.headers.get('content-type') == "application/x-x509-ca-cert"
|
assert r.headers.get('content-type') == "application/x-x509-ca-cert"
|
||||||
assert r.text == buf
|
header, _, certificate_der_bytes = pem.unarmor(r.text.encode("ascii"))
|
||||||
|
cert = x509.Certificate.load(certificate_der_bytes)
|
||||||
|
|
||||||
|
assert cert.subject.native.get("common_name") == "Certidude at ca.example.lan"
|
||||||
|
assert cert.subject.native.get("organizational_unit_name") == "Certificate Authority"
|
||||||
|
assert cert.serial_number >= 0x150000000000000000000000000000
|
||||||
|
assert cert.serial_number <= 0xfffffffffffffffffffffffffffffffffffffff
|
||||||
|
assert cert["tbs_certificate"]["validity"]["not_before"].native.replace(tzinfo=None) < datetime.utcnow()
|
||||||
|
assert cert["tbs_certificate"]["validity"]["not_after"].native.replace(tzinfo=None) > datetime.utcnow() + timedelta(days=7000)
|
||||||
|
assert cert["tbs_certificate"]["validity"]["not_before"].native.replace(tzinfo=None) < datetime.utcnow()
|
||||||
|
|
||||||
|
extensions = cert["tbs_certificate"]["extensions"].native
|
||||||
|
assert extensions[0] == OrderedDict([
|
||||||
|
('extn_id', 'basic_constraints'),
|
||||||
|
('critical', True),
|
||||||
|
('extn_value', OrderedDict([
|
||||||
|
('ca', True),
|
||||||
|
('path_len_constraint', None)]
|
||||||
|
))]), extensions[0]
|
||||||
|
# assert extensions[1][0] == "key_identifier", extensions[1]
|
||||||
|
|
||||||
|
assert extensions[2] == OrderedDict([
|
||||||
|
('extn_id', 'key_usage'),
|
||||||
|
('critical', True),
|
||||||
|
('extn_value', {'key_cert_sign', 'crl_sign'})]), extensions[3]
|
||||||
|
assert len(extensions) == 3
|
||||||
|
|
||||||
|
public_key = asymmetric.load_public_key(cert["tbs_certificate"]["subject_public_key_info"])
|
||||||
|
assert public_key.algorithm == "ec"
|
||||||
|
|
||||||
|
|
||||||
r = client().simulate_get("/api/certificate")
|
|
||||||
assert r.status_code == 200
|
|
||||||
assert r.headers.get('content-type') == "application/x-x509-ca-cert"
|
|
||||||
assert r.text == buf
|
|
||||||
|
|
||||||
# Password is bot, users created by Travis
|
# Password is bot, users created by Travis
|
||||||
usertoken = "Basic dXNlcmJvdDpib3Q="
|
usertoken = "Basic dXNlcmJvdDpib3Q="
|
||||||
@ -419,6 +445,38 @@ def test_cli_setup_authority():
|
|||||||
assert r.status_code == 200, r.text
|
assert r.status_code == 200, r.text
|
||||||
assert r.headers.get('content-type') == "application/x-pem-file"
|
assert r.headers.get('content-type') == "application/x-pem-file"
|
||||||
|
|
||||||
|
header, _, certificate_der_bytes = pem.unarmor(r.text.encode("ascii"))
|
||||||
|
cert = x509.Certificate.load(certificate_der_bytes)
|
||||||
|
assert cert.subject.native.get("common_name") == "test"
|
||||||
|
assert cert.subject.native.get("organizational_unit_name") == "Roadwarrior"
|
||||||
|
assert cert.serial_number >= 0x150000000000000000000000000000
|
||||||
|
assert cert.serial_number <= 0xfffffffffffffffffffffffffffffffffffffff
|
||||||
|
assert cert["tbs_certificate"]["validity"]["not_before"].native.replace(tzinfo=None) < datetime.utcnow()
|
||||||
|
assert cert["tbs_certificate"]["validity"]["not_after"].native.replace(tzinfo=None) > datetime.utcnow() + timedelta(days=100)
|
||||||
|
assert cert["tbs_certificate"]["validity"]["not_before"].native.replace(tzinfo=None) < datetime.utcnow()
|
||||||
|
|
||||||
|
public_key = asymmetric.load_public_key(cert["tbs_certificate"]["subject_public_key_info"])
|
||||||
|
assert public_key.algorithm == "ec"
|
||||||
|
"""
|
||||||
|
extensions = cert["tbs_certificate"]["extensions"].native
|
||||||
|
assert extensions[0] == OrderedDict([
|
||||||
|
('extn_id', 'basic_constraints'),
|
||||||
|
('critical', True),
|
||||||
|
('extn_value', OrderedDict([
|
||||||
|
('ca', True),
|
||||||
|
('path_len_constraint', None)]
|
||||||
|
))]), extensions[0]
|
||||||
|
# assert extensions[1][0] == "key_identifier", extensions[1]
|
||||||
|
|
||||||
|
assert extensions[2] == OrderedDict([
|
||||||
|
('extn_id', 'key_usage'),
|
||||||
|
('critical', True),
|
||||||
|
('extn_value', {'key_cert_sign', 'crl_sign'})]), extensions[3]
|
||||||
|
assert len(extensions) == 3
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
r = client().simulate_get("/api/signed/test/", headers={"Accept":"application/json"})
|
r = client().simulate_get("/api/signed/test/", headers={"Accept":"application/json"})
|
||||||
assert r.status_code == 200, r.text
|
assert r.status_code == 200, r.text
|
||||||
assert r.headers.get('content-type') == "application/json"
|
assert r.headers.get('content-type') == "application/json"
|
||||||
@ -628,12 +686,7 @@ def test_cli_setup_authority():
|
|||||||
assert ev_url.startswith("http://ca.example.lan/ev/sub/")
|
assert ev_url.startswith("http://ca.example.lan/ev/sub/")
|
||||||
|
|
||||||
|
|
||||||
#######################
|
# TODO: issue token, should fail because there are no routers
|
||||||
### Token mechanism ###
|
|
||||||
#######################
|
|
||||||
|
|
||||||
# TODO
|
|
||||||
|
|
||||||
|
|
||||||
#############
|
#############
|
||||||
### nginx ###
|
### nginx ###
|
||||||
@ -768,6 +821,19 @@ def test_cli_setup_authority():
|
|||||||
assert "Writing certificate to:" in result.output, result.output
|
assert "Writing certificate to:" in result.output, result.output
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
#######################
|
||||||
|
### Token mechanism ###
|
||||||
|
#######################
|
||||||
|
|
||||||
|
r = client().simulate_post("/api/token/",
|
||||||
|
body="username=userbot",
|
||||||
|
headers={"content-type": "application/x-www-form-urlencoded", "Authorization":admintoken})
|
||||||
|
assert r.status_code == 200
|
||||||
|
|
||||||
|
# TODO: check consume
|
||||||
|
|
||||||
|
|
||||||
#################################
|
#################################
|
||||||
### Subscribe to event source ###
|
### Subscribe to event source ###
|
||||||
#################################
|
#################################
|
||||||
@ -1055,8 +1121,9 @@ def test_cli_setup_authority():
|
|||||||
clean_server()
|
clean_server()
|
||||||
|
|
||||||
# Bootstrap domain controller here,
|
# Bootstrap domain controller here,
|
||||||
# Samba startup takes some timec
|
# Samba startup takes some time
|
||||||
os.system("samba-tool domain provision --server-role=dc --domain=EXAMPLE --realm=EXAMPLE.LAN --host-name=ca")
|
os.system("samba-tool domain provision --server-role=dc --domain=EXAMPLE --realm=EXAMPLE.LAN --host-name=ca")
|
||||||
|
os.system("systemctl restart samba-ad-dc")
|
||||||
os.system("samba-tool user add userbot S4l4k4l4 --given-name='User' --surname='Bot'")
|
os.system("samba-tool user add userbot S4l4k4l4 --given-name='User' --surname='Bot'")
|
||||||
os.system("samba-tool user add adminbot S4l4k4l4 --given-name='Admin' --surname='Bot'")
|
os.system("samba-tool user add adminbot S4l4k4l4 --given-name='Admin' --surname='Bot'")
|
||||||
os.system("samba-tool group addmembers 'Domain Admins' adminbot")
|
os.system("samba-tool group addmembers 'Domain Admins' adminbot")
|
||||||
@ -1069,7 +1136,7 @@ def test_cli_setup_authority():
|
|||||||
with open("/etc/resolv.conf", "w") as fh:
|
with open("/etc/resolv.conf", "w") as fh:
|
||||||
fh.write("nameserver 127.0.0.1\nsearch example.lan\n")
|
fh.write("nameserver 127.0.0.1\nsearch example.lan\n")
|
||||||
# TODO: dig -t srv perhaps?
|
# TODO: dig -t srv perhaps?
|
||||||
os.system("samba")
|
|
||||||
|
|
||||||
# Samba bind 636 late (probably generating keypair)
|
# Samba bind 636 late (probably generating keypair)
|
||||||
# so LDAPS connections below will fail
|
# so LDAPS connections below will fail
|
||||||
@ -1088,28 +1155,29 @@ def test_cli_setup_authority():
|
|||||||
assert os.system("echo S4l4k4l4 | kinit administrator") == 0
|
assert os.system("echo S4l4k4l4 | kinit administrator") == 0
|
||||||
assert os.path.exists("/tmp/krb5cc_0")
|
assert os.path.exists("/tmp/krb5cc_0")
|
||||||
|
|
||||||
# Fork to not contaminate environment while creating service principal
|
# Set up HTTP service principal
|
||||||
spn_pid = os.fork()
|
os.system("sed -e 's/CA/CA\\nkerberos method = system keytab/' -i /etc/samba/smb.conf ")
|
||||||
if not spn_pid:
|
assert os.system("KRB5_KTNAME=FILE:/etc/certidude/server.keytab net ads keytab add HTTP -k") == 0
|
||||||
os.system("sed -e 's/CA/CA\\nkerberos method = system keytab/' -i /etc/samba/smb.conf ")
|
assert os.path.exists("/etc/certidude/server.keytab")
|
||||||
os.environ["KRB5_KTNAME"] = "FILE:/etc/certidude/server.keytab"
|
os.system("chown root:certidude /etc/certidude/server.keytab")
|
||||||
assert os.system("net ads keytab add HTTP -k") == 0
|
os.system("chmod 640 /etc/certidude/server.keytab")
|
||||||
assert os.path.exists("/etc/certidude/server.keytab")
|
|
||||||
os.system("chown root:certidude /etc/certidude/server.keytab")
|
|
||||||
os.system("chmod 640 /etc/certidude/server.keytab")
|
|
||||||
return
|
|
||||||
else:
|
|
||||||
os.waitpid(spn_pid, 0)
|
|
||||||
|
|
||||||
assert_cleanliness()
|
assert_cleanliness()
|
||||||
r = requests.get("http://ca.example.lan/api/")
|
r = requests.get("http://ca.example.lan/api/")
|
||||||
assert r.status_code == 502, r.text
|
assert r.status_code == 502, r.text
|
||||||
|
|
||||||
|
|
||||||
|
# Bootstrap authority again with:
|
||||||
|
# - RSA certificates
|
||||||
|
# - Kerberos auth
|
||||||
|
# - OCSP disabled
|
||||||
|
# - SCEP enabled
|
||||||
|
# - CRL disabled
|
||||||
|
|
||||||
# Bootstrap authority
|
|
||||||
assert not os.path.exists("/var/lib/certidude/ca_key.pem")
|
assert not os.path.exists("/var/lib/certidude/ca_key.pem")
|
||||||
assert os.system("certidude setup authority --skip-packages") == 0
|
assert os.system("certidude setup authority --skip-packages") == 0
|
||||||
|
assert os.path.exists("/var/lib/certidude/ca_key.pem")
|
||||||
|
assert os.path.exists("/etc/cron.hourly/certidude")
|
||||||
|
|
||||||
|
|
||||||
# Make modifications to /etc/certidude/server.conf so
|
# Make modifications to /etc/certidude/server.conf so
|
||||||
@ -1289,12 +1357,11 @@ def test_cli_setup_authority():
|
|||||||
|
|
||||||
assert os.system("systemctl stop certidude") == 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() == \
|
assert open("/etc/apparmor.d/local/usr.lib.ipsec.charon").read() == \
|
||||||
"/etc/certidude/authority/ca.example.lan/client_key.pem r,\n" + \
|
"/etc/certidude/authority/ca.example.lan/client_key.pem r,\n" + \
|
||||||
"/etc/certidude/authority/ca.example.lan/ca_cert.pem r,\n" + \
|
"/etc/certidude/authority/ca.example.lan/ca_cert.pem r,\n" + \
|
||||||
"/etc/certidude/authority/ca.example.lan/client_cert.pem r,\n"
|
"/etc/certidude/authority/ca.example.lan/client_cert.pem r,\n"
|
||||||
assert len(inbox) == 0, inbox # Make sure all messages were checked
|
# TODO: pop mails from /var/mail and check content
|
||||||
|
|
||||||
os.system("service nginx stop")
|
os.system("service nginx stop")
|
||||||
os.system("service openvpn stop")
|
os.system("service openvpn stop")
|
||||||
|