Merge lp:~michael.nelson/charms/trusty/elasticsearch/add-ufw into lp:~charmers/charms/trusty/elasticsearch/trunk

Proposed by Michael Nelson
Status: Merged
Approved by: Matt Bruzek
Approved revision: 38
Merge reported by: Matt Bruzek
Merged at revision: not available
Proposed branch: lp:~michael.nelson/charms/trusty/elasticsearch/add-ufw
Merge into: lp:~charmers/charms/trusty/elasticsearch/trunk
Diff against target: 414 lines (+324/-3)
7 files modified
README.md (+1/-1)
ansible_module_backports/ufw (+264/-0)
hooks/hooks.py (+8/-1)
playbook.yaml (+10/-0)
tasks/install-elasticsearch.yml (+4/-1)
tasks/setup-ufw.yml (+29/-0)
unit_tests/test_hooks.py (+8/-0)
To merge this branch: bzr merge lp:~michael.nelson/charms/trusty/elasticsearch/add-ufw
Reviewer Review Type Date Requested Status
Matt Bruzek (community) Approve
Kapil Thangavelu (community) Approve
Review via email: mp+225934@code.launchpad.net

Commit message

Firewall admin port 9200 so it's only available on the localhost or for any juju-related clients.

Description of the change

Firewall admin port 9200 so it's only available on the localhost or for any juju-related clients.

I've tested this with one of my deployments. I'll do a followup branch which firewalls the node-to-node communication port (9300) to be available only to peers.

The trusty version of ansible (1.5.2) doesn't include the ufw module, so I'm including it in the charm here (we could also grab it from the network during install, but don't want to depend on 3rd party networks). It seems like a nice way to get newer ansible functionality (modules) for our trusty charms, without having to install ansible from a ppa or similar.

To post a comment you must log in.
Revision history for this message
Kapil Thangavelu (hazmat) wrote :

ignoring expose and lack of open-port compatibility for now (es exposed publicly is bad juju). what happens on upgrade of an extant charm? afaics it will break them by installing ufw default deny mode without triggering reconsideration of the current clients. in general i've found this sort of add/one remove one pattern in response to relation events to be not ideal practice .. ie. just more complicated cause managing global state transitions and ordering, vs. just consider all the current ones and render the set / activate the delta ie a single entry point that all hooks can dispatch to.

37. By Michael Nelson

Reset firewall for each new client.

38. By Michael Nelson

Set client_relation_id as a var for simpler tasks.

Revision history for this message
Michael Nelson (michael.nelson) wrote :

Thanks Kapil.

I've updated so that it resets the firewall on install, upgrade-charm
and client-relation-joined/departed.

I wasn't particularly happy having to reset the firewall each time -
it would have been much nicer to instead be writing a templated config
file for ufw (ie. only reloading the firewall if the config changes),
but afaict it's not possible.

I've included a demo of an upgrade-charm pre-ufw to this on the followup MP at:

https://code.launchpad.net/~michael.nelson/charms/trusty/elasticsearch/ufw-for-peers-too/+merge/225968

Revision history for this message
Kapil Thangavelu (hazmat) wrote :

lgtm

review: Approve
Revision history for this message
Matt Bruzek (mbruzek) wrote :

I have tested this and the follow up MP. I was able to deploy and the firewall rules looked to be correct in an ELK deployment.

+1

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'README.md'
--- README.md 2014-07-28 18:10:43 +0000
+++ README.md 2014-07-30 06:36:30 +0000
@@ -33,7 +33,7 @@
3333
34And when they have started you can inspect the cluster health:34And when they have started you can inspect the cluster health:
3535
36 juju ssh elasticsearch/0 "curl http://localhost:9200/_cat/health?v"36 juju run --unit elasticsearch/0 "curl http://localhost:9200/_cat/health?v"
37 epoch timestamp cluster status node.total node.data shards ...37 epoch timestamp cluster status node.total node.data shards ...
38 1404728290 10:18:10 elasticsearch green 2 2 038 1404728290 10:18:10 elasticsearch green 2 2 0
3939
4040
=== added directory 'ansible_module_backports'
=== added file 'ansible_module_backports/ufw'
--- ansible_module_backports/ufw 1970-01-01 00:00:00 +0000
+++ ansible_module_backports/ufw 2014-07-30 06:36:30 +0000
@@ -0,0 +1,264 @@
1#!/usr/bin/python
2# -*- coding: utf-8 -*-
3
4# (c) 2014, Ahti Kitsik <ak@ahtik.com>
5# (c) 2014, Jarno Keskikangas <jarno.keskikangas@gmail.com>
6# (c) 2013, Aleksey Ovcharenko <aleksey.ovcharenko@gmail.com>
7# (c) 2013, James Martin <jmartin@basho.com>
8#
9# This file is part of Ansible
10#
11# Ansible is free software: you can redistribute it and/or modify
12# it under the terms of the GNU General Public License as published by
13# the Free Software Foundation, either version 3 of the License, or
14# (at your option) any later version.
15#
16# Ansible is distributed in the hope that it will be useful,
17# but WITHOUT ANY WARRANTY; without even the implied warranty of
18# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
19# GNU General Public License for more details.
20#
21# You should have received a copy of the GNU General Public License
22# along with Ansible. If not, see <http://www.gnu.org/licenses/>.
23
24DOCUMENTATION = '''
25---
26module: ufw
27short_description: Manage firewall with UFW
28description:
29 - Manage firewall with UFW.
30version_added: 1.6
31author: Aleksey Ovcharenko, Jarno Keskikangas, Ahti Kitsik
32notes:
33 - See C(man ufw) for more examples.
34requirements:
35 - C(ufw) package
36options:
37 state:
38 description:
39 - C(enabled) reloads firewall and enables firewall on boot.
40 - C(disabled) unloads firewall and disables firewall on boot.
41 - C(reloaded) reloads firewall.
42 - C(reset) disables and resets firewall to installation defaults.
43 required: false
44 choices: ['enabled', 'disabled', 'reloaded', 'reset']
45 policy:
46 description:
47 - Change the default policy for incoming or outgoing traffic.
48 required: false
49 alias: default
50 choices: ['allow', 'deny', 'reject']
51 direction:
52 description:
53 - Select direction for a rule or default policy command.
54 required: false
55 choices: ['in', 'out', 'incoming', 'outgoing']
56 logging:
57 description:
58 - Toggles logging. Logged packets use the LOG_KERN syslog facility.
59 choices: ['on', 'off', 'low', 'medium', 'high', 'full']
60 required: false
61 insert:
62 description:
63 - Insert the corresponding rule as rule number NUM
64 required: false
65 rule:
66 description:
67 - Add firewall rule
68 required: false
69 choices: ['allow', 'deny', 'reject', 'limit']
70 log:
71 description:
72 - Log new connections matched to this rule
73 required: false
74 choices: ['yes', 'no']
75 from_ip:
76 description:
77 - Source IP address.
78 required: false
79 aliases: ['from', 'src']
80 default: 'any'
81 from_port:
82 description:
83 - Source port.
84 required: false
85 to_ip:
86 description:
87 - Destination IP address.
88 required: false
89 aliases: ['to', 'dest']
90 default: 'any'
91 to_port:
92 description:
93 - Destination port.
94 required: false
95 aliases: ['port']
96 proto:
97 description:
98 - TCP/IP protocol.
99 choices: ['any', 'tcp', 'udp', 'ipv6', 'esp', 'ah']
100 required: false
101 name:
102 description:
103 - Use profile located in C(/etc/ufw/applications.d)
104 required: false
105 aliases: ['app']
106 delete:
107 description:
108 - Delete rule.
109 required: false
110 choices: ['yes', 'no']
111'''
112
113EXAMPLES = '''
114# Allow everything and enable UFW
115ufw: state=enabled policy=allow
116
117# Set logging
118ufw: logging=on
119
120# Sometimes it is desirable to let the sender know when traffic is
121# being denied, rather than simply ignoring it. In these cases, use
122# reject instead of deny. In addition, log rejected connections:
123ufw: rule=reject port=auth log=yes
124
125# ufw supports connection rate limiting, which is useful for protecting
126# against brute-force login attacks. ufw will deny connections if an IP
127# address has attempted to initiate 6 or more connections in the last
128# 30 seconds. See http://www.debian-administration.org/articles/187
129# for details. Typical usage is:
130ufw: rule=limit port=ssh proto=tcp
131
132# Allow OpenSSH
133ufw: rule=allow name=OpenSSH
134
135# Delete OpenSSH rule
136ufw: rule=allow name=OpenSSH delete=yes
137
138# Deny all access to port 53:
139ufw: rule=deny port=53
140
141# Allow all access to tcp port 80:
142ufw: rule=allow port=80 proto=tcp
143
144# Allow all access from RFC1918 networks to this host:
145ufw: rule=allow src={{ item }}
146with_items:
147- 10.0.0.0/8
148- 172.16.0.0/12
149- 192.168.0.0/16
150
151# Deny access to udp port 514 from host 1.2.3.4:
152ufw: rule=deny proto=udp src=1.2.3.4 port=514
153
154# Allow incoming access to eth0 from 1.2.3.5 port 5469 to 1.2.3.4 port 5469
155ufw: rule=allow interface=eth0 direction=in proto=udp src=1.2.3.5 from_port=5469 dest=1.2.3.4 to_port=5469
156
157# Deny all traffic from the IPv6 2001:db8::/32 to tcp port 25 on this host.
158# Note that IPv6 must be enabled in /etc/default/ufw for IPv6 firewalling to work.
159ufw: rule=deny proto=tcp src=2001:db8::/32 port=25
160'''
161
162from operator import itemgetter
163
164
165def main():
166 module = AnsibleModule(
167 argument_spec = dict(
168 state = dict(default=None, choices=['enabled', 'disabled', 'reloaded', 'reset']),
169 default = dict(default=None, aliases=['policy'], choices=['allow', 'deny', 'reject']),
170 logging = dict(default=None, choices=['on', 'off', 'low', 'medium', 'high', 'full']),
171 direction = dict(default=None, choices=['in', 'incoming', 'out', 'outgoing']),
172 delete = dict(default=False, type='bool'),
173 insert = dict(default=None),
174 rule = dict(default=None, choices=['allow', 'deny', 'reject', 'limit']),
175 interface = dict(default=None, aliases=['if']),
176 log = dict(default=False, type='bool'),
177 from_ip = dict(default='any', aliases=['src', 'from']),
178 from_port = dict(default=None),
179 to_ip = dict(default='any', aliases=['dest', 'to']),
180 to_port = dict(default=None, aliases=['port']),
181 proto = dict(default=None, aliases=['protocol'], choices=['any', 'tcp', 'udp', 'ipv6', 'esp', 'ah']),
182 app = dict(default=None, aliases=['name'])
183 ),
184 supports_check_mode = True,
185 mutually_exclusive = [['app', 'proto', 'logging']]
186 )
187
188 cmds = []
189
190 def execute(cmd):
191 cmd = ' '.join(map(itemgetter(-1), filter(itemgetter(0), cmd)))
192
193 cmds.append(cmd)
194 (rc, out, err) = module.run_command(cmd)
195
196 if rc != 0:
197 module.fail_json(msg=err or out)
198
199 params = module.params
200
201 # Ensure at least one of the command arguments are given
202 command_keys = ['state', 'default', 'rule', 'logging']
203 commands = dict((key, params[key]) for key in command_keys if params[key])
204
205 if len(commands) < 1:
206 module.fail_json(msg="Not any of the command arguments %s given" % commands)
207
208 if('interface' in params and 'direction' not in params):
209 module.fail_json(msg="Direction must be specified when creating a rule on an interface")
210
211 # Ensure ufw is available
212 ufw_bin = module.get_bin_path('ufw', True)
213
214 # Save the pre state and rules in order to recognize changes
215 (_, pre_state, _) = module.run_command(ufw_bin + ' status verbose')
216 (_, pre_rules, _) = module.run_command("grep '^### tuple' /lib/ufw/user*.rules")
217
218 # Execute commands
219 for (command, value) in commands.iteritems():
220 cmd = [[ufw_bin], [module.check_mode, '--dry-run']]
221
222 if command == 'state':
223 states = { 'enabled': 'enable', 'disabled': 'disable',
224 'reloaded': 'reload', 'reset': 'reset' }
225 execute(cmd + [['-f'], [states[value]]])
226
227 elif command == 'logging':
228 execute(cmd + [[command], [value]])
229
230 elif command == 'default':
231 execute(cmd + [[command], [value], [params['direction']]])
232
233 elif command == 'rule':
234 # Rules are constructed according to the long format
235 #
236 # ufw [--dry-run] [delete] [insert NUM] allow|deny|reject|limit [in|out on INTERFACE] [log|log-all] \
237 # [from ADDRESS [port PORT]] [to ADDRESS [port PORT]] \
238 # [proto protocol] [app application]
239 cmd.append([module.boolean(params['delete']), 'delete'])
240 cmd.append([params['insert'], "insert %s" % params['insert']])
241 cmd.append([value])
242 cmd.append([module.boolean(params['log']), 'log'])
243
244 for (key, template) in [('direction', "%s" ), ('interface', "on %s" ),
245 ('from_ip', "from %s" ), ('from_port', "port %s" ),
246 ('to_ip', "to %s" ), ('to_port', "port %s" ),
247 ('proto', "proto %s"), ('app', "app '%s'")]:
248
249 value = params[key]
250 cmd.append([value, template % (value)])
251
252 execute(cmd)
253
254 # Get the new state
255 (_, post_state, _) = module.run_command(ufw_bin + ' status verbose')
256 (_, post_rules, _) = module.run_command("grep '^### tuple' /lib/ufw/user*.rules")
257 changed = (pre_state != post_state) or (pre_rules != post_rules)
258
259 return module.exit_json(changed=changed, commands=cmds, msg=post_state.rstrip())
260
261# import module snippets
262from ansible.module_utils.basic import *
263
264main()
0265
=== added symlink 'hooks/client-relation-departed'
=== target is u'hooks.py'
=== modified file 'hooks/hooks.py'
--- hooks/hooks.py 2014-07-07 10:22:27 +0000
+++ hooks/hooks.py 2014-07-30 06:36:30 +0000
@@ -4,6 +4,7 @@
4import sys4import sys
5import charmhelpers.contrib.ansible5import charmhelpers.contrib.ansible
6import charmhelpers.payload.execd6import charmhelpers.payload.execd
7import charmhelpers.core.host
78
89
9hooks = charmhelpers.contrib.ansible.AnsibleHooks(10hooks = charmhelpers.contrib.ansible.AnsibleHooks(
@@ -17,8 +18,8 @@
17 'start',18 'start',
18 'stop',19 'stop',
19 'upgrade-charm',20 'upgrade-charm',
20 'client-relation-changed',
21 'client-relation-joined',21 'client-relation-joined',
22 'client-relation-departed',
22 ])23 ])
2324
2425
@@ -30,6 +31,12 @@
30 charmhelpers.contrib.ansible.install_ansible_support(31 charmhelpers.contrib.ansible.install_ansible_support(
31 from_ppa=False)32 from_ppa=False)
3233
34 # We copy the backported ansible modules here because they need to be
35 # in place by the time ansible runs any hook.
36 charmhelpers.core.host.rsync(
37 'ansible_module_backports',
38 '/usr/share/ansible')
39
3340
34if __name__ == "__main__":41if __name__ == "__main__":
35 hooks.execute(sys.argv)42 hooks.execute(sys.argv)
3643
=== modified file 'playbook.yaml'
--- playbook.yaml 2014-07-16 18:38:22 +0000
+++ playbook.yaml 2014-07-30 06:36:30 +0000
@@ -10,9 +10,19 @@
10 - name: Restart ElasticSearch10 - name: Restart ElasticSearch
11 service: name=elasticsearch state=restarted11 service: name=elasticsearch state=restarted
1212
13 vars:
14 - service_name: "{{ local_unit.split('/')[0] }}"
15 - client_relation_id: "{{ relations['client'].keys()[0] | default('') }}"
16
13 tasks:17 tasks:
1418
15 - include: tasks/install-elasticsearch.yml19 - include: tasks/install-elasticsearch.yml
20 - include: tasks/setup-ufw.yml
21 tags:
22 - install
23 - upgrade-charm
24 - client-relation-joined
25 - client-relation-departed
16 - include: tasks/peer-relations.yml26 - include: tasks/peer-relations.yml
1727
18 - name: Update configuration28 - name: Update configuration
1929
=== modified file 'tasks/install-elasticsearch.yml'
--- tasks/install-elasticsearch.yml 2014-05-27 14:22:40 +0000
+++ tasks/install-elasticsearch.yml 2014-07-30 06:36:30 +0000
@@ -17,10 +17,13 @@
17 when: apt_repository != ""17 when: apt_repository != ""
1818
19- name: Install dependent packages.19- name: Install dependent packages.
20 apt: pkg=openjdk-7-jre-headless state=latest update_cache=yes20 apt: pkg={{ item }} state=latest update_cache=yes
21 tags:21 tags:
22 - install22 - install
23 - upgrade-charm23 - upgrade-charm
24 with_items:
25 - openjdk-7-jre-headless
26 - ufw
2427
25- name: Check for local elasticsearch.deb in payload.28- name: Check for local elasticsearch.deb in payload.
26 stat: path=files/elasticsearch.deb29 stat: path=files/elasticsearch.deb
2730
=== added file 'tasks/setup-ufw.yml'
--- tasks/setup-ufw.yml 1970-01-01 00:00:00 +0000
+++ tasks/setup-ufw.yml 2014-07-30 06:36:30 +0000
@@ -0,0 +1,29 @@
1# XXX 2014-07-08 michael nelson ip6 not supported on image (?)
2# ufw errors unless you switch off ipv6 support. Not sure if it's
3# related to the kernel used on the cloud image, but the actual
4# error is:
5# ip6tables v1.4.21: can't initialize ip6tables table `filter':
6# Table does not exist (do you need to insmod?)
7# Perhaps ip6tables or your kernel needs to be upgraded.
8- name: Update ufw config to avoid error
9 lineinfile: dest=/etc/default/ufw
10 regexp="^IPV6=yes$"
11 line="IPV6=no"
12
13# XXX 2014-07-30 michael nelson: It'd be much nicer if we could
14# just render a config file for ufw, as it would be idempotent.
15# As it is, there isn't a way to do that (afaics), so instead we
16# reset the firewall rules each time based on the current clients.
17- name: Reset firewall
18 ufw: state=reset policy=allow logging=on
19
20- name: Turn on fire wall with logging.
21 ufw: state=enabled policy=allow logging=on
22
23- name: Open the firewall for all clients
24 ufw: rule=allow src={{ item.value['private-address'] }} port=9200 proto=tcp
25 with_dict: relations["client"]["{{ client_relation_id }}"] | default({})
26 when: not item.key.startswith(service_name)
27
28- name: Deny all other requests on 9200
29 ufw: rule=deny port=9200
030
=== modified file 'unit_tests/test_hooks.py'
--- unit_tests/test_hooks.py 2014-07-07 10:22:27 +0000
+++ unit_tests/test_hooks.py 2014-07-30 06:36:30 +0000
@@ -44,6 +44,14 @@
44 execd = self.mock_charmhelpers.payload.execd44 execd = self.mock_charmhelpers.payload.execd
45 execd.execd_preinstall.assert_called_once_with()45 execd.execd_preinstall.assert_called_once_with()
4646
47 def test_copys_backported_ansible_modules(self):
48 hooks.execute(['install'])
49
50 rsync = self.mock_charmhelpers.core.host.rsync
51 rsync.assert_called_once_with(
52 'ansible_module_backports',
53 '/usr/share/ansible')
54
4755
48class DefaultHooksTestCase(unittest.TestCase):56class DefaultHooksTestCase(unittest.TestCase):
4957

Subscribers

People subscribed via source and target branches

to all changes: