1
0
mirror of https://github.com/laurivosandi/certidude synced 2024-12-22 16:25:17 +00:00

Merge branch 'master' of github.com:laurivosandi/certidude

This commit is contained in:
Lauri Võsandi 2017-04-14 01:50:33 +03:00
commit 8bf9ebfebb
14 changed files with 139 additions and 55 deletions

View File

@ -10,7 +10,7 @@ virtualenv:
install:
- pip install -r requirements.txt
- pip install --editable .
- pip install codecov pytest-cov
- pip install codecov pytest-cov click ipaddress humanize falcon
script:
- py.test --cov-report xml --cov=certidude tests/
cache:
@ -19,5 +19,10 @@ cache:
addons:
apt:
packages:
- python-ldap
- python-xattr
- python-setproctitle
- python-markdown
- python-jinja2
- python-configparser
- python-pyasn1
- python-openssl

View File

@ -6,7 +6,6 @@ import logging
import os
import click
import hashlib
import xattr
from datetime import datetime
from time import sleep
from certidude import authority, mailer
@ -46,6 +45,8 @@ class SessionResource(object):
@login_required
@event_source
def on_get(self, req, resp):
import xattr
def serialize_requests(g):
for common_name, path, buf, obj, server in g():
yield dict(
@ -126,7 +127,6 @@ class SessionResource(object):
requests=serialize_requests(authority.list_requests),
signed=serialize_certificates(authority.list_signed),
revoked=serialize_certificates(authority.list_revoked),
users=User.objects.all(),
admin_users = User.objects.filter_admins(),
user_subnets = config.USER_SUBNETS,
autosign_subnets = config.AUTOSIGN_SUBNETS,

View File

@ -1,11 +1,10 @@
import falcon
import logging
from ipaddress import ip_address
from xattr import getxattr, listxattr
from datetime import datetime
from certidude import config, authority
from certidude.decorators import serialize
from xattr import getxattr, listxattr
logger = logging.getLogger(__name__)

View File

@ -1,6 +1,5 @@
import click
import falcon
import logging
import os
import re
@ -22,6 +21,7 @@ if "kerberos" in config.AUTHENTICATION_BACKENDS:
def authenticate(optional=False):
import falcon
def wrapper(func):
def kerberos_authenticate(resource, req, resp, *args, **kwargs):
# If LDAP enabled and device is not Kerberos capable fall

View File

@ -15,7 +15,6 @@ from cryptography.hazmat.primitives import hashes, serialization
from certidude import config, push, mailer, const
from certidude import errors
from jinja2 import Template
from xattr import getxattr, listxattr, setxattr
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]))?$"
@ -259,7 +258,6 @@ def generate_pkcs12_bundle(common_name, key_size=4096, owner=None):
try:
from OpenSSL import crypto
except ImportError:
logger.error("For P12 bundles please install pyOpenSSL: pip install pyOpenSSL")
raise
else:
p12 = crypto.PKCS12()
@ -297,6 +295,8 @@ def sign(common_name, overwrite=False):
def _sign(csr, buf, overwrite=False):
assert buf.startswith("-----BEGIN CERTIFICATE REQUEST-----\n")
assert isinstance(csr, x509.CertificateSigningRequest)
from xattr import getxattr, listxattr, setxattr
common_name, = csr.subject.get_attributes_for_oid(NameOID.COMMON_NAME)
cert_path = os.path.join(config.SIGNED_DIR, common_name.value + ".pem")
renew = False
@ -375,5 +375,3 @@ def _sign(csr, buf, overwrite=False):
push.publish("request-signed", common_name.value)
return cert, cert_buf

View File

@ -8,7 +8,6 @@ import os
import pwd
import random
import re
import requests
import signal
import socket
import string
@ -17,6 +16,7 @@ import sys
from configparser import ConfigParser, NoOptionError, NoSectionError
from certidude.helpers import certidude_request_certificate
from certidude.common import expand_paths, ip_address, ip_network
from certidude.decorators import apt, rpm, pip
from cryptography import x509
from cryptography.x509.oid import NameOID, ExtendedKeyUsageOID
from cryptography.hazmat.backends import default_backend
@ -24,9 +24,7 @@ from cryptography.hazmat.primitives import serialization
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import rsa
from datetime import datetime, timedelta
from humanize import naturaltime
from jinja2 import Environment, PackageLoader
from setproctitle import setproctitle
import const
logger = logging.getLogger(__name__)
@ -67,6 +65,8 @@ ExecStart=%s request
@click.option("-r", "--renew", default=False, is_flag=True, help="Renew now")
@click.option("-f", "--fork", default=False, is_flag=True, help="Fork to background")
def certidude_request(fork, renew):
import requests
if not os.path.exists(const.CLIENT_CONFIG_PATH):
click.echo("No %s!" % const.CLIENT_CONFIG_PATH)
return 1
@ -485,6 +485,8 @@ def certidude_setup_nginx(authority, site_config, tls_config, common_name, direc
default="/etc/openvpn/client-to-site.conf",
type=click.File(mode="w", atomic=True, lazy=True),
help="OpenVPN configuration file")
@apt("openvpn python-requests-kerberos")
@rpm("openvpn python2-requests-kerberos")
def certidude_setup_openvpn_client(authority, remote, config, proto):
# Create corresponding section in Certidude client configuration file
@ -602,6 +604,9 @@ def certidude_setup_strongswan_server(authority, config, secrets, subnet, route,
@click.command("client", help="Set up strongSwan client")
@click.argument("authority")
@click.argument("remote")
@apt("network-manager-openvpn-gnome python-requests-kerberos")
@rpm("NetworkManager-openvpn-gnome python2-requests-kerberos")
@pip("ipsecparse")
def certidude_setup_strongswan_client(authority, config, remote, dpdaction):
# Create corresponding section in /etc/certidude/client.conf
client_config = ConfigParser()
@ -649,6 +654,8 @@ def certidude_setup_strongswan_client(authority, config, remote, dpdaction):
@click.command("networkmanager", help="Set up strongSwan client via NetworkManager")
@click.argument("authority") # Certidude server
@click.argument("remote") # StrongSwan gateway
@apt("strongswan-nm")
@rpm("NetworkManager-strongswan-gnome")
def certidude_setup_strongswan_networkmanager(authority, remote):
endpoint = "IPSec to %s" % remote
@ -744,10 +751,12 @@ def certidude_setup_openvpn_networkmanager(authority, remote):
@click.option("--authority-lifetime", default=20*365, help="Authority certificate lifetime in days, 20 years by default")
@click.option("--organization", "-o", default=None, help="Company or organization name")
@click.option("--organizational-unit", "-ou", default=None)
@click.option("--push-server", default="http://" + const.FQDN, help="Push server, by default http://%s" % const.FQDN)
@click.option("--push-server", help="Push server, by default http://%s" % const.FQDN)
@click.option("--directory", help="Directory for authority files")
@click.option("--server-flags", is_flag=True, help="Add TLS Server and IKE Intermediate extended key usage flags")
@click.option("--outbox", default="smtp://smtp.%s" % const.DOMAIN, help="SMTP server, smtp://smtp.%s by default" % const.DOMAIN)
@apt("python-setproctitle python-openssl python-falcon python-humanize python-markdown python-xattr")
@rpm("python-setproctitle pyOpenSSL python-falcon python-humanize python-markdown pyxattr")
def certidude_setup_authority(username, kerberos_keytab, nginx_config, country, state, locality, organization, organizational_unit, common_name, directory, authority_lifetime, push_server, outbox, server_flags):
openvpn_profile_template_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), "templates", "openvpn-client.conf")
bootstrap_template_path = os.path.join(os.path.dirname(os.path.realpath(__file__)), "templates", "bootstrap.conf")
@ -802,8 +811,7 @@ def certidude_setup_authority(username, kerberos_keytab, nginx_config, country,
else:
click.echo("Warning: /etc/krb5.keytab or /etc/samba/smb.conf not found, Kerberos unconfigured")
working_directory = os.path.realpath(os.path.dirname(__file__))
static_path = os.path.join(os.path.realpath(os.path.dirname(__file__)), "static")
certidude_path = sys.argv[0]
# Push server config generation
@ -812,15 +820,16 @@ def certidude_setup_authority(username, kerberos_keytab, nginx_config, country,
listen = "0.0.0.0"
port = "80"
else:
nginx_client_config.write(env.get_template("nginx.conf").render(vars()))
click.echo("Generated: %s" % nginx_client_config.name)
port = "8080"
nginx_config.write(env.get_template("nginx.conf").render(vars()))
click.echo("Generated: %s" % nginx_config.name)
if not os.path.exists("/etc/nginx/sites-enabled/certidude.conf"):
os.symlink("../sites-available/certidude.conf", "/etc/nginx/sites-enabled/certidude.conf")
click.echo("Symlinked %s -> /etc/nginx/sites-enabled/" % nginx_client_config.name)
click.echo("Symlinked %s -> /etc/nginx/sites-enabled/" % nginx_config.name)
if os.path.exists("/etc/nginx/sites-enabled/default"):
os.unlink("/etc/nginx/sites-enabled/default")
if not push_server:
click.echo("Remember to install nchan instead of regular nginx!")
click.echo("Remember to install nchan capable nginx instead of regular nginx!")
if os.path.exists("/etc/systemd"):
if os.path.exists("/etc/systemd/system/certidude.service"):
@ -974,7 +983,7 @@ def certidude_list(verbose, show_key_type, show_extensions, show_path, show_sign
# e - expired
# y - not valid yet
# r - revoked
from humanize import naturaltime
from certidude import authority
def dump_common(common_name, path, cert):
@ -1076,6 +1085,7 @@ def certidude_cron():
@click.option("-l", "--listen", default="0.0.0.0", help="Listen address")
@click.option("-f", "--fork", default=False, is_flag=True, help="Fork to background")
def certidude_serve(port, listen, fork):
from setproctitle import setproctitle
from certidude.signer import SignServer
from certidude import const
click.echo("Using configuration from: %s" % const.CONFIG_PATH)
@ -1234,6 +1244,7 @@ def certidude_serve(port, listen, fork):
@click.option("-s", "--slot", default="9a", help="Yubikey slot to use, 9a by default")
@click.option("-u", "--username", default=os.getenv("USER"), help="Username to use, %s by default" % os.getenv("USER"))
def certidude_setup_yubikey(authority, slot, username, pin):
import requests
cmd = "ykinfo", "-q", "-s"
click.echo("Executing: %s" % " ".join(cmd))
serial = subprocess.check_output(cmd).strip()

View File

@ -2,6 +2,7 @@
import click
import os
import socket
import sys
CONFIG_DIR = os.path.expanduser("~/.certidude") if os.getuid() else "/etc/certidude"
CONFIG_PATH = os.path.join(CONFIG_DIR, "server.conf")
@ -18,7 +19,11 @@ SIGNER_LOG_PATH = os.path.join(CONFIG_DIR, "signer.log") if os.getuid() else "/v
if os.getenv("TRAVIS"):
FQDN = "buildbot"
else:
try:
FQDN = socket.getaddrinfo(socket.gethostname(), 0, socket.AF_INET, 0, 0, socket.AI_CANONNAME)[0][3]
except socket.gaierror:
click.echo("Failed to resolve fully qualified hostname of this machine, make sure hostname -f works")
sys.exit(254)
if "." in FQDN:
HOSTNAME, DOMAIN = FQDN.split(".", 1)

View File

@ -1,10 +1,11 @@
import falcon
import click
import ipaddress
import json
import logging
import os
import subprocess
import types
from datetime import date, time, datetime, timedelta
from certidude.auth import User
from urlparse import urlparse
logger = logging.getLogger("api")
@ -13,6 +14,7 @@ def csrf_protection(func):
"""
Protect resource from common CSRF attacks by checking user agent and referrer
"""
import falcon
def wrapped(self, req, resp, *args, **kwargs):
# Assume curl and python-requests are used intentionally
if req.user_agent.startswith("curl/") or req.user_agent.startswith("python-requests/"):
@ -40,6 +42,7 @@ def csrf_protection(func):
def event_source(func):
import falcon
def wrapped(self, req, resp, *args, **kwargs):
if req.get_header("Accept") == "text/event-stream":
resp.status = falcon.HTTP_SEE_OTHER
@ -50,6 +53,7 @@ def event_source(func):
class MyEncoder(json.JSONEncoder):
def default(self, obj):
from certidude.auth import User
if isinstance(obj, ipaddress._IPAddressBase):
return str(obj)
if isinstance(obj, set):
@ -72,6 +76,7 @@ def serialize(func):
"""
Falcon response serialization
"""
import falcon
def wrapped(instance, req, resp, **kwargs):
if not req.client_accepts("application/json"):
logger.debug("Client did not accept application/json")
@ -83,3 +88,43 @@ def serialize(func):
resp.body = json.dumps(func(instance, req, resp, **kwargs), cls=MyEncoder)
return wrapped
def apt(packages):
"""
Install packages for Debian and Ubuntu
"""
def wrapper(func):
def wrapped(*args, **kwargs):
if os.path.exists("/usr/bin/apt-get"):
cmd = ["/usr/bin/apt-get", "install", "-yqq"] + packages.split(" ")
click.echo("Running: %s" % " ".join(cmd))
subprocess.call(cmd)
return func(*args, **kwargs)
return wrapped
return wrapper
def rpm(packages):
"""
Install packages for Fedora and CentOS
"""
def wrapper(func):
def wrapped(*args, **kwargs):
if os.path.exists("/usr/bin/dnf"):
cmd = ["/usr/bin/dnf", "install", "-y"] + packages.split(" ")
click.echo("Running: %s" % " ".join(cmd))
subprocess.call(cmd)
return func(*args, **kwargs)
return wrapped
return wrapper
def pip(packages):
def wrapper(func):
def wrapped(*args, **kwargs):
click.echo("Running: pip install %s" % packages)
import pip
pip.main(['install'] + packages.split(" "))
return func(*args, **kwargs)
return wrapped
return wrapper

View File

@ -1,5 +1,4 @@
import falcon
import logging
logger = logging.getLogger("api")
@ -8,6 +7,8 @@ def whitelist_subnets(subnets):
"""
Validate source IP address of API call against subnet list
"""
import falcon
def wrapper(func):
def wrapped(self, req, resp, *args, **kwargs):
# Check for administration subnet whitelist
@ -26,6 +27,8 @@ def whitelist_subnets(subnets):
return wrapper
def whitelist_content_types(*content_types):
import falcon
def wrapper(func):
def wrapped(self, req, resp, *args, **kwargs):
for content_type in content_types:

View File

@ -1,7 +1,6 @@
import click
import os
import requests
import subprocess
import tempfile
from base64 import b64encode
@ -30,6 +29,7 @@ def certidude_request_certificate(server, system_keytab_required, key_path, requ
"""
Exchange CSR for certificate using Certidude HTTP API server
"""
import requests
# Create directories
for path in key_path, request_path, certificate_path, authority_path, revocations_path:

View File

@ -1,13 +1,29 @@
# To set up SSL certificates using Let's Encrypt run:
#
# apt install letsencrypt
# certbot certonly -d {{common_name}} --webroot /var/www/html/
#
# Also uncomment URL rewriting and SSL configuration below
server {
server_name {{ common_name }};
listen 80 default_server;
# rewrite ^ https://$server_name$request_uri? permanent;
#}
#server {
# server_name {{ common_name }};
# listen 443 ssl http2 default_server;
# add_header Strict-Transport-Security "max-age=15768000; includeSubDomains; preload;";
# ssl_certificate /etc/letsencrypt/live/{{common_name}}/fullchain.pem;
# ssl_certificate_key /etc/letsencrypt/live/{{common_name}}/privkey.pem;
error_page 500 502 503 504 /50x.html;
root {{static_path}};
location /api/ {
proxy_pass http://127.0.0.1{% if listen != 80 }:{{ listen }}{% endif %}/api/;
proxy_pass http://127.0.0.1{% if port != 80 %}:{{ port }}{% endif %}/api/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_connect_timeout 600;
@ -16,7 +32,15 @@ server {
send_timeout 600;
}
{% if not push_server %}
# This is for Let's Encrypt
location /.well-known/ {
alias /var/www/html/.well-known/;
}
{% if not push_server %}
# This only works with nchan, for Debian 9 just apt install libnginx-mod-nchan
# For Ubuntu and older Debian releases install nchan from https://nchan.io/
location ~ "^/lp/pub/(.*)" {
allow 127.0.0.1;
nchan_publisher;
@ -40,7 +64,7 @@ server {
nchan_channel_id $1;
nchan_subscriber eventsource;
}
{% endif %}
{% endif %}
}

View File

@ -1,8 +1,6 @@
import click
import grp
import ldap
import ldap.sasl
import os
import pwd
from certidude import const, config
@ -65,6 +63,9 @@ class PosixUserManager(object):
class DirectoryConnection(object):
def __enter__(self):
import ldap
import ldap.sasl
# TODO: Implement simple bind
if not os.path.exists(config.LDAP_GSSAPI_CRED_CACHE):
raise ValueError("Ticket cache at %s not initialized, unable to "
@ -87,8 +88,7 @@ class ActiveDirectoryUserManager(object):
with DirectoryConnection() as conn:
ft = config.LDAP_USER_FILTER % username
attribs = "cn", "givenName", "sn", "mail", "userPrincipalName"
r = conn.search_s(config.LDAP_BASE, ldap.SCOPE_SUBTREE,
ft.encode("utf-8"), attribs)
r = conn.search_s(config.LDAP_BASE, 2, ft.encode("utf-8"), attribs)
for dn, entry in r:
if not dn:
continue
@ -110,8 +110,7 @@ class ActiveDirectoryUserManager(object):
def filter(self, ft):
with DirectoryConnection() as conn:
attribs = "givenName", "surname", "samaccountname", "cn", "mail", "userPrincipalName"
r = conn.search_s(config.LDAP_BASE, ldap.SCOPE_SUBTREE,
ft.encode("utf-8"), attribs)
r = conn.search_s(config.LDAP_BASE, 2, ft.encode("utf-8"), attribs)
for dn,entry in r:
if not dn:
continue
@ -145,8 +144,7 @@ class ActiveDirectoryUserManager(object):
def is_admin(self, user):
with DirectoryConnection() as conn:
ft = config.LDAP_ADMIN_FILTER % user.name
r = conn.search_s(config.LDAP_BASE, ldap.SCOPE_SUBTREE,
ft.encode("utf-8"), ["cn"])
r = conn.search_s(config.LDAP_BASE, 2, ft.encode("utf-8"), ["cn"])
for dn, entry in r:
if not dn:
continue

View File

@ -1,11 +1,7 @@
click==6.7
configparser==3.5.0
cryptography==1.7.2
falcon==1.1.0
humanize==0.5.1
ipaddress==1.0.18
Jinja2==2.9.5
Markdown==2.6.8
pyldap==2.4.28
python-dateutil==2.6.0
requests==2.10.0
click>=6.7
configparser>=3.5.0
cryptography>=1.7.1
Jinja2>=2.8
pyasn1>=0.1.9
requests>=2.12.4
requests-kerberos>=0.7.0

View File

@ -17,15 +17,15 @@ setup(
"certidude.api"
],
long_description=open("README.rst").read(),
# Include here only stuff required to run certidude client
install_requires=[
"setproctitle",
"click",
"falcon",
"jinja2",
"pyopenssl",
"humanize",
"cryptography",
"markupsafe",
"configparser",
"jinja2",
"pyasn1",
"requests",
"requests-kerberos"
],
scripts=[
"misc/certidude"