diff --git a/roles/README b/roles/README index e31411af2e..bef320bd27 100644 --- a/roles/README +++ b/roles/README @@ -32,6 +32,7 @@ To re-import/update the OpenShift Ansible roles: openshift_repos os_env_extras os_env_extras_node + os_firewall pods ) diff --git a/roles/os_firewall/README.md b/roles/os_firewall/README.md new file mode 100644 index 0000000000..187d74b06a --- /dev/null +++ b/roles/os_firewall/README.md @@ -0,0 +1,66 @@ +OS Firewall +=========== + +OS Firewall manages firewalld and iptables firewall settings for a minimal use +case (Adding/Removing rules based on protocol and port number). + +Requirements +------------ + +None. + +Role Variables +-------------- + +| Name | Default | | +|---------------------------|---------|----------------------------------------| +| os_firewall_use_firewalld | True | If false, use iptables | +| os_firewall_allow | [] | List of service,port mappings to allow | +| os_firewall_deny | [] | List of service, port mappings to deny | + +Dependencies +------------ + +None. + +Example Playbook +---------------- + +Use iptables and open tcp ports 80 and 443: +``` +--- +- hosts: servers + vars: + os_firewall_use_firewalld: false + os_firewall_allow: + - service: httpd + port: 80/tcp + - service: https + port: 443/tcp + roles: + - os_firewall +``` + +Use firewalld and open tcp port 443 and close previously open tcp port 80: +``` +--- +- hosts: servers + vars: + os_firewall_allow: + - service: https + port: 443/tcp + os_firewall_deny: + - service: httpd + port: 80/tcp + roles: + - os_firewall +``` + +License +------- + +Apache License, Version 2.0 + +Author Information +------------------ +Jason DeTiberus - jdetiber@redhat.com diff --git a/roles/os_firewall/defaults/main.yml b/roles/os_firewall/defaults/main.yml new file mode 100644 index 0000000000..bcf1d9a345 --- /dev/null +++ b/roles/os_firewall/defaults/main.yml @@ -0,0 +1,2 @@ +--- +os_firewall_use_firewalld: True diff --git a/roles/os_firewall/library/os_firewall_manage_iptables.py b/roles/os_firewall/library/os_firewall_manage_iptables.py new file mode 100755 index 0000000000..1cb539a8c1 --- /dev/null +++ b/roles/os_firewall/library/os_firewall_manage_iptables.py @@ -0,0 +1,273 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +# vim: expandtab:tabstop=4:shiftwidth=4 +# pylint: disable=fixme, missing-docstring +from subprocess import call, check_output + +DOCUMENTATION = ''' +--- +module: os_firewall_manage_iptables +short_description: This module manages iptables rules for a given chain +author: Jason DeTiberus +requirements: [ ] +''' +EXAMPLES = ''' +''' + + +class IpTablesError(Exception): + def __init__(self, msg, cmd, exit_code, output): + super(IpTablesError, self).__init__(msg) + self.msg = msg + self.cmd = cmd + self.exit_code = exit_code + self.output = output + + +class IpTablesAddRuleError(IpTablesError): + pass + + +class IpTablesRemoveRuleError(IpTablesError): + pass + + +class IpTablesSaveError(IpTablesError): + pass + + +class IpTablesCreateChainError(IpTablesError): + def __init__(self, chain, msg, cmd, exit_code, output): # pylint: disable=too-many-arguments, line-too-long + super(IpTablesCreateChainError, self).__init__(msg, cmd, exit_code, + output) + self.chain = chain + + +class IpTablesCreateJumpRuleError(IpTablesError): + def __init__(self, chain, msg, cmd, exit_code, output): # pylint: disable=too-many-arguments, line-too-long + super(IpTablesCreateJumpRuleError, self).__init__(msg, cmd, exit_code, + output) + self.chain = chain + + +# TODO: impliment rollbacks for any events that where successful and an +# exception was thrown later. for example, when the chain is created +# successfully, but the add/remove rule fails. +class IpTablesManager(object): # pylint: disable=too-many-instance-attributes + def __init__(self, module): + self.module = module + self.ip_version = module.params['ip_version'] + self.check_mode = module.check_mode + self.chain = module.params['chain'] + self.create_jump_rule = module.params['create_jump_rule'] + self.jump_rule_chain = module.params['jump_rule_chain'] + self.cmd = self.gen_cmd() + self.save_cmd = self.gen_save_cmd() + self.output = [] + self.changed = False + + def save(self): + try: + self.output.append(check_output(self.save_cmd, + stderr=subprocess.STDOUT)) + except subprocess.CalledProcessError as ex: + raise IpTablesSaveError( + msg="Failed to save iptables rules", + cmd=ex.cmd, exit_code=ex.returncode, output=ex.output) + + def verify_chain(self): + if not self.chain_exists(): + self.create_chain() + if self.create_jump_rule and not self.jump_rule_exists(): + self.create_jump() + + def add_rule(self, port, proto): + rule = self.gen_rule(port, proto) + if not self.rule_exists(rule): + self.verify_chain() + + if self.check_mode: + self.changed = True + self.output.append("Create rule for %s %s" % (proto, port)) + else: + cmd = self.cmd + ['-A'] + rule + try: + self.output.append(check_output(cmd)) + self.changed = True + self.save() + except subprocess.CalledProcessError as ex: + raise IpTablesCreateChainError( + chain=self.chain, + msg="Failed to create rule for " + "%s %s" % (proto, port), + cmd=ex.cmd, exit_code=ex.returncode, + output=ex.output) + + def remove_rule(self, port, proto): + rule = self.gen_rule(port, proto) + if self.rule_exists(rule): + if self.check_mode: + self.changed = True + self.output.append("Remove rule for %s %s" % (proto, port)) + else: + cmd = self.cmd + ['-D'] + rule + try: + self.output.append(check_output(cmd)) + self.changed = True + self.save() + except subprocess.CalledProcessError as ex: + raise IpTablesRemoveRuleError( + chain=self.chain, + msg="Failed to remove rule for %s %s" % (proto, port), + cmd=ex.cmd, exit_code=ex.returncode, output=ex.output) + + def rule_exists(self, rule): + check_cmd = self.cmd + ['-C'] + rule + return True if call(check_cmd) == 0 else False + + def gen_rule(self, port, proto): + return [self.chain, '-p', proto, '-m', 'state', '--state', 'NEW', + '-m', proto, '--dport', str(port), '-j', 'ACCEPT'] + + def create_jump(self): + if self.check_mode: + self.changed = True + self.output.append("Create jump rule for chain %s" % self.chain) + else: + try: + cmd = self.cmd + ['-L', self.jump_rule_chain, '--line-numbers'] + output = check_output(cmd, stderr=subprocess.STDOUT) + + # break the input rules into rows and columns + input_rules = [s.split() for s in output.split('\n')] + + # Find the last numbered rule + last_rule_num = None + last_rule_target = None + for rule in input_rules[:-1]: + if rule: + try: + last_rule_num = int(rule[0]) + except ValueError: + continue + last_rule_target = rule[1] + + # Naively assume that if the last row is a REJECT rule, then + # we can add insert our rule right before it, otherwise we + # assume that we can just append the rule. + if (last_rule_num and last_rule_target + and last_rule_target == 'REJECT'): + # insert rule + cmd = self.cmd + ['-I', self.jump_rule_chain, + str(last_rule_num)] + else: + # append rule + cmd = self.cmd + ['-A', self.jump_rule_chain] + cmd += ['-j', self.chain] + output = check_output(cmd, stderr=subprocess.STDOUT) + self.changed = True + self.output.append(output) + self.save() + except subprocess.CalledProcessError as ex: + if '--line-numbers' in ex.cmd: + raise IpTablesCreateJumpRuleError( + chain=self.chain, + msg=("Failed to query existing " + + self.jump_rule_chain + + " rules to determine jump rule location"), + cmd=ex.cmd, exit_code=ex.returncode, + output=ex.output) + else: + raise IpTablesCreateJumpRuleError( + chain=self.chain, + msg=("Failed to create jump rule for chain " + + self.chain), + cmd=ex.cmd, exit_code=ex.returncode, + output=ex.output) + + def create_chain(self): + if self.check_mode: + self.changed = True + self.output.append("Create chain %s" % self.chain) + else: + try: + cmd = self.cmd + ['-N', self.chain] + self.output.append(check_output(cmd, + stderr=subprocess.STDOUT)) + self.changed = True + self.output.append("Successfully created chain %s" % + self.chain) + self.save() + except subprocess.CalledProcessError as ex: + raise IpTablesCreateChainError( + chain=self.chain, + msg="Failed to create chain: %s" % self.chain, + cmd=ex.cmd, exit_code=ex.returncode, output=ex.output + ) + + def jump_rule_exists(self): + cmd = self.cmd + ['-C', self.jump_rule_chain, '-j', self.chain] + return True if call(cmd) == 0 else False + + def chain_exists(self): + cmd = self.cmd + ['-L', self.chain] + return True if call(cmd) == 0 else False + + def gen_cmd(self): + cmd = 'iptables' if self.ip_version == 'ipv4' else 'ip6tables' + return ["/usr/sbin/%s" % cmd] + + def gen_save_cmd(self): # pylint: disable=no-self-use + return ['/usr/libexec/iptables/iptables.init', 'save'] + + +def main(): + module = AnsibleModule( + argument_spec=dict( + name=dict(required=True), + action=dict(required=True, choices=['add', 'remove', + 'verify_chain']), + chain=dict(required=False, default='OS_FIREWALL_ALLOW'), + create_jump_rule=dict(required=False, type='bool', default=True), + jump_rule_chain=dict(required=False, default='INPUT'), + protocol=dict(required=False, choices=['tcp', 'udp']), + port=dict(required=False, type='int'), + ip_version=dict(required=False, default='ipv4', + choices=['ipv4', 'ipv6']), + ), + supports_check_mode=True + ) + + action = module.params['action'] + protocol = module.params['protocol'] + port = module.params['port'] + + if action in ['add', 'remove']: + if not protocol: + error = "protocol is required when action is %s" % action + module.fail_json(msg=error) + if not port: + error = "port is required when action is %s" % action + module.fail_json(msg=error) + + iptables_manager = IpTablesManager(module) + + try: + if action == 'add': + iptables_manager.add_rule(port, protocol) + elif action == 'remove': + iptables_manager.remove_rule(port, protocol) + elif action == 'verify_chain': + iptables_manager.verify_chain() + except IpTablesError as ex: + module.fail_json(msg=ex.msg) + + return module.exit_json(changed=iptables_manager.changed, + output=iptables_manager.output) + + +# pylint: disable=redefined-builtin, unused-wildcard-import, wildcard-import +# import module snippets +from ansible.module_utils.basic import * +if __name__ == '__main__': + main() diff --git a/roles/os_firewall/meta/main.yml b/roles/os_firewall/meta/main.yml new file mode 100644 index 0000000000..8592371e86 --- /dev/null +++ b/roles/os_firewall/meta/main.yml @@ -0,0 +1,14 @@ +--- +galaxy_info: + author: Jason DeTiberus + description: os_firewall + company: Red Hat, Inc. + license: Apache License, Version 2.0 + min_ansible_version: 1.7 + platforms: + - name: EL + versions: + - 7 + categories: + - system +dependencies: [] diff --git a/roles/os_firewall/tasks/firewall/firewalld.yml b/roles/os_firewall/tasks/firewall/firewalld.yml new file mode 100644 index 0000000000..5089eb3e07 --- /dev/null +++ b/roles/os_firewall/tasks/firewall/firewalld.yml @@ -0,0 +1,81 @@ +--- +- name: Install firewalld packages + yum: + name: firewalld + state: present + register: install_result + +- name: Check if iptables-services is installed + command: rpm -q iptables-services + register: pkg_check + failed_when: pkg_check.rc > 1 + changed_when: no + +- name: Ensure iptables services are not enabled + service: + name: "{{ item }}" + state: stopped + enabled: no + with_items: + - iptables + - ip6tables + when: pkg_check.rc == 0 + +- name: Reload systemd units + command: systemctl daemon-reload + when: install_result | changed + +- name: Start and enable firewalld service + service: + name: firewalld + state: started + enabled: yes + register: result + +- name: need to pause here, otherwise the firewalld service starting can sometimes cause ssh to fail + pause: seconds=10 + when: result | changed + +- name: Mask iptables services + command: systemctl mask "{{ item }}" + register: result + changed_when: "'iptables' in result.stdout" + with_items: + - iptables + - ip6tables + when: pkg_check.rc == 0 + ignore_errors: yes + +# TODO: Ansible 1.9 will eliminate the need for separate firewalld tasks for +# enabling rules and making them permanent with the immediate flag +- name: Add firewalld allow rules + firewalld: + port: "{{ item.port }}" + permanent: false + state: enabled + with_items: os_firewall_allow + when: os_firewall_allow is defined + +- name: Persist firewalld allow rules + firewalld: + port: "{{ item.port }}" + permanent: true + state: enabled + with_items: os_firewall_allow + when: os_firewall_allow is defined + +- name: Remove firewalld allow rules + firewalld: + port: "{{ item.port }}" + permanent: false + state: disabled + with_items: os_firewall_deny + when: os_firewall_deny is defined + +- name: Persist removal of firewalld allow rules + firewalld: + port: "{{ item.port }}" + permanent: true + state: disabled + with_items: os_firewall_deny + when: os_firewall_deny is defined diff --git a/roles/os_firewall/tasks/firewall/iptables.yml b/roles/os_firewall/tasks/firewall/iptables.yml new file mode 100644 index 0000000000..9af9d8d29e --- /dev/null +++ b/roles/os_firewall/tasks/firewall/iptables.yml @@ -0,0 +1,63 @@ +--- +- name: Install iptables packages + yum: + name: "{{ item }}" + state: present + with_items: + - iptables + - iptables-services + register: install_result + +- name: Check if firewalld is installed + command: rpm -q firewalld + register: pkg_check + failed_when: pkg_check.rc > 1 + changed_when: no + +- name: Ensure firewalld service is not enabled + service: + name: firewalld + state: stopped + enabled: no + when: pkg_check.rc == 0 + +- name: Reload systemd units + command: systemctl daemon-reload + when: install_result | changed + +- name: Start and enable iptables service + service: + name: iptables + state: started + enabled: yes + register: result + +- name: need to pause here, otherwise the iptables service starting can sometimes cause ssh to fail + pause: seconds=10 + when: result | changed + +# TODO: submit PR upstream to add mask/unmask to service module +- name: Mask firewalld service + command: systemctl mask firewalld + register: result + changed_when: "'firewalld' in result.stdout" + when: pkg_check.rc == 0 + ignore_errors: yes + +- name: Add iptables allow rules + os_firewall_manage_iptables: + name: "{{ item.service }}" + action: add + protocol: "{{ item.port.split('/')[1] }}" + port: "{{ item.port.split('/')[0] }}" + with_items: os_firewall_allow + when: os_firewall_allow is defined + +- name: Remove iptables rules + os_firewall_manage_iptables: + name: "{{ item.service }}" + action: remove + protocol: "{{ item.port.split('/')[1] }}" + port: "{{ item.port.split('/')[0] }}" + with_items: os_firewall_deny + when: os_firewall_deny is defined diff --git a/roles/os_firewall/tasks/main.yml b/roles/os_firewall/tasks/main.yml new file mode 100644 index 0000000000..ad89ef97c9 --- /dev/null +++ b/roles/os_firewall/tasks/main.yml @@ -0,0 +1,6 @@ +--- +- include: firewall/firewalld.yml + when: os_firewall_use_firewalld + +- include: firewall/iptables.yml + when: not os_firewall_use_firewalld