浏览代码

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
Jason DeTiberus 10 年之前
父节点
当前提交
7c90cacef0

+ 0 - 16
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

+ 3 - 1
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 }

+ 0 - 34
roles/openshift_common/tasks/firewall.yml

@@ -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

+ 8 - 8
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

+ 4 - 0
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

+ 66 - 0
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

+ 2 - 0
roles/os_firewall/defaults/main.yml

@@ -0,0 +1,2 @@
+---
+os_firewall_use_firewalld: True

+ 254 - 0
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()

+ 13 - 0
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: []

+ 68 - 0
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

+ 53 - 0
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

+ 6 - 0
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