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.
- checksums.yaml +7 -0
- data/LICENSE +674 -0
- data/README.md +105 -0
- data/Rakefile +47 -0
- data/app/controllers/foreman_cfssl/certs_controller.rb +121 -0
- data/app/models/concerns/foreman_cfssl/cert.rb +31 -0
- data/app/views/dashboard/_foreman_cfssl_widget.html.erb +2 -0
- data/app/views/foreman_cfssl/certs/import.html.erb +13 -0
- data/app/views/foreman_cfssl/certs/index.html.erb +44 -0
- data/app/views/foreman_cfssl/certs/new.html.erb +16 -0
- data/app/views/foreman_cfssl/certs/show.html.erb +73 -0
- data/config/routes.rb +9 -0
- data/db/certs.sql +21 -0
- data/db/migrate/20170801221500_create_certs.rb +21 -0
- data/lib/foreman_cfssl.rb +4 -0
- data/lib/foreman_cfssl/engine.rb +64 -0
- data/lib/foreman_cfssl/version.rb +3 -0
- data/lib/tasks/foreman_cfssl_tasks.rake +47 -0
- metadata +88 -0
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,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>
|