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
pull/5518/head
Jarno Keskikangas 11 years ago
parent 651c04a3ec
commit f4e8a86c87

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

Loading…
Cancel
Save