Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,17 @@ jobs:
- name: Run Windows tests
shell: pwsh
run: ./testing/test_install_certs_windows.ps1

test-linux-jvm:
name: Test (Linux JVM)
runs-on: ubuntu-latest
# No actions/setup-java here — unlike the macOS/Windows JVM jobs that
# exercise a single host JDK, this job runs a 4-distro Docker matrix
# (Ubuntu / Debian / RHEL / Amazon Linux) and installs a JDK *inside*
# each container. The host runner doesn't need a JDK; setting one up
# would only mask divergence in the per-distro keytool versions.
steps:
- uses: actions/checkout@v4

- name: Run Linux JVM smoke matrix
run: ./testing/test_install_certs_jvm_linux.sh
120 changes: 119 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,28 @@ Scripts to install a CA certificate, configure Node/npm and Python (pip, uv, Hug

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

## Quickstart — which script do I need?

| Your toolchain | Your OS | Go to |
|---|---|---|
| **Maven / Gradle / sbt / Ivy** (JVM) | Linux | [Linux (JVM)](#linux-jvm-install_certs_jvm_linuxsh) |
| **Node / npm or Python (pip / uv / Hugging Face)** | macOS | [macOS](#macos-install_certs_macossh) |
| **Node / npm or Python** | Linux (Debian / Ubuntu) | [Linux (Debian/Ubuntu)](#linux-debianubuntu-install_certs_debian_ubuntush) |
| **Node / npm or Python** | Windows | [Windows](#windows-install_certs_windowsps1) |

Reference: research wiki [Maven Support in package-reroute (DFLOW-136 / DFLOW-116)](https://jfrog-int.atlassian.net/wiki/spaces/RTFACT/pages/2440101931/).

## Script index

| Script | Platform | Purpose |
|--------|----------|---------|
| **install_certs_macos.sh** | macOS | Install cert, set env vars (Node/Python), and clear Docker Hub credentials |
| **validate_install_macos.sh** | macOS | Validate PEM and env config |
| **install_certs_debian_ubuntu.sh** | Debian/Ubuntu | Install cert into system trust + profile.d + user shell rc + Docker cleanup |
| **validate_certs_debian_ubuntu.sh** | Debian/Ubuntu | Validate PEM and env config |
| **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` |
| **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) |
| **_jvm_linux_paths.sh** | Linux (JVM) | Shared constants dot-sourced by installer + validator. Not directly executable. |
| **install_certs_windows.ps1** | Windows | Install cert, set env vars (Node/Python), and clear Docker Hub credentials |
| **validate_install_windows.ps1** | Windows | Validate PEM and env config |

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

### Testing

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

**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.

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

---

## Linux (JVM): install_certs_jvm_linux.sh

### Overview

`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.

The script auto-detects between two lab-verified paths:

- **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.**
- **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.

Auto-detection logic (`detect_mode` in the script):

1. If `update-ca-trust` is not on `PATH` → **Path B**.
2. If `/etc/pki/ca-trust/extracted/java/cacerts` does not exist → **Path B**.
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).
4. If the resolved `java`'s `lib/security/cacerts` symlinks to the RHEL system store → **Path A**; otherwise **Path B**.

`--mode java-tool-options` or `--mode update-ca-trust` overrides the detection.

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.

### Requirements

- **Linux** (Debian/Ubuntu family OR RHEL/Fedora/CentOS/Amazon-Linux family).
- **Root** (`sudo`).
- **`openssl`** on `PATH`.
- **`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.

### Options

| Option | Required | Description |
|--------|----------|-------------|
| `--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). |
| `--mode auto\|java-tool-options\|update-ca-trust` | No (default: **auto**) | Override path detection. |
| `--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. |
| `-h`, `--help` | — | Usage. |

### Examples

```bash
# Auto-detect; works on both RHEL family and Debian/Ubuntu
sudo ./install_certs_jvm_linux.sh --use-cert /tmp/ZscalerRoot0.pem

# Force JAVA_TOOL_OPTIONS path even on a RHEL host
sudo ./install_certs_jvm_linux.sh --use-cert /tmp/ZscalerRoot0.pem --mode java-tool-options

# Custom basename for the anchor file / JKS alias
sudo ./install_certs_jvm_linux.sh --use-cert /tmp/ZscalerRoot0.pem --cert-name zscaler-root
```

### Validation: validate_certs_jvm_linux.sh

**`--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.

```bash
./validate_certs_jvm_linux.sh --expected-subject "O=Zscaler"
sudo ./validate_certs_jvm_linux.sh --expected-subject "O=Zscaler" --all-users
./validate_certs_jvm_linux.sh --expected-subject "O=Zscaler" --cert-name zscaler-root
```

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).

### Caveats

- **`/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.
- **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.
- **`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.
- **`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.
- **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.
- **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.
- **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.
- **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`.
- **`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`.
- **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.
- **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.
- **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.

### Testing

`./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.

```bash
# From repo root (any host with Docker)
./testing/test_install_certs_jvm_linux.sh
```

Exit code 0 if all containers pass, 1 if any test fails.

The same matrix runs on every push and pull request via `.github/workflows/ci.yml` (`test-linux-jvm` job).

### Summary (Linux JVM)

- **One run as root**, single cert source via `--use-cert`.
- **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.
- **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`.
- **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.
- Users must open a new login shell (or `source /etc/environment`) for env changes to take effect. `gradle --stop` to refresh the Gradle Daemon.

---

## Windows: install_certs_windows.ps1

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

There is no CI job for the Debian/Ubuntu scripts in this workflow.
55 changes: 55 additions & 0 deletions _jvm_linux_paths.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# (c) JFrog Ltd. (2026)
# Shared constants for install_certs_jvm_linux.sh and validate_certs_jvm_linux.sh.
# Sourced — must NOT be executed directly. Has no shebang on purpose.
#
# Both scripts read this file via:
# SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# . "${SCRIPT_DIR}/_jvm_linux_paths.sh"
#
# Keep installer and validator in lockstep by changing only this file.
#
# Cross-platform siblings (keep CLI shapes and contracts in sync):
# _jvm_macos_paths.sh — per-user JKS under ~/Library
# _jvm_windows_paths.ps1 — per-user JKS under %LOCALAPPDATA%

# Default base name for the installed CA file (Path A) and the JKS alias (Path B).
# Overridable via --cert-name on the installer; the validator must be invoked with
# the same --cert-name when a non-default value was used.
#
# CROSS-PLATFORM NOTE: Linux is the only sibling where --cert-name affects
# a filesystem-visible path (Path A: /etc/pki/ca-trust/source/anchors/${CERT_BASENAME}.crt).
# macOS and Windows treat --cert-name as a JKS-alias-only cosmetic. A fleet
# script that wraps all three installers with the same --cert-name must
# remember to also pass --cert-name to the LINUX validator.
JVM_LINUX_DEFAULT_CERT_BASENAME="package-route-custom-ca"

# Path A — RHEL family with Red Hat OpenJDK. The JDK symlinks its
# lib/security/cacerts to RHEL_JAVA_CACERTS, so system trust IS Java trust.
RHEL_ANCHOR_DIR="/etc/pki/ca-trust/source/anchors"
RHEL_JAVA_CACERTS="/etc/pki/ca-trust/extracted/java/cacerts"

# Path B — JKS truststore + JAVA_TOOL_OPTIONS. Used on Debian/Ubuntu, Amazon
# Corretto, Eclipse Temurin, SDKMAN-installed JDKs, and manual .tar.gz installs.
#
# Note: Linux uses /etc/ssl/package-route-jvm rather than nesting under
# /etc/ssl/JFrog/package-route-jvm (as macOS/Windows do under their per-user
# trees). The flat path matches /etc/ssl conventions; if a future JFrog tool
# needs a sibling dir, it can carve out /etc/ssl/jfrog-<tool>/ alongside.
JKS_DIR="/etc/ssl/package-route-jvm"
JKS_PATH="${JKS_DIR}/truststore.jks"

# OpenJDK convention for cacerts and similar truststores. NOT a secret in
# this script's use case: we import only `trustedCertEntry` records (public
# CA certs), so the password protects file *integrity* via the keystore MAC
# but not any private key material. A JKS that ever holds a PrivateKeyEntry
# would additionally rely on this password to encrypt the key — not relevant
# here. Persisted in /etc/environment via -Djavax.net.ssl.trustStorePassword
# so unattended JVMs can open the store.
JKS_PASSWORD="changeit"

ENVIRONMENT_FILE="/etc/environment"

# 30-day expiry warn threshold (in seconds) — used by validate_pem. Must stay
# in lockstep with the macOS bash sibling (-checkend 2592000) and the Windows
# .NET sibling (AddDays(30)). Do not change without updating all three.
JVM_LINUX_EXPIRY_WARN_SECONDS=2592000
Loading
Loading