From 7c90cacef0f5cf61fb8ac3adb905507dd4247d84 Mon Sep 17 00:00:00 2001 From: Jason DeTiberus Date: Tue, 3 Mar 2015 13:06:49 -0500 Subject: refactor firewall management into new role - Add os_firewall role - Remove firewall settings from base_os, add wait task to os_firewall - Added a iptables firewall module for maintaining the following (in a mostly naive manner): - ensure the OPENSHIFT_ALLOW chain is defined - ensure that there is a jump rule in the INPUT chain for OPENSHIFT_ALLOW - adds or removes entries from the OPENSHIFT_ALLOW chain - issues '/usr/libexec/iptables/iptables.init save' when rules are changed - Limitations of iptables firewall module - only allows setting of ports/protocols to open - no testing on ipv6 support - made os_firewall a dependency of openshift_common - Hardcoded openshift_common to use iptables (through the vars directory) until upstream support is in place for firewalld --- roles/os_firewall/README.md | 66 ++++++ roles/os_firewall/defaults/main.yml | 2 + .../library/os_firewall_manage_iptables.py | 254 +++++++++++++++++++++ roles/os_firewall/meta/main.yml | 13 ++ roles/os_firewall/tasks/firewall/firewalld.yml | 68 ++++++ roles/os_firewall/tasks/firewall/iptables.yml | 53 +++++ roles/os_firewall/tasks/main.yml | 6 + 7 files changed, 462 insertions(+) create mode 100644 roles/os_firewall/README.md create mode 100644 roles/os_firewall/defaults/main.yml create mode 100644 roles/os_firewall/library/os_firewall_manage_iptables.py create mode 100644 roles/os_firewall/meta/main.yml create mode 100644 roles/os_firewall/tasks/firewall/firewalld.yml create mode 100644 roles/os_firewall/tasks/firewall/iptables.yml create mode 100644 roles/os_firewall/tasks/main.yml (limited to 'roles/os_firewall') diff --git a/roles/os_firewall/README.md b/roles/os_firewall/README.md new file mode 100644 index 000000000..fe6318184 --- /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 +------- + +ASL 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 000000000..bcf1d9a34 --- /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 100644 index 000000000..fef710055 --- /dev/null +++ b/roles/os_firewall/library/os_firewall_manage_iptables.py @@ -0,0 +1,254 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- + +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): + 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): + super(IpTablesCreateChainError, self).__init__(msg, cmd, exit_code, output) + self.chain = chain + + +class IpTablesCreateJumpRuleError(IpTablesError): + def __init__(self, chain, msg, cmd, exit_code, output): + 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: + def __init__(self, module, ip_version, check_mode, chain): + self.module = module + self.ip_version = ip_version + self.check_mode = check_mode + self.chain = 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 e: + raise IpTablesSaveError( + msg="Failed to save iptables rules", + cmd=e.cmd, exit_code=e.returncode, output=e.output) + + def add_rule(self, port, proto): + rule = self.gen_rule(port, proto) + if not self.rule_exists(rule): + if not self.chain_exists(): + self.create_chain() + if not self.jump_rule_exists(): + self.create_jump_rule() + + 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 e: + raise IpTablesCreateChainError( + chain=self.chain, + msg="Failed to create rule for " + "%s %s" % (self.proto, self.port), + cmd=e.cmd, exit_code=e.returncode, + output=e.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 e: + raise IpTablesRemoveChainError( + chain=self.chain, + msg="Failed to remove rule for %s %s" % (proto, port), + cmd=e.cmd, exit_code=e.returncode, output=e.output) + + def rule_exists(self, rule): + check_cmd = self.cmd + ['-C'] + rule + return True if subprocess.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_rule(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', 'INPUT', '--line-numbers'] + output = check_output(cmd, stderr=subprocess.STDOUT) + + # break the input rules into rows and columns + input_rules = map(lambda s: s.split(), 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] + + # Raise an exception if we do not find a valid INPUT rule + if not last_rule_num or not last_rule_target: + raise IpTablesCreateJumpRuleError( + chain=self.chain, + msg="Failed to find existing INPUT rules", + cmd=None, exit_code=None, output=None) + + # 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_target == 'REJECT': + # insert rule + cmd = self.cmd + ['-I', 'INPUT', str(last_rule_num)] + else: + # append rule + cmd = self.cmd + ['-A', 'INPUT'] + cmd += ['-j', self.chain] + output = check_output(cmd, stderr=subprocess.STDOUT) + changed = True + self.output.append(output) + except subprocess.CalledProcessError as e: + if '--line-numbers' in e.cmd: + raise IpTablesCreateJumpRuleError( + chain=self.chain, + msg="Failed to query existing INPUT rules to " + "determine jump rule location", + cmd=e.cmd, exit_code=e.returncode, + output=e.output) + else: + raise IpTablesCreateJumpRuleError( + chain=self.chain, + msg="Failed to create jump rule for chain %s" % + self.chain, + cmd=e.cmd, exit_code=e.returncode, + output=e.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) + except subprocess.CalledProcessError as e: + raise IpTablesCreateChainError( + chain=self.chain, + msg="Failed to create chain: %s" % self.chain, + cmd=e.cmd, exit_code=e.returncode, output=e.output + ) + + def jump_rule_exists(self): + cmd = self.cmd + ['-C', 'INPUT', '-j', self.chain] + return True if subprocess.call(cmd) == 0 else False + + def chain_exists(self): + cmd = self.cmd + ['-L', self.chain] + return True if subprocess.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): + cmd = 'iptables' if self.ip_version == 'ipv4' else 'ip6tables' + 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']), + protocol=dict(required=True, choices=['tcp', 'udp']), + port=dict(required=True, 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'] + ip_version = module.params['ip_version'] + chain = 'OS_FIREWALL_ALLOW' + + iptables_manager = IpTablesManager(module, ip_version, module.check_mode, chain) + + try: + if action == 'add': + iptables_manager.add_rule(port, protocol) + elif action == 'remove': + iptables_manager.remove_rule(port, protocol) + except IpTablesError as e: + module.fail_json(msg=e.msg) + + return module.exit_json(changed=iptables_manager.changed, + output=iptables_manager.output) + + +# import module snippets +from ansible.module_utils.basic import * +main() diff --git a/roles/os_firewall/meta/main.yml b/roles/os_firewall/meta/main.yml new file mode 100644 index 000000000..e431f531c --- /dev/null +++ b/roles/os_firewall/meta/main.yml @@ -0,0 +1,13 @@ +galaxy_info: + author: Jason DeTiberus + description: os_firewall + company: Red Hat, Inc. + license: ASL 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 000000000..f6d5fe2eb --- /dev/null +++ b/roles/os_firewall/tasks/firewall/firewalld.yml @@ -0,0 +1,68 @@ +--- +- name: Install firewalld packages + yum: + name: firewalld + state: present + +- 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: Ensure iptables services are not enabled + service: + name: "{{ item }}" + state: stopped + enabled: no + with_items: + - iptables + - ip6tables + +- name: Mask iptables services + command: systemctl mask "{{ item }}" + register: result + failed_when: result.rc != 0 + changed_when: False + with_items: + - iptables + - ip6tables + +# 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: allow + when: allow is defined + +- name: Persist firewalld allow rules + firewalld: + port: "{{ item.port }}" + permanent: true + state: enabled + with_items: allow + when: allow is defined + +- name: Remove firewalld allow rules + firewalld: + port: "{{ item.port }}" + permanent: false + state: disabled + with_items: deny + when: deny is defined + +- name: Persist removal of firewalld allow rules + firewalld: + port: "{{ item.port }}" + permanent: true + state: disabled + with_items: deny + when: 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 000000000..4f051c2bd --- /dev/null +++ b/roles/os_firewall/tasks/firewall/iptables.yml @@ -0,0 +1,53 @@ +--- +- name: Install iptables packages + yum: + name: "{{ item }}" + state: present + with_items: + - iptables + - iptables-services + +- name: Start and enable iptables services + service: + name: "{{ os_firewall_svc }}" + state: started + enabled: yes + with_items: + - iptables + - ip6tables + register: result + +- name: need to pause here, otherwise the iptables service starting can sometimes cause ssh to fail + pause: seconds=10 + when: result | changed + +- name: Ensure firewalld service is not enabled + service: + name: firewalld + state: stopped + enabled: no + +- name: Mask firewalld service + command: systemctl mask firewalld + register: result + failed_when: result.rc != 0 + changed_when: False + 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: allow + when: 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: deny + when: deny is defined diff --git a/roles/os_firewall/tasks/main.yml b/roles/os_firewall/tasks/main.yml new file mode 100644 index 000000000..ad89ef97c --- /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 -- cgit v1.2.1