vagrant-docker-certificates-manager 0.3.0 → 0.4.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +12 -0
- data/lib/vagrant-docker-certificates-manager/VERSION +1 -1
- data/lib/vagrant-docker-certificates-manager/actions/install.rb +15 -3
- data/lib/vagrant-docker-certificates-manager/actions/uninstall.rb +43 -2
- data/lib/vagrant-docker-certificates-manager/config.rb +59 -10
- data/lib/vagrant-docker-certificates-manager/helpers.rb +48 -30
- data/lib/vagrant-docker-certificates-manager/util/cert.rb +94 -0
- data/lib/vagrant-docker-certificates-manager/util/registry.rb +51 -0
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 7205874687ebbce5453b9267447c8a007d949416b370e67d141b4e633193ce1b
|
|
4
|
+
data.tar.gz: 5970d32848bbb46f63f4d3b1dd0c12823350937e69363dbd1c4e1d7e7cf1ab16
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: da2f09201144ccaa74456e248c84d3c89297be73764c7d7cd2cc5a5fda5547c08b03d1eb9099fdab2808bf7a51e392faab5496ed3033ef86a4e96c9121388e88
|
|
7
|
+
data.tar.gz: 8a8f01b14910172242e72b62bd9dd889fcc79f56e0449f2d48ccffd763c91f5dab7a887ca52b214efdd8038d976c7e0c9464d4069b95c03ecd226fa1f30ebefd
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.4.0](https://github.com/julienpoirou/vagrant-docker-certificates-manager/compare/v0.3.0...v0.4.0) (2026-07-03)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Fonctionnalités ✨
|
|
7
|
+
|
|
8
|
+
* Improve plugin quality and publish RubyDoc ([278298b](https://github.com/julienpoirou/vagrant-docker-certificates-manager/commit/278298b614ac9e4b0797d806bed3e0015d7fdb1f))
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Corrections 🐛
|
|
12
|
+
|
|
13
|
+
* Resolve rubocop offenses and enable pages for RubyDoc deploy ([227c596](https://github.com/julienpoirou/vagrant-docker-certificates-manager/commit/227c5967c1480a2771413cf5738d2ac1f030d495))
|
|
14
|
+
|
|
3
15
|
## [0.3.0](https://github.com/julienpoirou/vagrant-docker-certificates-manager/compare/v0.2.0...v0.3.0) (2026-03-09)
|
|
4
16
|
|
|
5
17
|
|
|
@@ -1 +1 @@
|
|
|
1
|
-
0.
|
|
1
|
+
0.4.0
|
|
@@ -24,6 +24,15 @@ module VagrantDockerCertificatesManager
|
|
|
24
24
|
@app.call(env)
|
|
25
25
|
end
|
|
26
26
|
|
|
27
|
+
# Installs a configured certificate into the current host trust store.
|
|
28
|
+
#
|
|
29
|
+
# The operation is idempotent: when the fingerprint is already tracked,
|
|
30
|
+
# the current machine adopts the existing registry entry instead of
|
|
31
|
+
# reinstalling the certificate.
|
|
32
|
+
#
|
|
33
|
+
# @param cfg [#cert_path, #cert_name] Vagrant certificate configuration.
|
|
34
|
+
# @param env [Hash, nil] Vagrant environment hash.
|
|
35
|
+
# @return [Hash] Normalized result with code, status, data, or error.
|
|
27
36
|
def self.perform_install(cfg, env)
|
|
28
37
|
unless File.file?(cfg.cert_path)
|
|
29
38
|
return { code: 1, status: "error",
|
|
@@ -32,9 +41,11 @@ error: UiHelpers.t("errors.invalid_path", path: cfg.cert_path) }
|
|
|
32
41
|
|
|
33
42
|
name = cfg.cert_name.to_s.strip.empty? ? Cert.default_name_from(cfg.cert_path) : cfg.cert_name
|
|
34
43
|
fp = Cert.sha1(cfg.cert_path)
|
|
44
|
+
mid = env && env[:machine]&.id
|
|
45
|
+
|
|
35
46
|
if Registry.all.key?(fp)
|
|
36
|
-
|
|
37
|
-
|
|
47
|
+
Registry.adopt(fp, mid) if mid
|
|
48
|
+
return { code: 0, status: "success", data: { os: OS.detect, cert: name, already: true } }
|
|
38
49
|
end
|
|
39
50
|
|
|
40
51
|
os = OS.detect
|
|
@@ -51,7 +62,8 @@ firefox: cfg.manage_firefox)
|
|
|
51
62
|
"path" => File.expand_path(cfg.cert_path),
|
|
52
63
|
"name" => name,
|
|
53
64
|
"nickname" => Cert.nickname_for(name),
|
|
54
|
-
"os" => os.to_s
|
|
65
|
+
"os" => os.to_s,
|
|
66
|
+
"owners" => [mid].compact
|
|
55
67
|
})
|
|
56
68
|
{ code: 0, status: "success", data: { os: os, cert: name } }
|
|
57
69
|
end
|
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "fileutils"
|
|
3
4
|
require_relative "../util/os"
|
|
4
5
|
require_relative "../util/ui"
|
|
5
6
|
require_relative "../util/cert"
|
|
7
|
+
require_relative "../util/generator"
|
|
6
8
|
require_relative "../util/registry"
|
|
7
9
|
require_relative "../helpers"
|
|
8
10
|
|
|
@@ -21,22 +23,61 @@ module VagrantDockerCertificatesManager
|
|
|
21
23
|
result[:status] == "success" ? "uninstall.success" : "uninstall.fail",
|
|
22
24
|
name: cfg.cert_name)
|
|
23
25
|
end
|
|
26
|
+
purge_generated(cfg, env)
|
|
24
27
|
@app.call(env)
|
|
25
28
|
end
|
|
26
29
|
|
|
27
|
-
def
|
|
30
|
+
def purge_generated(cfg, env)
|
|
31
|
+
# Generated material is purged only through explicit env flags to avoid
|
|
32
|
+
# deleting a CA/server certificate that may be shared outside this action.
|
|
33
|
+
files = []
|
|
34
|
+
files += [Generator::CA_CERT, Generator::CA_KEY, Generator::CRL] if env_flag?("VDCM_PURGE_CA_ON_DESTROY")
|
|
35
|
+
files += [Generator::SRV_CRT, Generator::SRV_KEY] if env_flag?("VDCM_PURGE_SERVER_ON_DESTROY")
|
|
36
|
+
return if files.empty?
|
|
37
|
+
|
|
38
|
+
dir = cfg.cert_dir
|
|
39
|
+
files.uniq.each do |name|
|
|
40
|
+
path = File.join(dir, name)
|
|
41
|
+
next unless File.exist?(path)
|
|
42
|
+
|
|
43
|
+
File.delete(path)
|
|
44
|
+
Ui.say(env, :info, "purge.removed", path: path)
|
|
45
|
+
rescue StandardError => e
|
|
46
|
+
Ui.say(env, :warn, "purge.failed", path: path, error: e.message)
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def env_flag?(name)
|
|
51
|
+
ENV[name].to_s == "1"
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Removes a configured certificate when the current machine is the last owner.
|
|
55
|
+
#
|
|
56
|
+
# Shared certificates stay installed until every machine recorded in the
|
|
57
|
+
# registry has released ownership.
|
|
58
|
+
#
|
|
59
|
+
# @param cfg [#cert_path, #cert_name] Vagrant certificate configuration.
|
|
60
|
+
# @param env [Hash, nil] Vagrant environment hash.
|
|
61
|
+
# @return [Hash] Normalized result with code, status, data, or error.
|
|
62
|
+
def self.perform_uninstall(cfg, env)
|
|
28
63
|
fp_entry = Registry.find_by_path(cfg.cert_path)
|
|
29
64
|
unless fp_entry
|
|
30
65
|
return({ code: 1, status: "error",
|
|
31
66
|
error: UiHelpers.t("errors.not_found_for_remove", path: cfg.cert_path) })
|
|
32
67
|
end
|
|
33
68
|
fp, rec = fp_entry
|
|
69
|
+
mid = env && env[:machine]&.id
|
|
70
|
+
|
|
71
|
+
if mid && Registry.release(fp, mid)
|
|
72
|
+
return({ code: 0, status: "success", data: { kept: true, cert: rec["name"] } })
|
|
73
|
+
end
|
|
74
|
+
|
|
34
75
|
os = OS.detect
|
|
35
76
|
ok = case os
|
|
36
77
|
when :mac then OS.mac_remove_by_fp(fp)
|
|
37
78
|
when :linux then OS.linux_uninstall_cert(rec["name"], nss: cfg.manage_nss_browsers,
|
|
38
79
|
firefox: cfg.manage_firefox)
|
|
39
|
-
when :windows then OS.win_remove_by_fp(fp)
|
|
80
|
+
when :windows then OS.win_remove_by_fp(fp, disable_firefox: !Registry.others_for_os?(fp, "windows"))
|
|
40
81
|
else return({ code: 2, status: "error", error: UiHelpers.t("errors.os_unsupported") })
|
|
41
82
|
end
|
|
42
83
|
Registry.untrack(fp) if ok
|
|
@@ -1,37 +1,86 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module VagrantDockerCertificatesManager
|
|
4
|
+
# Vagrant configuration for certificate generation, installation, and cleanup.
|
|
5
|
+
#
|
|
6
|
+
# @!attribute cert_path
|
|
7
|
+
# @return [String] Path to the CA certificate installed into the trust store.
|
|
8
|
+
# @!attribute cert_name
|
|
9
|
+
# @return [String] Friendly certificate name used in host trust stores.
|
|
10
|
+
# @!attribute install_on_up
|
|
11
|
+
# @return [Boolean] Whether to install the certificate during `vagrant up`.
|
|
12
|
+
# @!attribute remove_on_destroy
|
|
13
|
+
# @return [Boolean] Whether to remove the certificate during `vagrant destroy`.
|
|
14
|
+
# @!attribute manage_firefox
|
|
15
|
+
# @return [Boolean] Whether Firefox stores should be managed where supported.
|
|
16
|
+
# @!attribute manage_nss_browsers
|
|
17
|
+
# @return [Boolean] Whether NSS browser stores should be managed where supported.
|
|
18
|
+
# @!attribute generate_on_up
|
|
19
|
+
# @return [Boolean] Whether local certificate material should be generated during `vagrant up`.
|
|
4
20
|
class Config < Vagrant.plugin("2", :config)
|
|
5
21
|
attr_accessor :cert_path, :cert_name, :install_on_up, :remove_on_destroy,
|
|
6
22
|
:manage_firefox, :manage_nss_browsers, :locale, :verbose,
|
|
7
|
-
:container_name
|
|
23
|
+
:container_name,
|
|
24
|
+
:generate_on_up, :ca_cn, :ca_days, :server_domain, :crl_url
|
|
8
25
|
|
|
9
26
|
def initialize
|
|
10
|
-
@cert_path =
|
|
11
|
-
@cert_name =
|
|
12
|
-
@install_on_up =
|
|
13
|
-
@remove_on_destroy =
|
|
14
|
-
@manage_firefox =
|
|
15
|
-
@manage_nss_browsers =
|
|
16
|
-
@locale =
|
|
17
|
-
@verbose =
|
|
18
|
-
@container_name =
|
|
27
|
+
@cert_path = UNSET_VALUE
|
|
28
|
+
@cert_name = UNSET_VALUE
|
|
29
|
+
@install_on_up = UNSET_VALUE
|
|
30
|
+
@remove_on_destroy = UNSET_VALUE
|
|
31
|
+
@manage_firefox = UNSET_VALUE
|
|
32
|
+
@manage_nss_browsers = UNSET_VALUE
|
|
33
|
+
@locale = UNSET_VALUE
|
|
34
|
+
@verbose = UNSET_VALUE
|
|
35
|
+
@container_name = UNSET_VALUE
|
|
36
|
+
@generate_on_up = UNSET_VALUE
|
|
37
|
+
@ca_cn = UNSET_VALUE
|
|
38
|
+
@ca_days = UNSET_VALUE
|
|
39
|
+
@server_domain = UNSET_VALUE
|
|
40
|
+
@crl_url = UNSET_VALUE
|
|
19
41
|
end
|
|
20
42
|
|
|
21
43
|
def finalize!
|
|
44
|
+
@cert_path = "certs/rootca.cert.pem" if @cert_path == UNSET_VALUE
|
|
45
|
+
@cert_name = "local.dev" if @cert_name == UNSET_VALUE
|
|
46
|
+
@install_on_up = false if @install_on_up == UNSET_VALUE
|
|
47
|
+
@remove_on_destroy = false if @remove_on_destroy == UNSET_VALUE
|
|
48
|
+
@manage_firefox = false if @manage_firefox == UNSET_VALUE
|
|
49
|
+
@manage_nss_browsers = true if @manage_nss_browsers == UNSET_VALUE
|
|
50
|
+
@locale = "en" if @locale == UNSET_VALUE
|
|
51
|
+
@verbose = false if @verbose == UNSET_VALUE
|
|
52
|
+
@container_name = nil if @container_name == UNSET_VALUE
|
|
53
|
+
@generate_on_up = false if @generate_on_up == UNSET_VALUE
|
|
54
|
+
@ca_cn = "local-ca" if @ca_cn == UNSET_VALUE
|
|
55
|
+
@ca_days = 3650 if @ca_days == UNSET_VALUE
|
|
56
|
+
@server_domain = nil if @server_domain == UNSET_VALUE
|
|
57
|
+
@crl_url = nil if @crl_url == UNSET_VALUE
|
|
58
|
+
|
|
22
59
|
@install_on_up = !!@install_on_up
|
|
23
60
|
@remove_on_destroy = !!@remove_on_destroy
|
|
24
61
|
@manage_firefox = !!@manage_firefox
|
|
25
62
|
@manage_nss_browsers = !!@manage_nss_browsers
|
|
26
63
|
@verbose = !!@verbose
|
|
64
|
+
@generate_on_up = !!@generate_on_up
|
|
27
65
|
@locale = (@locale || "en").to_s
|
|
66
|
+
@ca_cn = (@ca_cn || "local-ca").to_s
|
|
67
|
+
@ca_days = @ca_days.to_i
|
|
28
68
|
end
|
|
29
69
|
|
|
30
70
|
def validate(_machine)
|
|
31
71
|
errors = []
|
|
32
72
|
errors << "cert_path must be provided" if @cert_path.to_s.strip.empty?
|
|
33
73
|
errors << "cert_name must be provided" if @cert_name.to_s.strip.empty?
|
|
74
|
+
unless @locale.is_a?(String) && %w[en fr].include?(@locale.to_s[0, 2].downcase)
|
|
75
|
+
errors << "locale must be 'en' or 'fr'"
|
|
76
|
+
end
|
|
77
|
+
errors << "ca_days must be a positive integer" if @generate_on_up && @ca_days.to_i <= 0
|
|
34
78
|
{ "vagrant-docker-certificates-manager" => errors }
|
|
35
79
|
end
|
|
80
|
+
|
|
81
|
+
def cert_dir
|
|
82
|
+
d = File.dirname(@cert_path.to_s)
|
|
83
|
+
d.empty? ? "certs" : d
|
|
84
|
+
end
|
|
36
85
|
end
|
|
37
86
|
end
|
|
@@ -31,7 +31,11 @@ module VagrantDockerCertificatesManager
|
|
|
31
31
|
return if defined?(@i18n_setup) && @i18n_setup
|
|
32
32
|
::I18n.enforce_available_locales = false
|
|
33
33
|
base = File.expand_path("../../locales", __dir__)
|
|
34
|
-
|
|
34
|
+
paths = Dir[File.join(base, "*.yml")]
|
|
35
|
+
if paths.empty? && File.directory?(base)
|
|
36
|
+
paths = Dir.children(base).grep(/\.ya?ml\z/).map { |file| File.join(base, file) }
|
|
37
|
+
end
|
|
38
|
+
::I18n.load_path |= paths
|
|
35
39
|
::I18n.available_locales = SUPPORTED
|
|
36
40
|
default = ((ENV["VDCM_LANG"] || ENV["LANG"] || "en")[0, 2] rescue "en").to_sym
|
|
37
41
|
::I18n.default_locale = SUPPORTED.include?(default) ? default : :en
|
|
@@ -98,8 +102,6 @@ module VagrantDockerCertificatesManager
|
|
|
98
102
|
ENV["VDCM_DEBUG"].to_s == "1"
|
|
99
103
|
end
|
|
100
104
|
|
|
101
|
-
# ── display ───────────────────────────────────────────────────────────────
|
|
102
|
-
|
|
103
105
|
def level_to_emoji(level)
|
|
104
106
|
case level
|
|
105
107
|
when :success then :success
|
|
@@ -130,8 +132,6 @@ module VagrantDockerCertificatesManager
|
|
|
130
132
|
say(env_or_ui, :info, nil, raw: "#{e(:bug)} #{msg}")
|
|
131
133
|
end
|
|
132
134
|
|
|
133
|
-
# ── help ──────────────────────────────────────────────────────────────────
|
|
134
|
-
|
|
135
135
|
def print_general_help(no_emoji: false, ui: nil)
|
|
136
136
|
setup_i18n!
|
|
137
137
|
lines = []
|
|
@@ -139,6 +139,10 @@ module VagrantDockerCertificatesManager
|
|
|
139
139
|
lines << " #{t('cli.usage')}"
|
|
140
140
|
t_hash("help.commands").each_value { |line| lines << " #{line}" }
|
|
141
141
|
|
|
142
|
+
emit_lines(lines, ui)
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def emit_lines(lines, ui)
|
|
142
146
|
if ui
|
|
143
147
|
lines.each { |ln| ui.info(ln) }
|
|
144
148
|
else
|
|
@@ -151,39 +155,53 @@ module VagrantDockerCertificatesManager
|
|
|
151
155
|
topic = (topic || "").to_s.strip.downcase
|
|
152
156
|
return print_general_help(no_emoji: no_emoji, ui: ui) if topic.empty?
|
|
153
157
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
usage = t("#{base}.usage", default: nil)
|
|
157
|
-
desc = t("#{base}.description", default: nil)
|
|
158
|
-
opts = t_hash("#{base}.options")
|
|
159
|
-
exs = ::I18n.t(ns_key("#{base}.examples"), default: [])
|
|
158
|
+
fields = topic_help_fields("help.topic.#{topic}")
|
|
159
|
+
return print_general_help(no_emoji: no_emoji, ui: ui) if topic_help_empty?(fields)
|
|
160
160
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
end
|
|
161
|
+
emit_lines(build_topic_help_lines(topic, fields, no_emoji), ui)
|
|
162
|
+
end
|
|
164
163
|
|
|
164
|
+
def topic_help_fields(base)
|
|
165
|
+
{
|
|
166
|
+
title: t("#{base}.title", default: nil),
|
|
167
|
+
usage: t("#{base}.usage", default: nil),
|
|
168
|
+
desc: t("#{base}.description", default: nil),
|
|
169
|
+
opts: t_hash("#{base}.options"),
|
|
170
|
+
exs: ::I18n.t(ns_key("#{base}.examples"), default: [])
|
|
171
|
+
}
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def topic_help_empty?(fields)
|
|
175
|
+
fields[:title].nil? && fields[:usage].nil? && fields[:desc].nil? &&
|
|
176
|
+
fields[:opts].empty? && fields[:exs].empty?
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
def build_topic_help_lines(topic, fields, no_emoji)
|
|
165
180
|
lines = []
|
|
166
|
-
lines << "#{e(:info, no_emoji: no_emoji)} #{title || t('help.topic_fallback_title', topic: topic)}"
|
|
181
|
+
lines << "#{e(:info, no_emoji: no_emoji)} #{fields[:title] || t('help.topic_fallback_title', topic: topic)}"
|
|
167
182
|
lines << " #{t('help.usage_label')}"
|
|
168
|
-
lines << " #{usage || 'vagrant certs help'}"
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
183
|
+
lines << " #{fields[:usage] || 'vagrant certs help'}"
|
|
184
|
+
|
|
185
|
+
desc = fields[:desc]
|
|
186
|
+
append_help_section(lines, t("help.description_label"), [" #{desc}"]) if desc && !desc.strip.empty?
|
|
187
|
+
unless fields[:opts].empty?
|
|
188
|
+
append_help_section(lines, t("help.options_label"), fields[:opts].values.map do |l|
|
|
189
|
+
" #{l}"
|
|
190
|
+
end)
|
|
176
191
|
end
|
|
192
|
+
|
|
193
|
+
exs = fields[:exs]
|
|
177
194
|
if exs.is_a?(Array) && !exs.empty?
|
|
178
|
-
lines
|
|
179
|
-
|
|
195
|
+
append_help_section(lines, t("help.examples_label"), exs.map do |ex|
|
|
196
|
+
" #{ex}"
|
|
197
|
+
end)
|
|
180
198
|
end
|
|
199
|
+
lines
|
|
200
|
+
end
|
|
181
201
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
lines.each { |ln| puts ln }
|
|
186
|
-
end
|
|
202
|
+
def append_help_section(lines, label, body_lines)
|
|
203
|
+
lines << " #{label}"
|
|
204
|
+
body_lines.each { |line| lines << line }
|
|
187
205
|
end
|
|
188
206
|
end
|
|
189
207
|
end
|
|
@@ -31,5 +31,99 @@ module VagrantDockerCertificatesManager
|
|
|
31
31
|
def nickname_for(name)
|
|
32
32
|
"#{MARKER}:#{name}"
|
|
33
33
|
end
|
|
34
|
+
|
|
35
|
+
def fingerprint_of(cert)
|
|
36
|
+
OpenSSL::Digest::SHA1.hexdigest(cert.to_der).upcase
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Generates a local development certificate authority.
|
|
40
|
+
#
|
|
41
|
+
# The generated CA is intended for local Docker/Vagrant workflows, not as a
|
|
42
|
+
# public browser-trusted certificate authority.
|
|
43
|
+
#
|
|
44
|
+
# @param cn [String] Common name for the CA certificate.
|
|
45
|
+
# @param days [Integer] Validity period in days.
|
|
46
|
+
# @param org [String] Organization attribute written to the subject.
|
|
47
|
+
# @param country [String] Country attribute written to the subject.
|
|
48
|
+
# @return [Array<OpenSSL::X509::Certificate, OpenSSL::PKey::RSA>] Generated certificate and private key.
|
|
49
|
+
def generate_ca(common_name:, days: 3650, org: "VDCM", country: "FR")
|
|
50
|
+
key = OpenSSL::PKey::RSA.generate(2048)
|
|
51
|
+
cert = OpenSSL::X509::Certificate.new
|
|
52
|
+
|
|
53
|
+
cert.version = 2
|
|
54
|
+
cert.serial = 1
|
|
55
|
+
cert.subject = OpenSSL::X509::Name.parse("/CN=#{common_name}/O=#{org}/C=#{country}")
|
|
56
|
+
cert.issuer = cert.subject
|
|
57
|
+
cert.public_key = key.public_key
|
|
58
|
+
cert.not_before = Time.now
|
|
59
|
+
cert.not_after = Time.now + (days * 24 * 60 * 60)
|
|
60
|
+
|
|
61
|
+
ef = OpenSSL::X509::ExtensionFactory.new(cert, cert)
|
|
62
|
+
cert.add_extension(ef.create_extension("basicConstraints", "CA:TRUE", true))
|
|
63
|
+
cert.add_extension(ef.create_extension("keyUsage", "keyCertSign,cRLSign", true))
|
|
64
|
+
cert.add_extension(ef.create_extension("subjectKeyIdentifier", "hash"))
|
|
65
|
+
cert.sign(key, OpenSSL::Digest.new("SHA256"))
|
|
66
|
+
|
|
67
|
+
[cert, key]
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def load_ca(cert_path, key_path)
|
|
71
|
+
[
|
|
72
|
+
OpenSSL::X509::Certificate.new(File.read(cert_path)),
|
|
73
|
+
OpenSSL::PKey::RSA.new(File.read(key_path))
|
|
74
|
+
]
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# Generates a server certificate signed by the supplied local CA.
|
|
78
|
+
#
|
|
79
|
+
# Modern clients validate the SAN extension; the CN is kept mainly for
|
|
80
|
+
# readability and compatibility with older tooling.
|
|
81
|
+
#
|
|
82
|
+
# @param ca_cert [OpenSSL::X509::Certificate] CA certificate used as issuer.
|
|
83
|
+
# @param ca_key [OpenSSL::PKey::RSA] CA private key used to sign the certificate.
|
|
84
|
+
# @param domain [String] DNS name written to CN and subjectAltName.
|
|
85
|
+
# @param days [Integer] Validity period in days.
|
|
86
|
+
# @param crl_url [String, nil] Optional CRL distribution point URL.
|
|
87
|
+
# @return [Array<OpenSSL::X509::Certificate, OpenSSL::PKey::RSA>] Generated certificate and private key.
|
|
88
|
+
def generate_server(ca_cert, ca_key, domain:, days: 825, crl_url: nil)
|
|
89
|
+
key = OpenSSL::PKey::RSA.generate(2048)
|
|
90
|
+
cert = OpenSSL::X509::Certificate.new
|
|
91
|
+
|
|
92
|
+
cert.version = 2
|
|
93
|
+
cert.serial = 2
|
|
94
|
+
cert.subject = OpenSSL::X509::Name.parse("/CN=#{domain}")
|
|
95
|
+
cert.issuer = ca_cert.subject
|
|
96
|
+
cert.public_key = key.public_key
|
|
97
|
+
cert.not_before = Time.now
|
|
98
|
+
cert.not_after = Time.now + (days * 24 * 60 * 60)
|
|
99
|
+
|
|
100
|
+
ef = OpenSSL::X509::ExtensionFactory.new(ca_cert, cert)
|
|
101
|
+
cert.add_extension(ef.create_extension("subjectAltName", "DNS:#{domain}"))
|
|
102
|
+
cert.add_extension(ef.create_extension("basicConstraints", "CA:FALSE"))
|
|
103
|
+
cert.add_extension(ef.create_extension("keyUsage", "digitalSignature,keyEncipherment", true))
|
|
104
|
+
cert.add_extension(ef.create_extension("extendedKeyUsage", "serverAuth"))
|
|
105
|
+
unless crl_url.to_s.strip.empty?
|
|
106
|
+
cert.add_extension(ef.create_extension("crlDistributionPoints", "URI:#{crl_url}"))
|
|
107
|
+
end
|
|
108
|
+
cert.sign(ca_key, OpenSSL::Digest.new("SHA256"))
|
|
109
|
+
|
|
110
|
+
[cert, key]
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# Generates an empty certificate revocation list for the local CA.
|
|
114
|
+
#
|
|
115
|
+
# @param ca_cert [OpenSSL::X509::Certificate] CA certificate used as issuer.
|
|
116
|
+
# @param ca_key [OpenSSL::PKey::RSA] CA private key used to sign the CRL.
|
|
117
|
+
# @param days [Integer] Validity period in days.
|
|
118
|
+
# @return [OpenSSL::X509::CRL] Signed CRL.
|
|
119
|
+
def generate_crl(ca_cert, ca_key, days: 3650)
|
|
120
|
+
crl = OpenSSL::X509::CRL.new
|
|
121
|
+
crl.issuer = ca_cert.subject
|
|
122
|
+
crl.version = 1
|
|
123
|
+
crl.last_update = Time.now
|
|
124
|
+
crl.next_update = Time.now + (days * 24 * 60 * 60)
|
|
125
|
+
crl.sign(ca_key, OpenSSL::Digest.new("SHA256"))
|
|
126
|
+
crl
|
|
127
|
+
end
|
|
34
128
|
end
|
|
35
129
|
end
|
|
@@ -15,6 +15,12 @@ module VagrantDockerCertificatesManager
|
|
|
15
15
|
FileUtils.mkdir_p(File.dirname(db_path))
|
|
16
16
|
end
|
|
17
17
|
|
|
18
|
+
# Loads tracked certificates from the best-effort local registry.
|
|
19
|
+
#
|
|
20
|
+
# A corrupt or missing registry must not block Vagrant actions, because the
|
|
21
|
+
# OS trust store remains the source of truth.
|
|
22
|
+
#
|
|
23
|
+
# @return [Hash] Registry data keyed by certificate fingerprint.
|
|
18
24
|
def load
|
|
19
25
|
ensure_dir!
|
|
20
26
|
return {} unless File.exist?(db_path)
|
|
@@ -28,6 +34,11 @@ module VagrantDockerCertificatesManager
|
|
|
28
34
|
File.write(db_path, JSON.pretty_generate(data))
|
|
29
35
|
end
|
|
30
36
|
|
|
37
|
+
# Tracks a certificate fingerprint and its ownership metadata.
|
|
38
|
+
#
|
|
39
|
+
# @param fp [String] Certificate fingerprint.
|
|
40
|
+
# @param attrs [Hash] Persisted metadata such as path, name, OS, and owners.
|
|
41
|
+
# @return [void]
|
|
31
42
|
def track(fp, attrs)
|
|
32
43
|
data = load
|
|
33
44
|
data[fp] = attrs
|
|
@@ -41,6 +52,42 @@ module VagrantDockerCertificatesManager
|
|
|
41
52
|
removed
|
|
42
53
|
end
|
|
43
54
|
|
|
55
|
+
# Adds an owner to an already tracked certificate.
|
|
56
|
+
#
|
|
57
|
+
# @param fp [String] Certificate fingerprint.
|
|
58
|
+
# @param owner [String, #to_s] Vagrant machine id adopting the certificate.
|
|
59
|
+
# @return [Boolean] Whether the certificate was already tracked.
|
|
60
|
+
# rubocop:disable Naming/PredicateMethod -- boolean return kept for symmetry with #release
|
|
61
|
+
def adopt(fp, owner)
|
|
62
|
+
data = load
|
|
63
|
+
return false unless data[fp]
|
|
64
|
+
data[fp]["owners"] = Array(data[fp]["owners"]) | [owner.to_s]
|
|
65
|
+
save(data)
|
|
66
|
+
true
|
|
67
|
+
end
|
|
68
|
+
# rubocop:enable Naming/PredicateMethod
|
|
69
|
+
|
|
70
|
+
# Releases one owner from a tracked certificate.
|
|
71
|
+
#
|
|
72
|
+
# Keep the certificate installed while at least one Vagrant machine still
|
|
73
|
+
# owns it.
|
|
74
|
+
#
|
|
75
|
+
# @param fp [String] Certificate fingerprint.
|
|
76
|
+
# @param owner [String, #to_s] Vagrant machine id releasing the certificate.
|
|
77
|
+
# @return [Boolean] Whether other owners remain after the release.
|
|
78
|
+
# rubocop:disable Naming/PredicateMethod -- boolean return kept for symmetry with #adopt
|
|
79
|
+
def release(fp, owner)
|
|
80
|
+
data = load
|
|
81
|
+
rec = data[fp]
|
|
82
|
+
return false unless rec
|
|
83
|
+
owners = Array(rec["owners"])
|
|
84
|
+
owners.delete(owner.to_s)
|
|
85
|
+
rec["owners"] = owners
|
|
86
|
+
save(data)
|
|
87
|
+
!owners.empty?
|
|
88
|
+
end
|
|
89
|
+
# rubocop:enable Naming/PredicateMethod
|
|
90
|
+
|
|
44
91
|
def find_by_path(path)
|
|
45
92
|
data = load
|
|
46
93
|
data.find { |_fp, v| File.expand_path(v["path"]) == File.expand_path(path) }
|
|
@@ -49,5 +96,9 @@ module VagrantDockerCertificatesManager
|
|
|
49
96
|
def all
|
|
50
97
|
load
|
|
51
98
|
end
|
|
99
|
+
|
|
100
|
+
def others_for_os?(fp, os)
|
|
101
|
+
load.any? { |k, v| k != fp && v["os"].to_s == os.to_s }
|
|
102
|
+
end
|
|
52
103
|
end
|
|
53
104
|
end
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: vagrant-docker-certificates-manager
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.4.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Julien Poirou
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-
|
|
11
|
+
date: 2026-07-04 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: i18n
|