Skip to content

Commit e597dcf

Browse files
akushskyclaude
andcommitted
DFLOW-152 - JVM client setup (macOS)
Adds a macOS client-side installer + validator that wires a corporate CA into the JVM trust path so Maven / Gradle / sbt / Apache Ivy traffic redirected through package-reroute validates correctly. JVM trust only — does not configure Node/npm/Python and does not touch Docker credentials. Pair with install_certs_macos.sh if you need those flows. Based on the published research wiki (DFLOW-136): https://jfrog-int.atlassian.net/wiki/spaces/RTFACT/pages/2440101931 Sibling to DFLOW-150 (Linux). Single path on macOS — there is no update-ca-trust fork because KeychainStore is broken per JDK-8321045: 1. Build a per-user JKS truststore at ~/Library/Application Support/JFrog/package-route-jvm/truststore.jks 2. Write a per-user LaunchAgent at ~/Library/LaunchAgents/com.jfrog.package-reroute.jto-env.plist calling `launchctl setenv JAVA_TOOL_OPTIONS=…` at RunAtLoad 3. Bootstrap the agent into `gui/<uid>` so Dock-launched IDEs (IntelliJ, JetBrains Toolbox, `open -a …`) inherit the env var The ~/.zshrc / ~/.bash_profile shortcut is deliberately NOT touched — verified in the research wiki to silently fail for GUI-launched IDE builds. Files: install_certs_jvm_macos.sh Installer (~540 lines). validate_certs_jvm_macos.sh Companion validator. _jvm_macos_paths.sh Shared constants sourced by both. testing/test_install_certs_jvm_macos.sh 12-invariant smoke runner. .github/workflows/ci.yml New test-macos-jvm job. README.md New "macOS (JVM)" section. Hardening fixes folded in from a round of pr-review-toolkit review (5 agents, 21 findings: 5 Critical + 13 Important + 3 Minor): Critical: - validate_pem leaf-cert (CA:FALSE) check now works on stock macOS LibreSSL. The previous `openssl x509 -ext` flag is OpenSSL 3.x-only; LibreSSL silently failed → leaf certs accepted as trust anchors. New path parses `-noout -text` output (portable across both). - chown failure capture at both call sites (JKS dir + plist). Under --all-users a silent chown would leave root-owned plists in a user's ~/Library/LaunchAgents/, which launchd silently refuses to load — exactly the phantom-success the project bans. - --cert-name semantics clarified in constants file + --help. The previous "LaunchAgent label suffix" claim was wishful thinking; the flag is alias-cosmetic only. - Test runner docstring rewritten (cleanup runs only on positive cases) and the iter_all_users "mirrors install_certs_macos.sh" claim corrected (it's a stricter filter, not a mirror). Important: - get_single_target_user filter+order matches install_certs_macos.sh (SUDO_USER → /dev/console → logname; rejects loginwindow pseudo-user). - get_user_home eval-on-username fallback replaced with dscacheutil. - bootstrap_launch_agent retry bumped 5×100ms → 20×100ms to survive EDR exec latency; warn text no longer suggests a logout dance. - Validator --help documents the absence of --cert-name. - Validator final summary qualifies "All checks passed" with a WARN count so a green exit doesn't over-promise when launchctl getenv was skipped (no GUI session). - install_as_test_user / validate_as_test_user capture combined stdout/stderr to a tempfile; dump on UNEXPECTED exit only. - Smoke matrix gained 3 new invariants: --all-users iteration (#11), plist content validation via plutil -extract (#10), validate_pem warn-paths for 30-day-expiry and multi-cert bundle (#12). - Test #9 (launchctl getenv) now retries 20×100ms to mirror the installer. - CI workflow uses explicit `sudo env JAVA_HOME=… PATH=…` instead of --preserve-env, surviving future actions/setup-java renames. PATH includes /usr/sbin so chown is resolvable under sudo. Minor: - README requirements list names plutil/launchctl/dscl/stat. - README "five launch contexts" claim scoped to what the test actually verifies. - JKS_PASSWORD "not a secret" comment scoped to trustedCertEntry-only stores (PrivateKeyEntry would change the calculus). Smoke matrix: 12/12 green locally on dev Mac, 12/12 green on CI (Test (macOS JVM) job, macos-latest runner with Temurin 21). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 3403ac6 commit e597dcf

6 files changed

Lines changed: 1420 additions & 0 deletions

File tree

.github/workflows/ci.yml

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,29 @@ jobs:
1818
- name: Run macOS tests
1919
run: sudo ./testing/test_install_certs_macos.sh
2020

21+
test-macos-jvm:
22+
name: Test (macOS JVM)
23+
runs-on: macos-latest
24+
steps:
25+
- uses: actions/checkout@v4
26+
27+
- uses: actions/setup-java@v4
28+
with:
29+
distribution: temurin
30+
java-version: '21'
31+
32+
# Pass JAVA_HOME and PATH explicitly to the sudo'd test runner rather
33+
# than relying on `sudo --preserve-env=…`: future actions/setup-java
34+
# versions could rename or rescope the JAVA_HOME env var, silently
35+
# breaking the preserve-by-name approach. Explicit `env VAR=…` is
36+
# the same idiom and survives such churn.
37+
- name: Run macOS JVM smoke matrix
38+
run: |
39+
sudo env \
40+
JAVA_HOME="$JAVA_HOME" \
41+
PATH="$JAVA_HOME/bin:/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin" \
42+
./testing/test_install_certs_jvm_macos.sh
43+
2144
test-windows:
2245
name: Test (Windows)
2346
runs-on: windows-latest

README.md

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,9 @@ Reference: research wiki [Maven Support in package-reroute (DFLOW-136 / DFLOW-11
2121
|--------|----------|---------|
2222
| **install_certs_macos.sh** | macOS | Install cert, set env vars (Node/Python), and clear Docker Hub credentials |
2323
| **validate_install_macos.sh** | macOS | Validate PEM and env config |
24+
| **install_certs_jvm_macos.sh** | macOS (JVM) | Install CA for Maven/Gradle/sbt/Ivy: JKS + per-user LaunchAgent setting `JAVA_TOOL_OPTIONS` |
25+
| **validate_certs_jvm_macos.sh** | macOS (JVM) | Validate JVM truststore install (JKS subject + plist + `launchctl getenv`) |
26+
| **_jvm_macos_paths.sh** | macOS (JVM) | Shared constants sourced by both installer and validator. Not directly executable. |
2427
| **install_certs_debian_ubuntu.sh** | Debian/Ubuntu | Install cert into system trust + profile.d + user shell rc + Docker cleanup |
2528
| **validate_certs_debian_ubuntu.sh** | Debian/Ubuntu | Validate PEM and env config |
2629
| **install_certs_jvm_linux.sh** | Linux (JVM) | Install CA for Maven/Gradle/sbt/Ivy: RHEL family → `update-ca-trust extract` into system anchors; others → per-host JKS + `JAVA_TOOL_OPTIONS` in `/etc/environment` |
@@ -317,6 +320,98 @@ Users must open a **new terminal** (or `source ~/.zshrc`) for the new environmen
317320

318321
---
319322

323+
## macOS (JVM): install_certs_jvm_macos.sh
324+
325+
### Overview
326+
327+
`install_certs_jvm_macos.sh` wires a custom CA certificate into the JVM trust path on macOS so Maven, Gradle, sbt, and Apache Ivy traffic redirected through `package-reroute` validates correctly. **JVM trust only** — does not configure Node/npm or Python, and does not touch Docker credentials. Pair with `install_certs_macos.sh` if you need those.
328+
329+
Single path on macOS — there is no OS-trust fallback because macOS-specific `KeychainStore` is broken per [JDK-8321045](https://bugs.openjdk.org/browse/JDK-8321045). The script:
330+
331+
1. Builds a per-user JKS truststore at `~/Library/Application Support/JFrog/package-route-jvm/truststore.jks` containing only the customer CA.
332+
2. Writes a per-user LaunchAgent plist at `~/Library/LaunchAgents/com.jfrog.package-reroute.jto-env.plist` that calls `launchctl setenv JAVA_TOOL_OPTIONS=…` at `RunAtLoad`.
333+
3. Bootstraps the agent into `gui/<uid>` via `launchctl bootstrap` so the env var becomes available to every subsequently-launched GUI process (Dock-launched IntelliJ, JetBrains Toolbox, `open -a …`).
334+
335+
The `~/.zshrc` / `~/.bash_profile` shortcut is deliberately NOT used: it silently fails for Dock-launched IDE builds because GUI apps don't read the shell's interactive init. The LaunchAgent is the only recipe verified to reach Dock-launched and `open`-launched GUI applications, which inherit `JAVA_TOOL_OPTIONS` from the launchd `gui/<uid>` domain. Terminal sessions inherit transitively because Terminal.app itself is launchd-spawned.
336+
337+
### Requirements
338+
339+
- **macOS**.
340+
- **Root** (`sudo`) — needed to chown per-user files and to bootstrap into other users' `gui/<uid>` domains under `--all-users`.
341+
- **`openssl`** on `PATH` (ships with macOS as LibreSSL; full OpenSSL via Homebrew also works).
342+
- **`keytool`** on `PATH` (provided by any JDK — Homebrew openjdk, Adoptium Temurin, JetBrains JBR, etc.).
343+
- macOS built-ins used by the installer (all preinstalled on supported macOS versions): `plutil` (LaunchAgent plist validation), `launchctl` (bootstrap into `gui/<uid>`), `dscl` (user / home lookup), `stat` (UID-based filtering under `--all-users`).
344+
345+
### Options
346+
347+
| Option | Required | Description |
348+
|--------|----------|-------------|
349+
| `--use-cert <path>` | **Yes** | Path to an existing PEM/CRT certificate file. Validation: parseable X.509, not expired, `CA:TRUE` in basicConstraints. Bundles emit a warning (only the first cert imports). |
350+
| `--cert-name <name>` | No (default: `package-route-custom-ca`) | Base name for the JKS alias. Must match `[A-Za-z0-9._-]+`. Pass the same value to the validator (the validator matches by subject so this is informational unless multiple CAs coexist). |
351+
| `--all-users` | No | Iterate `/Users/*` and install the LaunchAgent + JKS for every account with UID ≥ 501. Default = only `SUDO_USER` (or the GUI console user under JAMF). |
352+
| `-h`, `--help` || Usage. |
353+
354+
### Examples
355+
356+
```bash
357+
# Single user (typical: install for the developer running sudo)
358+
sudo ./install_certs_jvm_macos.sh --use-cert /tmp/ZscalerRoot0.pem
359+
360+
# Fleet onboarding (shared Mac with multiple accounts)
361+
sudo ./install_certs_jvm_macos.sh --use-cert /tmp/ZscalerRoot0.pem --all-users
362+
363+
# Custom alias for the JKS
364+
sudo ./install_certs_jvm_macos.sh --use-cert /tmp/ZscalerRoot0.pem --cert-name zscaler-root
365+
```
366+
367+
### Validation: validate_certs_jvm_macos.sh
368+
369+
**`--expected-subject` is required.** Per user, asserts:
370+
- JKS file exists at the per-user path.
371+
- `keytool -list -v` shows an `Owner:` line matching the substring (case-insensitive).
372+
- LaunchAgent plist exists and passes `plutil -lint`.
373+
- `launchctl getenv JAVA_TOOL_OPTIONS` in `gui/<uid>` returns the JKS path (warn-not-fail when the user is not in an active GUI session — the plist will load at next login).
374+
375+
```bash
376+
./validate_certs_jvm_macos.sh --expected-subject "O=Zscaler"
377+
sudo ./validate_certs_jvm_macos.sh --expected-subject "O=Zscaler" --all-users
378+
```
379+
380+
`--all-users` requires root (other users' `~/Library` is `0700`). Exit code 0 if all checks pass, 1 otherwise.
381+
382+
### Caveats
383+
384+
- **Already-running apps must be restarted.** macOS does not re-poll the launchd domain env on Cmd-Tab. Quit and relaunch IntelliJ / your IDE after install for the env var to take effect.
385+
- **Gradle Daemon caching.** Run `gradle --stop` after onboarding so the daemon re-reads `JAVA_TOOL_OPTIONS` at next start.
386+
- **`Picked up JAVA_TOOL_OPTIONS:` banner.** Every JVM startup prints this to stderr. CI parsers that strict-match empty-stderr need to tolerate it.
387+
- **`changeit` truststore password.** OpenJDK convention; *not* a secret. The JKS holds only public CA certificates and the password protects file integrity, not contents.
388+
- **`gui/<uid>` domain only exists for logged-in GUI users.** Under `--all-users`, accounts that are not currently logged in get the plist installed but `launchctl bootstrap` is soft-skipped; launchd loads the plist automatically at their next login.
389+
- **JAMF / headless kiosk caveat.** On a Mac running JAMF policies *before* any user has logged in, `gui/<uid>` is not yet running, so `launchctl bootstrap gui/<uid>` either fails or loads into a non-running domain. The plist will load on first interactive login. For truly headless boxes (rack-mounted Mac mini build agents), pair this installer with a `/Library/LaunchDaemons` (system-scope) trust-bootstrap before the first user login, or run the installer interactively as part of provisioning.
390+
- **`KeychainStore` truststoreType is rejected.** Broken per JDK-8321045 — incomplete for `SystemRootCertificates.keychain`. No OS-trust fallback on macOS.
391+
- **`~/.zshrc` / `~/.bash_profile` are deliberately NOT touched.** They silently fail for Dock-launched IDE builds — see Overview.
392+
- **IntelliJ per-IDE SSL store** (`~/Library/Application Support/JetBrains/IntelliJIdea<ver>/ssl/cacerts`) is a different layer (plugin marketplace, VCS). Not configured by this script.
393+
394+
### Testing
395+
396+
`./testing/test_install_certs_jvm_macos.sh` runs the 9-invariant smoke matrix locally and on CI. Each run targets `SUDO_USER`'s per-user files and cleans up between cases via `trap EXIT`.
397+
398+
```bash
399+
# Local
400+
sudo ./testing/test_install_certs_jvm_macos.sh
401+
```
402+
403+
The same matrix runs on every push and pull request via `.github/workflows/ci.yml` (`test-macos-jvm` job).
404+
405+
### Summary (macOS JVM)
406+
407+
- **One run as root**, single cert source via `--use-cert`.
408+
- Per-user JKS at `~/Library/Application Support/JFrog/package-route-jvm/truststore.jks`.
409+
- Per-user LaunchAgent at `~/Library/LaunchAgents/com.jfrog.package-reroute.jto-env.plist` bootstrapped into `gui/<uid>`.
410+
- **Idempotent**, **re-runnable**, **JDK-version-agnostic**. New JDK installs do not require re-running the script.
411+
- Restart already-running apps; `gradle --stop` for the Gradle Daemon.
412+
413+
---
414+
320415
## Linux (Debian/Ubuntu): install_certs_debian_ubuntu.sh
321416

322417
### Overview
@@ -563,6 +658,7 @@ On **push** and **pull request** to `main` or `master`, GitHub Actions runs:
563658
| Job | Runner | Command |
564659
|-----|--------|---------|
565660
| Test (macOS) | `macos-latest` | `sudo ./testing/test_install_certs_macos.sh` |
661+
| Test (macOS JVM) | `macos-latest` | `sudo ./testing/test_install_certs_jvm_macos.sh` |
566662
| Test (Windows) | `windows-latest` | `./testing/test_install_certs_windows.ps1` (PowerShell) |
567663
| Test (Linux JVM) | `ubuntu-latest` | `./testing/test_install_certs_jvm_linux.sh` |
568664

_jvm_macos_paths.sh

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
# (c) JFrog Ltd. (2026)
2+
# Shared constants for install_certs_jvm_macos.sh and validate_certs_jvm_macos.sh.
3+
# Sourced — must NOT be executed directly. Has no shebang on purpose.
4+
#
5+
# Both scripts read this file via:
6+
# SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
7+
# . "${SCRIPT_DIR}/_jvm_macos_paths.sh"
8+
#
9+
# Keep installer and validator in lockstep by changing only this file.
10+
11+
# Default base name used as the JKS alias inside the per-user truststore.
12+
# Overridable via --cert-name on the installer (affects ONLY the alias name
13+
# visible in `keytool -list` output — the JKS file path, plist path, and
14+
# LaunchAgent label are all fixed per-user, so a different --cert-name on
15+
# re-run replaces the previous CA rather than installing alongside it).
16+
JVM_MACOS_DEFAULT_CERT_BASENAME="package-route-custom-ca"
17+
18+
# Per-user JKS truststore. macOS convention: per-user resources under the user's
19+
# Library directory so each account stays isolated. Matches the existing
20+
# install_certs_macos.sh pattern (per-user PEM under ~/<extract-path>/).
21+
JKS_RELATIVE_DIR="Library/Application Support/JFrog/package-route-jvm"
22+
JKS_BASENAME="truststore.jks"
23+
24+
# Per-user LaunchAgent that calls `launchctl setenv JAVA_TOOL_OPTIONS=…` at
25+
# RunAtLoad. This is the only macOS recipe that reaches Dock-launched IDEs;
26+
# the ~/.zshrc shortcut silently fails for GUI-spawned subprocesses.
27+
LAUNCH_AGENT_RELATIVE_DIR="Library/LaunchAgents"
28+
LAUNCH_AGENT_LABEL="com.jfrog.package-reroute.jto-env"
29+
LAUNCH_AGENT_BASENAME="${LAUNCH_AGENT_LABEL}.plist"
30+
31+
# OpenJDK convention for cacerts and similar truststores. NOT a secret in
32+
# this script's use case: we import only `trustedCertEntry` records (public
33+
# CA certs), so the password protects file *integrity* via the keystore MAC
34+
# but not any private key material. (A JKS that ever holds a PrivateKeyEntry
35+
# would additionally rely on this password to encrypt the key — not relevant
36+
# here.) Persisted in the LaunchAgent plist via -Djavax.net.ssl.trustStorePassword
37+
# so unattended JVMs can open the store.
38+
JKS_PASSWORD="changeit"

0 commit comments

Comments
 (0)