Scripts to install a CA certificate, configure Node/npm and Python (pip, uv, Hugging Face Hub, and related TLS clients), and clear Docker Hub credentials that can break redirected Docker Hub pulls.
This document describes the certificate installation and validation scripts for macOS, Linux (Debian/Ubuntu), and Windows.
| 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_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 |
Environment variables by platform (see each section for details):
| Variable | Typical use | Notes |
|---|---|---|
NODE_USE_SYSTEM_CA=1 |
Node/npm | macOS, Debian, Windows when npm is configured |
NODE_EXTRA_CA_CERTS=<path> |
Node/npm | PEM path (bundle allowed) |
UV_NATIVE_TLS=true / 1 |
Python uv | macOS uses true; Windows uses 1; not set by the Debian/Ubuntu script |
UV_SYSTEM_CERTS=true |
Python uv | Set by macOS for python, huggingface, or all |
REQUESTS_CA_BUNDLE=<path> |
Python requests / many HTTPS stacks | PEM or bundle path |
SSL_CERT_FILE=<path> |
OpenSSL-backed tools | Set on Debian/Ubuntu for python, huggingface, or all to the system CA bundle |
HF_HUB_DISABLE_XET=1 |
Python huggingface_hub | Set when huggingface or all: disables XET (not supported with typical MITM / Artifactory redirect flows) |
HF_HUB_ETAG_TIMEOUT=86400 |
Python huggingface_hub | Set when huggingface or all: ETag check timeout (seconds); reduces spurious failures on slow paths |
HF_HUB_DOWNLOAD_TIMEOUT=86400 |
Python huggingface_hub | Set when huggingface or all: download timeout (seconds) |
Each install script includes a best-effort cleanup for Docker Hub credentials. This fixes environments where corporate egress redirects registry-1.docker.io to JFrog Artifactory: locally stored Docker Hub credentials can otherwise be sent to JFrog's token endpoint and produce Bad Credentials.
The cleanup attempts docker logout for these Docker Hub key forms:
https://index.docker.io/v1/
index.docker.io
docker.io
https://registry-1.docker.io/
registry-1.docker.io
If Docker CLI is unavailable, the scripts skip this step. The cleanup is idempotent, logs warnings on logout failures, and never aborts the certificate install.
The cleanup runs in the user context where possible because Docker credentials and credential stores are user-scoped:
- macOS: uses
SUDO_USERwhen present; under JAMF/root execution, falls back to the console user from/dev/consoleand runs throughlaunchctl asuser. - Debian/Ubuntu: uses
SUDO_USER, thenlogname, then a best-effort activeloginctlsession fallback. - Windows: runs for the current PowerShell user. If the script runs as
SYSTEM, Docker cleanup is skipped with a warning because the user's Docker credential store is not accessible from that context.
Known limitation: Docker Desktop GUI sign-in can recreate CLI credentials later. If a user signed in through Docker Desktop, sign out in the Docker Desktop UI as well.
install_certs_macos.sh configures Node/npm and/or Python on macOS to use a custom CA certificate (e.g. for corporate proxy or package routing). It:
- Runs only as root (e.g.
sudo). - Either exports a full PEM bundle from macOS system Keychains, or uses an existing PEM file you provide.
- For each user in
/Users/*, writes or updates the certificate file and sets environment variables in that user’s~/.zshrcso Node and Python use the certificate. - Clears Docker Hub credentials for the target non-root user, when Docker is installed.
With Keychain export, each user gets package-route.pem under ~/<extract-path>/. The bundle includes Apple's system roots and enterprise CAs from /Library/Keychains/System.keychain. With --use-cert, the install uses your PEM path as-is for every user.
- macOS (script uses
securityfor Keychain export when--extract-pathis used). - Root (script exits with an error and suggests
sudoif not root). - openssl on
PATH. Optional: use--install-dependenciesto install it via Homebrew in the same run if missing. - When using --extract-path: the security (Keychain) tool must be available (system tool;
/usr/binis prepended toPATHby the script).
sudo ./install_certs_macos.sh [OPTIONS]| Option | Required | Description |
|---|---|---|
--package <npm|python|huggingface|all> |
No (default: all) | npm (Node only), python (Python TLS: uv, requests—no Hugging Face Hub vars), huggingface (Python TLS + HF_HUB_*), all (npm + python + Hugging Face Hub). |
--extract-path <path> |
Yes* | Directory under each user’s home where package-route.pem is written: ~/<path with leading / stripped>/package-route.pem (e.g. opt/certs → ~/opt/certs/..., certs → ~/certs/...). |
--use-cert <path> |
Yes* | Use this existing PEM file instead of exporting from Keychain. Cannot be used with --extract-path. |
--install-dependencies |
No | If openssl is missing, install it via Homebrew and continue in the same run. |
-h, --help |
— | Print usage and exit. |
* You must use either --extract-path or --use-cert, not both and not neither.
1. Export Keychain bundle and configure npm + Python for all users
PEM is written under each user’s home (e.g. ~/opt/certs/package-route.pem for --extract-path /opt/certs) and each user’s .zshrc is updated:
sudo ./install_certs_macos.sh \
--package all \
--extract-path /opt/certs2. Use an existing PEM file (e.g. from IT)
No Keychain access; same PEM path is set for every user:
sudo ./install_certs_macos.sh \
--package all \
--use-cert /opt/certs/company-ca.pem3. Only configure Python TLS (UV_NATIVE_TLS, REQUESTS_CA_BUNDLE; no HF_HUB_*)
sudo ./install_certs_macos.sh \
--package python \
--extract-path certsWith a relative --extract-path, each user gets their own file, e.g. /Users/jane/certs/package-route.pem.
4. Python TLS + Hugging Face Hub (huggingface; same TLS vars plus HF_HUB_*)
sudo ./install_certs_macos.sh \
--package huggingface \
--extract-path certs5. Install openssl if missing, then run (single run)
sudo ./install_certs_macos.sh \
--install-dependencies \
--package all \
--extract-path /opt/certsvalidate_install_macos.sh checks that the certificate installation succeeded: PEM file(s) exist and are valid (via openssl x509). --expected-subject is required for every invocation. It does not require root unless you use --all-users.
| Option | Description |
|---|---|
--expected-subject <pattern> |
Required. At least one cert in each PEM file (bundle) must have a subject matching <pattern> (case-insensitive). |
| (default scope) | Read NODE_EXTRA_CA_CERTS and REQUESTS_CA_BUNDLE from the current user’s ~/.zshrc (with ~ expanded), then validate each referenced PEM file. UV_NATIVE_TLS is not validated. |
--all-users |
(Root only.) For each user in /Users/*, read their ~/.zshrc, resolve cert paths, and validate each PEM. Use: sudo ./validate_install_macos.sh --expected-subject <pattern> --all-users. |
Requirements: openssl on PATH (same paths as the install script are prepended).
Exit code: 0 if all checks pass, 1 if any check fails.
# After install: validate current user’s config and cert path(s)
./validate_install_macos.sh --expected-subject Zscaler
# Validate every user’s config (run as root)
sudo ./validate_install_macos.sh --expected-subject Zscaler --all-usersTests live in testing/. Automated tests cover macOS and Windows only (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.
Requirements: openssl on PATH (for generating a temporary cert in tests).
# From repo root
./testing/test_install_certs_macos.sh
# Or from repo root with testing as current dir
cd testing && ./test_install_certs_macos.shExit code 0 if all tests pass, 1 otherwise.
| Area | Covered | Not covered |
|---|---|---|
| install_certs_macos.sh | CLI and pre-root: --help; unknown option; invalid --package; no cert source; --use-cert + --extract-path conflict; --use-cert with missing file; non-root exit and message. --use-cert: valid PEM path and --package npm/python/huggingface (non-root → run as root); invalid PEM content rejected with "Invalid or missing PEM" when run as root (tested when passwordless sudo available). |
Post-root: PATH/openssl, --install-dependencies (Homebrew); Keychain export; per-user loop; Docker credential cleanup; writing PEM and updating .zshrc. Requires root and/or Keychain; not run in CI. |
| validate_install_macos.sh | CLI: unknown option (exit 1); missing --expected-subject (exit 1). Main paths: default with mock HOME and .zshrc; missing PEM in .zshrc (exit 1); --all-users without root (exit 1). Covers validate_pem, get_export_path, validate_user_config. |
Multi-cert bundle in validate_pem; --all-users as root. |
Tests are black-box (exit codes and stderr).
test_install_certs_windows.ps1 runs automated tests for install_certs_windows.ps1 (CLI and parameter validation; -UseCert -Package python sets Python TLS only and leaves existing HF_HUB_* unchanged; -Package huggingface adds HF_HUB_*; -Package all sets npm + TLS + HF; when run as admin) and validate_install_windows.ps1 (-ExpectedSubject required, env-based validation: valid PEM, missing file, invalid PEM, subject match and no-match, and system-level env when run as admin). Run the test script as Administrator so install script tests and system-level validate tests execute; the script uses a temp directory and an embedded PEM.
Requirements: Windows with PowerShell. The install and validate scripts must be in the parent of testing/ (repo root).
# From repo root (PowerShell on Windows, as Administrator)
powershell -NoProfile -ExecutionPolicy Bypass -File testing/test_install_certs_windows.ps1From a non-Windows host you can run the tests on a Windows VM via SSH (e.g. copy the scripts and invoke the same command over ssh jump-windows).
Exit code 0 if all tests pass, 1 otherwise. Output shows pass/fail per test and a final count.
| Area | Covered |
|---|---|
| install_certs_windows.ps1 | When run as admin: script passes admin check (no "must run as Administrator" error). No cert source (parameter set error); invalid -Package; -CertName without -ExtractPath (and reverse); -UseCert and -CertName together; -UseCert with nonexistent file; -UseCert with invalid PEM; -UseCert with valid PEM (no "not a file" or "Invalid PEM" error). Packages: -UseCert -Package python sets TLS-only Machine vars and does not remove pre-existing HF_HUB_*; -Package huggingface adds HF_HUB_*; -Package all sets npm + TLS + HF. |
| validate_install_windows.ps1 | -ExpectedSubject required (exit 1 if missing); current user env (no paths → exit 0); env path to valid PEM (exit 0), missing file (exit 1), invalid PEM (exit 1); subject mismatch (exit 1, FAIL message); system-level (Machine) env when run as admin. |
Tests are black-box (exit codes and stdout/stderr). Paths are passed to the validate script via a temp file when invoking as a child process to avoid command-line parsing issues with backslashes.
- --package defaults to
allif omitted; must benpm,python,huggingface, orall(all = npm + Python TLS + Hugging Face Hub). - Cert source is one of:
- Keychain export:
--extract-pathset;--use-certmust not be set. - Use file:
--use-certset;--extract-pathmust not be set.
- Keychain export:
- Script exits with an error if:
- Both
--extract-pathand--use-certare used, or - Neither cert source is provided.
- Both
- Script must run as root; otherwise it prints an error and suggests
sudo $0 [options]. - Prepends
/usr/binand common Homebrew paths toPATHsoopensslandsecurityare found. - If
--install-dependenciesis set andopensslis not onPATH:- Tries Homebrew (
/opt/homebrew/bin/brewor/usr/local/bin/brew). - Runs
brew install openssl, then adds the newopenssltoPATHand continues in the same run.
- Tries Homebrew (
- If
opensslis still missing after that (or without the flag), script exits with an error. - If cert source is Keychain export (
--extract-path), script checks thatsecurityis available; if not, it exits (system tool, cannot be installed).
- --use-cert: Validates the file with
openssl x509 -nooutand uses it as the certificate for all users. No Keychain access. - --extract-path:
- Exports all trusted root CAs from
SystemRootCertificates.keychainandSystem.keychain. - Writes the full bundle to each user's
package-route.pem.
- Exports all trusted root CAs from
For each directory in /Users/* (skipping Shared and non-directories):
- Cert file path:
- If --use-cert: use that path for every user.
- If --extract-path: use
<homedir>/<extract-path with leading / stripped>/package-route.pem(e.g./opt/certs→~/opt/certs/package-route.pem,certs→~/certs/package-route.pem). Script creates the directory, writes the PEM, andchowns to that user.
- For each user, the script creates
~/.zshrcif needed, then callsadd_exports_to_filewith that file and the user’s cert path. If~/.zshrcis a directory, it skips that user with a warning.
After cert and shell config updates, the script runs Docker Hub credential cleanup as the target non-root user (SUDO_USER, or the console user under JAMF). It uses docker logout when Docker is available; otherwise it skips the cleanup.
For npm (if --package is npm or all):
- Ensure
NODE_USE_SYSTEM_CA=1. - Add or replace
NODE_EXTRA_CA_CERTSso it points at the selected cert path.
For Python TLS (if --package is python, huggingface, or all):
- Ensure
UV_NATIVE_TLS=trueandUV_SYSTEM_CERTS=true. - Add or replace
REQUESTS_CA_BUNDLEso it points at the selected cert path.
For Hugging Face Hub (if --package is huggingface or all):
- Ensure
HF_HUB_DISABLE_XET=1,HF_HUB_ETAG_TIMEOUT=86400,HF_HUB_DOWNLOAD_TIMEOUT=86400(same add/replace/leave-as-is behavior viaensure_export). Withpythononly, those lines are not added or updated; any existing exports in.zshrcare left unchanged (so user or prior-run values are not stripped).
- One run as root (optionally with
--install-dependenciesto install openssl). - One cert source: either Keychain export (
--extract-path) or existing file (--use-cert). - Per user: PEM at
~/<extract-path>/package-route.pemfor each user (leading/on--extract-pathis stripped); env vars in~/.zshrcpoint to that path. With--use-cert, the same PEM path is used for every user. - If user already had a different env path: script replaces it with the selected cert path.
- Docker: clears Docker Hub credentials for the target user if Docker is installed.
Users must open a new terminal (or source ~/.zshrc) for the new environment variables to take effect.
install_certs_debian_ubuntu.sh installs a PEM/CRT into the Debian/Ubuntu system trust store (update-ca-certificates), writes a managed file under /etc/profile.d/package-route-certs.sh, and updates the target non-root user’s shell rc (~/.zshrc or ~/.bashrc, depending on their login shell). It only supports an existing certificate file (--use-cert); there is no Keychain or cert-store extraction on Linux in this repo.
- npm:
NODE_USE_SYSTEM_CA=1andNODE_EXTRA_CA_CERTSpointing at the installed cert under/usr/local/share/ca-certificates/(default basenamepackage-route-custom-ca.crt, overridable with--cert-name). - Python TLS:
REQUESTS_CA_BUNDLEandSSL_CERT_FILEpoint at the system CA bundle (/etc/ssl/certs/ca-certificates.crt), which includes your CA afterupdate-ca-certificates.UV_NATIVE_TLSis not set (unlike macOS/Windows Python flows).HF_HUB_*are set only forhuggingfaceorall; withpythononly, existingHF_HUB_*lines in the user’s~/.bashrc/~/.zshrcare not removed. - Docker: best-effort Docker Hub credential cleanup runs for the target non-root user.
- Debian or Ubuntu (script checks
/etc/os-release). - Root (
sudo). opensslandupdate-ca-certificatesonPATH.- Optional: Docker CLI for Docker Hub credential-store cleanup. If Docker CLI is missing, the cleanup step is skipped.
| Option | Required | Description |
|---|---|---|
--use-cert <path> |
Yes | Path to an existing PEM/CRT file. |
--package npm|python|huggingface|all |
No (default: all) | What to configure. |
--cert-name <name> |
No (default: package-route-custom-ca) |
Base name for the file installed under /usr/local/share/ca-certificates/<name>.crt (not a Keychain/subject pattern). |
-h, --help |
— | Usage. |
sudo ./install_certs_debian_ubuntu.sh --use-cert /tmp/company-ca.pem
sudo ./install_certs_debian_ubuntu.sh --use-cert /tmp/company-ca.pem --package npm
sudo ./install_certs_debian_ubuntu.sh --use-cert /tmp/company-ca.pem --package huggingface
sudo ./install_certs_debian_ubuntu.sh --use-cert /tmp/company-ca.pem --cert-name my-org-ca--expected-subject is required. Checks PEM paths from the current user’s ~/.bashrc / ~/.zshrc and, when present, /etc/profile.d/package-route-certs.sh (NODE_EXTRA_CA_CERTS, REQUESTS_CA_BUNDLE, SSL_CERT_FILE). With --all-users (root only), validates /home/* users’ rc files.
./validate_certs_debian_ubuntu.sh --expected-subject "O=Example"
sudo ./validate_certs_debian_ubuntu.sh --all-users --expected-subject "O=Example"install_certs_windows.ps1 configures Node/npm and/or Python on Windows to use a custom CA certificate. It must be run as Administrator (or SYSTEM); the script exits with an error otherwise.
- Either extracts a certificate from the Windows cert store (LocalMachine\Root) by subject substring (
-CertName), or uses an existing PEM file you provide (-UseCert). If multiple certs match the pattern, the script logs a warning and picks one (prefers a subject containingRoot, otherwise the first match). - With -CertName and -ExtractPath: writes package-route.pem per user under each user’s profile and sets User-level env vars in the registry for each user. npm:
NODE_USE_SYSTEM_CA,NODE_EXTRA_CA_CERTS. Python TLS (python,huggingface, orall):UV_NATIVE_TLS,REQUESTS_CA_BUNDLE. Hugging Face Hub (huggingfaceorall):HF_HUB_DISABLE_XET,HF_HUB_ETAG_TIMEOUT,HF_HUB_DOWNLOAD_TIMEOUT. - With -UseCert: does not write a PEM file; sets Machine-level env vars. The script deletes overlapping User-level vars so they do not override Machine (User wins over Machine on Windows). Which vars are set or cleared depends on
-Package(see env table above). - Clears Docker Hub credentials for the current PowerShell user. When running as
SYSTEM, this cleanup is skipped with a warning because it must run in the user's Windows session.
Re-runs merge certs: if the target file already exists, the script saves its content, overwrites with the new cert, then appends other certs from the saved copy (dedupe by SHA-256 fingerprint). So running with a second cert adds it to the bundle instead of replacing it.
- Windows with PowerShell.
- Run as Administrator (or SYSTEM). The script checks and exits with an error if not elevated.
- When using -CertName: at least one certificate in LocalMachine\Root must match; if several match, the script warns and selects one (see Overview).
- Optional: Docker Desktop / Docker CLI for Docker Hub credential-store cleanup. If Docker CLI is missing, the cleanup step is skipped.
Run from a directory that contains the script (or use full path):
powershell -ExecutionPolicy Bypass -File install_certs_windows.ps1 -Package all -CertName Zscaler -ExtractPath certs\npm
# Or use an existing PEM:
powershell -ExecutionPolicy Bypass -File install_certs_windows.ps1 -Package all -UseCert C:\path\to\ca.pem| Parameter | Required | Description |
|---|---|---|
-Package |
No (default: all) | npm, python, huggingface, or all (all = npm + Python TLS + Hugging Face Hub). |
-CertName |
Yes* | Substring used to match cert Subject in the store (*CertName* wildcard). Requires -ExtractPath. Cannot be used with -UseCert. |
-ExtractPath |
Yes* | Directory under each user’s profile for package-route.pem (rooted paths are normalized to a folder under the profile, same idea as macOS). Requires -CertName. |
-UseCert |
Yes* | Path to an existing PEM file. Cannot be used with -CertName / -ExtractPath. |
* Use either (-CertName and -ExtractPath) or -UseCert.
Extract from store and configure all users (run as admin):
.\install_certs_windows.ps1 -Package all -CertName Zscaler -ExtractPath certs\npmUse an existing PEM (Machine-level env; User-level cert vars are deleted):
.\install_certs_windows.ps1 -Package all -UseCert C:\Users\Administrator\other-ca\company-ca.pemOnly npm:
.\install_certs_windows.ps1 -Package npm -CertName "Amazon Root CA 1" -ExtractPath certs\npm- Admin required. The script must be run as Administrator (or SYSTEM).
- Cert source: either store (
-CertName+-ExtractPath) or file (-UseCert). - Extract path: per-user package-route.pem and User-level env per
-Package(npm / Python TLS / Hugging Face as above); re-runs merge and dedupe by fingerprint. Machine-level cert vars are cleared so only User applies (avoids duplication if you previously used -UseCert). - UseCert: no PEM written; Machine-level env set per
-Package;pythondoes not setHF_HUB_*and does not clear pre-existingHF_HUB_*on the target scope. User-level cert vars are deleted so only Machine applies when using-UseCertas admin. - Docker: clears Docker Hub credentials for the current PowerShell user. Run the script in the user's session for this step;
SYSTEMcannot clean up the user's Docker credential store.
Users must start a new terminal for env changes to take effect.
validate_install_windows.ps1 checks that the certificate installation is valid: PEM file(s) exist and are valid (same validation as the install script). -ExpectedSubject is required for every invocation. It does not require admin unless you use -AllUsers.
| Parameter | Description |
|---|---|
-ExpectedSubject <pattern> |
Required. At least one cert in each PEM file (bundle) must have a subject matching <pattern> (case-insensitive). |
| (default scope) | Read NODE_EXTRA_CA_CERTS and REQUESTS_CA_BUNDLE from the current user's environment (User then Machine), then validate each referenced PEM file. |
-AllUsers |
(Admin only.) For each user in C:\Users\*, read their User registry env, resolve cert paths, and validate each PEM. |
Exit code: 0 if all checks passed, 1 if any check failed.
# After install: validate current user's env and cert path(s)
.\validate_install_windows.ps1 -ExpectedSubject Zscaler
# Validate every user's config (run as Administrator)
.\validate_install_windows.ps1 -ExpectedSubject Zscaler -AllUsersOn push and pull request to main or master, GitHub Actions runs:
| Job | Runner | Command |
|---|---|---|
| Test (macOS) | macos-latest |
sudo ./testing/test_install_certs_macos.sh |
| Test (Windows) | windows-latest |
./testing/test_install_certs_windows.ps1 (PowerShell) |
There is no CI job for the Debian/Ubuntu scripts in this workflow.