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 #!/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:
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: 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: 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 required: false
default: None choices: ['enabled', 'disabled', 'reloaded', 'reseted']
version_added: "2.1" 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: 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')
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))
def execute(cmd):
cmd = ' '.join(map(itemgetter(-1), filter(itemgetter(0), cmd)))
cmds.append(cmd)
(rc, out, err) = module.run_command(cmd) (rc, out, err) = module.run_command(cmd)
if rc != 0: if rc != 0:
if err: module.fail_json(msg=err or out)
module.fail_json(msg=err)
else:
module.fail_json(msg=out)
result['cmd'] = cmd params = module.params
result['msg'] = out.rstrip()
if isinstance(changed_marker, basestring): # Ensure at least one of the command arguments are given
result['changed'] = result['msg'] in changed_marker command_keys = ['state', 'default', 'rule', 'logging']
else: commands = dict((key, params[key]) for key in command_keys if params[key])
result['changed'] = any(item in result['msg'] for item in changed_marker)
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 magic from lib/ansible/module_common.py
#<<INCLUDE_ANSIBLE_MODULE_COMMON>> #<<INCLUDE_ANSIBLE_MODULE_COMMON>>