Summary
A GitLab CI pipeline utilizing the frontend-maven-plugin failed during the install-node-and-npm goal. While the build succeeds locally and within the same Docker container when run manually, the CI environment throws an SSL handshake error (Remote host terminated the handshake: SSL peer shut down incorrectly) specifically when attempting to download the Node.js binary from nodejs.org. The investigation reveals a misconfiguration of the SSL trust store, where the developer successfully configured Java to trust internal Artifactory certificates but failed to account for the fact that the plugin’s download mechanism may not be respecting those same Java system properties.
Root Cause
The failure stems from a split-brain trust configuration between the Java Virtual Machine (JVM) and the underlying process executing the download:
- Java-Centric Trust: The engineer explicitly passed
-Djavax.net.ssl.trustStoreto themvncommand. This tells the JVM to use a customcacertsfile containing the internal company certificates. - Plugin Execution Context: The
frontend-maven-pluginoften spawns external processes or uses specific HTTP libraries to fetch binaries (like Node.js) via standard network calls. - The SSL Mismatch: While Maven (the JVM process) can now talk to the internal Artifactory, the download of the Node.js tarball is being intercepted by an SSL-inspecting proxy/firewall within the GitLab CI network.
- The Handshake Failure: Because the external download process is not using the custom
cacerts-copied.jksprovided via the Maven flag, it does not trust the proxy’s certificate. The proxy terminates the connection, resulting in theSSL peer shut down incorrectlyerror.
Why This Happens in Real Systems
In enterprise environments, “it works on my machine” is a common symptom of Network Interception:
- Transparent Proxies: Most corporate CI runners sit behind a transparent proxy that performs Deep Packet Inspection (DPI). This requires the proxy to present its own certificate to the client.
- Environment Divergence: Local machines often have these root certificates pre-installed in the OS trust store, whereas minimalist Docker images used in CI do not.
- Tooling Fragmentation: A single build pipeline often orchestrates multiple runtimes (Java, Node, Python, Go). Configuring the trust store for the primary runtime (Java) does not automatically propagate that trust to secondary runtimes (Node/npm) or the shell processes they spawn.
Real-World Impact
- Pipeline Fragility: Builds that pass in local development environments fail mysteriously in the CI/CD pipeline, leading to developer frustration and wasted compute resources.
- Increased Lead Time: Engineers spend hours debugging “network issues” that are actually certificate management issues, delaying deployment cycles.
- Security Risks: Engineers may be tempted to bypass SSL checks (e.g., using
NODE_TLS_REJECT_UNAUTHORIZED=0) to “just make it work,” which introduces Man-in-the-Middle (MitM) vulnerabilities into the build process.
Example or Code (if necessary and relevant)
To fix this, you must ensure the environment itself trusts the certificate, not just the Java process. The most robust way is to update the OS-level trust store within the CI script:
# Instead of just modifying the Java cacerts, update the OS trust store
# This ensures all tools (curl, wget, node, npm) trust the corporate proxy
# For Debian/Ubuntu based images:
cp setup_ci/my-artifactory.crt /usr/local/share/ca-certificates/my-artifactory.crt
update-ca-certificates
# For Alpine based images:
cp setup_ci/my-artifactory.crt /usr/local/share/ca-certificates/my-artifactory.crt
update-ca-certificates
# Now run maven without needing to manually point to a JKS for everything
mvn -s setup_ci/settings.xml compile
How Senior Engineers Fix It
Senior engineers move away from application-level workarounds and toward infrastructure-level solutions:
- Immutable Base Images: Rather than running
keytoolinside the.gitlab-ci.ymlevery single time (which is slow and error-prone), they bake the corporate certificates into a custom Golden Docker Image. - Global Environment Configuration: They use environment variables like
NODE_EXTRA_CA_CERTSto ensure Node.js specifically knows which certificates to trust. - Centralized Artifact Management: Instead of downloading Node.js directly from
nodejs.org(which goes over the public internet), they configure thefrontend-maven-pluginto pull binaries from an internal Artifactory/Nexus mirror that is already within the trusted network perimeter.
Why Juniors Miss It
- Focusing on the Error Message: Juniors often see “SSL error” and assume it’s a problem with the destination (
nodejs.org) or a transient network glitch, rather than a local trust deficiency. - Tool-Specific Silos: They treat Java, Maven, and Node as independent islands. They solve the problem for the tool they see (Maven) but fail to realize that the tool is actually a wrapper for other processes that have their own security requirements.
- Relying on Command-Line Flags: They rely on passing
-Dproperties to the command line, which is a “surface-level” fix that doesn’t address the underlying system-level trust requirement.