diff options
| author | Jason DeTiberus <jdetiber@redhat.com> | 2015-03-03 13:06:49 -0500 | 
|---|---|---|
| committer | Jason DeTiberus <jdetiber@redhat.com> | 2015-03-05 17:37:19 -0500 | 
| commit | 7c90cacef0f5cf61fb8ac3adb905507dd4247d84 (patch) | |
| tree | 708aacc9b414cc8392927ecd1ab515a0faaa7f49 | |
| parent | 151f10b010651a49dfb4b46ca74e966be36b1279 (diff) | |
| download | openshift-7c90cacef0f5cf61fb8ac3adb905507dd4247d84.tar.gz openshift-7c90cacef0f5cf61fb8ac3adb905507dd4247d84.tar.bz2 openshift-7c90cacef0f5cf61fb8ac3adb905507dd4247d84.tar.xz openshift-7c90cacef0f5cf61fb8ac3adb905507dd4247d84.zip | |
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
| -rw-r--r-- | roles/base_os/tasks/main.yaml | 16 | ||||
| -rw-r--r-- | roles/openshift_common/meta/main.yml | 4 | ||||
| -rw-r--r-- | roles/openshift_common/tasks/firewall.yml | 34 | ||||
| -rw-r--r-- | roles/openshift_common/tasks/main.yml | 16 | ||||
| -rw-r--r-- | roles/openshift_common/vars/main.yml | 4 | ||||
| -rw-r--r-- | roles/os_firewall/README.md | 66 | ||||
| -rw-r--r-- | roles/os_firewall/defaults/main.yml | 2 | ||||
| -rw-r--r-- | roles/os_firewall/library/os_firewall_manage_iptables.py | 254 | ||||
| -rw-r--r-- | roles/os_firewall/meta/main.yml | 13 | ||||
| -rw-r--r-- | roles/os_firewall/tasks/firewall/firewalld.yml | 68 | ||||
| -rw-r--r-- | roles/os_firewall/tasks/firewall/iptables.yml | 53 | ||||
| -rw-r--r-- | roles/os_firewall/tasks/main.yml | 6 | 
12 files changed, 477 insertions, 59 deletions
| diff --git a/roles/base_os/tasks/main.yaml b/roles/base_os/tasks/main.yaml index 51fe1e5b6..aad611f70 100644 --- a/roles/base_os/tasks/main.yaml +++ b/roles/base_os/tasks/main.yaml @@ -15,19 +15,3 @@    yum:      pkg: bash-completion      state: installed - -- name: Install firewalld -  yum: -    pkg: firewalld -    state: installed - -- 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 diff --git a/roles/openshift_common/meta/main.yml b/roles/openshift_common/meta/main.yml index 128da25b4..7dc4603d0 100644 --- a/roles/openshift_common/meta/main.yml +++ b/roles/openshift_common/meta/main.yml @@ -1,3 +1,4 @@ +---  galaxy_info:    author: Jason DeTiberus    description: OpenShift Common @@ -10,4 +11,5 @@ galaxy_info:      - 7    categories:    - cloud -dependencies: [] +dependencies: +- { role: os_firewall } diff --git a/roles/openshift_common/tasks/firewall.yml b/roles/openshift_common/tasks/firewall.yml deleted file mode 100644 index 514466769..000000000 --- a/roles/openshift_common/tasks/firewall.yml +++ /dev/null @@ -1,34 +0,0 @@ ---- -# 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/openshift_common/tasks/main.yml b/roles/openshift_common/tasks/main.yml index b94fca690..723bdd9fa 100644 --- a/roles/openshift_common/tasks/main.yml +++ b/roles/openshift_common/tasks/main.yml @@ -7,6 +7,14 @@  - name: Configure local facts file    file: path=/etc/ansible/facts.d/ state=directory mode=0750 +- name: Add KUBECONFIG to .bash_profile for user root +  lineinfile: +    dest: /root/.bash_profile +    regexp: "KUBECONFIG=" +    line: "export KUBECONFIG=/var/lib/openshift/openshift.local.certificates/admin/.kubeconfig" +    state: present +    insertafter: EOF +  - name: Set common OpenShift facts    include: set_facts.yml    facts: @@ -19,11 +27,3 @@    - section: common      option: debug_level      value: "{{ openshift_debug_level }}" - -- name: Add KUBECONFIG to .bash_profile for user root -  lineinfile: -    dest: /root/.bash_profile -    regexp: "KUBECONFIG=" -    line: "export KUBECONFIG=/var/lib/openshift/openshift.local.certificates/admin/.kubeconfig" -    state: present -    insertafter: EOF diff --git a/roles/openshift_common/vars/main.yml b/roles/openshift_common/vars/main.yml index c93898665..0855c0cc5 100644 --- a/roles/openshift_common/vars/main.yml +++ b/roles/openshift_common/vars/main.yml @@ -1,2 +1,6 @@  ---  openshift_master_credentials_dir: /var/lib/openshift/openshift.local.certificates/admin/ + +# TODO: Upstream kubernetes only supports iptables currently, if this changes, +# then these variable should be moved to defaults +openshift_use_firewalld: False 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 | 
