Add functionality and reduce complexity.

* Separate 'state', 'policy' and 'rule' commands
* Support for 'logging' command
* Support for 'direction' and 'interface' attributes
* Reliable change notifications based on 'ufw status verbose' diff
* Update documentation
* Cleanup
This commit is contained in:
Jarno Keskikangas 2014-01-06 22:44:25 +02:00
parent 651c04a3ec
commit f4e8a86c87

View file

@ -1,9 +1,12 @@
#!/usr/bin/python
# -*- coding: utf-8 -*-
# (c) 2014, Jarno Keskikangas <jarno.keskikangas@gmail.com>
# (c) 2013, Aleksey Ovcharenko <aleksey.ovcharenko@gmail.com>
# (c) 2013, James Martin <jmartin@basho.com>
#
# This file is part of Ansible
#
# Ansible is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
@ -16,251 +19,228 @@
#
# You should have received a copy of the GNU General Public License
# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
DOCUMENTATION = '''
---
module: ufw
short_description: This module handles Ubuntu UFW operations
short_description: Manage firewall with UFW
description:
- This module handles Ubuntu UFW operations
- Manage firewall with UFW.
version_added: 1.5
author: Aleksey Ovcharenko, Jarno Keskikangas
notes:
- See C(man ufw) for more examples.
requirements:
- C(ufw) package
options:
default_policy:
description:
- Change the default policy for incoming traffic.
required: false
choices: ['allow', 'deny', 'reject']
default: None
delete:
description:
- Delete rule instead of creation.
required: false
choices: ['yes', 'no']
default: 'no'
state:
description: |
I(enable) reloads firewall and enables firewall on boot.
I(disable) unloads firewall and disables firewall on boot.
I(reload) reloads firewall.
I(reset) disables and resets firewall to installation defaults.
I(allow) adds allow rule. See B(EXAMPLES).
I(deny) adds deny rule. See B(EXAMPLES).
I(reject) adds reject rule. See B(EXAMPLES).
I(limit) adds limit rule. Currently only IPv4 is supported. See B(EXAMPLES).
required: false
choices: ['enable', 'disable', 'reload', 'reset', 'allow', 'deny', 'reject', 'limit']
aliases: ['rule']
default: 'allow'
name:
description:
- Use profile located in /etc/ufw/applications.d
- C(enabled) reloads firewall and enables firewall on boot.
- C(disabled) unloads firewall and disables firewall on boot.
- C(reloaded) reloads firewall.
- C(reseted) disables and resets firewall to installation defaults.
required: false
default: None
version_added: "2.1"
choices: ['enabled', 'disabled', 'reloaded', 'reseted']
policy:
description:
- Change the default policy for incoming or outgoing traffic.
required: false
alias: default
choices: ['allow', 'deny', 'reject']
direction:
description:
- Select direction for a rule or default policy command.
required: false
choices: ['in', 'out', 'incoming', 'outgoing']
logging:
description:
- Toggles logging. Logged packets use the LOG_KERN syslog facility.
choices: ['on', 'off', 'low', 'medium', 'high', 'full']
required: false
rule:
description:
- Add firewall rule
required: false
choises: ['allow', 'deny', 'reject', 'limit']
log:
description:
- Log new connections matched to this rule
required: false
choises: ['yes', 'no']
from_ip:
description:
- Source IP address.
required: false
aliases: ['src']
aliases: ['from', 'src']
default: 'any'
from_port:
description:
- Source port.
required: false
default: 'any'
to_ip:
description:
- Destination IP address.
required: false
aliases: ['dest']
aliases: ['to', 'dest']
default: 'any'
to_port:
description:
- Destination port.
required: false
default: 'any'
aliases: ['port']
proto:
description:
- TCP/IP protocol.
choices: ['any', 'tcp', 'udp', 'ipv6']
required: false
log:
name:
description:
- Toggles logging. Logged packets use the LOG_KERN syslog facility.
choices: ['yes', 'no']
- Use profile located in C(/etc/ufw/applications.d)
required: false
default: 'no'
version_added: 2.0
notes:
- See C(man 8 ufw) for more example.
requirements: [ ]
author: Aleksey Ovcharenko
aliases: ['app']
delete:
description:
- Delete rule.
required: false
choices: ['yes', 'no']
'''
EXAMPLES = '''
# Allow everything and enable UFW
ufw: state={{ item }}
with_items:
- allow
- enable
ufw: state=enable policy=allow logging=on
# Sometimes it is desirable to let the sender know when traffic is
# being denied, rather than simply ignoring it. In these cases, use
# reject instead of deny. For example:
ufw: state=reject port=auth
# reject instead of deny. In addition, log rejected connections:
ufw: rule=reject port=auth log=yes
# ufw supports connection rate limiting, which is useful for protecting
# against brute-force login attacks. ufw will deny connections if an IP
# address has attempted to initiate 6 or more connections in the last
# 30 seconds. See http://www.debian-administration.org/articles/187
# for details. Typical usage is:
ufw: state=limit port=ssh proto=tcp
ufw: rule=limit port=ssh proto=tcp
# Allow OpenSSH
ufw: state=allow name=OpenSSH
ufw: rule=allow name=OpenSSH
# Delete OpenSSH rule
ufw: rule=allow name=OpenSSH delete=yes
# Deny all access to port 53:
ufw: state=deny port=53
ufw: rule=deny port=53
# Allow all access to tcp port 80:
ufw: state=allow to_port=80 proto=tcp
ufw: rule=allow port=80 proto=tcp
# Allow all access from RFC1918 networks to this host:
ufw: state=allow from_ip={{ item }}
ufw: rule=allow src={{ item }}
with_items:
- 10.0.0.0/8
- 172.16.0.0/12
- 192.168.0.0/16
# Deny access to udp port 514 from host 1.2.3.4:
ufw: state=deny proto=udp from_ip=1.2.3.4 to_port=514
ufw: rule=deny proto=udp src=1.2.3.4 port=514
# Allow access to udp 1.2.3.4 port 5469 from 1.2.3.5 port 5469:
ufw: state=allow proto=udp from_ip=1.2.3.5 from_port=5469 to_ip=1.2.3.4 to_port=5469
# Allow incoming access to eth0 from 1.2.3.5 port 5469 to 1.2.3.4 port 5469
ufw: rule=allow interface=eth0 direction=in proto=udp src=1.2.3.5 from_port=5469 dest=1.2.3.4 to_port=5469
# Deny all traffic from the IPv6 2001:db8::/32 to tcp port 25 on this host.
# Note that IPv6 must be enabled in /etc/default/ufw for IPv6 firewalling to work.
ufw: state=deny proto=tcp src=2001:db8::/32 port=25
ufw: rule=deny proto=tcp src=2001:db8::/32 port=25
'''
import platform
from operator import itemgetter
def main():
module = AnsibleModule(
argument_spec = dict(
default_policy = dict(default=None, choices=['allow', 'deny', 'reject'], required=False),
state = dict(default=None, aliases=['rule'], choices=['enable', 'disable', 'reload', 'reset', 'allow', 'deny', 'reject', 'limit'], required=False),
name = dict(default=None, required=False),
from_ip = dict(default='any', aliases=['src'], required=False),
from_port = dict(default='any', required=False),
to_ip = dict(default='any', aliases=['dest'], required=False),
to_port = dict(default='any', aliases=['port'], required=False),
proto = dict(default='any', choices=['any', 'tcp', 'udp', 'ipv6'], required=False),
delete = dict(default=False, choices=BOOLEANS, required=False),
log = dict(default=False, choices=BOOLEANS, required=False)
state = dict(default=None, choices=['enabled', 'disabled', 'reloaded', 'reseted']),
default = dict(default=None, aliases=['policy'], choices=['allow', 'deny', 'reject']),
logging = dict(default=None, choises=['on', 'off', 'low', 'medium', 'high', 'full']),
direction = dict(default=None, choises=['in', 'incoming', 'out', 'outgoing']),
delete = dict(default=False, choices=BOOLEANS),
rule = dict(default=None, choices=['allow', 'deny', 'reject', 'limit']),
interface = dict(default=None, aliases=['if']),
log = dict(default=False, choices=BOOLEANS),
from_ip = dict(default='any', aliases=['src', 'from']),
from_port = dict(default=None),
to_ip = dict(default='any', aliases=['dest', 'to']),
to_port = dict(default=None, aliases=['port']),
proto = dict(default=None, aliases=['protocol'], choices=['any', 'tcp', 'udp', 'ipv6']),
app = dict(default=None, aliases=['name'])
),
supports_check_mode = True
supports_check_mode = True,
mutually_exclusive = [['app', 'proto']]
)
default_policy = module.params.get('default_policy')
state = module.params.get('state')
name = module.params.get('name')
from_ip = module.params.get('from_ip')
from_port = module.params.get('from_port')
to_ip = module.params.get('to_ip')
to_port = module.params.get('to_port')
proto = module.params.get('proto')
delete = module.params['delete']
log = module.params['log']
system = platform.system()
if "Linux" not in system:
module.exit_json(msg="Not implemented for system %s. Only Linux (Ubuntu) is supported" % (system), changed=False)
else:
dist = platform.dist()
if dist and 'Ubuntu' not in dist[0]:
module.exit_json(msg="Not implemented for distrubution %s. Only Ubuntu is supported" % (dist[0]), changed=False)
result = {}
result['state'] = state
cmd = module.get_bin_path('ufw')
if module.check_mode:
cmd = cmd + ' --dry-run'
if default_policy:
if state:
module.fail_json(msg="'default_policy' and 'state' are mutually exclusive options.")
else:
if default_policy in ['allow', 'deny', 'reject']:
cmd = cmd + ' default %s' % (default_policy)
changed_marker = "Default incoming policy changed to '%s'\n(be sure to update your rules accordingly)" % (default_policy)
else:
module.fail_json(msg="Wrong default policy %s. See 'ansible-doc ufw' for usage." % (default_policy))
if not default_policy:
if not state:
module.fail_json(msg="You must specify either 'default_policy' or 'state' option.")
else:
if state in 'enable':
cmd = cmd + ' -f %s' % (state)
changed_marker = 'Firewall is active and enabled on system startup'
elif state in 'disable':
cmd = cmd + ' -f %s' % (state)
changed_marker = 'Firewall stopped and disabled on system startup'
elif state in 'reload':
cmd = cmd + ' -f %s' % (state)
changed_marker = 'Firewall reloaded'
elif state in 'reset':
cmd = cmd + ' -f %s' % (state)
changed_marker = 'Backing up'
elif state in ['allow', 'deny', 'reject', 'limit']:
changed_marker = ['Rules updated', 'Rules updated (v6)', 'Rule added', 'Rule added (v6)', 'Rule deleted', 'Rule deleted (v6)' ]
if delete:
cmd = cmd + ' delete'
cmd = cmd + ' %s' % (state)
if log:
cmd = cmd + ' log'
if name:
cmd = cmd + ' %s' % (name)
else:
if proto and proto not in 'any':
cmd = cmd + ' proto %s' % (proto)
if from_ip and from_ip not in 'any':
cmd = cmd + ' from %s' % (from_ip)
if from_port and from_port not in 'any':
cmd = cmd + ' port %s' % (from_port)
elif from_port and from_port not in 'any':
cmd = cmd + ' from port %s' % (from_port)
if to_ip:
cmd = cmd + ' to %s' % (to_ip)
if to_port and to_port not in 'any':
cmd = cmd + ' port %s' % (to_port)
elif to_port and to_port not in 'any':
cmd = cmd + ' to port %s' % (to_port)
else:
module.fail_json(msg="Wrong rule %s. See 'ansible-doc ufw' for usage." % (state))
cmds = []
def execute(cmd):
cmd = ' '.join(map(itemgetter(-1), filter(itemgetter(0), cmd)))
cmds.append(cmd)
(rc, out, err) = module.run_command(cmd)
if rc != 0:
if err:
module.fail_json(msg=err)
else:
module.fail_json(msg=out)
module.fail_json(msg=err or out)
result['cmd'] = cmd
result['msg'] = out.rstrip()
params = module.params
if isinstance(changed_marker, basestring):
result['changed'] = result['msg'] in changed_marker
else:
result['changed'] = any(item in result['msg'] for item in changed_marker)
# Ensure at least one of the command arguments are given
command_keys = ['state', 'default', 'rule', 'logging']
commands = dict((key, params[key]) for key in command_keys if params[key])
return module.exit_json(**result)
if len(commands) < 1:
module.fail_json(msg="Not any of the command arguments %s given" % commands)
# Ensure ufw is available
ufw_bin = module.get_bin_path('ufw', True)
# Save the pre state in order to recognize changes reliably
(_, pre_state, _) = module.run_command(ufw_bin + ' status verbose')
# Execute commands
for (command, value) in commands.iteritems():
cmd = [[ufw_bin], [module.check_mode, '--dry-run']]
if command == 'state':
states = { 'enabled': 'enable', 'disabled': 'disable',
'reloaded': 'reload', 'reseted': 'reset' }
execute(cmd + [['-f'], [states[value]]])
elif command == 'logging':
execute(cmd + [[command, value]])
elif command == 'default':
execute(cmd + [[command], [value], [params['direction']]])
elif command == 'rule':
# Rules are constructed according to the long format
#
# ufw [--dry-run] [delete] [insert NUM] allow|deny|reject|limit [in|out on INTERFACE] [log|log-all] \
# [from ADDRESS [port PORT]] [to ADDRESS [port PORT]] \
# [proto protocol] [app application]
cmd.append([module.boolean(params['delete']), 'delete'])
cmd.append([value])
cmd.append([module.boolean(params['log']), 'log'])
for (key, template) in [('direction', "%s" ), ('interface', "on %s" ),
('from_ip', "from %s" ), ('from_port', "port %s" ),
('to_ip', "to %s" ), ('to_port', "port %s" ),
('proto', "proto %s"), ('app', "app '%s'")]:
value = params[key]
cmd.append([value, template % (value)])
execute(cmd)
# Get the new state
(_, post_state, _) = module.run_command(ufw_bin + ' status verbose')
changed = pre_state != post_state
return module.exit_json(changed=changed, commands=cmds, msg=post_state.rstrip())
# include magic from lib/ansible/module_common.py
#<<INCLUDE_ANSIBLE_MODULE_COMMON>>