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()
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
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']))
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)
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)
def ports_iter(): port = get_server_setting('port') if isinstance(port, int): yield port return for p in port: yield p
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
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))
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')
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)
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)
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!')
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!")
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')
def show_handler(args): config = get_server_setting('secret_keeping') or {} config['enabled'] = get_server_setting('secret_keeping:enabled') pprint.pprint(config)
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.')