From 7c90cacef0f5cf61fb8ac3adb905507dd4247d84 Mon Sep 17 00:00:00 2001
From: Jason DeTiberus <jdetiber@redhat.com>
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.3