acmesmith 2.8.0 → 2.9.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 +21 -2
- data/Dockerfile +7 -6
- data/README.md +24 -2
- data/config.sample.yml +15 -0
- data/lib/acmesmith/challenge_responder_filter.rb +4 -8
- data/lib/acmesmith/challenge_responders/pebble_challtestsrv_dns.rb +1 -1
- data/lib/acmesmith/client.rb +5 -0
- data/lib/acmesmith/command.rb +13 -0
- data/lib/acmesmith/config.rb +16 -0
- data/lib/acmesmith/ordering_service.rb +20 -3
- data/lib/acmesmith/subject_name_filter.rb +17 -0
- data/lib/acmesmith/version.rb +1 -1
- metadata +3 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 5230018ae89aac8b573c6353b1f447b078538a2ac3211e4d626eeba429a2a5af
|
|
4
|
+
data.tar.gz: a5ee69e253291d3ce838f248fed18ae5a29a01bd2aea4895b582e34ceeae5c9a
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 27e82c020a219babe961f13db21bf72ee6c3084ce16ef023da4efbf494d004a91def0cc0f7c96316f9540249e51a888b803fdc48a82970ef319afebe4de08ada
|
|
7
|
+
data.tar.gz: ab4c2e3277e4af4cbbed7de6087c11c11ee4c0d741892a4da6ea458297fd726db87e49a0e73d32dff52111d61c4fdc60db04a775db70fcb2e06c05cf997f2454
|
data/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,22 @@
|
|
|
1
|
+
## [unreleased]
|
|
2
|
+
|
|
3
|
+
## v2.9.0 (2026-03-14)
|
|
4
|
+
|
|
5
|
+
### Enhancements
|
|
6
|
+
|
|
7
|
+
- `acmesmith order` and configuration file gain `profiles` configuration to allow switching ACME profiles such as `shortlived` [#81](https://github.com/sorah/acmesmith/pull/81)
|
|
8
|
+
|
|
9
|
+
```yaml
|
|
10
|
+
profiles:
|
|
11
|
+
- name: "shortlived"
|
|
12
|
+
filter:
|
|
13
|
+
subject_name_suffix: [".shortlived.example.invalid"]
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
### Changes
|
|
17
|
+
|
|
18
|
+
- Prebuilt Docker images are no longer pushed to Dockerhub. Use GHCR instead: [ghcr.io/sorah/acmesmith](https://ghcr.io/sorah/acmesmith)
|
|
19
|
+
|
|
1
20
|
## v2.8.0 (2025-11-11)
|
|
2
21
|
|
|
3
22
|
### Enhancements
|
|
@@ -11,8 +30,8 @@
|
|
|
11
30
|
|
|
12
31
|
### Updates
|
|
13
32
|
|
|
14
|
-
- Update gemspec to the latest bundler's provided template. This removes certain irrevant files from a released gem package. [#
|
|
15
|
-
- Use rubygems.org trusted publishing via GitHub Actions [#
|
|
33
|
+
- Update gemspec to the latest bundler's provided template. This removes certain irrevant files from a released gem package. [#75](https://github.com/sorah/acmesmith/issues/75)
|
|
34
|
+
- Use rubygems.org trusted publishing via GitHub Actions [#75](https://github.com/sorah/acmesmith/issues/75)
|
|
16
35
|
|
|
17
36
|
## v2.7.1 (2025-08-21)
|
|
18
37
|
|
data/Dockerfile
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
FROM sorah/ruby:3.4-dev as builder
|
|
1
|
+
FROM ghcr.io/sorah-rbpkg/ruby:3.4-dev as builder
|
|
2
2
|
|
|
3
3
|
#RUN apt-get update \
|
|
4
4
|
# && apt-get install -y libmysqlclient-dev git-core \
|
|
@@ -10,13 +10,14 @@ COPY Gemfile.lock /app/
|
|
|
10
10
|
COPY acmesmith.gemspec /app/
|
|
11
11
|
RUN sed -i -e 's|Acmesmith::VERSION|"0.0.0"|g' -e '/^require.*acmesmith.version/d' -e '/`git/d' acmesmith.gemspec
|
|
12
12
|
|
|
13
|
-
RUN
|
|
13
|
+
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
|
|
14
|
+
--mount=type=cache,target=/var/lib/apt/lists,sharing=locked \
|
|
15
|
+
apt-get update \
|
|
16
|
+
&& apt-get install -y --no-install-recommends libssl-dev
|
|
14
17
|
|
|
15
|
-
|
|
18
|
+
RUN bundle install --path /gems --jobs 100 --without development
|
|
16
19
|
|
|
17
|
-
|
|
18
|
-
# && apt-get install -y libmysqlclient20 \
|
|
19
|
-
# && rm -rf /var/lib/apt/lists/*
|
|
20
|
+
FROM ghcr.io/sorah-rbpkg/ruby:3.4
|
|
20
21
|
|
|
21
22
|
WORKDIR /app
|
|
22
23
|
COPY . /app/
|
data/README.md
CHANGED
|
@@ -42,7 +42,7 @@ docker run -v /path/to/acmesmith.yml:/app/acmesmith.yml:ro sorah/acmesmith:lates
|
|
|
42
42
|
|
|
43
43
|
[`Dockerfile`](./Dockerfile) is available. Default confguration file is at `/app/acmesmith.yml`.
|
|
44
44
|
|
|
45
|
-
Pre-built docker images are provided at https://
|
|
45
|
+
Pre-built docker images are provided at https://ghcr.io/sorah/acmesmith for your convenience
|
|
46
46
|
Built with GitHub Actions & [sorah-rbpkg/dockerfiles](https://github.com/sorah-rbpkg/dockerfiles).
|
|
47
47
|
|
|
48
48
|
## Usage
|
|
@@ -176,6 +176,28 @@ chain_preferences:
|
|
|
176
176
|
- '\Aapp\d+.example.org\z'
|
|
177
177
|
```
|
|
178
178
|
|
|
179
|
+
### Profiles
|
|
180
|
+
|
|
181
|
+
Some ACME CAs support [certificate profiles](https://datatracker.ietf.org/doc/draft-ietf-acme-profiles/) that allow selecting different certificate types (e.g., short-lived, classic). Use the `list-profiles` command to discover available profiles from your CA:
|
|
182
|
+
|
|
183
|
+
```
|
|
184
|
+
$ acmesmith list-profiles
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
To configure profile selection per domain, add `profiles` to your configuration. The first matching rule wins:
|
|
188
|
+
|
|
189
|
+
```yaml
|
|
190
|
+
profiles:
|
|
191
|
+
- name: "shortlived"
|
|
192
|
+
filter:
|
|
193
|
+
subject_name_suffix:
|
|
194
|
+
- .shortlived.example.com
|
|
195
|
+
- name: "classic"
|
|
196
|
+
# no filter = default/fallback
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
The `filter` block supports the same options as challenge responders: `subject_name_exact`, `subject_name_suffix`, and `subject_name_regexp`.
|
|
200
|
+
|
|
179
201
|
## Vendor dependent notes
|
|
180
202
|
|
|
181
203
|
- [./docs/vendor/aws.md](./docs/vendor/aws.md): IAM and KMS key policies, and some tips
|
|
@@ -195,7 +217,7 @@ bundle exec rspec
|
|
|
195
217
|
integration test using [letsencrypt/pebble](https://github.com/letsencrypt/pebble). needs Docker:
|
|
196
218
|
|
|
197
219
|
```
|
|
198
|
-
ACMESMITH_CI_START_PEBBLE=1
|
|
220
|
+
ACMESMITH_CI_START_PEBBLE=1 bundle exec rspec -t integration_pebble
|
|
199
221
|
```
|
|
200
222
|
|
|
201
223
|
## Writing plugins
|
data/config.sample.yml
CHANGED
|
@@ -38,6 +38,21 @@ challenge_responders:
|
|
|
38
38
|
# Last resort
|
|
39
39
|
- route53: {}
|
|
40
40
|
|
|
41
|
+
###
|
|
42
|
+
### Profiles (optional)
|
|
43
|
+
###
|
|
44
|
+
|
|
45
|
+
## ACME certificate profiles allow selecting different certificate types.
|
|
46
|
+
## Use `acmesmith list-profiles` to discover available profiles.
|
|
47
|
+
## First matching rule wins.
|
|
48
|
+
# profiles:
|
|
49
|
+
# - name: "shortlived"
|
|
50
|
+
# filter:
|
|
51
|
+
# subject_name_suffix:
|
|
52
|
+
# - .shortlived.example.com
|
|
53
|
+
# - name: "classic"
|
|
54
|
+
# # no filter = default/fallback
|
|
55
|
+
|
|
41
56
|
###
|
|
42
57
|
### advanced options
|
|
43
58
|
###
|
|
@@ -1,18 +1,14 @@
|
|
|
1
|
-
require 'acmesmith/
|
|
1
|
+
require 'acmesmith/subject_name_filter'
|
|
2
2
|
|
|
3
3
|
module Acmesmith
|
|
4
4
|
class ChallengeResponderFilter
|
|
5
|
-
def initialize(responder,
|
|
5
|
+
def initialize(responder, **filter)
|
|
6
6
|
@responder = responder
|
|
7
|
-
@
|
|
8
|
-
exact: subject_name_exact,
|
|
9
|
-
suffix: subject_name_suffix,
|
|
10
|
-
regexp: subject_name_regexp,
|
|
11
|
-
)
|
|
7
|
+
@subject_name_filter = SubjectNameFilter.new(**filter)
|
|
12
8
|
end
|
|
13
9
|
|
|
14
10
|
def applicable?(domain)
|
|
15
|
-
@
|
|
11
|
+
@subject_name_filter.match?(domain) && @responder.applicable?(domain)
|
|
16
12
|
end
|
|
17
13
|
end
|
|
18
14
|
end
|
|
@@ -42,7 +42,7 @@ module Acmesmith
|
|
|
42
42
|
end
|
|
43
43
|
|
|
44
44
|
def warn_test
|
|
45
|
-
unless ENV['
|
|
45
|
+
unless ENV['ACMESMITH_ACKNOWLEDGE_PEBBLE_CHALLTESTSRV_IS_INSECURE']
|
|
46
46
|
$stderr.puts '!!!!!!!!! WARNING WARNING WARNING !!!!!!!!!'
|
|
47
47
|
$stderr.puts '!!!! pebble-challtestsrv command is for TEST USAGE ONLY. It is trivially insecure, offering no authentication. Only use pebble-challtestsrv in a controlled test environment.'
|
|
48
48
|
$stderr.puts '!!!! https://github.com/letsencrypt/pebble/blob/master/cmd/pebble-challtestsrv/README.md'
|
data/lib/acmesmith/client.rb
CHANGED
|
@@ -30,6 +30,10 @@ module Acmesmith
|
|
|
30
30
|
raise NotImplementedError, "Domain authorization in advance is still not available in acme-client (v2). Required authorizations will be performed when ordering certificates"
|
|
31
31
|
end
|
|
32
32
|
|
|
33
|
+
def list_profiles
|
|
34
|
+
acme.profiles
|
|
35
|
+
end
|
|
36
|
+
|
|
33
37
|
def post_issue_hooks(name)
|
|
34
38
|
cert = load_certificate_from_storage(name)
|
|
35
39
|
execute_post_issue_hooks(cert)
|
|
@@ -221,6 +225,7 @@ module Acmesmith
|
|
|
221
225
|
private_key: private_key,
|
|
222
226
|
challenge_responder_rules: config.challenge_responders,
|
|
223
227
|
chain_preferences: config.chain_preferences,
|
|
228
|
+
profile_rules: config.profile_rules,
|
|
224
229
|
not_before: not_before,
|
|
225
230
|
not_after: not_after
|
|
226
231
|
)
|
data/lib/acmesmith/command.rb
CHANGED
|
@@ -54,6 +54,19 @@ module Acmesmith
|
|
|
54
54
|
end
|
|
55
55
|
map 'post-issue-hooks' => :post_issue_hooks
|
|
56
56
|
|
|
57
|
+
desc "list-profiles", "List available ACME certificate profiles"
|
|
58
|
+
def list_profiles
|
|
59
|
+
profiles = client.list_profiles
|
|
60
|
+
if profiles.nil? || profiles.empty?
|
|
61
|
+
puts "No profiles available from this ACME directory"
|
|
62
|
+
return
|
|
63
|
+
end
|
|
64
|
+
profiles.each do |name, description|
|
|
65
|
+
puts "#{name}: #{description}"
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
map 'list-profiles' => :list_profiles
|
|
69
|
+
|
|
57
70
|
desc "list [NAME]", "list certificates or its versions"
|
|
58
71
|
def list(name = nil)
|
|
59
72
|
if name
|
data/lib/acmesmith/config.rb
CHANGED
|
@@ -2,6 +2,7 @@ require 'yaml'
|
|
|
2
2
|
require 'acmesmith/storages'
|
|
3
3
|
require 'acmesmith/challenge_responders'
|
|
4
4
|
require 'acmesmith/challenge_responder_filter'
|
|
5
|
+
require 'acmesmith/subject_name_filter'
|
|
5
6
|
require 'acmesmith/domain_name_filter'
|
|
6
7
|
require 'acmesmith/post_issuing_hooks'
|
|
7
8
|
|
|
@@ -9,6 +10,7 @@ module Acmesmith
|
|
|
9
10
|
class Config
|
|
10
11
|
ChallengeResponderRule = Struct.new(:challenge_responder, :filter, keyword_init: true)
|
|
11
12
|
ChainPreference = Struct.new(:root_issuer_name, :root_issuer_key_id, :filter, keyword_init: true)
|
|
13
|
+
ProfileRule = Data.define(:name, :filter)
|
|
12
14
|
|
|
13
15
|
def self.load_yaml(path)
|
|
14
16
|
new YAML.load_file(path)
|
|
@@ -35,6 +37,10 @@ module Acmesmith
|
|
|
35
37
|
if @config.key?('chain_preferences') && !@config.fetch('chain_preferences').kind_of?(Array)
|
|
36
38
|
raise ArgumentError, "config['chain_preferences'] must be an Array"
|
|
37
39
|
end
|
|
40
|
+
|
|
41
|
+
if @config.key?('profiles') && !@config.fetch('profiles').kind_of?(Array)
|
|
42
|
+
raise ArgumentError, "config['profiles'] must be an Array"
|
|
43
|
+
end
|
|
38
44
|
end
|
|
39
45
|
|
|
40
46
|
def [](key)
|
|
@@ -124,6 +130,16 @@ module Acmesmith
|
|
|
124
130
|
end
|
|
125
131
|
end
|
|
126
132
|
|
|
133
|
+
def profile_rules
|
|
134
|
+
@profile_rules ||= begin
|
|
135
|
+
specs = @config['profiles'] || []
|
|
136
|
+
specs.map do |spec|
|
|
137
|
+
filter = spec.fetch('filter', {}).map { |k,v| [k.to_sym, v] }.to_h
|
|
138
|
+
ProfileRule.new(name: spec['name'], filter: SubjectNameFilter.new(**filter))
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
|
|
127
143
|
# def post_actions
|
|
128
144
|
# end
|
|
129
145
|
end
|
|
@@ -14,18 +14,21 @@ module Acmesmith
|
|
|
14
14
|
# @param chain_preferences [Array<Acmesmith::Config::ChainPreference>] chain_preferences
|
|
15
15
|
# @param not_before [Time]
|
|
16
16
|
# @param not_after [Time]
|
|
17
|
-
def initialize(acme:, common_name:, identifiers:, private_key:, challenge_responder_rules:, chain_preferences:, not_before: nil, not_after: nil)
|
|
17
|
+
def initialize(acme:, common_name:, identifiers:, private_key:, challenge_responder_rules:, chain_preferences:, profile_rules: [], not_before: nil, not_after: nil)
|
|
18
18
|
@acme = acme
|
|
19
19
|
@common_name = common_name
|
|
20
20
|
@identifiers = identifiers
|
|
21
21
|
@private_key = private_key
|
|
22
22
|
@challenge_responder_rules = challenge_responder_rules
|
|
23
23
|
@chain_preferences = chain_preferences
|
|
24
|
+
@profile_rules = profile_rules
|
|
24
25
|
@not_before = not_before
|
|
25
26
|
@not_after = not_after
|
|
27
|
+
|
|
28
|
+
@order_url = nil # https://github.com/unixcharles/acme-client/pull/263
|
|
26
29
|
end
|
|
27
30
|
|
|
28
|
-
attr_reader :acme, :common_name, :identifiers, :private_key, :challenge_responder_rules, :chain_preferences, :not_before, :not_after
|
|
31
|
+
attr_reader :acme, :common_name, :identifiers, :private_key, :challenge_responder_rules, :chain_preferences, :profile_rules, :not_before, :not_after
|
|
29
32
|
|
|
30
33
|
def perform!
|
|
31
34
|
puts "=> Ordering a certificate for the following identifiers:"
|
|
@@ -35,9 +38,15 @@ module Acmesmith
|
|
|
35
38
|
puts " * SAN: #{san}"
|
|
36
39
|
end
|
|
37
40
|
|
|
41
|
+
resolved_profile = profile
|
|
42
|
+
if resolved_profile
|
|
43
|
+
puts
|
|
44
|
+
puts " * Profile: #{resolved_profile}"
|
|
45
|
+
end
|
|
46
|
+
|
|
38
47
|
puts
|
|
39
48
|
puts "=> Placing an order"
|
|
40
|
-
@order = acme.new_order(identifiers: identifiers, not_before: not_before, not_after: not_after)
|
|
49
|
+
@order = acme.new_order(identifiers: identifiers, not_before: not_before, not_after: not_after, profile: resolved_profile)
|
|
41
50
|
puts " * URL: #{order.url}"
|
|
42
51
|
|
|
43
52
|
ensure_authorization()
|
|
@@ -72,12 +81,16 @@ module Acmesmith
|
|
|
72
81
|
puts
|
|
73
82
|
|
|
74
83
|
print " * Requesting..."
|
|
84
|
+
@order_url = order.url if defined?(Acme::Client::Error::OrderNotReloadable)
|
|
75
85
|
order.finalize(csr: csr)
|
|
76
86
|
puts" [ ok ]"
|
|
77
87
|
end
|
|
78
88
|
|
|
79
89
|
def wait_order_for_complete
|
|
90
|
+
# Workaround for https://github.com/unixcharles/acme-client/pull/263
|
|
91
|
+
|
|
80
92
|
while %w(ready processing).include?(order.status)
|
|
93
|
+
order.instance_variable_set(:@url, @order_url) if @order_url
|
|
81
94
|
order.reload()
|
|
82
95
|
puts " * Waiting for complete: status=#{order.status}"
|
|
83
96
|
sleep 2
|
|
@@ -104,6 +117,10 @@ module Acmesmith
|
|
|
104
117
|
identifiers[1..-1]
|
|
105
118
|
end
|
|
106
119
|
|
|
120
|
+
def profile
|
|
121
|
+
profile_rules.find { |rule| rule.filter.match?(common_name) }&.name
|
|
122
|
+
end
|
|
123
|
+
|
|
107
124
|
# @return [Acme::Client::CertificateRequest]
|
|
108
125
|
def csr
|
|
109
126
|
@csr ||= Acme::Client::CertificateRequest.new(subject: { common_name: common_name }, names: sans, private_key: private_key)
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
require 'acmesmith/domain_name_filter'
|
|
2
|
+
|
|
3
|
+
module Acmesmith
|
|
4
|
+
class SubjectNameFilter
|
|
5
|
+
def initialize(subject_name_exact: nil, subject_name_suffix: nil, subject_name_regexp: nil)
|
|
6
|
+
@domain_name_filter = DomainNameFilter.new(
|
|
7
|
+
exact: subject_name_exact,
|
|
8
|
+
suffix: subject_name_suffix,
|
|
9
|
+
regexp: subject_name_regexp,
|
|
10
|
+
)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def match?(domain)
|
|
14
|
+
@domain_name_filter.match?(domain)
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
data/lib/acmesmith/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: acmesmith
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 2.
|
|
4
|
+
version: 2.9.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Sorah Fukumori
|
|
@@ -183,6 +183,7 @@ files:
|
|
|
183
183
|
- lib/acmesmith/storages/base.rb
|
|
184
184
|
- lib/acmesmith/storages/filesystem.rb
|
|
185
185
|
- lib/acmesmith/storages/s3.rb
|
|
186
|
+
- lib/acmesmith/subject_name_filter.rb
|
|
186
187
|
- lib/acmesmith/utils/finder.rb
|
|
187
188
|
- lib/acmesmith/version.rb
|
|
188
189
|
homepage: https://github.com/sorah/acmesmith
|
|
@@ -206,7 +207,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
206
207
|
- !ruby/object:Gem::Version
|
|
207
208
|
version: '0'
|
|
208
209
|
requirements: []
|
|
209
|
-
rubygems_version:
|
|
210
|
+
rubygems_version: 4.0.3
|
|
210
211
|
specification_version: 4
|
|
211
212
|
summary: ACME client (Let's encrypt client) to manage certificate in multi server
|
|
212
213
|
environment with cloud services (e.g. AWS)
|