24 Mar 2026 · 8 min read

Java Secure Dev: Step-by-Step Setup Guide

Companion to Stop Using localhost:8080 - Why Your Dev Environment Needs Production-Grade Network Security

This article provides the detailed implementation steps. For the why, design decisions, and broader context, see the companion article.

The companion article covered the rationale — Java libraries often include hidden network behaviour: Sentry telemetry, update checks, analytics reporting. You don’t see these calls locally, but production firewalls block them, and you end up debugging a failed deployment instead of writing features.

So, this article picks up where that one left off. I am going to walk you through the Java-specific setup — proxy configuration, Maven settings, domain whitelisting, and how to actually catch those silent network calls.

Before You Start

I am assuming you have the Docker Compose infrastructure from the companion article in place. After that setup, you should have:

  • Network-isolated containers (ingress-net, egress-net, internet)
  • Squid proxy on port 3128 with domain whitelisting
  • Caddy reverse proxy for HTTPS (optional but recommended)

Step 1: Java Proxy Configuration

The key thing for Java is that both the JDK and Maven need to respect the proxy settings. If you miss either, you will have calls leaking out without going through Squid.

1a. JAVA_OPTS Environment Variables

Set JAVA_OPTS in your docker-compose.yml:

services:
  app:
    environment:
      JAVA_OPTS: >-
        -Dhttp.proxyHost=egress
        -Dhttp.proxyPort=3128
        -Dhttps.proxyHost=egress
        -Dhttps.proxyPort=3128
        -Dhttp.nonProxyHosts=localhost,127.0.0.1,::1

This ensures all HTTP/HTTPS connections from the JDK go through Squid, while local requests (localhost, loopback) bypass the proxy.

1b. Maven Proxy Settings

Create app/mvnw-proxy-settings.xml:

<settings>
  <proxies>
    <proxy>
      <id>squid</id>
      <active>true</active>
      <protocol>http</protocol>
      <host>egress</host>
      <port>3128</port>
      <nonProxyHosts>localhost,127.0.0.1,172.17.0.0/16</nonProxyHosts>
    </proxy>
    <proxy>
      <id>squid-https</id>
      <active>true</active>
      <protocol>https</protocol>
      <host>egress</host>
      <port>3128</port>
      <nonProxyHosts>localhost,127.0.0.1,172.17.0.0/16</nonProxyHosts>
    </proxy>
  </proxies>
</settings>

Then tell Maven to use these settings:

mvn -s app/mvnw-proxy-settings.xml clean install

Or set it permanently in your Dockerfile:

RUN mkdir -p /home/dev/.m2 && \
    cp /app/mvnw-proxy-settings.xml /home/dev/.m2/settings.xml

Step 2: Domain Whitelisting for Java Ecosystem

Create egress/domain-lists.d/allowed-domains-java.txt:

# Maven Central and build tools
repo.maven.apache.org
maven.apache.org
search.maven.org
api.maven.apache.org
central.maven.org

# Gradle ecosystem
services.gradle.org
plugins.gradle.org
repo.gradle.org

# Spring framework
repo.spring.io
api.spring.io
spring.io

# Documentation
docs.oracle.com
docs.gradle.org
docs.spring.io
maven.apache.org/plugins

These domains are automatically loaded by Squid when you run the setup from the companion article. You can add more as you discover what your project needs.

Step 3: Certificate Revocation Lists (CRLs)

This one caught me off guard. When your app makes HTTPS calls through the proxy, Java validates certificates. In offline or restricted networks, CRL (Certificate Revocation List) checking can fail, and you end up with cryptic SSL errors.

Squid can cache CRLs to prevent these failures. Create egress/crl-lists.d/crl-cache.txt with URLs of common CRL endpoints:

http://crl.microsoft.com/pki/crl/products/MicRootCert.crl
http://ocsp.digicert.com
http://crl3.digicert.com/sha2-secure-web-server-ca.crl

Configure Squid to cache these (in egress/squid.conf):

# Cache CRLs for 1 day
refresh_pattern crl   1440    50%     1440

This prevents CRL validation timeouts during development. One less thing to debug.

Step 4: Spring Boot Telemetry Example

This is where it gets interesting. Here is what happens when you add a telemetry SDK to a Spring Boot app:

<!-- pom.xml -->
<dependency>
    <groupId>io.sentry</groupId>
    <artifactId>sentry-spring-boot-starter</artifactId>
    <version>7.10.0</version>
</dependency>

When your app starts with the proxy enabled, watch the logs:

docker-compose logs -f egress

You will see Sentry making calls to sentry.io. If that domain is not whitelisted, the call gets blocked, and Squid logs it:

1234567890.567  0 192.168.1.100 TCP_DENIED/403 1234 CONNECT sentry.io:443 ...

This is the moment you discover: “Our app phones home. Is that intentional?” And you discover it before it becomes an emergency in production.

So, what do you do when a call is blocked?

  1. Identify the blocked domain in Squid logs
  2. Research the library’s documentation — why does this library contact that domain?
  3. Make a decision: whitelist it, configure the SDK to disable telemetry, or use a different library
  4. Add the domain to the appropriate domain-lists.d/ file

Step 5: Finding Blocked URLs Programmatically

Watching logs manually gets tedious. So, I wrote a small script to automate the discovery.

Create scripts/find-blocked-urls.sh:

#!/usr/bin/env bash

# Extract all TCP_DENIED and TCP_REJECTED entries from Squid logs
docker-compose logs egress | grep -E "TCP_DENIED|TCP_REJECTED" | \
  awk '{print $7}' | \
  sed 's/:.*$//' | \
  sort | uniq

echo ""
echo "Add these to egress/domain-lists.d/allowed-domains-*.txt"

Run it after your app starts up and tries to connect to the outside world:

bash scripts/find-blocked-urls.sh

Step 6: Testing and Verification

6a. Verify Proxy Configuration

Let’s make sure your Java application actually respects the proxy:

docker-compose exec app bash -c \
  'echo | openssl s_client -connect google.com:443 -servername google.com -proxy egress:3128 2>/dev/null | grep -i "^subject"'

This should work — Google is allowed by default. Now test a blocked domain:

docker-compose exec app bash -c \
  'echo | openssl s_client -connect example-blocked.com:443 -servername example-blocked.com -proxy egress:3128 2>&1 | grep -i "connection"'

This should fail with a proxy error. If it does, your setup is working.

6b. Maven Dependency Resolution

Next, verify Maven can download dependencies through the proxy:

docker-compose exec app mvn -s mvnw-proxy-settings.xml dependency:resolve

Watch Squid logs in another terminal:

docker-compose logs -f egress | grep "TCP_"

You will see all the requests to Maven Central flowing through.

6c. Spring Boot Startup Verification

For Spring Boot apps, watch the startup:

docker-compose logs -f app

And simultaneously watch Squid egress filtering:

docker-compose logs -f egress | grep "TCP_"

Any telemetry or unexpected external calls will show up immediately.

Step 7: Project Structure

Here is how I organise the project:

.
├── docker-compose.yml              # Main orchestration
├── .env.example                    # Environment template
├── scripts/
│   └── find-blocked-urls.sh        # Find blocked domains automatically
├── app/
│   ├── Dockerfile                  # Debian/Alpine + JDK + Maven
│   ├── mvnw-proxy-settings.xml     # Maven proxy config
│   ├── entrypoint.sh               # Sets JAVA_OPTS
│   └── src/                        # Your application code
├── egress/
│   ├── Dockerfile                  # Alpine + Squid
│   ├── squid.conf                  # Squid configuration
│   ├── entrypoint.sh               # Consolidates domain lists
│   ├── tester.sh                   # Proxy health check
│   ├── domain-lists.d/
│   │   ├── allowed-domains-java.txt    # Java ecosystem
│   │   ├── allowed-domains-git.txt     # Version control
│   │   └── allowed-domains-dev.txt     # Dev tools
│   └── crl-lists.d/
│       └── crl-cache.txt           # Certificate revocation lists
└── ingress/
    └── Caddyfile                   # HTTPS + security headers

From Development to Production

The reason I invest this effort in the dev setup is that the patterns are identical in production.

In production (Kubernetes):

  • App pod → Service mesh sidecar (Envoy) → External egress
  • Ingress controller (HTTPS + headers) → Pod HTTP traffic
  • NetworkPolicies restrict pod-to-pod communication

In development (Docker Compose):

  • App container → Squid proxy → External egress
  • Caddy reverse proxy (HTTPS + headers) → App HTTP traffic
  • Docker networks restrict container communication

The mental model is identical. When your team practises this in development, they understand why production has a proxy, they know what their dependencies actually do, and security violations are caught locally — not in production.

If you later move to Kubernetes, you are not learning a new paradigm. You are scaling patterns you already practise.

Complete Working Example

All the configurations shown in this article and the companion are available in a ready-to-use template on GitLab: java-secure-dev-env

It includes the complete docker-compose.yml, production-grade Squid configuration with inotify-based reloading, domain lists organised by purpose, CRL caching, a tester script, VS Code Dev Container integration, and a working Spring Boot example with Sentry telemetry.

To use it in your project:

# Clone the template
git clone https://gitlab.com/mandraketech/java-secure-dev-env.git --depth 1 template

# Copy into your project
cp -r template/{app,egress,ingress,docker-compose.yml,.env.example,.devcontainer.json} your-project/

# Configure
cp .env.example .env
# Edit .env with your project name, JDK version, etc.

# Start
docker-compose up -d

Watch the logs to see your application’s network behaviour:

docker-compose logs -f egress | grep "TCP_"

The first time something unexpected gets blocked, you will understand why this was worth the effort. 🙂


See Also


Credits

The infrastructure code was written and tested manually. This article draft was generated with AI assistance and then revised for voice and technical accuracy.