Update firewall role to use an action plugin

This change updates our firewall role so that its using an action plugin
to invoke the iptables module. This is being done to speed up rule creation
and maintenance.

Tests have been updated to ensure we're capturing all of the logic within
the action plugin accordingly.

Depends-On: Ie9b6fd5792efb270ae577b08d6a2d5b78dabe5e7
Closes-Bug: #1856094
Change-Id: I3e4c6586796753b5d1cb9aa6a7c3eee6ecc235fb
Signed-off-by: Kevin Carter <kecarter@redhat.com>
This commit is contained in:
Kevin Carter 2019-12-11 20:02:49 -06:00
parent b91ec6ba1f
commit 38c75fb83e
No known key found for this signature in database
GPG Key ID: CE94BD890A47B20A
6 changed files with 343 additions and 199 deletions

View File

@ -0,0 +1,320 @@
# Copyright 2019 Red Hat, Inc.
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
DOCUMENTATION = """
---
module: tripleo_iptables
author:
- Kevin Carter (@cloudnull) <kecarter@redhat.com>
version_added: '2.8'
short_description: Runs iptables module commands in bulk.
notes: []
description:
- This module accepts iptables rules in list format and batches their
creation to speed up the creation of rules at scale.
options:
tripleo_rules:
description:
- List of rules to batch, rules have been constructed using the tripleo
spec and will be formatted to match the input values of the core
iptables module.
required: True
"""
EXAMPLES = """
- name: Run Package Installation
tripleo_iptables:
tripleo_rules:
- '1 rule special':
dport:
- 1234
- 4321
- '2 rule special also':
dport:
- 2345
- 5432
"""
from ansible.plugins.action import ActionBase
from ansible.plugins.filter import ipaddr
from ansible.utils.display import Display
DISPLAY = Display()
RULE_STATES = {
'enabled': 'present',
'present': 'present',
'absent': 'absent',
'disabled': 'absent'
}
IPTABLES_BIN = {
'ipv4': 'iptables',
'ipv6': 'ip6tables'
}
IPTABLES_CHAIN_CMD = """
if ! {cmd} --list "{chain}"; then
{cmd} -N "{chain}"
fi
"""
IPTABLES_CHAINS = ('INPUT', 'OUTPUT', 'FORWARD')
class ActionModule(ActionBase):
"""Batch iptables rules for faster rule creation."""
def _run_module(self, name, args, task_vars):
"""Runs an ansible module and collects return information.
:returns: boolean
"""
module_return = self._execute_module(
module_name=name,
module_args=args,
task_vars=task_vars
)
changed = module_return.get('changed')
if changed:
self.return_data['changed'] = True
self.return_data['stdout'] = module_return.get('stdout')
self.return_data['stderr'] = module_return.get('stderr')
self.return_data['msg'] = module_return.get('msg')
self.return_data['cmd'] = module_return.get('cmd')
self.return_data['rc'] = module_return.get('rc', 0)
fatal = self.return_data['failed'] = module_return.get(
'failed',
False
)
DISPLAY.vv('Module name: {}'.format(name))
DISPLAY.vv('Module args: {}'.format(args))
if fatal:
DISPLAY.error('Failed, module return: {}'.format(module_return))
DISPLAY.error('Failed, return data: {}'.format(self.return_data))
return fatal
@staticmethod
def _check_rule_data(rule_data, ipversion):
"""Check the rule data for compatible ip version information.
This function uses the ansible ipaddr filter to validate IP
information when a source or destination has been provided.
:returns: boolean
"""
kwargs_hash = {
'ipv6': {
'version': 6,
'query': 'ipv6',
'alias': 'ipv6'
},
'ipv4': {
'version': 4,
'query': 'ipv4',
'alias': 'ipv4'
}
}
for arg in ('source', 'destination'):
ip_data = rule_data.get(arg)
if ip_data:
DISPLAY.v(
'Checking "{}" against "{}" with ip version "{}"'.format(
arg,
ip_data,
ipversion
)
)
ip_data_check = ipaddr.ipaddr(
value=ip_data,
**kwargs_hash[ipversion]
)
DISPLAY.vvv('ipaddr filter return "{}"'.format(ip_data_check))
if not ip_data_check:
DISPLAY.v(
'Rule has a "{}" but the value "{}" is not applicable'
' to ip version "{}"'.format(
arg,
ip_data,
ipversion
)
)
DISPLAY.vvv('Rule data: "{}"'.format(rule_data))
return False
else:
return True
def queue_rules(self):
"""Add chains and rules to the required queues."""
for item in self._task.args['tripleo_rules']:
rule_data = dict()
rule = item['rule']
ipversions = rule.get('ipversion', ['ipv4', 'ipv6'])
if not isinstance(ipversions, list):
ipversions = [ipversions]
state = rule.get('extras', dict()).get('ensure', 'enabled')
rule_data['state'] = RULE_STATES[state]
action = rule_data['action'] = rule.get('action', 'insert')
if action == 'drop':
rule_data['action'] = 'insert'
rule_data['state'] = 'absent'
rule_data['chain'] = rule.get('chain', 'INPUT')
rule_data['jump'] = rule.get('jump', 'ACCEPT')
rule_data['protocol'] = rule.get('proto', 'tcp')
if 'table' in rule:
rule_data['table'] = rule['table']
if 'interface' in rule:
rule_data['in_interface'] = rule['interface']
if 'sport' in rule:
rule_data['source_port'] = rule['sport']
if 'source' in rule:
rule_data['source'] = rule['source']
if rule_data['protocol'] != 'gre':
rule_data['ctstate'] = rule.get('state', 'NEW')
if 'limit' in rule:
rule_data['limit'] = rule['limit']
if 'limit_burst' in rule:
rule_data['limit_burst'] = rule['limit_burst']
if 'destination' in rule:
rule_data['destination'] = rule['destination']
for ipversion in ipversions:
if not self._check_rule_data(rule_data=rule_data,
ipversion=ipversion):
continue
versioned_rule_data = rule_data.copy()
versioned_rule_data['ip_version'] = ipversion
if 'rule_name' in item:
versioned_rule_data['comment'] = '{} {}'.format(
item['rule_name'],
ipversion
)
if not versioned_rule_data['chain'] in IPTABLES_CHAINS:
chain = versioned_rule_data['chain']
DISPLAY.v(
'Queueing chain: {}, ip version {}'.format(
chain, ipversion
)
)
self.iptables_chains.append(
{
'ipv': ipversion,
'chain': chain,
'command': IPTABLES_CHAIN_CMD.format(
cmd=IPTABLES_BIN[ipversion],
chain=chain
)
}
)
if 'dport' in rule:
dport_rule_data = versioned_rule_data.copy()
dports = rule['dport']
if not isinstance(dports, list):
dports = [dports]
for dport in dports:
if isinstance(dport, int):
dport_rule_data['destination_port'] = dport
else:
dport = dport.replace('-', ':')
dport_rule_data['destination_port'] = dport
DISPLAY.v(
'Queueing port rule: {},'
' ip version: {},'
' dport: {}'.format(
dport_rule_data.get('comment', None),
ipversion,
dport_rule_data['destination_port']
)
)
self.iptables_rules.append(dport_rule_data.copy())
else:
DISPLAY.v(
'Queueing service rule: {},'
' ip version: {}'.format(
versioned_rule_data.get('comment', None),
ipversion
)
)
self.iptables_rules.append(versioned_rule_data.copy())
def run(self, tmp=None, task_vars=None):
"""Run the iptables firewall rule batcher.
When rules are batched, the chains will be created before the rules.
"""
self.return_data = dict()
self.iptables_rules = list()
self.iptables_chains = list()
self.queue_rules()
for iptables_chain in self.iptables_chains:
DISPLAY.v(
'Managing chain: {} for version {}'.format(
iptables_chain['chain'],
iptables_chain['ipv']
)
)
return_data = self._low_level_execute_command(
iptables_chain['command'],
executable='/bin/bash'
)
if return_data['rc'] > 0:
DISPLAY.error(msg='Failed command: {}'.format(iptables_chain))
DISPLAY.error(msg='Failed chain data: {}'.format(return_data))
return return_data
for iptables_rule in self.iptables_rules:
DISPLAY.v(
'Managing rule: {},'
' dport: {},'
' ip version: {}'.format(
iptables_rule.get('comment', 'undefined'),
iptables_rule.get('destination_port', 'undefined'),
iptables_rule['ip_version'],
)
)
fatal = self._run_module(
name='iptables',
args=iptables_rule,
task_vars=task_vars
)
if fatal:
return self.return_data
return self.return_data

View File

@ -47,3 +47,11 @@
dport: 2211
'006 ironic-inspector':
dport: 2212
'124 snmp':
dport: 2212
source: '192.168.24.1/24'
chain: test-chain2
'125 snmp':
dport: 2212
destination: '::'
chain: test-chain2

View File

@ -61,3 +61,13 @@
dport: 2212
extras:
ensure: 'absent'
'124 snmp':
dport: 2212
source: '192.168.24.1/24'
extras:
ensure: 'absent'
'125 snmp':
dport: 2212
destination: '::'
extras:
ensure: 'absent'

View File

@ -39,18 +39,6 @@
list
}}"
- name: Check rule set
fail:
msg: >-
`{{ item['rule_name'] }}` firewall rule cannot be created. TCP or UDP rules
for INPUT or OUTPUT need sport or dport defined.
when:
- ((item['rule']['proto'] | default('tcp')) in ['tcp', 'udp']) and
(item['rule']['dport'] is undefined) and
((item['rule']['chain'] | default('INPUT')) != 'FORWARD') and
((item['rule']['table'] | default('filter')) != 'nat')
loop: "{{ firewall_rules_sorted }}"
- name: Firewall add block
become: true
block:
@ -65,6 +53,8 @@
state: started
enabled: true
- name: Enable filewall port config
include_tasks: tripleo_firewall_add.yml
loop: "{{ firewall_rules_sorted }}"
- name: Manage firewall rules
tripleo_iptables:
tripleo_rules: "{{ firewall_rules_sorted }}"
notify:
- Save firewall rules

View File

@ -1,156 +0,0 @@
---
# Copyright 2019 Red Hat, Inc.
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
# NOTE(Cloudnull): This task exists because the iptables module will not
# create a chain. There is a feature request open for this
# [ https://github.com/ansible/ansible/issues/25099 ].
# A change has been added to support this functionality but
# it is awaiting review and merge.
# [ https://github.com/ansible/ansible/pull/32158 ]. When
# this change is merged this task should be removed.
- name: Ensure chains exist
shell: |-
EXIT_CODE=0
if ! iptables --list "{{ item['rule']['chain'] }}"; then
iptables -N "{{ item['rule']['chain'] }}"
EXIT_CODE=99
fi
if ! ip6tables --list "{{ item['rule']['chain'] }}"; then
ip6tables -N "{{ item['rule']['chain'] }}"
EXIT_CODE=99
fi
exit ${EXIT_CODE}
when:
- (item['rule']['chain'] | default('INPUT')) != 'INPUT'
- tripleo_firewall_port_states[(item['rule']['extras'] | default({}))['ensure'] | default('enabled')] == "present"
register: iptables_chain
changed_when: iptables_chain.rc == 99
failed_when: not (iptables_chain.rc in [0, 99])
- include_tasks: tripleo_firewall_state.yml
# NOTE(Cloudnull): This task adds multiport rules using a loop instead of using
# the multiport key word. While multiport is perfectly functional
# using raw iptables rules, it is not supported in the ansible
# module. The use of the loop will be revised just as soon as the
# pull request [ https://github.com/ansible/ansible/pull/21071 ]
# is merged.
- name: Firewall port rule (ipv4)
iptables:
action: insert
table: "{{ item['rule']['table'] | default(omit) }}"
chain: "{{ item['rule']['chain'] | default('INPUT') }}"
in_interface: "{{ item['rule']['interface'] | default(omit) }}"
protocol: "{{ item['rule']['proto'] | default('tcp') }}"
destination_port: "{{ port | replace('-', ':') }}"
destination: "{{ item['rule']['destination'] | default(omit) }}"
source_port: "{{ item['rule']['sport'] | default(omit) | replace('-', ':') }}"
source: "{{ item['rule']['source'] | default(omit) }}"
comment: "{{ item['rule_name'] }} ipv4"
jump: "{{ item['rule']['jump'] | default('ACCEPT') }}"
ctstate: "{{ tripleo_ctstate }}"
limit: "{{ item['rule']['limit'] | default(omit) }}"
limit_burst: "{{ item['rule']['limit_burst'] | default(omit) }}"
ip_version: ipv4
state: "{{ tripleo_firewall_port_states[(item['rule']['extras'] | default({}))['ensure'] | default('enabled')] }}"
when:
- item['rule']['dport'] is defined
- (item['rule']['ipversion'] | default('ipv4')) != 'ipv6'
- item['rule']['source'] | default('127.0.0.1') | ipv4
- item['rule']['destination'] | default('127.0.0.1') | ipv4
loop: "{{ ((item['rule']['dport'] is iterable) and (item['rule']['dport'] is not string)) | ternary(item['rule']['dport'], [item['rule']['dport']]) }}"
loop_control:
loop_var: port
notify:
- Save firewall rules
# NOTE(Cloudnull): This task adds multiport rules using a loop instead of using
# the multiport key word. While multiport is perfectly functional
# using raw iptables rules, it is not supported in the ansible
# module. The use of the loop will be revised just as soon as the
# pull request [ https://github.com/ansible/ansible/pull/21071 ]
# is merged.
- name: Firewall port rule (ipv6)
iptables:
action: insert
table: "{{ item['rule']['table'] | default(omit) }}"
chain: "{{ item['rule']['chain'] | default('INPUT') }}"
in_interface: "{{ item['rule']['interface'] | default(omit) }}"
protocol: "{{ item['rule']['proto'] | default('tcp') }}"
destination_port: "{{ port | replace('-', ':') }}"
destination: "{{ item['rule']['destination'] | default(omit) }}"
source_port: "{{ item['rule']['sport'] | default(omit) | replace('-', ':') }}"
source: "{{ item['rule']['source'] | default(omit) }}"
comment: "{{ item['rule_name'] }} ipv6"
jump: "{{ item['rule']['jump'] | default('ACCEPT') }}"
ctstate: "{{ tripleo_ctstate }}"
limit: "{{ item['rule']['limit'] | default(omit) }}"
limit_burst: "{{ item['rule']['limit_burst'] | default(omit) }}"
ip_version: ipv6
state: "{{ tripleo_firewall_port_states[(item['rule']['extras'] | default({}))['ensure'] | default('enabled')] }}"
when:
- item['rule']['dport'] is defined
- (item['rule']['ipversion'] | default('ipv6')) != 'ipv4'
- item['rule']['source'] | default('::') | ipv6
- item['rule']['destination'] | default('::') | ipv6
loop: "{{ ((item['rule']['dport'] is iterable) and (item['rule']['dport'] is not string)) | ternary(item['rule']['dport'], [item['rule']['dport']]) }}"
loop_control:
loop_var: port
notify:
- Save firewall rules
- name: Firewall protocol rule (ipv4)
iptables:
action: insert
table: "{{ item['rule']['table'] | default(omit) }}"
chain: "{{ item['rule']['chain'] | default('INPUT') }}"
in_interface: "{{ item['rule']['interface'] | default(omit) }}"
protocol: "{{ item['rule']['proto'] | default(omit) }}"
source_port: "{{ item['rule']['sport'] | default(omit) | replace('-', ':') }}"
source: "{{ item['rule']['source'] | default(omit) }}"
comment: "{{ item['rule_name'] }} ipv4"
jump: "{{ item['rule']['jump'] | default('ACCEPT') }}"
ctstate: "{{ tripleo_ctstate }}"
limit: "{{ item['rule']['limit'] | default(omit) }}"
limit_burst: "{{ item['rule']['limit_burst'] | default(omit) }}"
ip_version: ipv4
state: "{{ tripleo_firewall_port_states[(item['rule']['extras'] | default({}))['ensure'] | default('enabled')] }}"
when:
- (item['rule']['ipversion'] | default('ipv4')) != 'ipv6'
- item['rule']['proto'] is defined
- item['rule']['dport'] is undefined
- name: Firewall protocol rule (ipv6)
iptables:
action: insert
table: "{{ item['rule']['table'] | default(omit) }}"
chain: "{{ item['rule']['chain'] | default('INPUT') }}"
in_interface: "{{ item['rule']['interface'] | default(omit) }}"
protocol: "{{ item['rule']['proto'] | default(omit) }}"
source_port: "{{ item['rule']['sport'] | default(omit) | replace('-', ':') }}"
source: "{{ item['rule']['source'] | default(omit) }}"
comment: "{{ item['rule_name'] }} ipv6"
jump: "{{ item['rule']['jump'] | default('ACCEPT') }}"
ctstate: "{{ tripleo_ctstate }}"
limit: "{{ item['rule']['limit'] | default(omit) }}"
limit_burst: "{{ item['rule']['limit_burst'] | default(omit) }}"
ip_version: ipv6
state: "{{ tripleo_firewall_port_states[(item['rule']['extras'] | default({}))['ensure'] | default('enabled')] }}"
when:
- (item['rule']['ipversion'] | default('ipv6')) != 'ipv4'
- item['rule']['proto'] is defined
- item['rule']['dport'] is undefined

View File

@ -1,28 +0,0 @@
---
# Copyright 2019 Red Hat, Inc.
# All Rights Reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
- name: Set gre state fact
set_fact:
tripleo_ctstate: []
when:
- (item['rule']['proto'] | default('tcp')) == 'gre'
- name: Set general state fact
set_fact:
tripleo_ctstate: "{{ item['rule']['ctstate'] | default(item['rule']['state'] | default('NEW')) }}"
when:
- (item['rule']['proto'] | default('tcp')) != 'gre'