smart_proxy_vault 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/README.md ADDED
@@ -0,0 +1,152 @@
1
+ [![Build Status](https://img.shields.io/travis/visioncritical/smart_proxy_vault/master.svg)](https://travis-ci.org/visioncritical/smart_proxy_vault)
2
+ [![Code Quality](https://img.shields.io/codeclimate/github/visioncritical/smart_proxy_vault.svg)](https://codeclimate.com/github/visioncritical/smart_proxy_vault)
3
+ [![Code Climate](https://img.shields.io/codeclimate/coverage/github/visioncritical/smart_proxy_vault.svg)](https://codeclimate.com/github/visioncritical/smart_proxy_vault/coverage)
4
+ [![Gem](https://img.shields.io/gem/v/smart_proxy_vault.svg)](https://rubygems.org/gems/smart_proxy_vault/versions)
5
+ [![GitHub license](https://img.shields.io/badge/license-GPLv3-blue.svg)](./LICENSE)
6
+
7
+
8
+ # Smart Proxy - Vault Plugin
9
+
10
+ A Smart Proxy plugin will return a Vault token after authenticating a client.
11
+
12
+ ## Design
13
+
14
+ The authentication portion of this plugin has been designed to be modular. Below is a current list of clients this plugin knows how to authenticate:
15
+
16
+ * Chef
17
+
18
+ If you're unable to use one of the above to authenticate your clients, you can always write your own & submit a PR (see [DEVELOPMENT.md](documentation/DEVELOPMENT.md)).
19
+
20
+ ## Installation
21
+
22
+ Add this line to your Smart Proxy bundler.d/vault.rb gemfile:
23
+
24
+ ```ruby
25
+ gem 'smart_proxy_vault'
26
+ ```
27
+
28
+ And then execute:
29
+
30
+ ```bash
31
+ bundle install
32
+ ```
33
+
34
+ ## Settings
35
+
36
+ Example:
37
+
38
+ ```yaml
39
+ ---
40
+ :enabled: true
41
+ :auth_backend: 'chef'
42
+ :vault:
43
+ :address: "https://vault.example.com"
44
+ :token: "UUID"
45
+ :ssl_verify: true
46
+ :add_token_metadata: true
47
+ :token_options:
48
+ :policies: ['policyname']
49
+ :ttl: '72h'
50
+ :chef:
51
+ :endpoint: 'https://chef.example.com'
52
+ :client: 'user'
53
+ :key: '/path/to/client.pem'
54
+ :ssl_verify: true
55
+ ```
56
+
57
+ #### General
58
+
59
+ #####:enabled:
60
+
61
+ Toggles whether or not this plugin is enabled for Smart Proxy.
62
+
63
+ #####:auth_backend:
64
+
65
+ Specifies what authentication module you would like to use to authenticate your clients (must correspond to a filename in [lib/smart_proxy_vault/authentication/](lib/smart_proxy_vault/authentication/))
66
+
67
+ #####:vault:
68
+
69
+ A hash of Vault settings that are used to configure a connection to the Vault server (determined by the [Vault](https://github.com/hashicorp/vault-ruby) gem).
70
+
71
+ ```yaml
72
+ # https://github.com/hashicorp/vault-ruby/blob/master/lib/vault/configurable.rb
73
+ :vault:
74
+ :address:
75
+ :token:
76
+ :open_timeout:
77
+ :proxy_address:
78
+ :proxy_password:
79
+ :proxy_port:
80
+ :proxy_username:
81
+ :read_timeout:
82
+ :ssl_ciphers:
83
+ :ssl_pem_file:
84
+ :ssl_pem_passphrase:
85
+ :ssl_ca_cert:
86
+ :ssl_ca_path:
87
+ :ssl_verify:
88
+ :ssl_timeout:
89
+ :timeout:
90
+ ```
91
+
92
+ #####:add_token_metadata:
93
+
94
+ If set to true, this plugin will add the requesting client's ID (as determined by the auth_backend) in the metadata & display-name fields when requesting a token.
95
+
96
+ #####:token_options:
97
+
98
+ A hash of parameters that will be passed to the token creation call ([/auth/token/create](https://www.vaultproject.io/docs/auth/token.html)).
99
+
100
+ #### Chef Backend
101
+
102
+ Only to be specified when the `:auth_backend:` is `chef`. Refer to the [Chef backend](documentation/CHEF.md) documentation for more information.
103
+
104
+ #####:chef:
105
+
106
+ A hash of settings that are used to configure a connection to the Chef server (used by the [Chef API](https://github.com/sethvargo/chef-api) gem).
107
+
108
+ ```yaml
109
+ # https://github.com/sethvargo/chef-api/blob/master/lib/chef-api/configurable.rb
110
+ :chef:
111
+ :endpoint:
112
+ :flavor:
113
+ :client:
114
+ :key:
115
+ :proxy_address:
116
+ :proxy_password:
117
+ :proxy_port:
118
+ :proxy_username:
119
+ :ssl_pem_file:
120
+ :ssl_verify:
121
+ :user_agent:
122
+ ```
123
+
124
+ ## Usage
125
+
126
+ To configure this plugin you can use template from [settings.d/vault.yml.example](settings.d/vault.yml.example). You must place the vault.yml config file in your Smart Proxy's `config/settings.d/` directory.
127
+
128
+ ### Endpoints
129
+
130
+ #### `/vault/token/issue`
131
+
132
+ ##### Parameters
133
+
134
+ `ttl=X[d,h,m,s]`
135
+
136
+ Overrides the token TTL specified in the [`:token_options:`](#token_options) section. This value must be **lower** than the default TTL.
137
+
138
+ Example:
139
+
140
+ `/vault/token/issue?ttl=60s`
141
+
142
+ ### Caveats
143
+
144
+ In order to use this plugin effectively, the Ruby installation on your Smart Proxy server should be version 2.0.0 or higher, and be compiled against a version of OpenSSL that supports TLS (=>1.0.1). I recommend using [RVM](https://rvm.io/) & [Passenger](https://www.phusionpassenger.com) to run your Smart Proxy server.
145
+
146
+ ```
147
+ $ irb
148
+ 2.2.1 :001 > require 'openssl'
149
+ => true
150
+ 2.2.1 :002 > OpenSSL::OPENSSL_VERSION
151
+ => "OpenSSL 1.0.1e 11 Feb 2013"
152
+ ```
@@ -0,0 +1 @@
1
+ gem 'smart_proxy_vault'
@@ -0,0 +1,54 @@
1
+ require 'chef-api'
2
+
3
+ module VaultPlugin
4
+ module Authentication
5
+ module Chef
6
+ def vault_client
7
+ request.env['HTTP_X_VAULT_CLIENT']
8
+ end
9
+
10
+ def signature
11
+ request.env['HTTP_X_VAULT_SIGNATURE'] || request.env['HTTP_X_VAULT_SIGNATURE'].chomp
12
+ end
13
+
14
+ def authorized?
15
+ logger.info('Starting Chef client authentication for smart_proxy_vault')
16
+ request.env.each do |key,value|
17
+ logger.debug("header #{key}: #{value}")
18
+ end if logger.level == 0
19
+
20
+ if vault_client.nil? || signature.nil?
21
+ log_halt 401, "Failed to authenticate Chef client - #{vault_client}. Missing headers."
22
+ end
23
+
24
+ unless authenticate
25
+ log_halt 401, "Failed to authenticate Chef client - #{vault_client}. Verification failed."
26
+ end
27
+ logger.info("Successfully authenticated Chef client - #{vault_client}")
28
+ end
29
+
30
+ def chefapi
31
+ chefapi_settings = ::VaultPlugin::Plugin.settings.chef
32
+ connection = ::ChefAPI::Connection.new(chefapi_settings)
33
+ connection.ssl_verify = chefapi_settings[:ssl_verify] || false
34
+ connection
35
+ end
36
+
37
+ def authenticate
38
+ begin
39
+ node = chefapi.clients.fetch vault_client
40
+ rescue StandardError => e
41
+ log_halt 401, 'Failed to authenticate to the Chef server: ' + e.message
42
+ end
43
+ log_halt(401, "Could not find Chef client - #{vault_client}") if node.nil?
44
+
45
+ rsa = OpenSSL::PKey::RSA.new node.public_key
46
+ decoded_signature = Base64.decode64(signature)
47
+ # The body should contain the public key of the node
48
+ body = Digest::MD5.hexdigest rsa.public_key.to_s
49
+
50
+ rsa.verify(OpenSSL::Digest::SHA512.new, decoded_signature, body)
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,33 @@
1
+ require_relative 'authentication/chef'
2
+
3
+ module VaultPlugin
4
+ module Authentication
5
+ def auth_backend
6
+ ::VaultPlugin::Plugin.settings.auth_backend.to_sym
7
+ end
8
+
9
+ def auth_module
10
+ Object.const_get('::VaultPlugin::Authentication::' + auth_backend.capitalize.to_s)
11
+ end
12
+
13
+ # Creates convenient accessor methods for all keys underneath auth_backend
14
+ def create_setting_accessors
15
+ ::VaultPlugin::Plugin.settings[auth_backend].each do |key,value|
16
+ define_singleton_method(key.to_sym) { value }
17
+ end
18
+ end
19
+
20
+ def authorized?
21
+ create_setting_accessors
22
+ extend auth_module
23
+ authorized?
24
+ end
25
+
26
+ # Returns the human-readable identity for the requesting client
27
+ # Optionally used in a token's metadata & display-name
28
+ def client
29
+ extend auth_module
30
+ client
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,31 @@
1
+ module VaultPlugin
2
+ module Helpers
3
+ def settings_ttl
4
+ ::VaultPlugin::Plugin.settings.token_options[:ttl]
5
+ end
6
+
7
+ def to_seconds(string)
8
+ case string.slice(-1)
9
+ when 'd'
10
+ string.tr('d', '').to_i * 30 * 24
11
+ when 'h'
12
+ string.tr('h', '').to_i * 3600
13
+ when 'm'
14
+ string.tr('m', '').to_i * 60
15
+ when 's'
16
+ string.tr('s', '').to_i
17
+ else
18
+ log_halt 400, "Invalid TTL - #{string}. Must end with 'd', 'h', 'm' or 's'."
19
+ end
20
+ end
21
+
22
+ # Only allow clients to specify a TTL that is shorter than the default
23
+ def valid_ttl?(ttl)
24
+ return true if ttl.nil? || settings_ttl.nil?
25
+ unless (to_seconds(settings_ttl) >= to_seconds(ttl))
26
+ log_halt 400, "Invalid TTL - #{ttl}. Must be shorter or equal to #{settings_ttl}."
27
+ end
28
+ true
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,3 @@
1
+ map '/vault' do
2
+ run VaultPlugin::VaultAPI
3
+ end
@@ -0,0 +1,14 @@
1
+ module VaultPlugin
2
+ class Plugin < ::Proxy::Plugin
3
+ plugin 'vault', VaultPlugin::VERSION
4
+
5
+ settings_file 'vault.yml'
6
+ default_settings auth_backend: 'chef',
7
+ vault: {},
8
+ add_token_metadata: false,
9
+ token_options: {},
10
+ chef: {}
11
+
12
+ https_rackup_path File.expand_path('https_config.ru', File.expand_path('../', __FILE__))
13
+ end
14
+ end
@@ -0,0 +1,20 @@
1
+ module VaultPlugin
2
+ class VaultAPI < ::Sinatra::Base
3
+ include ::Proxy::Log
4
+ include ::VaultPlugin::Authentication
5
+ include ::VaultPlugin::VaultBackend
6
+ helpers ::Proxy::Helpers, ::VaultPlugin::Helpers
7
+
8
+ ::Sinatra::Base.register Authentication
9
+
10
+ before do
11
+ content_type :json
12
+ authorized?
13
+ end
14
+
15
+ get '/token/issue' do
16
+ ttl = params[:ttl]
17
+ issue(ttl) if valid_ttl? ttl
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,43 @@
1
+ module VaultPlugin
2
+ module VaultBackend
3
+ class API
4
+ attr_reader :connection
5
+
6
+ def initialize(child, ttl)
7
+ vault_settings = ::VaultPlugin::Plugin.settings.vault
8
+ @connection = ::Vault::Client.new(vault_settings)
9
+ @child = child
10
+ @ttl = ttl
11
+ @token_options = token_options
12
+ end
13
+
14
+ def issue_token
15
+ @connection.auth_token.create(@token_options).auth.client_token
16
+ end
17
+
18
+ private
19
+ def metadata
20
+ if ::VaultPlugin::Plugin.settings.add_token_metadata == true
21
+ return { meta: { client: @child, smartproxy_generated: true },
22
+ display_name: @child }
23
+ end
24
+ {}
25
+ end
26
+
27
+ def token_options
28
+ options = metadata.merge ::VaultPlugin::Plugin.settings[:token_options]
29
+ options[:ttl] = @ttl unless @ttl.nil?
30
+ options
31
+ end
32
+ end
33
+
34
+ def issue(ttl)
35
+ begin
36
+ vault = API.new client, ttl
37
+ vault.issue_token
38
+ rescue StandardError => e
39
+ log_halt 500, 'Failed to generate Vault token ' + e.message
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,3 @@
1
+ module VaultPlugin
2
+ VERSION = '0.2.0'
3
+ end
@@ -0,0 +1,11 @@
1
+ require 'sinatra/base'
2
+ require 'vault'
3
+ require 'base64'
4
+ require 'openssl'
5
+
6
+ require 'smart_proxy_vault/authentication'
7
+ require 'smart_proxy_vault/helpers'
8
+ require 'smart_proxy_vault/vault_backend'
9
+ require 'smart_proxy_vault/vault_api'
10
+ require 'smart_proxy_vault/version'
11
+ require 'smart_proxy_vault/vault'
@@ -0,0 +1,16 @@
1
+ ---
2
+ :enabled: true
3
+ :auth_backend: 'chef'
4
+ :vault:
5
+ :address: "https://vault.example.com"
6
+ :token: "UUID"
7
+ :ssl_verify: true
8
+ :add_token_metadata: true
9
+ :token_options:
10
+ :policies: ['policyname']
11
+ :ttl: '72h'
12
+ :chef:
13
+ :endpoint: 'https://chef.example.com'
14
+ :client: 'user'
15
+ :key: '/path/to/client.pem'
16
+ :ssl_verify: true
@@ -0,0 +1,102 @@
1
+ require 'test_helper'
2
+ require 'smart_proxy_vault'
3
+
4
+ class AuthenticationChefTest < Test::Unit::TestCase
5
+ include Rack::Test::Methods
6
+
7
+ ###
8
+ # Classes
9
+ ###
10
+
11
+ class Mock
12
+ include VaultPlugin::Authentication::Chef
13
+
14
+ def logger
15
+ @logger ||= Logger.new(StringIO.new)
16
+ @logger.level = Logger::INFO
17
+ @logger
18
+ end
19
+
20
+ def log_halt(*args)
21
+ throw :halt
22
+ end
23
+ end
24
+
25
+ ###
26
+ # Helper Methods
27
+ ###
28
+
29
+ def sign_request(key_path)
30
+ rsa = OpenSSL::PKey::RSA.new File.read key_path
31
+ body = Digest::MD5.hexdigest rsa.public_key.to_s
32
+ Base64.strict_encode64(rsa.sign(OpenSSL::Digest::SHA512.new, body))
33
+ end
34
+
35
+ def stub_client(client, signature)
36
+ stub.proxy(AuthenticationChefTest::Mock).new do |obj|
37
+ stub(obj).vault_client { client }
38
+ stub(obj).signature { signature }
39
+ end
40
+ end
41
+
42
+ def stub_response(client, public_key, status=200)
43
+ response = %({"name": "#{client}", "admin": false, "public_key": "#{public_key.gsub("\n","\\n")}", "private_key": false, "validator": false})
44
+ stub_request(:get, "https://chef.example.com/clients/#{client}").to_return(:status => status, :body => response.to_s, :headers => {'content-type' => 'application/json'} )
45
+ end
46
+
47
+ ###
48
+ # Test Methods
49
+ ###
50
+
51
+ def setup
52
+ stub.proxy(::VaultPlugin::Plugin.settings).chef {{
53
+ :endpoint => 'https://chef.example.com',
54
+ :client => 'fry',
55
+ :key => 'test/fixtures/authentication/chef/fry.pem',
56
+ :ssl_verify => true
57
+ }}
58
+
59
+ @fry_client = FactoryGirl.create(:rsa, file: 'test/fixtures/authentication/chef/fry.pem' )
60
+ @fry_client_path = 'test/fixtures/authentication/chef/fry.pem'
61
+ @bender_client = FactoryGirl.create(:rsa, file: 'test/fixtures/authentication/chef/bender.pem' )
62
+ @bender_client_path = 'test/fixtures/authentication/chef/bender.pem'
63
+
64
+ @fry_signature = sign_request @fry_client_path
65
+ @bender_signature = sign_request @bender_client_path
66
+ end
67
+
68
+ def test_signature_verification_match
69
+ stub_client 'fry', @fry_signature
70
+ stub_response 'fry', @fry_client.public_key.to_s
71
+ chefauth = Mock.new
72
+ assert_nothing_thrown do
73
+ assert chefauth.authorized?, 'Encoding & Decoding a message with the same key should pass verification'
74
+ end
75
+ end
76
+
77
+ def test_signature_verification_mismatch
78
+ stub_client 'bender', @bender_signature
79
+ stub_response 'bender', @fry_client.public_key.to_s
80
+ chefauth = Mock.new
81
+ assert_throws :halt do
82
+ refute chefauth.authorized?, 'Encoding & Decoding a message with the different key should fail verification'
83
+ end
84
+ end
85
+
86
+ def test_header_requirement
87
+ stub_client(nil, nil)
88
+ chefauth = Mock.new
89
+ assert_throws :halt do
90
+ refute chefauth.authorized?, 'A request without headers should fail'
91
+ end
92
+ end
93
+
94
+ def test_client_not_found
95
+ stub_client('zoidberg', @bender_signature)
96
+ stub_response('zoidberg', @bender_signature, 404)
97
+ chefauth = Mock.new
98
+ assert_throws :halt do
99
+ refute chefauth.authorized?, %(It should fail when a client can't be found on the Chef server)
100
+ end
101
+ end
102
+ end