easy_caddy 0.1.2 → 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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1c38478a91bcf7a5ef93a10693e73d829b74ab1e88709fb38112c1120d30e499
4
- data.tar.gz: c4923a78a738fdd89079df4f80f3f35ac3185a421be7b2e3ae3509d985168790
3
+ metadata.gz: f79d1b7b22e30b050b203bfe25656423ab378073dab52891326bce74646ed846
4
+ data.tar.gz: 412188255ada0400ddadd06e4c737d6863a6f6277ee3202e99e380421fcdee2c
5
5
  SHA512:
6
- metadata.gz: 59e2de015249c22dc56aa731e9a059655a9548d27582947a6d1ba38bffd713950fc23db4e21867ffee2cb2ae7d6e36bd13a8fe4eff4356e06b25115df2f326c8
7
- data.tar.gz: 4a2226c3ad545bdd047dfcb896636eaf52a9fa1bae1a8984d68a7bbf84300e0dc9fcc9c61fc64fe259f994c37b1d3f8fd12e4d413764e6b3775577302f569937
6
+ metadata.gz: 68a99984be74ff92c495a7edbe02a5bae90ae8d695bfc27746f490dd1ab6932df08bc8d60e2c5d449f0c11c79a3242286b7d441c992e3bd496ba303b8f188b27
7
+ data.tar.gz: c2d2bddb738808abdbe2d5b95402d96131e8ca83dc9c6150bcc12bbd4072951692f836aa54f8afa4257daf8f20f6de1b856348801c24d52109a75b392a912eb7
data/CHANGELOG.md CHANGED
@@ -5,6 +5,27 @@ 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.4] — 2026-06-16
9
+
10
+ ### Added
11
+
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`).
28
+
8
29
  ## [0.1.2] — 2026-06-09
9
30
 
10
31
  ### Added
@@ -63,5 +84,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
63
84
  `SIGTERM`/`SIGINT`, and unregisters on exit — designed to drop into a
64
85
  Procfile alongside the Rails server.
65
86
 
87
+ [0.1.4]: https://github.com/pniemczyk/easy_caddy/releases/tag/v0.1.4
66
88
  [0.1.2]: https://github.com/pniemczyk/easy_caddy/releases/tag/v0.1.2
67
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.
@@ -268,11 +305,29 @@ ecaddy reload
268
305
 
269
306
  ---
270
307
 
308
+ ### `ecaddy retrust`
309
+
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.
314
+
315
+ ```bash
316
+ ecaddy retrust
317
+ ```
318
+
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.
323
+
324
+ ---
325
+
271
326
  ### `ecaddy version`
272
327
 
273
328
  ```bash
274
329
  ecaddy version
275
- # ecaddy 0.1.2
330
+ # ecaddy 0.1.4
276
331
  ```
277
332
 
278
333
  ## Global config layout
@@ -78,6 +78,16 @@ module EasyCaddy
78
78
  [out, $CHILD_STATUS.success?]
79
79
  end
80
80
 
81
+ def self.untrust
82
+ system("#{BINARY} untrust")
83
+ end
84
+
85
+ # @return [Array(String, Boolean)] combined output and whether the command succeeded
86
+ def self.untrust_with_output
87
+ out = `#{BINARY} untrust 2>&1`
88
+ [out, $CHILD_STATUS.success?]
89
+ end
90
+
81
91
  ADMIN_ENDPOINT = 'http://localhost:2019/pki/ca/local'
82
92
 
83
93
  # Polls Caddy's admin API until it responds or the timeout elapses.
@@ -15,6 +15,7 @@ require_relative 'commands/ensure'
15
15
  require_relative 'commands/run'
16
16
  require_relative 'commands/logs'
17
17
  require_relative 'commands/audit'
18
+ require_relative 'commands/retrust'
18
19
 
19
20
  module EasyCaddy
20
21
  class CLI < Thor
@@ -99,6 +100,11 @@ module EasyCaddy
99
100
  Commands::Audit.new(site: options[:site], fix: options[:fix]).call
100
101
  end
101
102
 
103
+ desc 'retrust', 'Re-trust local CA and restart Caddy to reissue certs (fixes net::ERR_CERT_DATE_INVALID)'
104
+ def retrust
105
+ Commands::Retrust.new.call
106
+ end
107
+
102
108
  desc 'version', 'Print ecaddy version'
103
109
  def version
104
110
  puts "ecaddy #{EasyCaddy::VERSION}"
@@ -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 runs as root and created this 0600 log; `caddy validate` runs as ' \
415
- 'you and cannot open it. Make it group-writable.',
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: "Make the log group-writable (chmod #{Caddy::LOG_FILE_MODE})",
424
- command: "chmod #{Caddy::LOG_FILE_MODE} #{path}",
425
- verify: -> { File.writable?(path) },
426
- escalation: "You don't own this file needs sudo.",
427
- next_fix: Fix.new(
428
- label: "#{path} still not writable",
429
- description: 'chmod the root-owned log via sudo',
430
- command: "sudo chmod #{Caddy::LOG_FILE_MODE} #{path}",
431
- verify: -> { File.writable?(path) },
432
- escalation: "Still not writable. Check `ls -l #{path}`; " \
433
- "you may need `sudo chown $USER:staff #{path}`.",
434
- next_fix: nil
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
- cn = ssl.peer_cert&.subject&.to_a&.find { |name, _| name == 'CN' }&.at(1) || '?'
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
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../caddy'
4
+ require_relative '../error'
5
+
6
+ module EasyCaddy
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.
10
+ class Retrust
11
+ # rubocop:disable Metrics/MethodLength
12
+ def call
13
+ raise Error, 'Caddy is not running. Start it with: brew services start caddy' unless Caddy.running?
14
+
15
+ puts ' Removing local CA from trust store...'
16
+ puts ' (you may be prompted for your password)'
17
+ output, success = Caddy.untrust_with_output
18
+ raise Error, "caddy untrust failed:\n#{output}" unless success
19
+
20
+ puts ' Re-trusting local CA...'
21
+ puts ' (you may be prompted for your password)'
22
+ output, success = Caddy.trust_with_output
23
+ raise Error, "caddy trust failed:\n#{output}" unless success
24
+
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.'
33
+ end
34
+ # rubocop:enable Metrics/MethodLength
35
+ end
36
+ end
37
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module EasyCaddy
4
- VERSION = '0.1.2'
4
+ VERSION = '0.1.4'
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: easy_caddy
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.2
4
+ version: 0.1.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Pawel Niemczyk
@@ -134,6 +134,7 @@ files:
134
134
  - lib/easy_caddy/commands/register_helpers.rb
135
135
  - lib/easy_caddy/commands/reload.rb
136
136
  - lib/easy_caddy/commands/remove.rb
137
+ - lib/easy_caddy/commands/retrust.rb
137
138
  - lib/easy_caddy/commands/run.rb
138
139
  - lib/easy_caddy/commands/setup.rb
139
140
  - lib/easy_caddy/commands/status.rb