예제 #1
0
def main():
    global log, pipes

    log = get_logger('server')

    ports = None
    port = get_server_setting('port')
    if isinstance(port, int):
        ports = [port]
    elif isinstance(port, dict):
        ports = list(port.keys())
    local_port = get_server_setting('local_port')
    if local_port in ports:
        sys.exit('Configuration error! Local port {} is also configured as a '
                 'non-local port.'.format(local_port))
    ports.append(local_port)

    prepare_database()

    children = {}

    def sigint_handler(*args):
        for p in children.values():
            try:
                os.kill(p.pid, signal.SIGINT)
            except:
                pass

    with Manager() as manager:
        pipes = manager.dict()
        clean_up_pipes()
        for port in ports:
            p = Process(target=startServer,
                        args=(port, pipes),
                        kwargs={'local_only': port == local_port})
            p.daemon = True
            p.start()
            children[port] = p

        # Make sure the children didn't die on startup, e.g., because they
        # couldn't bind to their ports.
        time.sleep(1)
        problems = False
        for port in children.keys():
            if not children[port].is_alive():
                log.error(
                    'Child process for port {} died on startup. Maybe '
                    'its port is in use?', port)
                problems = True
        if problems:
            sigint_handler()
            log.error('Exiting because one or more servers failed to start up')
            sys.exit(1)

        signal.signal(signal.SIGINT, sigint_handler)
        for p in children.values():
            p.join()
예제 #2
0
def check_password(auth_name):
    # Get list of users who are allowed access
    users = get_server_setting(auth_name + ':users') or []
    if isinstance(users, str):
        users = [users]
    users = {u: None for u in users}

    # Get list of groups who are allowed access
    groups = get_server_setting(auth_name + ':groups') or []
    if isinstance(groups, str):
        groups = [groups]

    # Get users for authorized groups
    for group in groups:
        group_users = get_server_setting('groups:' + group) or []
        if isinstance(group_users, str):
            group_users = [group_users]
        if group_users:
            users.update({u: None for u in group_users})
        else:
            log.warn('check_password: group {} is empty', group)

    # Warn about and remove invalid users
    for user in list(users.keys()):
        users[user] = get_server_setting('users:' + user)
        if not users[user]:
            log.warn('check_password: user {} has no password', user)
            del users[user]

    auth_info = get_server_setting(auth_name + ':passwords') or {}
    users.update(auth_info)
    if not users:
        log.debug('No auth: no passwords configured in {}', auth_name)
        return False

    if not request.authorization:
        log.debug('No auth: no username specified in request')
        return False
    try:
        password_hash = users[request.authorization.username]
    except KeyError:
        # N.B. One does not log usernames from authentication requests, in
        # case the user accidentally typed the password in the username field,
        # so that one doesn't accidentally log passwords.
        log.debug('No auth: invalid username for {}', auth_name)
        return False
    if not pbkdf2_sha256.verify(request.authorization.password, password_hash):
        # It's OK to log the username here, since we've already confirmed that
        # it's a valid username, not a password.
        log.warn('No auth: incorrect password for {} in {}',
                 request.authorization.username, auth_name)
        return False
    log.info('check_password: authenticated {} for {}',
             request.authorization.username, auth_name)
    return True
예제 #3
0
def encrypt_handler(args):
    if not get_server_setting('secret_keeping:enabled'):
        sys.exit('You cannot encrypt when secret-keeping is disabled.')
    db = get_db()
    selectors = get_selectors()
    spec = {'$or': [{s.plain_mongo: {'$exists': True}} for s in selectors]}
    for doc in db.clients.find(spec):
        doc, update = encrypt_document(doc)
        if update:
            db.clients.update({'_id': doc['_id']}, update)
            log.info('Encrypted data in client document {} (host {})',
                     doc['_id'], doc['hostname'])
            print('Encrypted client document {} (host {})'.format(
                doc['_id'], doc['hostname']))
    spec = {'key': {'$in': [s.plain_mongo for s in selectors]}}
    for doc in db.audit_trail.find(spec):
        doc, update = encrypt_document(doc, selectors=audit_trail_selectors)
        if update:
            update['$set']['key'] = next(s.enc_mongo for s in selectors
                                         if s.plain_mongo == doc['key'])
            db.audit_trail.update({'_id': doc['_id']}, update)
            log.info('Encrypted data in audit trail document {} (host {})',
                     doc['_id'], doc['hostname'])
            print('Encrypted audit trail document {} (host {})'.format(
                doc['_id'], doc['hostname']))
예제 #4
0
def deselect_handler(args):
    selectors = get_server_setting(selectors_setting)
    if isinstance(selectors, str):
        selectors = [selectors]
        set_server_setting(selectors_setting, selectors)
    if selectors is None:
        selectors = []
        set_server_setting(selectors_setting, selectors)

    errors = False
    for selector in args.selector:
        try:
            selectors.remove(selector)
        except ValueError:
            sys.stderr.write(
                'Selector {} is not currently selected.\n'.format(selector))
            errors = True
    if errors:
        sys.exit(1)

    for selector in args.selector:
        log.info('Removing secret-keeping selector {}', selector)

    save_server_settings()

    set_client_setting(selectors_setting, selectors)
    save_client_settings()

    print(restart_note)
예제 #5
0
def check_ip_auth(auth_name):
    auth_info = get_server_setting(auth_name + ':ipranges')
    if not auth_info:
        log.debug('No auth: no IP ranges configured in {}', auth_name)
        return False
    try:
        remote_addr = IPv4Address(request.remote_addr)
    except:
        try:
            remote_addr = IPv6Address(request.remote_addr)
        except:
            log.debug("No auth: can't parse IP address {}",
                      request.remote_addr)
            return False
    for range_string in auth_info:
        try:
            ip_range = IPv4Network(range_string)
        except:
            try:
                ip_range = IPv6Network(range_string)
            except:
                raise Exception('Invalid address range {} in {}'.format(
                    range_string, auth_name))
        if remote_addr in ip_range:
            log.debug('Auth success: {} is in {}', remote_addr, ip_range)
            return True
    log.debug('No auth: no matching IP ranges for {} in {}', remote_addr,
              auth_name)
예제 #6
0
def ports_iter():
    port = get_server_setting('port')
    if isinstance(port, int):
        yield port
        return
    for p in port:
        yield p
예제 #7
0
def no_auth_needed(auth_name, mandatory=False):
    auth_info = get_server_setting(auth_name)
    if not auth_info:
        if mandatory:
            raise Exception('No authentication information configured in {}'.
                            format(auth_name))
        log.debug('Request permitted: authentication is not mandatory')
        return True
    return False
예제 #8
0
def combine_secret_key():
    key_name = get_server_setting('secret_keeping:key_name')
    split_dir = os.path.join(var_dir, key_name)
    key_file = os.path.join(split_dir, 'private_key.asc')
    os.makedirs(split_dir, exist_ok=True)
    split_files = [f for f in os.listdir(split_dir) if re.search(r'\.\d', f)]
    combine_threshold = get_server_setting('secret_keeping:combine_threshold')
    if len(split_files) < combine_threshold:
        input('Put at least {} of the secret-keeper files into\n{}.\n'
              'Hit Enter when done: '.format(combine_threshold, split_dir))
        split_files = [
            f for f in os.listdir(split_dir) if re.search(r'\.\d', f)
        ]
        if len(split_files) < combine_threshold:
            sys.exit('Too few secret-keeper files provided.')
    cmd = ['gfcombine', '-o', key_file]
    cmd.extend(os.path.join(split_dir, f) for f in split_files)
    subprocess.check_output(cmd, stderr=subprocess.STDOUT)
    gpg_command('--import', key_file)
    subprocess.check_output(('shred', '-u', key_file))
예제 #9
0
def disable_handler(args):
    if not get_server_setting('secret_keeping:enabled'):
        sys.exit('Secret-keeping is not enabled.')

    set_server_setting('secret_keeping:enabled', False)
    save_server_settings()

    set_client_setting('secret_keeping:enabled', False)
    save_client_settings()

    print(restart_note)

    log.info('Disabled secret-keeping')
예제 #10
0
def remove_port(args):
    port = args.port
    ports = get_server_setting('port')
    if not ports:
        sys.exit('There are no ports to remove!')
    if isinstance(ports, int) or len(ports) == 1:
        sys.exit("You can't remove the only configured port!")
    if port not in ports:
        sys.exit('Port {} is not currently configured'.format(port))
    if isinstance(ports, list):
        ports.remove(port)
    else:
        ports.pop(port)
    set_server_setting('port', ports)
    save_server_settings()
    print('Removed port {}.'.format(port))
    show_configuration(args)
예제 #11
0
def select_handler(args):
    selectors = get_server_setting(selectors_setting)
    if isinstance(selectors, str):
        selectors = [selectors]
        set_server_setting(selectors_setting, selectors)
    if selectors is None:
        selectors = []
        set_server_setting(selectors_setting, selectors)

    errors = False
    for selector in args.selector:
        if selector in selectors:
            sys.stderr.write(
                'Selector {} is already added.\n'.format(selector))
            errors = True
    if errors:
        sys.exit(1)

    if not args.force:
        db = get_db()
        for selector in args.selector:
            if not db.clients.find_one({selector: {
                    '$exists': True
            }},
                                       projection=[]):
                sys.stderr.write(
                    'Selector {} does not match anything.\n'
                    'Specify --force to save anyway.\n'.format(selector))
                errors = True
        if errors:
            sys.exit(1)

    for selector in args.selector:
        log.info('Adding secret-keeping selector {}', selector)

    selectors.extend(args.selector)
    save_server_settings()

    set_client_setting(selectors_setting, selectors)
    save_client_settings()

    print(restart_note)
예제 #12
0
def main(args):
    if args.yes:
        maybe_changed = partial(maybe_changed_extended, use_default=True)
    else:
        maybe_changed = maybe_changed_extended

    generate_key('server', gpg_user_ids['server'])
    generate_key('client', gpg_user_ids['client'])
    import_key('server', gpg_user_ids['client'])
    import_key('client', gpg_user_ids['server'])

    default = not (get_client_setting('loaded')
                   and get_server_setting('loaded'))

    do_config = default if args.yes else \
        get_bool('Do you want to configure things interactively?', default)

    server_changed = client_changed = False

    if do_config:
        if isinstance(get_server_setting('port'), int):
            # Otherwise, the settings file has been edited to make the port
            # either a list of ports or a mapping, and we don't want to try to
            # configure it here.
            server_changed |= maybe_changed(
                'server', 'port', get_int,
                'What port should the server listen on?')

        server_changed |= maybe_changed(
            'server', 'local_port', get_int,
            'What local-only port should the server use?')

        configure_ssl = True
        port = get_server_setting('port')
        if isinstance(port, dict):
            for port_number, port_settings in port.items():
                if 'ssl' in port_settings:
                    # If there are already port-specific SSL settings, then
                    # don't try to configure SSL in this script.
                    configure_ssl = False
        if configure_ssl:
            default = bool(
                get_server_setting('ssl:certificate', None)
                or get_server_setting('ssl:key', None))
            configure_ssl = maybe_get_bool(
                'Do you want the server to use SSL?', default, args.yes)
        if not configure_ssl:
            if get_server_setting('ssl:certificate', None):
                set_server_setting('ssl:certificate', None)
                server_changed = True
            if get_server_setting('ssl:key', None):
                set_server_setting('ssl:key', None)
                server_changed = True
        else:
            while True:
                server_changed |= maybe_changed('server', 'ssl:certificate',
                                                get_string,
                                                'SSL certificate file path:')
                if os.path.exists(get_server_setting('ssl:certificate')):
                    break
                print('That file does not exist.')

            while True:
                server_changed |= maybe_changed('server', 'ssl:key',
                                                get_string,
                                                'SSL key file path:')
                if os.path.exists(get_server_setting('ssl:key')):
                    break
                print('That file does not exist.')

        server_changed |= maybe_changed('server', 'database:host',
                                        get_string_or_list,
                                        'Database host:port:')
        if get_server_setting('database:host'):
            server_changed |= maybe_changed('server',
                                            'database:replicaset',
                                            get_string_none,
                                            'Replicaset name:',
                                            empty_ok=True)
        server_changed |= maybe_changed('server', 'database:name', get_string,
                                        'Database name:')
        server_changed |= maybe_changed('server',
                                        'database:username',
                                        get_string_none,
                                        'Database username:'******'database:username'):
            server_changed |= maybe_changed('server', 'database:password',
                                            get_string, 'Database password:'******'Server', maybe_changed)
        server_changed |= maybe_changed(
            'server', 'audit_cron:enabled', get_bool,
            'Do you want to enable the audit cron job?')

        if get_server_setting('audit_cron:enabled'):
            server_changed |= maybe_changed(
                'server', 'audit_cron:email', get_string,
                'What email address should get the audit output?')

        port = get_server_setting('port')
        if port == 443:
            sample_url = 'https://hostname'
        elif port == 80:
            sample_url = 'http://hostname'
        else:
            sample_url = 'http://hostname:{}'.format(port)
        prompt = 'URL base, e.g., {}, for clients to reach server:'.format(
            sample_url)

        client_changed |= maybe_changed('client', 'server_url', get_string,
                                        prompt)

        client_changed |= maybe_changed('client',
                                        'geolocation_api_key',
                                        get_string,
                                        'Google geolocation API key, if any:',
                                        empty_ok=True)
        prompter = partial(get_int, minimum=1)
        client_changed |= maybe_changed(
            'client', 'schedule:collect_interval', prompter,
            'How often (minutes) do you want to collect data?')
        client_changed |= maybe_changed(
            'client', 'schedule:submit_interval', prompter,
            'How often (minutes) do you want re-try submits?')

        client_changed |= configure_logging('Client', maybe_changed)

        save_server_settings()
        if server_changed:
            print('Saved server settings.')

        save_client_settings()
        if client_changed:
            print('Saved client settings.')

    service_file = '/etc/systemd/system/penguindome-server.service'
    service_exists = os.path.exists(service_file)
    default = not service_exists

    if service_exists:
        prompt = ("Do you want to replace the server's systemd "
                  "configuration?")
    else:
        prompt = 'Do you want to add the server to systemd?'

    do_service = maybe_get_bool(prompt, default, args.yes)

    if do_service and not args.skipsystemctl:
        with NamedTemporaryFile('w+') as temp_service_file:
            temp_service_file.write(
                dedent('''\
                [Unit]
                Description=PenguinDome Server
                After=network.target

                [Service]
                Type=simple
                ExecStart={server_exe}

                [Install]
                WantedBy=multi-user.target
            '''.format(server_exe=os.path.join(top_dir, 'bin', 'server'))))
            temp_service_file.flush()
            os.chmod(temp_service_file.name, 0o644)
            shutil.copy(temp_service_file.name, service_file)
        subprocess.check_output(('systemctl', 'daemon-reload'),
                                stderr=subprocess.STDOUT)
        service_exists = True

    if service_exists and not args.skipsystemctl:
        try:
            subprocess.check_output(
                ('systemctl', 'is-enabled', 'penguindome-server'),
                stderr=subprocess.STDOUT)
        except Exception:
            if maybe_get_bool('Do you want to enable the server?', True,
                              args.yes):
                subprocess.check_output(
                    ('systemctl', 'enable', 'penguindome-server'),
                    stderr=subprocess.STDOUT)
                is_enabled = True
            else:
                is_enabled = False
        else:
            is_enabled = True

        if is_enabled:
            try:
                subprocess.check_output(
                    ('systemctl', 'status', 'penguindome-server'),
                    stderr=subprocess.STDOUT)
            except Exception:
                if maybe_get_bool('Do you want to start the server?', True,
                                  args.yes):
                    subprocess.check_output(
                        ('systemctl', 'start', 'penguindome-server'),
                        stderr=subprocess.STDOUT)
            else:
                if maybe_get_bool('Do you want to restart the server?',
                                  server_changed, args.yes):
                    subprocess.check_output(
                        ('systemctl', 'restart', 'penguindome-server'),
                        stderr=subprocess.STDOUT)

        if get_server_setting('audit_cron:enabled'):
            cron_file = '/etc/cron.d/penguindome-audit'
            cron_exists = os.path.exists(cron_file)

            if cron_exists:
                prompt = 'Do you want to replace the audit crontab?'
            else:
                prompt = 'Do you want to install the audit crontab?'

            do_crontab = maybe_get_bool(prompt, args.audit_crontab
                                        or not cron_exists, args.audit_crontab
                                        or args.yes)

            if do_crontab:
                email = get_server_setting('audit_cron:email')
                minute = int(random.random() * 60)
                minute2 = (minute + 1) % 60

                crontab = dedent('''\
                    MAILTO={email}
                    {minute2} * * * * root "{top_dir}/bin/issues" audit --cron
                '''.format(minute2=minute2, email=email, top_dir=top_dir))

                with NamedTemporaryFile('w+') as temp_cron_file:
                    temp_cron_file.write(crontab)
                    temp_cron_file.flush()
                    os.chmod(temp_cron_file.name, 0o644)
                    shutil.copy(temp_cron_file.name, cron_file)

                print('Installed {}'.format(cron_file))

    if (client_changed
            or not glob.glob(os.path.join(releases_dir, '*.tar.asc'))
            and not args.skipbuildrelease):
        if client_changed:
            prompt = ('Do you want to build a release with the new client '
                      'settings?')
        else:
            prompt = 'Do you want to build a client release?'

        if maybe_get_bool(prompt, True, args.yes):
            # Sometimes sign fails the first time because of GnuPG weirdness.
            # The client_release script will call sign as well, but we call it
            # first just in case it fails the first time.
            try:
                subprocess.check_output((os.path.join('bin', 'sign'), ),
                                        stderr=subprocess.STDOUT)
            except Exception:
                pass
            subprocess.check_output((os.path.join('bin', 'client_release'), ),
                                    stderr=subprocess.STDOUT)

    print('Done!')
예제 #13
0
def delete_secret_key():
    key_fingerprint = get_server_setting('secret_keeping:key_fingerprint')
    gpg_command('--delete-secret-keys', key_fingerprint)
    print("\nDon't forget to 'shred -u' the secret-keeper files!\n")
    log.warn("Don't forget to 'shred -u' the secret-keeper files!")
예제 #14
0
def enable_handler(args):
    if get_server_setting('secret_keeping:enabled'):
        sys.exit('Secret-keeping is already enabled.')

    if get_server_setting('secret_keeping:key_id'):
        if not (args.replace or args.preserve):
            sys.exit('Must specify --replace or --preserve.')
    else:
        args.replace = True

    args.shares = args.shares or \
        get_server_setting('secret_keeping:num_shares')
    if args.shares < 2:
        sys.exit('--num-shares must be at least 2.')

    args.combine_threshold = args.combine_threshold or \
        get_server_setting('secret_keeping:combine_threshold')
    if args.combine_threshold < 2:
        sys.exit('--combine-threshold must be at least 2.')
    if args.combine_threshold > args.shares:
        sys.exit(
            '--combine-threshold must be less than {}.'.format(args.shares +
                                                               1))

    if args.replace:
        key_name = 'penguindome-secret-keeping-' + uuid.uuid4().hex
        output = gpg_command('--passphrase',
                             '',
                             '--quick-gen-key',
                             key_name,
                             with_trustdb=True,
                             quiet=False)
        match = re.search(r'key (.*) marked as ultimately trusted', output)
        key_id = match.group(1)
        match = re.search(r'/([0-9A-F]+)\.rev', output)
        key_fingerprint = match.group(1)

        split_dir = os.path.join(var_dir, key_name)
        key_file = os.path.join(split_dir, 'private_key.asc')
        os.makedirs(split_dir)
        gpg_command('--export-secret-key', '--armor', '-o', key_file, key_id)
        subprocess.check_output(('gfsplit', '-n', str(
            args.combine_threshold), '-m', str(args.shares), key_file),
                                stderr=subprocess.STDOUT)
        try:
            gpg_command('--delete-secret-keys', key_fingerprint)
        except subprocess.CalledProcessError as e:
            sys.exit('Failed to delete secret key:\n{}'.format(
                e.output.decode('utf8')))
        subprocess.check_output(('shred', '-u', key_file),
                                stderr=subprocess.STDOUT)

        with NamedTemporaryFile() as public_key_file:
            gpg_command('--export', '-o', public_key_file.name, key_id)
            set_gpg('client')
            try:
                gpg_command('--import', public_key_file.name)
            finally:
                set_gpg('server')

        set_server_setting('secret_keeping:key_name', key_name)
        set_server_setting('secret_keeping:key_id', key_id)
        set_server_setting('secret_keeping:key_fingerprint', key_fingerprint)

        set_client_setting('secret_keeping:key_id', key_id)

    set_server_setting('secret_keeping:num_shares', args.shares)
    set_server_setting('secret_keeping:combine_threshold',
                       args.combine_threshold)
    set_server_setting('secret_keeping:enabled', True)
    save_server_settings()

    set_client_setting('secret_keeping:enabled', True)
    save_client_settings()

    print(
        distribute_secrets_note.format(m=args.shares,
                                       n=args.combine_threshold,
                                       split_dir=split_dir))
    print(restart_note)

    log.info('Enabled secret-keeping')
예제 #15
0
def show_handler(args):
    config = get_server_setting('secret_keeping') or {}
    config['enabled'] = get_server_setting('secret_keeping:enabled')
    pprint.pprint(config)
예제 #16
0
def configure_port(args, add=False):
    if args.ssl_self_signed and (args.certificate or args.key):
        sys.exit('--certificate and --key are incompatible with '
                 '--ssl-self-signed.')

    changed = False
    port = args.port
    ports = get_server_setting('port', None)
    if not ports:
        ports = []
    elif isinstance(ports, int):
        ports = [ports]
    if isinstance(ports, list):
        ports = {port: {} for port in ports}
        set_server_setting('port', ports)

    if port in ports:
        if add:
            sys.exit('Port {} is already present.'.format(args.port))
        which = 'Configured'
    else:
        ports[port] = {}
        changed = True
        which = 'Added'

    if not ports[port]:
        ports[port] = {}
    port_settings = ports[port]
    gps = partial(get_port_setting, port)

    def ss(setting, value):
        nonlocal changed
        set_setting(port_settings, setting, value)
        changed = True

    if args.deprecated is not None:
        if bool(gps('deprecated')) != args.deprecated:
            ss('deprecated', args.deprecated)

    if args.ssl_self_signed:
        cert_file, key_file = make_self_signed_cert(args.ssl_self_signed)
        ss('ssl:certificate', cert_file)
        ss('ssl:key', key_file)
        if args.ssl is not False:
            args.ssl = True

    if args.certificate and gps('ssl:certificate') != args.certificate:
        if not (args.key or gps('ssl:key')):
            sys.exit('You must specify both a certificate and a key.')
        if not os.path.exists(args.certificate):
            sys.exit('The certificate file {} does not exist'.format(
                args.certificate))
        ss('ssl:certificate', args.certificate)

    if args.key and gps('ssl:key') != args.key:
        if not gps('ssl:certificate'):
            sys.exit('You must specify both a certificate and a key.')
        if not os.path.exists(args.key):
            sys.exit('The key file {} does not exist'.format(
                args.key))
        ss('ssl:key', args.key)

    if args.ssl:
        if not (gps('ssl:certificate') and gps('ssl:key')):
            sys.exit('You must specify a certificate and key to enable SSL.')
        if not gps('ssl:enabled', bool(gps('ssl:certificate'))):
            ss('ssl:enabled', True)
    elif args.ssl is False:
        if gps('ssl:enabled', bool(gps('ssl:certificate'))):
            ss('ssl:enabled', False)

    if changed:
        save_server_settings()

        url = get_client_setting('server_url')
        if url:
            url = urlparse(url)
            if ':' in url[1]:
                _, client_port = url[1].split(':')
                client_port = int(client_port)
            else:
                client_port = {'http': 80, 'https': 443}[url[0]]
            client_ssl = {'http': False, 'https': True}[url[0]]
            if port == client_port:
                if client_ssl != gps(
                        'ssl:enabled', bool(gps('ssl:certificate'))):
                    print('\n'
                          'WARNING: Client is configured to use port {} and {}'
                          'using SSL.\n'
                          '         Should it be?\n'.format(
                              port, '' if client_ssl else 'not '))
                if gps('deprecated'):
                    print('\n'
                          'WARNING: Client is configured to use deprecated '
                          'port {}.\n'
                          '         Do you need to change the client port?'.
                          format(port))

        print('{} port {}.'.format(which, port))

        print("\n"
              "WARNING: Don't forget to restart the server.\n")

        if args.ssl_self_signed and args.ssl:
            print("\n"
                  "WARNING: Don't forget to configure client CA file\n"
                  "         (see help for 'configure-client').\n")

        show_configuration(args)
    else:
        print('No changes.')