From d2a259b88785f02636e9d99d19412e1160ccf6b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lauri=20V=C3=B5sandi?= Date: Tue, 29 Mar 2016 22:03:27 +0300 Subject: [PATCH] Merge authority setup and production setup --- README.rst | 188 +++++------------------------ certidude/cli.py | 185 ++++++++++++++-------------- certidude/templates/certidude.conf | 2 +- certidude/templates/nginx.conf | 74 +++++------- certidude/templates/uwsgi.ini | 2 +- 5 files changed, 154 insertions(+), 297 deletions(-) diff --git a/README.rst b/README.rst index 1bbd3b0..7274a26 100644 --- a/README.rst +++ b/README.rst @@ -74,23 +74,14 @@ To install Certidude: .. code:: bash - apt-get install -y python python-pip python-dev cython python-configparser \ + apt-get install -y python python-pip python-dev cython \ + python-cffi python-configparser \ python-pysqlite2 python-mysql.connector python-ldap \ build-essential libffi-dev libssl-dev libkrb5-dev \ ldap-utils krb5-user \ libsasl2-modules-gssapi-mit pip install certidude -Make sure you're running PyOpenSSL 0.15+ from PyPI, -not the outdated one provided by APT. - -Create a system user for ``certidude``: - -.. code:: bash - - adduser --system --no-create-home --group certidude - mkdir /etc/certidude - Setting up authority -------------------- @@ -103,27 +94,46 @@ You can check it with: hostname -f -The command should return ca.example.com +The command should return ``ca.example.com``. -Certidude can set up certificate authority relatively easily, -following will set up certificate authority in /var/lib/certidude/hostname.domain.tld: +If necessary tweak machine's fully qualified hostname in ``/etc/hosts``: + +.. code:: + + 127.0.0.1 localhost + 127.0.1.1 ca.example.com ca + +Then proceed to install `nchan `_ and ``uwsgi``: + +.. code:: bash + + wget https://nchan.slact.net/download/nginx-common.deb https://nchan.slact.net/download/nginx-extras.deb + dpkg -i nginx-common.deb nginx-extras.deb + apt-get install nginx uwsgi uwsgi-plugin-python + +Certidude can set up certificate authority relatively easily. +Following will set up certificate authority in ``/var/lib/certidude/hostname.domain.tld``, +configure uWSGI in ``/etc/uwsgi/apps-available/certidude.ini``, +nginx in ``/etc/nginx/sites-available/certidude.conf``, +cronjobs in ``/etc/cron.hourly/certidude`` and much more: .. code:: bash certidude setup authority -Tweak the configuration in /etc/certidude/server.conf until you meet your requirements and +Tweak the configuration in ``/etc/certidude/server.conf`` until you meet your requirements and spawn the signer process: .. code:: bash certidude signer spawn -Finally serve the certificate authority via web: +Finally restart services: .. code:: bash - certidude serve + service nginx restart + service uwsgi restart Certificate management @@ -148,137 +158,6 @@ Use web interface or following to sign a certificate on server: certidude sign client-hostname-or-common-name -Production deployment ---------------------- - -Install ``nginx`` and ``uwsgi``: - -.. code:: bash - - apt-get install nginx uwsgi uwsgi-plugin-python - -For easy setup following is reccommended: - -.. code:: bash - - certidude setup production - -Otherwise manually configure ``uwsgi`` application in ``/etc/uwsgi/apps-available/certidude.ini``: - -.. code:: ini - - [uwsgi] - master = true - processes = 1 - vaccum = true - uid = certidude - gid = certidude - plugins = python - chdir = /tmp - module = certidude.wsgi - callable = app - chmod-socket = 660 - chown-socket = certidude:www-data - buffer-size = 32768 - env = LANG=C.UTF-8 - env = LC_ALL=C.UTF-8 - env = KRB5_KTNAME=/etc/certidude/server.keytab - env = KRB5CCNAME=/run/certidude/krb5cc - -Also enable the application: - -.. code:: bash - - ln -s ../apps-available/certidude.ini /etc/uwsgi/apps-enabled/certidude.ini - -We support `nchan `_, -configure the site in /etc/nginx/sites-available/certidude: - -.. code:: - - upstream certidude_api { - server unix:///run/uwsgi/app/certidude/socket; - } - - server { - server_name localhost; - listen 80 default_server; - listen [::]:80 default_server ipv6only=on; - root /usr/local/lib/python2.7/dist-packages/certidude/static; - - location /api/ { - include uwsgi_params; - uwsgi_pass certidude_api; - } - - # Add following three if you wish to enable push server on this machine - location /pub { - allow 127.0.0.1; - nchan_publisher http; - nchan_store_messages off; - nchan_channel_id $arg_id; - } - - location ~ "^/lp/(.*)" { - nchan_subscriber longpoll; - nchan_channel_id $1; - } - - location ~ "^/ev/(.*)" { - nchan_subscriber eventsource; - nchan_channel_id $1; - } - } - -Enable the site: - -.. code:: bash - - ln -s ../sites-available/certidude /etc/nginx/sites-enabled/certidude - -Also adjust ``/etc/nginx/nginx.conf``: - -.. code:: - - user www-data; - worker_processes 4; - pid /run/nginx.pid; - - events { - worker_connections 768; - } - - http { - sendfile on; - tcp_nopush on; - tcp_nodelay on; - keepalive_timeout 65; - types_hash_max_size 2048; - include /etc/nginx/mime.types; - default_type application/octet-stream; - access_log /var/log/nginx/access.log; - error_log /var/log/nginx/error.log; - gzip on; - gzip_disable "msie6"; - include /etc/nginx/conf.d/*; - include /etc/nginx/sites-enabled/*; - } - -In your Certidude server's /etc/certidude/server.conf make sure Certidude -is aware of your nginx setup: - -.. code:: - - push_server = http://push.example.com/ - -Restart the services: - -.. code:: bash - - service uwsgi restart - service nginx restart - - Setting up Active Directory authentication ------------------------------------------ @@ -291,13 +170,6 @@ Install dependencies: apt-get install samba-common-bin krb5-user ldap-utils -Make sure Certidude machine's fully qualified hostname is correct in ``/etc/hosts``: - -.. code:: - - 127.0.0.1 localhost - 127.0.1.1 ca.example.lan ca - Reset Samba client configuration in ``/etc/samba/smb.conf``: .. code:: ini @@ -306,7 +178,7 @@ Reset Samba client configuration in ``/etc/samba/smb.conf``: security = ads netbios name = CA workgroup = EXAMPLE - realm = EXAMPLE.LAN + realm = EXAMPLE.COM kerberos method = system keytab Reset Kerberos configuration in ``/etc/krb5.conf``: @@ -314,7 +186,7 @@ Reset Kerberos configuration in ``/etc/krb5.conf``: .. code:: ini [libdefaults] - default_realm = EXAMPLE.LAN + default_realm = EXAMPLE.COM dns_lookup_realm = true dns_lookup_kdc = true @@ -417,7 +289,7 @@ To run from source tree: .. code:: bash - PYTHONPATH=. KRB5_KTNAME=/etc/certidude/server.keytab LANG=C.UTF-8 python misc/certidude + PYTHONPATH=. KRB5CCNAME=/run/certidude/krb5cc KRB5_KTNAME=/etc/certidude/server.keytab LANG=C.UTF-8 python misc/certidude To install the package from the source: diff --git a/certidude/cli.py b/certidude/cli.py index a02c63b..b81bd9b 100755 --- a/certidude/cli.py +++ b/certidude/cli.py @@ -236,10 +236,9 @@ def certidude_request_spawn(fork): os.unlink(pid_path) -@click.command("spawn", help="Run privilege isolated signer process") -@click.option("-k", "--kill", default=False, is_flag=True, help="Kill previous instance") +@click.command("spawn", help="Restart privilege isolated signer process") @click.option("-n", "--no-interaction", default=True, is_flag=True, help="Don't load password protected keys") -def certidude_signer_spawn(kill, no_interaction): +def certidude_signer_spawn(no_interaction): """ Spawn privilege isolated signer process """ @@ -284,15 +283,14 @@ def certidude_signer_spawn(kill, no_interaction): pid = 0 if pid > 0: - if kill: - try: - click.echo("Killing %d" % pid) - os.kill(pid, signal.SIGTERM) - sleep(1) - os.kill(pid, signal.SIGKILL) - sleep(1) - except EnvironmentError: - pass + try: + click.echo("Killing %d" % pid) + os.kill(pid, signal.SIGTERM) + sleep(1) + os.kill(pid, signal.SIGKILL) + sleep(1) + except EnvironmentError: + pass child_pid = os.fork() @@ -693,71 +691,19 @@ def certidude_setup_openvpn_networkmanager(server, email_address, common_name, o services.set(endpoint, "service", "network-manager/openvpn") services.write(open("/etc/certidude/services.conf", "w")) -@click.command("production", help="Set up nginx, uwsgi and cron") + +@click.command("authority", help="Set up Certificate Authority in a directory") @click.option("--username", default="certidude", help="Service user account, created if necessary, 'certidude' by default") -@click.option("--hostname", default=HOSTNAME, help="nginx hostname, '%s' by default" % HOSTNAME) -@click.option("--static-path", default=os.path.join(os.path.dirname(__file__), "static"), help="Static files") -@click.option("--kerberos-keytab", default="/etc/certidude/server.keytab", help="Specify Kerberos keytab") -@click.option("--push-server", default=None, help="Push server URL") +@click.option("--static-path", default=os.path.join(os.path.dirname(__file__), "static"), help="Path to Certidude's static JS/CSS/etc") +@click.option("--kerberos-keytab", default="/etc/certidude/server.keytab", help="Kerberos keytab for using 'kerberos' authentication backend, /etc/certidude/server.keytab by default") @click.option("--nginx-config", "-n", - default="/etc/nginx/nginx.conf", + default="/etc/nginx/sites-available/certidude.conf", type=click.File(mode="w", atomic=True, lazy=True), - help="nginx configuration, /etc/nginx/nginx.conf by default") + help="nginx site config for serving Certidude, /etc/nginx/sites-available/certidude by default") @click.option("--uwsgi-config", "-u", default="/etc/uwsgi/apps-available/certidude.ini", type=click.File(mode="w", atomic=True, lazy=True), - help="uwsgi configuration, /etc/uwsgi/ by default") -def certidude_setup_production(username, hostname, push_server, nginx_config, uwsgi_config, static_path, kerberos_keytab): - try: - pwd.getpwnam(username) - click.echo("Username '%s' already exists, excellent!" % username) - except KeyError: - cmd = "adduser", "--system", "--no-create-home", "--group", username - subprocess.check_call(cmd) - - if subprocess.call("net ads testjoin", shell=True): - click.echo("Domain membership check failed, 'net ads testjoin' returned non-zero value", err=True) - exit(255) - - if not os.path.exists(kerberos_keytab): - subprocess.call("KRB5_KTNAME=FILE:" + kerberos_keytab + " net ads keytab add HTTP -P") - click.echo("Created service principal in Kerberos keytab '%s'" % kerberos_keytab) - - if os.path.exists("/etc/krb5.keytab") and os.path.exists("/etc/samba/smb.conf"): - # Fetch Kerberos ticket for system account - cp = ConfigParser() - cp.read("/etc/samba/smb.conf") - domain = cp.get("global", "realm").lower() - base = ",".join(["dc=" + j for j in domain.split(".")]) - with open("/etc/cron.hourly/certidude", "w") as fh: - fh.write("#!/bin/bash\n") - fh.write("KRB5CCNAME=/run/certidude/krb5cc-new kinit -k %s$\n" % cp.get("global", "netbios name")) - fh.write("chown certidude /run/certidude/krb5cc-new\n") - fh.write("mv /run/certidude/krb5cc-new /run/certidude/krb5cc\n") - os.chmod("/etc/cron.hourly/certidude", 0o755) - click.echo("Created /etc/cron.hourly/certidude for automatic Kerberos TGT renewal") - else: - click.echo("Warning: cronjob for Kerberos ticket renewal not created, LDAP with GSSAPI will not be available!") - - - if not static_path.endswith("/"): - static_path += "/" - - nginx_config.write(env.get_template("nginx.conf").render(vars())) - click.echo("Generated: %s" % nginx_config.name) - uwsgi_config.write(env.get_template("uwsgi.ini").render(vars())) - click.echo("Generated: %s" % uwsgi_config.name) - - if os.path.exists("/etc/uwsgi/apps-enabled/certidude.ini"): - os.unlink("/etc/uwsgi/apps-enabled/certidude.ini") - os.symlink(uwsgi_config.name, "/etc/uwsgi/apps-enabled/certidude.ini") - click.echo("Symlinked %s -> /etc/uwsgi/apps-enabled/certidude.ini" % uwsgi_config.name) - - if not push_server: - click.echo("Remember to install nchan instead of regular nginx!") - - -@click.command("authority", help="Set up Certificate Authority in a directory") + help="uwsgi configuration for serving Certidude API, /etc/uwsgi/apps-available/certidude.ini by default") @click.option("--parent", "-p", help="Parent CA, none by default") @click.option("--common-name", "-cn", default=FQDN, help="Common name, fully qualified hostname by default") @click.option("--country", "-c", default=None, help="Country, none by default") @@ -775,7 +721,70 @@ def certidude_setup_production(username, hostname, push_server, nginx_config, uw @click.option("--directory", default=os.path.join("/var/lib/certidude", FQDN), help="Directory for authority files, /var/lib/certidude/%s/ by default" % FQDN) @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" % constants.DOMAIN, help="SMTP server, smtp://smtp.%s by default" % constants.DOMAIN) -def certidude_setup_authority(parent, country, state, locality, organization, organizational_unit, common_name, directory, certificate_lifetime, authority_lifetime, revocation_list_lifetime, revoked_url, certificate_url, push_server, email_address, outbox, server_flags): +def certidude_setup_authority(username, static_path, kerberos_keytab, nginx_config, uwsgi_config, parent, country, state, locality, organization, organizational_unit, common_name, directory, certificate_lifetime, authority_lifetime, revocation_list_lifetime, revoked_url, certificate_url, push_server, email_address, outbox, server_flags): + + # Expand variables + ca_key = os.path.join(directory, "ca_key.pem") + ca_crt = os.path.join(directory, "ca_crt.pem") + if not static_path.endswith("/"): + static_path += "/" + certidude_conf = os.path.join("/etc/certidude/server.conf") + + try: + pwd.getpwnam("certidude") + except KeyError: + cmd = "adduser", "--system", "--no-create-home", "--group", "certidude" + if subprocess.call(cmd): + click.echo("Failed to create system user 'certidude'") + return 255 + + if not os.path.exists("/etc/certidude"): + click.echo("Creating /etc/certidude") + os.makedirs("/etc/certidude") + + if os.path.exists(kerberos_keytab): + click.echo("Service principal keytab found in '%s'" % kerberos_keytab) + else: + click.echo("To use 'kerberos' authentication backend create service principal with:") + click.echo() + click.echo(" KRB5_KTNAME=FILE:%s net ads keytab add HTTP -P" % kerberos_keytab) + click.echo(" chown %s %s" % (username, kerberos_keytab)) + click.echo() + + if os.path.exists("/etc/krb5.keytab") and os.path.exists("/etc/samba/smb.conf"): + + # Fetch Kerberos ticket for system account + cp = ConfigParser() + cp.read("/etc/samba/smb.conf") + domain = cp.get("global", "realm").lower() + base = ",".join(["dc=" + j for j in domain.split(".")]) + with open("/etc/cron.hourly/certidude", "w") as fh: + fh.write("#!/bin/bash\n") + fh.write("KRB5CCNAME=/run/certidude/krb5cc-new kinit -k %s$\n" % cp.get("global", "netbios name")) + fh.write("chown certidude /run/certidude/krb5cc-new\n") + fh.write("mv /run/certidude/krb5cc-new /run/certidude/krb5cc\n") + os.chmod("/etc/cron.hourly/certidude", 0o755) + click.echo("Created /etc/cron.hourly/certidude for automatic Kerberos TGT renewal") + else: + click.echo("Warning: /etc/krb5.keytab or /etc/samba/smb.conf not found, Kerberos unconfigured") + + nginx_config.write(env.get_template("nginx.conf").render(vars())) + click.echo("Generated: %s" % nginx_config.name) + uwsgi_config.write(env.get_template("uwsgi.ini").render(vars())) + click.echo("Generated: %s" % uwsgi_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_config.name) + if not os.path.exists("/etc/uwsgi/apps-enabled/certidude.ini"): + os.symlink("../apps-available/certidude.ini", "/etc/uwsgi/apps-enabled/certidude.ini") + click.echo("Symlinked %s -> /etc/uwsgi/apps-enabled/" % uwsgi_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!") from cryptography import x509 from cryptography.x509.oid import NameOID, ExtendedKeyUsageOID @@ -783,16 +792,19 @@ def certidude_setup_authority(parent, country, state, locality, organization, or from cryptography.hazmat.primitives import hashes, serialization from cryptography.hazmat.primitives.asymmetric import rsa - # Make sure common_name is valid - if not re.match(r"^[\.\-_a-zA-Z0-9]+$", common_name): - raise click.ClickException("CA name can contain only alphanumeric, '_' and '-' characters") + _, _, uid, gid, gecos, root, shell = pwd.getpwnam("certidude") + os.setgid(gid) + + if os.path.exists(certidude_conf): + click.echo("Configuration file %s already exists, remove to regenerate" % certidude_conf) + else: + os.umask(0o137) + with open(certidude_conf, "w") as fh: + fh.write(env.get_template("certidude.conf").render(vars())) + click.echo("Generated %s" % certidude_conf) if os.path.lexists(directory): - raise click.ClickException("Output directory {} already exists.".format(directory)) - - certidude_conf = os.path.join("/etc/certidude/server.conf") - if os.path.exists(certidude_conf): - raise click.ClickException("Configuration file %s already exists" % certidude_conf) + raise click.ClickException("CA directory %s already exists, remove to regenerate" % directory) click.echo("CA configuration files are saved to: {}".format(directory)) @@ -809,11 +821,6 @@ def certidude_setup_authority(parent, country, state, locality, organization, or if not certificate_url: certificate_url = "http://%s/api/certificate/" % common_name - # File paths - ca_key = os.path.join(directory, "ca_key.pem") - ca_crt = os.path.join(directory, "ca_crt.pem") - - subject = issuer = x509.Name([ x509.NameAttribute(o, value) for o, value in ( (NameOID.COUNTRY_NAME, country), @@ -867,9 +874,6 @@ def certidude_setup_authority(parent, country, state, locality, organization, or click.echo("Signing %s..." % cert.subject) - _, _, uid, gid, gecos, root, shell = pwd.getpwnam("certidude") - os.setgid(gid) - # Create authority directory with 750 permissions os.umask(0o027) if not os.path.exists(directory): @@ -883,8 +887,6 @@ def certidude_setup_authority(parent, country, state, locality, organization, or # Set permission bits to 640 os.umask(0o137) - with open(certidude_conf, "w") as fh: - fh.write(env.get_template("certidude.conf").render(vars())) with open(ca_crt, "wb") as fh: fh.write(cert.public_bytes(serialization.Encoding.PEM)) @@ -906,7 +908,7 @@ def certidude_setup_authority(parent, country, state, locality, organization, or click.echo() click.echo("Use following to launch privilege isolated signer processes:") click.echo() - click.echo(" certidude signer spawn -k") + click.echo(" certidude signer spawn") click.echo() click.echo("Use following command to serve CA read-only:") click.echo() @@ -1157,7 +1159,6 @@ certidude_setup.add_command(certidude_setup_authority) certidude_setup.add_command(certidude_setup_openvpn) certidude_setup.add_command(certidude_setup_strongswan) certidude_setup.add_command(certidude_setup_client) -certidude_setup.add_command(certidude_setup_production) certidude_setup.add_command(certidude_setup_nginx) certidude_request.add_command(certidude_request_spawn) certidude_signer.add_command(certidude_signer_spawn) diff --git a/certidude/templates/certidude.conf b/certidude/templates/certidude.conf index 595871f..95ae843 100644 --- a/certidude/templates/certidude.conf +++ b/certidude/templates/certidude.conf @@ -32,7 +32,7 @@ posix admin group = sudo ;backend = ldap ldap computer filter = (&(objectclass=user)(objectclass=computer)(samaccountname=%s)) ldap user filter = (&(objectclass=user)(objectclass=person)(samaccountname=%s)) -ldap admin filter = (&(memberOf=cn=Domain Admins,cn=Users,dc=example,dc=com)(samaccountname=%s)) +ldap admin filter = (&(memberOf=cn=Domain Admins,cn=Users,{% if base %}{{ base }}{% else %}dc=example,dc=com{% endif %})(samaccountname=%s)) # Users are allowed to log in from user subnets user subnets = 0.0.0.0/0 diff --git a/certidude/templates/nginx.conf b/certidude/templates/nginx.conf index 1c7e1c0..86df244 100644 --- a/certidude/templates/nginx.conf +++ b/certidude/templates/nginx.conf @@ -1,54 +1,38 @@ -user www-data; -worker_processes 4; -pid /run/nginx.pid; -events { - worker_connections 1024; +upstream certidude_api { + server unix:///run/uwsgi/app/certidude/socket; } -http { - include mime.types; - default_type application/octet-stream; - sendfile on; - keepalive_timeout 65; - gzip on; +server { + server_name {{ common_name }}; + listen 80 default_server; + error_page 500 502 503 504 /50x.html; - upstream certidude_api { - server unix:///run/uwsgi/app/certidude/socket; + root {{static_path}}; + + location /api/ { + include uwsgi_params; + uwsgi_pass certidude_api; } - server { - server_name {{hostname}}; # TODO: FQDN, SSL - listen 80 default_server; - listen [::]:80 default_server ipv6only=on; - error_page 500 502 503 504 /50x.html; - - root {{static_path}}; - - location /api/ { - include uwsgi_params; - uwsgi_pass certidude_api; - } - - {% if not push_server %} - location /pub { - allow 127.0.0.1; - nchan_publisher http; - nchan_store_messages off; - nchan_channel_id $arg_id; - } - - location ~ "^/lp/(.*)" { - nchan_subscriber longpoll; - nchan_channel_id $1; - } - - location ~ "^/ev/(.*)" { - nchan_subscriber eventsource; - nchan_channel_id $1; - } - {% endif %} - + {% if not push_server %} + location /pub { + allow 127.0.0.1; + nchan_publisher http; + nchan_store_messages off; + nchan_channel_id $arg_id; } + + location ~ "^/lp/(.*)" { + nchan_subscriber longpoll; + nchan_channel_id $1; + } + + location ~ "^/ev/(.*)" { + nchan_subscriber eventsource; + nchan_channel_id $1; + } + {% endif %} + } diff --git a/certidude/templates/uwsgi.ini b/certidude/templates/uwsgi.ini index 25e6737..8d25093 100644 --- a/certidude/templates/uwsgi.ini +++ b/certidude/templates/uwsgi.ini @@ -1,5 +1,5 @@ [uwsgi] -exec-as-root = /usr/local/bin/certidude signer spawn -k +exec-as-root = /usr/local/bin/certidude signer spawn master = true processes = 1 vacuum = true