Export-CertBundle.ps1

<#PSScriptInfo
 
.VERSION 1.0
 
.GUID e8a0a1b3-a9a0-4c75-8fd9-689295e01453
 
.AUTHOR david.ramos@hotmail.com
 
.TAGS Certificate PEM TLS SSL Netskope Proxy WSL Docker TrustStore CorporateNetwork DevTools HTTPS
 
.PROJECTURI https://github.com/MrDRamos/PsUtils/blob/master/Internet/Export-CertBundle.ps1
#>



<#
.SYNOPSIS
Creates one PEM CA bundle so apps can trust HTTPS in Netskope-intercepted networks.
 
.DESCRIPTION
In many enterprise environments, Netskope inspects HTTPS traffic by decrypting and re-signing
connections with an enterprise certificate. Windows usually trusts that certificate because it
is in the OS certificate store.
 
The problem: many developer tools (Python, Node.js, curl, Git, containerized apps) do not
always use the Windows certificate store directly. They often expect a PEM certificate bundle.
That mismatch causes TLS errors even when your browser works.
See: https://community.netskope.com/next-gen-swg-2/configuring-developer-tools-with-netskope-ssl-inspection-8493
 
This script exports trusted CA certificates from Windows stores and writes them into one PEM
bundle file (default: corp_certs.pem). You can point tools to this file to mitigate TLS trust
failures caused by interception.
 
.PARAMETER Path
Output path for the generated PEM bundle.
Defaults to ./corp_certs.pem (current working directory).
Supports local paths and UNC/fileshare paths (for example, \\server\share\corp_certs.pem).
 
.INPUTS
None. This script does not accept pipeline input.
 
.OUTPUTS
None. The script writes a PEM file to disk at the path provided.
 
.NOTES
WSL/Ubuntu note:
Even when running Linux tools in WSL, traffic still goes through your corporate network path.
If Netskope is intercepting HTTPS, WSL commands can fail with TLS trust errors unless Linux
tools are configured to trust the same CA chain.
 
Shell context note:
Unless otherwise noted, command examples use PowerShell on Windows.
 
Trust scope note:
The generated bundle includes all trusted root and intermediate CA certificates from the
current user and local machine Windows stores. Use it for enterprise developer tooling and
private container images, but avoid redistributing it as a public application bundle unless
you intend to trust that full certificate set.
 
7 common WSL break points:
- apt update / apt install
- curl and wget
- git clone/fetch over HTTPS
- pip install
- npm install / npx
- Maven/Gradle dependency downloads
- Docker builds started from WSL
 
Common error messages this helps with:
- SSL: CERTIFICATE_VERIFY_FAILED
- unable to get local issuer certificate
- self signed certificate in certificate chain
 
This script is a mitigation for trust issues caused by HTTPS interception. It does not disable
certificate verification.
 
.EXAMPLE
# 1) Generate the certificate bundle (default: ./corp_certs.pem)
./Export-CertBundle.ps1
 
# 2) Optional: generate it at a custom path
./Export-CertBundle.ps1 -Path C:\tmp\corp_certs.pem
 
.EXAMPLE
# Python (requests, pip)
# Mitigation: point Python/OpenSSL tools to the generated PEM bundle.
$env:SSL_CERT_FILE = "$PWD/corp_certs.pem"
python -c "import requests; print(requests.get('https://pypi.org').status_code)"
pip --cert "$PWD/corp_certs.pem" install requests
 
.EXAMPLE
# Node.js and npm
# Mitigation: add extra trusted CAs for Node runtime and npm registry calls.
$env:NODE_EXTRA_CA_CERTS = "$PWD/corp_certs.pem"
node -e "require('https').get('https://registry.npmjs.org', r => console.log(r.statusCode))"
npm config set cafile "$PWD/corp_certs.pem"
 
.EXAMPLE
# curl
# Mitigation: pass the bundle explicitly for TLS verification.
curl.exe --cacert "$PWD/corp_certs.pem" https://example.com
 
.EXAMPLE
# Git (clone/fetch over HTTPS)
# Mitigation: configure Git to trust the bundle.
git config --global http.sslCAInfo "$PWD/corp_certs.pem"
git ls-remote https://github.com/git/git.git
 
.EXAMPLE
# Docker build/runtime (Linux base images)
# Mitigation: copy the certificate and update container trust store.
# Assumption: corp_certs.pem is in the Docker build context (usually the current build directory).
# Dockerfile
# FROM ubuntu:24.04
# COPY corp_certs.pem /usr/local/share/ca-certificates/corp_certs.crt
# RUN apt-get update && apt-get install -y ca-certificates && update-ca-certificates
 
.EXAMPLE
# 1) LocalStack container: npm install fails with Netskope TLS errors
# Symptom examples: CERTIFICATE_VERIFY_FAILED, self signed certificate in certificate chain
# Mitigation: copy the generated bundle into the image, update OS trust, and point Node/npm to it.
# Assumption: corp_certs.pem is in the Docker build context when using COPY.
# Dockerfile
# FROM localstack/localstack:latest
# USER root
# COPY corp_certs.pem /usr/local/share/ca-certificates/corp_certs.crt
# RUN apt-get update && apt-get install -y ca-certificates && update-ca-certificates
# ENV NODE_EXTRA_CA_CERTS=/etc/ssl/certs/ca-certificates.crt
# ENV NPM_CONFIG_CAFILE=/etc/ssl/certs/ca-certificates.crt
# USER localstack
#
# 2) Example run commands if the file is mounted instead of baked into the image:
# 1) Start LocalStack with the certificate mounted.
# 2) Run a follow-up command in the running container to rebuild the CA bundle.
# This is required because mounting alone does not update /etc/ssl/certs/ca-certificates.crt.
# 3) Reliability note: for repeatable environments, baking the certificate into the image is
# usually more reliable than runtime updates.
# docker run -d --name localstack ^
# -v "$PWD/corp_certs.pem:/usr/local/share/ca-certificates/corp_certs.crt" ^
# -e NODE_EXTRA_CA_CERTS=/etc/ssl/certs/ca-certificates.crt ^
# -e NPM_CONFIG_CAFILE=/etc/ssl/certs/ca-certificates.crt ^
# localstack/localstack:latest
# docker exec -u root localstack sh -lc "update-ca-certificates"
 
.EXAMPLE
# Java tools (Maven/Gradle) workaround
# Java often expects a Java truststore, not a PEM bundle. A simple workaround on Windows is
# to use the Windows trust store directly:
$env:JAVA_TOOL_OPTIONS = "-Djavax.net.ssl.trustStoreType=Windows-ROOT"
mvn -v
 
.EXAMPLE
# WSL Ubuntu (Linux shell on Windows)
# Motivation: browsers may work while WSL CLI tools fail because they use Linux trust settings.
# Mitigation: install the generated bundle into Ubuntu's CA store, then validate.
# In WSL bash:
# sudo cp /mnt/c/tmp/corp_certs.pem /usr/local/share/ca-certificates/corp_certs.crt
# (Places certificate in the source directory)
#
# sudo update-ca-certificates
# (Compiles all certificates from /usr/local/share/ca-certificates/ into the master bundle at /etc/ssl/certs/ca-certificates.crt)
#
# curl https://example.com
# git ls-remote https://github.com/git/git.git
#
# Optional per-tool overrides (if an app still fails):
# Note: these point to /etc/ssl/certs/ca-certificates.crt (the compiled bundle), NOT the source directory.
# export SSL_CERT_FILE=/etc/ssl/certs/ca-certificates.crt
# export REQUESTS_CA_BUNDLE=/etc/ssl/certs/ca-certificates.crt
# export NODE_EXTRA_CA_CERTS=/etc/ssl/certs/ca-certificates.crt
# export NPM_CONFIG_CAFILE=/etc/ssl/certs/ca-certificates.crt
# export GIT_SSL_CAINFO=/etc/ssl/certs/ca-certificates.crt
# export CURL_CA_BUNDLE=/etc/ssl/certs/ca-certificates.crt
 
#>

[CmdletBinding()]
param (
    [Parameter()]
    [string] $Path = "./corp_certs.pem"
)

try
{
    $ParentDir = Split-Path -LiteralPath $Path -Parent
    if ([string]::IsNullOrWhiteSpace($ParentDir))
    {
        $ParentDir = "."
    }

    if (!(Test-Path -LiteralPath $ParentDir -PathType Container -ErrorAction Stop))
    {
        throw "The directory '$ParentDir' does not exist. Please provide a valid path."
    }

    $ResolvedParent = (Resolve-Path -LiteralPath $ParentDir -ErrorAction Stop).Path
    $Path = Join-Path -Path $ResolvedParent -ChildPath (Split-Path -LiteralPath $Path -Leaf)
}
catch
{
    Write-Error "Invalid output path '$Path'. $_"
    exit 1
}

try 
{
    $CertS = @(
        (Get-ChildItem Cert:\CurrentUser\Root -ErrorAction Stop)
        (Get-ChildItem Cert:\LocalMachine\Root -ErrorAction Stop)
        (Get-ChildItem Cert:\CurrentUser\CA -ErrorAction Stop)
        (Get-ChildItem Cert:\LocalMachine\CA -ErrorAction Stop)
    ) |
        Where-Object { $_.RawData -ne $null } |
        Sort-Object -Property Thumbprint -Unique
}
catch 
{
    Write-Error "Failed to read one or more Windows certificate stores. $_"
    exit 1
}

if (!$CertS) 
{
    Write-Error "No certificates with raw data were found in the selected Windows certificate stores."
    exit 1
}

try 
{
    $CertBundle = foreach ($Cert in $CertS) 
    {
        "-----BEGIN CERTIFICATE-----"
        [System.Convert]::ToBase64String($Cert.RawData, "InsertLineBreaks") -replace "`r`n", "`n"
        "-----END CERTIFICATE-----"
        ""
    }

    $CertBundle -join "`n" | Out-File -Encoding ascii $Path -NoNewline -ErrorAction Stop
    if ($VerbosePreference)    {
        Write-Verbose "Successfully wrote certificate bundle to: $Path"
    }
}
catch 
{
    Write-Error "Failed to write certificate bundle to: $Path`n$_"
    exit 1
}