Composer install fails in Docker: curl (35) OpenSSL SSL_connect SSL_ERROR_SYSCALL when fetching repo.openclassify.com/packages.json

Summary

The curl (35) OpenSSL SSL_ERROR_SYSCALL error occurs when the Docker container attempts an SSL handshake with repo.openclassify.com but the underlying TCP connection is reset or refused before the handshake completes. This is typically caused by TLS version mismatches, network path interference (proxy/firewall), or system time skew, rather than a certificate validation issue. Since the connection to bitbucket.org succeeds, the container’s network stack is functional, narrowing the cause to the specific interaction between the container’s OpenSSL version and the target server’s requirements or the network path in between.

Root Cause

The specific error SSL_ERROR_SYSCALL during the handshake phase indicates that the OpenSSL library encountered a system-level error (a non-positive return code from a system call like connect, read, or write) before the TLS protocol could be established.

  • TLS Version/Extension Mismatch: The private repository server likely requires TLS 1.3 with specific cipher suites or extensions (like ALPN), or conversely, it has deprecated TLS 1.0/1.1 entirely. Older base images (even php:8.2-fpm-bullseye) might negotiate an older TLS version that the server aggressively rejects, causing an immediate TCP reset (RST).
  • Network Path Interference: A corporate proxy, transparent firewall, or WSL2 network bridge might be interfering with the TLS handshake for this specific domain, dropping packets or resetting the connection.
  • System Time Skew: If the Docker container’s system time is significantly off (e.g., by years), the TLS “not before” certificate validation fails, often manifesting as a syscall error rather than a clear certificate error.
  • DNS Resolution with IPv6: The curl command might be attempting an IPv6 connection (-6) to a server that has broken IPv6 routing, causing the handshake to time out at the network layer. The user’s use of -4 was a correct diagnostic step.

Why This Happens in Real Systems

  • Container Isolation: Docker containers do not share the host’s network stack by default (unless using network_mode: host). This means they rely on the container’s internal /etc/ssl/certs and /etc/ssl/private, which may lack enterprise root CAs or have outdated TLS libraries compared to the host.
  • WSL2 Networking: WSL2 uses a virtualized network. Traffic to the internet is NATed. If the host (Windows) has a corporate firewall or proxy intercepting traffic, it might handle standard sites (Bitbucket) differently than obscure private repositories (OpenClassify), causing TLS stripping or inspection failures.
  • Private CA Requirements: Internal repositories often use self-signed certificates or private Certificate Authorities (CAs) that are not in the standard Linux trust store. While the error is SSL_ERROR_SYSCALL (connection failure) rather than certificate verify failed, the underlying SSL context might be failing to initialize properly due to trust store issues.

Real-World Impact

  • CI/CD Pipeline Failure: The build process halts completely because composer install cannot fetch dependencies. No vendor directory means the application cannot start.
  • Development Blockage: Developers using Docker Desktop (Windows/Mac) are blocked from running the project locally, while Linux users might not experience the issue if their host system certificates are mounted or if they use network_mode: host.
  • Security Risks: Disabling SSL verification (as a misguided workaround) exposes the application to man-in-the-middle attacks during the dependency fetch.

Example or Code

Diagnostic Curl Commands (Execute inside the container)

To isolate the issue, run these commands inside the running PHP container:

# 1. Check system time (critical for TLS)
date

# 2. Test connectivity with verbose TLS output and force IPv4
curl -v --ipv4 https://repo.openclassify.com/packages.json

# 3. Test with TLS 1.2 explicitly (to test version mismatch)
curl -v --tls-max 1.2 https://repo.openclassify.com/packages.json

# 4. Check the CA bundle path used by OpenSSL
openssl version -d

Dockerfile Configuration (The Fix)

Ensure the base image includes the necessary certificates and uses a recent OpenSSL version.

FROM php:8.2-fpm-bullseye

# Update certificate store and install necessary tools
RUN apt-get update && apt-get install -y \
    ca-certificates \
    openssl \
    && update-ca-certificates

# Ensure the time is synced (in Linux containers, this usually syncs with host, 
# but explicit NTP is good practice in some environments)
RUN apt-get install -y ntp

# Copy application code
WORKDIR /var/www/html

Composer Configuration (Workaround/Strictness)

If the issue is strictly TLS 1.3 requirement, forcing Composer to use specific SSL settings can help, though fixing the environment is better.

{
    "config": {
        "secure-http": false,
        "ssl": {
            "verify_peer": false,
            "verify_peer_name": false
        }
    }
}

Note: This is insecure and should only be used for local debugging.

How Senior Engineers Fix It

  1. Verify System Time: Run date inside the container. If it is off by more than a few minutes, mount the host’s time or install ntpdate. Time skew is the silent killer of TLS.
  2. Check OpenSSL Version: Ensure the container uses a base image with OpenSSL 1.1.1 or newer to support TLS 1.3. php:8.2-fpm-bullseye should be sufficient, but confirm with openssl version.
  3. Inspect Network Path:
    • If the network requires an HTTP proxy, set environment variables inside the container: HTTP_PROXY, HTTPS_PROXY, http_proxy, https_proxy.
    • Use docker run --rm -it --network host ... to test if the issue is specific to the Docker bridge network or a general network issue.
  4. Mount Host Certificates (WSL2 Specific): If the company uses a private CA, mount the Windows certificates into the container.
    # In docker-compose.yml
    volumes:
      - /etc/ssl/certs:/etc/ssl/certs:ro
      - /etc/ssl/private:/etc/ssl/private:ro
  5. Packet Capture: If all else fails, use tcpdump or Wireshark to capture the TLS handshake. Look for a RST packet immediately following the Client Hello. This confirms a firewall or server-side rejection.

Why Juniors Miss It

  • Treating all SSL Errors as Certificate Issues: Juniors often see “SSL” and immediately try to disable verification (--insecure, verify_peer: false). They fail to distinguish between certificate validation errors (provable via openssl s_client) and connection/handshake errors (like SSL_ERROR_SYSCALL).
  • Ignoring System Context: Developers often treat the container as a “black box” and forget that it has its own OS, time, and certificate store. They assume curl inside the container behaves exactly like on their host machine.
  • Misinterpreting SSL_ERROR_SYSCALL: This error is vague. It doesn’t mean “Certificate Invalid”; it means “The underlying BIO (Basic Input/Output) operation failed.” Juniors stop investigating after seeing “SSL” without checking the TCP connection (connectivity) or system calls (time/permissions).
  • Overlooking WSL2 Networking Nuances: Windows/WSL2 networking is complex. Juniors might not realize that localhost inside WSL2 is not always the same as localhost on Windows, and firewall rules on Windows can block Docker container traffic to specific external IPs.