Skip to content

Commit 8703a4e

Browse files
akushskyclaude
andcommitted
DFLOW-150 - JVM client setup (Linux)
Adds a Linux 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_debian_ubuntu.sh if you need those flows. Based on the published research wiki (DFLOW-136): https://jfrog-int.atlassian.net/wiki/spaces/RTFACT/pages/2440101931 Two lab-verified paths, auto-detected: Path A — update-ca-trust RHEL/Fedora/CentOS/Amazon-Linux when a JDK whose lib/security/cacerts symlinks to /etc/pki/ca-trust/extracted/java/cacerts is on PATH (Red Hat OpenJDK). Drops the CA into /etc/pki/ca-trust/source/anchors/ and runs `update-ca-trust extract`. No env var. Path B — JKS + JAVA_TOOL_OPTIONS Everything else (Debian/Ubuntu, Amazon Corretto, Eclipse Temurin, SDKMAN, manual .tar.gz installs). Builds a JKS at /etc/ssl/package-route-jvm/truststore.jks containing only the customer CA; writes JAVA_TOOL_OPTIONS to /etc/environment plus the developer user's .bashrc/.zshrc. JDK-version-agnostic by construction. Detection rule (detect_mode): 1. No update-ca-trust on PATH -> Path B 2. /etc/pki/ca-trust/extracted/java/cacerts absent -> Path B 3. No `java` on PATH -> Path A (assumes Red Hat OpenJDK will follow via dnf; emits a loud end-of-run warning) 4. `java`'s cacerts symlinks to RHEL store -> Path A; else Path B --mode java-tool-options / --mode update-ca-trust overrides detection. Files: install_certs_jvm_linux.sh Installer (mirrors install_certs_debian_ubuntu.sh patterns). validate_certs_jvm_linux.sh Companion validator; subject-substring match. _jvm_linux_paths.sh Shared constants sourced by both scripts so they cannot drift. testing/test_install_certs_jvm_linux.sh Docker matrix runner (4 distros x 10 invariants). .github/workflows/ci.yml New test-linux-jvm job runs the matrix runner. README.md New "Linux (JVM)" section with the full design + caveats. Hardening fixes folded in from two rounds of pr-review-toolkit review: Critical: - --cert-name actually propagates to the Path B JKS alias and the validator accepts --cert-name so Path A's anchor file can be located when a non-default basename was used. - validate_pem rejects expired certs, leaf certs (CA:FALSE), warns on bundles (keytool -importcert -noprompt only imports the first cert). - install_via_update_ca_trust and validate_keystore_contains_subject capture keytool output explicitly; the previous pipefail+SIGPIPE pattern (... | grep -qi "$subject") silently turned real keytool failures into "subject not found" misdiagnoses AND real positive matches into false negatives. - validator detect_mode "both paths present" branch now routes the whole warn block to stderr; previously the warn lines leaked into the command-substitution capture and broke the case dispatch, yielding a silent exit-0 with NO checks run. Important: - validator FAILs (not OKs) when keytool is missing on the JKS path. - --all-users root check moved into parse_args (fast-fail exit 1). - update_user_shell_rc tracks RC_UPDATED and the final summary prints a loud WARNING when the per-user rc step was skipped. - --mode jto -> --mode java-tool-options for self-documenting CLI. - mktemp is -p $(dirname target) so `mv` is rename(2) (atomic), and an empty-awk-output check refuses to clobber /etc/environment. - --cert-name regex-validated to [A-Za-z0-9._-]+ to prevent path traversal in the anchor file path. - chown failure on the user's rc file captured + warned. - get_target_user verifies the picked username exists in /etc/passwd. Smoke matrix (testing/test_install_certs_jvm_linux.sh): Ubuntu 22.04, Debian 12, RHEL UBI 9, Amazon Linux 2023, in parallel. Per-container invariants: positive install+validate, subject mismatch -> exit 1, idempotent re-install (no duplicate lines/aliases), custom --cert-name round-trips, path-traversal rejected, malformed PEM rejected, expired CA rejected (skipped when openssl can't produce a verifiably-expired test cert), leaf cert (CA:FALSE) rejected, forced --mode update-ca-trust (Path A) on hosts with update-ca-trust, dual-artifact validator regression test. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent b9491bd commit 8703a4e

6 files changed

Lines changed: 1477 additions & 1 deletion

File tree

.github/workflows/ci.yml

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,3 +27,17 @@ jobs:
2727
- name: Run Windows tests
2828
shell: pwsh
2929
run: ./testing/test_install_certs_windows.ps1
30+
31+
test-linux-jvm:
32+
name: Test (Linux JVM)
33+
runs-on: ubuntu-latest
34+
# No actions/setup-java here — unlike the macOS/Windows JVM jobs that
35+
# exercise a single host JDK, this job runs a 4-distro Docker matrix
36+
# (Ubuntu / Debian / RHEL / Amazon Linux) and installs a JDK *inside*
37+
# each container. The host runner doesn't need a JDK; setting one up
38+
# would only mask divergence in the per-distro keytool versions.
39+
steps:
40+
- uses: actions/checkout@v4
41+
42+
- name: Run Linux JVM smoke matrix
43+
run: ./testing/test_install_certs_jvm_linux.sh

README.md

Lines changed: 119 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,28 @@ Scripts to install a CA certificate, configure Node/npm and Python (pip, uv, Hug
44

55
This document describes the certificate installation and validation scripts for **macOS**, **Linux (Debian/Ubuntu)**, and **Windows**.
66

7+
## Quickstart — which script do I need?
8+
9+
| Your toolchain | Your OS | Go to |
10+
|---|---|---|
11+
| **Maven / Gradle / sbt / Ivy** (JVM) | Linux | [Linux (JVM)](#linux-jvm-install_certs_jvm_linuxsh) |
12+
| **Node / npm or Python (pip / uv / Hugging Face)** | macOS | [macOS](#macos-install_certs_macossh) |
13+
| **Node / npm or Python** | Linux (Debian / Ubuntu) | [Linux (Debian/Ubuntu)](#linux-debianubuntu-install_certs_debian_ubuntush) |
14+
| **Node / npm or Python** | Windows | [Windows](#windows-install_certs_windowsps1) |
15+
16+
Reference: research wiki [Maven Support in package-reroute (DFLOW-136 / DFLOW-116)](https://jfrog-int.atlassian.net/wiki/spaces/RTFACT/pages/2440101931/).
17+
18+
## Script index
19+
720
| Script | Platform | Purpose |
821
|--------|----------|---------|
922
| **install_certs_macos.sh** | macOS | Install cert, set env vars (Node/Python), and clear Docker Hub credentials |
1023
| **validate_install_macos.sh** | macOS | Validate PEM and env config |
1124
| **install_certs_debian_ubuntu.sh** | Debian/Ubuntu | Install cert into system trust + profile.d + user shell rc + Docker cleanup |
1225
| **validate_certs_debian_ubuntu.sh** | Debian/Ubuntu | Validate PEM and env config |
26+
| **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` |
27+
| **validate_certs_jvm_linux.sh** | Linux (JVM) | Validate JVM truststore install (auto-detects Path A vs B; checks anchor file or JKS subject + `/etc/environment` + shell-rc) |
28+
| **_jvm_linux_paths.sh** | Linux (JVM) | Shared constants dot-sourced by installer + validator. Not directly executable. |
1329
| **install_certs_windows.ps1** | Windows | Install cert, set env vars (Node/Python), and clear Docker Hub credentials |
1430
| **validate_install_windows.ps1** | Windows | Validate PEM and env config |
1531

@@ -174,7 +190,7 @@ sudo ./validate_install_macos.sh --expected-subject Zscaler --all-users
174190

175191
### Testing
176192

177-
Tests live in **testing/**. Automated tests cover **macOS** and **Windows** only (not Debian/Ubuntu).
193+
Tests live in **testing/**. Automated tests cover **macOS**, **Windows**, and **Linux JVM** (not Debian/Ubuntu).
178194

179195
**test_install_certs_macos.sh** runs automated tests for **install_certs_macos.sh** (CLI and argument validation) and **validate_install_macos.sh** (validation with a temp PEM and mock home). No root required for the default test run.
180196

@@ -347,6 +363,107 @@ sudo ./validate_certs_debian_ubuntu.sh --all-users --expected-subject "O=Example
347363

348364
---
349365

366+
## Linux (JVM): install_certs_jvm_linux.sh
367+
368+
### Overview
369+
370+
`install_certs_jvm_linux.sh` wires a custom CA certificate into the JVM trust path 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_debian_ubuntu.sh` if you need the Node/Python flows or Docker Hub credential cleanup.
371+
372+
The script auto-detects between two lab-verified paths:
373+
374+
- **Path A — `update-ca-trust`** for RHEL/Fedora/CentOS/Amazon-Linux when a JDK whose `lib/security/cacerts` is symlinked to `/etc/pki/ca-trust/extracted/java/cacerts` is on `PATH` (Red Hat OpenJDK, and on some images Corretto). Drops the CA into `/etc/pki/ca-trust/source/anchors/` and runs `update-ca-trust extract`. **No env var is set.**
375+
- **Path B — JKS + `JAVA_TOOL_OPTIONS`** for everything else (Debian/Ubuntu, manual JDK installs that don't symlink to the system store, SDKMAN, snap-confined JDKs). Builds a JKS truststore at `/etc/ssl/package-route-jvm/truststore.jks` containing only the customer CA, then sets `JAVA_TOOL_OPTIONS` in `/etc/environment`. JDK-version-agnostic by construction — one env var serves all current and future JDKs.
376+
377+
Auto-detection logic (`detect_mode` in the script):
378+
379+
1. If `update-ca-trust` is not on `PATH`**Path B**.
380+
2. If `/etc/pki/ca-trust/extracted/java/cacerts` does not exist → **Path B**.
381+
3. If no `java` is on `PATH`**Path A** (assumes Red Hat OpenJDK will be installed via `dnf`; the installer prints a loud end-of-run warning instructing the user to re-run with `--mode java-tool-options` if they install Corretto/Temurin/SDKMAN instead).
382+
4. If the resolved `java`'s `lib/security/cacerts` symlinks to the RHEL system store → **Path A**; otherwise **Path B**.
383+
384+
`--mode java-tool-options` or `--mode update-ca-trust` overrides the detection.
385+
386+
Both scripts source a small shared file `_jvm_linux_paths.sh` for the constants block (CA basename default, anchor dir, JKS path, password, env file) so the installer and validator cannot drift.
387+
388+
### Requirements
389+
390+
- **Linux** (Debian/Ubuntu family OR RHEL/Fedora/CentOS/Amazon-Linux family).
391+
- **Root** (`sudo`).
392+
- **`openssl`** on `PATH`.
393+
- **`keytool`** on `PATH` (provided by any JDK) **for Path B**. On Path A the verification step uses `keytool` opportunistically but the install itself does not need it; the installer emits a warning if `keytool` is missing on Path A.
394+
395+
### Options
396+
397+
| Option | Required | Description |
398+
|--------|----------|-------------|
399+
| `--use-cert <path>` | **Yes** | Path to an existing PEM/CRT certificate file. Validated: must be a parseable X.509, not expired, with `CA:TRUE` in basicConstraints. Bundles emit a warning (only the first cert imports). |
400+
| `--mode auto\|java-tool-options\|update-ca-trust` | No (default: **auto**) | Override path detection. |
401+
| `--cert-name <name>` | No (default: `package-route-custom-ca`) | Base name applied to the Path A anchor file (`/etc/pki/ca-trust/source/anchors/<name>.crt`) AND the Path B JKS alias. Must match `[A-Za-z0-9._-]+`. Pass the same value to the validator. |
402+
| `-h`, `--help` || Usage. |
403+
404+
### Examples
405+
406+
```bash
407+
# Auto-detect; works on both RHEL family and Debian/Ubuntu
408+
sudo ./install_certs_jvm_linux.sh --use-cert /tmp/ZscalerRoot0.pem
409+
410+
# Force JAVA_TOOL_OPTIONS path even on a RHEL host
411+
sudo ./install_certs_jvm_linux.sh --use-cert /tmp/ZscalerRoot0.pem --mode java-tool-options
412+
413+
# Custom basename for the anchor file / JKS alias
414+
sudo ./install_certs_jvm_linux.sh --use-cert /tmp/ZscalerRoot0.pem --cert-name zscaler-root
415+
```
416+
417+
### Validation: validate_certs_jvm_linux.sh
418+
419+
**`--expected-subject` is required.** The validator auto-detects which path was used (by checking for the JKS file or the anchor file), then asserts the customer CA is present by case-insensitive subject substring match. `--all-users` iterates `/home/*` and is Path B-specific (requires root; checked at `parse_args` time, fails fast). Pass `--cert-name <name>` if the installer was invoked with a non-default value.
420+
421+
```bash
422+
./validate_certs_jvm_linux.sh --expected-subject "O=Zscaler"
423+
sudo ./validate_certs_jvm_linux.sh --expected-subject "O=Zscaler" --all-users
424+
./validate_certs_jvm_linux.sh --expected-subject "O=Zscaler" --cert-name zscaler-root
425+
```
426+
427+
Exit code 0 if all checks pass, 1 otherwise. Missing `JAVA_TOOL_OPTIONS` in a user rc file is a **warning** (not a failure) — `/etc/environment` is the authoritative source for system-wide config. Missing `keytool` while a keystore exists is a **failure** (cannot verify the core invariant).
428+
429+
### Caveats
430+
431+
- **`/etc/environment` activation** (Path B). GUI-launched apps (IntelliJ from the GNOME/KDE launcher) inherit `/etc/environment` via the session manager at login. Existing sessions need a logoff/login to pick up the new env var. The script updates the SUDO_USER's `.bashrc`/`.zshrc` so the *current* shell session has it without re-login.
432+
- **Gradle Daemon caching** (Path B). A Gradle Daemon started before the env var was set still uses its captured environment. Run `gradle --stop` after onboarding.
433+
- **`Picked up JAVA_TOOL_OPTIONS:` banner** (Path B). Every JVM prints this to stderr at startup. CI log parsers that strict-match empty-stderr need to tolerate it.
434+
- **`changeit` truststore password.** The JKS at `/etc/ssl/package-route-jvm/truststore.jks` uses the OpenJDK convention password `changeit`. This is **not** a secret — JKS truststores protect file integrity, not contents, and the trust anchor inside is a public CA certificate. The password is persisted in `/etc/environment` via the `-Djavax.net.ssl.trustStorePassword` flag so unattended JVMs can open the store.
435+
- **Path B truststore extends the JDK's bundled cacerts.** `-Djavax.net.ssl.trustStore=…` in OpenJDK *replaces* the JVM trust source — a JKS containing only the corporate CA would break every public-CA TLS handshake (Maven Central, Gradle plugin portal, Let's Encrypt-fronted mirrors). The installer therefore copies `$JAVA_HOME/lib/security/cacerts` to `/etc/ssl/package-route-jvm/truststore.jks` first, then `keytool -importcert` appends the corporate CA. The resulting store has ~150 public roots **plus** the corporate one. Path A is unaffected — `update-ca-trust extract` already builds the system-wide Java cacerts by merging system anchors with the JDK's defaults.
436+
- **Mixed-distro auto-detection.** On RHEL family with a non-Red-Hat JDK on PATH (Corretto, Temurin, SDKMAN), auto-detection lands on Path B because the JDK's `lib/security/cacerts` is not symlinked to the RHEL system store. On RHEL family with **no** JDK on PATH yet, auto-detection picks Path A on the assumption that Red Hat OpenJDK will follow via `dnf` — the installer emits a loud end-of-run warning naming this assumption.
437+
- **Snap-confined JDKs** are not configured by Path A (read-only squashfs). Auto-detection lands on Path B because the snap cacerts is not symlinked to the system one. Path B works because `JAVA_TOOL_OPTIONS` is read by the JVM at startup regardless of how the JDK was installed.
438+
- **Container-internal JDKs.** Maven/Gradle running inside Docker on a developer machine need the CA wired into the *container* image — host-side install does not propagate. Use a `RUN` step in the Dockerfile or pass `JAVA_TOOL_OPTIONS` via `docker run -e`.
439+
- **`MAVEN_OPTS` clobbering.** If your shell or `~/.mavenrc` sets `MAVEN_OPTS`, those args land AFTER `JAVA_TOOL_OPTIONS` and can override the trust store flags. If `mvn` fails TLS after install, check `env | grep -E '^(JAVA_TOOL_OPTIONS|MAVEN_OPTS)='` — if both are set, ensure `MAVEN_OPTS` does NOT also include `-Djavax.net.ssl.trustStore`.
440+
- **IntelliJ per-IDE SSL store.** `~/.config/JetBrains/<IDE>/ssl/cacerts` is a separate trust store used by the IDE for the plugin marketplace and VCS integration — NOT by Maven/Gradle runs spawned from IntelliJ (those use the JBR's truststore which the env var path covers). If `mvn` works in Terminal but IntelliJ Maven sync fails, add the CA via Settings → Tools → Server Certificates.
441+
- **Idempotent re-runs.** Path B re-creates the JKS each run (single alias guaranteed) and replaces (not appends) the env var line in `/etc/environment`. Path A re-copies the anchor and re-runs `update-ca-trust extract` (with fingerprint-compare to surface a deliberate replacement vs. an idempotent re-run). Running the script twice produces the same final state.
442+
- **Existing anchor file replacement (Path A).** A file at `/etc/pki/ca-trust/source/anchors/<cert-name>.crt` placed by other tooling is replaced if its SHA-256 fingerprint differs from `--use-cert`. The installer prints a `[warn] Replacing existing anchor` line with both fingerprints so the swap is auditable.
443+
444+
### Testing
445+
446+
`./testing/test_install_certs_jvm_linux.sh` runs the Docker smoke matrix across four distro × JDK combinations in parallel from any host with Docker. Each container builds a self-signed lab CA, runs the installer, runs the validator, then exercises negative subject, idempotency, custom `--cert-name`, path-traversal rejection, malformed-PEM rejection, expired-CA rejection, and leaf-cert rejection.
447+
448+
```bash
449+
# From repo root (any host with Docker)
450+
./testing/test_install_certs_jvm_linux.sh
451+
```
452+
453+
Exit code 0 if all containers pass, 1 if any test fails.
454+
455+
The same matrix runs on every push and pull request via `.github/workflows/ci.yml` (`test-linux-jvm` job).
456+
457+
### Summary (Linux JVM)
458+
459+
- **One run as root**, single cert source via `--use-cert`.
460+
- **Path A**: anchor in `/etc/pki/ca-trust/source/anchors/<cert-name>.crt`; `update-ca-trust extract` updates the system Java trust store. No env var.
461+
- **Path B**: JKS at `/etc/ssl/package-route-jvm/truststore.jks` with alias `<cert-name>`; `JAVA_TOOL_OPTIONS` in `/etc/environment` and in the developer user's `.bashrc`/`.zshrc`.
462+
- **Idempotent** (both paths), **re-runnable**, **JDK-version-agnostic** across currently-supported JDKs — JKS format is still read by JDK 8–25 (Path B); Path A piggy-backs on the OS trust store that Red Hat OpenJDK already symlinks. A future JDK that drops JKS support would require a Path B format bump.
463+
- Users must open a new login shell (or `source /etc/environment`) for env changes to take effect. `gradle --stop` to refresh the Gradle Daemon.
464+
465+
---
466+
350467
## Windows: install_certs_windows.ps1
351468

352469
### Overview
@@ -448,5 +565,6 @@ On **push** and **pull request** to `main` or `master`, GitHub Actions runs:
448565
|-----|--------|---------|
449566
| Test (macOS) | `macos-latest` | `sudo ./testing/test_install_certs_macos.sh` |
450567
| Test (Windows) | `windows-latest` | `./testing/test_install_certs_windows.ps1` (PowerShell) |
568+
| Test (Linux JVM) | `ubuntu-latest` | `./testing/test_install_certs_jvm_linux.sh` |
451569

452570
There is no CI job for the Debian/Ubuntu scripts in this workflow.

_jvm_linux_paths.sh

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
# (c) JFrog Ltd. (2026)
2+
# Shared constants for install_certs_jvm_linux.sh and validate_certs_jvm_linux.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_linux_paths.sh"
8+
#
9+
# Keep installer and validator in lockstep by changing only this file.
10+
#
11+
# Cross-platform siblings (keep CLI shapes and contracts in sync):
12+
# _jvm_macos_paths.sh — per-user JKS under ~/Library
13+
# _jvm_windows_paths.ps1 — per-user JKS under %LOCALAPPDATA%
14+
15+
# Default base name for the installed CA file (Path A) and the JKS alias (Path B).
16+
# Overridable via --cert-name on the installer; the validator must be invoked with
17+
# the same --cert-name when a non-default value was used.
18+
#
19+
# CROSS-PLATFORM NOTE: Linux is the only sibling where --cert-name affects
20+
# a filesystem-visible path (Path A: /etc/pki/ca-trust/source/anchors/${CERT_BASENAME}.crt).
21+
# macOS and Windows treat --cert-name as a JKS-alias-only cosmetic. A fleet
22+
# script that wraps all three installers with the same --cert-name must
23+
# remember to also pass --cert-name to the LINUX validator.
24+
JVM_LINUX_DEFAULT_CERT_BASENAME="package-route-custom-ca"
25+
26+
# Path A — RHEL family with Red Hat OpenJDK. The JDK symlinks its
27+
# lib/security/cacerts to RHEL_JAVA_CACERTS, so system trust IS Java trust.
28+
RHEL_ANCHOR_DIR="/etc/pki/ca-trust/source/anchors"
29+
RHEL_JAVA_CACERTS="/etc/pki/ca-trust/extracted/java/cacerts"
30+
31+
# Path B — JKS truststore + JAVA_TOOL_OPTIONS. Used on Debian/Ubuntu, Amazon
32+
# Corretto, Eclipse Temurin, SDKMAN-installed JDKs, and manual .tar.gz installs.
33+
#
34+
# Note: Linux uses /etc/ssl/package-route-jvm rather than nesting under
35+
# /etc/ssl/JFrog/package-route-jvm (as macOS/Windows do under their per-user
36+
# trees). The flat path matches /etc/ssl conventions; if a future JFrog tool
37+
# needs a sibling dir, it can carve out /etc/ssl/jfrog-<tool>/ alongside.
38+
JKS_DIR="/etc/ssl/package-route-jvm"
39+
JKS_PATH="${JKS_DIR}/truststore.jks"
40+
41+
# OpenJDK convention for cacerts and similar truststores. NOT a secret in
42+
# this script's use case: we import only `trustedCertEntry` records (public
43+
# CA certs), so the password protects file *integrity* via the keystore MAC
44+
# but not any private key material. A JKS that ever holds a PrivateKeyEntry
45+
# would additionally rely on this password to encrypt the key — not relevant
46+
# here. Persisted in /etc/environment via -Djavax.net.ssl.trustStorePassword
47+
# so unattended JVMs can open the store.
48+
JKS_PASSWORD="changeit"
49+
50+
ENVIRONMENT_FILE="/etc/environment"
51+
52+
# 30-day expiry warn threshold (in seconds) — used by validate_pem. Must stay
53+
# in lockstep with the macOS bash sibling (-checkend 2592000) and the Windows
54+
# .NET sibling (AddDays(30)). Do not change without updating all three.
55+
JVM_LINUX_EXPIRY_WARN_SECONDS=2592000

0 commit comments

Comments
 (0)