mirror of
https://github.com/laurivosandi/certidude
synced 2024-12-23 00:25:18 +00:00
Merge branch 'master' of github.com:laurivosandi/certidude
This commit is contained in:
commit
dc9e01b4ad
@ -44,7 +44,6 @@ class SessionResource(object):
|
|||||||
@login_required
|
@login_required
|
||||||
@event_source
|
@event_source
|
||||||
def on_get(self, req, resp):
|
def on_get(self, req, resp):
|
||||||
|
|
||||||
return dict(
|
return dict(
|
||||||
user = dict(
|
user = dict(
|
||||||
name=req.context.get("user").name,
|
name=req.context.get("user").name,
|
||||||
|
@ -12,6 +12,8 @@ KEYWORDS = (
|
|||||||
(u"iPhone", u"iphone"),
|
(u"iPhone", u"iphone"),
|
||||||
(u"iPad", u"ipad"),
|
(u"iPad", u"ipad"),
|
||||||
(u"Ubuntu", u"ubuntu"),
|
(u"Ubuntu", u"ubuntu"),
|
||||||
|
(u"Fedora", u"fedora"),
|
||||||
|
(u"Linux", u"linux"),
|
||||||
)
|
)
|
||||||
|
|
||||||
class BundleResource(object):
|
class BundleResource(object):
|
||||||
@ -29,8 +31,15 @@ class BundleResource(object):
|
|||||||
hashlib.sha256(req.user_agent).hexdigest()[:8])
|
hashlib.sha256(req.user_agent).hexdigest()[:8])
|
||||||
|
|
||||||
logger.info(u"Signing bundle %s for %s", common_name, req.context.get("user"))
|
logger.info(u"Signing bundle %s for %s", common_name, req.context.get("user"))
|
||||||
resp.set_header("Content-Type", "application/x-pkcs12")
|
if config.BUNDLE_FORMAT == "p12":
|
||||||
resp.set_header("Content-Disposition", "attachment; filename=%s.p12" % common_name.encode("ascii"))
|
resp.set_header("Content-Type", "application/x-pkcs12")
|
||||||
resp.body, cert = authority.generate_pkcs12_bundle(common_name,
|
resp.set_header("Content-Disposition", "attachment; filename=%s.p12" % common_name.encode("ascii"))
|
||||||
owner=req.context.get("user"))
|
resp.body, cert = authority.generate_pkcs12_bundle(common_name,
|
||||||
|
owner=req.context.get("user"))
|
||||||
|
elif config.BUNDLE_FORMAT == "ovpn":
|
||||||
|
resp.set_header("Content-Type", "application/x-openvpn")
|
||||||
|
resp.set_header("Content-Disposition", "attachment; filename=%s.ovpn" % common_name.encode("ascii"))
|
||||||
|
resp.body, cert = authority.generate_ovpn_bundle(common_name,
|
||||||
|
owner=req.context.get("user"))
|
||||||
|
else:
|
||||||
|
raise ValueError("Unknown bundle format %s" % config.BUNDLE_FORMAT)
|
||||||
|
@ -122,9 +122,9 @@ def authenticate(optional=False):
|
|||||||
import ldap
|
import ldap
|
||||||
|
|
||||||
if not req.auth:
|
if not req.auth:
|
||||||
resp.append_header("WWW-Authenticate", "Basic")
|
raise falcon.HTTPUnauthorized("Unauthorized",
|
||||||
raise falcon.HTTPUnauthorized("Forbidden",
|
"No authentication header provided",
|
||||||
"Please authenticate with %s domain account or supply UPN" % const.DOMAIN)
|
("Basic",))
|
||||||
|
|
||||||
if not req.auth.startswith("Basic "):
|
if not req.auth.startswith("Basic "):
|
||||||
raise falcon.HTTPForbidden("Forbidden", "Bad header: %s" % req.auth)
|
raise falcon.HTTPForbidden("Forbidden", "Bad header: %s" % req.auth)
|
||||||
@ -133,26 +133,35 @@ def authenticate(optional=False):
|
|||||||
basic, token = req.auth.split(" ", 1)
|
basic, token = req.auth.split(" ", 1)
|
||||||
user, passwd = b64decode(token).split(":", 1)
|
user, passwd = b64decode(token).split(":", 1)
|
||||||
|
|
||||||
for server in config.LDAP_SERVERS:
|
click.echo("Connecting to %s as %s" % (config.LDAP_AUTHENTICATION_URI, user))
|
||||||
click.echo("Connecting to %s as %s" % (server, user))
|
conn = ldap.initialize(config.LDAP_AUTHENTICATION_URI)
|
||||||
conn = ldap.initialize(server)
|
conn.set_option(ldap.OPT_REFERRALS, 0)
|
||||||
conn.set_option(ldap.OPT_REFERRALS, 0)
|
|
||||||
try:
|
|
||||||
conn.simple_bind_s(user if "@" in user else "%s@%s" % (user, const.DOMAIN), passwd)
|
|
||||||
except ldap.LDAPError, e:
|
|
||||||
resp.append_header("WWW-Authenticate", "Basic")
|
|
||||||
logger.critical(u"LDAP bind authentication failed for user %s from %s",
|
|
||||||
repr(user), req.context.get("remote_addr"))
|
|
||||||
raise falcon.HTTPUnauthorized("Forbidden",
|
|
||||||
"Please authenticate with %s domain account or supply UPN" % const.DOMAIN)
|
|
||||||
|
|
||||||
req.context["ldap_conn"] = conn
|
if "@" not in user:
|
||||||
break
|
user = "%s@%s" % (user, const.DOMAIN)
|
||||||
else:
|
logger.debug("Expanded username to %s", user)
|
||||||
raise ValueError("No LDAP servers!")
|
|
||||||
|
|
||||||
|
try:
|
||||||
|
conn.simple_bind_s(user, passwd)
|
||||||
|
except ldap.STRONG_AUTH_REQUIRED:
|
||||||
|
logger.critical("LDAP server demands encryption, use ldaps:// instead of ldaps://")
|
||||||
|
raise
|
||||||
|
except ldap.SERVER_DOWN:
|
||||||
|
logger.critical("Failed to connect LDAP server at %s, are you sure LDAP server's CA certificate has been copied to this machine?",
|
||||||
|
config.LDAP_AUTHENTICATION_URI)
|
||||||
|
raise
|
||||||
|
except ldap.INVALID_CREDENTIALS:
|
||||||
|
logger.critical(u"LDAP bind authentication failed for user %s from %s",
|
||||||
|
repr(user), req.context.get("remote_addr"))
|
||||||
|
raise falcon.HTTPUnauthorized("Forbidden",
|
||||||
|
"Please authenticate with %s domain account or supply UPN" % const.DOMAIN,
|
||||||
|
("Basic",))
|
||||||
|
|
||||||
|
req.context["ldap_conn"] = conn
|
||||||
req.context["user"] = User.objects.get(user)
|
req.context["user"] = User.objects.get(user)
|
||||||
return func(resource, req, resp, *args, **kwargs)
|
retval = func(resource, req, resp, *args, **kwargs)
|
||||||
|
conn.unbind_s()
|
||||||
|
return retval
|
||||||
|
|
||||||
|
|
||||||
def pam_authenticate(resource, req, resp, *args, **kwargs):
|
def pam_authenticate(resource, req, resp, *args, **kwargs):
|
||||||
|
@ -14,6 +14,7 @@ from cryptography.hazmat.primitives import hashes, serialization
|
|||||||
from certidude import config, push, mailer, const
|
from certidude import config, push, mailer, const
|
||||||
from certidude.wrappers import Certificate, Request
|
from certidude.wrappers import Certificate, Request
|
||||||
from certidude import errors
|
from certidude import errors
|
||||||
|
from jinja2 import Template
|
||||||
|
|
||||||
RE_HOSTNAME = "^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\-]*[A-Za-z0-9])(@(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\-]*[A-Za-z0-9]))?$"
|
RE_HOSTNAME = "^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\-]*[A-Za-z0-9])(@(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9\-]*[A-Za-z0-9]))?$"
|
||||||
|
|
||||||
@ -21,7 +22,6 @@ RE_HOSTNAME = "^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9\-]*[a-zA-Z0-9])\.)*([A-Za-z
|
|||||||
# https://jamielinux.com/docs/openssl-certificate-authority/
|
# https://jamielinux.com/docs/openssl-certificate-authority/
|
||||||
# http://pycopia.googlecode.com/svn/trunk/net/pycopia/ssl/certs.py
|
# http://pycopia.googlecode.com/svn/trunk/net/pycopia/ssl/certs.py
|
||||||
|
|
||||||
|
|
||||||
# Cache CA certificate
|
# Cache CA certificate
|
||||||
certificate = Certificate(open(config.AUTHORITY_CERTIFICATE_PATH))
|
certificate = Certificate(open(config.AUTHORITY_CERTIFICATE_PATH))
|
||||||
|
|
||||||
@ -185,6 +185,39 @@ def delete_request(common_name):
|
|||||||
requests.delete(config.PUSH_PUBLISH % request.fingerprint(),
|
requests.delete(config.PUSH_PUBLISH % request.fingerprint(),
|
||||||
headers={"User-Agent": "Certidude API"})
|
headers={"User-Agent": "Certidude API"})
|
||||||
|
|
||||||
|
def generate_ovpn_bundle(common_name, owner=None):
|
||||||
|
# Construct private key
|
||||||
|
click.echo("Generating 4096-bit RSA key...")
|
||||||
|
|
||||||
|
key = rsa.generate_private_key(
|
||||||
|
public_exponent=65537,
|
||||||
|
key_size=4096,
|
||||||
|
backend=default_backend()
|
||||||
|
)
|
||||||
|
|
||||||
|
csr = x509.CertificateSigningRequestBuilder().subject_name(x509.Name([
|
||||||
|
x509.NameAttribute(k, v) for k, v in (
|
||||||
|
(NameOID.COMMON_NAME, common_name),
|
||||||
|
(NameOID.GIVEN_NAME, owner and owner.given_name),
|
||||||
|
(NameOID.SURNAME, owner and owner.surname),
|
||||||
|
) if v
|
||||||
|
]))
|
||||||
|
|
||||||
|
# Sign CSR
|
||||||
|
cert = sign(Request(
|
||||||
|
csr.sign(key, hashes.SHA512(), default_backend()).public_bytes(serialization.Encoding.PEM)), overwrite=True)
|
||||||
|
|
||||||
|
bundle = Template(open(config.OPENVPN_BUNDLE_TEMPLATE).read()).render(
|
||||||
|
ca = certificate.dump(),
|
||||||
|
key = key.private_bytes(
|
||||||
|
encoding=serialization.Encoding.PEM,
|
||||||
|
format=serialization.PrivateFormat.TraditionalOpenSSL,
|
||||||
|
encryption_algorithm=serialization.NoEncryption()
|
||||||
|
),
|
||||||
|
cert = cert.dump(),
|
||||||
|
crl=export_crl(),
|
||||||
|
)
|
||||||
|
return bundle, cert
|
||||||
|
|
||||||
def generate_pkcs12_bundle(common_name, key_size=4096, owner=None):
|
def generate_pkcs12_bundle(common_name, key_size=4096, owner=None):
|
||||||
"""
|
"""
|
||||||
|
@ -18,8 +18,10 @@ AUTHENTICATION_BACKENDS = set([j for j in
|
|||||||
AUTHORIZATION_BACKEND = cp.get("authorization", "backend") # whitelist, ldap, posix
|
AUTHORIZATION_BACKEND = cp.get("authorization", "backend") # whitelist, ldap, posix
|
||||||
ACCOUNTS_BACKEND = cp.get("accounts", "backend") # posix, ldap
|
ACCOUNTS_BACKEND = cp.get("accounts", "backend") # posix, ldap
|
||||||
|
|
||||||
if ACCOUNTS_BACKEND == "ldap":
|
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_BASE = cp.get("accounts", "ldap base")
|
||||||
|
|
||||||
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])
|
||||||
@ -38,6 +40,9 @@ SIGNED_DIR = cp.get("authority", "signed dir")
|
|||||||
REVOKED_DIR = cp.get("authority", "revoked dir")
|
REVOKED_DIR = cp.get("authority", "revoked dir")
|
||||||
OUTBOX = cp.get("authority", "outbox")
|
OUTBOX = cp.get("authority", "outbox")
|
||||||
|
|
||||||
|
BUNDLE_FORMAT = cp.get("authority", "bundle format")
|
||||||
|
OPENVPN_BUNDLE_TEMPLATE = cp.get("authority", "openvpn bundle template")
|
||||||
|
|
||||||
USER_CERTIFICATE_ENROLLMENT = {
|
USER_CERTIFICATE_ENROLLMENT = {
|
||||||
"forbidden": False, "single allowed": True, "multiple allowed": True }[
|
"forbidden": False, "single allowed": True, "multiple allowed": True }[
|
||||||
cp.get("authority", "user certificate enrollment")]
|
cp.get("authority", "user certificate enrollment")]
|
||||||
@ -78,17 +83,4 @@ elif "ldap" == AUTHORIZATION_BACKEND:
|
|||||||
else:
|
else:
|
||||||
raise NotImplementedError("Unknown authorization backend '%s'" % AUTHORIZATION_BACKEND)
|
raise NotImplementedError("Unknown authorization backend '%s'" % AUTHORIZATION_BACKEND)
|
||||||
|
|
||||||
for line in open("/etc/ldap/ldap.conf"):
|
|
||||||
line = line.strip().lower()
|
|
||||||
if "#" in line:
|
|
||||||
line, _ = line.split("#", 1)
|
|
||||||
if not " " in line:
|
|
||||||
continue
|
|
||||||
key, value = line.split(" ", 1)
|
|
||||||
if key == "uri":
|
|
||||||
LDAP_SERVERS = set([j for j in value.split(" ") if j])
|
|
||||||
click.echo("LDAP servers: %s" % " ".join(LDAP_SERVERS))
|
|
||||||
elif key == "base":
|
|
||||||
LDAP_BASE = value
|
|
||||||
|
|
||||||
# TODO: Check if we don't have base or servers
|
# TODO: Check if we don't have base or servers
|
||||||
|
@ -9,11 +9,12 @@ backends = pam
|
|||||||
;backends = ldap
|
;backends = ldap
|
||||||
;backends = kerberos ldap
|
;backends = kerberos ldap
|
||||||
;backends = kerberos pam
|
;backends = kerberos pam
|
||||||
|
ldap uri = ldaps://dc1.example.com
|
||||||
|
|
||||||
[accounts]
|
[accounts]
|
||||||
# The accounts backend specifies how the user's given name, surname and e-mail
|
# The accounts backend specifies how the user's given name, surname and e-mail
|
||||||
# address are looked up. In case of 'posix' basically 'getent passwd' is performed,
|
# address are looked up. In case of 'posix' basically 'getent passwd' is performed,
|
||||||
# in case of 'ldap' a search is performed on LDAP server specified in /etc/ldap/ldap.conf
|
# in case of 'ldap' a search is performed on LDAP server specified by ldap uri
|
||||||
# with Kerberos credential cache initialized at path specified by environment variable KRB5CCNAME
|
# with Kerberos credential cache initialized at path specified by environment variable KRB5CCNAME
|
||||||
# If certidude setup authority was performed correctly the credential cache should be
|
# If certidude setup authority was performed correctly the credential cache should be
|
||||||
# updated automatically by /etc/cron.hourly/certidude
|
# updated automatically by /etc/cron.hourly/certidude
|
||||||
@ -21,6 +22,8 @@ backends = pam
|
|||||||
backend = posix
|
backend = posix
|
||||||
;backend = ldap
|
;backend = ldap
|
||||||
ldap gssapi credential cache = /run/certidude/krb5cc
|
ldap gssapi credential cache = /run/certidude/krb5cc
|
||||||
|
ldap uri = ldap://dc1.example.com
|
||||||
|
ldap base = {% if base %}{{ base }}{% else %}dc=example,dc=com{% endif %}
|
||||||
|
|
||||||
[authorization]
|
[authorization]
|
||||||
# The authorization backend specifies how the users are authorized.
|
# The authorization backend specifies how the users are authorized.
|
||||||
@ -92,3 +95,8 @@ revoked dir = {{ directory }}/revoked/
|
|||||||
expired dir = {{ directory }}/expired/
|
expired dir = {{ directory }}/expired/
|
||||||
outbox = {{ outbox }}
|
outbox = {{ outbox }}
|
||||||
|
|
||||||
|
bundle format = p12
|
||||||
|
;bundle format = ovpn
|
||||||
|
|
||||||
|
openvpn bundle template = /etc/certidude/template.ovpn
|
||||||
|
|
||||||
|
@ -70,17 +70,15 @@ class DirectoryConnection(object):
|
|||||||
raise ValueError("Ticket cache at %s not initialized, unable to "
|
raise ValueError("Ticket cache at %s not initialized, unable to "
|
||||||
"authenticate with computer account against LDAP server!" % config.LDAP_GSSAPI_CRED_CACHE)
|
"authenticate with computer account against LDAP server!" % config.LDAP_GSSAPI_CRED_CACHE)
|
||||||
os.environ["KRB5CCNAME"] = config.LDAP_GSSAPI_CRED_CACHE
|
os.environ["KRB5CCNAME"] = config.LDAP_GSSAPI_CRED_CACHE
|
||||||
for server in config.LDAP_SERVERS:
|
self.conn = ldap.initialize(config.LDAP_ACCOUNTS_URI)
|
||||||
self.conn = ldap.initialize(server)
|
self.conn.set_option(ldap.OPT_REFERRALS, 0)
|
||||||
self.conn.set_option(ldap.OPT_REFERRALS, 0)
|
click.echo("Connecing to %s using Kerberos ticket cache from %s" %
|
||||||
click.echo("Connecing to %s using Kerberos ticket cache from %s" %
|
(config.LDAP_ACCOUNTS_URI, config.LDAP_GSSAPI_CRED_CACHE))
|
||||||
(server, config.LDAP_GSSAPI_CRED_CACHE))
|
self.conn.sasl_interactive_bind_s('', ldap.sasl.gssapi())
|
||||||
self.conn.sasl_interactive_bind_s('', ldap.sasl.gssapi())
|
return self.conn
|
||||||
return self.conn
|
|
||||||
raise ValueError("No LDAP servers specified!")
|
|
||||||
|
|
||||||
def __exit__(self, type, value, traceback):
|
def __exit__(self, type, value, traceback):
|
||||||
self.conn.unbind_s
|
self.conn.unbind_s()
|
||||||
|
|
||||||
|
|
||||||
class ActiveDirectoryUserManager(object):
|
class ActiveDirectoryUserManager(object):
|
||||||
|
Loading…
Reference in New Issue
Block a user