foreman_cfssl 0.0.1

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,105 @@
1
+ ## foreman_cfssl
2
+
3
+ A Foreman plugin that uses CFSSL to generate certificates, plus a little management. It adds a "Certificates" sub-item to "Infrastructure" menu.
4
+
5
+ ### Warning
6
+
7
+ This plugin stores private keys in clear text in database, so think twice before using a certificate generated by it, or importing existing certificates signed by other certificate authorities. However when importing a certificate you can leave the private key field blank or upload an encrypted version.
8
+
9
+ If you do want to put valuable certificate keys into the system, consider:
10
+
11
+ * Secure the CFSSL role in Foreman
12
+ * Secure the database Foreman connects
13
+ * Be aware about Rails/Foreman vulnerabilities
14
+
15
+ ### Prerequisites
16
+
17
+ [CFSSL](https://github.com/cloudflare/cfssl) binary is used for generating/inspecting certificates. The executable `cfssl` must be on $PATH.
18
+
19
+ ### Installation
20
+
21
+ See [Foreman plugin installation](https://theforeman.org/plugins/#2.3AdvancedInstallationfromGems).
22
+
23
+ The plugin needs a "certs" table, which can be created by running:
24
+
25
+ ```
26
+ foreman-rake db:migrate
27
+ ```
28
+
29
+ or by running [the SQL](https://github.com/qingbo/foreman_cfssl/blob/master/db/certs.sql).
30
+
31
+ ### Configuration and Usage
32
+
33
+ #### ini file
34
+
35
+ Example:
36
+
37
+ `/etc/foreman/plugins/foreman_cfssl.yaml`:
38
+
39
+ ```
40
+ :foreman_cfssl:
41
+ :ca: /etc/foreman/plugins/foreman_cfssl/ca.pem
42
+ :ca_key: /etc/foreman/plugins/foreman_cfssl/ca-key.pem
43
+ :config: /etc/foreman/plugins/foreman_cfssl/config.json
44
+ :csr_template: /etc/foreman/plugins/foreman_cfssl/csr-template.json
45
+ :private_key_import: false
46
+ ```
47
+
48
+ #### CFSSL config
49
+
50
+ More documentation can be found on CFSSL project page, but here are the two JSON files referenced mentioned above:
51
+
52
+ `/etc/foreman/plugins/foreman_cfssl/config.json`:
53
+
54
+ ```
55
+ {
56
+ "signing": {
57
+ "default": {
58
+ "expiry": "43800h"
59
+ },
60
+ "profiles": {
61
+ "server": {
62
+ "expiry": "43800h",
63
+ "usages": [
64
+ "signing",
65
+ "key encipherment",
66
+ "server auth"
67
+ ]
68
+ },
69
+ "client": {
70
+ "expiry": "43800h",
71
+ "usages": [
72
+ "signing",
73
+ "key encipherment",
74
+ "client auth"
75
+ ]
76
+ }
77
+ }
78
+ }
79
+ }
80
+ ```
81
+
82
+ On certificate generation page, user can select a profile, fill in "common name" and SAN list. Inputs are merged into the CSR template below and fed into cfssl command.
83
+
84
+ `/etc/foreman/plugins/foreman_cfssl/csr-template.json`:
85
+
86
+ ```
87
+ {
88
+ "key": {
89
+ "algo": "rsa",
90
+ "size": 2048
91
+ },
92
+ "names": [
93
+ {
94
+ "C": "US",
95
+ "ST": "MA",
96
+ "L": "Newton",
97
+ "OU": "My Corp"
98
+ }
99
+ ]
100
+ }
101
+ ```
102
+
103
+ #### Foreman role
104
+
105
+ A single role "CFSSL" controls all permissions.
data/Rakefile ADDED
@@ -0,0 +1,47 @@
1
+ #!/usr/bin/env rake
2
+ begin
3
+ require 'bundler/setup'
4
+ rescue LoadError
5
+ puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
6
+ end
7
+ begin
8
+ require 'rdoc/task'
9
+ rescue LoadError
10
+ require 'rdoc/rdoc'
11
+ require 'rake/rdoctask'
12
+ RDoc::Task = Rake::RDocTask
13
+ end
14
+
15
+ RDoc::Task.new(:rdoc) do |rdoc|
16
+ rdoc.rdoc_dir = 'rdoc'
17
+ rdoc.title = 'ForemanCfssl'
18
+ rdoc.options << '--line-numbers'
19
+ rdoc.rdoc_files.include('README.rdoc')
20
+ rdoc.rdoc_files.include('lib/**/*.rb')
21
+ end
22
+
23
+ APP_RAKEFILE = File.expand_path('../test/dummy/Rakefile', __FILE__)
24
+
25
+ Bundler::GemHelper.install_tasks
26
+
27
+ require 'rake/testtask'
28
+
29
+ Rake::TestTask.new(:test) do |t|
30
+ t.libs << 'lib'
31
+ t.libs << 'test'
32
+ t.pattern = 'test/**/*_test.rb'
33
+ t.verbose = false
34
+ end
35
+
36
+ task default: :test
37
+
38
+ begin
39
+ require 'rubocop/rake_task'
40
+ RuboCop::RakeTask.new
41
+ rescue => _
42
+ puts 'Rubocop not loaded.'
43
+ end
44
+
45
+ task :default do
46
+ Rake::Task['rubocop'].execute
47
+ end
@@ -0,0 +1,121 @@
1
+ module ForemanCfssl
2
+ require 'date'
3
+ require 'open3'
4
+ require 'json'
5
+ class CertsController < ApplicationController
6
+ def index
7
+ order = params[:order] || 'not_after DESC'
8
+ @certs = Cert.all.paginate(:page => params[:page]).order(order)
9
+ end
10
+
11
+ def import
12
+ @cert = Cert.new
13
+ @allow_key_import = ['allow', 'true'].include?(SETTINGS[:foreman_cfssl][:private_key_import])
14
+ end
15
+
16
+ def import_save
17
+ @cert = Cert.new(params[:foreman_cfssl_cert].permit(:owner_email, :pem, :key))
18
+
19
+ allow_key_import = ['allow', 'true'].include?(SETTINGS[:foreman_cfssl][:private_key_import])
20
+ if @cert.key.include?('PRIVATE') && !allow_key_import
21
+ @cert.errors.add(:key, "Don't upload an unencrypted private key. Consider encrypting it.")
22
+ render 'import' and return
23
+ end
24
+
25
+ @cert.user = User.current
26
+ @cert.imported_at = Time.now
27
+
28
+ self.expand_pem
29
+
30
+ if @cert.save
31
+ flash[:notice] = "Successfully imported certificate"
32
+ redirect_to @cert
33
+ else
34
+ render 'import'
35
+ end
36
+ end
37
+
38
+ def new
39
+ @cert = Cert.new
40
+
41
+ # profiles for select
42
+ config_path = SETTINGS[:foreman_cfssl][:config]
43
+ config = JSON.parse(File.read(config_path))
44
+ @profiles = config['signing']['profiles'].keys
45
+
46
+ # CA information
47
+ ca_pem = File.read(SETTINGS[:foreman_cfssl][:ca])
48
+ @ca_info = JSON.pretty_generate(extract_cert_info(ca_pem))
49
+ end
50
+
51
+ def create
52
+ # Added several attr_accessors in model to ease form processing
53
+ @cert = Cert.new(params[:foreman_cfssl_cert].permit(:owner_email, :common_name, :profile, :hosts))
54
+
55
+ # Read ini configurations
56
+ configs = SETTINGS[:foreman_cfssl]
57
+ ca = configs[:ca]
58
+ ca_key = configs[:ca_key]
59
+ ca_config = configs[:config]
60
+ csr_template = configs[:csr_template]
61
+
62
+ # fill in details to create certificate signing request config
63
+ csr = JSON.parse(File.read(csr_template))
64
+ csr['CN'] = @cert.common_name
65
+ csr['hosts'] = @cert.hosts.strip.split(%r{\s+})
66
+
67
+ # generate certificates
68
+ stdin, stdout, stderr = Open3.popen3("cfssl gencert -config=#{ca_config} -ca=#{ca} -ca-key=#{ca_key} -profile=#{@cert.profile} -")
69
+ stdin.puts JSON.dump(csr)
70
+ stdin.close
71
+
72
+ result = JSON.parse(stdout.gets(sep=nil))
73
+ @cert.pem = result['cert']
74
+ @cert.key = result['key']
75
+ @cert.user = User.current
76
+
77
+ self.expand_pem
78
+
79
+ if @cert.save
80
+ flash[:notice] = "Successfully issued certificate for #{@cert.common_name}"
81
+ redirect_to @cert
82
+ else
83
+ render 'new'
84
+ end
85
+ end
86
+
87
+ def show
88
+ @cert = Cert.find(params[:id])
89
+ end
90
+
91
+ def destroy
92
+ @cert = Cert.find(params[:id])
93
+ @cert.destroy
94
+
95
+ flash[:notice] = "Certificate deleted"
96
+ redirect_to certs_path
97
+ end
98
+
99
+ def expand_pem
100
+ cert_info = self.extract_cert_info(@cert.pem)
101
+
102
+ @cert.subject = JSON.dump(cert_info['subject'])
103
+ @cert.issuer = JSON.dump(cert_info['issuer'])
104
+ @cert.serial_number = cert_info['serial_number']
105
+ @cert.sans = cert_info.has_key?('sans') ? JSON.dump(cert_info['sans']) : '[]'
106
+ @cert.not_before = DateTime.parse(cert_info['not_before'])
107
+ @cert.not_after = DateTime.parse(cert_info['not_after'])
108
+ @cert.sigalg = cert_info['sigalg']
109
+ @cert.authority_key_id = cert_info['authority_key_id']
110
+ @cert.subject_key_id = cert_info['subject_key_id']
111
+ end
112
+
113
+ def extract_cert_info(pem)
114
+ stdin, stdout, stderr = Open3.popen3('cfssl certinfo -cert=-')
115
+ stdin.puts pem
116
+ stdin.close
117
+
118
+ return JSON.parse(stdout.gets(sep=nil))
119
+ end
120
+ end
121
+ end
@@ -0,0 +1,31 @@
1
+ module ForemanCfssl
2
+ class Cert < ActiveRecord::Base
3
+ belongs_to :user
4
+ # quick hack to ease form submission
5
+ attr_accessor :common_name, :hosts
6
+
7
+ def subject_info
8
+ JSON.parse(subject)
9
+ end
10
+
11
+ def issuer_info
12
+ JSON.parse(issuer)
13
+ end
14
+
15
+ def sans_info
16
+ JSON.parse(sans)
17
+ end
18
+
19
+ def source_type
20
+ imported_at ? "imported" : "issued"
21
+ end
22
+
23
+ def expired?
24
+ not_after < Time.now
25
+ end
26
+
27
+ def expiring?
28
+ not_after < Time.now + 30.days && ! expired?
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,2 @@
1
+ <h4 class="header ca">Certificates</h4>
2
+ <%= _('Widget content') %>
@@ -0,0 +1,13 @@
1
+ <% title "Certificates - Import" %>
2
+
3
+ <% title_actions link_to("Back", index_path, class: "btn btn-default") %>
4
+
5
+ <%= form_for(@cert, url: {controller: 'foreman_cfssl/certs', action: 'import_save'}) do |f| %>
6
+ <%= text_f f, :owner_email, :help_inline => "Who own's this certificate? For reference only." %>
7
+ <%= textarea_f f, :pem, {rows: 20, help_inline: "Certificate signed by CA"} %>
8
+ <%= textarea_f f, :key, {rows: 20, help_inline:
9
+ @private_key_import ? "It's recommended to upload an encrypted version of the private key."
10
+ : "Private key upload is disallowed. Leave it blank or upload an encrypted one."} %>
11
+
12
+ <div><%= f.submit 'Import', class: 'btn btn-primary' %></div>
13
+ <% end %>
@@ -0,0 +1,44 @@
1
+ <% title "Certificates" %>
2
+
3
+ <% title_actions new_link('Issue'),
4
+ link_to("Import", import_path, class: "btn btn-primary") %>
5
+
6
+ <table class="<%= table_css_classes 'table-fixed' %>">
7
+ <thead>
8
+ <tr>
9
+ <th>User</th>
10
+ <th>Source</th>
11
+ <th>Owner</th>
12
+ <th>Common Name</th>
13
+ <th>SANs</th>
14
+ <th>Issuer</th>
15
+ <th><%= sort :not_after, :as => "Invalid After" %></th>
16
+ <th>Actions</th>
17
+ </tr>
18
+ </thead>
19
+ <tbody>
20
+ <% @certs.each do |cert| %>
21
+ <tr style="
22
+ <% if cert.expired? %>text-decoration: line-through;<% end %>
23
+ <% if cert.expiring? %>background-color: #c33; color: #fff;<% end %>
24
+ ">
25
+ <td><%= cert.user.name %></td>
26
+ <td><%= cert.source_type %></td>
27
+ <td><%= cert.owner_email %></td>
28
+ <td><%= cert.subject_info['common_name'] %></td>
29
+ <td>
30
+ <ul>
31
+ <% cert.sans_info.each do |san| %>
32
+ <li><%= san %></li>
33
+ <% end %>
34
+ </ul>
35
+ </td>
36
+ <td><%= cert.issuer_info['common_name'] %></td>
37
+ <td><%= cert.not_after %></td>
38
+ <td><%= link_to 'show', cert %></td>
39
+ </tr>
40
+ <% end %>
41
+ </tbody>
42
+ </table>
43
+
44
+ <%= will_paginate_with_info @certs %>
@@ -0,0 +1,16 @@
1
+ <% title "Certificates - Issue New" %>
2
+
3
+ <% title_actions link_to("Back", index_path, class: "btn btn-default") %>
4
+
5
+ <%= form_for(@cert, url: {controller: 'foreman_cfssl/certs', action: 'create'}) do |f| %>
6
+
7
+ <%= text_f f, :owner_email, :help_inline => "Who own's this certificate? For reference only. Not used in generation." %>
8
+ <%= text_f f, :common_name, :help_inline => "Name of the subject" %>
9
+ <%= select_f f, :profile, @profiles, :to_s, :to_s, :help_inline => "A profile configured in CFSSL" %>
10
+ <%= textarea_f f, :hosts, {rows: 5, help_inline: "List of domain names the subject will be serving"} %>
11
+
12
+ <p>Signing CA:</p>
13
+ <pre><%= @ca_info %></pre>
14
+
15
+ <div><%= f.submit 'Issue Certificate', class: 'btn btn-primary' %></div>
16
+ <% end %>
@@ -0,0 +1,73 @@
1
+ <% title "Certificates - Details" %>
2
+
3
+ <% title_actions link_to("Back", index_path, class: "btn btn-default"),
4
+ link_to("Delete", @cert, method: 'delete',
5
+ data: {confirm: '!!! Do you want to delete this from database?'}, class: "btn btn-danger")
6
+ %>
7
+
8
+ <table class="<%= table_css_classes 'table-fixed' %>">
9
+ <thead>
10
+ <tr>
11
+ <th width="100px">Field</th>
12
+ <th>Value</th>
13
+ </tr>
14
+ </thead>
15
+ <tbody>
16
+ <tr>
17
+ <td>User</td>
18
+ <td><%= @cert.user.name %></td>
19
+ </tr>
20
+ <tr>
21
+ <td>Owner</td>
22
+ <td><%= @cert.owner_email %></td>
23
+ </tr>
24
+ <tr>
25
+ <td>Profile</td>
26
+ <td><%= @cert.profile %></td>
27
+ </tr>
28
+ <tr>
29
+ <td>Subject</td>
30
+ <td><pre><%= JSON.pretty_generate(JSON.parse(@cert.subject)) %></pre></td>
31
+ </tr>
32
+ <tr>
33
+ <td>Issuer</td>
34
+ <td><pre><%= JSON.pretty_generate(JSON.parse(@cert.issuer)) %></pre></td>
35
+ </tr>
36
+ <tr>
37
+ <td>Serial number</td>
38
+ <td><%= @cert.serial_number %></td>
39
+ </tr>
40
+ <tr>
41
+ <td>SANs</td>
42
+ <td><pre><%= JSON.pretty_generate(JSON.parse(@cert.sans)) %></pre></td>
43
+ </tr>
44
+ <tr>
45
+ <td>Valid not before</td>
46
+ <td><%= @cert.not_before %></td>
47
+ </tr>
48
+ <tr>
49
+ <td>Valid not after</td>
50
+ <td><%= @cert.not_after %></td>
51
+ </tr>
52
+ <tr>
53
+ <td>SigAlg</td>
54
+ <td><%= @cert.sigalg %></td>
55
+ </tr>
56
+ <tr>
57
+ <td>CA key ID</td>
58
+ <td><%= @cert.authority_key_id %></td>
59
+ </tr>
60
+ <tr>
61
+ <td>Key ID</td>
62
+ <td><%= @cert.subject_key_id %></td>
63
+ </tr>
64
+ <tr>
65
+ <td>Certificate</td>
66
+ <td><pre><%= @cert.pem %></pre></td>
67
+ </tr>
68
+ <tr>
69
+ <td>Key</td>
70
+ <td><pre><%= @cert.key %></pre></td>
71
+ </tr>
72
+ </tbody>
73
+ </table>