Module: Portless::CaBundle

Defined in:
lib/portless/ca_bundle.rb

Overview

The CA bundle handed to proxied apps via SSL_CERT_FILE.

SSL_CERT_FILE replaces OpenSSL's trust file rather than adding to it, so pointing an app at our local CA alone makes it distrust every public certificate — Frankfurter, Stripe, S3, GitHub — and all outbound HTTPS from the dev server fails with "unable to get local issuer certificate". We instead assemble a combined bundle (the system's default roots + our CA) and point apps at that, so they trust the public web and sibling .localhost hosts served under our CA.

Constant Summary collapse

FALLBACK_ROOTS =

Well-known system trust stores, tried when OpenSSL's compiled-in default isn't readable (varies by distro / OpenSSL build).

[
  "/etc/ssl/cert.pem",                     # macOS, OpenBSD
  "/etc/ssl/certs/ca-certificates.crt",    # Debian, Ubuntu, Alpine
  "/etc/pki/tls/certs/ca-bundle.crt",      # Fedora, RHEL
  "/etc/ssl/ca-bundle.pem"                 # openSUSE
].freeze

Class Method Summary collapse

Class Method Details

.bundle_pathObject



42
# File 'lib/portless/ca_bundle.rb', line 42

def bundle_path = State.path("ca-bundle.pem")

.pathObject

Absolute path to the combined bundle, (re)building it when missing or older than either input. Returns nil when our CA doesn't exist yet or assembly fails, so the caller simply omits the SSL_CERT_FILE override (apps keep their own defaults) rather than shipping a broken trust store.



32
33
34
35
36
37
38
39
40
# File 'lib/portless/ca_bundle.rb', line 32

def path
  return nil unless File.exist?(State.ca_cert)

  bundle = bundle_path
  rebuild(bundle) if stale?(bundle)
  File.exist?(bundle) ? bundle : nil
rescue StandardError
  nil
end

.rebuild(bundle) ⇒ Object



51
52
53
54
55
56
57
58
59
60
61
62
63
64
# File 'lib/portless/ca_bundle.rb', line 51

def rebuild(bundle)
  State.ensure_dir!
  pems = []
  roots = system_roots
  pems << File.read(roots) if roots && File.exist?(roots)
  pems << File.read(State.ca_cert)

  # Atomic write: concurrent app starts may both rebuild; a temp file + rename
  # keeps any reader from seeing a half-written bundle.
  tmp = "#{bundle}.#{Process.pid}.tmp"
  File.write(tmp, "#{pems.join("\n").strip}\n")
  File.rename(tmp, bundle)
  State.fix_ownership(bundle)
end

.stale?(bundle) ⇒ Boolean

Returns:

  • (Boolean)


44
45
46
47
48
49
# File 'lib/portless/ca_bundle.rb', line 44

def stale?(bundle)
  return true unless File.exist?(bundle)

  inputs = [ State.ca_cert, system_roots ].compact.select { |f| File.exist?(f) }
  inputs.empty? || inputs.any? { |f| File.mtime(f) > File.mtime(bundle) }
end

.system_rootsObject

The public root store OpenSSL would use by default, so our bundle is a superset of it. An operator-set SSL_CERT_FILE wins (but never our own bundle, which would read itself in circles).



69
70
71
72
73
74
75
76
77
# File 'lib/portless/ca_bundle.rb', line 69

def system_roots
  env = ENV["SSL_CERT_FILE"].to_s
  return env if !env.empty? && env != bundle_path && File.exist?(env)

  default = OpenSSL::X509::DEFAULT_CERT_FILE
  return default if default && File.exist?(default)

  FALLBACK_ROOTS.find { |f| File.exist?(f) }
end