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?
- Identify the blocked domain in Squid logs
- Research the library’s documentation — why does this library contact that domain?
- Make a decision: whitelist it, configure the SDK to disable telemetry, or use a different library
- 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
- Stop Using localhost:8080 - Why Your Dev Environment Needs Production-Grade Network Security — The companion article with the rationale and architecture
- Squid Proxy Documentation — Full reference for ACLs, caching, logging
- Caddy Reverse Proxy Guide — Security headers, TLS, logging
- Maven Proxy Configuration — More on settings.xml
- Java Network Properties — JDK proxy options
- Docker Network Isolation — Internal networks and security
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.