#!/usr/bin/env python # $Id$ # # Copyright (C) 2015-2016 Parsons Government Services ("PARSONS") # Portions copyright (C) 2014 Dragon Research Labs ("DRL") # Portions copyright (C) 2013 Internet Systems Consortium ("ISC") # # Permission to use, copy, modify, and distribute this software for any # purpose with or without fee is hereby granted, provided that the above # copyright notices and this permission notice appear in all copies. # # THE SOFTWARE IS PROVIDED "AS IS" AND PARSONS, DRL, AND ISC DISCLAIM # ALL WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED # WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL # PARSONS, DRL, OR ISC BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR # CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS # OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, # NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION # WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. import os import sys import socket import urllib2 import argparse import platform import textwrap import subprocess import rpki.autoconf fqdn = socket.getfqdn() vhost_template = """\ # # Stuff that should be visible with both HTTP and HTTPS is (now) # outside the vhost block (see if this works properly...). # # # Allow access to the directory where rcynic-html writes # its output files. # %(allow)s # # Add alias pointing to rcynic-html's output files. # # If for some reason you need to change this, be careful to leave # the trailing slash off the URL, otherwise /rcynic will be # swallowed by the WSGIScriptAlias # Alias /rcynic %(RCYNIC_HTML_DIR)s/ # # Allow access to the directory where pubd writes RRDP files. # %(allow)s # # Add alias pointing to pubd's RRD output files. # Alias /rrdp %(datarootdir)s/rpki/rrdp-publication/ # # RRDP "notification" file needs a short expiration: this is # a critical part of how RRDP interacts with HTTP caching. # Timeout is per current RRDP I-D, this will need to track # any changes as the specification evolves. # ExpiresActive on ExpiresDefault "access plus 1 minute" # # By default, this configuration assumes that you use name-based # virtual hosting. If that's not what you want, you may need # to change this. # # # By default, we enable an HTTPS virtual host on this machine's # fully qualified domain name. This works for simple # configurations, but if you're running a more complex Apache # configuration or want to run the GUI on a different hostname, # you may need to change this. # ServerName %(fqdn)s # # Configure the WSGI application to run as a separate process from # the Apache daemon itself. # %(WSGI_DAEMON_PROCESS)s %(WSGI_PROCESS_GROUP)s # # Allow access to our WSGI directory. # %(allow)s # # Define the URL to the RPKI GUI # WSGIScriptAlias / %(datarootdir)s/rpki/wsgi/rpki.wsgi # # Allow access to static content (icons, etc). # %(allow)s # # Add the aliases Django expects for static content. # Alias /media/ %(datarootdir)s/rpki/media/ Alias /site_media/ %(datarootdir)s/rpki/media/ # # Redirect to the GUI dashboard when someone hits the bare vhost. # RedirectMatch ^/$ /rpki/ # # Enable HTTPS # SSLEngine on # # Specify HTTPS server certificate and key files for this virtual host. # This should suffice for simple configurations, but if you're running # a more complex Apache configuration you may need to change or remove # these lines. # SSLCertificateFile %(sysconfdir)s/rpki/apache.cer SSLCertificateKeyFile %(sysconfdir)s/rpki/apache.key # # Recommended settings based on # https://wiki.mozilla.org/Security/Server_Side_TLS # (Currently using the Intermediate cipher suite) # SSLProtocol all -SSLv2 -SSLv3 SSLCipherSuite ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-AES256-GCM-SHA384:DHE-RSA-AES128-GCM-SHA256:DHE-DSS-AES128-GCM-SHA256:kEDH+AESGCM:ECDHE-RSA-AES128-SHA256:ECDHE-ECDSA-AES128-SHA256:ECDHE-RSA-AES128-SHA:ECDHE-ECDSA-AES128-SHA:ECDHE-RSA-AES256-SHA384:ECDHE-ECDSA-AES256-SHA384:ECDHE-RSA-AES256-SHA:ECDHE-ECDSA-AES256-SHA:DHE-RSA-AES128-SHA256:DHE-RSA-AES128-SHA:DHE-DSS-AES128-SHA256:DHE-RSA-AES256-SHA256:DHE-DSS-AES256-SHA:DHE-RSA-AES256-SHA:AES128-GCM-SHA256:AES256-GCM-SHA384:AES128-SHA256:AES256-SHA256:AES128-SHA:AES256-SHA:AES:CAMELLIA:DES-CBC3-SHA:!aNULL:!eNULL:!EXPORT:!DES:!RC4:!MD5:!PSK:!aECDH:!EDH-DSS-DES-CBC3-SHA:!EDH-RSA-DES-CBC3-SHA:!KRB5-DES-CBC3-SHA SSLHonorCipherOrder on SSLCompression off # OCSP Stapling, only in httpd 2.3.3 and later #SSLUseStapling on #SSLStaplingResponderTimeout 5 #SSLStaplingReturnResponderErrors off # On Apache 2.4+, SSLStaplingCache must be set *outside* of the VirtualHost #SSLStaplingCache shmcb:/var/run/ocsp(128000) # Enable this if your want HSTS (recommended) # Header add Strict-Transport-Security "max-age=15768000" # # Take pity on users running Internet Exploder # BrowserMatch "MSIE [2-6]" ssl-unclean-shutdown nokeepalive downgrade-1.0 force-response-1.0 BrowserMatch "MSIE [17-9]" ssl-unclean-shutdown """ allow_22_template = ''' Order deny,allow Allow from all\ ''' allow_24_template = ''' Require all granted\ ''' name_virtual_host_template = '''\ # # In most cases we want to use name-based virtual hosting. If this causes # problems with your existing Apache configuration, try commenting out this line. # NameVirtualHost *:443 ''' def Guess(args): """ Guess what platform this is and dispatch to platform constructor. """ system = platform.system() if system == "FreeBSD": return FreeBSD(args) if system == "Darwin": return Darwin(args) if system == "Linux": distro = platform.linux_distribution()[0].lower() if distro == "debian": return Debian(args) if distro == "ubuntu": return Ubuntu(args) if distro in ("fedora", "centos"): return Redhat(args) raise NotImplementedError("Can't guess what platform this is, sorry") class Platform(object): """ Abstract base class representing an operating system platform. """ apache_cer = os.path.join(rpki.autoconf.sysconfdir, "rpki", "apache.cer") apache_key = os.path.join(rpki.autoconf.sysconfdir, "rpki", "apache.key") apache_conf = os.path.join(rpki.autoconf.sysconfdir, "rpki", "apache.conf") apache_conf_sample = apache_conf + ".sample" apache_conf_preface = "" def __init__(self, args): self.args = args self.log("RPKI Apache configuration: platform \"%s\", action \"%s\"" % ( self.__class__.__name__, args.action)) getattr(self, args.action)() def log(self, msg): if self.args.verbose: print msg def run(self, *cmd, **kwargs): self.log("Running %s" % " ".join(cmd)) subprocess.check_call(cmd, **kwargs) req_cmd = ("openssl", "req", "-new", "-config", "/dev/stdin", "-out", "/dev/stdout", "-keyout", apache_key, "-newkey", "rsa:2048") x509_cmd = ("openssl", "x509", "-req", "-sha256", "-signkey", apache_key, "-in", "/dev/stdin", "-out", apache_cer, "-days", "3650") req_conf = '''\ [req] default_bits = 2048 default_md = sha256 distinguished_name = req_dn prompt = no encrypt_key = no [req_dn] CN = %s ''' % fqdn def unlink(self, fn, silent = False): if os.path.lexists(fn): if not silent: self.log("Removing %s" % fn) os.unlink(fn) elif not silent: self.log("Would have removed %s if it existed" % fn) def del_certs(self, silent = False): self.unlink(self.apache_cer, silent) self.unlink(self.apache_key, silent) def add_certs(self): if os.path.exists(self.apache_cer) and os.path.exists(self.apache_key): return self.del_certs() req = subprocess.Popen(self.req_cmd, stdin = subprocess.PIPE, stdout = subprocess.PIPE, stderr = open("/dev/null", "w")) x509 = subprocess.Popen(self.x509_cmd, stdin = req.stdout, stderr = open("/dev/null", "w")) req.stdin.write(self.req_conf) req.stdin.close() if req.wait(): raise subprocess.CalledProcessError(req.returncode, self.req_cmd) if x509.wait(): raise subprocess.CalledProcessError(x509.returncode, self.x509_cmd) self.log("Created %s and %s, chmoding %s" % ( self.apache_cer, self.apache_key, self.apache_key)) os.chmod(self.apache_key, 0600) _vhost = None @property def vhost(self): if self._vhost is None: allow = allow_22_template if self.args.apache_version <= 22 else allow_24_template self._vhost = vhost_template % dict(rpki.autoconf.__dict__, fqdn = fqdn, allow = allow) return self._vhost @property def name_virtual_host(self): return name_virtual_host_template if self.args.apache_version <= 22 else "" @property def too_complex(self): return textwrap.dedent('''\ # It looks like you already have HTTPS enabled in your # Apache configuration, which makes your configuration too # complex for us to enable support for the RPKI GUI automatically. # # To enable support, take a look at %s # and copy what you need from that file into %s, # paying attention to the comments which mark the bits that # you might (or might not) need to change or omit, depending # on the details of your particular Apache configuration. ''' % (self.apache_conf_sample, self.apache_conf)) @property def apache_conf_target(self): raise NotImplementedError def restart(self): raise NotImplementedError def install(self): with open(self.apache_conf_sample, "w") as f: self.log("Writing %s" % f.name) f.write(self.apache_conf_preface) f.write(self.name_virtual_host) f.write(self.vhost) if not os.path.exists(self.apache_conf): self.unlink(self.apache_conf) with open(self.apache_conf, "w") as f: self.log("Writing %s" % f.name) if self.test_url("https://%s/" % fqdn): f.write(self.too_complex) sys.stdout.write(self.too_complex) else: if not self.test_tcp("localhost", 443): f.write(self.apache_conf_preface) f.write(self.name_virtual_host) f.write(self.vhost) if not os.path.exists(self.apache_conf_target): self.unlink(self.apache_conf_target) self.log("Symlinking %s to %s" % ( self.apache_conf_target, self.apache_conf)) os.symlink(self.apache_conf, self.apache_conf_target) self.add_certs() self.enable() self.restart() def enable(self): pass def disable(self): pass def remove(self): try: same = open(self.apache_conf, "r").read() == open(self.apache_conf_sample, "r").read() except: same = False self.unlink(self.apache_conf_sample) if same: self.unlink(self.apache_conf) self.unlink(self.apache_conf_target) self.disable() self.restart() def purge(self): self.remove() self.unlink(self.apache_conf) self.del_certs() @staticmethod def test_url(url = "https://localhost/"): try: urllib2.urlopen(url).close() except IOError: return False else: return True @staticmethod def test_tcp(host = "localhost", port = 443, family = socket.AF_UNSPEC, proto = socket.SOCK_STREAM): try: addrinfo = socket.getaddrinfo(host, port, family, proto) except socket.error: return False for af, socktype, proto, canon, sa in addrinfo: # pylint: disable=W0612 try: s = socket.socket(af, socktype, proto) s.connect(sa) s.close() except socket.error: continue else: return True return False class FreeBSD(Platform): """ FreeBSD. """ # On FreeBSD we have to ask httpd what version it is before we know # where to put files or what to call the service. In FreeBSD's makefiles, # this value is called APACHE_VERSION, and is calculated thusly: # # httpd -V | sed -ne 's/^Server version: Apache\/\([0-9]\)\.\([0-9]*\).*/\1\2/p' _apache_name = None @property def apache_name(self): if self._apache_name is None: self._apache_name = "apache%s" % self.args.apache_version return self._apache_name @property def apache_conf_target(self): return "/usr/local/etc/%s/Includes/rpki.conf" % self.apache_name apache_conf_preface = textwrap.dedent('''\ # These directives tell Apache to listen on the HTTPS port # and to enable name-based virtual hosting. If you already # have HTTPS enabled elsewhere in your configuration, you may # need to remove these. Listen [::]:443 Listen 0.0.0.0:443 ''') def restart(self): self.run("service", self.apache_name, "restart") class Debian(Platform): """ Debian and related platforms like Ubuntu. """ # Pull the current version number for released code. Use # something very large when there is no version (eg, "sid"). @property def distribution_version(self): v = platform.linux_distribution()[1].split(".") if all(d.isdigit() for d in v): return tuple(int(d) for d in v) else: return (99999999, 0) # On Debian, the filename must end in .conf on Stretch and must not # end in .conf on Wheezy. Haven't checked Jessie yet, will need to # update this if we ever sort out the version skew mess on Jessie. @property def apache_conf_target(self): if self.distribution_version < (8, 0): return "/etc/apache2/sites-available/rpki" else: return "/etc/apache2/sites-available/rpki.conf" snake_oil_cer = "/etc/ssl/certs/ssl-cert-snakeoil.pem" snake_oil_key = "/etc/ssl/private/ssl-cert-snakeoil.key" def add_certs(self): if not os.path.exists(self.snake_oil_cer) or not os.path.exists(self.snake_oil_key): return Platform.add_certs(self) if not os.path.exists(self.apache_cer): self.unlink(self.apache_cer) os.symlink(self.snake_oil_cer, self.apache_cer) if not os.path.exists(self.apache_key): self.unlink(self.apache_key) os.symlink(self.snake_oil_key, self.apache_key) def enable(self): self.run("a2enmod", "ssl") self.run("a2enmod", "expires") self.run("a2ensite", "rpki") # # In light of BREACH and CRIME attacks, mod_deflate is looking # like a bad idea, so make sure it's off. self.run("a2dismod", "-f", "deflate") def disable(self): self.run("a2dissite", "rpki") def restart(self): self.run("service", "apache2", "restart") class Ubuntu(Debian): # On Ubuntu, the filename must end in .conf on Trusty and must not # end in .conf on Precise. @property def apache_conf_target(self): if self.distribution_version < (14, 0): return "/etc/apache2/sites-available/rpki" else: return "/etc/apache2/sites-available/rpki.conf" class NIY(Platform): # pylint: disable=W0223 def __init__(self, args): super(NIY, self).__init__(args) raise NotImplementedError("Platform %s not implemented yet, sorry" % self.__class__.__name__) class Redhat(NIY): # pylint: disable=W0223 "Redhat family of Linux distributions (Fedora, CentOS)." class Darwin(NIY): # pylint: disable=W0223 "Mac OS X (aka Darwin)." def main(): """ Generate and (de)install configuration suitable for using Apache httpd to drive the RPKI web interface under WSGI. """ parser = argparse.ArgumentParser(description = __doc__) group1 = parser.add_mutually_exclusive_group() group2 = parser.add_mutually_exclusive_group() parser.add_argument("-v", "--verbose", help = "whistle while you work", action = "store_true") parser.add_argument("--apache-version", help = "Apache version (default " + rpki.autoconf.APACHE_VERSION + ")", type = int, default = rpki.autoconf.APACHE_VERSION) group1.add_argument("--freebsd", help = "configure for FreeBSD", action = "store_const", dest = "platform", const = FreeBSD) group1.add_argument("--debian", help = "configure for Debian", action = "store_const", dest = "platform", const = Debian) group1.add_argument("--ubuntu", help = "configure for Ubuntu", action = "store_const", dest = "platform", const = Ubuntu) group1.add_argument("--redhat", "--fedora", "--centos", help = "configure for Redhat/Fedora/CentOS", action = "store_const", dest = "platform", const = Redhat) group1.add_argument("--macosx", "--darwin", help = "configure for Mac OS X (Darwin)", action = "store_const", dest = "platform", const = Darwin) group1.add_argument("--guess", help = "guess which platform configuration to use", action = "store_const", dest = "platform", const = Guess) group2.add_argument("-i", "--install", help = "install configuration", action = "store_const", dest = "action", const = "install") group2.add_argument("-r", "--remove", "--deinstall", "--uninstall", help = "remove configuration", action = "store_const", dest = "action", const = "remove") group2.add_argument("-P", "--purge", help = "remove configuration with extreme prejudice", action = "store_const", dest = "action", const = "purge") parser.set_defaults(platform = Guess, action = "install") args = parser.parse_args() try: args.platform(args) except Exception, e: sys.exit(str(e)) if __name__ == "__main__": main()