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

Subscribers

People subscribed via source and target branches

to all changes: