Skip to content

Commit 52a582f

Browse files
committed
v0.1.7: Added the cloudflare_threat_control LWRP
Based off an initial work by Adrien Siebert (https://github.com/asiebert) This LWRP allows to manage Cloudflare's threat control services Also adapted the Vagrantfile to make it possible to use Chef-Zero instead of solo. However, I'll need to switch to Test-KItchen pretty soon as the vagrant-chef-zero plugin is going to be deprecated pretty soon.
1 parent 0a1368d commit 52a582f

File tree

15 files changed

+208
-58
lines changed

15 files changed

+208
-58
lines changed

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,4 @@
11
/.vagrant
2+
/.bundle
3+
/vendor
4+
/.zero-knife.rb

Berksfile.lock

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,4 @@ DEPENDENCIES
44
metadata: true
55

66
GRAPH
7-
cloudflare (0.1.6)
7+
cloudflare (0.1.7)

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@
22

33
This file is used to list changes made in each version of cloudflare.
44

5+
## 0.1.7:
6+
7+
* Added the `threat_control` LWRP allowing to whitelist or blacklist IPs on CloudFlare (see https://support.cloudflare.com/hc/en-us/articles/200171266-How-do-I-block-or-trust-visitors-in-Threat-Control-)
8+
59
## 0.1.6:
610

711
* Added the `shared_A_record` attribute to the LWRP to make it possible to have several A records with the same name (aka DNS load balancing)

README.md

Lines changed: 50 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
Description
22
===========
33

4-
This Chef! cookbook defines one LWRP that you can easily use in your own cookbook to create and delete Cloudflare DNS records.
4+
This Chef! cookbook defines one LWRP that you can easily use in your own cookbook to create and delete Cloudflare DNS records and manage your threat control services.
55

66
It is built on top of B4k3r's Ruby wrapper for the Cloudflare API. (https://github.com/B4k3r/cloudflare)
77

@@ -27,6 +27,8 @@ There also are a number of optional node attributes:
2727

2828
Those attributes come in especially handy if you have a number of servers and get throttled by Cloudflare's API limits.
2929

30+
A couple of threat-control-related attributes are explained in the "`threat_control` Resource" section below.
31+
3032
Usage
3133
=====
3234

@@ -64,10 +66,43 @@ Another example:
6466

6567
would delete the `server_name.example.com` record from your Cloudflare account.
6668

69+
`threat_control` Resource
70+
-------------------------
71+
CloudFlare's threat control can be used to whitelist or blacklist IPs hitting your domains going through their network.
72+
cf. [CloudFlare FAQ - How do I block or trust visitors in Threat Control?](https://support.cloudflare.com/hc/en-us/articles/200171266-How-do-I-block-or-trust-visitors-in-Threat-Control-)
73+
74+
Attribute:
75+
76+
* `ip` (optional): the IP you want to white/blacklist (defaults to `node.ipaddress`)
77+
78+
Actions: `:whitelist`, `:blacklist`, `:remove_ip`
79+
Should be self-explanatory, the latter one being used to remove a white/blacklisted IP from their respective list.
80+
Note that the default action is `:nothing`!
81+
82+
Examples:
83+
84+
cloudflare_threat_control 'whitelist_current_server' do
85+
action :whitelist
86+
end
87+
88+
cloudflare_threat_control 'shall_we_trust_this?' do
89+
ip '208.73.210.203'
90+
action :blacklist
91+
end
92+
93+
A word on how this LWRP works: we don't want to have Chef hit Cloudflare's API every time it runs, for Cloudflare's API usage thresholds are pretty low. The thing is, unlike DNS records (for which we can query a DNS server instead of the API), there's no way to double-check the current status without making API calls.
94+
To get around this, this LWRP caches the current status (and trusts that the cached information is valid) for a while, which is reasonable if your Chef recipe is the only way this should ever be modified in your setup. The cache's validity is controlled by the `['cloudflare']['threat_control']['cache_duration']` node attribute, which defaults to 1 day. Note this attribute expects this duration in days, but does accept floats (so you can set it to `1.0 / 24.0` to reduce it to one hour).
95+
96+
If you happen to have few enough servers that you don't care about Cloudflare's API usage thresholds, or if you really want your recipe to make calls to the API at every run, you can set the `['cloudflare']['threat_control']['disable_cache']` node attribute to `true` (defaults to `false`).
97+
98+
A caveat worth noting with this resource is that [Cloudflare's API](https://www.cloudflare.com/docs/client-api.html) as of today (08/18/14) offers no way to check the current status of a given IP w.r.t threat control settings, nor does it give any information when making a call to set an IP's status regarding its previous status. As a result, _this resource will be marked as updated whenever an API call is made, even if the status wasn't actually changed_.
99+
100+
Note that you can't use the cache with Chef solo, as there's nowhere to save the information to. [Chef-zero](https://github.com/opscode/chef-zero) will do nicely though if you don't have a Chef server around.
101+
67102
Example recipe
68103
==============
69104

70-
You can have a look at the `cloudflare::example` recipe for examples on how to use the LWRPs.
105+
You can have a look at the `cloudflare::example` recipe for examples on how to use the DNS-related LWRPs.
71106

72107
You can also test my cookbook with Vagrant (see the 'Vagrant' section below).
73108

@@ -85,10 +120,14 @@ You also need to define 3 environment variables to be able to use my Vagrantfile
85120
You can do so by typing e.g. `export CLOUDFLARE_EMAIL='[email protected]'` and so on in your shell.
86121

87122
Be aware that the example recipe will then proceed to create a few DNS records on that DNS zone with your credentials, so use with caution! That being said, all said records will start with 'cl-cb-test-' so they have little chance of clonflicting with exisiting records on your account.
123+
Also, it does white-black list a couple of private IP addresses.
88124

89125
You can also easily clean up the test records created that way by running `CLOUDFLARE_CLEANUP=1 vagrant provision`.
90126

91-
Then playing with this cookbook should be as easy as running `bundle install && vagrant up`!
127+
Then playing with this cookbook should be as easy as running `bundle install --path vendor/bundle && vagrant up`!
128+
129+
Note that if you want to test/do stuff on the caching mechanism for the threat control LWRP, you'll need to use Chef-zero. For now this project uses the [Vagrant-Chef-Zero](https://github.com/andrewgross/vagrant-chef-zero) plugin, but we'll soon migrate to Test Kitchen.
130+
To use the Chef-Zero plugin, simply install it, the Vagrantfile will pick it up automatically.
92131

93132
Contributing & Feedback
94133
=======================
@@ -99,6 +138,9 @@ Feel free to reach me at <[email protected]>
99138
Changes
100139
=======
101140

141+
* 0.1.7 (Aug 18, 2014)
142+
* Added the `threat_control` LWRP allowing to whitelist or blacklist IPs on CloudFlare (see https://support.cloudflare.com/hc/en-us/articles/200171266-How-do-I-block-or-trust-visitors-in-Threat-Control-)
143+
102144
* 0.1.6 (Jul 9, 2014)
103145
* Added the `shared_A_record` attribute to the LWRP to make it possible to have several A records with the same name (aka DNS load balancing)
104146
* Properly updating LWRP states when an action has been performed
@@ -124,3 +166,8 @@ Changes
124166

125167
* 0.1.0 (Oct 3, 2013)
126168
* Initial release
169+
170+
Contributors
171+
============
172+
173+
* [Adrien Siebert](https://github.com/asiebert)

Vagrantfile

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,13 +21,26 @@ Vagrant.configure(VAGRANTFILE_API_VERSION) do |config|
2121

2222
config.berkshelf.enabled = true
2323

24-
config.vm.provision :chef_solo do |chef|
24+
# we use chef-zero instead of solo if the plugin is available
25+
# that makes it easy to test the caching mechanism for the
26+
# threat_control resource
27+
if Vagrant.has_plugin? 'vagrant-chef-zero'
28+
provisioner = :chef_client
29+
else
30+
provisioner = :chef_solo
31+
end
32+
33+
config.vm.provision provisioner do |chef|
2534
chef.json = {
2635
'cloudflare' => {
2736
'credentials' => {
2837
'email' => CLOUDFLARE_EMAIL,
2938
'api_key' => CLOUDFLARE_API_KEY
3039
},
40+
'threat_control' => {
41+
# one minute, to be able to test quickly
42+
'cache_duration' => 1.0 / (24.0 * 60.0)
43+
},
3144
'example_zone' => CLOUDFLARE_ZONE,
3245
'debug' => true
3346
}

attributes/default.rb

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,3 +16,16 @@
1616
# If you set the attribute above to true, that's the DNS server we're going
1717
# to ask - defaults to Cloudflare's main public DNS server
1818
default['cloudflare']['DNS_server'] = 'ns.cloudflare.com'
19+
20+
21+
# Interval during which the threat control caching in node's attributes remains valid
22+
# In days, as a float
23+
default['cloudflare']['threat_control']['cache_duration'] = 1.0
24+
# If set to true, we won't care about the cached information - be aware that will
25+
# result in quite a lot of API calls (one per resource and per chef run)
26+
default['cloudflare']['threat_control']['disable_cache'] = false
27+
28+
# Do not edit this manually, this is where this cookbook caches the threat-control
29+
# statuses
30+
# For the record, it maps IPs to a hash with the 'status' and 'datetime' keys
31+
default['cloudflare']['threat_control']['status_cache'] = {}

libraries/provider.rb

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
class Chef::Provider
2+
3+
# this needs to be done at run time, not compile time
4+
def load_cloudflare_cookbook_gems
5+
return if defined? @@cloudflare_cookbook_gems_loaded
6+
7+
chef_gem 'cloudflare' do
8+
action :nothing
9+
version '2.0.1' # TODO update to latest 2.0.2
10+
end.run_action(:install, :immediately)
11+
12+
require 'resolv'
13+
require 'cloudflare'
14+
require 'date'
15+
16+
@@cloudflare_cookbook_gems_loaded = true
17+
end
18+
19+
end

metadata.json

Lines changed: 1 addition & 29 deletions
Large diffs are not rendered by default.

metadata.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,4 @@
44
license 'unlicense'
55
description 'Registers your server with Cloudflare\'s DNS service'
66
long_description IO.read(File.join(File.dirname(__FILE__), 'README.md'))
7-
version '0.1.6'
7+
version '0.1.7'

providers/dns_record.rb

Lines changed: 0 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -39,18 +39,3 @@
3939
Chef::Log.info "No DNS record named #{new_resource.complete_url} found, can't delete"
4040
end
4141
end
42-
43-
private
44-
45-
# this needs to be done at run time, not compile time
46-
def load_cloudflare_cookbook_gems
47-
return if defined? @@cloudflare_cookbook_gems_loaded
48-
49-
chef_gem 'cloudflare' do
50-
action :nothing
51-
version '2.0.1'
52-
end.run_action(:install, :immediately)
53-
require 'resolv'
54-
require 'cloudflare'
55-
@@cloudflare_cookbook_gems_loaded = true
56-
end

providers/threat_control.rb

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
action :whitelist do
2+
action_for_status :whitelist
3+
end
4+
5+
action :blacklist do
6+
action_for_status :blacklist
7+
end
8+
9+
action :remove_ip do
10+
action_for_status :remove_ip
11+
end
12+
13+
private
14+
15+
# it's the same logic for all the actions, so let's DRY that up
16+
def action_for_status status
17+
load_cloudflare_cookbook_gems
18+
ip = new_resource.ip
19+
20+
if trust_cache? && status_from_cache(ip) == status
21+
Chef::Log.info "[CF] Cache says that #{ip} is already in status #{status}, nothing to do"
22+
else
23+
# we need to make the actual call to the API
24+
new_resource.set_status status
25+
Chef::Log.info "[CF] #{ip}'s threat control was succesfully set to #{status}"
26+
27+
# no harm in caching even if the cache isn't used
28+
set_cache ip, status
29+
30+
# as noted in the README, we can't know whether the status
31+
# really was updated with Cloudflare, so...
32+
new_resource.updated_by_last_action true
33+
end
34+
end
35+
36+
# we trust the cache iff it's not disabled and we're not running chef-solo
37+
def trust_cache?
38+
!node['cloudflare']['threat_control']['disable_cache'] \
39+
&& !Chef::Config[:solo]
40+
end
41+
42+
# returns the cache's status for that IP
43+
# if the cache is stale, it removes that entry
44+
# and returns `:none`
45+
def status_from_cache ip
46+
clean_cache
47+
node['cloudflare']['threat_control']['status_cache'].fetch(ip)['status'].to_sym
48+
rescue KeyError
49+
# not found
50+
:none
51+
end
52+
53+
def is_stale? str_datetime
54+
DateTime.now > DateTime.strptime(str_datetime) + node['cloudflare']['threat_control']['cache_duration']
55+
end
56+
57+
def set_cache ip, status
58+
node.normal['cloudflare']['threat_control']['status_cache'][ip] = {
59+
'datetime' => DateTime.now.strftime,
60+
'status' => status
61+
}
62+
end
63+
64+
# we clean the whole cache every time we use it
65+
# might look a tad inefficient, but keeps code simple
66+
# and avoids lingering obsolete values
67+
def clean_cache
68+
node.normal['cloudflare']['threat_control']['status_cache'].reject! { |ip, data| is_stale? data['datetime'] }
69+
end

recipes/example-cleanup.rb

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,10 @@
88
action :delete
99
end
1010
end
11+
12+
['172.20.126.126', '172.20.127.127'].each do |ip|
13+
cloudflare_threat_control "remove-threat-control-#{ip}" do
14+
ip ip
15+
action :remove_ip
16+
end
17+
end

recipes/example.rb

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,3 +76,17 @@
7676
content 'getchef.com'
7777
record_name "#{prefix}-conflicting-CNAME"
7878
end
79+
80+
##
81+
## Threat control testing
82+
##
83+
84+
cloudflare_threat_control 'whitelist_test_server_a' do
85+
ip '172.20.126.126'
86+
action :whitelist
87+
end
88+
89+
cloudflare_threat_control 'blacklist_test_server_b' do
90+
ip '172.20.127.127'
91+
action :blacklist
92+
end

resources/dns_record.rb

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
attribute :name, :name_attribute => true, :kind_of => String, :required => true
55
attribute :record_name, :kind_of => [String, FalseClass], :default => false
66
attribute :zone, :kind_of => String, :required => true
7-
attribute :content, :kind_of => [String, FalseClass], :default => false
7+
attribute :content, :kind_of => String, :default => node.ipaddress
88
attribute :type, :kind_of => String, :equal_to => ['A', 'CNAME'], :default => 'A'
99
attribute :ttl, :kind_of => Fixnum, :default => 1
1010
attribute :shared_A_record, :kind_of => [TrueClass, FalseClass], :default => false
@@ -92,13 +92,6 @@ def zone *args
9292
old_zone *args
9393
end
9494

95-
alias_method :old_content, :content
96-
def content *args
97-
# we default to the node's ipaddress if no content was explicitely given
98-
@content = node.ipaddress if !@content
99-
old_content *args
100-
end
101-
10295
def shared_A_record?
10396
type == 'A' && shared_A_record
10497
end

resources/threat_control.rb

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
actions :whitelist, :blacklist, :remove_ip
2+
default_action :nothing
3+
4+
attribute :ip, :kind_of => String, :default => node.ipaddress
5+
6+
def set_status status
7+
response = node.cloudflare_client.send status, ip
8+
if response['result'] != 'success'
9+
Chef::Application.fatal! "Unable to set threat control status to #{status} for ip #{ip} : Cloudflare's API returned #{response}"
10+
end
11+
end

0 commit comments

Comments
 (0)