easy_caddy 0.1.3 → 0.1.4
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 +18 -6
- data/README.md +46 -6
- data/lib/easy_caddy/cli.rb +1 -1
- data/lib/easy_caddy/commands/audit.rb +98 -23
- data/lib/easy_caddy/commands/retrust.rb +12 -1
- data/lib/easy_caddy/version.rb +1 -1
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: f79d1b7b22e30b050b203bfe25656423ab378073dab52891326bce74646ed846
|
|
4
|
+
data.tar.gz: 412188255ada0400ddadd06e4c737d6863a6f6277ee3202e99e380421fcdee2c
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 68a99984be74ff92c495a7edbe02a5bae90ae8d695bfc27746f490dd1ab6932df08bc8d60e2c5d449f0c11c79a3242286b7d441c992e3bd496ba303b8f188b27
|
|
7
|
+
data.tar.gz: c2d2bddb738808abdbe2d5b95402d96131e8ca83dc9c6150bcc12bbd4072951692f836aa54f8afa4257daf8f20f6de1b856348801c24d52109a75b392a912eb7
|
data/CHANGELOG.md
CHANGED
|
@@ -5,14 +5,26 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
-
## [0.1.
|
|
8
|
+
## [0.1.4] — 2026-06-16
|
|
9
9
|
|
|
10
10
|
### Added
|
|
11
11
|
|
|
12
|
-
- `ecaddy retrust` — re-trust the local Caddy CA certificate
|
|
13
|
-
then `caddy trust
|
|
14
|
-
|
|
15
|
-
|
|
12
|
+
- `ecaddy retrust` — re-trust the local Caddy CA certificate and reissue certs.
|
|
13
|
+
Runs `caddy untrust` then `caddy trust`, then restarts Caddy so it reissues the
|
|
14
|
+
short-lived `*.localhost` leaf certs — fixing `net::ERR_CERT_DATE_INVALID` (a stale
|
|
15
|
+
cached leaf) and authority errors in one step. The trust steps trigger the native
|
|
16
|
+
macOS password prompt.
|
|
17
|
+
- `ecaddy audit` now detects a leaf certificate that is outside its validity window
|
|
18
|
+
(expired or not-yet-valid) and reports it as `ERR_CERT_DATE_INVALID` instead of a
|
|
19
|
+
false "browser-trusted ✓". `audit --fix` offers a restart that escalates to
|
|
20
|
+
`ecaddy retrust`.
|
|
21
|
+
|
|
22
|
+
### Changed
|
|
23
|
+
|
|
24
|
+
- `ecaddy audit --fix` now resolves a root-owned, unwritable log file via an
|
|
25
|
+
interactive choice — keep as-is, take ownership (`sudo chown`), or delete — rather
|
|
26
|
+
than forcing a `chmod`. The finding also points to re-registering the site as the
|
|
27
|
+
durable fix (the fragment is rewritten with `mode 0660`).
|
|
16
28
|
|
|
17
29
|
## [0.1.2] — 2026-06-09
|
|
18
30
|
|
|
@@ -72,6 +84,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
72
84
|
`SIGTERM`/`SIGINT`, and unregisters on exit — designed to drop into a
|
|
73
85
|
Procfile alongside the Rails server.
|
|
74
86
|
|
|
75
|
-
[0.1.
|
|
87
|
+
[0.1.4]: https://github.com/pniemczyk/easy_caddy/releases/tag/v0.1.4
|
|
76
88
|
[0.1.2]: https://github.com/pniemczyk/easy_caddy/releases/tag/v0.1.2
|
|
77
89
|
[0.1.0]: https://github.com/pniemczyk/easy_caddy/releases/tag/v0.1.0
|
data/README.md
CHANGED
|
@@ -235,6 +235,27 @@ Exits `0` if all clear or only INFO findings. Exits `1` on any BLOCK.
|
|
|
235
235
|
|
|
236
236
|
---
|
|
237
237
|
|
|
238
|
+
### `ecaddy audit`
|
|
239
|
+
|
|
240
|
+
Full system + TLS audit with optional fixes. Where `doctor` checks the registry,
|
|
241
|
+
`audit` also probes the live Caddy service, the brew-service state, a TLS handshake
|
|
242
|
+
per domain, and the system-keychain trust state.
|
|
243
|
+
|
|
244
|
+
```bash
|
|
245
|
+
ecaddy audit # report-only
|
|
246
|
+
ecaddy audit --fix # prompt to run each suggested fix
|
|
247
|
+
ecaddy audit --site fishme # limit to one site
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
With `--fix`, `audit` walks each finding, prints the proposed command, asks for
|
|
251
|
+
confirmation, runs it, and re-verifies — chaining to a fallback fix when the first
|
|
252
|
+
doesn't resolve it (e.g. `caddy trust` → `sudo caddy trust`). It also flags leaf
|
|
253
|
+
certs outside their validity window as `ERR_CERT_DATE_INVALID` (fix: restart →
|
|
254
|
+
`ecaddy retrust`), and for a root-owned, unwritable log file it offers a choice —
|
|
255
|
+
keep as-is, take ownership (`sudo chown`), or delete.
|
|
256
|
+
|
|
257
|
+
---
|
|
258
|
+
|
|
238
259
|
### `ecaddy edit NAME`
|
|
239
260
|
|
|
240
261
|
Open a site's fragment in `$EDITOR`. Caddy is validated and reloaded after you save.
|
|
@@ -247,6 +268,22 @@ This edits the copy in `~/.config/caddy/sites/fishme.caddy`, not your project so
|
|
|
247
268
|
|
|
248
269
|
---
|
|
249
270
|
|
|
271
|
+
### `ecaddy logs --site NAME`
|
|
272
|
+
|
|
273
|
+
Tail a site's Caddy log files. `ecaddy` reads the fragment, extracts every
|
|
274
|
+
`output file PATH` directive, and shells out to `tail` on them.
|
|
275
|
+
|
|
276
|
+
```bash
|
|
277
|
+
ecaddy logs --site fishme # tail -F (follow)
|
|
278
|
+
ecaddy logs --site fishme --lines 100 # last 100 lines
|
|
279
|
+
ecaddy logs --site fishme --no-follow # print and exit
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
Works for both enabled and disabled sites. If the Caddyfile has no `output file`
|
|
283
|
+
directives, `ecaddy` prints guidance and exits.
|
|
284
|
+
|
|
285
|
+
---
|
|
286
|
+
|
|
250
287
|
### `ecaddy remove NAME`
|
|
251
288
|
|
|
252
289
|
Remove a site's fragment and registry entry entirely.
|
|
@@ -270,16 +307,19 @@ ecaddy reload
|
|
|
270
307
|
|
|
271
308
|
### `ecaddy retrust`
|
|
272
309
|
|
|
273
|
-
Re-trust the local Caddy CA
|
|
274
|
-
`net::ERR_CERT_DATE_INVALID`
|
|
275
|
-
has expired
|
|
310
|
+
Re-trust the local Caddy CA _and_ reissue certificates. Run this when your browser
|
|
311
|
+
shows `net::ERR_CERT_DATE_INVALID` or `NET::ERR_CERT_AUTHORITY_INVALID` on a
|
|
312
|
+
`*.localhost` site — the cached leaf cert has expired, or the local CA is missing
|
|
313
|
+
from the system keychain.
|
|
276
314
|
|
|
277
315
|
```bash
|
|
278
316
|
ecaddy retrust
|
|
279
317
|
```
|
|
280
318
|
|
|
281
|
-
Runs `caddy untrust` (removes the old cert) then `caddy trust` (re-installs it)
|
|
282
|
-
|
|
319
|
+
Runs `caddy untrust` (removes the old cert) then `caddy trust` (re-installs it),
|
|
320
|
+
then restarts Caddy so it reissues the short-lived `*.localhost` leaf certs. macOS
|
|
321
|
+
prompts for your password for each keychain operation. Afterwards, fully reload your
|
|
322
|
+
browser (or quit and reopen it) to drop the stale cached certificate.
|
|
283
323
|
|
|
284
324
|
---
|
|
285
325
|
|
|
@@ -287,7 +327,7 @@ macOS will prompt for your password for each keychain operation.
|
|
|
287
327
|
|
|
288
328
|
```bash
|
|
289
329
|
ecaddy version
|
|
290
|
-
# ecaddy 0.1.
|
|
330
|
+
# ecaddy 0.1.4
|
|
291
331
|
```
|
|
292
332
|
|
|
293
333
|
## Global config layout
|
data/lib/easy_caddy/cli.rb
CHANGED
|
@@ -100,7 +100,7 @@ module EasyCaddy
|
|
|
100
100
|
Commands::Audit.new(site: options[:site], fix: options[:fix]).call
|
|
101
101
|
end
|
|
102
102
|
|
|
103
|
-
desc 'retrust', 'Re-trust local CA
|
|
103
|
+
desc 'retrust', 'Re-trust local CA and restart Caddy to reissue certs (fixes net::ERR_CERT_DATE_INVALID)'
|
|
104
104
|
def retrust
|
|
105
105
|
Commands::Retrust.new.call
|
|
106
106
|
end
|
|
@@ -24,7 +24,15 @@ module EasyCaddy
|
|
|
24
24
|
YELLW = "\e[33m"
|
|
25
25
|
RESET = "\e[0m"
|
|
26
26
|
|
|
27
|
-
Fix = Data.define(:label, :description, :command, :verify, :escalation, :next_fix)
|
|
27
|
+
Fix = Data.define(:label, :description, :command, :verify, :escalation, :next_fix, :choices) do
|
|
28
|
+
def initialize(label:, description:, command:, verify:, escalation:, next_fix:, choices: nil)
|
|
29
|
+
super
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# A single labelled option offered when a Fix has multiple remedies (e.g. a root-owned log
|
|
34
|
+
# the user may want to keep, take ownership of, or delete). command: nil means "do nothing".
|
|
35
|
+
Choice = Data.define(:label, :command, :verify)
|
|
28
36
|
|
|
29
37
|
def initialize(site: nil, fix: false)
|
|
30
38
|
@site_filter = site
|
|
@@ -72,7 +80,8 @@ module EasyCaddy
|
|
|
72
80
|
command: fix[:command],
|
|
73
81
|
verify: fix[:verify],
|
|
74
82
|
escalation: fix[:escalation],
|
|
75
|
-
next_fix: fix[:next_fix]
|
|
83
|
+
next_fix: fix[:next_fix],
|
|
84
|
+
choices: fix[:choices]
|
|
76
85
|
)
|
|
77
86
|
end
|
|
78
87
|
# rubocop:enable Metrics/MethodLength
|
|
@@ -89,7 +98,8 @@ module EasyCaddy
|
|
|
89
98
|
command: fix[:command],
|
|
90
99
|
verify: fix[:verify],
|
|
91
100
|
escalation: fix[:escalation],
|
|
92
|
-
next_fix: fix[:next_fix]
|
|
101
|
+
next_fix: fix[:next_fix],
|
|
102
|
+
choices: fix[:choices]
|
|
93
103
|
)
|
|
94
104
|
end
|
|
95
105
|
# rubocop:enable Metrics/MethodLength
|
|
@@ -344,13 +354,21 @@ module EasyCaddy
|
|
|
344
354
|
end
|
|
345
355
|
|
|
346
356
|
domains.each do |domain|
|
|
347
|
-
handshake_ok, detail = tls_probe(domain)
|
|
357
|
+
handshake_ok, detail, cert = tls_probe(domain)
|
|
348
358
|
unless handshake_ok
|
|
349
359
|
hint, fix = tls_hint_and_fix(detail, domain)
|
|
350
360
|
fail("#{domain} — TLS ✗ #{detail}", hint: hint, fix: fix)
|
|
351
361
|
next
|
|
352
362
|
end
|
|
353
363
|
|
|
364
|
+
unless cert_date_valid?(cert)
|
|
365
|
+
fail("#{domain} — TLS ✓ but cert DATE INVALID (browser shows ERR_CERT_DATE_INVALID) #{detail}",
|
|
366
|
+
hint: 'The served leaf certificate is expired or not yet valid. ' \
|
|
367
|
+
'Restart Caddy to reissue it, then fully reload the browser.',
|
|
368
|
+
fix: cert_date_fix(domain))
|
|
369
|
+
next
|
|
370
|
+
end
|
|
371
|
+
|
|
354
372
|
if browser_trusts?(domain)
|
|
355
373
|
ok("#{domain} — TLS ✓ browser-trusted ✓ #{detail}")
|
|
356
374
|
else
|
|
@@ -362,6 +380,23 @@ module EasyCaddy
|
|
|
362
380
|
end
|
|
363
381
|
end
|
|
364
382
|
|
|
383
|
+
def cert_date_fix(domain)
|
|
384
|
+
{
|
|
385
|
+
description: 'Restart Caddy to reissue the leaf certificate',
|
|
386
|
+
command: 'brew services restart caddy',
|
|
387
|
+
verify: -> { cert_date_valid?(tls_probe(domain)[2]) },
|
|
388
|
+
escalation: 'Restart didn\'t refresh the cert — re-trust the CA and reissue in one step.',
|
|
389
|
+
next_fix: Fix.new(
|
|
390
|
+
label: "#{domain} cert still date-invalid — re-trusting CA and reissuing",
|
|
391
|
+
description: 'Full re-trust + restart',
|
|
392
|
+
command: 'ecaddy retrust',
|
|
393
|
+
verify: -> { cert_date_valid?(tls_probe(domain)[2]) },
|
|
394
|
+
escalation: 'Still invalid. Check your system clock, then try `sudo caddy untrust && sudo caddy trust`.',
|
|
395
|
+
next_fix: nil
|
|
396
|
+
)
|
|
397
|
+
}
|
|
398
|
+
end
|
|
399
|
+
|
|
365
400
|
def browser_trust_fix(domain)
|
|
366
401
|
{
|
|
367
402
|
description: 'Install Caddy CA into System keychain',
|
|
@@ -411,8 +446,8 @@ module EasyCaddy
|
|
|
411
446
|
ok("log #{path} (#{humanize_bytes(File.size(path))})")
|
|
412
447
|
else
|
|
413
448
|
fail("log #{path} — NOT writable by you (root-owned?)",
|
|
414
|
-
hint: 'Caddy
|
|
415
|
-
'
|
|
449
|
+
hint: 'Caddy created this 0600 log as root; `caddy validate` runs as you and can\'t open it. ' \
|
|
450
|
+
'Durable fix: re-register the site so its fragment carries `mode 0660`.',
|
|
416
451
|
fix: log_permission_fix(path))
|
|
417
452
|
end
|
|
418
453
|
end
|
|
@@ -420,19 +455,20 @@ module EasyCaddy
|
|
|
420
455
|
# rubocop:disable Metrics/MethodLength
|
|
421
456
|
def log_permission_fix(path)
|
|
422
457
|
{
|
|
423
|
-
description:
|
|
424
|
-
command:
|
|
425
|
-
verify: -> { File.writable?(path) },
|
|
426
|
-
escalation: "
|
|
427
|
-
next_fix:
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
458
|
+
description: 'Resolve the root-owned log file',
|
|
459
|
+
command: nil,
|
|
460
|
+
verify: -> { File.writable?(path) || !File.exist?(path) },
|
|
461
|
+
escalation: "Still not resolved. Check `ls -l #{path}`.",
|
|
462
|
+
next_fix: nil,
|
|
463
|
+
choices: [
|
|
464
|
+
Choice.new(label: 'Keep as-is (skip)', command: nil, verify: nil),
|
|
465
|
+
Choice.new(label: 'Update owner — take ownership (recommended)',
|
|
466
|
+
command: "sudo chown \"$USER:staff\" #{path}",
|
|
467
|
+
verify: -> { File.writable?(path) }),
|
|
468
|
+
Choice.new(label: 'Delete — remove the log (Caddy will recreate it)',
|
|
469
|
+
command: "sudo rm #{path}",
|
|
470
|
+
verify: -> { !File.exist?(path) })
|
|
471
|
+
]
|
|
436
472
|
}
|
|
437
473
|
end
|
|
438
474
|
# rubocop:enable Metrics/MethodLength
|
|
@@ -495,14 +531,24 @@ module EasyCaddy
|
|
|
495
531
|
ssl.hostname = domain
|
|
496
532
|
ssl.sync_close = true
|
|
497
533
|
ssl.connect
|
|
498
|
-
|
|
534
|
+
cert = ssl.peer_cert
|
|
535
|
+
cn = cert&.subject&.to_a&.find { |name, _| name == 'CN' }&.at(1) || '?'
|
|
499
536
|
ssl.close
|
|
500
|
-
[true, "cert CN=#{cn}"]
|
|
537
|
+
[true, "cert CN=#{cn}", cert]
|
|
501
538
|
end
|
|
502
539
|
rescue Errno::ECONNREFUSED
|
|
503
|
-
[false, 'connection refused on :443 (Caddy not running or not bound)']
|
|
540
|
+
[false, 'connection refused on :443 (Caddy not running or not bound)', nil]
|
|
504
541
|
rescue StandardError => e
|
|
505
|
-
[false, e.message.split("\n").first]
|
|
542
|
+
[false, e.message.split("\n").first, nil]
|
|
543
|
+
end
|
|
544
|
+
|
|
545
|
+
# True when the leaf cert is currently within its validity window. A cert outside it is
|
|
546
|
+
# exactly what the browser reports as ERR_CERT_DATE_INVALID.
|
|
547
|
+
def cert_date_valid?(cert)
|
|
548
|
+
return true if cert.nil? # can't tell — don't raise a false alarm
|
|
549
|
+
|
|
550
|
+
now = Time.now
|
|
551
|
+
now >= cert.not_before && now <= cert.not_after
|
|
506
552
|
end
|
|
507
553
|
# rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength
|
|
508
554
|
|
|
@@ -562,6 +608,8 @@ module EasyCaddy
|
|
|
562
608
|
return
|
|
563
609
|
end
|
|
564
610
|
|
|
611
|
+
return run_choice_fix(fix, prompt) if fix.choices
|
|
612
|
+
|
|
565
613
|
puts
|
|
566
614
|
puts " Issue: #{fix.label}"
|
|
567
615
|
puts " Fix: #{fix.description}"
|
|
@@ -597,6 +645,33 @@ module EasyCaddy
|
|
|
597
645
|
end
|
|
598
646
|
end
|
|
599
647
|
# rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/PerceivedComplexity
|
|
648
|
+
|
|
649
|
+
# rubocop:disable Metrics/AbcSize, Metrics/MethodLength
|
|
650
|
+
def run_choice_fix(fix, prompt)
|
|
651
|
+
puts
|
|
652
|
+
puts " Issue: #{fix.label}"
|
|
653
|
+
choice = prompt.select(" #{fix.description}:") do |menu|
|
|
654
|
+
fix.choices.each { |c| menu.choice c.label, c }
|
|
655
|
+
end
|
|
656
|
+
|
|
657
|
+
return if choice.command.nil? # "keep as-is" / skip
|
|
658
|
+
|
|
659
|
+
puts " Command: #{choice.command}"
|
|
660
|
+
unless system(choice.command)
|
|
661
|
+
puts " #{RED}✗#{RESET} command failed to run"
|
|
662
|
+
return
|
|
663
|
+
end
|
|
664
|
+
|
|
665
|
+
return puts(" #{GREEN}✓#{RESET} applied") if choice.verify.nil?
|
|
666
|
+
|
|
667
|
+
sleep 0.5
|
|
668
|
+
if choice.verify.call
|
|
669
|
+
puts " #{GREEN}✓#{RESET} applied and verified"
|
|
670
|
+
else
|
|
671
|
+
puts " #{YELLW}!#{RESET} applied, but the issue is still present"
|
|
672
|
+
end
|
|
673
|
+
end
|
|
674
|
+
# rubocop:enable Metrics/AbcSize, Metrics/MethodLength
|
|
600
675
|
end
|
|
601
676
|
# rubocop:enable Metrics/ClassLength
|
|
602
677
|
end
|
|
@@ -5,7 +5,10 @@ require_relative '../error'
|
|
|
5
5
|
|
|
6
6
|
module EasyCaddy
|
|
7
7
|
module Commands
|
|
8
|
+
# Removes and re-installs Caddy's local root CA, then restarts the service so it
|
|
9
|
+
# reissues fresh leaf certs — clearing browser ERR_CERT_DATE_INVALID / authority errors.
|
|
8
10
|
class Retrust
|
|
11
|
+
# rubocop:disable Metrics/MethodLength
|
|
9
12
|
def call
|
|
10
13
|
raise Error, 'Caddy is not running. Start it with: brew services start caddy' unless Caddy.running?
|
|
11
14
|
|
|
@@ -19,8 +22,16 @@ module EasyCaddy
|
|
|
19
22
|
output, success = Caddy.trust_with_output
|
|
20
23
|
raise Error, "caddy trust failed:\n#{output}" unless success
|
|
21
24
|
|
|
22
|
-
|
|
25
|
+
# Re-trusting only re-installs the root CA; it does not refresh the short-lived
|
|
26
|
+
# `*.localhost` leaf certs a browser may have cached as expired (ERR_CERT_DATE_INVALID).
|
|
27
|
+
# Restarting forces Caddy to reissue them.
|
|
28
|
+
puts ' Restarting Caddy to reissue certificates...'
|
|
29
|
+
raise Error, 'caddy restart failed — try: brew services restart caddy' unless Caddy.restart_service
|
|
30
|
+
|
|
31
|
+
puts ' Done. CA re-trusted and certificates reissued.'
|
|
32
|
+
puts ' Fully reload your browser (or quit and reopen it) to clear the cached certificate.'
|
|
23
33
|
end
|
|
34
|
+
# rubocop:enable Metrics/MethodLength
|
|
24
35
|
end
|
|
25
36
|
end
|
|
26
37
|
end
|
data/lib/easy_caddy/version.rb
CHANGED