Class: MCollective::Signer::Choria

Inherits:
Base
  • Object
show all
Defined in:
lib/mcollective/signer/choria.rb

Overview

This is a Secure Request Signer that allows either local signing of requests using the users own certificate or delegation based signing via a webservice

This allows one to integrate the Choria CLI into a centralised authentication, authorization and auditing system

Available settings:

choria.security.request_signer.plugin - the plugin to use, `choria` for this one - the default.
choria.security.request_signer.token_file - a file holding a token like a JWT or similar
choria.security.request_signer.token_environment - a ENV key holding a token like a JWT or similar
choria.security.request_signer.url - a endpoint that implements the v1 signer protocol

The webservice has to support the specification found at choria.io/schemas/choria/signer/v1/service.json

Instance Method Summary collapse

Methods inherited from Base

inherited, #initialize

Constructor Details

This class inherits a constructor from MCollective::Signer::Base

Instance Method Details

#calleridString

Determines the callerid for this client

When a remote signer is enabled the caller is extracted from the JWT otherwise a choria=user style ID is generated

Returns:

Raises:

  • (Exception)

    when the JWT is invalid



63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
# File 'lib/mcollective/signer/choria.rb', line 63

def callerid
  if remote_signer?
    parts = token.split(".")

    raise("Invalid JWT token") unless parts.length == 3

    claims = JSON.parse(Base64.decode64(parts[1]))

    raise("Invalid JWT token") unless claims.include?("callerid")
    raise("Invalid JWT token") unless claims["callerid"].is_a?(String)
    raise("Invalid JWT token") if claims["callerid"].empty?

    claims["callerid"]
  else
    "choria=%s" % choria.certname
  end
end

#choriaObject



180
181
182
# File 'lib/mcollective/signer/choria.rb', line 180

def choria
  @choria ||= security.choria
end

#client_public_certString

Note:

paths determined by Puppet AIO packages

The path to a client public certificate

Returns:



155
156
157
# File 'lib/mcollective/signer/choria.rb', line 155

def client_public_cert
  security.client_public_cert
end

#local_sign!(secure_request) ⇒ Object

Signs using local certificates

Parameters:

  • secure_request (Hash)

    a v1 secure request

Raises:

  • (StandardError)

    on signing error



140
141
142
143
144
145
146
147
# File 'lib/mcollective/signer/choria.rb', line 140

def local_sign!(secure_request)
  Log.info("Signing secure request using local credentials")

  secure_request["signature"] = sign(secure_request["message"])
  secure_request["pubcert"] = File.read(client_public_cert).chomp

  nil
end

#remote_sign!(secure_request) ⇒ Object

Performs a remote sign operation against a configured web service

Parameters:

  • secure_request (Hash)

    a v1 secure request

Raises:

  • (StandardError)

    on signing error



96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
# File 'lib/mcollective/signer/choria.rb', line 96

def remote_sign!(secure_request)
  Log.info("Signing secure request using remote signer %s" % remote_signer_url)

  uri = remote_signer_url
  post = choria.http_post(uri.request_uri)
  post.body = sign_request_body(secure_request).to_json
  post["Content-type"] = "application/json"

  http = choria.https(:target => uri.host, :port => uri.port)
  http.use_ssl = false if uri.scheme == "http"

  # While this might appear alarming it's expected that the clients
  # in this situation will not have any Choria CA issued certificates
  # and so wish to use a remote signer - the certificate management woes
  # being one of the main reasons for centralised AAA.
  #
  # So there is no realistic way to verify these requests especially in the
  # event that these signers run on private IPs and such as would be typical
  # so while we do this big No No of disabling verify here it really is the
  # only thing that make sense.
  http.verify_mode = OpenSSL::SSL::VERIFY_NONE if http.use_ssl?

  resp = http.request(post)

  signature = {}

  if resp.code == "200"
    signature = JSON.parse(resp.body)
  else
    raise("Could not get remote signature: %s: %s" % [resp.code, resp.body])
  end

  raise("Could not get remote signature: %s" % signature["error"]) if signature["error"]

  signed_request = JSON.parse(Base64.decode64(signature["secure_request"]))
  signed_request.each do |k, v|
    secure_request[k] = v
  end
end

#remote_signer?Boolean

Determines if a remote signer is configured

Returns:

  • (Boolean)


162
163
164
# File 'lib/mcollective/signer/choria.rb', line 162

def remote_signer?
  !!(remote_signer_url == "" || remote_signer_url)
end

#remote_signer_urlURI, Nil

Determines the remote url to submit standard signing requests to

Returns:

  • (URI, Nil)


169
170
171
172
173
174
# File 'lib/mcollective/signer/choria.rb', line 169

def remote_signer_url
  return nil unless @config.pluginconf["choria.security.request_signer.url"]
  return nil if @config.pluginconf["choria.security.request_signer.url"] == ""

  URI.parse(@config.pluginconf["choria.security.request_signer.url"])
end

#securityObject



176
177
178
# File 'lib/mcollective/signer/choria.rb', line 176

def security
  @security ||= PluginManager["security_plugin"]
end

#sign(string, id = nil) ⇒ String

Signs a string using the private key

Parameters:

  • string (String)

    the string to sign

  • id (String) (defaults to: nil)

    a callerid to sign as

Returns:

  • (String)

    Base64 encoded signature

Raises:

  • (Exception)

    in case OpenSSL fails for some reason or keys cannot be found



150
151
152
# File 'lib/mcollective/signer/choria.rb', line 150

def sign(string, id=nil)
  security.sign(string, id)
end

#sign_request_body(secure_request) ⇒ Hash

The body that would be submitted to the remote service

Parameters:

  • secure_request (Hash)

    a v1 secure request

Returns:

  • (Hash)


85
86
87
88
89
90
# File 'lib/mcollective/signer/choria.rb', line 85

def sign_request_body(secure_request)
  {
    "token" => token,
    "request" => Base64.encode64(secure_request["message"])
  }
end

#sign_secure_request!(secure_request) ⇒ Object

Signs the secure request

Signing supports either local mode using local certificates or delegating to a remote signer that is written in conformance with the signer specification version 1

Parameters:

  • secure_request (Hash)

    a v1 secure request



46
47
48
49
50
51
52
53
54
# File 'lib/mcollective/signer/choria.rb', line 46

def sign_secure_request!(secure_request)
  return if $choria_unsafe_disable_protocol_security # rubocop:disable Style/GlobalVars

  if remote_signer?
    remote_sign!(secure_request)
  else
    local_sign!(secure_request)
  end
end

#tokenString?

Retrieves the token from either a local file or the users environment

Returns:



23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
# File 'lib/mcollective/signer/choria.rb', line 23

def token
  file = @config.pluginconf["choria.security.request_signer.token_file"]
  env = @config.pluginconf["choria.security.request_signer.token_environment"]

  if file
    file = File.expand_path(file)

    raise("No token found in %s, please authenticate using your configured authentication service" % file) unless File.exist?(file)

    return File.read(file).chomp
  end

  raise("could not find token in environment variable %s" % env) unless ENV[env]

  ENV[env].chomp
end